Engine & GUI Stack

three.js with WebGPU for the 3D world, Lit + plain HTML/CSS for the GUI shell.

Engine & Tooling

Split

State Management

A central store lives at game/src/state (extracted from game/src/simulation/state to top-level for clarity). This is the single source of truth for all game state (plans, terrain, camera, UI state). Lit components dispatch actions or read state via this store; no prop drilling, no scattered context providers.

Boundary

three.js owns the 3D canvas exclusively. Lit owns everything in the DOM overlay. The two talk through a small typed event/store bridge (selection, camera state, tool mode) — kept intentionally thin so either side can be iterated on independently.

WebGPU as a Hard Gate

WebGPU browser support is not negotiable — no WebGL fallback is planned. Target: Chrome, Firefox, and Edge on Windows/macOS/Linux. Safari is out-of-scope. This is acceptable for the target audience: resource-intensive city builders naturally attract players with modern systems that already have robust WebGPU support, and the performance benefits of compute shaders and procedural GPU generation justify the gate.

Coordinate System

The game engine is strictly z-up, right-handed: +z is up, +x and +y form the ground plane, yaw rotates around +z, and pitch tilts forward / backward with positive pitch looking up. Every coordinate triple carried by game state, simulation, and the render layer is in this frame; three.js (whose camera and lights default to y-up) is used as a z-up world here and the render layer never remaps axes to three's y-up convention. The ground plane's position attribute is a flat x/y quad at z=0, so THREE.PlaneGeometry matches the engine's frame directly with no mesh rotation.

This is a load-bearing invariant: rotating axes at the render layer (e.g. an "engine-to-three roll" to align with three's y-up default) breaks the lookAt math as soon as the camera has a non-zero z, because the position and the lookAt target end up in different frames. The render layer keeps both in the engine frame and lets lookAt orient the camera's local -Z along the engine forward.

Face Winding

Front faces are clockwise when viewed from the surface's outward normal. For a horizontal surface in the ground plane (a road, a parcel, a terrain tile), the outward normal is +z, so the index buffer must list the ring of vertices in CW order when projected onto the x/y plane and viewed from above (i.e. from +z). Three's computeVertexNormals and front-face culling both rely on this convention; getting the winding backwards inverts the normal and back-face culls the surface away from any camera looking down at it.

Coordinate Precision

Simulation state (splines, blocks, parcels, plats, and every other game data model) is authored and stored in cm, as plain JS number — a float64, with sub-nanometer absolute precision even hundreds of kilometers from the origin. There is no reason to narrow this for gameplay logic: CPU-side geometry and simulation math get the full precision float64 offers, at any city scale.

The risk is downstream, at the GPU vertex-buffer layer: WebGPU stores vertex data as float32, whose absolute precision degrades with distance from the origin (~1.2mm of error by 10km out). Uploading raw world-space cm coordinates straight to the GPU will visibly jitter or z-fight well before a city reaches "tens of kilometers" across.

Resolution: keep authoritative geometry in cm/float64 everywhere in simulation and other game data models; only marshal the data dependent on a render pass through a floating-origin (camera-relative / "relative to eye") transform immediately before the CPU→GPU handoff — recentering a local float32 origin near the camera or the currently-edited sketch. This is a standard technique for large worlds (Cesium, flight sims) and three.js's camera utilities can handle most of this translation layer. It's a rendering-layer transform, not a units change: nothing upstream of the GPU handoff ever deals in float32.

Why This Pairing

Browser Support

Browser Windows macOS Linux Mobile
Chrome Future (DLC)
Firefox Future (DLC)
Edge
Safari ❌ out-of-scope ❌ out-of-scope

Future Ideas (DLC)