376 lines
13 KiB
C#
376 lines
13 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|