feat(client): render province boundaries on planetary map (#927, D-205)

Export pre-computed drainage basin boundaries from atlas_province_boundaries
into markers.json during generate_atlas. AtlasMarkerOverlay draws province
polylines with semi-transparent fill under the political_zones overlay.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 16:12:59 +02:00
co-authored by Claude Opus 4.6
parent d869435eee
commit 13259dbfd5
2 changed files with 80 additions and 2 deletions
@@ -69,9 +69,9 @@ func _draw() -> void:
else:
draw_rect(Rect2(Vector2.ZERO, Vector2(tex_w, tex_h)), Color(0.05, 0.07, 0.10, 1.0))
# Political zone tint (currency zone band — single tint over whole body for MVP)
# Political zones — province boundaries from drainage analysis
if viewer.is_overlay_visible("political_zones"):
draw_rect(Rect2(Vector2.ZERO, Vector2(tex_w, tex_h)), COLOR_POLITICAL)
_draw_province_boundaries(markers)
# Infrastructure (roads + rail)
if viewer.is_overlay_visible("infrastructure"):
@@ -238,6 +238,39 @@ static func _city_key(city: Dictionary) -> String:
return "h:%d" % city.hash()
# =============================================================================
# Province boundaries (D-205, #927)
# =============================================================================
const COLOR_PROVINCE_BORDER: Color = Color(0.45, 0.65, 0.85, 0.55)
const COLOR_PROVINCE_FILL: Color = Color(0.25, 0.45, 0.65, 0.08)
const PROVINCE_BORDER_WIDTH: float = 1.2
func _draw_province_boundaries(markers: Dictionary) -> void:
var provinces: Array = markers.get("provinces", [])
if provinces.is_empty():
draw_rect(Rect2(Vector2.ZERO, Vector2(viewer.get_heightmap_texture().get_width(), viewer.get_heightmap_texture().get_height())), COLOR_POLITICAL)
return
for prov: Dictionary in provinces:
var path: Array = prov.get("path", [])
if path.size() < 3:
continue
var points: PackedVector2Array = _province_path_to_canvas(path)
if points.size() >= 3:
draw_colored_polygon(points, COLOR_PROVINCE_FILL)
draw_polyline(points, COLOR_PROVINCE_BORDER, PROVINCE_BORDER_WIDTH, true)
func _province_path_to_canvas(path: Array) -> PackedVector2Array:
var out: PackedVector2Array = PackedVector2Array()
for pt: Variant in path:
if pt is Array and pt.size() >= 2:
out.append(viewer.grid_to_canvas(Vector2(float(pt[1]), float(pt[0]))))
return out
# =============================================================================
# Overlay placeholders (populated by server side signals eventually)
# =============================================================================
+45
View File
@@ -1162,6 +1162,7 @@ def load_markers(body_dir: Path) -> dict:
"cities": [],
"railroads": [],
"pois": [],
"provinces": [],
}
@@ -1310,6 +1311,38 @@ def process_body(
return {"status": "generated", "markers": markers}
def query_province_boundaries(conn: sqlite3.Connection, body_id: str) -> list[dict]:
"""Read pre-computed province boundaries for a body from atlas_province_boundaries."""
rows = conn.execute(
"SELECT basin_id, path, area_pct FROM atlas_province_boundaries WHERE body_id = ? ORDER BY basin_id",
(body_id,),
).fetchall()
provinces = []
for row in rows:
provinces.append({
"basin_id": row[0],
"path": json.loads(row[1]),
"area_pct": row[2],
})
return provinces
def inject_provinces_into_markers(conn: sqlite3.Connection, body_id: str, body_dir: Path) -> bool:
"""Add provinces array to an existing markers.json. Returns True if written."""
markers_path = body_dir / "markers.json"
if not markers_path.exists():
return False
provinces = query_province_boundaries(conn, body_id)
if not provinces:
return False
with open(markers_path) as f:
markers = json.load(f)
markers["provinces"] = provinces
with open(markers_path, "w") as f:
json.dump(markers, f, indent=2)
return True
def main():
parser = argparse.ArgumentParser(
description="Atlas generation — terrain-aware city placement and infrastructure (#832)"
@@ -1406,6 +1439,18 @@ def main():
n_errors += 1
print(f" [{i+1}/{len(bodies)}] {body_id:20s} ERROR: {result.get('message', '')}")
# Inject province boundaries into markers.json for bodies that have them
if not args.dry_run:
n_provinces = 0
for body_info in bodies:
body_id = body_info["body_id"]
terrain_ref = body_info["terrain_reference"]
body_dir = REPO_ROOT / Path(terrain_ref).parent
if inject_provinces_into_markers(conn, body_id, body_dir):
n_provinces += 1
if n_provinces > 0:
print(f"\n Province boundaries injected into {n_provinces} markers.json files.")
if not args.dry_run:
# Stamp generator metadata (#855, #856) together with the atlas data
# in a single commit — atlas data + stamp land atomically, and the