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

@@ -35,6 +35,24 @@ public class GeoTilePrefabImporter : EditorWindow
private bool includeFurniture = false;
private bool includeEnhancedTrees = false;
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<TileMetadata> tileIndexCache = new List<TileMetadata>();
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 furniture (optional)
private GameObject lampPrefab;
private GameObject benchPrefab;
@@ -45,7 +63,10 @@ public class GeoTilePrefabImporter : EditorWindow
private struct TileMetadata
{
public string TileKey;
public string TileId;
public int XKey;
public int YKey;
public double Xmin, Ymin, Xmax, Ymax;
public double GlobalMin, GlobalMax;
public int OutRes;
@@ -60,6 +81,8 @@ public class GeoTilePrefabImporter : EditorWindow
private void OnGUI()
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
GUILayout.Label("Input Paths", EditorStyles.boldLabel);
tilesCsvPath = EditorGUILayout.TextField("tile_index.csv", tilesCsvPath);
heightmapsDir = EditorGUILayout.TextField("height_png16 dir", heightmapsDir);
@@ -96,6 +119,19 @@ public class GeoTilePrefabImporter : EditorWindow
bollardPrefab = (GameObject)EditorGUILayout.ObjectField("Bollard Prefab", bollardPrefab, typeof(GameObject), false);
defaultFurniturePrefab = (GameObject)EditorGUILayout.ObjectField("Default Furniture", 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(15);
if (GUILayout.Button("Generate Prefabs"))
ImportTilesAsPrefabs();
@@ -110,6 +146,8 @@ public class GeoTilePrefabImporter : EditorWindow
"TerrainData and TerrainLayers are saved as separate .asset files.\n" +
"IMPORTANT: Use 'Place All Prefabs in Scene' to position tiles correctly.",
MessageType.Info);
EditorGUILayout.EndScrollView();
}
private void PlaceAllPrefabsInScene()
@@ -211,6 +249,7 @@ public class GeoTilePrefabImporter : EditorWindow
EnsureDirectoryExists($"{prefabOutputDir}/TerrainLayers");
// Parse CSV
RefreshTileIndexCache();
var tiles = ParseTilesCsv();
if (tiles == null || tiles.Count == 0)
{
@@ -218,17 +257,24 @@ public class GeoTilePrefabImporter : EditorWindow
return;
}
Debug.Log($"[GeoTilePrefabImporter] Found {tiles.Count} tiles to process.");
var selectedTiles = ApplySelection(tiles);
if (selectedTiles.Count == 0)
{
Debug.LogError("[GeoTilePrefabImporter] Selection is empty. Adjust Tile Selection and try again.");
return;
}
Debug.Log($"[GeoTilePrefabImporter] Found {selectedTiles.Count} tiles to process.");
int created = 0, skipped = 0, failed = 0;
for (int i = 0; i < tiles.Count; i++)
for (int i = 0; i < selectedTiles.Count; i++)
{
var tile = tiles[i];
var tile = selectedTiles[i];
EditorUtility.DisplayProgressBar(
"Creating Tile Prefabs",
$"Processing {tile.TileId} ({i + 1}/{tiles.Count})",
(float)i / tiles.Count);
$"Processing {tile.TileId} ({i + 1}/{selectedTiles.Count})",
(float)i / selectedTiles.Count);
string prefabPath = $"{prefabOutputDir}/{tile.TileId}.prefab";
if (File.Exists(prefabPath) && !overwriteExisting)
@@ -259,6 +305,168 @@ public class GeoTilePrefabImporter : EditorWindow
Debug.Log($"[GeoTilePrefabImporter] DONE. Created={created}, Skipped={skipped}, Failed={failed}");
}
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<TileMetadata> ApplySelection(List<TileMetadata> tiles)
{
if (!tileKeyLookup.TryGetValue(bottomLeftKey, out var bottomLeft) ||
!tileKeyLookup.TryGetValue(topRightKey, out var topRight))
return tiles;
var selected = new List<TileMetadata>();
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<TileMetadata> ParseTilesCsv()
{
var tiles = new List<TileMetadata>();
@@ -288,6 +496,7 @@ public class GeoTilePrefabImporter : EditorWindow
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++)
{
@@ -295,7 +504,11 @@ public class GeoTilePrefabImporter : EditorWindow
if (string.IsNullOrWhiteSpace(line)) continue;
var parts = line.Split(',');
int maxIdx = Math.Max(Math.Max(Math.Max(IDX_TILE, IDX_XMAX), IDX_YMAX), Math.Max(IDX_GMAX, IDX_RES));
int maxIdx = Math.Max(
Math.Max(Math.Max(IDX_TILE, IDX_XMIN), Math.Max(IDX_YMIN, IDX_XMAX)),
Math.Max(Math.Max(IDX_YMAX, IDX_GMIN), Math.Max(IDX_GMAX, IDX_RES)));
if (IDX_TILE_KEY >= 0)
maxIdx = Math.Max(maxIdx, IDX_TILE_KEY);
if (parts.Length <= maxIdx)
{
Debug.LogWarning($"[GeoTilePrefabImporter] Skipping line {i + 1}: too few columns.");
@@ -304,11 +517,19 @@ public class GeoTilePrefabImporter : EditorWindow
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 TileMetadata
{
TileKey = tileKey,
TileId = parts[IDX_TILE].Trim(),
Xmin = double.Parse(parts[IDX_XMIN], ci),
Ymin = double.Parse(parts[IDX_YMIN], ci),
XKey = xKey,
YKey = yKey,
Xmin = xmin,
Ymin = ymin,
Xmax = double.Parse(parts[IDX_XMAX], ci),
Ymax = double.Parse(parts[IDX_YMAX], ci),
GlobalMin = double.Parse(parts[IDX_GMIN], ci),
@@ -325,6 +546,58 @@ public class GeoTilePrefabImporter : EditorWindow
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 TileKeyOption
{
public string Key;
public int X;
public int Y;
}
private bool CreateTilePrefab(TileMetadata tile)
{
// Validate height range
@@ -407,6 +680,7 @@ public class GeoTilePrefabImporter : EditorWindow
// Store metadata as component for later use
var metadata = root.AddComponent<GeoTileMetadata>();
metadata.tileKey = tile.TileKey;
metadata.tileId = tile.TileId;
metadata.xmin = tile.Xmin;
metadata.ymin = tile.Ymin;