Enhance README with detailed testing instructions and clarify world generation process; implement chunk boundary handling in OsmWorldBuilder; integrate Vitest for testing in the web project; add theme toggle functionality and improve styling in the web interface.
This commit is contained in:
@@ -46,12 +46,21 @@ dotnet run --project src/TheLivingWorld.Api
|
||||
npm --prefix src/TheLivingWorld.Web run dev
|
||||
```
|
||||
|
||||
Tests:
|
||||
Tests — the backend under xUnit, the client under Vitest:
|
||||
|
||||
```bash
|
||||
dotnet test
|
||||
```
|
||||
|
||||
```bash
|
||||
npm --prefix src/TheLivingWorld.Web test
|
||||
```
|
||||
|
||||
The client tests cover the pure half of the renderer: geometry helpers, camera maths, layer ordering and the
|
||||
palettes. Modules that hold PixiJS values are deliberately kept out of them, which is why `layers.ts` imports
|
||||
`Container` as a type only and the container construction lives in `mapView.ts` — the ordering rules stay
|
||||
testable without a browser.
|
||||
|
||||
## How a world is made
|
||||
|
||||
1. **Fetch.** `OverpassClient` posts one bounding-box query to Overpass and streams the response into
|
||||
@@ -63,6 +72,9 @@ dotnet test
|
||||
3. **Import.** `OsmWorldBuilder` reads each element's tags, decides what it is, and creates one ECS entity per
|
||||
feature. Multipolygon relations are stitched into rings by `RingAssembler`; everything is clipped to the
|
||||
world square by `GeometryClipper`, so a highway crossing town does not drag geometry 40 km off the map.
|
||||
Lines are then cut again at chunk boundaries — a road that spans the map becomes one entity per chunk it
|
||||
crosses, which is what keeps a chunk's extent close to its own square. Neighbouring pieces overlap by a
|
||||
metre and a half so the seam is covered rather than left as a hairline gap.
|
||||
4. **Systems.** `ComputeBoundsSystem` fills each entity's extent, `AssignChunksSystem` buckets it into the
|
||||
chunk grid.
|
||||
5. **Export.** `ChunkExporter` walks the ECS world and writes one JSON file per chunk, plus an index.
|
||||
@@ -106,13 +118,43 @@ in wire format and streamed straight from disk.
|
||||
|
||||
## The client
|
||||
|
||||
`MapView` owns one scaled container holding five layers — land cover, water, road casings, road fills,
|
||||
buildings — so a building in one chunk never ends up under a park from the next. `Camera` is the only place
|
||||
the Y flip lives; everything else thinks in map coordinates.
|
||||
`MapView` owns one scaled container holding the layer stack from `layers.ts`, plus a screen-space layer for
|
||||
place names above it. `Camera` is the only place the Y flip lives; everything else thinks in map coordinates.
|
||||
|
||||
**Layers are global, not per chunk.** Every chunk paints into the same ordered set of containers rather than
|
||||
into a container of its own. That is what makes junctions correct: with per-chunk containers the ordering
|
||||
would only hold inside a chunk, so a side street loaded after a trunk road would paint straight over it
|
||||
wherever the two meet. Roads are sorted into importance bands — tunnel, minor, local, secondary, major,
|
||||
bridge — and within each band every casing goes down before any fill, so the fills merge into one continuous
|
||||
surface. Land cover gets the same treatment in three bands: zoning blocks, natural cover, then parks and
|
||||
pitches.
|
||||
|
||||
`ChunkManager` fetches chunks as the camera reaches them and drops their graphics once they are well out of
|
||||
view, keeping the parsed data cached so panning back is instant. Detail thins out as you zoom away: footpaths
|
||||
disappear first, then small buildings, and stroke widths gain a floor so hairlines stay visible.
|
||||
view, keeping the parsed data cached so panning back is instant. When a chunk is redrawn — a zoom step or a
|
||||
theme switch — the new set fades in over the old one rather than replacing it outright.
|
||||
|
||||
Detail thins out as you zoom away: footpaths disappear first, then small buildings, and stroke widths gain a
|
||||
floor so hairlines stay visible. At street level the map picks up the things that only read close up:
|
||||
|
||||
- buildings extrude, with walls drawn down from every footprint edge to a roof lifted by the building's height
|
||||
- footways, paths, steps and cycleways switch to dashed lines so they never read as pale streets
|
||||
- railways become a dark bed with light sleepers dashed over it
|
||||
- one-way streets grow chevrons pointing the way traffic runs
|
||||
- gentle bends in roads and watercourses are rounded off by Chaikin corner cutting; corners sharper than 50°
|
||||
are left alone, because a gridded town is full of genuine right angles
|
||||
|
||||
Place names are drawn in screen space so text keeps a constant size at every zoom, and the work is split in
|
||||
two. `labelPlacement.ts` decides *which* names to show: candidates are ranked — water bodies first, then
|
||||
arterials, then land cover, then side streets — and placed greedily, dropping anything that would overlap a
|
||||
label already placed, or any street name too long for the road it belongs to. Because a road is split across
|
||||
chunks, the pieces are folded back together by OSM id so a street gets one label rather than one per chunk.
|
||||
That pass runs on the same slow timer as chunk bookkeeping. `LabelLayer` then moves the chosen labels to
|
||||
follow the camera every frame, which is nearly free — without that split they lag a fast pan by up to a tenth
|
||||
of a second and snap back when the next placement lands.
|
||||
|
||||
Both palettes live in `theme.ts` and nothing else in the renderer names a colour. Switching theme changes the
|
||||
render profile key, which is the same signal a zoom change uses, so every loaded chunk redraws through the
|
||||
usual dissolve instead of a special case. The page chrome follows via a `data-theme` attribute.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -130,6 +172,10 @@ coordinates and is not committed.
|
||||
|
||||
- The Overpass response is parsed in one pass rather than streamed. Fine for the small towns this targets; a
|
||||
dense 20 km city would want a streaming reader.
|
||||
- Chunk assignment buckets a feature by the centre of its extent and never splits geometry, so a long road
|
||||
overhangs its chunk. The exported chunk bounds are widened to match and the client culls against those.
|
||||
- Buildings are flat footprints shaded by height. No 3D, no roofs.
|
||||
- Lines are split at chunk boundaries, but polygons are not: a large forest or landuse block still belongs
|
||||
whole to the chunk holding the centre of its extent and overhangs its neighbours. The exported chunk bounds
|
||||
are widened to match and the client culls against those. Splitting polygons too would risk hairline seams
|
||||
between the filled pieces.
|
||||
- Building extrusion is a flat fake — walls swept in one fixed direction, no perspective and no roof shapes.
|
||||
- Labels are placed along a straight line at the middle of a road, not curved along its path, so a name on a
|
||||
sharply bending street sits at the average angle rather than following it.
|
||||
|
||||
Reference in New Issue
Block a user