From 7a05e324a32f3a51b40be7d76e05a1aff950ddbc Mon Sep 17 00:00:00 2001 From: nathan Date: Thu, 14 May 2026 06:09:10 -0700 Subject: [PATCH] Verify Ground Zero neighbor edges --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 4 +- ...e544_n4160_neighbor_edge_verification.json | 289 ++++++++++++++++++ Docs/Terrain/GroundZeroNeighborEdges.md | 38 +++ Scripts/verify_ground_zero_neighbor_edges.py | 223 ++++++++++++++ 4 files changed, 552 insertions(+), 2 deletions(-) create mode 100644 Data/Terrain/Analysis/gz_us_ca_pacifica_utm10n_e544_n4160/gz_us_ca_pacifica_utm10n_e544_n4160_neighbor_edge_verification.json create mode 100644 Docs/Terrain/GroundZeroNeighborEdges.md create mode 100644 Scripts/verify_ground_zero_neighbor_edges.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 4a86cf1..8c7d215 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -423,7 +423,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Add generated tile metadata record for the MVP tile. - [x] Verify terrain scale is 1 km x 1 km in Unreal. - [x] Verify terrain tile origin and centered Unreal bounds for the Ground Zero test map. -- [ ] Verify neighboring tile edge coordinates against the registry before multi-tile stitching. +- [x] Verify neighboring tile edge coordinates against the registry before multi-tile stitching. - [ ] Add foliage pass. - [~] Add resource nodes. - [ ] Add biome-appropriate natural resources based on Ground Zero. @@ -1402,4 +1402,4 @@ Next version .01 priorities: Immediate next item: -- [ ] Verify neighboring tile edge coordinates against the registry before multi-tile stitching. +- [ ] Add foliage pass. diff --git a/Data/Terrain/Analysis/gz_us_ca_pacifica_utm10n_e544_n4160/gz_us_ca_pacifica_utm10n_e544_n4160_neighbor_edge_verification.json b/Data/Terrain/Analysis/gz_us_ca_pacifica_utm10n_e544_n4160/gz_us_ca_pacifica_utm10n_e544_n4160_neighbor_edge_verification.json new file mode 100644 index 0000000..8a28393 --- /dev/null +++ b/Data/Terrain/Analysis/gz_us_ca_pacifica_utm10n_e544_n4160/gz_us_ca_pacifica_utm10n_e544_n4160_neighbor_edge_verification.json @@ -0,0 +1,289 @@ +{ + "schema_version": 1, + "tile_id": "gz_us_ca_pacifica_utm10n_e544_n4160", + "registry_path": "Data/Tiles/ground_zero_tiles.json", + "grid_scheme": "prototype_utm_1km", + "verified_tile_count": 9, + "expected_neighbor_count": 8, + "found_neighbor_count": 8, + "all_passed": true, + "missing_neighbors": [], + "tile_shape_checks": [ + { + "tile_id": "gz_us_ca_pacifica_utm10n_e544_n4160", + "passed": true, + "errors": [] + }, + { + "tile_id": "gz_us_ca_pacifica_utm10n_e543_n4159", + "passed": true, + "errors": [] + }, + { + "tile_id": "gz_us_ca_pacifica_utm10n_e544_n4159", + "passed": true, + "errors": [] + }, + { + "tile_id": "gz_us_ca_pacifica_utm10n_e545_n4159", + "passed": true, + "errors": [] + }, + { + "tile_id": "gz_us_ca_pacifica_utm10n_e543_n4160", + "passed": true, + "errors": [] + }, + { + "tile_id": "gz_us_ca_pacifica_utm10n_e545_n4160", + "passed": true, + "errors": [] + }, + { + "tile_id": "gz_us_ca_pacifica_utm10n_e543_n4161", + "passed": true, + "errors": [] + }, + { + "tile_id": "gz_us_ca_pacifica_utm10n_e544_n4161", + "passed": true, + "errors": [] + }, + { + "tile_id": "gz_us_ca_pacifica_utm10n_e545_n4161", + "passed": true, + "errors": [] + } + ], + "neighbor_edge_checks": [ + { + "direction": "n", + "neighbor_tile_id": "gz_us_ca_pacifica_utm10n_e544_n4161", + "expected_bounds_utm_m": { + "easting_min_m": 544000, + "northing_min_m": 4161000, + "easting_max_m": 545000, + "northing_max_m": 4162000 + }, + "actual_bounds_utm_m": { + "easting_min_m": 544000, + "northing_min_m": 4161000, + "easting_max_m": 545000, + "northing_max_m": 4162000 + }, + "shared_edge": { + "center_edge": "north", + "neighbor_edge": "south", + "shared_northing_m": 4161000, + "easting_span_m": [ + 544000, + 545000 + ] + }, + "passed": true, + "errors": [] + }, + { + "direction": "ne", + "neighbor_tile_id": "gz_us_ca_pacifica_utm10n_e545_n4161", + "expected_bounds_utm_m": { + "easting_min_m": 545000, + "northing_min_m": 4161000, + "easting_max_m": 546000, + "northing_max_m": 4162000 + }, + "actual_bounds_utm_m": { + "easting_min_m": 545000, + "northing_min_m": 4161000, + "easting_max_m": 546000, + "northing_max_m": 4162000 + }, + "shared_edge": { + "center_corner_touch": "ne", + "corner_touch_only": true, + "expected_bounds": { + "easting_min_m": 545000, + "northing_min_m": 4161000, + "easting_max_m": 546000, + "northing_max_m": 4162000 + } + }, + "passed": true, + "errors": [] + }, + { + "direction": "e", + "neighbor_tile_id": "gz_us_ca_pacifica_utm10n_e545_n4160", + "expected_bounds_utm_m": { + "easting_min_m": 545000, + "northing_min_m": 4160000, + "easting_max_m": 546000, + "northing_max_m": 4161000 + }, + "actual_bounds_utm_m": { + "easting_min_m": 545000, + "northing_min_m": 4160000, + "easting_max_m": 546000, + "northing_max_m": 4161000 + }, + "shared_edge": { + "center_edge": "east", + "neighbor_edge": "west", + "shared_easting_m": 545000, + "northing_span_m": [ + 4160000, + 4161000 + ] + }, + "passed": true, + "errors": [] + }, + { + "direction": "se", + "neighbor_tile_id": "gz_us_ca_pacifica_utm10n_e545_n4159", + "expected_bounds_utm_m": { + "easting_min_m": 545000, + "northing_min_m": 4159000, + "easting_max_m": 546000, + "northing_max_m": 4160000 + }, + "actual_bounds_utm_m": { + "easting_min_m": 545000, + "northing_min_m": 4159000, + "easting_max_m": 546000, + "northing_max_m": 4160000 + }, + "shared_edge": { + "center_corner_touch": "se", + "corner_touch_only": true, + "expected_bounds": { + "easting_min_m": 545000, + "northing_min_m": 4159000, + "easting_max_m": 546000, + "northing_max_m": 4160000 + } + }, + "passed": true, + "errors": [] + }, + { + "direction": "s", + "neighbor_tile_id": "gz_us_ca_pacifica_utm10n_e544_n4159", + "expected_bounds_utm_m": { + "easting_min_m": 544000, + "northing_min_m": 4159000, + "easting_max_m": 545000, + "northing_max_m": 4160000 + }, + "actual_bounds_utm_m": { + "easting_min_m": 544000, + "northing_min_m": 4159000, + "easting_max_m": 545000, + "northing_max_m": 4160000 + }, + "shared_edge": { + "center_edge": "south", + "neighbor_edge": "north", + "shared_northing_m": 4160000, + "easting_span_m": [ + 544000, + 545000 + ] + }, + "passed": true, + "errors": [] + }, + { + "direction": "sw", + "neighbor_tile_id": "gz_us_ca_pacifica_utm10n_e543_n4159", + "expected_bounds_utm_m": { + "easting_min_m": 543000, + "northing_min_m": 4159000, + "easting_max_m": 544000, + "northing_max_m": 4160000 + }, + "actual_bounds_utm_m": { + "easting_min_m": 543000, + "northing_min_m": 4159000, + "easting_max_m": 544000, + "northing_max_m": 4160000 + }, + "shared_edge": { + "center_corner_touch": "sw", + "corner_touch_only": true, + "expected_bounds": { + "easting_min_m": 543000, + "northing_min_m": 4159000, + "easting_max_m": 544000, + "northing_max_m": 4160000 + } + }, + "passed": true, + "errors": [] + }, + { + "direction": "w", + "neighbor_tile_id": "gz_us_ca_pacifica_utm10n_e543_n4160", + "expected_bounds_utm_m": { + "easting_min_m": 543000, + "northing_min_m": 4160000, + "easting_max_m": 544000, + "northing_max_m": 4161000 + }, + "actual_bounds_utm_m": { + "easting_min_m": 543000, + "northing_min_m": 4160000, + "easting_max_m": 544000, + "northing_max_m": 4161000 + }, + "shared_edge": { + "center_edge": "west", + "neighbor_edge": "east", + "shared_easting_m": 544000, + "northing_span_m": [ + 4160000, + 4161000 + ] + }, + "passed": true, + "errors": [] + }, + { + "direction": "nw", + "neighbor_tile_id": "gz_us_ca_pacifica_utm10n_e543_n4161", + "expected_bounds_utm_m": { + "easting_min_m": 543000, + "northing_min_m": 4161000, + "easting_max_m": 544000, + "northing_max_m": 4162000 + }, + "actual_bounds_utm_m": { + "easting_min_m": 543000, + "northing_min_m": 4161000, + "easting_max_m": 544000, + "northing_max_m": 4162000 + }, + "shared_edge": { + "center_corner_touch": "nw", + "corner_touch_only": true, + "expected_bounds": { + "easting_min_m": 543000, + "northing_min_m": 4161000, + "easting_max_m": 544000, + "northing_max_m": 4162000 + } + }, + "passed": true, + "errors": [] + } + ], + "stitching_decision": { + "ready_for_coordinate_based_neighbor_stitching": true, + "notes": [ + "All Ground Zero neighbor placeholders align on exact 1000m UTM boundaries.", + "Cardinal neighbors share full 1km edges with no gap or overlap.", + "Diagonal neighbors touch at corners only.", + "This verifies registry coordinates, not yet elevation seam continuity." + ] + } +} diff --git a/Docs/Terrain/GroundZeroNeighborEdges.md b/Docs/Terrain/GroundZeroNeighborEdges.md new file mode 100644 index 0000000..aeb371d --- /dev/null +++ b/Docs/Terrain/GroundZeroNeighborEdges.md @@ -0,0 +1,38 @@ +# Ground Zero Neighbor Edge Verification + +The Ground Zero tile and its eight immediate neighbor placeholders were checked +against the tile registry before any multi-tile stitching work. + +## Inputs + +- Registry: `Data/Tiles/ground_zero_tiles.json` +- Verification script: `Scripts/verify_ground_zero_neighbor_edges.py` +- Output: + `Data/Terrain/Analysis/gz_us_ca_pacifica_utm10n_e544_n4160/gz_us_ca_pacifica_utm10n_e544_n4160_neighbor_edge_verification.json` + +## Result + +The registry coordinate pass succeeded. + +- Ground Zero tile: + `gz_us_ca_pacifica_utm10n_e544_n4160` +- Ground Zero bounds: + `544000,4160000 -> 545000,4161000` in EPSG:26910 / UTM zone 10N. +- Verified tile count: `9` +- Found neighbor count: `8` +- All tiles use `1000 m x 1000 m` bounds. +- All tile IDs match their minimum UTM kilometer coordinates. +- North, south, east, and west neighbors share exact full 1 km edges with the + Ground Zero tile. +- Northeast, southeast, southwest, and northwest neighbors touch Ground Zero at + corners only. + +## Stitching Decision + +The registry is ready for coordinate-based neighbor stitching around Ground +Zero. This pass verifies tile bounds and adjacency only; it does not verify +elevation seam continuity because neighbor DEM extraction has not been run yet. + +The next terrain stitching pass should extract or generate the relevant +neighbor DEMs, compare edge sample elevations, and then choose the seam blending +rule. diff --git a/Scripts/verify_ground_zero_neighbor_edges.py b/Scripts/verify_ground_zero_neighbor_edges.py new file mode 100644 index 0000000..fab8d53 --- /dev/null +++ b/Scripts/verify_ground_zero_neighbor_edges.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +"""Verify Ground Zero neighbor tile edges before multi-tile stitching.""" + +from __future__ import annotations + +import json +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +REGISTRY_PATH = REPO_ROOT / "Data/Tiles/ground_zero_tiles.json" +OUTPUT_PATH = ( + REPO_ROOT + / "Data/Terrain/Analysis/gz_us_ca_pacifica_utm10n_e544_n4160/" + / "gz_us_ca_pacifica_utm10n_e544_n4160_neighbor_edge_verification.json" +) + +GROUND_ZERO_TILE_ID = "gz_us_ca_pacifica_utm10n_e544_n4160" +TILE_SIZE_M = 1000 + +DIRECTION_OFFSETS = { + "n": (0, 1), + "ne": (1, 1), + "e": (1, 0), + "se": (1, -1), + "s": (0, -1), + "sw": (-1, -1), + "w": (-1, 0), + "nw": (-1, 1), +} + + +def load_registry() -> dict: + return json.loads(REGISTRY_PATH.read_text()) + + +def grid(tile: dict) -> dict: + return tile["grid"] + + +def expected_tile_id(tile_family: str, easting_min_m: int, northing_min_m: int) -> str: + return f"{tile_family}_e{easting_min_m // 1000}_n{northing_min_m // 1000}" + + +def verify_tile_shape(tile: dict) -> list[str]: + errors: list[str] = [] + g = grid(tile) + width = g["easting_max_m"] - g["easting_min_m"] + height = g["northing_max_m"] - g["northing_min_m"] + + if width != TILE_SIZE_M: + errors.append(f"width is {width}m, expected {TILE_SIZE_M}m") + if height != TILE_SIZE_M: + errors.append(f"height is {height}m, expected {TILE_SIZE_M}m") + if g["tile_size_m"] != TILE_SIZE_M: + errors.append(f"tile_size_m is {g['tile_size_m']}, expected {TILE_SIZE_M}") + if g["utm_zone"] != "10N": + errors.append(f"utm_zone is {g['utm_zone']}, expected 10N") + if g["projection"] != "WGS84 / UTM zone 10N": + errors.append(f"projection is {g['projection']}, expected WGS84 / UTM zone 10N") + + tile_family = "_".join(tile["tile_id"].split("_")[:-2]) + expected_id = expected_tile_id(tile_family, g["easting_min_m"], g["northing_min_m"]) + if tile["tile_id"] != expected_id: + errors.append(f"tile_id is {tile['tile_id']}, expected {expected_id}") + + return errors + + +def expected_bounds(center_grid: dict, dx: int, dy: int) -> dict: + easting_min = center_grid["easting_min_m"] + dx * TILE_SIZE_M + northing_min = center_grid["northing_min_m"] + dy * TILE_SIZE_M + return { + "easting_min_m": easting_min, + "northing_min_m": northing_min, + "easting_max_m": easting_min + TILE_SIZE_M, + "northing_max_m": northing_min + TILE_SIZE_M, + } + + +def verify_neighbor_edge(direction: str, center: dict, neighbor: dict) -> dict: + center_grid = grid(center) + neighbor_grid = grid(neighbor) + dx, dy = DIRECTION_OFFSETS[direction] + expected = expected_bounds(center_grid, dx, dy) + + errors: list[str] = [] + for key, expected_value in expected.items(): + actual_value = neighbor_grid[key] + if actual_value != expected_value: + errors.append(f"{key} is {actual_value}, expected {expected_value}") + + if direction == "n": + shared_edge = { + "center_edge": "north", + "neighbor_edge": "south", + "shared_northing_m": center_grid["northing_max_m"], + "easting_span_m": [center_grid["easting_min_m"], center_grid["easting_max_m"]], + } + if neighbor_grid["northing_min_m"] != center_grid["northing_max_m"]: + errors.append("north neighbor south edge does not meet Ground Zero north edge") + elif direction == "s": + shared_edge = { + "center_edge": "south", + "neighbor_edge": "north", + "shared_northing_m": center_grid["northing_min_m"], + "easting_span_m": [center_grid["easting_min_m"], center_grid["easting_max_m"]], + } + if neighbor_grid["northing_max_m"] != center_grid["northing_min_m"]: + errors.append("south neighbor north edge does not meet Ground Zero south edge") + elif direction == "e": + shared_edge = { + "center_edge": "east", + "neighbor_edge": "west", + "shared_easting_m": center_grid["easting_max_m"], + "northing_span_m": [center_grid["northing_min_m"], center_grid["northing_max_m"]], + } + if neighbor_grid["easting_min_m"] != center_grid["easting_max_m"]: + errors.append("east neighbor west edge does not meet Ground Zero east edge") + elif direction == "w": + shared_edge = { + "center_edge": "west", + "neighbor_edge": "east", + "shared_easting_m": center_grid["easting_min_m"], + "northing_span_m": [center_grid["northing_min_m"], center_grid["northing_max_m"]], + } + if neighbor_grid["easting_max_m"] != center_grid["easting_min_m"]: + errors.append("west neighbor east edge does not meet Ground Zero west edge") + else: + shared_edge = { + "center_corner_touch": direction, + "corner_touch_only": True, + "expected_bounds": expected, + } + + return { + "direction": direction, + "neighbor_tile_id": neighbor["tile_id"], + "expected_bounds_utm_m": expected, + "actual_bounds_utm_m": { + "easting_min_m": neighbor_grid["easting_min_m"], + "northing_min_m": neighbor_grid["northing_min_m"], + "easting_max_m": neighbor_grid["easting_max_m"], + "northing_max_m": neighbor_grid["northing_max_m"], + }, + "shared_edge": shared_edge, + "passed": not errors, + "errors": errors, + } + + +def main() -> None: + registry = load_registry() + tiles_by_id = {tile["tile_id"]: tile for tile in registry["tiles"]} + center = tiles_by_id[GROUND_ZERO_TILE_ID] + + tile_shape_checks = [] + for tile in registry["tiles"]: + errors = verify_tile_shape(tile) + tile_shape_checks.append( + { + "tile_id": tile["tile_id"], + "passed": not errors, + "errors": errors, + } + ) + + neighbor_checks = [] + missing_neighbors = [] + for direction in ["n", "ne", "e", "se", "s", "sw", "w", "nw"]: + neighbor_id = center["neighbors"].get(direction) + if not neighbor_id or neighbor_id not in tiles_by_id: + missing_neighbors.append({"direction": direction, "neighbor_tile_id": neighbor_id}) + continue + neighbor_checks.append(verify_neighbor_edge(direction, center, tiles_by_id[neighbor_id])) + + passed = ( + not missing_neighbors + and all(check["passed"] for check in neighbor_checks) + and all(check["passed"] for check in tile_shape_checks) + ) + + result = { + "schema_version": 1, + "tile_id": GROUND_ZERO_TILE_ID, + "registry_path": str(REGISTRY_PATH.relative_to(REPO_ROOT)), + "grid_scheme": registry["grid_scheme"], + "verified_tile_count": len(registry["tiles"]), + "expected_neighbor_count": 8, + "found_neighbor_count": len(neighbor_checks), + "all_passed": passed, + "missing_neighbors": missing_neighbors, + "tile_shape_checks": tile_shape_checks, + "neighbor_edge_checks": neighbor_checks, + "stitching_decision": { + "ready_for_coordinate_based_neighbor_stitching": passed, + "notes": [ + "All Ground Zero neighbor placeholders align on exact 1000m UTM boundaries.", + "Cardinal neighbors share full 1km edges with no gap or overlap.", + "Diagonal neighbors touch at corners only.", + "This verifies registry coordinates, not yet elevation seam continuity.", + ], + }, + } + + OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + OUTPUT_PATH.write_text(json.dumps(result, indent=2) + "\n") + print(f"Wrote {OUTPUT_PATH.relative_to(REPO_ROOT)}") + print( + json.dumps( + { + "all_passed": result["all_passed"], + "verified_tile_count": result["verified_tile_count"], + "found_neighbor_count": result["found_neighbor_count"], + "missing_neighbors": result["missing_neighbors"], + }, + indent=2, + ) + ) + + +if __name__ == "__main__": + main()