Planning
Central to a Surveyor's toolbox, and the most dramatic way a player can change the city — literally building it.
Overview
The city is a continuous 3D world — no tile-based simulation grid underneath. Grids are a UI aid, drawn only in contexts where people reason spatially in units: building plots, city blocks, and the design-time surveying/road-sketching mode. They disappear in normal play.
Design-Time Surveying
- A dedicated top-down/ortho mode for terraforming and road layout, echoing SimCity 4’s survey tools.
- Shows a light construction grid plus terrain contour lines; road centerlines snap to grid + contour intersections with adjustable tolerance.
- Toggled off (or faded to near-invisible) once back in the free 3D camera / play mode.
Plots & Blocks
- A block is a polygon bounded by road-bundle frontage edges, not a fixed tile region.
- A parcel/lot subdivides a block along its frontage, sized in world units (cm, as plain
float64— see Coordinate precision), snapped to a light alignment grid rather than forced onto fixed tiles. - This keeps blocks irregular and organic, like real subdivided city blocks, while still giving players clean alignment guides while editing.
- Default alignment grid is a 10m cell with a 5m snap tolerance.
Road-bundle frontage edges are defined in Arc-Spline Transport.
Parcel Subdivision
Subdivision co-generates parcels with the road graph so every buildable lot has street access and neighboring lots do not claim the same ground. The pipeline follows Chen, Song & Ortner's co-generation idea adapted to our stack: never emit overlapping candidates and repair them afterward — if two parcels would share an interior point, the generator that produced both is wrong.
Closed blocks
When frontage edges form a closed loop, the network yields a
block. The block interior subdivides along frontage
(extractBlocks → subdivideBlock). Each
child inherits or receives a frontage edge; landlocked children are
not emitted.
Open networks & junctions
Roads that do not enclose a loop subdivide
road-facing: each side of a centerline gets a strip of
parcels along its offset frontage
(subdivideRoadFrontage). Where two or more open roads
share a centerline endpoint, independently subdivided strips would
leave a gap or overlap at the corner. Before ordinary frontage
subdivision runs, junction closure builds that
corner by construction:
-
For each interior corner between angularly adjacent offset arms,
emit a wedge parcel whose outline is the two
frontage offsets plus the corner that joins them
(
subdivisionMode: "junction"). -
Trim each road's ordinary frontage by the arc-length those wedges
already cover (
trimStart/trimEnd), so the first road-facing parcel meets the wedge at the miter rather than overlapping it.
Wedge reach is capped at the ray-intersection (miter) distance between the two frontage lines (and further by half a typical frontage width at the call site). A fixed reach past the miter produces a self-intersecting bowtie.
Mid-span crossings (a stem meeting a through road's interior, or two through centerlines crossing away from shared endpoints) are the same algorithm with a different junction detector. Until that detector exists, pairs that are not closed by construction use a conservative drop-both safety net based on real polygon intersection — not a centroid-in-polygon heuristic, which misses long thin road-facing overlaps whose centroids lie outside each other.
Invariants
- Every parcel has street frontage (see Data Model).
- No generate → detect-overlap → delete → synthesize-patch loop at junctions.
- Contiguous single coverage around endpoint junctions (cross, Y, multi-way, tight corners at or above the minimum approach angle) is the acceptance bar; geometry optimization may refine vertices only after topology is already valid.
Data Model
| Entity | Fields |
|---|---|
| Block | boundary polyline, frontage edges[] |
| Parcel | polygon, basePolygon (immutable raw lot boundary), blockId, frontageEdgeId, zoneType, density, frontSetback, sideSetback, rearSetback, maxLotCoverage, parkingMinimum, subdivisionMode ("road-facing" | "block-based" | "junction") |
| Plat | id, status, roadIds, blocks, parcels, zoneIntent, gridCell (world-space alignment, default 10m), footprintGridCell (model-space construction snapping, default 50cm) |
Every parcel must have street frontage to be buildable and valuable. Landlocked parcels (no street access) are legally and economically unusable. Therefore,
frontageEdgeIdmust never be null or undefined.
Parcel Constraints & Construction Grid
The dedicated Parcels surveyor tool allows players to inspect and customize individual platted lots without affecting the surrounding road network or triggering zoning actions.
- Local Construction Grid: When
maxLotCoverage < 1.0, building footprints are anchored flush against frontage and snapped to a 50cm grid cell in parcel-local model space (canonical rectangle) before 3D extrusion and mapping back to world coordinates. - Bowtie Elimination: Each parcel records its immutable
basePolygonfrom initial subdivision. Setback edits recalculate one-shot against this base geometry using bisection scaling if requested insets would cause self-intersecting polygon folds. - Inside-Curve Radial Clamping: Along curved roads, rear lot lines converge radially toward the center of curvature; depth is clamped safely short of the focal singularity ($R - 50\text{cm}$) to prevent ray-crossing.
Saved Plans
A plan is a named, saved set of sketched transport networks — a snapshot the player can restore into the current sketch rather than a submitted plat. Distinct from a plat (the block/parcel/zoning document a Surveyor session submits): a plan only holds the transport-line sketches used to refit those curves later.
| Entity | Fields |
|---|---|
| Plan | id, name, createdAt, transportNetworks (map of transport type → TransportNetwork[]) |
| TransportNetwork | id, polyline (raw sketched centerline), roadClass, speedLimit, evenLanes, oddLanes, grades (terrain-aware evaluation), terrainSnapshot (baseline heightmap state) |
| Transport Type | road, rail |
Terrain Integration
Survey mode integrates with the terrain system to provide visual and cost-based feedback:
- Visualization: Terrain contours (10m interval), construction grid, elevation labels, and toggled slope ramp render beneath road sketches.
- Grade Evaluation: When a road sketch is committed, terrain grade is evaluated per-segment (max 6% freeway, 8% arterial, 12% local). Violations flagged; total cost adjusted for slope.
- Terrain Snapshot: Plan stores a compressed baseline heightmap state at creation time, enabling consistent re-rendering if terrain regenerates and supporting undo/redo within survey sessions.
- Camera Easing: Entering survey mode smoothly eases camera from orbital perspective to fixed ortho top-down (500ms ease). Exiting eases back to prior orbital pose (target + yaw/pitch).
Undo/Redo in Survey Mode
Full undo/redo stack using Memento pattern (immutable snapshots + caretaker history):
- Each sketch commit creates a memento capturing plan state + terrain snapshot.
- Memento creation debounced every 500ms using fastest available wasm compression (jsr).
- History limited to 30 entries to prevent unbounded memory growth.
- Keyboard shortcuts: Ctrl+Z (undo), Ctrl+Y (redo).
- Undo reverts sketches and restores terrain snapshots; redo re-applies with grade re-evaluation.
Future Work
- Parking Lot Geometry & Rendering: Visualizing and reserving leftover space behind building footprints as parking lots (tracked in Parking Tool).
- Parking Minimum Validation: Validating that unbuilt lot area accommodates required parking stalls.
- Multi-Parcel Bulk Editing: Copy/pasting setback and coverage constraints across multiple selected parcels.
- 3D Viewport Grid Visualization: Overlaying construction grid lines on selected parcels in the 3D viewport.
- Height / Story Caps: Adding per-parcel
maxFloorsconstraints into building massing heuristics. - Zoning Rule Integration: Defaulting setbacks, coverage, and parking minimums from municipal zoning codes.