diff --git a/client/ui/implant/apps/atlas/atlas_marker_overlay.gd b/client/ui/implant/apps/atlas/atlas_marker_overlay.gd index 399540be2..d02631d0d 100644 --- a/client/ui/implant/apps/atlas/atlas_marker_overlay.gd +++ b/client/ui/implant/apps/atlas/atlas_marker_overlay.gd @@ -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) # ============================================================================= diff --git a/tooling/planet-gen/generate_atlas.py b/tooling/planet-gen/generate_atlas.py index c12878ad1..66c3f87cc 100644 --- a/tooling/planet-gen/generate_atlas.py +++ b/tooling/planet-gen/generate_atlas.py @@ -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