add tile key selection for addressables builds
This commit is contained in:
375
Assets/Scripts/Editor/GeoTileAddressablesWindow.cs
Normal file
375
Assets/Scripts/Editor/GeoTileAddressablesWindow.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user