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

@@ -0,0 +1,375 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
public class GeoTileAddressablesWindow : EditorWindow
{
private string tileIndexCsvPath = "Assets/GeoData/tile_index.csv";
private string tilePrefabsDir = "Assets/TilePrefabs";
private string outputRoot = "ServerData/TileBundles";
private BuildTarget buildTarget = BuildTarget.StandaloneLinux64;
private float tileSizeX = 1000f;
private float tileSizeY = 1000f;
private float overlapX = 0.5f;
private float overlapY = 0.5f;
private string bottomLeftKey = "";
private string topRightKey = "";
private bool overwriteExisting = true;
private bool verboseLogging = false;
private Vector2 scrollPosition;
private enum BuildSource
{
Prefabs,
SceneTerrains
}
private BuildSource buildSource = BuildSource.Prefabs;
private readonly List<GeoTileAddressablesBuilder.TileRecord> tileRecords = new List<GeoTileAddressablesBuilder.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 cachedCsvPath = "";
private DateTime cachedCsvWriteTimeUtc = DateTime.MinValue;
private float cachedTileSizeX = 0f;
private float cachedTileSizeY = 0f;
private float cachedOverlapX = 0f;
private float cachedOverlapY = 0f;
private struct TileKeyOption
{
public string Key;
public int X;
public int Y;
}
[MenuItem("Tools/Geo Tiles/Addressables Build Window")]
public static void ShowWindow()
{
var win = GetWindow<GeoTileAddressablesWindow>("Geo Tile Addressables Build");
win.minSize = new Vector2(620, 420);
}
private void OnGUI()
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
GUILayout.Label("Geo Tile Addressables Build", EditorStyles.boldLabel);
GUILayout.Space(8);
GUILayout.Label("Paths", EditorStyles.boldLabel);
tileIndexCsvPath = EditorGUILayout.TextField("Tile Index CSV", tileIndexCsvPath);
tilePrefabsDir = EditorGUILayout.TextField("Tile Prefabs Dir", tilePrefabsDir);
outputRoot = EditorGUILayout.TextField("Output Root", outputRoot);
buildTarget = (BuildTarget)EditorGUILayout.EnumPopup("Build Target", buildTarget);
GUILayout.Space(10);
GUILayout.Label("Tile Key Config", EditorStyles.boldLabel);
tileSizeX = EditorGUILayout.FloatField("Tile Size X (m)", tileSizeX);
tileSizeY = EditorGUILayout.FloatField("Tile Size Y (m)", tileSizeY);
overlapX = EditorGUILayout.FloatField("Overlap X (m)", overlapX);
overlapY = EditorGUILayout.FloatField("Overlap Y (m)", overlapY);
RefreshTileCache();
GUILayout.Space(10);
GUILayout.Label("Tile Selection", EditorStyles.boldLabel);
DrawTileSelection();
GUILayout.Space(10);
GUILayout.Label("Build Options", EditorStyles.boldLabel);
buildSource = (BuildSource)EditorGUILayout.EnumPopup("Source", buildSource);
overwriteExisting = EditorGUILayout.ToggleLeft("Overwrite existing bundles", overwriteExisting);
verboseLogging = EditorGUILayout.ToggleLeft("Verbose logging", verboseLogging);
if (buildSource == BuildSource.SceneTerrains)
EditorGUILayout.HelpBox("Scene terrain source is not implemented yet.", MessageType.Info);
GUILayout.Space(12);
if (GUILayout.Button("Build Addressables (Selected Tiles)"))
BuildSelectedTiles();
if (GUILayout.Button("Open Output Folder"))
OpenOutputFolder();
EditorGUILayout.EndScrollView();
}
private void RefreshTileCache()
{
if (!File.Exists(tileIndexCsvPath))
{
tileRecords.Clear();
tileKeyOptions.Clear();
tileKeyLookup.Clear();
tileKeyStrings = Array.Empty<string>();
return;
}
var writeTime = File.GetLastWriteTimeUtc(tileIndexCsvPath);
if (tileIndexCsvPath == cachedCsvPath &&
writeTime == cachedCsvWriteTimeUtc &&
Mathf.Approximately(tileSizeX, cachedTileSizeX) &&
Mathf.Approximately(tileSizeY, cachedTileSizeY) &&
Mathf.Approximately(overlapX, cachedOverlapX) &&
Mathf.Approximately(overlapY, cachedOverlapY))
{
return;
}
var config = new GeoTileAddressablesBuilder.TileKeyConfig
{
TileSizeX = tileSizeX,
TileSizeY = tileSizeY,
OverlapX = overlapX,
OverlapY = overlapY
};
tileRecords.Clear();
tileRecords.AddRange(GeoTileAddressablesBuilder.ParseTilesCsv(tileIndexCsvPath, config));
BuildTileKeyOptions();
cachedCsvPath = tileIndexCsvPath;
cachedCsvWriteTimeUtc = writeTime;
cachedTileSizeX = tileSizeX;
cachedTileSizeY = tileSizeY;
cachedOverlapX = overlapX;
cachedOverlapY = overlapY;
}
private void BuildTileKeyOptions()
{
tileKeyOptions.Clear();
tileKeyLookup.Clear();
for (int i = 0; i < tileRecords.Count; i++)
{
var tile = tileRecords[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 DrawTileSelection()
{
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 < tileRecords.Count; i++)
{
var tile = tileRecords[i];
if (tile.XKey >= bottomLeft.X && tile.XKey <= topRight.X &&
tile.YKey >= bottomLeft.Y && tile.YKey <= topRight.Y)
count++;
}
return count;
}
private List<GeoTileAddressablesBuilder.TileRecord> GetSelectedTiles()
{
var selected = new List<GeoTileAddressablesBuilder.TileRecord>();
if (!tileKeyLookup.TryGetValue(bottomLeftKey, out var bottomLeft) ||
!tileKeyLookup.TryGetValue(topRightKey, out var topRight))
return selected;
for (int i = 0; i < tileRecords.Count; i++)
{
var tile = tileRecords[i];
if (tile.XKey >= bottomLeft.X && tile.XKey <= topRight.X &&
tile.YKey >= bottomLeft.Y && tile.YKey <= topRight.Y)
selected.Add(tile);
}
return selected;
}
private void BuildSelectedTiles()
{
if (buildSource != BuildSource.Prefabs)
{
Debug.LogError("[GeoTileAddressablesWindow] Scene terrain source is not implemented.");
return;
}
RefreshTileCache();
if (tileRecords.Count == 0)
{
Debug.LogError("[GeoTileAddressablesWindow] No tiles loaded from CSV.");
return;
}
var selectedTiles = GetSelectedTiles();
if (selectedTiles.Count == 0)
{
Debug.LogError("[GeoTileAddressablesWindow] Selection is empty.");
return;
}
var request = new GeoTileAddressablesBuilder.BuildRequest
{
TileIndexCsvPath = tileIndexCsvPath,
TilePrefabsDir = tilePrefabsDir,
OutputRoot = outputRoot,
Target = buildTarget,
TargetGroup = GetBuildTargetGroup(buildTarget),
TileSizeMeters = tileSizeX,
TileKeyConfig = new GeoTileAddressablesBuilder.TileKeyConfig
{
TileSizeX = tileSizeX,
TileSizeY = tileSizeY,
OverlapX = overlapX,
OverlapY = overlapY
},
SelectedTiles = selectedTiles,
OverwriteExisting = overwriteExisting,
Verbose = verboseLogging
};
GeoTileAddressablesBuilder.BuildSelectedTiles(request);
}
private void OpenOutputFolder()
{
if (string.IsNullOrWhiteSpace(outputRoot))
return;
var normalized = outputRoot.Replace("\\", "/").TrimEnd('/');
var resolved = normalized.IndexOf("[BuildTarget]", StringComparison.OrdinalIgnoreCase) >= 0
? normalized.Replace("[BuildTarget]", buildTarget.ToString())
: $"{normalized}/{buildTarget}";
var fullPath = Path.GetFullPath(resolved);
if (Directory.Exists(fullPath))
EditorUtility.RevealInFinder(fullPath);
else
Debug.LogWarning($"[GeoTileAddressablesWindow] Output folder not found: {fullPath}");
}
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 static BuildTargetGroup GetBuildTargetGroup(BuildTarget target)
{
switch (target)
{
case BuildTarget.Android:
return BuildTargetGroup.Android;
case BuildTarget.StandaloneLinux64:
case BuildTarget.StandaloneWindows:
case BuildTarget.StandaloneWindows64:
case BuildTarget.StandaloneOSX:
return BuildTargetGroup.Standalone;
case BuildTarget.iOS:
return BuildTargetGroup.iOS;
case BuildTarget.WebGL:
return BuildTargetGroup.WebGL;
default:
return BuildTargetGroup.Standalone;
}
}
}