add tile key selection for addressables builds

This commit is contained in:
2026-01-23 16:07:24 +01:00
parent bd1e6f4f4d
commit 04ef2c68cc
16 changed files with 1383 additions and 185 deletions

View File

@@ -43,6 +43,24 @@ public class GeoTileImporter : EditorWindow
private bool deleteExistingEnhancedTrees = false;
private string enhancedTreesParentName = "Geo_Trees_Enhanced";
private Vector2 scrollPosition;
private float tileKeySizeX = 1000f;
private float tileKeySizeY = 1000f;
private float tileKeyOverlapX = 0.5f;
private float tileKeyOverlapY = 0.5f;
private string bottomLeftKey = "";
private string topRightKey = "";
private readonly List<TileRecord> tileIndexCache = new List<TileRecord>();
private readonly List<TileKeyOption> tileKeyOptions = new List<TileKeyOption>();
private readonly Dictionary<string, TileKeyOption> tileKeyLookup = new Dictionary<string, TileKeyOption>(StringComparer.OrdinalIgnoreCase);
private string[] tileKeyStrings = Array.Empty<string>();
private string cachedTileCsvPath = "";
private DateTime cachedTileCsvTimeUtc = DateTime.MinValue;
private float cachedTileKeySizeX = 0f;
private float cachedTileKeySizeY = 0f;
private float cachedTileOverlapX = 0f;
private float cachedTileOverlapY = 0f;
// Prefabs for trees and furniture (assign in editor)
private GameObject treePrefab;
private GameObject lampPrefab;
@@ -60,6 +78,8 @@ public class GeoTileImporter : EditorWindow
private void OnGUI()
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
GUILayout.Label("Inputs", EditorStyles.boldLabel);
tilesCsvPath = EditorGUILayout.TextField("tile_index.csv", tilesCsvPath);
heightmapsDir = EditorGUILayout.TextField("height_png16 dir", heightmapsDir);
@@ -110,6 +130,19 @@ public class GeoTileImporter : EditorWindow
bollardPrefab = (GameObject)EditorGUILayout.ObjectField("Bollard Prefab", bollardPrefab, typeof(GameObject), false);
defaultFurniturePrefab = (GameObject)EditorGUILayout.ObjectField("Default Furniture Prefab", defaultFurniturePrefab, typeof(GameObject), false);
GUILayout.Space(12);
GUILayout.Label("Tile Key Config", EditorStyles.boldLabel);
tileKeySizeX = EditorGUILayout.FloatField("Tile Size X (m)", tileKeySizeX);
tileKeySizeY = EditorGUILayout.FloatField("Tile Size Y (m)", tileKeySizeY);
tileKeyOverlapX = EditorGUILayout.FloatField("Overlap X (m)", tileKeyOverlapX);
tileKeyOverlapY = EditorGUILayout.FloatField("Overlap Y (m)", tileKeyOverlapY);
RefreshTileIndexCache();
GUILayout.Space(10);
GUILayout.Label("Tile Selection", EditorStyles.boldLabel);
DrawTileSelectionUI();
GUILayout.Space(12);
if (GUILayout.Button("Import / Rebuild"))
ImportTiles();
@@ -119,6 +152,8 @@ public class GeoTileImporter : EditorWindow
"Absolute elevation mapping: Terrain Y = global_min, Terrain height = (global_max - global_min).\n" +
"CSV is header-driven (order-independent). Optionally applies ortho JPGs and instantiates buildings/trees GLBs.",
MessageType.Info);
EditorGUILayout.EndScrollView();
}
private static void EnsureHeightmapImportSettings(string assetPath)
@@ -258,6 +293,8 @@ public class GeoTileImporter : EditorWindow
applyOrthoTextures = false;
}
RefreshTileIndexCache();
var parent = GameObject.Find(parentName);
if (parent == null) parent = new GameObject(parentName);
@@ -267,118 +304,51 @@ public class GeoTileImporter : EditorWindow
DestroyImmediate(parent.transform.GetChild(i).gameObject);
}
var ci = CultureInfo.InvariantCulture;
var lines = File.ReadAllLines(tilesCsvPath);
Debug.Log($"[GeoTileImporter] Read {lines.Length} lines.");
if (lines.Length < 2)
var tiles = ParseTilesCsv();
if (tiles == null || tiles.Count == 0)
{
Debug.LogError("[GeoTileImporter] CSV has no data rows (need header + at least 1 row).");
Debug.LogError("[GeoTileImporter] No valid tiles found in CSV.");
return;
}
var headerLine = lines[0].Trim();
var headerMap = BuildHeaderMap(headerLine);
Debug.Log($"[GeoTileImporter] Header: {headerLine}");
Debug.Log($"[GeoTileImporter] Header columns mapped: {string.Join(", ", headerMap.Keys)}");
// Required columns (order-independent)
string[] required = { "tile_id", "xmin", "ymin", "global_min", "global_max", "out_res" };
if (!HasAll(headerMap, required))
var selectedTiles = ApplySelection(tiles);
if (selectedTiles.Count == 0)
{
Debug.LogError("[GeoTileImporter] CSV missing required columns. Required: " +
string.Join(", ", required) +
"\nFound: " + string.Join(", ", headerMap.Keys));
Debug.LogError("[GeoTileImporter] Selection is empty. Adjust Tile Selection and try again.");
return;
}
int IDX_TILE = headerMap["tile_id"];
int IDX_XMIN = headerMap["xmin"];
int IDX_YMIN = headerMap["ymin"];
int IDX_GMIN = headerMap["global_min"];
int IDX_GMAX = headerMap["global_max"];
int IDX_RES = headerMap["out_res"];
// Compute origin from min xmin/ymin
double originX = double.PositiveInfinity;
double originY = double.PositiveInfinity;
int validRowsForOrigin = 0;
for (int i = 1; i < lines.Length; i++)
for (int i = 0; i < selectedTiles.Count; i++)
{
var line = lines[i].Trim();
if (string.IsNullOrWhiteSpace(line)) continue;
var parts = line.Split(',');
// Robust: just ensure indices exist in this row
int needMaxIndex = Math.Max(Math.Max(Math.Max(Math.Max(IDX_TILE, IDX_XMIN), IDX_YMIN), IDX_GMIN), Math.Max(IDX_GMAX, IDX_RES));
if (parts.Length <= needMaxIndex)
{
Debug.LogWarning($"[GeoTileImporter] Origin scan: skipping line {i + 1} (too few columns: {parts.Length}). Line: '{line}'");
continue;
}
try
{
double xmin = double.Parse(parts[IDX_XMIN], ci);
double ymin = double.Parse(parts[IDX_YMIN], ci);
originX = Math.Min(originX, xmin);
originY = Math.Min(originY, ymin);
validRowsForOrigin++;
}
catch (Exception e)
{
Debug.LogWarning($"[GeoTileImporter] Origin scan parse failed line {i + 1}: '{line}'\n{e.Message}");
}
originX = Math.Min(originX, selectedTiles[i].Xmin);
originY = Math.Min(originY, selectedTiles[i].Ymin);
}
if (validRowsForOrigin == 0 || double.IsInfinity(originX) || double.IsInfinity(originY))
if (double.IsInfinity(originX) || double.IsInfinity(originY))
{
Debug.LogError("[GeoTileImporter] Could not compute origin (no valid rows parsed). Check CSV numeric format.");
return;
}
Debug.Log($"[GeoTileImporter] Origin: ({originX}, {originY}) from {validRowsForOrigin} valid rows.");
Debug.Log($"[GeoTileImporter] Origin: ({originX}, {originY}) from {selectedTiles.Count} selected tiles.");
int imported = 0, skipped = 0;
int importedTextures = 0;
var placements = new List<(string tileId, float ux, float uz, float gmin)>();
for (int i = 1; i < lines.Length; i++)
for (int i = 0; i < selectedTiles.Count; i++)
{
var line = lines[i].Trim();
if (string.IsNullOrWhiteSpace(line)) continue;
var tile = selectedTiles[i];
var tileId = tile.TileId;
double xmin = tile.Xmin;
double ymin = tile.Ymin;
double gmin = tile.GlobalMin;
double gmax = tile.GlobalMax;
var parts = line.Split(',');
int needMaxIndex = Math.Max(Math.Max(Math.Max(Math.Max(IDX_TILE, IDX_XMIN), IDX_YMIN), IDX_GMIN), Math.Max(IDX_GMAX, IDX_RES));
if (parts.Length <= needMaxIndex)
{
skipped++;
Debug.LogWarning($"[GeoTileImporter] Skipping line {i + 1} (too few columns: {parts.Length}). Line: '{line}'");
continue;
}
string tileId = parts[IDX_TILE].Trim();
double xmin, ymin, gmin, gmax;
int outRes;
try
{
xmin = double.Parse(parts[IDX_XMIN], ci);
ymin = double.Parse(parts[IDX_YMIN], ci);
gmin = double.Parse(parts[IDX_GMIN], ci);
gmax = double.Parse(parts[IDX_GMAX], ci);
outRes = int.Parse(parts[IDX_RES], ci);
}
catch (Exception e)
{
skipped++;
Debug.LogWarning($"[GeoTileImporter] Parse failed line {i + 1} tile '{tileId}': {e.Message}\nLine: '{line}'");
continue;
}
if (outRes != heightmapResolution)
Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: out_res={outRes} but importer expects {heightmapResolution}.");
if (tile.OutRes != heightmapResolution)
Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: out_res={tile.OutRes} but importer expects {heightmapResolution}.");
float heightRange = (float)(gmax - gmin);
if (heightRange <= 0.0001f)
@@ -493,7 +463,7 @@ public class GeoTileImporter : EditorWindow
}
}
Debug.Log($"[GeoTileImporter] Imported {tileId} @ XZ=({ux},{uz}) Y={gmin} heightRange={heightRange} usedU16={usedU16}");
Debug.Log($"[GeoTileImporter] Imported {tileId} ({tile.TileKey}) @ XZ=({ux},{uz}) Y={gmin} heightRange={heightRange} usedU16={usedU16}");
imported++;
placements.Add((tileId, ux, uz, (float)gmin));
}
@@ -509,6 +479,313 @@ public class GeoTileImporter : EditorWindow
ImportEnhancedTrees(placements);
}
private void RefreshTileIndexCache()
{
if (!File.Exists(tilesCsvPath))
{
tileIndexCache.Clear();
tileKeyOptions.Clear();
tileKeyLookup.Clear();
tileKeyStrings = Array.Empty<string>();
return;
}
var writeTime = File.GetLastWriteTimeUtc(tilesCsvPath);
if (tilesCsvPath == cachedTileCsvPath &&
writeTime == cachedTileCsvTimeUtc &&
Mathf.Approximately(tileKeySizeX, cachedTileKeySizeX) &&
Mathf.Approximately(tileKeySizeY, cachedTileKeySizeY) &&
Mathf.Approximately(tileKeyOverlapX, cachedTileOverlapX) &&
Mathf.Approximately(tileKeyOverlapY, cachedTileOverlapY))
{
return;
}
tileIndexCache.Clear();
tileIndexCache.AddRange(ParseTilesCsv());
BuildTileKeyOptions();
cachedTileCsvPath = tilesCsvPath;
cachedTileCsvTimeUtc = writeTime;
cachedTileKeySizeX = tileKeySizeX;
cachedTileKeySizeY = tileKeySizeY;
cachedTileOverlapX = tileKeyOverlapX;
cachedTileOverlapY = tileKeyOverlapY;
}
private void BuildTileKeyOptions()
{
tileKeyOptions.Clear();
tileKeyLookup.Clear();
for (int i = 0; i < tileIndexCache.Count; i++)
{
var tile = tileIndexCache[i];
if (string.IsNullOrWhiteSpace(tile.TileKey))
continue;
if (tileKeyLookup.ContainsKey(tile.TileKey))
continue;
var option = new TileKeyOption
{
Key = tile.TileKey,
X = tile.XKey,
Y = tile.YKey
};
tileKeyOptions.Add(option);
tileKeyLookup[tile.TileKey] = option;
}
tileKeyOptions.Sort((a, b) =>
{
int cmp = a.X.CompareTo(b.X);
return cmp != 0 ? cmp : a.Y.CompareTo(b.Y);
});
tileKeyStrings = new string[tileKeyOptions.Count];
for (int i = 0; i < tileKeyOptions.Count; i++)
tileKeyStrings[i] = tileKeyOptions[i].Key;
}
private void DrawTileSelectionUI()
{
if (tileKeyOptions.Count == 0)
{
EditorGUILayout.HelpBox("No tile keys available. Check the tile index CSV and key config.", MessageType.Info);
EditorGUILayout.LabelField("Tiles in selection", "0");
return;
}
if (string.IsNullOrEmpty(bottomLeftKey) || !tileKeyLookup.ContainsKey(bottomLeftKey))
bottomLeftKey = tileKeyOptions[0].Key;
var bottomLeft = tileKeyLookup[bottomLeftKey];
var topRightOptions = GetTopRightOptions(bottomLeft);
if (topRightOptions.Count == 0)
topRightOptions.Add(bottomLeft);
if (string.IsNullOrEmpty(topRightKey) || !ContainsKey(topRightOptions, topRightKey))
topRightKey = topRightOptions[topRightOptions.Count - 1].Key;
int bottomLeftIndex = Array.IndexOf(tileKeyStrings, bottomLeftKey);
int newBottomLeftIndex = EditorGUILayout.Popup("Bottom-Left Key", bottomLeftIndex, tileKeyStrings);
if (newBottomLeftIndex != bottomLeftIndex)
{
bottomLeftKey = tileKeyStrings[newBottomLeftIndex];
bottomLeft = tileKeyLookup[bottomLeftKey];
topRightOptions = GetTopRightOptions(bottomLeft);
if (topRightOptions.Count == 0)
topRightOptions.Add(bottomLeft);
if (!ContainsKey(topRightOptions, topRightKey))
topRightKey = topRightOptions[topRightOptions.Count - 1].Key;
}
var topRightKeys = BuildKeyArray(topRightOptions);
int topRightIndex = Array.IndexOf(topRightKeys, topRightKey);
if (topRightIndex < 0)
topRightIndex = topRightKeys.Length - 1;
int newTopRightIndex = EditorGUILayout.Popup("Top-Right Key", topRightIndex, topRightKeys);
if (newTopRightIndex != topRightIndex)
topRightKey = topRightKeys[newTopRightIndex];
EditorGUILayout.LabelField("Tiles in selection", CountSelectedTiles().ToString());
}
private List<TileKeyOption> GetTopRightOptions(TileKeyOption bottomLeft)
{
var options = new List<TileKeyOption>();
for (int i = 0; i < tileKeyOptions.Count; i++)
{
var option = tileKeyOptions[i];
if (option.X >= bottomLeft.X && option.Y >= bottomLeft.Y)
options.Add(option);
}
return options;
}
private int CountSelectedTiles()
{
if (!tileKeyLookup.TryGetValue(bottomLeftKey, out var bottomLeft) ||
!tileKeyLookup.TryGetValue(topRightKey, out var topRight))
return 0;
int count = 0;
for (int i = 0; i < tileIndexCache.Count; i++)
{
var tile = tileIndexCache[i];
if (tile.XKey >= bottomLeft.X && tile.XKey <= topRight.X &&
tile.YKey >= bottomLeft.Y && tile.YKey <= topRight.Y)
count++;
}
return count;
}
private List<TileRecord> ApplySelection(List<TileRecord> tiles)
{
if (!tileKeyLookup.TryGetValue(bottomLeftKey, out var bottomLeft) ||
!tileKeyLookup.TryGetValue(topRightKey, out var topRight))
return tiles;
var selected = new List<TileRecord>();
for (int i = 0; i < tiles.Count; i++)
{
var tile = tiles[i];
if (tile.XKey >= bottomLeft.X && tile.XKey <= topRight.X &&
tile.YKey >= bottomLeft.Y && tile.YKey <= topRight.Y)
selected.Add(tile);
}
return selected;
}
private List<TileRecord> ParseTilesCsv()
{
var tiles = new List<TileRecord>();
var ci = CultureInfo.InvariantCulture;
var lines = File.ReadAllLines(tilesCsvPath);
if (lines.Length < 2)
{
Debug.LogError("[GeoTileImporter] CSV has no data rows (need header + at least 1 row).");
return tiles;
}
var headerLine = lines[0].Trim();
var headerMap = BuildHeaderMap(headerLine);
Debug.Log($"[GeoTileImporter] Header columns mapped: {string.Join(", ", headerMap.Keys)}");
string[] required = { "tile_id", "xmin", "ymin", "global_min", "global_max", "out_res" };
if (!HasAll(headerMap, required))
{
Debug.LogError("[GeoTileImporter] CSV missing required columns. Required: " +
string.Join(", ", required) +
"\nFound: " + string.Join(", ", headerMap.Keys));
return tiles;
}
int IDX_TILE = headerMap["tile_id"];
int IDX_XMIN = headerMap["xmin"];
int IDX_YMIN = headerMap["ymin"];
int IDX_GMIN = headerMap["global_min"];
int IDX_GMAX = headerMap["global_max"];
int IDX_RES = headerMap["out_res"];
int IDX_TILE_KEY = headerMap.ContainsKey("tile_key") ? headerMap["tile_key"] : -1;
for (int i = 1; i < lines.Length; i++)
{
var line = lines[i].Trim();
if (string.IsNullOrWhiteSpace(line))
continue;
var parts = line.Split(',');
int needMaxIndex = Math.Max(Math.Max(Math.Max(Math.Max(IDX_TILE, IDX_XMIN), IDX_YMIN), IDX_GMIN), Math.Max(IDX_GMAX, IDX_RES));
if (IDX_TILE_KEY >= 0)
needMaxIndex = Math.Max(needMaxIndex, IDX_TILE_KEY);
if (parts.Length <= needMaxIndex)
{
Debug.LogWarning($"[GeoTileImporter] Skipping line {i + 1} (too few columns: {parts.Length}).");
continue;
}
try
{
var xmin = double.Parse(parts[IDX_XMIN], ci);
var ymin = double.Parse(parts[IDX_YMIN], ci);
var tileKeyRaw = IDX_TILE_KEY >= 0 ? parts[IDX_TILE_KEY].Trim() : "";
var tileKey = ResolveTileKey(tileKeyRaw, xmin, ymin, out var xKey, out var yKey);
tiles.Add(new TileRecord
{
TileId = parts[IDX_TILE].Trim(),
TileKey = tileKey,
XKey = xKey,
YKey = yKey,
Xmin = xmin,
Ymin = ymin,
GlobalMin = double.Parse(parts[IDX_GMIN], ci),
GlobalMax = double.Parse(parts[IDX_GMAX], ci),
OutRes = int.Parse(parts[IDX_RES], ci)
});
}
catch (Exception e)
{
Debug.LogWarning($"[GeoTileImporter] Parse error line {i + 1}: {e.Message}");
}
}
return tiles;
}
private string ResolveTileKey(string tileKeyRaw, double xmin, double ymin, out int xKey, out int yKey)
{
if (!string.IsNullOrWhiteSpace(tileKeyRaw) && TryParseTileKey(tileKeyRaw, out xKey, out yKey))
return tileKeyRaw;
float sizeX = tileKeySizeX <= 0f ? 1f : tileKeySizeX;
float sizeY = tileKeySizeY <= 0f ? 1f : tileKeySizeY;
xKey = (int)Math.Floor((xmin + tileKeyOverlapX) / sizeX);
yKey = (int)Math.Floor((ymin + tileKeyOverlapY) / sizeY);
return $"{xKey}_{yKey}";
}
private static bool TryParseTileKey(string tileKey, out int xKey, out int yKey)
{
xKey = 0;
yKey = 0;
if (string.IsNullOrWhiteSpace(tileKey))
return false;
var parts = tileKey.Split('_');
if (parts.Length != 2)
return false;
return int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out xKey)
&& int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out yKey);
}
private static bool ContainsKey(List<TileKeyOption> options, string key)
{
for (int i = 0; i < options.Count; i++)
{
if (string.Equals(options[i].Key, key, StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
private static string[] BuildKeyArray(List<TileKeyOption> options)
{
var keys = new string[options.Count];
for (int i = 0; i < options.Count; i++)
keys[i] = options[i].Key;
return keys;
}
private struct TileRecord
{
public string TileId;
public string TileKey;
public int XKey;
public int YKey;
public double Xmin;
public double Ymin;
public double GlobalMin;
public double GlobalMax;
public int OutRes;
}
private struct TileKeyOption
{
public string Key;
public int X;
public int Y;
}
private void ImportBuildings(List<(string tileId, float ux, float uz, float gmin)> placements)
{
if (!importBuildings)