From 439cd943e485733f39e021b160d982908612ff13 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 21:55:27 -0700 Subject: [PATCH] feat(editor): add --info-project-extents for combined spatial bbox Project-wide companion to --info-zone-extents. Walks every zone in , computes each zone's tile XY range and Z height range, then unions them into a project-wide world bounding box. Per-zone breakdown table shows each zone's disjoint world ranges so overlap issues are visible at a glance. Useful for sizing the world map overview, sanity-checking that zones don't overlap (project-union should equal sum of per-zone areas for disjoint layouts), and understanding total project area in both yards and meters. Verified on 2-zone test project: each zone correctly resolves to one 533x533yd tile, project union is 1066x533yd (the two tiles arranged horizontally), Z range matches manifest baseHeight + heightmap delta. Brings command count to 178. --- tools/editor/main.cpp | 143 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 68e1f509..078b24a1 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -684,6 +684,8 @@ static void printUsage(const char* argv0) { std::printf(" Run validate-project + open-only + check-project-refs together; one PASS/FAIL\n"); std::printf(" --info-zone-bytes [--json]\n"); std::printf(" Per-file size breakdown grouped by category, sorted largest-first\n"); + std::printf(" --info-project-extents [--json]\n"); + std::printf(" Combined spatial bounding box across every zone (per-zone table + project union)\n"); std::printf(" --info-zone-extents [--json]\n"); std::printf(" Compute the zone's bounding box (XY tile range, Z height min/max)\n"); std::printf(" --info-zone-water [--json]\n"); @@ -879,7 +881,8 @@ int main(int argc, char* argv[]) { "--validate-png", "--validate-blp", "--zone-summary", "--info-zone-tree", "--info-project-tree", "--info-zone-bytes", "--info-project-bytes", - "--info-zone-extents", "--info-zone-water", + "--info-zone-extents", "--info-project-extents", + "--info-zone-water", "--info-zone-density", "--export-zone-summary-md", "--export-quest-graph", "--export-zone-csv", "--export-zone-html", "--export-project-html", @@ -5871,6 +5874,144 @@ int main(int argc, char* argv[]) { widthX, widthY, heightZ, widthX * 0.9144f, widthY * 0.9144f, heightZ * 0.9144f); return 0; + } else if (std::strcmp(argv[i], "--info-project-extents") == 0 && i + 1 < argc) { + // Combined spatial bounding box across every zone in + // . Per-zone XY tile range + Z height range, + // unioned into a project-wide world box. Useful for + // understanding total project area, sizing the world map + // overview, or sanity-checking that zones don't overlap + // (the union should equal the sum of disjoint per-zone + // boxes). + std::string projectDir = argv[++i]; + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) i++; + namespace fs = std::filesystem; + if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) { + std::fprintf(stderr, + "info-project-extents: %s is not a directory\n", + projectDir.c_str()); + return 1; + } + std::vector zones; + for (const auto& entry : fs::directory_iterator(projectDir)) { + if (!entry.is_directory()) continue; + if (!fs::exists(entry.path() / "zone.json")) continue; + zones.push_back(entry.path().string()); + } + std::sort(zones.begin(), zones.end()); + constexpr float kTileSize = 533.33333f; + struct ZBox { + std::string name; + int tileCount = 0; + float wMinX = 1e30f, wMaxX = -1e30f; + float wMinY = 1e30f, wMaxY = -1e30f; + float zMin = 1e30f, zMax = -1e30f; + }; + std::vector rows; + float gMinX = 1e30f, gMaxX = -1e30f; + float gMinY = 1e30f, gMaxY = -1e30f; + float gZMin = 1e30f, gZMax = -1e30f; + int totalTiles = 0; + for (const auto& zoneDir : zones) { + ZBox b; + b.name = fs::path(zoneDir).filename().string(); + wowee::editor::ZoneManifest zm; + if (!zm.load(zoneDir + "/zone.json")) { + rows.push_back(b); + continue; + } + b.tileCount = static_cast(zm.tiles.size()); + if (zm.tiles.empty()) { + rows.push_back(b); + continue; + } + int tMinX = 64, tMaxX = -1, tMinY = 64, tMaxY = -1; + for (const auto& [tx, ty] : zm.tiles) { + tMinX = std::min(tMinX, tx); + tMaxX = std::max(tMaxX, tx); + tMinY = std::min(tMinY, ty); + tMaxY = std::max(tMaxY, ty); + } + b.wMinX = (32.0f - tMaxY - 1) * kTileSize; + b.wMaxX = (32.0f - tMinY) * kTileSize; + b.wMinY = (32.0f - tMaxX - 1) * kTileSize; + b.wMaxY = (32.0f - tMinX) * kTileSize; + for (const auto& [tx, ty] : zm.tiles) { + std::string tileBase = zoneDir + "/" + zm.mapName + "_" + + std::to_string(tx) + "_" + std::to_string(ty); + if (!wowee::pipeline::WoweeTerrainLoader::exists(tileBase)) continue; + wowee::pipeline::ADTTerrain terrain; + wowee::pipeline::WoweeTerrainLoader::load(tileBase, terrain); + for (const auto& chunk : terrain.chunks) { + if (!chunk.heightMap.isLoaded()) continue; + float baseZ = chunk.position[2]; + for (float h : chunk.heightMap.heights) { + if (!std::isfinite(h)) continue; + b.zMin = std::min(b.zMin, baseZ + h); + b.zMax = std::max(b.zMax, baseZ + h); + } + } + } + if (b.zMin > b.zMax) { b.zMin = 0; b.zMax = 0; } + gMinX = std::min(gMinX, b.wMinX); + gMaxX = std::max(gMaxX, b.wMaxX); + gMinY = std::min(gMinY, b.wMinY); + gMaxY = std::max(gMaxY, b.wMaxY); + gZMin = std::min(gZMin, b.zMin); + gZMax = std::max(gZMax, b.zMax); + totalTiles += b.tileCount; + rows.push_back(b); + } + if (totalTiles == 0) { + gMinX = gMaxX = gMinY = gMaxY = gZMin = gZMax = 0.0f; + } + float gWidthX = gMaxX - gMinX; + float gWidthY = gMaxY - gMinY; + float gHeightZ = gZMax - gZMin; + if (jsonOut) { + nlohmann::json j; + j["project"] = projectDir; + j["zoneCount"] = zones.size(); + j["totalTiles"] = totalTiles; + j["worldBox"] = {{"min", {gMinX, gMinY, gZMin}}, + {"max", {gMaxX, gMaxY, gZMax}}}; + j["sizeYards"] = {gWidthX, gWidthY, gHeightZ}; + nlohmann::json zarr = nlohmann::json::array(); + for (const auto& b : rows) { + zarr.push_back({{"name", b.name}, + {"tileCount", b.tileCount}, + {"worldBox", {{"min", {b.wMinX, b.wMinY, b.zMin}}, + {"max", {b.wMaxX, b.wMaxY, b.zMax}}}}}); + } + j["zones"] = zarr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("Project extents: %s\n", projectDir.c_str()); + std::printf(" zones : %zu\n", zones.size()); + std::printf(" total tiles : %d\n", totalTiles); + if (totalTiles == 0) { + std::printf(" *no tiles in any zone manifest*\n"); + return 0; + } + std::printf(" world union : (%.1f, %.1f, %.1f) - (%.1f, %.1f, %.1f) yards\n", + gMinX, gMinY, gZMin, gMaxX, gMaxY, gZMax); + std::printf(" total size : %.1f x %.1f x %.1f yards (%.0fm x %.0fm x %.1fm)\n", + gWidthX, gWidthY, gHeightZ, + gWidthX * 0.9144f, gWidthY * 0.9144f, gHeightZ * 0.9144f); + std::printf("\n zone tiles worldX (min..max) worldY (min..max)\n"); + for (const auto& b : rows) { + if (b.tileCount == 0) { + std::printf(" %-20s %5d (no tiles)\n", + b.name.substr(0, 20).c_str(), b.tileCount); + continue; + } + std::printf(" %-20s %5d %9.1f .. %9.1f %9.1f .. %9.1f\n", + b.name.substr(0, 20).c_str(), b.tileCount, + b.wMinX, b.wMaxX, b.wMinY, b.wMaxY); + } + return 0; } else if (std::strcmp(argv[i], "--info-zone-water") == 0 && i + 1 < argc) { // Aggregate water-layer stats across all tiles in a zone. // Useful for confirming a 'lake zone' actually has water,