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:
@@ -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)
|
||||
# =============================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user