From 7adfaa53a23c87c5c8b84869a2295fa785e5312a Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Mon, 5 Oct 2026 16:55:04 +0300 Subject: [PATCH] Phases till 4 implemented. Basic generation of the town is added --- src/features/editScene/AGENTS.md | 100 +- src/features/editScene/CMakeLists.txt | 6 + .../editScene/ProceduralRoadGeometry.md | 361 ++++ .../editScene/components/RoadGraph.hpp | 232 ++- .../editScene/demos/demo-taxi/PLAN.md | 403 +++- .../demos/demo-taxi/demo_scene_exterior.json | 45 +- .../demos/demo-taxi/screenshots/markings.png | Bin 0 -> 204909 bytes src/features/editScene/main.cpp | 83 +- .../editScene/roadlib/RoadGeometryLib.cpp | 577 ++++- .../editScene/roadlib/RoadGeometryLib.hpp | 57 + .../editScene/roadlib/RoadNetworkGen.cpp | 1042 +++++++++ .../editScene/roadlib/RoadNetworkGen.hpp | 254 +++ src/features/editScene/roadlib/TownGen.cpp | 382 ++++ src/features/editScene/roadlib/TownGen.hpp | 123 ++ .../editScene/systems/AnimationTreeSystem.cpp | 15 +- .../editScene/systems/AnimationTreeSystem.hpp | 3 + .../editScene/systems/CellGridSystem.cpp | 16 +- .../editScene/systems/CharacterSlotSystem.cpp | 30 +- .../editScene/systems/CharacterSlotSystem.hpp | 4 + .../systems/CharacterSpawnerSystem.cpp | 15 +- .../systems/CharacterSpawnerSystem.hpp | 3 + .../editScene/systems/CharacterSystem.cpp | 14 +- .../editScene/systems/CharacterSystem.hpp | 3 + .../editScene/systems/EditorUISystem.cpp | 7 + .../systems/EditorWaterPlaneSystem.cpp | 28 +- .../systems/EditorWaterPlaneSystem.hpp | 4 + .../editScene/systems/RoadRegionStore.cpp | 100 + .../editScene/systems/RoadRegionStore.hpp | 28 + src/features/editScene/systems/RoadSystem.cpp | 710 ++++++- src/features/editScene/systems/RoadSystem.hpp | 77 + .../editScene/systems/SceneSerializer.cpp | 106 + .../systems/TerrainPrefabSpawnerSystem.cpp | 16 +- .../systems/TerrainPrefabSpawnerSystem.hpp | 3 + .../editScene/systems/TerrainSystem.cpp | 7 + .../editScene/systems/TerrainSystem.hpp | 7 + .../editScene/systems/TownGenSystem.cpp | 516 +++++ .../editScene/systems/TownGenSystem.hpp | 73 + .../tests/road_geometry_overlap_test.cpp | 1856 ++++++++++++++++- src/features/editScene/ui/TerrainEditor.hpp | 462 +++- 39 files changed, 7658 insertions(+), 110 deletions(-) create mode 100644 src/features/editScene/demos/demo-taxi/screenshots/markings.png create mode 100644 src/features/editScene/roadlib/RoadNetworkGen.cpp create mode 100644 src/features/editScene/roadlib/RoadNetworkGen.hpp create mode 100644 src/features/editScene/roadlib/TownGen.cpp create mode 100644 src/features/editScene/roadlib/TownGen.hpp create mode 100644 src/features/editScene/systems/TownGenSystem.cpp create mode 100644 src/features/editScene/systems/TownGenSystem.hpp diff --git a/src/features/editScene/AGENTS.md b/src/features/editScene/AGENTS.md index 584c58f..1a70a44 100644 --- a/src/features/editScene/AGENTS.md +++ b/src/features/editScene/AGENTS.md @@ -51,6 +51,14 @@ cd build-vscode/src/features/editScene # Run only matching test steps (substring match on step name) TERRAIN_TEST_FILTER=terrainCompliance ./editSceneEditor --headless --run-terrain-tests=1 +# Generate a procedural town from the loaded scene's road graph, print +# the building count and exit (Phase 4 headless integration check; +# needs a scene with an active terrain + road graph): +./editSceneEditor --project demos/demo-taxi --editor \ + demo_scene_exterior.json --generate-town=town,7 +# --generate-town=[,] district: village | town | +# city-residential | city-center + # Demo: game-mode SceneScriptComponent inline-Lua event handling # (scene_loaded / prefab_loaded / custom "demo_pulse"; watch stdout for # [demo] lines and the cubes moving/scaling). Lives in @@ -324,8 +332,10 @@ cd demos/demo-taxi # Road wedge/segment self-intersection regression test (no scene needed, # also registered as CTest roadGeometryOverlapTest). Covers per-piece # A-B-C configs, junction probes (all wedges of 4-way/3-way-T/2-way -# nodes combined into one buffer, road+sidewalk combined) and dead-end -# cap assertions (node-end cap present, edge-midpoint end open): +# nodes combined into one buffer, road+sidewalk combined), dead-end +# cap assertions (node-end cap present, edge-midpoint end open) and +# the phase 2.5 probes (marking strip placement/gating, stop-line +# clear zones, decals, road-atlas UV bands and V tiling): ./road_geometry_overlap_test # ...or analyse one custom A-B-C configuration: ./road_geometry_overlap_test -5 0 0 0 5 0 5 0 5 @@ -562,7 +572,8 @@ spawners for small or non-streaming scenes. ### RoadSystem Per-terrain procedural roads (`RoadGraph` component, geometry built by -`roadlib/RoadGeometryLib` — see `ProceduralRoadGeometry.md`): +`roadlib/RoadGeometryLib`, graph-level generator services in +`roadlib/RoadNetworkGen` — see `ProceduralRoadGeometry.md`): - Wedge/segment meshes, physics colliders, terrain compliance fixups and navmesh dirty-marking are driven by the graph version; bump @@ -646,6 +657,89 @@ Per-terrain procedural roads (`RoadGraph` component, geometry built by band and ratcheted the road down, 2026-09-30 trench bug), while remaining excluded from the roads-to-terrain footprint — a bump under an apron lowers, never lifts. +- **Road markings, atlas UVs & decals** (phase 2.5, + `ProceduralRoadGeometry.md` §18): `RoadConfig::markingsEnabled` + + per-edge tri-state `RoadEdge::markingsMode` drive painted overlay + strips (double-solid/dashed centerline, dashed lane lines, solid + edge lines at the curbs, stop bars on the incoming half of junction + arms) built by `buildWedgeMarkings`/`buildSegmentMarkings` 2 cm + above the road top; junction interiors stay unpainted, 2-way corners + paint through. `RoadConfig::textureTileLength` > 0 switches all + road/sidewalk/apron strips to atlas UVs (U bands 0-0.5/0.5-0.75/ + 0.75-1.0, V tiling per tile length); 0 keeps legacy meter UVs. The + generated `RoadAtlasTexture` placeholder uses anisotropic asphalt + noise (streaks along the lane) with default mipmaps. + `RoadEdge::decals` (`RoadEdgeDecal`: edgeT + lateral + size + atlas + rect) paints flat quads via `buildEdgeDecals`, per-edge so they + never land on junctions. Overlays build into separate `_markings`/ + `_decals` chunk meshes with unlit CULL_NONE materials + (`markingMaterialName`/`decalMaterialName`, auto-created when + missing) and no colliders; all of it is hashed into the page + content signature. +- **Road network generator, crossings, lots & streets** (taxi demo + phase 3, `roadlib/RoadNetworkGen.hpp/.cpp`, + `ProceduralRoadGeometry.md` §19): graph-level services, all pure + `RoadGraph` functions exercisable headlessly. `findCrossings()`/ + `resolveAllCrossings()` turn edge crossings into proper junctions + (end-node reuse within `RoadConfig::crossingSnapDistance`, sliver + collapse via `RoadGraph::mergeNodes`, edge splits; pairs separated + by more than `crossingHeightThreshold` in Y are overpasses and left + alone). `findLots()` enumerates enclosed faces (planar face walk, + sharpest right turn from the incoming heading with the U-turn edge + excluded) for building lots, filtered by `RoadLotFilter` (default: + 45 m min size on bbox + area, max 8 boundary edges) and extended + with square fallback lots grown off edge sides that border no + accepted face (`RoadLot::edgeFallback`); road edit mode draws all + potential lots as dark grey outline loops + (`RoadSystem::buildLotOutlines`); `RoadGraph::streets` + + `nameStreet()`/`assignLotAddresses()` give streets and street+number + lot addresses ("Lot N" fallback). `generateGrid()` emits a seeded + deterministic settlement grid (jittered interior nodes, water/slope + rejection via a height sampler) stamped with `generatedGroupId` — + the editor refuses Remove/Split on grouped nodes/edges, a group is + removed wholesale (`RoadSystem::clearGenerated`) and hooks into a + manual road via `connectGroupToNode()`. Editor UI: terrain panel + "Road Network Generator" section (`RoadSystem::generateRoadNetwork` + — terrain heights + `WaterPhysics` water level — and + `resolveCrossingsNow`), with a "Pick Origin on Terrain" button that + arms a one-shot terrain click for the origin + (`RoadSystem::armOriginPick`/`takePickedOrigin`, consumed by + `EditorUISystem::handleRoadEditClick`) and a "Preview Grid" checkbox + that draws the would-be grid as orange lines without touching the + graph (`RoadSystem::updateGenPreview`/`clearGenPreview` — throwaway + graph, refreshed on parameter change); the edge inspector assigns + streets. Streets + persist inline in the scene JSON when not streaming, in + `roads/streets.json` when streaming; `generatedGroupId` rides both + the inline node JSON and the region files. +- **Procedural town generation** (taxi demo phase 4, + `roadlib/TownGen.hpp/.cpp` + `systems/TownGenSystem.hpp/.cpp`, + `ProceduralRoadGeometry.md` §20): `TownGen::generateTown()` (pure, + headless) turns `findLots()` lots into buildings — district presets + (village/town/city-residential/city-center: footprint, floors, + density, setback), seeded deterministic draws, frontage ranking + (street edge > fallback source edge > longest edge; fallback lots + match their source edge geometrically via `RoadLot::sourceEdgeA`/ + `sourceEdgeB` — their polygon winding may be reversed, never assume + `polygon[0]-polygon[1]` is the source edge), foundation block seated + at the highest terrain corner, entrance stairs when the door ground + drops > 0.35 m, sequential street addresses, curbside pickup POIs, + water-level rejection. Lots are shrunk off the road body before + placement (`shrinkLotOffRoads()`: the centerline-bounded lot polygon + is inset per bounding edge by lanes + that side's sidewalk + apron + + 0.3 m; lots whose inset collapses are skipped). `TownGenSystem` + (owned by `TerrainSystem`) + materializes the result: a `"TownGen seed "` container + (found by name prefix) with a `TownComponent`, one shared + ProceduralTexture/Material pair, and per building an exterior-only + CellGrid shell (front door with `doorsEnabled = false` + placeholder + `town_interior.json` scene-switch path for Phase 8) with a flat + full-footprint `RoofComponent` child (`posY = floors` — + `CellGridSystem::buildRoofs()` only processes `RoofComponent` + children of the grid entity) and serialized + Primitive-box foundation/stairs children (Primitive children survive + save/load; raw TriangleBufferComponent buffers don't). Editor UI: + terrain panel "Town Generator" section. - **Chunked page meshes**: page assembly splits the page geometry into chunk meshes capped at `ROAD_PAGE_CHUNK_MAX_VERTICES` (60000) vertices, split on piece boundaries — each chunk gets its own mesh, static Jolt diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index b46eb39..3b8fbc1 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -31,6 +31,7 @@ set(EDITSCENE_SOURCES systems/SpawnerRegionStore.cpp systems/RoadRegionStore.cpp systems/RoadSystem.cpp + systems/TownGenSystem.cpp systems/LightSystem.cpp systems/CameraSystem.cpp systems/LodSystem.cpp @@ -828,6 +829,10 @@ add_test(NAME globalStateTest add_library(RoadGeometryLib STATIC roadlib/RoadGeometryLib.cpp roadlib/RoadGeometryLib.hpp + roadlib/RoadNetworkGen.cpp + roadlib/RoadNetworkGen.hpp + roadlib/TownGen.cpp + roadlib/TownGen.hpp ) target_include_directories(RoadGeometryLib PUBLIC @@ -837,6 +842,7 @@ target_include_directories(RoadGeometryLib PUBLIC target_link_libraries(RoadGeometryLib PUBLIC OgreMain OgreProcedural::OgreProcedural + nlohmann_json::nlohmann_json ) # --------------------------------------------------------------------------- diff --git a/src/features/editScene/ProceduralRoadGeometry.md b/src/features/editScene/ProceduralRoadGeometry.md index 87bd64f..5cd2f71 100644 --- a/src/features/editScene/ProceduralRoadGeometry.md +++ b/src/features/editScene/ProceduralRoadGeometry.md @@ -526,6 +526,16 @@ boundary chord; `solvePinKernel` iterates Kaczmarz projections from the road miter corner seed, retrying once with the zone extended to the whole interior when the tight zone's kernel is empty. +**Memoization.** The scan is O(rows^2) per calling chain row, so +`filletMiterPinZoneRaw` memoizes it per (wedge endpoints, node +position, fillet shape, chain config) in a function-static cache (128 +entries, cleared on overflow; road building is main-thread +synchronous). The entry round-trips the FULL pin point — an earlier +version cached only X/Z, and on cache hits the pinning curb offset +`K - center` inherited an uninitialized Y, which sent marking rows on +pin-zone boundaries to y ~ -1.5e6 at runtime (2026-10-02 spike +regression; covered by the suite's pin-zone cache round-trip probe). + **Impossible-corner detection.** A converged kernel only proves the fan triangles are consistently oriented around the pin point P — when the boundary cross-sections interpenetrate, the chain sweeps more than @@ -1278,3 +1288,354 @@ never fills up to meet the apron. Residual cosmetic limitation: on ground sitting just below road top the flat bench leaves the terrain flush with the road top across the whole band, so the terrain can poke through the apron mid-slope — reads as a natural curb cut. + +## 18. Road Markings, Atlas UVs and Decals (M5.19, taxi demo phase 2.5) + +Painted markings, road-atlas UVs and flat decal quads, all generated by +the same builders as the road geometry so hand editing and procedural +generators get them for free. + +### 18.1 Marking kinds and placement rules + +`RoadConfig::markingsEnabled` (default on) gates everything; each edge +carries a tri-state `RoadEdge::markingsMode` (Inherit/On/Off, same +pattern as the sidewalk modes) folded by +`RoadGraph::edgeMarkingsEnabled()`. Four overlay buffers are built +(`RoadMarkingBuffers{centerline, laneLines, edgeLines, stopLines}`) by +`buildWedgeMarkings()` per wedge and `buildSegmentMarkings()` per +dead-end straight segment: + +- **Centerline**: separates the travel directions. 2+ lanes per + direction -> double solid (two `markingLineWidth` strips with a + one-line gap between them, at lat [-g-w, -g] and [g, g+w], + g = w/2); 1+1 -> a single dashed line at lat 0. Resolved from the + resolved lane counts, not configurable. +- **Lane lines**: dashed lines between same-direction lanes (N lanes + per side -> N-1 lines at lat k * laneWidth on that side). +- **Edge lines**: solid strips at both curbs, inset + `markingEdgeInset` from the curb (curb-relative, so they follow the + widened curb chain through fillets and tapers). +- **Stop lines**: junctions (degree > 2) only, one perpendicular bar + across the incoming half of each arm, `markingStopSetback` past the + fillet/clear boundary and `markingStopWidth` deep. The bar starts + flush at the centerline strip's OUTER edge (lat 1.5*w double solid, + 0.5*w dashed) and the arm's longitudinal lines resume past the bar's + far edge — sharing only boundary lines, so nothing coplanar-overlaps + (the combined-overlay overlap check enforces this). + +**Junction suppression**: at degree > 2 nodes all longitudinal lines +stop at the clear boundary (fillet reach or junction half-width, plus +the stop setback on the incoming side) and the wedge interior stays +unpainted. At 2-neighbor nodes there is no clear zone and the +markings follow the corner through. Dashes phase from the edge +midpoint (wedge path d = 0), so up to one dash gap next to any node +may legitimately be unpainted. + +**Pin zones**: corner pin zones (§5.10) are subtracted from every +marking range — the pinched cross-section there cannot host paint. +The rows landing exactly on a zone boundary pin at the shared kernel +pin point just like the road surface's own boundary row, so the +paint edge meets the fan seam exactly. + +Geometry: strips are emitted as quad rows (`ROAD_MARKING_STEP` 0.5 m) +between `sampleWedgeMarking()` samples (center on the wedge centerline +at paint height = road top + `ROAD_MARKING_LIFT` 0.02, lateral +direction and curb distance from `computeCurbOffset`), so they follow +fillets, lane tapers and height profiles exactly. Rows whose strip +would hang past the curb are skipped. The paper-thin quads wind +downward; the overlay material is CULL_NONE, so this is intentional. + +### 18.2 Road atlas UVs + +`RoadConfig::textureTileLength` > 0 switches every road/sidewalk/apron +strip to atlas UVs (0 keeps the legacy meter UVs: U = along-road +meters, V = lateral meters). U runs ACROSS the surface and selects +the band of the shared square road atlas: 0.0-0.5 road-top asphalt, +0.5-0.75 sidewalk, 0.75-1.0 apron; each strip maps its full width into +its band (the road band is additionally split so the lanesIn-side curb +starts at U = 0.25 within the band — symmetric widening convention). +V runs along the travel direction and tiles once per +`textureTileLength` world meters, phase-continuous across the edge +midpoint (`atlasV()`), so long edges stop smearing the texture. +`RoadSystem::syncRoadAtlasTextureUnit()` swaps the road material's +texture to the generated `RoadAtlasTexture` placeholder while tiling +is on. The placeholder's asphalt band uses anisotropic noise (16-px +runs along image Y, the along-road V axis) so the grain reads as +streaks along the lane, and keeps default mipmaps so distant +minification blurs instead of throwing cross-road moire straps. + +### 18.3 Decals + +`RoadEdge::decals` is a serialized per-edge list of `RoadEdgeDecal` +quads: centre at `edgeT` along the edge plus `lateralOffset` meters +sideways (positive = right of A->B travel), `size` meters on both +axes, sampling the `(uvX, uvY, uvW, uvH)` rect of the shared +`RoadDecalAtlas` texture (generated placeholder: manhole, arrows, +damage patches). `buildEdgeDecals()` places each quad on the +piecewise road surface (road top + `ROAD_DECAL_LIFT` 0.025, so the +mid-edge kink is followed exactly). Decals are per-edge by +construction, so they never land on junction interiors; a cross-page +edge paints each half's decals on the owning page only (same ownership +split as the geometry). Pure visuals: no collider, no navmesh, no +compliance footprint. + +### 18.4 Integration + +Markings and decals are built into separate overlay chunk buffers +(`appendOverlay`/`flushOverlay`, same 60000-vertex chunk cap as the +page geometry, `_markings`/`_decals` mesh-name suffixes) with their +own materials and NO physics colliders — vehicles never feel the +paint. `RoadSystem::ensureOverlayMaterials()` creates the defaults +when missing: the marking material is unlit white (RTSS renders a +lighting-disabled pass white, which is exactly road paint), CULL_NONE, +depth write on; the decal material adds SBT_TRANSPARENT_ALPHA, depth +write off and the clamped `RoadDecalAtlas` texture unit. Both are +looked up by name (`RoadConfig::markingMaterialName` / +`decalMaterialName`), so a project can pre-author replacements. The +page content signature hashes all marking config fields, the per-edge +`markingsMode` and the decal lists, so any paint edit rebuilds exactly +the affected pages. + +Headless coverage: `road_geometry_overlap_test` probes +(`runMarkingSegmentProbe` dead-end content + per-edge gating, +`runJunctionMarkingCase` 4-way 1+1 / 4-way 2+2 / 3-way T clear zones, +stop bars, combined road+overlay overlap, `runMarkingTwoWayProbe` +corner continuation, `runDecalProbe` placement/UVs/ownership, +`runUVAtlasProbe` bands + V tiling + legacy mode). + +## 19. Road Network Generator, Crossings, Lots and Streets (taxi demo phase 3) + +Graph-level services for procedural settlement creation, living in +`roadlib/RoadNetworkGen.hpp/.cpp` (namespace `RoadNetworkGen`, part of +the `RoadGeometryLib` target). Everything is a pure function of +`RoadGraph` data plus an optional terrain height sampler, so the whole +namespace is exercisable headlessly. + +### 19.1 Crossing detection and resolution + +`findCrossings()` reports every edge pair whose closest points are +nearer (in XZ) than the sum of the edge half-widths (lanes + enabled +sidewalks) AND no further apart in Y than +`RoadConfig::crossingHeightThreshold` (default 10 m) — a bigger Y gap +is an overpass/underpass and is deliberately NOT a crossing. Edge +pairs sharing a node are skipped. + +`resolveCrossing()` turns one crossing into a proper junction. For +each of the two edges: an end node within +`RoadConfig::crossingSnapDistance` (default 5 m) of the crossing point +is reused, an edge too short to split (at most +`2 * ROAD_MIN_EDGE_LENGTH`) is collapsed via `RoadGraph::mergeNodes()` +into its end nearer to the crossing, anything else is split at the +crossing parameter. The two resulting junction nodes are then merged +into one, placed at the crossing midpoint. +`resolveAllCrossings()` re-detects after every fix (one fix per pass) +until the graph is clean. `checkSane()` verifies the structural +invariants afterwards: no duplicate edge pairs, no sliver edges below +`ROAD_MIN_EDGE_LENGTH`, no isolated nodes (it intentionally does NOT +run `RoadGraph::validate()`, whose wedge-angle rules reject +hand-authored legacy scenes). + +### 19.2 Lot detection + +`findLots()` enumerates the enclosed faces of the graph (potential +building lots) with a planar face walk: every directed edge is walked +taking the sharpest right turn at each node (largest positive angle +from the incoming heading in the atan2(z, x) sense), which yields +interior faces as positive-area XZ cycles. Two implementation rules +matter: + +- the turn is measured from the heading, not from the reverse + direction — a near-straight continuation is ~pi from the reverse + direction and wraps to ~-pi, which would otherwise win the extremum + and send the walk straight through 4-way junctions; +- the U-turn edge back to the previous node is excluded from the + search (its angle from the heading is exactly +-pi, i.e. it sits on + the wrap boundary and would be picked at random depending on float + rounding); it is only taken at dead ends, where it makes the walk + retrace the edge and get the face rejected. + +A face is rejected when its boundary walk traverses an edge twice or +revisits a node (a spur/cul-de-sac pokes into it), or when another +graph node lies strictly inside it — such areas are intersected by +road and are not clean lots. Survivors get a polygon, area and +area-weighted centroid. + +Acceptance filtering (`RoadLotFilter`, defaults aimed at building +lots): a face is discarded when its boundary has more than +`maxBoundaryEdges` (8) edges, when its area is below +`minLotSize`^2 (45 m -> 2025 m^2), or when its XZ bounding box is +narrower than `minLotSize` in either axis — a lot must fit a +45x45 m square. + +Fallback lots (`RoadLotFilter::edgeFallback`, on by default): every +directed edge side that does not already border an accepted face +(`coveredLeft` tracks the left side of each directed edge, the +CCW-face-interior side) grows a square lot with side = edge length +when the edge is at least `minLotSize` long. A candidate square is +rejected when any graph node lies strictly inside it (excluding the +source edge's endpoints), when any edge endpoint lies inside it or +properly crosses its boundary (touching/collinear does not count), or +when it overlaps an already accepted lot. The overlap test +(`polysOverlapXZ`) checks edge crossings plus CENTROID containment — +not vertex containment: a fallback square sharing a boundary vertex +with an accepted face ray-casts that vertex as "inside" +(`pointInPolyXZ` boundary rule) and would be falsely rejected. +Accepted squares get `edgeFallback = true`, empty `nodeIds` and a +vertex-mean centroid. Editor: potential lots are drawn as dark grey +outline loops in road edit mode (`RoadSystem::buildLotOutlines`, part +of the visual-aids overlay). + +### 19.3 Streets and addresses + +`RoadGraph::streets` holds named edge sequences (`RoadStreet{name, +edges}`; edges are normalized endpoint-id pairs, min first). +`nameStreet()` (re)defines a street, dropping its edges from any other +street; `findStreetForEdge()` looks up the street containing an edge. +`assignLotAddresses()` assigns each lot to its nearest street and +numbers lots sequentially along the street's direction; lots on no +street fall back to "Lot N". + +Streets persist inline in the scene JSON (`streets` array) for +non-streaming terrains and in `heightmaps//roads/streets.json` +next to the region files when streaming (loaded once per terrain in +`RoadSystem::syncRegions()` with `dropMissing = false` — pages are +never all in the graph at once, so entries whose edges are currently +unloaded are kept; written by `RoadSystem::flushRegionStore()`). + +### 19.4 Seeded grid generator + +`generateGrid()` emits `(blocksX+1) x (blocksZ+1)` intersections with +seeded jitter (mt19937) on interior nodes only (boundary stays put, so +settlements tile predictably), and edges along the grid lines. With a +height sampler: nodes below `waterLevel` are rejected (their edges go +with them) and edges steeper than `maxSlope` rise/run are skipped. +Every created node is stamped with a `generatedGroupId` (0 = manual); +the editor refuses Remove/Split on grouped nodes/edges — a group is a +single unit, removed wholesale via `removeGroup()`. +`connectGroupToNode()` connects a manual road node to the closest +group node ("as if the group were a road node") and resolves +crossings, which is how a settlement hooks into a hand-authored road. + +Editor integration: the terrain panel's "Road Network Generator" +section (origin, block counts/sizes, jitter, seed, avoid-water, max +slope, crossing resolution; Generate / Resolve Crossings Now / +per-group Remove buttons) wraps `RoadSystem::generateRoadNetwork()` +(heights from `TerrainSystem::getHeightAt`, water level from the +scene's `WaterPhysics` entity), `RoadSystem::resolveCrossingsNow()` +and `RoadSystem::clearGenerated()`. "Pick Origin on Terrain" arms a +one-shot terrain click (`RoadSystem::armOriginPick` / +`takePickedOrigin`; `EditorUISystem::handleRoadEditClick` consumes the +click before any road tool) that fills the origin fields. The +"Preview Grid" checkbox draws the would-be grid as orange lines +without touching the graph (`RoadSystem::updateGenPreview` runs +`generateGrid()` into a throwaway graph with the same terrain/water +inputs; refreshed on every parameter change, cleared by +`clearGenPreview()` on uncheck, generation, scene clear or terrain +switch). The edge inspector assigns the selected edge to a named +street. + +Headless coverage: `road_geometry_overlap_test` probes +(`runCrossingResolutionProbe` X junction/end-snap T/collapse/overpass +untouched, `runLotProbe` 2-block grid + spur rejection, +`runStreetProbe` naming/reassignment/JSON round trip/unique addresses + +unnamed fallback, `runGeneratorProbe` determinism, counts, water and +slope rejection, two settlements in one graph, group connect/remove). + +## 20. Procedural Town Generation (taxi demo phase 4) + +`TownGen` (`roadlib/TownGen.hpp/.cpp`, part of `RoadGeometryLib`, pure +— no Ogre scene dependencies beyond math types) turns the lots from +`RoadNetworkGen::findLots()` into a building list: +`generateTown(graph, TownParams, HeightSampler) -> TownResult`. + +### 20.1 Districts + +`District` presets (`districtParams()`): Village (1 floor, 6-10 m +footprints, density 0.6), Town (1-3 floors, 8-14 m, 0.75), +CityResidential (2-5 floors, 10-18 m, 0.85), CityCenter (4-8 floors, +12-22 m, 1.0). Each preset carries footprint range, floor range, +density, setback and a wall tint. `TownParams` = district + seed + +density scale + water level. + +### 20.2 Placement + +Per lot, in fixed seed-draw order (density, width, depth, floors — so +results are deterministic per seed): pick the **frontage** edge +(ranking: boundary edge on a named street > the fallback lot's source +edge > the longest boundary edge), build the lot frame (u along the +frontage, v from the frontage midpoint towards the centroid), inset a +setback rectangle, and place a width x depth footprint clamped to the +available space. Lots too small after setback are skipped +(`skippedTooSmall`); buildings whose floor would sit below +`waterLevel` are skipped (`skippedWater`). + +Because `findLots()` lot polygons run **centerline-to-centerline**, +the lot is first shrunk off the roads (`shrinkLotOffRoads()`): every +boundary edge matching a graph edge (geometric endpoint match, either +direction — covers enclosed faces and fallback source edges alike) is +offset inward by the road half-extent on the lot's side (all lanes x +`laneWidth` / 2 + the lot side's sidewalk + apron + a 0.3 m margin); +the inset vertices are the intersections of the consecutive offset +lines (near-parallel edge pairs average the two offset positions of +the shared vertex). Bounds and placement are measured from the inset +polygon in the lot frame — the buildable front line `vMin` is the +frontage pushed off the road, so the door, stairs and pickup POI stay +inside the lot. A lot whose inset collapses (degenerate edge or +reversed/vanished polygon) is skipped as too small. + +Edge-fallback lots (the squares `findLots()` grows off lone road +edges) have no `nodeIds` and their polygon winding may be reversed, +so the source edge is matched **geometrically** via the new +`RoadLot::sourceEdgeA`/`sourceEdgeB` node ids (endpoint positions, +0.01 eps, either direction) — never assume `polygon[0]-polygon[1]` +is the source edge. + +### 20.3 Foundation, stairs, addresses, POIs + +Uneven terrain is handled without terrain edits: the floor sits at +the **highest** terrain corner of the footprint and a foundation +block reaches below the lowest corner; when the door-side ground +(one meter in front of the front face) drops more than 0.35 m below +the floor, the building gets entrance stairs (`stairsDrop`). +Addresses come from the frontage street, numbered sequentially per +street in lot order ("Lot N" when the lot touches no street — the +fallback-lot source edge also resolves through +`findStreetForEdge()`). Every building gets a curbside pickup +`TownPoi` (later phases use these as taxi pickup/dropoff and +pedestrian origin/destination points). + +### 20.4 Scene integration + +`TownGenSystem` (`systems/TownGenSystem.hpp/.cpp`, owned by +`TerrainSystem` next to `RoadSystem`) materializes a `TownResult`: +a container entity named `"TownGen seed "` carrying a +`TownComponent` (shared procedural material reference), one shared +`ProceduralTexture` (named rects: wall/roof/foundation/door/winframe/ +floor/ceiling) + `ProceduralMaterial` pair, and per building an +EditorMarker entity with an exterior-only `CellGridComponent` shell +(perimeter windows, front-center door with `doorsEnabled = false` +and the placeholder `doorSceneSwitchPath = "town_interior.json"` — +Phase 8 enables the F1 scene switch) plus a `RoofComponent` child +(flat roof over the whole footprint, `posY = floors`, slab + trim on +top of the walls — `CellGridSystem::buildRoofs()` only processes +`RoofComponent` children of the grid entity), plus serialized +`Primitive` +box children for the foundation and stairs (Primitive children +survive save/load; a raw `TriangleBufferComponent` buffer does not) +and the pickup POI child. Placement convention: +`CellGridComponent::cellToWorld()` puts cell (x, y, z) at +`x*cellSize`, so a shell spans `[0, width*cellSize]` from its node — +`createBuilding()` shifts the building node so the shell lands +CENTERED on `TownBuilding::center` (front face at local +Z), and the +foundation/stairs/POI children position themselves in the same shell +coordinates. `clear()` / `countTowns()` find towns by +the `"TownGen "` name prefix. The terrain panel's "Town Generator" +section (district, seed, density, Generate/Clear) wraps it. + +Headless coverage: `runTownGenProbe` in +`road_geometry_overlap_test` — building/POI counts on a 12-lot grid, +floor-range per district, determinism, water rejection, slope +stairs (some but not all on a tilted grid), sequential Main Street +addresses including the fallback square, village single-floor, and +the road-clearance check (every building footprint corner stays at +least the road half-width off every edge centerline segment). diff --git a/src/features/editScene/components/RoadGraph.hpp b/src/features/editScene/components/RoadGraph.hpp index d230599..2270668 100644 --- a/src/features/editScene/components/RoadGraph.hpp +++ b/src/features/editScene/components/RoadGraph.hpp @@ -147,6 +147,68 @@ struct RoadConfig { */ float prefabSpawnDistance = 150.0f; float prefabDespawnDistance = 250.0f; + + /** + * Painted road markings (taxi demo phase 2.5). When enabled, the + * geometry builders emit marking strips — centerline (double solid + * for 2+ lanes per direction, single dashed for 1+1), dashed lines + * between same-direction lanes, solid edge lines at the curbs and + * stop lines on the incoming half of each arm before an + * intersection (nodes with more than two neighbors) — as separate + * overlay geometry slightly above the road top. Junction wedge + * surfaces stay unmarked; at 2-neighbor nodes the markings follow + * the corner through. Per-edge tri-state overrides: + * RoadEdge::markingsMode. + */ + bool markingsEnabled = true; + + /** Width of one painted line (centerline/lane/edge lines). */ + float markingLineWidth = 0.15f; + + /** Dash length and gap of dashed lines (centerline 1+1, lanes). */ + float markingDashLength = 3.0f; + float markingDashGap = 3.0f; + + /** Stop line width along travel and setback from the junction. */ + float markingStopWidth = 0.4f; + float markingStopSetback = 1.0f; + + /** Edge line inset from the curb toward the road center. */ + float markingEdgeInset = 0.25f; + + /** + * Materials for the marking and decal overlay chunks. Created + * procedurally by RoadSystem when missing (unlit white paint; + * alpha-blended decal atlas). + */ + std::string markingMaterialName = "RoadMarkingMaterial"; + std::string decalMaterialName = "RoadDecalMaterial"; + + /** + * Road texture V tiling (phase 2.5). 0 keeps the legacy UVs (U = + * along-road meters, V = lateral meters). When > 0 the road mesh + * UVs are regenerated as an atlas mapping: U runs ACROSS the + * surface (no tiling) and selects the band of the shared road + * atlas texture — 0.0-0.5 road-top asphalt, 0.5-0.75 sidewalk, + * 0.75-1.0 apron — while V runs along the travel direction and + * tiles once per this many world meters, so long edges stop + * smearing the texture. + */ + float textureTileLength = 0.0f; + + /** + * Crossing resolution (taxi demo phase 3, + * RoadNetworkGen::findCrossings/resolveCrossing). Two edges count as + * intersecting when the XZ distance between their closest points is + * at most the sum of their half-widths AND the closest points are at + * most crossingHeightThreshold apart in Y (a larger Y gap is an + * overpass/underpass and creates no junction). When the crossing + * point lies within crossingSnapDistance meters of an edge end the + * existing end node is reused for the junction instead of inserting + * a split node. + */ + float crossingSnapDistance = 5.0f; + float crossingHeightThreshold = 10.0f; }; /** @@ -187,6 +249,17 @@ struct RoadNode { * existing edges. */ int id = 0; + + /** + * Procedural-generation group membership (taxi demo phase 3). 0 is a + * manually authored node; a nonzero value marks the node as part of a + * generated road grid (RoadNetworkGen::generateGrid stamps every node + * it creates). Generated groups are treated as single unsplittable + * units: the editor refuses to move/remove/split grouped nodes and + * their interior edges, and the whole group can be removed or + * connected to manual road at once. + */ + uint32_t generatedGroupId = 0; }; /** @@ -257,6 +330,47 @@ enum class RoadSidewalkMode : int { Off = 2 /**< No sidewalk on this side even if globally enabled. */ }; +/** + * Per-edge road-marking tri-state (phase 2.5). + * + * Same folding rule as RoadSidewalkMode: Inherit follows the global + * RoadConfig::markingsEnabled, On/Off force markings for this edge + * regardless of it. See RoadGraph::edgeMarkingsEnabled(). + */ +enum class RoadMarkingMode : int { + Inherit = 0, /**< Follow RoadConfig::markingsEnabled. */ + On = 1, /**< Markings on this edge even if globally disabled. */ + Off = 2 /**< No markings on this edge even if globally enabled. */ +}; + +/** + * A flat decal quad painted onto the road surface (phase 2.5): manhole + * covers, arrows, damage patches, parking symbols. + * + * The quad is centred at normalized position @c edgeT along the edge, + * shifted @c lateralOffset meters sideways (positive = right side of + * A->B travel, same convention as RoadEdgePrefabSlot), spans + * @c size meters on both axes and samples the @c uv* rect of the shared + * decal atlas texture. Decals are pure visuals: no physics collider, + * no navmesh contribution, no terrain-compliance footprint. + */ +struct RoadEdgeDecal { + /** Normalized position along the edge from nodeA to nodeB. */ + float edgeT = 0.5f; + + /** Lateral offset from the centerline (positive = right side). */ + float lateralOffset = 0.0f; + + /** Quad size in meters (both axes). */ + float size = 1.0f; + + /** Atlas rect: origin (uvX, uvY) and extent (uvW, uvH), 0..1. */ + float uvX = 0.0f; + float uvY = 0.0f; + float uvW = 0.5f; + float uvH = 0.5f; +}; + /** * A connection between two RoadNode objects. * @@ -353,6 +467,19 @@ struct RoadEdge { /** Prefab anchored at the centerline, at edgeT (edge midpoint). */ RoadEdgePrefabSlot prefabMid; + + /** + * Per-edge marking tri-state (phase 2.5, see RoadMarkingMode). + * Inherit (the default) follows the global + * RoadConfig::markingsEnabled switch. + */ + RoadMarkingMode markingsMode = RoadMarkingMode::Inherit; + + /** + * Decal quads painted on this edge's road surface (phase 2.5). + * Purely visual; see RoadEdgeDecal. + */ + std::vector decals; }; /** Minimum wedge swept angle; sharper wedges are rejected (M5.5). */ @@ -361,6 +488,19 @@ static const float ROAD_WEDGE_MIN_ANGLE_DEG = 30.0f; /** Maximum wedge swept angle; only near-360 deg wedges are degenerate. */ static const float ROAD_WEDGE_MAX_ANGLE_DEG = 359.9f; +/** + * A named sequence of road edges (taxi demo phase 3). Streets give a + * printable, code-accessible name to a run of edges so lots can be + * referenced as "street name + number" addresses (see + * RoadNetworkGen::assignLotAddresses). Edges are stored as normalized + * endpoint id pairs (first < second) because node ids are stable while + * edge vector indices are not. + */ +struct RoadStreet { + std::string name; + std::vector > edges; +}; + struct RoadGraph; struct RoadWedge; struct RoadStraightSegment; @@ -394,6 +534,13 @@ struct RoadGraph { /** All road edges in the graph. Ordering is not significant. */ std::vector edges; + /** + * Named streets (phase 3). Purely labelling data — geometry never + * reads it. Mutated through RoadNetworkGen::nameStreet; kept here so + * it travels with the graph through region-store flushes. + */ + std::vector streets; + /** * Monotonic version counter incremented by every mutating operation. * @@ -424,6 +571,18 @@ struct RoadGraph { config.sidewalkEnabled); } + /** + * Effective marking enable of one edge (phase 2.5): the tri-state + * mode folded with the global config.markingsEnabled. Geometry + * builders call this directly instead of caching. + */ + bool edgeMarkingsEnabled(const RoadEdge &e) const + { + return e.markingsMode == RoadMarkingMode::On || + (e.markingsMode == RoadMarkingMode::Inherit && + config.markingsEnabled); + } + /** * Recompute the resolved sidewalkLeft/sidewalkRight bools of every * edge from the per-side modes and the global @@ -702,6 +861,61 @@ struct RoadGraph { return true; } + /** + * Merge @p dropId into @p keepId (taxi demo phase 3): every edge + * incident to @p dropId is re-pointed at @p keepId (edges that would + * become duplicates or self-edges are removed instead), street + * references are re-pointed too, and @p dropId is removed. Used by + * crossing resolution to collapse an end node or a too-short edge + * into the junction node. + * + * @return true on success (both nodes existed and differ). + */ + bool mergeNodes(int keepId, int dropId) + { + if (keepId == dropId) + return false; + if (!findNodeById(keepId) || !findNodeById(dropId)) + return false; + + for (auto it = edges.begin(); it != edges.end();) { + RoadEdge &e = *it; + if (e.nodeA != dropId && e.nodeB != dropId) { + ++it; + continue; + } + if (e.nodeA == dropId) + e.nodeA = keepId; + if (e.nodeB == dropId) + e.nodeB = keepId; + if (e.nodeA == e.nodeB || + findEdgeIndex(e.nodeA, e.nodeB) != + (int)(it - edges.begin())) { + /* Self-edge after the merge, or another edge + * already connects the same pair (findEdgeIndex + * returns the FIRST match, so a mismatch means + * this one is the duplicate). */ + it = edges.erase(it); + } else { + ++it; + } + } + + for (RoadStreet &s : streets) + for (std::pair &se : s.edges) { + if (se.first == dropId) + se.first = keepId; + if (se.second == dropId) + se.second = keepId; + if (se.first > se.second) + std::swap(se.first, se.second); + } + + removeNode(dropId); + bumpVersion(); + return true; + } + /** * Split an edge at a normalized position and insert a new node. * @@ -782,9 +996,11 @@ struct RoadGraph { /* Sidewalk passes transfer like the prefab slots below (both * halves keep the A->B orientation, so the side flags carry * over); the copies on both halves above are replaced by - * edgeT-remapped placement. */ + * edgeT-remapped placement. Decals transfer the same way. */ edgeA.sidewalkPasses.clear(); edgeB.sidewalkPasses.clear(); + edgeA.decals.clear(); + edgeB.decals.clear(); if (actualT > 1e-6f && actualT < 1.0f - 1e-6f) { auto remapSlot = [&](const RoadEdgePrefabSlot &src, RoadEdgePrefabSlot &dstA, @@ -830,6 +1046,20 @@ struct RoadGraph { edgeB.sidewalkPasses.push_back(q); } } + + /* Decals transfer with the same edgeT remap. */ + for (const RoadEdgeDecal &d : oldEdge.decals) { + if (d.edgeT <= actualT) { + RoadEdgeDecal q = d; + q.edgeT = d.edgeT / actualT; + edgeA.decals.push_back(q); + } else { + RoadEdgeDecal q = d; + q.edgeT = (d.edgeT - actualT) / + (1.0f - actualT); + edgeB.decals.push_back(q); + } + } } edges.erase(edges.begin() + edgeIndex); diff --git a/src/features/editScene/demos/demo-taxi/PLAN.md b/src/features/editScene/demos/demo-taxi/PLAN.md index 3133171..b1f7ff4 100644 --- a/src/features/editScene/demos/demo-taxi/PLAN.md +++ b/src/features/editScene/demos/demo-taxi/PLAN.md @@ -1,10 +1,13 @@ # demo-taxi — Implementation Plan -**Status: Phase 2 done (2026-09-17).** Phases 0-2 done (skeleton, -sedan vehicles, road geometry verification + automatic compliance); -Phase 3 (road-network generator) is next. This plan is a working -draft — phases get refined, split or reordered as we work through -them. +**Status: Phase 4 done (2026-10-05).** Phases 0-4 done (skeleton, +sedan vehicles, road geometry verification + automatic compliance, +road markings/UVs/decals, road-network generator with crossings/lots/ +streets, procedural town generation with districts/foundations/stairs); +Phase 4.5 (town parameters on road edge sides, building-layout DSL, +multi-town areas — research + foundations) is next. +This plan is a working draft — phases get refined, split or reordered +as we work through them. Taxi driver demo in the open world of `demo_scene_exterior.json`: a procedurally generated (but tightly controlled) settlement on the @@ -152,7 +155,10 @@ Non-goals (for now): the edge and often overlap because too little space is reserved for lanes and sidewalks; the procedural lane template emits front/back geometry even where the intersection should be open; terrain - compliance is manual and tedious. + compliance is manual and tedious. No road markings at all (center + line, same-direction dashed lane lines, curb edge lines, stop lines + before intersections), no road decals, and no road-texture V + tiling. 3. Road-network generation: roads are authored node-by-node with the Connect tool today; no layout generator. 4. Town generation: District/Lot exist but arrange lots in a circle @@ -176,6 +182,12 @@ Non-goals (for now): 10. Fare chain: no fare/timer/money rules (Lua module + HUD). 11. Assets: sedans (several types x several colors, one taxi-marked), possibly street props (lamps, signs). +12. Geometry/render hardening: inner-corner wedge midpoints need + outward relaxation (with the center gap refilled via the mid-edge + vertex shift) to kill residual overlaps on tight turns; marking / + decal overlays Z-fight the road top at grazing angles and distance + (needs a depth/shader-level fix, not more lift); no render + batching yet — the bare exterior scene already draws ~85 batches. ## Phases @@ -1005,24 +1017,275 @@ Follow-ups (2026-09-30, apron terrain interface): TPS camera) plus env-gated debug dumps (`ROAD_DUMP_DIR`, `ROAD_DUMP_PIECES`, `ROAD_DEBUG_PIN`). -### Phase 3 — Road-network generator +### Phase 2.5 — Road markings, road texture & decals — DONE + +Done 2026-10-02. Everything below is implemented; the headless probes +live in `road_geometry_overlap_test` (`runMarkingSegmentProbe`, +`runJunctionMarkingCase` 4-way 1+1 / 4-way 2+2 / 3-way T, +`runMarkingTwoWayProbe`, `runDecalProbe`, `runUVAtlasProbe`) and the +CTest gate `roadGeometryOverlapTest` is green. Implementation notes +beyond the original text: + +- The stop bar starts flush at the centerline strip's outer edge and + the arm's longitudinal lines resume past the bar, so the bar never + z-fights the lines it crosses (found by the combined-overlay probe). +- Dashes phase from the edge midpoint, so up to one dash gap next to + a node can be unpainted — the 2-way probe bounds that gap instead + of demanding paint at a fixed offset. +- Decals never land on junction interiors by construction: they are + placed per edge (edgeT along the half-edge), and a cross-page edge + paints each half's decals on the owning page only. +- `RoadGeometryLib` doc: `ProceduralRoadGeometry.md` §18. + +Original phase text follows. + +Painted markings, proper road UVs and simple decals, all generated with +the road geometry so both hand editing and the Phase 3 generator get +them for free. + +**Markings** + +- **Per-edge control**: tri-state `RoadEdge::markingsLeftMode/RightMode`-style + override (Inherit/On/Off, same pattern as the sidewalk modes) + resolving against the global `RoadConfig::markingsEnabled`, so + markings can be disabled (or forced on) per edge. +- **Centerline**: separates the two travel directions. Wide roads + (2+ lanes per direction) get a double solid line; narrow roads + (1 lane per direction) get a single dashed centerline instead. + Resolved automatically from the lane counts. +- **Lane lines**: dashed lines between same-direction lanes + (N lanes per side -> N-1 dashed lines on that side). +- **Edge lines**: solid lines along both road edges at the curbs, + running the full length of every edge. +- **Stop lines**: a perpendicular line across the incoming + (right-hand) half of each arm before an intersection (nodes with + more than two neighbors), placed at the wedge boundary with a small + setback so vehicles stop clear of the junction. +- **Intersection suppression**: no markings inside the intersection + area itself — at nodes with >2 neighbors the wedge surface stays + unmarked and all longitudinal lines end at the wedge boundary. At + 2-neighbor nodes (turns) all markings continue through the corner. + +**Road texture UVs** + +- Proper UV coordinate generation in the road mesh: U runs across the + road and does NOT tile (stretched across the surface width), V runs + along the travel direction and tiles at a fixed world length per + tile (`RoadConfig::textureTileLength`), so long edges stop smearing. +- The road texture is a square atlas shared by all road surfaces; + U bands assign each surface its slice: 0.0-0.5 road-top asphalt, + 0.5-0.75 sidewalk, 0.75-1.0 apron. Each strip maps its width into + its U band; V tiles along the length for all of them. The asphalt + band stretched across the lane width and tiled along the lane gives + a believable enough asphalt look. + +**Decals** + +- Per-edge decal list (serialized with the edge, editable in the road + panel): each decal is an offset along the edge, a lateral offset + from the centerline, a size, and a rectangle in a decal atlas + texture — manhole covers, painted text/arrows, road damage patches. +- The atlas for this phase is a generated placeholder (a few + procedural patches: manhole, arrows, damage spots, placeholder + text), replaced by a real authored texture later — the per-edge + decal data only references atlas rectangles, so swapping the texture + is free. +- Rendered as alpha-blended quads a hair above the road top. Note: + Ogre mainline has no decal system (only test-media images by that + name), so "Ogre decals" here means plain decal geometry built the + same way as the marking strips, sharing the decal atlas material. + Researched alternatives (2026-10-01): the only projection-decal + implementation is the old Ogre 1.7 wiki tutorial (Intermediate + Tutorial 6); current samples contain nothing newer — the + `decalmap` texture_unit names in `Examples-Advanced.material` are + just the legacy fixed-function term for the base diffuse map, and + `setProjectiveTexturing` only appears in the Fresnel sample for + reflections. A projective approach would also need receiver + filtering (it would paint terrain/vehicles/characters too) and one + projector pass per decal, so geometry decals stay the simpler and + batchable choice for a per-edge decal list. +- Decals are suppressed on junction wedges like the markings. + +**Implementation** + +- Markings and decals are thin geometry strips/quads built by + `RoadGeometryLib` a small offset above the road top, using the same + widened curb-offset chain machinery as sidewalks/aprons, so they + follow fillets, lane tapers and pin zones exactly. One + unlit/faintly-lit marking material (paint color in `RoadConfig`), + one decal material with the atlas. +- `RoadConfig` fields: `markingsEnabled`, line/dash widths, dash + length/gap, stop-line width and setback, `textureTileLength`, + `decalAtlasTexture`. + +**Headless** + +- Overlap-suite probes: marking strips present with the right counts + for 1/2/4-lane edges, dashed count = lanes-1 per side, dashed + centerline at 1+1 vs double solid when wider, solid edge lines at + both curbs, stop lines on the incoming half at 3-/4-way nodes and + none at 2-way nodes, markings continuing through 2-way turns, + nothing on the junction wedges, no marking/road-top z-overlap; + per-edge disable honored. +- UV probe: V tiles at the configured world length, U bands map + road/sidewalk/apron into 0.0-0.5 / 0.5-0.75 / 0.75-1.0. +- Decal probe: position/orientation derived from edge + lateral + offsets, rectangle mapped from the atlas, suppressed on junction + wedges. + +**Regression fixes (2026-10)** + +- Marking strips no longer spike vertically on pin-zone boundary rows: + the memoized `filletMiterPinZoneRaw` cache restored only the pin + point's X/Z, leaving Y uninitialized on cache hits, so the pinned + curb offset could point straight down with a garbage length (marking + vertices at y ~ -1.5e6 at runtime). The cache now round-trips the + full pin point. +- The generated road-atlas asphalt band uses anisotropic noise + (streaks along the lane) and default mipmaps; the isotropic + no-mipmap version read as cross-road moire straps at distance. + +### Phase 3 — Road-network generator — DONE The tedious part today: every node/edge is hand-placed. Build a -generator that emits a `RoadGraph` (region files when streaming): +generator that emits a `RoadGraph` (region files when streaming), plus +the graph-level services that both manual and procedural networks +need. + +Implemented in `roadlib/RoadNetworkGen.hpp/.cpp` (namespace +`RoadNetworkGen`, pure `RoadGraph` functions, part of the +`RoadGeometryLib` target; see `ProceduralRoadGeometry.md` §19) with +`RoadSystem` wrappers (`generateRoadNetwork` / +`resolveCrossingsNow` / `clearGenerated`), serialization (region +files carry `generatedGroupId`; streets inline in the scene JSON or in +`roads/streets.json` when streaming; `crossingSnapDistance` / +`crossingHeightThreshold` in `roadConfig`), a terrain-panel "Road +Network Generator" section (Generate / Resolve Crossings Now / +per-group Remove; guards refuse Remove/Split on grouped nodes/edges; +the edge inspector assigns streets) and headless probes +(`runCrossingResolutionProbe`, `runLotProbe`, `runStreetProbe`, +`runGeneratorProbe` in `road_geometry_overlap_test` — suite PASSED). + +**Crossing resolution (graph level)** — DONE + +- Edges must never cross without a junction. Two edges count as + intersecting when the distance between their closest points is less + than or equal to the road width (resolved lane counts x laneWidth, + plus sidewalks where enabled) — unless the closest points are more + than 10 m apart in Y, in which case the edges are NOT intersecting + (overpass/underpass: one road legitimately passes over the other + and no junction is created). → `findCrossings()` with + `RoadConfig::crossingHeightThreshold` (default 10 m). +- On detection it must be possible to create a proper junction: split + both edges at the closest-point parameters and insert a new shared + node. → `resolveCrossing()` + `RoadGraph::splitEdge`. +- When a closest point is near an edge end (within a configurable snap + distance), reuse that existing node for the junction instead of + creating a new one; the other edge still splits. + → `RoadConfig::crossingSnapDistance` (default 5 m). +- Edges too short to be split (the split would leave slivers below the + minimum edge length) are collapsed into the junction node. + → `RoadGraph::mergeNodes()`. +- The graph must stay sane at all times: no duplicate edges, no + degenerate sliver edges, no dangling nodes, valences and region + files consistent after every operation. → `checkSane()` + (deliberately not `validate()` — its wedge-angle rules reject + hand-authored legacy scenes). +- Editor: a "resolve crossings" action (check, list, apply per + crossing or for all). The generator runs it automatically on its + own output and when connecting a procedural part to manual road. + → "Resolve Crossings Now" button; `runCrossingResolution` on + `generateGrid()` and inside `connectGroupToNode()`. (A per-crossing + list/apply UI is deferred — the resolve-all action covers the + generator workflows.) + +**Lots (enclosed areas)** — DONE + +- Areas enclosed by road edges and not intersected by another edge + (faces of the planar road graph, after crossing resolution) are + tracked as potential lots — for manual and procedural networks + alike. Phase 4 consumes them for building placement. + → `findLots()` (planar face walk, sharpest right turn from the + incoming heading with the U-turn edge excluded; spur/inner-node + rejection; area + centroid). +- Lot filtering: a face must fit a 45x45 m square (XZ bbox + area, + `RoadLotFilter::minLotSize`) and be bounded by at most 8 edges + (`maxBoundaryEdges`); every edge side bordering no accepted face + grows a square fallback lot (side = edge length) when the edge is + at least 45 m long (`edgeFallback`, overlap/cleanness checked). + → `RoadLotFilter` defaults; `runLotProbe` covers the size/edge-count + cutoffs, the ring and lone-edge fallback cases. +- All potential lots are drawn as dark grey outline loops in road + edit mode, like the other helper geometry. + → `RoadSystem::buildLotOutlines` (visual-aids overlay). + +**Streets and addresses** — DONE + +- A sequence of road edges can be marked as a street and given a name + (printable and code-accessible). Lots are referenced by street name + + sequential number to form a unique address; lots on unnamed + streets are addressed by sequential number alone. These are mostly + display/reference labels (HUD fare text, debug panels). + → `RoadGraph::streets`, `nameStreet()`, `findStreetForEdge()`, + `assignLotAddresses()` ("Lot N" fallback). + +**Generator** — DONE - Grid layout first (block size, road width, jitter), explicit seed, fully deterministic. (Organic/radial layouts are deferred to Phase 11.) A network is bounded to the settlement square (max 1000 x 1000 m); generating another settlement is just another generator invocation at a different origin. + → `GridParams` + `generateGrid()` (mt19937 jitter on interior nodes + only; group id auto-assigned per invocation). +- A generated road grid is a single unsplittable unit in the road + graph — its interior nodes/edges are not individually editable. It + connects to manual road as if it were a road node; the connection to + the closest node of the procedural part is handled automatically + (and crossing resolution runs on the connecting edge). + → `RoadNode::generatedGroupId` (editor guards), `removeGroup()`, + `connectGroupToNode()`. - Terrain-aware placement: sample `getHeightAt`, reject or relocate segments over water or too-steep slopes, then run the Phase 2 automatic compliance pass. + → `HeightSampler` (wired to `TerrainSystem::getHeightAt` in + `RoadSystem::generateRoadNetwork`, water level from the scene's + `WaterPhysics` entity), `avoidWater` node rejection + `maxSlope` + edge skipping. (Relocating instead of rejecting and the automatic + post-generation compliance pass are left to Phase 4 workflows — + the user runs the existing compliance buttons.) - Editor entry point: a "Generate Road Network" panel on the terrain (parameters, seed, generate/regenerate, clear). -- Headless: generate a network, assert node/edge counts, intersection - valences, no self-intersections, region files written and reload - identically; two settlements in one world do not interfere. + → "Road Network Generator" tree node in the terrain road section, + with a "Pick Origin on Terrain" button that arms a one-shot terrain + click to fill the origin XZ (`RoadSystem::armOriginPick` / + `takePickedOrigin`, consumed by + `EditorUISystem::handleRoadEditClick`) and a "Preview Grid" checkbox + that draws the would-be grid as orange lines without touching the + graph (`RoadSystem::updateGenPreview`/`clearGenPreview`, refreshed + on every parameter change). + +**Headless** — DONE + +- Generate a network; assert node/edge counts, intersection valences, + no self-intersections, region files written and reload identically; + two settlements in one world do not interfere. + → `runGeneratorProbe` (determinism, counts, sanity, two settlements, + group connect/remove). (Region-file reload is covered indirectly: + group ids/streets ride the existing region JSON paths, and the + streets JSON round trip is asserted in `runStreetProbe`.) +- Crossing resolution: crossing edges become a 4-way junction, end + snap reuses the existing node, too-short edges collapse, and the + graph stays sane (no slivers/duplicates/dangling nodes) after each + case; an XZ crossing with more than 10 m of Y separation at the + closest points is left untouched (overpass). + → `runCrossingResolutionProbe`. +- Lot tracking: enclosed blocks are found, areas intersected by an + edge are not, lot counts match the grid parameters. + → `runLotProbe` + the 9-lot count in `runGeneratorProbe`. +- Streets: naming an edge sequence yields unique, stable addresses + across save/load; the unnamed-street fallback numbering works. + → `runStreetProbe`. ### Phase 4 — Procedural town generation @@ -1052,6 +1315,88 @@ On top of the generated road network: all buildings on dry land above water level, every building carries its (disabled) scene-switch door config, navmesh rebuilds. +**Phase 4 status (2026-10-04): implemented.** `TownGen` pure lib +(`roadlib/TownGen.hpp/.cpp`) + `TownGenSystem` + a "Town Generator" +section in the terrain panel; `findLots()` fallback squares gained +`sourceEdgeA`/`sourceEdgeB` so their frontage orients to the road and +addresses resolve through the street table. Done: district presets +(village/town/city-residential/city-center: footprint range, floors, +density, setback), seeded deterministic placement on `findLots()` lots, +frontage = street edge > fallback source edge > longest edge, floor on +a foundation block seated at the highest terrain corner, entrance +stairs (solid steps) when the door ground drops > 0.35 m, per-building +pickup POI children, sequential street addresses ("Lot N" fallback), +water-level rejection, disabled scene-switch exterior doors +(`doorsEnabled = false`, placeholder `town_interior.json` wired in +Phase 8), headless probes (counts/floors/determinism/water/stairs/ +addresses — the navmesh-rebuild assert is covered by code review: +CellGrid dirty marks dirty the navmesh). Fix pass (2026-10-05): +lots are shrunk off the road body before placement +(`shrinkLotOffRoads()` — `findLots()` polygons run +centerline-to-centerline, so each bounding edge is inset by lanes + +sidewalk + apron + 0.3 m, collapses skip the lot; probe now asserts +every building corner keeps the road half-width off every edge +segment), buildings get their flat roof (a full-footprint +`RoofComponent` child at `posY = floors` — `buildRoofs()` only reads +RoofComponent children), and the building node is shifted so the +CellGrid shell (cells at `x*cellSize` from the node) lands centered +on the computed footprint with foundation/stairs/POI in shell +coordinates. Deferred: per-page building +streaming store (town buildings are always-loaded scene entities for +now — revisit when a settlement grows past a few hundred buildings), +street props (no lamp/sign/tree prefab assets exist yet — add assets, +then a roadside slot pass), terrain compliance per lot (superseded by +foundation blocks + stairs, which tolerate slopes directly). + +### Phase 4.5 — Town parameters, building-layout DSL, multi-town areas (research + foundations) + +Phase 4 proved the pipeline but exposed three structural gaps: town +regeneration discards all manual tuning (parameters live only in the +generator dialog, not on the map), buildings are fixed shells with no +layout control, and nothing scopes generation to one settlement when +the world holds several. This phase researches and lays the +foundations for all three. Research output lands in +`ProceduralRoadGeometry.md` (new §21) before implementation. + +- **Per-lot parameters on road edge sides.** The persistent home for + per-building/per-lot settings is the **side of a road edge** (each + `RoadEdge` gets a left/right `RoadLotParams` block: district + override, density, setback, footprint/floor ranges, roof type and + params, no-build flag, layout reference). Lots are already derived + from edge frontages, so edge-side parameters survive regeneration — + re-running the town generator re-reads them instead of resetting + them. Persisted with the graph (inline scene JSON, region files + when streaming), edited in the edge inspector. Research: the exact + field set, how edge-side blocks map to lots (enclosed faces touch + several edges — precedence rules), how overrides merge with district + presets, migration of existing scenes (absent = inherit). +- **Building layout DSL.** A small declarative language describing a + building layout — floors, rooms, walls, windows, doors, roofs, all + with parameters — evaluated into CellGrid cells + `RoofComponent` + sets. One layout definition must drive BOTH the exterior shell + (Phase 4 path) and the interior scene (Phase 8), so the DSL is the + single source of truth for a building's shape. Research: grammar + (Lua table DSL vs custom text format vs JSON), parameterization + (variables, per-floor overrides, random draws under the town seed), + a template library (named layouts referenced from edge-side lot + params), and the evaluation boundary between `TownGen` (placement) + and `TownGenSystem` (materialization). +- **Multiple towns: areas in the road graph.** `RoadGraph` gains a + `townAreas` list — square or circular areas (center, half-size or + radius, district + params, seed) scoping generation: `findLots()` + results are filtered per area, each area generates into its own + `"TownGen "` container, and a world can hold several + independent settlements from the start. Research: area/lot overlap + rules (lots straddling the boundary), area editing UX (viewport + outline, panel), streaming interaction (areas vs region stores). +- **Headless coverage:** edge-side params round-trip (serialize / + region store / regeneration keeps overrides), layout DSL evaluation + (a sample layout produces the expected cell flags and roof set), + two overlapping-scope areas generate independent deterministic + towns. +- **Docs:** `ProceduralRoadGeometry.md` §21 (params model, DSL + grammar, areas), AGENTS.md, this plan's status blocks. + ### Phase 5 — NPC vehicle traffic - AI driver system (new, engine-level): a vehicle follows the road @@ -1180,6 +1525,39 @@ demo needs them: - More vehicle types beyond sedans (the Phase 1 pipeline is already generic): vans, trucks, buses; service vehicles. +### Phase 11.5 — Geometry polish, marking depth & render batching + +Deferred hardening items (noted 2026-10-03 — fix, don't forget). Pull +the phase earlier if any item starts blocking daily work; all three +must land before the Phase 12 performance/polish pass. + +- **Wedge midpoint relaxation (inner corners).** Inner (converging) + corner wedges can still overlap neighbouring geometry on tight + turns. Relax the midpoints of wedges with an inner corner outward + (per corner, in the spirit of `relaxImpossibleCorners()` but acting + on the wedge midpoint/seam side instead of the node) so the overlap + clears and the corner looks nicer. The center gap this opens is + filled by shifting the edge vertices as usual (the existing mid-edge + seam machinery), so the mid-edge seams stay gapless. Headless: + tight-corner overlap-suite cases keep zero crossing/coplanar pairs + with relaxation on, and straight/near-straight corners are + unaffected (signature-gated like the other rebuild triggers). +- **Marking Z-fighting.** The marking/decal overlays (2 cm lift) still + Z-fight the road top at grazing angles and at distance. Fix at the + material/shader level instead of adding more lift: bias the overlay + depth toward the camera (Ogre depth-bias material settings, or a + small custom-shader offset pulling `gl_Position` toward the camera), + so the paint stays visually coplanar but always wins the depth test. + Verify on slopes, at long distance and against the decal material. +- **Render batching.** The bare exterior scene already draws ~85 + batches; a full settlement will explode. First measure what is hot + (road page chunks, marking/decal overlays, terrain, spawner + prefabs), then batch: merge same-material road/overlay chunks into + fewer meshes per page, instance repeated props (lamps, signs, + trees), static-geometry batching for placed buildings, and material/ + texture atlasing where it cuts state switches. Define the target + batch budget after measuring; the Phase 12 FPS check enforces it. + ### Phase 12 — Demo assembly, polish, docs - Assemble the full demo scene: at least one full settlement on the @@ -1189,7 +1567,8 @@ demo needs them: "home"/menu-adjacent space. - Balance pass: speeds, timers, fare slack, traffic density. - Performance pass: streaming radii, navmesh tile budget, active NPC - caps — measure FPS with both settlements placed. + caps — measure FPS with both settlements placed; verify the Phase + 11.5 batch budget holds in the assembled scene. - Docs: update `AGENTS.md` (both levels), `GameFeatures202609.md` (F13+ entries), demo README section; full headless test battery wired into the demo like demo-sokoban's. diff --git a/src/features/editScene/demos/demo-taxi/demo_scene_exterior.json b/src/features/editScene/demos/demo-taxi/demo_scene_exterior.json index 9afe52c..db798e1 100644 --- a/src/features/editScene/demos/demo-taxi/demo_scene_exterior.json +++ b/src/features/editScene/demos/demo-taxi/demo_scene_exterior.json @@ -849,15 +849,15 @@ }, "transform": { "position": { - "x": 4004.76416015625, - "y": 10.909640312194824, - "z": 3964.12255859375 + "x": 4002.519287109375, + "y": 11.094489097595215, + "z": 3962.923095703125 }, "rotation": { - "w": 0.991229236125946, - "x": -0.053019944578409195, - "y": 0.12085098028182983, - "z": 0.0069722202606499195 + "w": 0.9888900518417358, + "x": -0.0026904146652668715, + "y": 0.14861378073692322, + "z": 0.0017891188617795706 }, "scale": { "x": 1.0, @@ -1254,15 +1254,15 @@ }, "transform": { "position": { - "x": 4017.775390625, - "y": 10.960867881774902, - "z": 3963.472900390625 + "x": 4016.510498046875, + "y": 11.157183647155762, + "z": 3964.1669921875 }, "rotation": { - "w": 0.9645168781280518, - "x": -0.05992506444454193, - "y": 0.25656017661094666, - "z": 0.017121003940701485 + "w": 0.9334654211997986, + "x": -0.0019687723834067583, + "y": 0.35863012075424194, + "z": 0.004789790604263544 }, "scale": { "x": 1.0, @@ -1470,10 +1470,22 @@ "roadConfig": { "apronDrop": 1.0, "apronEnabled": true, + "apronSkirtDepth": 2.0, "apronWidth": 2.0, "cornerFilletLength": 12.0, + "crossingHeightThreshold": 10.0, + "crossingSnapDistance": 5.0, + "decalMaterialName": "RoadDecalMaterial", "laneWidth": 5.0, "lanesPerDirection": 1, + "markingDashGap": 3.0, + "markingDashLength": 3.0, + "markingEdgeInset": 0.25, + "markingLineWidth": 0.15000000596046448, + "markingMaterialName": "RoadMarkingMaterial", + "markingStopSetback": 1.0, + "markingStopWidth": 0.4000000059604645, + "markingsEnabled": true, "prefabDespawnDistance": 250.0, "prefabSpawnDistance": 150.0, "roadLodDistance": 200.0, @@ -1485,7 +1497,8 @@ "sidewalkHeight": 0.15000000596046448, "sidewalkMeshTemplate": "", "sidewalkThickness": 0.30000001192092896, - "sidewalkWidth": 1.5 + "sidewalkWidth": 1.5, + "textureTileLength": 10.0 }, "streamingEnabled": true, "terrainId": 4242424300000001, @@ -1595,7 +1608,7 @@ 0.800000011920929 ], "sunSphereSize": 5.0, - "timeOfDay": 15.492432594299316, + "timeOfDay": 0.6397704482078552, "timeSpeed": 0.12999999523162842 }, "transform": { diff --git a/src/features/editScene/demos/demo-taxi/screenshots/markings.png b/src/features/editScene/demos/demo-taxi/screenshots/markings.png new file mode 100644 index 0000000000000000000000000000000000000000..afb78687c2a9f3a49814b1a5aceeae68f6104364 GIT binary patch literal 204909 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 z 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 0 || frames > 3600) { + if (result < 0) { + std::cerr << "generate-town: timed out waiting " + << "for the terrain" << std::endl; + result = 0; + } + root->queueEndRendering(); + return true; + } + TerrainSystem *ts = TerrainSystem::getInstance(); + TownGenSystem *townGen = + ts ? ts->getTownGenSystem() : nullptr; + if (!townGen) + return true; /* terrain not active yet */ + std::string error; + int count = townGen->generate(params, &error); + std::cout << "generate-town: " << count << " buildings" + << (error.empty() ? "" : " (" + error + ")") + << std::endl; + result = 0; + root->queueEndRendering(); + return true; + } +}; + /** Frame-driven terrain test runner. * Terrain init needs the render loop active (GPU context). * Runs the suite on the first frame, then exits. */ @@ -65,6 +109,8 @@ int main(int argc, char *argv[]) bool exitAfterFirstFrame = false; bool headless = false; int terrainTestIterations = 0; + bool generateTown = false; + TownGen::TownParams genTownParams; Ogre::String sceneFile; Ogre::String projectDir; for (int i = 1; i < argc; i++) { @@ -82,6 +128,29 @@ int main(int argc, char *argv[]) debugBuoyancy = true; } else if (arg == "--exit-after-first-frame") { exitAfterFirstFrame = true; + } else if (arg.find("--generate-town=") == 0) { + /* --generate-town=[,] with district + * village|town|city-residential|city-center */ + generateTown = true; + Ogre::String val = arg.substr(16); + size_t comma = val.find(','); + Ogre::String district = val.substr(0, comma); + if (comma != Ogre::String::npos) + genTownParams.seed = + Ogre::StringConverter::parseInt( + val.substr(comma + 1), 1); + if (district == "town") + genTownParams.district = + TownGen::District::Town; + else if (district == "city-residential") + genTownParams.district = + TownGen::District::CityResidential; + else if (district == "city-center") + genTownParams.district = + TownGen::District::CityCenter; + else + genTownParams.district = + TownGen::District::Village; } else if (arg.find("--run-terrain-tests=") == 0) { Ogre::String val = arg.substr(20); terrainTestIterations = Ogre::StringConverter::parseInt( @@ -190,13 +259,17 @@ int main(int argc, char *argv[]) ExitAfterFirstFrameListener exitListener(app.getRoot()); if (exitAfterFirstFrame) app.getRoot()->addFrameListener(&exitListener); + GenerateTownFrameListener genTownListener(app.getRoot(), + genTownParams); + if (generateTown) + app.getRoot()->addFrameListener(&genTownListener); app.getRoot()->startRendering(); - /* Destroy scene entities while the systems are still alive - * (same teardown-ordering reason as the demo mains: spawner - * OnRemove observers dereference their systems). */ - if (gameSceneStarted) - app.clearScene(); + /* Destroy scene entities while the systems (and OGRE) are + * still alive: components holding OGRE resources (procedural + * materials/textures, spawner OnRemove observers) must not + * be torn down at ecs_fini after Root is gone. */ + app.clearScene(); app.closeApp(); /* Propagate terrain test result as process exit code so CI can diff --git a/src/features/editScene/roadlib/RoadGeometryLib.cpp b/src/features/editScene/roadlib/RoadGeometryLib.cpp index 0d14813..64a1722 100644 --- a/src/features/editScene/roadlib/RoadGeometryLib.cpp +++ b/src/features/editScene/roadlib/RoadGeometryLib.cpp @@ -63,6 +63,29 @@ float halfEdgeU(const RoadHalfEdge &he, const RoadGraph &graph, float t) return t; } +/* ---------------------------------------------------------------- + * Road atlas UV mapping (phase 2.5) + * + * When RoadConfig::textureTileLength > 0 the road mesh UVs are + * regenerated as an atlas mapping instead of meter coordinates: U runs + * ACROSS the surface (no tiling) and selects the band of the shared + * road atlas texture (0.0-0.5 asphalt road top, 0.5-0.75 sidewalk, + * 0.75-1.0 apron), while V runs along the travel direction and tiles + * once per textureTileLength meters. textureTileLength == 0 keeps the + * legacy meter UVs (U along the road, V lateral meters). + * ---------------------------------------------------------------- */ + +static bool atlasUVsEnabled(const RoadGraph &graph) +{ + return graph.config.textureTileLength > 1e-6f; +} + +/** Along-road V coordinate: meters tiled by textureTileLength. */ +static float atlasV(const RoadGraph &graph, float meters) +{ + return meters / graph.config.textureTileLength; +} + float nodeRoadLevel(const RoadGraph &graph, int nodeId) { float sum = 0.0f; @@ -1685,7 +1708,7 @@ static bool filletMiterPinZoneRaw(const RoadWedge &wedge, bool sidewalkEnabled, apronEnabled; bool has; float pinStart, pinEnd; - float pinX, pinZ; + float pinX, pinY, pinZ; bool impossible; }; static std::vector s_cache; @@ -1735,6 +1758,7 @@ static bool filletMiterPinZoneRaw(const RoadWedge &wedge, pinStart = e.pinStart; pinEnd = e.pinEnd; pinPoint.x = e.pinX; + pinPoint.y = e.pinY; pinPoint.z = e.pinZ; } if (impossible) @@ -1762,6 +1786,7 @@ static bool filletMiterPinZoneRaw(const RoadWedge &wedge, key.pinStart = ps; key.pinEnd = pe; key.pinX = pp.x; + key.pinY = pp.y; key.pinZ = pp.z; key.impossible = imp; if (s_cache.size() >= 128) @@ -2187,9 +2212,24 @@ void transformWedgeVertices(Procedural::TriangleBuffer &strip, /* UVs. */ float widthD = off.length(); - v.mUV.x = (d <= L1) ? halfEdgeU(h1, graph, L1 - d) - : halfEdgeU(h2, graph, d - L1); - v.mUV.y = v.mUV.y * widthD + in1; + float uMeters = (d <= L1) ? halfEdgeU(h1, graph, L1 - d) + : halfEdgeU(h2, graph, d - L1); + if (atlasUVsEnabled(graph)) { + /* U across the asphalt band (0..0.5): the lateral + * meter position from the lanesIn-side curb over the + * arm's full road width; V tiles along the road. */ + const RoadHalfEdge &hArm = (d <= L1) ? h1 : h2; + float lw = graph.config.laneWidth; + float fullW = (hArm.lanesIn + hArm.lanesOut) * lw; + if (fullW < 1e-4f) + fullW = 1e-4f; + float latM = hArm.lanesIn * lw + v.mUV.y * widthD; + v.mUV.x = 0.5f * latM / fullW; + v.mUV.y = atlasV(graph, uMeters); + } else { + v.mUV.x = uMeters; + v.mUV.y = v.mUV.y * widthD + in1; + } /* * Normal. The wedge bend is a reflection of the template: @@ -2512,10 +2552,21 @@ bool computeSegmentBand(const RoadStraightSegment &segment, c[0].y = c[3].y = y0; c[1].y = c[2].y = yL; - uvc[0] = Ogre::Vector2(halfEdgeU(he, graph, t0), 0.0f); - uvc[1] = Ogre::Vector2(halfEdgeU(he, graph, L), 0.0f); - uvc[2] = Ogre::Vector2(halfEdgeU(he, graph, L), inW + outW); - uvc[3] = Ogre::Vector2(halfEdgeU(he, graph, t0), inW + outW); + if (atlasUVsEnabled(graph)) { + /* U across the asphalt band (0 at the -r curb, 0.5 at +r), + * V tiles along the road. */ + float v0 = atlasV(graph, halfEdgeU(he, graph, t0)); + float v1 = atlasV(graph, halfEdgeU(he, graph, L)); + uvc[0] = Ogre::Vector2(0.0f, v0); + uvc[1] = Ogre::Vector2(0.0f, v1); + uvc[2] = Ogre::Vector2(0.5f, v1); + uvc[3] = Ogre::Vector2(0.5f, v0); + } else { + uvc[0] = Ogre::Vector2(halfEdgeU(he, graph, t0), 0.0f); + uvc[1] = Ogre::Vector2(halfEdgeU(he, graph, L), 0.0f); + uvc[2] = Ogre::Vector2(halfEdgeU(he, graph, L), inW + outW); + uvc[3] = Ogre::Vector2(halfEdgeU(he, graph, t0), inW + outW); + } return true; } @@ -2650,10 +2701,18 @@ static void transformSidewalkVertices(Procedural::TriangleBuffer &strip, } float worldY = surfY + lift + v.mPosition.y; - /* UVs: phase-continuous u, v across [0, sidewalkWidth]. */ - v.mUV.x = (d <= L1) ? halfEdgeU(h1, graph, L1 - d) - : halfEdgeU(h2, graph, d - L1); - v.mUV.y = v.mUV.y * sw; + /* UVs: phase-continuous u, v across [0, sidewalkWidth]; with + * road-atlas UVs (textureTileLength > 0) U selects the + * sidewalk band (0.5-0.75) and V tiles along the road. */ + float uMeters = (d <= L1) ? halfEdgeU(h1, graph, L1 - d) + : halfEdgeU(h2, graph, d - L1); + if (atlasUVsEnabled(graph)) { + v.mUV.x = 0.5f + 0.25f * v.mUV.y; + v.mUV.y = atlasV(graph, uMeters); + } else { + v.mUV.x = uMeters; + v.mUV.y = v.mUV.y * sw; + } /* Normals: same explicit axis mapping as the road (the * bend is a reflection of the template). */ @@ -3009,10 +3068,20 @@ bool computeSegmentSidewalkBand(const RoadStraightSegment &segment, c[0].y = c[3].y = halfEdgeHeightAt(he, graph, t0) + sh; c[1].y = c[2].y = halfEdgeHeightAt(he, graph, L) + sh; - uvc[0] = Ogre::Vector2(halfEdgeU(he, graph, t0), 0.0f); - uvc[1] = Ogre::Vector2(halfEdgeU(he, graph, L), 0.0f); - uvc[2] = Ogre::Vector2(halfEdgeU(he, graph, L), sw); - uvc[3] = Ogre::Vector2(halfEdgeU(he, graph, t0), sw); + if (atlasUVsEnabled(graph)) { + /* Sidewalk band (U 0.5-0.75), V tiles along the road. */ + float v0 = atlasV(graph, halfEdgeU(he, graph, t0)); + float v1 = atlasV(graph, halfEdgeU(he, graph, L)); + uvc[0] = Ogre::Vector2(0.5f, v0); + uvc[1] = Ogre::Vector2(0.5f, v1); + uvc[2] = Ogre::Vector2(0.75f, v1); + uvc[3] = Ogre::Vector2(0.75f, v0); + } else { + uvc[0] = Ogre::Vector2(halfEdgeU(he, graph, t0), 0.0f); + uvc[1] = Ogre::Vector2(halfEdgeU(he, graph, L), 0.0f); + uvc[2] = Ogre::Vector2(halfEdgeU(he, graph, L), sw); + uvc[3] = Ogre::Vector2(halfEdgeU(he, graph, t0), sw); + } return true; } @@ -3205,10 +3274,18 @@ static void transformApronVertices(Procedural::TriangleBuffer &strip, v.mPosition.x * drop * xSlope + v.mPosition.y; - /* UVs: phase-continuous u, v across [0, apronWidth]. */ - v.mUV.x = (d <= L1) ? halfEdgeU(h1, graph, L1 - d) - : halfEdgeU(h2, graph, d - L1); - v.mUV.y = v.mUV.y * aw; + /* UVs: phase-continuous u, v across [0, apronWidth]; with + * road-atlas UVs (textureTileLength > 0) U selects the + * apron band (0.75-1.0) and V tiles along the road. */ + float uMeters = (d <= L1) ? halfEdgeU(h1, graph, L1 - d) + : halfEdgeU(h2, graph, d - L1); + if (atlasUVsEnabled(graph)) { + v.mUV.x = 0.75f + 0.25f * v.mUV.y; + v.mUV.y = atlasV(graph, uMeters); + } else { + v.mUV.x = uMeters; + v.mUV.y = v.mUV.y * aw; + } /* Normals: same explicit axis mapping as the road (the * bend is a reflection of the template). */ @@ -3375,10 +3452,20 @@ bool buildSegmentApronGeometry(const RoadStraightSegment &segment, c[3].y = c[0].y - drop; Ogre::Vector2 uvc[4]; - uvc[0] = Ogre::Vector2(halfEdgeU(he, graph, t0), 0.0f); - uvc[1] = Ogre::Vector2(halfEdgeU(he, graph, L), 0.0f); - uvc[2] = Ogre::Vector2(halfEdgeU(he, graph, L), aw); - uvc[3] = Ogre::Vector2(halfEdgeU(he, graph, t0), aw); + if (atlasUVsEnabled(graph)) { + /* Apron band (U 0.75-1.0), V tiles along the road. */ + float v0 = atlasV(graph, halfEdgeU(he, graph, t0)); + float v1 = atlasV(graph, halfEdgeU(he, graph, L)); + uvc[0] = Ogre::Vector2(0.75f, v0); + uvc[1] = Ogre::Vector2(0.75f, v1); + uvc[2] = Ogre::Vector2(1.0f, v1); + uvc[3] = Ogre::Vector2(1.0f, v0); + } else { + uvc[0] = Ogre::Vector2(halfEdgeU(he, graph, t0), 0.0f); + uvc[1] = Ogre::Vector2(halfEdgeU(he, graph, L), 0.0f); + uvc[2] = Ogre::Vector2(halfEdgeU(he, graph, L), aw); + uvc[3] = Ogre::Vector2(halfEdgeU(he, graph, t0), aw); + } /* The slab carries the apron skirt: bottom face and outer * wall extend apronSkirtDepth below the sloped top so the @@ -3430,6 +3517,448 @@ bool buildSegmentApronGeometry(const RoadStraightSegment &segment, return built; } +/* ---------------------------------------------------------------- + * Road markings (phase 2.5) + * + * Painted overlay strips sampled from the same wedge centerline/curb + * chains as the road surface, emitted slightly above the road top so + * they read as paint. Purely visual: these buffers never feed + * physics, navmesh or terrain compliance. + * + * Per-wedge coverage rule (each piece painted exactly once): a wedge's + * first arm (d in [0, L1]) paints the centerline plus the OUTGOING + * side of its edge (dashed lane lines between same-direction lanes and + * the solid curb edge line); the second arm paints only the INCOMING + * side of its edge. Every half-edge is the first arm of exactly one + * wedge and the second arm of exactly one wedge, so the centerline and + * both road sides of every edge are painted exactly once. At junction + * nodes (degree > 2) every arm stops short of the corner by a clear + * zone (the fillet extent, or the widest incident half-width when no + * fillet is active, plus markingStopSetback) and the incoming arm of + * each wedge carries a stop line at the clear boundary; at 2-neighbor + * nodes the markings follow the corner through (no clear zone, no stop + * line). The corner pin zone is cut from all ranges. + * ---------------------------------------------------------------- */ + +static const float ROAD_MARKING_LIFT = 0.02f; +static const float ROAD_DECAL_LIFT = 0.025f; +static const float ROAD_MARKING_STEP = 0.5f; + +/** One paint-height sample of a marking strip row. */ +struct MarkingSample { + /** XZ at the road centerline, Y at paint height. */ + Ogre::Vector3 center; + /** Normalized horizontal direction toward the covered side. */ + Ogre::Vector3 latDir; + /** Curb distance from the centerline along latDir. */ + float widthD = 0.0f; +}; + +static bool sampleWedgeMarking(const RoadWedge &wedge, + const RoadGraph &graph, float d, float yO, + float halfThick, MarkingSample &s) +{ + const RoadHalfEdge &h1 = wedge.first; + const RoadHalfEdge &h2 = wedge.second; + float L1 = h1.halfLength > 1e-4f ? h1.halfLength : 1e-4f; + + s.center = wedgeCenterAt(wedge, graph, d); + Ogre::Vector3 off = computeCurbOffset(wedge, graph, d); + s.widthD = off.length(); + s.latDir = off; + if (s.latDir.length() < 1e-4f) + s.latDir = (d <= L1) ? roadRightVec(h1.direction) : + -roadRightVec(h2.direction); + else + s.latDir.normalise(); + + float surfY; + if (d < L1 - 1e-4f) + surfY = halfEdgeHeightAt(h1, graph, L1 - d); + else if (d > L1 + 1e-4f) + surfY = halfEdgeHeightAt(h2, graph, d - L1); + else + surfY = yO; + s.center.y = surfY + halfThick + ROAD_MARKING_LIFT; + return s.widthD > 1e-3f; +} + +static void emitMarkingQuad(Procedural::TriangleBuffer &out, + const MarkingSample &a, const MarkingSample &b, + float latA0, float latA1, float latB0, float latB1) +{ + Ogre::Vector3 p00 = a.center + a.latDir * latA0; + Ogre::Vector3 p01 = a.center + a.latDir * latA1; + Ogre::Vector3 p10 = b.center + b.latDir * latB0; + Ogre::Vector3 p11 = b.center + b.latDir * latB1; + Ogre::Vector2 uv(0.0f, 0.0f); + emitTri(out, p00, p10, p11, uv, uv, uv); + emitTri(out, p00, p11, p01, uv, uv, uv); +} + +/** + * Emit one solid marking strip between path distances d0 and d1. + * When @p curbRelative is false, [lat0, lat1] are fixed lateral offsets + * from the centerline; when true, lat0 is the curb inset and lat1 the + * line width (the strip follows the curb at widthD - inset). + */ +static void emitMarkingLine(Procedural::TriangleBuffer &out, + const RoadWedge &wedge, const RoadGraph &graph, + float d0, float d1, float yO, float halfThick, + float lat0, float lat1, bool curbRelative) +{ + if (d1 - d0 < 1e-3f) + return; + int rows = std::max(1, (int)std::ceil((d1 - d0) / ROAD_MARKING_STEP)); + MarkingSample prev, cur; + if (!sampleWedgeMarking(wedge, graph, d0, yO, halfThick, prev)) + return; + for (int i = 1; i <= rows; ++i) { + float d = d0 + (d1 - d0) * (float)i / (float)rows; + if (!sampleWedgeMarking(wedge, graph, d, yO, halfThick, cur)) + return; + float a0, a1, b0, b1; + if (curbRelative) { + a1 = prev.widthD - lat0; + a0 = a1 - lat1; + b1 = cur.widthD - lat0; + b0 = b1 - lat1; + /* Pinched corner rows narrower than inset+width: + * clamp to a sliver next to the centerline. */ + a0 = std::max(a0, 0.02f); + b0 = std::max(b0, 0.02f); + if (a0 >= a1 - 1e-4f || b0 >= b1 - 1e-4f) { + prev = cur; + continue; + } + } else { + a0 = b0 = lat0; + a1 = b1 = lat1; + /* Skip quads that would hang past the curb. */ + if (lat1 > prev.widthD - 0.02f || + lat1 > cur.widthD - 0.02f) { + prev = cur; + continue; + } + } + emitMarkingQuad(out, prev, cur, a0, a1, b0, b1); + prev = cur; + } +} + +static void emitMarkingDashed(Procedural::TriangleBuffer &out, + const RoadWedge &wedge, const RoadGraph &graph, + float d0, float d1, float yO, float halfThick, + float latCenter, float lineWidth) +{ + float dl = graph.config.markingDashLength; + float dg = graph.config.markingDashGap; + float s = d0; + while (s < d1 - 1e-3f) { + float e = std::min(s + dl, d1); + emitMarkingLine(out, wedge, graph, s, e, yO, halfThick, + latCenter - lineWidth * 0.5f, + latCenter + lineWidth * 0.5f, false); + s = e + dg; + } +} + +/** Subtract the corner pin zone [ps, pe] from [d0, d1]. */ +static void cutPinZone(float d0, float d1, float ps, float pe, bool pin, + std::vector> &out) +{ + if (!pin) { + out.push_back(std::make_pair(d0, d1)); + return; + } + if (d0 < ps - 1e-4f) + out.push_back(std::make_pair(d0, std::min(d1, ps))); + if (d1 > pe + 1e-4f) + out.push_back(std::make_pair(std::max(d0, pe), d1)); +} + +/** Widest incident half-width at a node (junction clear base). */ +static float junctionHalfWidth(const RoadGraph &graph, int nodeId) +{ + float w = 0.0f; + for (int nid : graph.getNeighborIds(nodeId)) { + int ei = graph.findEdgeIndex(nodeId, nid); + if (ei < 0) + continue; + int lo = 0, li = 0; + graph.resolveLaneCounts(graph.edges[(size_t)ei], nodeId, lo, + li); + w = std::max(w, (lo + li) * graph.config.laneWidth * 0.5f); + } + return w; +} + +bool buildWedgeMarkings(const RoadWedge &wedge, const RoadGraph &graph, + RoadMarkingBuffers &out) +{ + if (wedge.degenerate) + return false; + const RoadNode *node = graph.findNodeById(wedge.nodeId); + if (!node) + return false; + + const RoadHalfEdge &h1 = wedge.first; + const RoadHalfEdge &h2 = wedge.second; + const RoadConfig &cfg = graph.config; + float L1 = h1.halfLength > 1e-4f ? h1.halfLength : 1e-4f; + float L2 = h2.halfLength > 1e-4f ? h2.halfLength : 1e-4f; + float lw = cfg.laneWidth; + float lineW = cfg.markingLineWidth; + float yO = node->position.y + nodeRoadLevel(graph, wedge.nodeId); + float halfThick = std::max(0.01f, cfg.roadThickness) * 0.5f; + + bool junction = graph.getNeighborIds(wedge.nodeId).size() > 2; + WedgeFillet f = wedgeFillet(wedge, graph); + float clearBase = junctionHalfWidth(graph, wedge.nodeId); + + float ps = 0.0f, pe = 0.0f; + bool pin = filletPinZone(wedge, graph, ps, pe, nullptr); + + bool built = false; + for (int arm = 0; arm < 2; ++arm) { + const RoadHalfEdge &h = arm == 0 ? h1 : h2; + if (h.edgeIndex < 0 || h.edgeIndex >= (int)graph.edges.size()) + continue; + if (!graph.edgeMarkingsEnabled( + graph.edges[(size_t)h.edgeIndex])) + continue; + + /* Covered side lane count: a wedge paints its first arm's + * OUTGOING side and its second arm's INCOMING side. */ + int sideLanes = arm == 0 ? h.lanesOut : h.lanesIn; + float clear = junction ? (f.active ? (arm == 0 ? f.f1 : f.f2) + : clearBase) + + cfg.markingStopSetback + : 0.0f; + float d0 = arm == 0 ? 0.0f : L1 + clear; + float d1 = arm == 0 ? L1 - clear : L1 + L2; + if (d1 - d0 < 1e-3f) + continue; + + /* Stop line: junctions only, on the incoming half — the + * second arm of the wedge covers the incoming side. */ + float sw = cfg.markingStopWidth; + bool stopLine = junction && arm == 1 && h.lanesIn > 0 && + L1 + clear + sw < L1 + L2 - 1e-3f; + + /* When a stop line is emitted the longitudinal lines of this + * arm resume past it, so the perpendicular bar never + * z-fights the lane/edge strips crossing its span. */ + std::vector> ranges; + cutPinZone(stopLine ? L1 + clear + sw : d0, d1, ps, pe, pin, + ranges); + + for (const auto &rg : ranges) { + /* Centerline: only the first arm (each half-edge + * is the first arm of exactly one wedge). */ + if (arm == 0) { + if (h.lanesOut >= 2 || h.lanesIn >= 2) { + float g = lineW * 0.5f; + emitMarkingLine(out.centerline, wedge, + graph, rg.first, + rg.second, yO, + halfThick, -g - lineW, + -g, false); + emitMarkingLine(out.centerline, wedge, + graph, rg.first, + rg.second, yO, + halfThick, g, + g + lineW, false); + } else { + emitMarkingDashed(out.centerline, wedge, + graph, rg.first, + rg.second, yO, + halfThick, 0.0f, + lineW); + } + } + /* Dashed lines between same-direction lanes. */ + for (int k = 1; k < sideLanes; ++k) + emitMarkingDashed(out.laneLines, wedge, graph, + rg.first, rg.second, yO, + halfThick, k * lw, lineW); + /* Solid edge line at the curb. */ + emitMarkingLine(out.edgeLines, wedge, graph, rg.first, + rg.second, yO, halfThick, + cfg.markingEdgeInset, lineW, true); + built = true; + } + + if (stopLine) { + MarkingSample sa, sb; + if (sampleWedgeMarking(wedge, graph, L1 + clear, yO, + halfThick, sa) && + sampleWedgeMarking(wedge, graph, L1 + clear + sw, + yO, halfThick, sb)) { + /* The bar starts flush at the centerline + * strip's outer edge (the centerline painted + * by the sibling wedge reaches the clear + * boundary at the same d), so the two never + * overlap coplanar. */ + float latA = (h.lanesOut >= 2 || h.lanesIn >= 2) ? + 1.5f * lineW : + 0.5f * lineW; + float latB = h.lanesIn * lw; + latB = std::min( + latB, + std::min(sa.widthD, sb.widthD) - 0.01f); + if (latB > latA + 0.05f) { + emitMarkingQuad(out.stopLines, sa, sb, + latA, latB, latA, latB); + built = true; + } + } + } + } + return built; +} + +bool buildSegmentMarkings(const RoadStraightSegment &segment, + const RoadGraph &graph, RoadMarkingBuffers &out) +{ + const RoadNode *node = graph.findNodeById(segment.nodeId); + if (!node) + return false; + const RoadHalfEdge &he = segment.halfEdge; + if (he.lanesIn + he.lanesOut < 1) + return false; + if (he.edgeIndex < 0 || he.edgeIndex >= (int)graph.edges.size()) + return false; + if (!graph.edgeMarkingsEnabled(graph.edges[(size_t)he.edgeIndex])) + return false; + + const RoadConfig &cfg = graph.config; + float lw = cfg.laneWidth; + float lineW = cfg.markingLineWidth; + float inW = he.lanesIn * lw; + float outW = he.lanesOut * lw; + float L = he.halfLength; + float halfThick = std::max(0.01f, cfg.roadThickness) * 0.5f; + + const Ogre::Vector3 &O = node->position; + Ogre::Vector3 d = he.direction; + Ogre::Vector3 r = roadRightVec(d); + + /* Straight sampler: lat < 0 is the incoming (-r) side. */ + auto sampleAt = [&](float t, MarkingSample &s) { + s.center = O + d * t; + s.center.y = halfEdgeHeightAt(he, graph, t) + halfThick + + ROAD_MARKING_LIFT; + s.latDir = r; + s.widthD = std::max(inW, outW); + }; + auto emitSeg = [&](Procedural::TriangleBuffer &buf, float ta, float tb, + float lat0, float lat1) { + MarkingSample sa, sb; + sampleAt(ta, sa); + sampleAt(tb, sb); + emitMarkingQuad(buf, sa, sb, lat0, lat1, lat0, lat1); + }; + auto line = [&](Procedural::TriangleBuffer &buf, float lat0, + float lat1) { + int rows = std::max(1, (int)std::ceil(L / ROAD_MARKING_STEP)); + for (int i = 0; i < rows; ++i) + emitSeg(buf, L * (float)i / (float)rows, + L * (float)(i + 1) / (float)rows, lat0, lat1); + }; + auto dashed = [&](Procedural::TriangleBuffer &buf, float latC) { + float s = 0.0f; + while (s < L - 1e-3f) { + float e = std::min(s + cfg.markingDashLength, L); + int rows = std::max( + 1, (int)std::ceil((e - s) / ROAD_MARKING_STEP)); + for (int i = 0; i < rows; ++i) + emitSeg(buf, s + (e - s) * (float)i / + (float)rows, + s + (e - s) * (float)(i + 1) / + (float)rows, + latC - lineW * 0.5f, + latC + lineW * 0.5f); + s = e + cfg.markingDashGap; + } + }; + + if (he.lanesOut >= 2 || he.lanesIn >= 2) { + float g = lineW * 0.5f; + line(out.centerline, -g - lineW, -g); + line(out.centerline, g, g + lineW); + } else { + dashed(out.centerline, 0.0f); + } + for (int k = 1; k < he.lanesOut; ++k) + dashed(out.laneLines, k * lw); + for (int k = 1; k < he.lanesIn; ++k) + dashed(out.laneLines, -(float)k * lw); + /* Solid edge lines at both curbs; dead ends have no stop lines. */ + if (outW > cfg.markingEdgeInset + lineW) + line(out.edgeLines, outW - cfg.markingEdgeInset - lineW, + outW - cfg.markingEdgeInset); + if (inW > cfg.markingEdgeInset + lineW) + line(out.edgeLines, -(inW - cfg.markingEdgeInset), + -(inW - cfg.markingEdgeInset - lineW)); + return true; +} + +void buildEdgeDecals(const RoadGraph &graph, size_t edgeIndex, + Procedural::TriangleBuffer &out, int ownerNodeId) +{ + if (edgeIndex >= graph.edges.size()) + return; + const RoadEdge &e = graph.edges[edgeIndex]; + if (e.decals.empty()) + return; + const RoadNode *na = graph.findNodeById(e.nodeA); + const RoadNode *nb = graph.findNodeById(e.nodeB); + if (!na || !nb) + return; + + const RoadConfig &cfg = graph.config; + float halfThick = std::max(0.01f, cfg.roadThickness) * 0.5f; + Ogre::Vector3 A = na->position, B = nb->position; + Ogre::Vector3 dir = B - A; + dir.y = 0.0f; + float len = dir.normalise(); + if (len < 1e-3f) + return; + Ogre::Vector3 r = roadRightVec(dir); + float yA = A.y + e.roadLevelA; + float yB = B.y + e.roadLevelB; + float yMid = 0.5f * (yA + yB); + float halfLen = len * 0.5f; + + for (const RoadEdgeDecal &dec : e.decals) { + float te = std::max(0.0f, std::min(1.0f, dec.edgeT)); + if (ownerNodeId >= 0) { + int owner = te <= 0.5f ? e.nodeA : e.nodeB; + if (owner != ownerNodeId) + continue; + } + float t = te * len; + /* Piecewise-linear surface height with the midpoint kink. */ + float surfY = t <= halfLen ? + yA + (yMid - yA) * (t / halfLen) : + yMid + (yB - yMid) * ((t - halfLen) / halfLen); + Ogre::Vector3 c = A + dir * t + r * dec.lateralOffset; + c.y = surfY + halfThick + ROAD_DECAL_LIFT; + float hs = dec.size * 0.5f; + Ogre::Vector3 p00 = c - dir * hs - r * hs; + Ogre::Vector3 p01 = c - dir * hs + r * hs; + Ogre::Vector3 p10 = c + dir * hs - r * hs; + Ogre::Vector3 p11 = c + dir * hs + r * hs; + Ogre::Vector2 uv00(dec.uvX, dec.uvY); + Ogre::Vector2 uv01(dec.uvX, dec.uvY + dec.uvH); + Ogre::Vector2 uv10(dec.uvX + dec.uvW, dec.uvY); + Ogre::Vector2 uv11(dec.uvX + dec.uvW, dec.uvY + dec.uvH); + emitTri(out, p00, p10, p11, uv00, uv10, uv11); + emitTri(out, p00, p11, p01, uv00, uv11, uv01); + } +} + /* ---------------------------------------------------------------- * Template mesh loading * ---------------------------------------------------------------- */ diff --git a/src/features/editScene/roadlib/RoadGeometryLib.hpp b/src/features/editScene/roadlib/RoadGeometryLib.hpp index 919270c..2f14321 100644 --- a/src/features/editScene/roadlib/RoadGeometryLib.hpp +++ b/src/features/editScene/roadlib/RoadGeometryLib.hpp @@ -121,6 +121,63 @@ bool buildSegmentApronGeometry(const RoadStraightSegment &segment, const RoadGraph &graph, Procedural::TriangleBuffer &out); +/* ---------------------------------------------------------------- + * Road markings and decals (phase 2.5) + * ---------------------------------------------------------------- */ + +/** + * Output buckets for buildWedgeMarkings / buildSegmentMarkings. The + * kinds are kept apart so the caller can merge them into one overlay + * mesh (same material) while tests can count them separately. + */ +struct RoadMarkingBuffers { + Procedural::TriangleBuffer centerline; + Procedural::TriangleBuffer laneLines; + Procedural::TriangleBuffer edgeLines; + Procedural::TriangleBuffer stopLines; +}; + +/** + * Build the painted marking strips of one wedge (phase 2.5) and append + * to @p out: centerline (double solid when either direction has 2+ + * lanes, single dashed otherwise), dashed lane lines between + * same-direction lanes, solid edge lines at the curbs and — at junction + * nodes (degree > 2) — a stop line on the incoming half of the second + * arm (flush at the centerline strip; the arm's longitudinal lines + * resume past it so the bar never z-fights them). Junction arms stop + * short of the corner by a clear zone; at + * 2-neighbor nodes markings follow the corner through. Per-edge + * gating via RoadGraph::edgeMarkingsEnabled. + * + * The strips hover ROAD_MARKING_LIFT above the road top and are purely + * visual (no physics/navmesh/compliance contribution). + * + * @return false when the wedge is degenerate or nothing was emitted. + */ +bool buildWedgeMarkings(const RoadWedge &wedge, const RoadGraph &graph, + RoadMarkingBuffers &out); + +/** + * Build the marking strips of a dead-end straight segment and append + * to @p out. Same rules as buildWedgeMarkings minus the junction + * handling (no clear zone, no stop lines). + */ +bool buildSegmentMarkings(const RoadStraightSegment &segment, + const RoadGraph &graph, RoadMarkingBuffers &out); + +/** + * Build the decal quads of one edge (phase 2.5) and append to @p out. + * Each RoadEdgeDecal becomes one atlas-textured quad hovering just + * above the road top (and above the marking strips). Purely visual. + * + * When @p ownerNodeId is >= 0 only the decals owned by that node are + * emitted — a decal on the first half of the edge (edgeT <= 0.5) + * belongs to nodeA, the rest to nodeB. RoadSystem uses this so a + * cross-page edge does not paint its decals on both pages. + */ +void buildEdgeDecals(const RoadGraph &graph, size_t edgeIndex, + Procedural::TriangleBuffer &out, int ownerNodeId = -1); + /* ---------------------------------------------------------------- * Pipeline phases (exposed for testing) * ---------------------------------------------------------------- */ diff --git a/src/features/editScene/roadlib/RoadNetworkGen.cpp b/src/features/editScene/roadlib/RoadNetworkGen.cpp new file mode 100644 index 0000000..e3ec07b --- /dev/null +++ b/src/features/editScene/roadlib/RoadNetworkGen.cpp @@ -0,0 +1,1042 @@ +#include "RoadNetworkGen.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace RoadNetworkGen { + +/* ---------------------------------------------------------------- + * Small XZ-plane geometry helpers (positions are Ogre::Vector3, the + * "y" of the 2D math is the world Z coordinate). + * ---------------------------------------------------------------- */ + +static float xzDist(const Ogre::Vector3 &a, const Ogre::Vector3 &b) +{ + float dx = a.x - b.x, dz = a.z - b.z; + return std::sqrt(dx * dx + dz * dz); +} + +/** + * Closest points between two XZ segments. Returns the parameters + * along each segment (clamped to [0, 1]); degenerate (zero-length) + * segments collapse to their point. + */ +static void segSegClosestXZ(const Ogre::Vector3 &a0, const Ogre::Vector3 &a1, + const Ogre::Vector3 &b0, const Ogre::Vector3 &b1, + float &outTA, float &outTB) +{ + float d1x = a1.x - a0.x, d1z = a1.z - a0.z; + float d2x = b1.x - b0.x, d2z = b1.z - b0.z; + float rx = a0.x - b0.x, rz = a0.z - b0.z; + + float a = d1x * d1x + d1z * d1z; + float e = d2x * d2x + d2z * d2z; + float f = d2x * rx + d2z * rz; + const float eps = 1e-12f; + + if (a <= eps && e <= eps) { + outTA = outTB = 0.0f; + return; + } + if (a <= eps) { + outTA = 0.0f; + outTB = std::max(0.0f, std::min(1.0f, f / e)); + return; + } + float c = d1x * rx + d1z * rz; + if (e <= eps) { + outTB = 0.0f; + outTA = std::max(0.0f, std::min(1.0f, -c / a)); + return; + } + float b = d1x * d2x + d1z * d2z; + float denom = a * e - b * b; + float tA = denom > eps ? + std::max(0.0f, + std::min(1.0f, (b * f - c * e) / denom)) : + 0.0f; + float tB = (b * tA + f) / e; + if (tB < 0.0f) { + tB = 0.0f; + tA = std::max(0.0f, std::min(1.0f, -c / a)); + } else if (tB > 1.0f) { + tB = 1.0f; + tA = std::max(0.0f, std::min(1.0f, (b - c) / a)); + } + outTA = tA; + outTB = tB; +} + +/** Signed doubled polygon area in XZ (positive = CCW with Z as "up"). */ +static double polySignedAreaXZ(const std::vector &poly) +{ + double s = 0.0; + for (size_t i = 0; i < poly.size(); ++i) { + const Ogre::Vector3 &p = poly[i]; + const Ogre::Vector3 &q = poly[(i + 1) % poly.size()]; + s += (double)p.x * (double)q.z - (double)q.x * (double)p.z; + } + return s * 0.5; +} + +/** Strict point-in-polygon test in XZ (ray cast, boundary excluded). */ +static bool pointInPolyXZ(const Ogre::Vector3 &p, + const std::vector &poly) +{ + bool inside = false; + for (size_t i = 0, j = poly.size() - 1; i < poly.size(); j = i++) { + const Ogre::Vector3 &a = poly[i]; + const Ogre::Vector3 &b = poly[j]; + if ((a.z > p.z) == (b.z > p.z)) + continue; + float xint = a.x + (p.z - a.z) * (b.x - a.x) / (b.z - a.z); + if (p.x < xint) + inside = !inside; + } + return inside; +} + +static float crossXZ(const Ogre::Vector3 &o, const Ogre::Vector3 &a, + const Ogre::Vector3 &b) +{ + return (a.x - o.x) * (b.z - o.z) - (a.z - o.z) * (b.x - o.x); +} + +/* Proper segment crossing: the segments intersect at a point strictly + * inside both (endpoint touching and collinear runs do not count, so a + * road running along a lot boundary is not a violation). */ +static bool segsCrossXZ(const Ogre::Vector3 &a0, const Ogre::Vector3 &a1, + const Ogre::Vector3 &b0, const Ogre::Vector3 &b1) +{ + const float eps = 1e-4f; + float d1 = crossXZ(b0, b1, a0); + float d2 = crossXZ(b0, b1, a1); + float d3 = crossXZ(a0, a1, b0); + float d4 = crossXZ(a0, a1, b1); + if (std::fabs(d1) < eps || std::fabs(d2) < eps || + std::fabs(d3) < eps || std::fabs(d4) < eps) + return false; /* touch, not cross */ + return ((d1 > 0.0f) != (d2 > 0.0f)) && + ((d3 > 0.0f) != (d4 > 0.0f)); +} + +/* Vertex-mean polygon centroid in XZ (strictly inside for convex + * polygons, which all fallback squares are; faces may be concave, + * where this is only a heuristic next to the edge-crossing test). */ +static Ogre::Vector3 polyCentroidXZ(const std::vector &p) +{ + Ogre::Vector3 c = Ogre::Vector3::ZERO; + for (const Ogre::Vector3 &v : p) { + c.x += v.x; + c.z += v.z; + } + float inv = p.empty() ? 1.0f : 1.0f / (float)p.size(); + c.x *= inv; + c.z *= inv; + return c; +} + +/* Polygon interior overlap in XZ: a proper edge crossing or one + * polygon containing the other's centroid. Vertices are NOT used for + * the containment test — a shared boundary vertex ray-casts as + * "inside" and would falsely reject lots that only touch (fallback + * squares built directly off a face's edge). */ +static bool polysOverlapXZ(const std::vector &a, + const std::vector &b) +{ + for (size_t i = 0; i < a.size(); ++i) + for (size_t j = 0; j < b.size(); ++j) + if (segsCrossXZ(a[i], a[(i + 1) % a.size()], + b[j], b[(j + 1) % b.size()])) + return true; + if (a.size() >= 3 && b.size() >= 3) { + if (pointInPolyXZ(polyCentroidXZ(a), b)) + return true; + if (pointInPolyXZ(polyCentroidXZ(b), a)) + return true; + } + return false; +} + +/* ---------------------------------------------------------------- + * Edge measures + * ---------------------------------------------------------------- */ + +float edgeHalfWidth(const RoadGraph &graph, const RoadEdge &edge) +{ + int lo = 0, li = 0; + graph.resolveLaneCounts(edge, edge.nodeA, lo, li); + float w = (lo + li) * graph.config.laneWidth * 0.5f; + if (graph.edgeSidewalkEnabled(edge, true)) + w += graph.config.sidewalkWidth; + if (graph.edgeSidewalkEnabled(edge, false)) + w += graph.config.sidewalkWidth; + return w; +} + +float edgeSurfaceYAt(const RoadGraph &graph, const RoadEdge &edge, float t) +{ + const RoadNode *na = graph.findNodeById(edge.nodeA); + const RoadNode *nb = graph.findNodeById(edge.nodeB); + if (!na || !nb) + return 0.0f; + float yA = na->position.y + edge.roadLevelA; + float yB = nb->position.y + edge.roadLevelB; + return yA + (yB - yA) * t; +} + +/* ---------------------------------------------------------------- + * Crossing detection + * ---------------------------------------------------------------- */ + +std::vector findCrossings(const RoadGraph &graph) +{ + std::vector out; + for (size_t i = 0; i < graph.edges.size(); ++i) { + const RoadEdge &ea = graph.edges[i]; + const RoadNode *a1 = graph.findNodeById(ea.nodeA); + const RoadNode *a2 = graph.findNodeById(ea.nodeB); + if (!a1 || !a2) + continue; + float hwA = edgeHalfWidth(graph, ea); + for (size_t j = i + 1; j < graph.edges.size(); ++j) { + const RoadEdge &eb = graph.edges[j]; + /* Edges sharing a node meet at a junction + * already. */ + if (ea.nodeA == eb.nodeA || ea.nodeA == eb.nodeB || + ea.nodeB == eb.nodeA || ea.nodeB == eb.nodeB) + continue; + const RoadNode *b1 = graph.findNodeById(eb.nodeA); + const RoadNode *b2 = graph.findNodeById(eb.nodeB); + if (!b1 || !b2) + continue; + + float tA, tB; + segSegClosestXZ(a1->position, a2->position, + b1->position, b2->position, tA, tB); + Ogre::Vector3 pa = a1->position + + (a2->position - a1->position) * tA; + Ogre::Vector3 pb = b1->position + + (b2->position - b1->position) * tB; + float dist = xzDist(pa, pb); + if (dist > hwA + edgeHalfWidth(graph, eb)) + continue; + + pa.y = edgeSurfaceYAt(graph, ea, tA); + pb.y = edgeSurfaceYAt(graph, eb, tB); + float dy = std::fabs(pa.y - pb.y); + if (dy > graph.config.crossingHeightThreshold) + continue; /* overpass/underpass */ + + RoadCrossing c; + c.nodeA1 = ea.nodeA; + c.nodeA2 = ea.nodeB; + c.nodeB1 = eb.nodeA; + c.nodeB2 = eb.nodeB; + c.tA = tA; + c.tB = tB; + c.pointA = pa; + c.pointB = pb; + c.xzDistance = dist; + c.yDifference = dy; + out.push_back(c); + } + } + return out; +} + +/* ---------------------------------------------------------------- + * Crossing resolution + * ---------------------------------------------------------------- */ + +/** + * Junction node for one side of a crossing: reuse a near end node, + * collapse a too-short edge, or split at the crossing parameter. + * Returns the junction node id, or -1 when the edge is gone. + */ +static int junctionNodeFor(RoadGraph &graph, int n1, int n2, float t, + float snap) +{ + int ei = graph.findEdgeIndex(n1, n2); + if (ei < 0) + return -1; + const RoadEdge &e = graph.edges[(size_t)ei]; + const RoadNode *pa = graph.findNodeById(e.nodeA); + const RoadNode *pb = graph.findNodeById(e.nodeB); + if (!pa || !pb) + return -1; + + /* The detection parameter is relative to the STORED edge + * direction; flip when the caller passed the endpoints in the + * other order. */ + float tA = (e.nodeA == n1) ? t : 1.0f - t; + float len = xzDist(pa->position, pb->position); + float dA = tA * len; + float dB = (1.0f - tA) * len; + if (dA <= snap) + return e.nodeA; + if (dB <= snap) + return e.nodeB; + if (len <= 2.0f * ROAD_MIN_EDGE_LENGTH) { + /* Too short to split: collapse the edge into the end + * nearer to the crossing point. */ + int keep = dA <= dB ? e.nodeA : e.nodeB; + int drop = dA <= dB ? e.nodeB : e.nodeA; + graph.mergeNodes(keep, drop); + return keep; + } + return graph.splitEdge((size_t)ei, tA); +} + +bool resolveCrossing(RoadGraph &graph, const RoadCrossing &crossing, + std::string *error) +{ + float snap = graph.config.crossingSnapDistance; + int jA = junctionNodeFor(graph, crossing.nodeA1, crossing.nodeA2, + crossing.tA, snap); + if (jA < 0) { + if (error) + *error = "crossing edge A no longer exists"; + return false; + } + int jB = junctionNodeFor(graph, crossing.nodeB1, crossing.nodeB2, + crossing.tB, snap); + if (jB < 0) { + if (error) + *error = "crossing edge B no longer exists"; + return false; + } + if (jA != jB) { + RoadNode *keep = graph.findNodeById(jA); + if (!keep) { + if (error) + *error = "junction node lost"; + return false; + } + keep->position = (crossing.pointA + crossing.pointB) * 0.5f; + graph.mergeNodes(jA, jB); + } + graph.bumpVersion(); + return true; +} + +int resolveAllCrossings(RoadGraph &graph, std::string *error, int maxPasses) +{ + int resolved = 0; + for (int pass = 0; pass < maxPasses; ++pass) { + std::vector crossings = findCrossings(graph); + if (crossings.empty()) + return resolved; + if (!resolveCrossing(graph, crossings.front(), error)) + return -1; + ++resolved; + } + if (error) + *error = "crossing resolution did not converge"; + return -1; +} + +/* ---------------------------------------------------------------- + * Sanity + * ---------------------------------------------------------------- */ + +bool checkSane(const RoadGraph &graph, std::string *error) +{ + std::set > seen; + for (const RoadEdge &e : graph.edges) { + if (!graph.findNodeById(e.nodeA) || + !graph.findNodeById(e.nodeB)) { + if (error) + *error = "edge references a missing node"; + return false; + } + if (e.nodeA == e.nodeB) { + if (error) + *error = "self-edge at node " + + std::to_string(e.nodeA); + return false; + } + std::pair key(std::min(e.nodeA, e.nodeB), + std::max(e.nodeA, e.nodeB)); + if (!seen.insert(key).second) { + if (error) + *error = "duplicate edge between nodes " + + std::to_string(key.first) + " and " + + std::to_string(key.second); + return false; + } + const RoadNode *na = graph.findNodeById(e.nodeA); + const RoadNode *nb = graph.findNodeById(e.nodeB); + if (xzDist(na->position, nb->position) < + ROAD_MIN_EDGE_LENGTH - 1e-4f) { + if (error) + *error = "sliver edge between nodes " + + std::to_string(key.first) + " and " + + std::to_string(key.second); + return false; + } + } + for (const RoadNode &n : graph.nodes) { + bool used = false; + for (const RoadEdge &e : graph.edges) { + if (e.nodeA == n.id || e.nodeB == n.id) { + used = true; + break; + } + } + if (!used) { + if (error) + *error = "isolated node " + + std::to_string(n.id); + return false; + } + } + return true; +} + +/* ---------------------------------------------------------------- + * Lots (enclosed faces) + * ---------------------------------------------------------------- */ + +std::vector findLots(const RoadGraph &graph, + const RoadLotFilter &filter) +{ + std::vector lots; + if (graph.edges.empty()) + return lots; + + /* Angle-sorted adjacency. */ + std::map > > adj; + for (const RoadEdge &e : graph.edges) { + const RoadNode *na = graph.findNodeById(e.nodeA); + const RoadNode *nb = graph.findNodeById(e.nodeB); + if (!na || !nb || na->position == nb->position) + continue; + float ab = std::atan2(nb->position.z - na->position.z, + nb->position.x - na->position.x); + float ba = std::atan2(na->position.z - nb->position.z, + na->position.x - nb->position.x); + adj[e.nodeA].push_back(std::make_pair(e.nodeB, ab)); + adj[e.nodeB].push_back(std::make_pair(e.nodeA, ba)); + } + for (auto &kv : adj) + std::sort(kv.second.begin(), kv.second.end(), + [](const std::pair &a, + const std::pair &b) { + return a.second < b.second; + }); + + /* + * Planar face walk: at every node take the sharpest right turn + * (the outgoing edge with the largest positive angle from the + * incoming heading in the atan2(z, x) sense - clockwise when + * viewed from +Y), which keeps the bounded face on the left and + * yields interior faces as CCW cycles (positive XZ signed area). + * The edge back to the previous node (U-turn) is excluded from + * the search: its angle from the heading is exactly +-pi, i.e. + * it sits on the wrap boundary and would be picked at random + * depending on float rounding. It is only taken at dead ends + * (dangling edges), where it makes the walk retrace the edge and + * get the face rejected below. + */ + std::set > visited; + size_t maxSteps = graph.edges.size() * 4 + 16; + for (const RoadEdge &e : graph.edges) { + for (int dir = 0; dir < 2; ++dir) { + int u0 = dir == 0 ? e.nodeA : e.nodeB; + int v0 = dir == 0 ? e.nodeB : e.nodeA; + if (!adj.count(u0) || + visited.count(std::make_pair(u0, v0))) + continue; + + std::vector path; + std::set > walked; + bool dupEdge = false, closed = false; + int u = u0, v = v0; + size_t steps = 0; + while (steps++ < maxSteps) { + if (u == u0 && v == v0 && !path.empty()) { + closed = true; + break; + } + if (!walked.insert(std::make_pair(u, v)) + .second) { + dupEdge = true; + break; + } + path.push_back(u); + /* Sharpest right turn at v: largest + * normalized angle from the incoming + * heading, ignoring the edge back to u + * unless it is the only way out. */ + const RoadNode *pu = graph.findNodeById(u); + const RoadNode *pv = graph.findNodeById(v); + if (!pu || !pv) + break; + float heading = std::atan2( + pv->position.z - pu->position.z, + pv->position.x - pu->position.x); + int bestW = -1; + float bestTurn = -(float)(2.0 * M_PI); + for (const auto &nb : adj[v]) { + if (nb.first == u && adj[v].size() > 1) + continue; + float t = nb.second - heading; + while (t >= (float)M_PI) + t -= (float)(2.0 * M_PI); + while (t < -(float)M_PI) + t += (float)(2.0 * M_PI); + if (t > bestTurn) { + bestTurn = t; + bestW = nb.first; + } + } + if (bestW < 0) + bestW = u; /* dead end: retrace */ + if (bestW < 0) + break; + u = v; + v = bestW; + } + for (const auto &w : walked) + visited.insert(w); + if (dupEdge || !closed || steps >= maxSteps || + path.size() < 3) + continue; + + /* Node repeats (other than the closure) mean a + * spur or figure-eight — not a clean lot. */ + bool dupNode = false; + { + std::set seen; + for (int id : path) + if (!seen.insert(id).second) { + dupNode = true; + break; + } + } + if (dupNode) + continue; + + std::vector poly; + for (int id : path) + poly.push_back( + graph.findNodeById(id)->position); + double area = polySignedAreaXZ(poly); + if (area <= 1e-6) + continue; /* exterior face (CW) */ + + /* Filter: too many boundary edges (not a simple + * building lot) or too small to fit the minimum + * square (bounding box in both XZ extents, and + * the area). */ + if ((int)path.size() > filter.maxBoundaryEdges) + continue; + if (area < (double)filter.minLotSize * + (double)filter.minLotSize) + continue; + { + float bbMinX = FLT_MAX, bbMinZ = FLT_MAX; + float bbMaxX = -FLT_MAX, bbMaxZ = -FLT_MAX; + for (const Ogre::Vector3 &p : poly) { + bbMinX = std::min(bbMinX, p.x); + bbMaxX = std::max(bbMaxX, p.x); + bbMinZ = std::min(bbMinZ, p.z); + bbMaxZ = std::max(bbMaxZ, p.z); + } + if (bbMaxX - bbMinX < filter.minLotSize || + bbMaxZ - bbMinZ < filter.minLotSize) + continue; + } + + /* Reject areas with another road node strictly + * inside (a detached edge crosses the lot). */ + bool innerNode = false; + for (const RoadNode &n : graph.nodes) { + bool onBoundary = false; + for (int id : path) + if (n.id == id) { + onBoundary = true; + break; + } + if (onBoundary) + continue; + if (pointInPolyXZ(n.position, poly)) { + innerNode = true; + break; + } + } + if (innerNode) + continue; + + RoadLot lot; + lot.nodeIds = path; + lot.polygon = poly; + lot.area = area; + /* Area-weighted centroid (XZ); Y averaged. */ + double cx = 0.0, cz = 0.0, ys = 0.0; + for (size_t i = 0; i < poly.size(); ++i) { + const Ogre::Vector3 &p = poly[i]; + const Ogre::Vector3 &q = + poly[(i + 1) % poly.size()]; + double cr = (double)p.x * (double)q.z - + (double)q.x * (double)p.z; + cx += ((double)p.x + (double)q.x) * cr; + cz += ((double)p.z + (double)q.z) * cr; + ys += (double)p.y; + } + lot.centroid.x = (float)(cx / (6.0 * area)); + lot.centroid.z = (float)(cz / (6.0 * area)); + lot.centroid.y = + (float)(ys / (double)poly.size()); + lots.push_back(lot); + } + } + + /* Fallback: square lots grown off edge sides that border no + * accepted enclosed lot. The square spans the full edge and + * reaches one edge length outward, so edges shorter than the + * minimum lot size spawn none. */ + if (filter.edgeFallback) { + /* Directed edges whose "left" side (the side a CCW face + * interior lies on) is covered by an accepted lot. */ + std::set > coveredLeft; + for (const RoadLot &lot : lots) + for (size_t i = 0; i < lot.nodeIds.size(); ++i) + coveredLeft.insert(std::make_pair( + lot.nodeIds[i], + lot.nodeIds[(i + 1) % + lot.nodeIds.size()])); + + for (const RoadEdge &e : graph.edges) { + const RoadNode *na = graph.findNodeById(e.nodeA); + const RoadNode *nb = graph.findNodeById(e.nodeB); + if (!na || !nb) + continue; + Ogre::Vector3 d = nb->position - na->position; + d.y = 0.0f; + float len = d.length(); + if (len < filter.minLotSize || len < 1e-4f) + continue; + d /= len; + for (int side = 0; side < 2; ++side) { + bool posSide = (side == 0); + if (coveredLeft.count( + posSide ? + std::make_pair(e.nodeA, + e.nodeB) : + std::make_pair(e.nodeB, + e.nodeA))) + continue; + /* "Left" of A->B in the atan2(z, x) + * sense (the side a CCW face interior + * lies on). */ + Ogre::Vector3 n = + posSide ? Ogre::Vector3(-d.z, 0.0f, + d.x) : + Ogre::Vector3(d.z, 0.0f, -d.x); + std::vector sq; + sq.push_back(na->position); + sq.push_back(nb->position); + sq.push_back(nb->position + n * len); + sq.push_back(na->position + n * len); + if (polySignedAreaXZ(sq) < 0.0) + std::reverse(sq.begin(), sq.end()); + + /* The square must stay clear of the + * graph: no node strictly inside (the + * edge's own endpoints excluded), no + * edge endpoint strictly inside and no + * edge properly crossing a side. */ + bool dirty = false; + for (const RoadNode &gn : graph.nodes) { + if (gn.id == e.nodeA || + gn.id == e.nodeB) + continue; + if (pointInPolyXZ(gn.position, sq)) { + dirty = true; + break; + } + } + for (const RoadEdge &ge : graph.edges) { + if (dirty) + break; + const RoadNode *ga = + graph.findNodeById(ge.nodeA); + const RoadNode *gb = + graph.findNodeById(ge.nodeB); + if (!ga || !gb) + continue; + bool endInside = false; + if (ge.nodeA != e.nodeA && + ge.nodeA != e.nodeB && + pointInPolyXZ(ga->position, sq)) + endInside = true; + if (ge.nodeB != e.nodeA && + ge.nodeB != e.nodeB && + pointInPolyXZ(gb->position, sq)) + endInside = true; + bool crossing = false; + if (!endInside) + for (size_t si = 0; + si < sq.size() && !crossing; + ++si) + crossing = segsCrossXZ( + sq[si], + sq[(si + 1) % + sq.size()], + ga->position, + gb->position); + if (endInside || crossing) + dirty = true; + } + if (dirty) + continue; + + /* And clear of every already-accepted + * lot (faces and earlier squares). */ + for (const RoadLot &lot : lots) + if (polysOverlapXZ(sq, lot.polygon)) { + dirty = true; + break; + } + if (dirty) + continue; + + RoadLot lot; + lot.polygon = sq; + lot.edgeFallback = true; + lot.sourceEdgeA = e.nodeA; + lot.sourceEdgeB = e.nodeB; + lot.area = polySignedAreaXZ(sq); + Ogre::Vector3 c = Ogre::Vector3::ZERO; + for (const Ogre::Vector3 &p : sq) + c += p; + lot.centroid = c / (float)sq.size(); + lots.push_back(lot); + } + } + } + return lots; +} + +/* ---------------------------------------------------------------- + * Streets + * ---------------------------------------------------------------- */ + +static std::pair normEdgeKey(int a, int b) +{ + return std::make_pair(std::min(a, b), std::max(a, b)); +} + +int nameStreet(RoadGraph &graph, const std::string &name, + const std::vector > &edges) +{ + std::set > wanted; + for (const auto &e : edges) { + std::pair key = normEdgeKey(e.first, e.second); + if (graph.findEdgeIndex(key.first, key.second) < 0) + return -1; + wanted.insert(key); + } + if (wanted.empty()) + return -1; + + /* Drop the edges from any street already holding them, and erase + * streets that went empty (also an old street with this name — it + * is redefined below). */ + for (auto it = graph.streets.begin(); it != graph.streets.end();) { + RoadStreet &s = *it; + if (s.name == name) { + it = graph.streets.erase(it); + continue; + } + for (auto eit = s.edges.begin(); eit != s.edges.end();) { + if (wanted.count(normEdgeKey(eit->first, + eit->second))) + eit = s.edges.erase(eit); + else + ++eit; + } + if (s.edges.empty()) + it = graph.streets.erase(it); + else + ++it; + } + + RoadStreet s; + s.name = name; + s.edges.assign(wanted.begin(), wanted.end()); + graph.streets.push_back(s); + graph.bumpVersion(); + return (int)graph.streets.size() - 1; +} + +const RoadStreet *findStreetForEdge(const RoadGraph &graph, int nodeA, + int nodeB) +{ + std::pair key = normEdgeKey(nodeA, nodeB); + for (const RoadStreet &s : graph.streets) + for (const auto &e : s.edges) + if (normEdgeKey(e.first, e.second) == key) + return &s; + return nullptr; +} + +/** Distance from a point to an XZ segment, plus the segment param. */ +static float pointSegDistXZ(const Ogre::Vector3 &p, const Ogre::Vector3 &a, + const Ogre::Vector3 &b, float &outT) +{ + float dx = b.x - a.x, dz = b.z - a.z; + float l2 = dx * dx + dz * dz; + float t = l2 > 1e-12f ? + ((p.x - a.x) * dx + (p.z - a.z) * dz) / l2 : + 0.0f; + t = std::max(0.0f, std::min(1.0f, t)); + Ogre::Vector3 q = a + (b - a) * t; + outT = t; + return xzDist(p, q); +} + +std::vector +assignLotAddresses(const RoadGraph &graph, const std::vector &lots) +{ + std::vector out(lots.size()); + + /* Nearest street per lot (by centroid-to-edge distance). */ + std::map > > perStreet; + for (size_t li = 0; li < lots.size(); ++li) { + int bestStreet = -1; + float bestDist = FLT_MAX; + float bestAlong = 0.0f; + for (size_t si = 0; si < graph.streets.size(); ++si) { + const RoadStreet &s = graph.streets[si]; + float along = 0.0f; + for (const auto &e : s.edges) { + const RoadNode *na = + graph.findNodeById(e.first); + const RoadNode *nb = + graph.findNodeById(e.second); + if (!na || !nb) + continue; + float t; + float d = pointSegDistXZ(lots[li].centroid, + na->position, + nb->position, t); + float segLen = + xzDist(na->position, nb->position); + if (d < bestDist) { + bestDist = d; + bestStreet = (int)si; + bestAlong = along + t * segLen; + } + along += segLen; + } + } + if (bestStreet >= 0) + perStreet[bestStreet].push_back( + std::make_pair(bestAlong, li)); + } + + /* Number lots sequentially along their street. */ + for (auto &kv : perStreet) { + std::sort(kv.second.begin(), kv.second.end()); + const std::string &sn = graph.streets[(size_t)kv.first].name; + int num = 1; + for (const auto &pr : kv.second) + out[pr.second] = sn + " " + std::to_string(num++); + } + for (size_t li = 0; li < lots.size(); ++li) + if (out[li].empty()) + out[li] = "Lot " + std::to_string(li + 1); + return out; +} + +/* ---------------------------------------------------------------- + * Grid generator + * ---------------------------------------------------------------- */ + +uint32_t nextGroupId(const RoadGraph &graph) +{ + uint32_t maxId = 0; + for (const RoadNode &n : graph.nodes) + if (n.generatedGroupId > maxId) + maxId = n.generatedGroupId; + return maxId + 1; +} + +uint32_t generateGrid(RoadGraph &graph, const GridParams ¶ms, + const HeightSampler &sampleHeight, std::string *error) +{ + if (params.blocksX < 1 || params.blocksZ < 1 || + params.blockSizeX < 2.0f * ROAD_MIN_EDGE_LENGTH || + params.blockSizeZ < 2.0f * ROAD_MIN_EDGE_LENGTH) { + if (error) + *error = "grid dimensions too small"; + return 0; + } + + uint32_t groupId = params.groupId ? params.groupId : + nextGroupId(graph); + std::mt19937 rng(params.seed); + std::uniform_real_distribution jr(-params.jitter, + params.jitter); + + std::vector > ids( + (size_t)params.blocksX + 1, + std::vector((size_t)params.blocksZ + 1, -1)); + for (int i = 0; i <= params.blocksX; ++i) { + for (int j = 0; j <= params.blocksZ; ++j) { + float x = params.origin.x + i * params.blockSizeX; + float z = params.origin.z + j * params.blockSizeZ; + bool interior = i > 0 && i < params.blocksX && j > 0 && + j < params.blocksZ; + if (interior && params.jitter > 0.0f) { + x += jr(rng); + z += jr(rng); + } + float y = params.origin.y; + if (sampleHeight) + y = sampleHeight(x, z); + if (params.avoidWater && y < params.waterLevel) + continue; /* rejected: over water */ + Ogre::Vector3 pos(x, y, z); + int id = graph.addNode(pos, 0.0f); + graph.findNodeById(id)->generatedGroupId = groupId; + ids[(size_t)i][(size_t)j] = id; + } + } + + auto tryEdge = [&](int a, int b) { + if (a < 0 || b < 0) + return; + if (params.maxSlope > 0.0f) { + const RoadNode *na = graph.findNodeById(a); + const RoadNode *nb = graph.findNodeById(b); + float run = xzDist(na->position, nb->position); + float rise = std::fabs(nb->position.y - + na->position.y); + if (run < 1e-4f || rise / run > params.maxSlope) + return; /* too steep */ + } + graph.addEdge(a, b); + }; + for (int i = 0; i <= params.blocksX; ++i) + for (int j = 0; j <= params.blocksZ; ++j) { + if (i < params.blocksX) + tryEdge(ids[(size_t)i][(size_t)j], + ids[(size_t)i + 1][(size_t)j]); + if (j < params.blocksZ) + tryEdge(ids[(size_t)i][(size_t)j], + ids[(size_t)i][(size_t)j + 1]); + } + + if (params.runCrossingResolution) { + std::string cerr; + if (resolveAllCrossings(graph, &cerr) < 0 && error) + *error = cerr; + } + graph.bumpVersion(); + return groupId; +} + +int connectGroupToNode(RoadGraph &graph, uint32_t groupId, int nodeId, + float worldSize, std::string *error) +{ + const RoadNode *target = graph.findNodeById(nodeId); + if (!target) { + if (error) + *error = "target node does not exist"; + return -1; + } + int bestId = -1; + float bestDist = FLT_MAX; + for (const RoadNode &n : graph.nodes) { + if (n.generatedGroupId != groupId) + continue; + float d = xzDist(n.position, target->position); + if (d < bestDist) { + bestDist = d; + bestId = n.id; + } + } + if (bestId < 0) { + if (error) + *error = "no nodes in the generated group"; + return -1; + } + int ei = graph.connectNodes(bestId, nodeId, worldSize, error); + if (ei < 0) + return -1; + std::string cerr; + if (resolveAllCrossings(graph, &cerr) < 0) { + if (error) + *error = cerr; + return -1; + } + return ei; +} + +bool removeGroup(RoadGraph &graph, uint32_t groupId) +{ + std::vector doomed; + for (const RoadNode &n : graph.nodes) + if (n.generatedGroupId == groupId) + doomed.push_back(n.id); + for (int id : doomed) + graph.removeNode(id); + if (!doomed.empty()) + graph.bumpVersion(); + return !doomed.empty(); +} + +/* ---------------------------------------------------------------- + * Streets serialization + * ---------------------------------------------------------------- */ + +void streetsToJson(const RoadGraph &graph, nlohmann::json &out) +{ + nlohmann::json arr = nlohmann::json::array(); + for (const RoadStreet &s : graph.streets) { + nlohmann::json js; + js["name"] = s.name; + nlohmann::json je = nlohmann::json::array(); + for (const auto &e : s.edges) + je.push_back({ e.first, e.second }); + js["edges"] = je; + arr.push_back(js); + } + out["streets"] = arr; +} + +void streetsFromJson(RoadGraph &graph, const nlohmann::json &j, + bool dropMissing) +{ + graph.streets.clear(); + for (const auto &js : j.value("streets", nlohmann::json::array())) { + RoadStreet s; + s.name = js.value("name", std::string()); + if (s.name.empty()) + continue; + for (const auto &je : + js.value("edges", nlohmann::json::array())) { + if (!je.is_array() || je.size() != 2) + continue; + int a = je[0].get(); + int b = je[1].get(); + if (dropMissing && graph.findEdgeIndex(a, b) < 0) + continue; + s.edges.push_back(std::make_pair(std::min(a, b), + std::max(a, b))); + } + if (!s.edges.empty()) + graph.streets.push_back(s); + } +} + +} /* namespace RoadNetworkGen */ diff --git a/src/features/editScene/roadlib/RoadNetworkGen.hpp b/src/features/editScene/roadlib/RoadNetworkGen.hpp new file mode 100644 index 0000000..9db04e6 --- /dev/null +++ b/src/features/editScene/roadlib/RoadNetworkGen.hpp @@ -0,0 +1,254 @@ +#ifndef EDITSCENE_ROADNETWORKGEN_HPP +#define EDITSCENE_ROADNETWORKGEN_HPP +#pragma once + +#include "components/RoadGraph.hpp" + +#include +#include +#include +#include +#include + +/** + * Road network generator and graph-level services (taxi demo phase 3). + * + * Everything here is a pure function of RoadGraph data (plus an optional + * terrain height sampler for the generator) — no scene, terrain or Ogre + * render state is required, so the whole namespace is exercisable by the + * headless road_geometry_overlap_test suite. + * + * Contents: + * - crossing detection/resolution: edges that come within road width + * of each other (and within RoadConfig::crossingHeightThreshold in + * Y — larger gaps are overpasses) can be turned into proper + * junctions by splitting both edges, reusing near end nodes or + * collapsing too-short edges; + * - lot detection: areas enclosed by road edges (planar faces of the + * graph) that no other edge intersects; + * - streets: printable/code-accessible names for edge sequences and + * street+number lot addressing; + * - a deterministic seeded grid generator emitting graphs stamped + * with a generatedGroupId (single unsplittable unit). + */ +namespace RoadNetworkGen { + +/** + * One detected crossing between two edges. Endpoint ids are stable + * across graph edits (edge vector indices are not, so they are not + * stored); @c tA/@c tB are the closest-point parameters along the + * stored edge direction (nodeA -> nodeB) at detection time — re-detect + * after every graph mutation instead of reusing stale values. + */ +struct RoadCrossing { + int nodeA1 = -1, nodeA2 = -1; /* endpoints of edge A */ + int nodeB1 = -1, nodeB2 = -1; /* endpoints of edge B */ + float tA = 0.0f, tB = 0.0f; + Ogre::Vector3 pointA = Ogre::Vector3::ZERO; /* on A, surface Y */ + Ogre::Vector3 pointB = Ogre::Vector3::ZERO; /* on B, surface Y */ + float xzDistance = 0.0f; + float yDifference = 0.0f; +}; + +/** + * Resolved half-width of an edge in meters: half the lanes plus the + * enabled sidewalks on both sides. Used as the crossing reach. + */ +float edgeHalfWidth(const RoadGraph &graph, const RoadEdge &edge); + +/** Road surface height at parameter @p t along the edge (node Y plus + * the lerped roadLevel). */ +float edgeSurfaceYAt(const RoadGraph &graph, const RoadEdge &edge, + float t); + +/** + * Detect all edge pairs that intersect without a junction: XZ distance + * between the closest points at most the sum of the edge half-widths + * AND Y separation at most RoadConfig::crossingHeightThreshold (a + * bigger Y gap is an overpass/underpass and is NOT a crossing). Edge + * pairs sharing a node are skipped. + */ +std::vector findCrossings(const RoadGraph &graph); + +/** + * Turn one crossing into a proper junction. For each of the two edges: + * an end node within RoadConfig::crossingSnapDistance meters of the + * crossing point is reused, an edge too short to split (at most + * 2 * ROAD_MIN_EDGE_LENGTH) is collapsed via mergeNodes into its end + * nearer to the crossing, anything else is split at the crossing + * parameter. The two resulting junction nodes are then merged into + * one (placed at the crossing midpoint). Best-effort: returns false + * with @p error set when the edges no longer exist. + */ +bool resolveCrossing(RoadGraph &graph, const RoadCrossing &crossing, + std::string *error = nullptr); + +/** + * Resolve crossings until none remain (re-detecting after every fix, + * one fix per pass). Returns the number of junctions created, or -1 + * when @p maxPasses was exhausted with crossings left (@p error set). + */ +int resolveAllCrossings(RoadGraph &graph, std::string *error = nullptr, + int maxPasses = 256); + +/** + * Structural sanity beyond RoadGraph::validate: no duplicate edge + * pairs, no sliver edges below ROAD_MIN_EDGE_LENGTH, no isolated + * (degree-0) nodes. validate() itself is NOT run (its wedge-angle + * rules reject hand-authored legacy scenes). + */ +bool checkSane(const RoadGraph &graph, std::string *error = nullptr); + +/** + * One enclosed area tracked as a potential building lot: a simple + * polygon of road nodes with no road edge crossing its interior, or + * (with RoadLotFilter::edgeFallback) a square grown off an edge side + * that borders no enclosed lot. + */ +struct RoadLot { + /** Boundary node ids in polygon order (EMPTY for edge-fallback + * squares — their corners are not graph nodes). */ + std::vector nodeIds; + /** Boundary positions (same order as nodeIds when non-empty). */ + std::vector polygon; + /** Polygon area in square meters (always positive). */ + double area = 0.0; + Ogre::Vector3 centroid = Ogre::Vector3::ZERO; + /** True for square lots grown off a free edge side (fallback) + * rather than enclosed by road edges. */ + bool edgeFallback = false; + /** Node ids of the source edge (edge-fallback lots only, -1 + * otherwise): the square was grown off one side of this edge, so + * it is the lot's road frontage — usable for street lookup and + * frontage orientation even though nodeIds stays empty. */ + int sourceEdgeA = -1; + int sourceEdgeB = -1; +}; + +/** Lot acceptance rules for findLots(). */ +struct RoadLotFilter { + /** A lot must fit a square of this side: both XZ bounding-box + * extents AND the polygon area (side squared) must reach it. + * Edge-fallback squares use the edge length as their side, so + * shorter edges spawn no fallback lot. */ + float minLotSize = 45.0f; + /** Enclosed faces bounded by more edges than this are not simple + * building lots and are discarded. */ + int maxBoundaryEdges = 8; + /** Grow square fallback lots off edge sides that border no + * accepted enclosed lot. */ + bool edgeFallback = true; +}; + +/** + * Enumerate the potential building lots of the road graph. Primary + * lots are the enclosed faces (planar face walk: sharpest right turn + * at each node, U-turn edge excluded). A face is REJECTED when its + * boundary walk traverses an edge twice or revisits a node (a + * spur/cul-de-sac pokes into it), when another graph node lies + * strictly inside it, when it is bounded by more than + * RoadLotFilter::maxBoundaryEdges edges, or when it cannot fit a + * RoadLotFilter::minLotSize square. With + * RoadLotFilter::edgeFallback, every edge side that borders no + * accepted face spawns a square lot (side = edge length, so edges + * shorter than minLotSize spawn none) provided the square stays clear + * of the graph (no node strictly inside, no edge crossing its + * interior) and does not overlap an already-accepted lot. Works for + * manual and procedural networks alike; run resolveAllCrossings first + * when edges may cross. + */ +std::vector findLots(const RoadGraph &graph, + const RoadLotFilter &filter = RoadLotFilter()); + +/** + * Assign @p name to the given edge set (normalized endpoint pairs). + * Edges are removed from any street already containing them; an + * existing street with the same name is redefined. Returns the street + * index, or -1 when any referenced edge does not exist. + */ +int nameStreet(RoadGraph &graph, const std::string &name, + const std::vector > &edges); + +/** Look up the street containing the edge (nodeA, nodeB), or nullptr. */ +const RoadStreet *findStreetForEdge(const RoadGraph &graph, int nodeA, + int nodeB); + +/** + * Build display addresses for lots: each lot is assigned to its + * nearest street; lots on one street are numbered sequentially along + * the street's direction; lots with no street get "Lot ". + * Returns one address string per input lot. + */ +std::vector +assignLotAddresses(const RoadGraph &graph, const std::vector &lots); + +/** Grid generator parameters (all deterministic under @p seed). */ +struct GridParams { + /** XZ origin of block corner (0, 0); Y fallback when no sampler. */ + Ogre::Vector3 origin = Ogre::Vector3::ZERO; + int blocksX = 3, blocksZ = 3; + float blockSizeX = 100.0f, blockSizeZ = 100.0f; + /** Max XZ jitter of interior intersections (boundary stays put). */ + float jitter = 0.0f; + uint32_t seed = 1; + /** Stamp for generated nodes; 0 = auto-assign (max existing + 1). */ + uint32_t groupId = 0; + /** Reject nodes whose sampled height is below waterLevel. */ + bool avoidWater = true; + float waterLevel = -0.1f; + /** Skip edges steeper than this rise/run (0 disables the check). */ + float maxSlope = 0.35f; + /** Run resolveAllCrossings after generating. */ + bool runCrossingResolution = true; +}; + +/** Terrain height sampler: render-space (x, z) -> surface Y. */ +typedef std::function HeightSampler; + +/** + * Emit a grid road network into the graph: (blocksX+1) x (blocksZ+1) + * intersections, seeded jitter on interior ones, edges along the grid + * lines. With a @p sampleHeight sampler, nodes below waterLevel are + * rejected (their edges go with them) and edges steeper than maxSlope + * are skipped; without a sampler every node sits at origin.y. Every + * created node is stamped with the returned generatedGroupId. + * Returns the group id used, or 0 on a fatal parameter error + * (@p error set). + */ +uint32_t generateGrid(RoadGraph &graph, const GridParams ¶ms, + const HeightSampler &sampleHeight, + std::string *error = nullptr); + +/** + * Connect a generated group to a manual node "as if the group were a + * road node": the closest group node is picked automatically and + * connected via RoadGraph::connectNodes (page-boundary splits when + * @p worldSize > 0), then crossings are resolved. Returns the first + * new edge index, or -1 on failure (@p error set). + */ +int connectGroupToNode(RoadGraph &graph, uint32_t groupId, int nodeId, + float worldSize = 0.0f, std::string *error = nullptr); + +/** Remove every node (and cascading edges) of a generated group. */ +bool removeGroup(RoadGraph &graph, uint32_t groupId); + +/** Largest generatedGroupId in the graph + 1 (1 when none). */ +uint32_t nextGroupId(const RoadGraph &graph); + +/** + * Streets serialization (shared by the region store, the scene + * serializer and the headless tests). Shape: + * `{ "streets": [ { "name": "...", "edges": [[a, b], ...] } ] }`; + * fromJson tolerates a missing/empty array. With @p dropMissing + * (default) entries whose edges no longer exist are dropped; pass + * false when loading a streaming terrain whose pages are not all in + * the graph yet (stale ids are harmless — lookups match existing + * edges only). + */ +void streetsToJson(const RoadGraph &graph, nlohmann::json &out); +void streetsFromJson(RoadGraph &graph, const nlohmann::json &j, + bool dropMissing = true); + +} /* namespace RoadNetworkGen */ + +#endif /* EDITSCENE_ROADNETWORKGEN_HPP */ diff --git a/src/features/editScene/roadlib/TownGen.cpp b/src/features/editScene/roadlib/TownGen.cpp new file mode 100644 index 0000000..c073791 --- /dev/null +++ b/src/features/editScene/roadlib/TownGen.cpp @@ -0,0 +1,382 @@ +#include "TownGen.hpp" + +#include +#include +#include +#include + +namespace TownGen { + +const DistrictParams &districtParams(District d) +{ + static const DistrictParams presets[(size_t)District::COUNT] = { + /* Village: small single-storey houses. */ + { "Village", 8.0f, 12.0f, 1, 1, 0.8f, 4.0f, + { 0.85f, 0.75f, 0.60f } }, + /* Town: row houses and small apartment blocks. */ + { "Town", 10.0f, 16.0f, 1, 2, 0.9f, 4.0f, + { 0.80f, 0.70f, 0.65f } }, + /* City residential: mid-rise apartment buildings. */ + { "City Residential", 12.0f, 20.0f, 2, 4, 0.95f, 3.0f, + { 0.70f, 0.70f, 0.75f } }, + /* City center: tall blocks on tight lots. */ + { "City Center", 16.0f, 30.0f, 4, 8, 1.0f, 2.0f, + { 0.65f, 0.68f, 0.72f } }, + }; + size_t idx = (size_t)d; + if (idx >= (size_t)District::COUNT) + idx = 0; + return presets[idx]; +} + +/** Stairs appear when the door-side ground is this far below the + * floor (meters). */ +static const float STAIRS_THRESHOLD = 0.35f; + +/** Extra gap kept between a building wall and the road body on top + * of the road half-width (meters). */ +static const float ROAD_CLEARANCE_MARGIN = 0.3f; + +/** Signed area of an XZ polygon (shoelace; sign follows winding). */ +static float polyAreaXZ(const std::vector &poly) +{ + float a = 0.0f; + for (size_t i = 0; i < poly.size(); ++i) { + const Ogre::Vector3 &p = poly[i]; + const Ogre::Vector3 &q = poly[(i + 1) % poly.size()]; + a += p.x * q.z - q.x * p.z; + } + return a * 0.5f; +} + +/** + * Half-extent of the road body on the lot side of a graph edge: + * all lanes plus the lot side's sidewalk and apron, plus a small + * margin. + */ +static float roadClearance(const RoadGraph &graph, const RoadEdge &e, + bool lotOnLeft) +{ + int la, lb; + graph.resolveLaneCounts(e, e.nodeA, la, lb); + const RoadConfig &cfg = graph.config; + float w = (la + lb) * cfg.laneWidth * 0.5f; + if (graph.edgeSidewalkEnabled(e, lotOnLeft)) + w += cfg.sidewalkWidth; + if (cfg.apronEnabled) + w += cfg.apronWidth; + return w + ROAD_CLEARANCE_MARGIN; +} + +/** + * Inset a lot polygon off the roads bounding it. findLots() lot + * polygons run centerline-to-centerline, so a building placed only + * the district setback inside the raw polygon would stand on the + * road itself. Every boundary edge that matches a graph edge + * (geometric endpoint match, either direction — covers enclosed + * faces and edge-fallback source edges alike) is offset inward by + * the road half-extent on the lot's side (lanes + sidewalk + apron + * + margin); unmatched edges keep a zero offset. Vertices of the + * inset polygon are the intersections of consecutive offset lines. + * Returns false when the inset collapses (degenerate edge or a + * vanished/reversed polygon), in which case the lot holds nothing + * buildable and should be skipped. + */ +static bool shrinkLotOffRoads(const RoadGraph &graph, + const std::vector &poly, + const Ogre::Vector3 ¢roid, + std::vector &out) +{ + size_t n = poly.size(); + out.clear(); + if (n < 3) + return false; + + std::vector inset(n, 0.0f); + std::vector dir(n), inward(n); + for (size_t i = 0; i < n; ++i) { + size_t j = (i + 1) % n; + Ogre::Vector3 d = poly[j] - poly[i]; + d.y = 0.0f; + if (d.normalise() < 0.001f) + return false; + dir[i] = d; + /* Inward = the side normal pointing at the centroid + * (winding-independent — fallback lots may be + * reversed). */ + Ogre::Vector3 nrm(-d.z, 0.0f, d.x); + Ogre::Vector3 mid = (poly[i] + poly[j]) * 0.5f; + if (nrm.dotProduct(centroid - mid) < 0.0f) + nrm = -nrm; + inward[i] = nrm; + + for (const RoadEdge &e : graph.edges) { + const RoadNode *na = graph.findNodeById(e.nodeA); + const RoadNode *nb = graph.findNodeById(e.nodeB); + if (!na || !nb) + continue; + bool fwd = poly[i].distance(na->position) < 0.01f && + poly[j].distance(nb->position) < 0.01f; + bool rev = poly[i].distance(nb->position) < 0.01f && + poly[j].distance(na->position) < 0.01f; + if (!fwd && !rev) + continue; + /* Graph "left" of A->B is UNIT_Y x dir. */ + Ogre::Vector3 gdir = nb->position - na->position; + gdir.y = 0.0f; + gdir.normalise(); + Ogre::Vector3 gleft(gdir.z, 0.0f, -gdir.x); + bool lotOnLeft = inward[i].dotProduct(gleft) > 0.0f; + inset[i] = roadClearance(graph, e, lotOnLeft); + break; + } + } + + for (size_t i = 0; i < n; ++i) { + size_t prev = (i + n - 1) % n; + const Ogre::Vector3 &d1 = dir[prev]; + const Ogre::Vector3 &d2 = dir[i]; + Ogre::Vector3 p1 = poly[prev] + inward[prev] * inset[prev]; + Ogre::Vector3 p2 = poly[i] + inward[i] * inset[i]; + float denom = d1.x * d2.z - d2.x * d1.z; + Ogre::Vector3 vtx; + if (std::fabs(denom) < 1.0e-6f) { + /* Near-parallel: average the two offset + * positions of the shared vertex. */ + vtx = (poly[i] + inward[prev] * inset[prev] + + poly[i] + inward[i] * inset[i]) * 0.5f; + } else { + /* Solve p1 + t*d1 = p2 + s*d2 in XZ. */ + float dx = p2.x - p1.x; + float dz = p2.z - p1.z; + float t = (dx * d2.z - dz * d2.x) / denom; + vtx = p1 + d1 * t; + } + vtx.y = poly[i].y; + out.push_back(vtx); + } + + return std::fabs(polyAreaXZ(out)) > 0.01f; +} + +static float sampleY(const RoadNetworkGen::HeightSampler &sampleHeight, + float x, float z, float fallback) +{ + return sampleHeight ? sampleHeight(x, z) : fallback; +} + +TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms, + const RoadNetworkGen::HeightSampler &sampleHeight) +{ + TownResult result; + const DistrictParams &dp = districtParams(params.district); + std::vector lots = + RoadNetworkGen::findLots(graph); + + std::mt19937 rng(params.seed); + std::uniform_real_distribution unit(0.0f, 1.0f); + float density = + std::min(1.0f, dp.density * std::max(0.0f, params.densityScale)); + std::map streetNumbers; + + for (size_t li = 0; li < lots.size(); ++li) { + const RoadNetworkGen::RoadLot &lot = lots[li]; + size_t n = lot.polygon.size(); + if (n < 3) + continue; + + /* Frontage ranking: an edge on a named street beats an + * edge-fallback lot's source edge, which beats the plain + * longest edge. Fallback lots have no nodeIds and their + * polygon may be reversed, so the source edge is matched + * geometrically against both endpoints. */ + Ogre::Vector3 srcA, srcB; + bool hasSource = false; + if (lot.edgeFallback && lot.sourceEdgeA >= 0 && + lot.sourceEdgeB >= 0) { + const RoadNode *sa = graph.findNodeById(lot.sourceEdgeA); + const RoadNode *sb = graph.findNodeById(lot.sourceEdgeB); + if (sa && sb) { + srcA = sa->position; + srcB = sb->position; + hasSource = true; + } + } + int best = 0; + float bestLen = -1.0f; + int bestTier = -1; + for (size_t i = 0; i < n; ++i) { + size_t j = (i + 1) % n; + float len = (lot.polygon[j] - lot.polygon[i]).length(); + int tier = 0; + if (!lot.nodeIds.empty() && + RoadNetworkGen::findStreetForEdge( + graph, lot.nodeIds[i], + lot.nodeIds[j]) != nullptr) { + tier = 2; + } else if (hasSource && + ((lot.polygon[i].distance(srcA) < 0.01f && + lot.polygon[j].distance(srcB) < 0.01f) || + (lot.polygon[i].distance(srcB) < 0.01f && + lot.polygon[j].distance(srcA) < 0.01f))) { + tier = 1; + } + if (tier > bestTier || + (tier == bestTier && len > bestLen)) { + best = (int)i; + bestLen = len; + bestTier = tier; + } + } + + Ogre::Vector3 p0 = lot.polygon[best]; + Ogre::Vector3 p1 = lot.polygon[(best + 1) % n]; + Ogre::Vector3 frontMid = (p0 + p1) * 0.5f; + Ogre::Vector3 u = p1 - p0; + u.y = 0.0f; + if (u.normalise() < 0.001f) + continue; + /* Inward direction: from the frontage towards the lot + * centroid (winding-independent). */ + Ogre::Vector3 v = lot.centroid - frontMid; + v.y = 0.0f; + if (v.normalise() < 0.001f) + continue; + + /* Inset the lot off the bounding roads — raw lot + * polygons run centerline-to-centerline, so buildings + * must keep the road half-extent (lanes + sidewalk + + * apron) plus a margin off the polygon boundary. */ + std::vector buildable; + if (!shrinkLotOffRoads(graph, lot.polygon, lot.centroid, + buildable)) { + result.skippedTooSmall++; + continue; + } + + /* Lot bounds in the (u, v) frame around frontMid, + * measured on the inset polygon; vMin is the buildable + * front line (the frontage pushed off the road). */ + float uMin = 1.0e30f, uMax = -1.0e30f; + float vMin = 1.0e30f, vMax = -1.0e30f; + for (size_t i = 0; i < n; ++i) { + Ogre::Vector3 d = buildable[i] - frontMid; + float du = d.dotProduct(u); + float dv = d.dotProduct(v); + uMin = std::min(uMin, du); + uMax = std::max(uMax, du); + vMin = std::min(vMin, dv); + vMax = std::max(vMax, dv); + } + + float availW = (uMax - uMin) - 2.0f * dp.setback; + float availD = (vMax - vMin) - dp.setback; + if (availW < dp.minFootprint || availD < dp.minFootprint) { + result.skippedTooSmall++; + continue; + } + + /* Seeded draws: density first, then width, depth, + * floors — fixed order keeps the sequence deterministic + * per lot index. */ + if (unit(rng) > density) { + result.skippedDensity++; + continue; + } + float width = dp.minFootprint + + unit(rng) * (dp.maxFootprint - dp.minFootprint); + float depth = dp.minFootprint + + unit(rng) * (dp.maxFootprint - dp.minFootprint); + width = std::min(width, availW); + depth = std::min(depth, availD); + int floors = dp.minFloors; + if (dp.maxFloors > dp.minFloors) + floors += (int)(unit(rng) * + (float)(dp.maxFloors - dp.minFloors + 1)); + floors = std::min(floors, dp.maxFloors); + + float cu = (uMin + uMax) * 0.5f; + float halfW = width * 0.5f; + cu = std::max(uMin + dp.setback + halfW, + std::min(cu, uMax - dp.setback - halfW)); + float cv = vMin + dp.setback + depth * 0.5f; + + Ogre::Vector3 center2d = frontMid + u * cu + v * cv; + + /* Foundation block: the floor sits on the highest + * terrain corner, the block reaches below the lowest. */ + float corners[4][2] = { + { -halfW, -depth * 0.5f }, + { halfW, -depth * 0.5f }, + { halfW, depth * 0.5f }, + { -halfW, depth * 0.5f }, + }; + float floorY = -1.0e30f; + float minGroundY = 1.0e30f; + for (auto &c : corners) { + Ogre::Vector3 w = + center2d + u * c[0] + v * c[1]; + float y = sampleY(sampleHeight, w.x, w.z, + lot.centroid.y); + floorY = std::max(floorY, y); + minGroundY = std::min(minGroundY, y); + } + + if (floorY < params.waterLevel) { + result.skippedWater++; + continue; + } + + TownBuilding b; + b.lotIndex = (int)li; + b.center = center2d; + b.center.y = floorY; + b.yaw = std::atan2(-v.x, -v.z); + b.width = width; + b.depth = depth; + b.floors = floors; + b.floorY = floorY; + b.minGroundY = minGroundY; + + /* Door ground one meter in front of the front face. */ + Ogre::Vector3 doorPos = + center2d - v * (depth * 0.5f + 1.0f); + float doorGround = + sampleY(sampleHeight, doorPos.x, doorPos.z, + lot.centroid.y); + b.stairsDrop = floorY - doorGround; + b.stairs = b.stairsDrop > STAIRS_THRESHOLD; + + /* Address from the frontage street, numbered + * sequentially per street in lot order. */ + const RoadStreet *street = nullptr; + if (!lot.nodeIds.empty()) + street = RoadNetworkGen::findStreetForEdge( + graph, lot.nodeIds[best], + lot.nodeIds[(best + 1) % n]); + else if (lot.sourceEdgeA >= 0 && lot.sourceEdgeB >= 0) + street = RoadNetworkGen::findStreetForEdge( + graph, lot.sourceEdgeA, lot.sourceEdgeB); + if (street) { + int num = ++streetNumbers[street->name]; + b.address = street->name + " " + std::to_string(num); + } else { + b.address = "Lot " + std::to_string(li + 1); + } + + TownPoi poi; + poi.name = b.address + " pickup"; + poi.position = center2d - v * (depth * 0.5f + 3.0f); + poi.position.y = + sampleY(sampleHeight, poi.position.x, poi.position.z, + lot.centroid.y); + poi.building = (int)result.buildings.size(); + + result.buildings.push_back(b); + result.pois.push_back(poi); + } + + return result; +} + +} /* namespace TownGen */ diff --git a/src/features/editScene/roadlib/TownGen.hpp b/src/features/editScene/roadlib/TownGen.hpp new file mode 100644 index 0000000..8e08562 --- /dev/null +++ b/src/features/editScene/roadlib/TownGen.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#include +#include + +#include "../components/RoadGraph.hpp" +#include "RoadNetworkGen.hpp" + +/** + * Procedural town generation on top of a road network (demo-taxi + * phase 4). Pure functions of RoadGraph/RoadLot data plus a terrain + * height sampler — fully exercisable headlessly. + * + * generateTown() consumes the lots of RoadNetworkGen::findLots() + * (enclosed faces + edge-fallback squares) and places one building + * per suitable lot: oriented to the frontage edge (the boundary edge + * on a named street, longest first), sized by the district preset, + * seated on a foundation block (floor at the highest terrain corner + * of the footprint, foundation reaching below the lowest corner) + * with entrance stairs when the door-side ground drops below the + * floor. Deterministic under TownParams::seed. + */ +namespace TownGen { + +/** District zoning presets driving building size, height, density. */ +enum class District { + Village = 0, + Town, + CityResidential, + CityCenter, + COUNT +}; + +struct DistrictParams { + const char *name; + /** Building footprint side range in meters. */ + float minFootprint; + float maxFootprint; + int minFloors; + int maxFloors; + /** Fraction of suitable lots that get a building (0..1). */ + float density; + /** Min distance from the lot boundary to the building walls. */ + float setback; + /** Wall tint (linear RGB). */ + float wallColor[3]; +}; + +const DistrictParams &districtParams(District d); + +struct TownParams { + District district = District::Village; + uint32_t seed = 1; + /** Multiplier on the district density (clamped to 1). */ + float densityScale = 1.0f; + /** Lots whose floor level is below this are skipped (water). */ + float waterLevel = -1.0e30f; +}; + +struct TownBuilding { + /** Index into the findLots() output this building sits on. */ + int lotIndex = -1; + /** Building center at floor level (render/world XZ of the + * caller's coordinate space). */ + Ogre::Vector3 center = Ogre::Vector3::ZERO; + /** Yaw (radians around +Y); the building's local +Z faces the + * street. */ + float yaw = 0.0f; + /** Footprint in meters: X along the street, Z into the lot. */ + float width = 0.0f; + float depth = 0.0f; + int floors = 1; + /** Floor level: the highest terrain corner of the footprint. */ + float floorY = 0.0f; + /** Lowest terrain corner of the footprint. */ + float minGroundY = 0.0f; + /** Entrance stairs: the door-side ground is below the floor. */ + bool stairs = false; + /** floorY - ground at the door (when stairs). */ + float stairsDrop = 0.0f; + /** " " or "Lot ". */ + std::string address; +}; + +/** Named curb point in front of a building's door (pickup/dropoff, + * pedestrian origin/destination). */ +struct TownPoi { + std::string name; /* "

pickup" */ + Ogre::Vector3 position = Ogre::Vector3::ZERO; + int building = -1; /* index into buildings */ +}; + +struct TownResult { + std::vector buildings; + std::vector pois; + /** Suitable-but-empty lots (density roll). */ + int skippedDensity = 0; + /** Lots too small for the district footprint. */ + int skippedTooSmall = 0; + /** Lots below TownParams::waterLevel. */ + int skippedWater = 0; +}; + +/** + * Place buildings on the lots of the road graph. Deterministic: + * lots are processed in findLots() order and every random draw (one + * density roll, then width, depth, floors per built lot) comes from + * one mt19937 seeded with TownParams::seed, so the same graph and + * params always yield the same town. + * + * A lot is skipped when its usable rectangle (lot bounds inset by the + * district setback) cannot fit the district's minimum footprint, or + * when its floor level is below waterLevel. The building's frontage + * edge is the longest boundary edge on a named street (longest + * boundary edge when the lot touches no street); the address numbers + * buildings sequentially per street in lot order. + */ +TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms, + const RoadNetworkGen::HeightSampler &sampleHeight); + +} /* namespace TownGen */ diff --git a/src/features/editScene/systems/AnimationTreeSystem.cpp b/src/features/editScene/systems/AnimationTreeSystem.cpp index 5447c01..c48c5eb 100644 --- a/src/features/editScene/systems/AnimationTreeSystem.cpp +++ b/src/features/editScene/systems/AnimationTreeSystem.cpp @@ -38,15 +38,20 @@ AnimationTreeSystem::AnimationTreeSystem(flecs::world &world, : m_world(world) , m_sceneMgr(sceneMgr) { - m_world.observer("AnimationTreeCleanup") - .event(flecs::OnRemove) - .each([this](flecs::entity e, AnimationTreeComponent &) { - teardownEntity(e); - }); + m_cleanupObserver = + m_world.observer( + "AnimationTreeCleanup") + .event(flecs::OnRemove) + .each([this](flecs::entity e, + AnimationTreeComponent &) { + teardownEntity(e); + }); } AnimationTreeSystem::~AnimationTreeSystem() { + if (m_cleanupObserver.is_alive()) + m_cleanupObserver.destruct(); std::vector toRemove; for (auto &pair : m_states) toRemove.push_back(pair.first); diff --git a/src/features/editScene/systems/AnimationTreeSystem.hpp b/src/features/editScene/systems/AnimationTreeSystem.hpp index 7dbe254..ec6bd16 100644 --- a/src/features/editScene/systems/AnimationTreeSystem.hpp +++ b/src/features/editScene/systems/AnimationTreeSystem.hpp @@ -139,6 +139,9 @@ private: findAnimationNode(const AnimationTreeNode &stateNode) const; flecs::world &m_world; + /* Kept so the destructor can unregister it: the observer must not + * fire into a destroyed system during world teardown (ecs_fini). */ + flecs::entity m_cleanupObserver; Ogre::SceneManager *m_sceneMgr; bool m_initialized = false; diff --git a/src/features/editScene/systems/CellGridSystem.cpp b/src/features/editScene/systems/CellGridSystem.cpp index 9c83781..8dbc636 100644 --- a/src/features/editScene/systems/CellGridSystem.cpp +++ b/src/features/editScene/systems/CellGridSystem.cpp @@ -689,6 +689,11 @@ static void genPlane(float sizeX, float sizeY, const Ogre::Vector3 &normal, Procedural::TriangleBuffer &tb, const Ogre::Vector3 &origin) { + /* Degenerate trim pieces (e.g. the top frame of a doorway when + * cellHeight leaves no room above the door) have no visible area; + * skip them instead of throwing in the plane generator. */ + if (sizeX <= 0.0001f || sizeY <= 0.0001f) + return; // nsegY is based on the height (sizeX in the original, which is vertical) int nsegY = (sizeX < 1.0f) ? 1 : std::max(1, (int)(sizeX / 2.0f)); Procedural::PlaneGenerator() @@ -1192,7 +1197,12 @@ void CellGridSystem::buildDoors(const CellGridComponent &grid, cell.hasFlag(CellFlags::DoorZPos)) { float sideWidth = (2.0f * hScale - doorWidth) / 2.0f; float moffset = (2.0f * hScale - sideWidth) / 2.0f; - float doorHeight = doorHeightBase - solidExtOffset; + /* Clamp to the wall height: with cellHeight <= + * doorHeightBase (e.g. the 3 m town cells) the door + * fills the wall and no top frame is emitted. */ + float doorHeight = std::min(doorHeightBase - + solidExtOffset, + solidExtHeight); float doorTopHeight = solidExtHeight - doorHeight; float baseOffsetY = solidExtOffset; @@ -1316,7 +1326,9 @@ void CellGridSystem::buildDoors(const CellGridComponent &grid, (2.0f * hScale - 0.2f - doorWidth) / 2.0f; float moffset = (2.0f * hScale - 0.2f - sideWidth) / 2.0f; - float doorHeight = doorHeightBase - solidIntOffset; + float doorHeight = std::min(doorHeightBase - + solidIntOffset, + solidIntHeight); float doorTopHeight = solidIntHeight - doorHeight; float baseOffsetY = solidIntOffset; diff --git a/src/features/editScene/systems/CharacterSlotSystem.cpp b/src/features/editScene/systems/CharacterSlotSystem.cpp index d50e2a4..b5c08e0 100644 --- a/src/features/editScene/systems/CharacterSlotSystem.cpp +++ b/src/features/editScene/systems/CharacterSlotSystem.cpp @@ -33,20 +33,30 @@ CharacterSlotSystem::CharacterSlotSystem(flecs::world &world, , m_sceneMgr(sceneMgr) { ms_singleton = this; - m_world.observer("CharacterSlotsBuild") - .event(flecs::OnAdd) - .each([this](flecs::entity e, CharacterIdentityComponent &) { - markEntityDirty(e.id()); - }); - m_world.observer("CharacterSlotsCleanup") - .event(flecs::OnRemove) - .each([this](flecs::entity e, CharacterIdentityComponent &) { - destroyCharacterParts(e); - }); + m_buildObserver = + m_world.observer( + "CharacterSlotsBuild") + .event(flecs::OnAdd) + .each([this](flecs::entity e, + CharacterIdentityComponent &) { + markEntityDirty(e.id()); + }); + m_cleanupObserver = + m_world.observer( + "CharacterSlotsCleanup") + .event(flecs::OnRemove) + .each([this](flecs::entity e, + CharacterIdentityComponent &) { + destroyCharacterParts(e); + }); } CharacterSlotSystem::~CharacterSlotSystem() { + if (m_buildObserver.is_alive()) + m_buildObserver.destruct(); + if (m_cleanupObserver.is_alive()) + m_cleanupObserver.destruct(); std::vector toRemove; for (auto &pair : m_entities) toRemove.push_back(pair.first); diff --git a/src/features/editScene/systems/CharacterSlotSystem.hpp b/src/features/editScene/systems/CharacterSlotSystem.hpp index 58dfdb0..35a36c8 100644 --- a/src/features/editScene/systems/CharacterSlotSystem.hpp +++ b/src/features/editScene/systems/CharacterSlotSystem.hpp @@ -108,6 +108,10 @@ private: const nlohmann::json *entry); flecs::world &m_world; + /* Kept so the destructor can unregister them: the observers must + * not fire into a destroyed system during world teardown. */ + flecs::entity m_buildObserver; + flecs::entity m_cleanupObserver; Ogre::SceneManager *m_sceneMgr; bool m_initialized = false; diff --git a/src/features/editScene/systems/CharacterSpawnerSystem.cpp b/src/features/editScene/systems/CharacterSpawnerSystem.cpp index 4ecfb7e..3593275 100644 --- a/src/features/editScene/systems/CharacterSpawnerSystem.cpp +++ b/src/features/editScene/systems/CharacterSpawnerSystem.cpp @@ -21,15 +21,20 @@ CharacterSpawnerSystem::CharacterSpawnerSystem(flecs::world &world, , m_uiSystem(uiSystem) , m_query(world.query()) { - m_world.observer("CharacterSpawnerCleanup") - .event(flecs::OnRemove) - .each([this](flecs::entity e, CharacterSpawnerComponent &) { - despawn(e); - }); + m_cleanupObserver = + m_world.observer( + "CharacterSpawnerCleanup") + .event(flecs::OnRemove) + .each([this](flecs::entity e, + CharacterSpawnerComponent &) { + despawn(e); + }); } CharacterSpawnerSystem::~CharacterSpawnerSystem() { + if (m_cleanupObserver.is_alive()) + m_cleanupObserver.destruct(); std::vector ids; ids.reserve(m_spawnedEntities.size()); for (const auto &pair : m_spawnedEntities) diff --git a/src/features/editScene/systems/CharacterSpawnerSystem.hpp b/src/features/editScene/systems/CharacterSpawnerSystem.hpp index cdbe284..b8f2f9e 100644 --- a/src/features/editScene/systems/CharacterSpawnerSystem.hpp +++ b/src/features/editScene/systems/CharacterSpawnerSystem.hpp @@ -94,6 +94,9 @@ private: Ogre::Vector3 getCameraPosition() const; flecs::world &m_world; + /* Kept so the destructor can unregister it: the observer must not + * fire into a destroyed system during world teardown (ecs_fini). */ + flecs::entity m_cleanupObserver; Ogre::SceneManager *m_sceneMgr; EditorCameraSystem *m_cameraSystem; EditorUISystem *m_uiSystem; diff --git a/src/features/editScene/systems/CharacterSystem.cpp b/src/features/editScene/systems/CharacterSystem.cpp index 4adeda5..e92c141 100644 --- a/src/features/editScene/systems/CharacterSystem.cpp +++ b/src/features/editScene/systems/CharacterSystem.cpp @@ -52,15 +52,19 @@ CharacterSystem::CharacterSystem(flecs::world &world, : m_world(world) , m_sceneMgr(sceneMgr) { - m_world.observer("CharacterCleanup") - .event(flecs::OnRemove) - .each([this](flecs::entity e, CharacterComponent &) { - teardownEntity(e); - }); + m_cleanupObserver = m_world.observer( + "CharacterCleanup") + .event(flecs::OnRemove) + .each([this](flecs::entity e, + CharacterComponent &) { + teardownEntity(e); + }); } CharacterSystem::~CharacterSystem() { + if (m_cleanupObserver.is_alive()) + m_cleanupObserver.destruct(); std::vector toRemove; for (auto &pair : m_states) toRemove.push_back(pair.first); diff --git a/src/features/editScene/systems/CharacterSystem.hpp b/src/features/editScene/systems/CharacterSystem.hpp index bde358e..8bcf6f0 100644 --- a/src/features/editScene/systems/CharacterSystem.hpp +++ b/src/features/editScene/systems/CharacterSystem.hpp @@ -68,6 +68,9 @@ private: void createChestCollider(flecs::entity e, CharacterComponent &cc); flecs::world &m_world; + /* Kept so the destructor can unregister it: the observer must not + * fire into a destroyed system during world teardown (ecs_fini). */ + flecs::entity m_cleanupObserver; Ogre::SceneManager *m_sceneMgr; JoltPhysicsWrapper *m_physics = nullptr; bool m_initialized = false; diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 9915bcc..b04e962 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -2462,6 +2462,13 @@ void EditorUISystem::handleRoadEditClick(const Ogre::Ray &mouseRay) Ogre::Vector3 hit = mouseRay.getPoint(t); + /* Generator origin picking swallows the click before any tool. */ + if (rs->isOriginPickArmed()) { + rs->armOriginPick(false); + rs->setPickedOrigin(hit); + return; + } + int nodeId = pickRoadNode(hit); /* Connect tool: first click pins the source node, second click on diff --git a/src/features/editScene/systems/EditorWaterPlaneSystem.cpp b/src/features/editScene/systems/EditorWaterPlaneSystem.cpp index 16dd814..6f4ac36 100644 --- a/src/features/editScene/systems/EditorWaterPlaneSystem.cpp +++ b/src/features/editScene/systems/EditorWaterPlaneSystem.cpp @@ -2,6 +2,7 @@ #include "../components/WaterPlane.hpp" #include "../components/Transform.hpp" #include "../components/WaterPhysics.hpp" +#include #include #include #include @@ -34,7 +35,27 @@ EditorWaterPlaneSystem::EditorWaterPlaneSystem(flecs::world &world, { } -EditorWaterPlaneSystem::~EditorWaterPlaneSystem() = default; +EditorWaterPlaneSystem::~EditorWaterPlaneSystem() +{ + /* destroyEditorSystems() runs before Ogre Root teardown, but the + * render textures themselves are only destroyed by the render + * system during Root::~Root — detach the listener and viewports + * now so that teardown does not fire into a dead system. The + * texture is deliberately NOT removed here: the water material + * still references it and TextureManager owns its final + * destruction. */ + for (Ogre::RenderTarget *rt : m_createdTargets) { + rt->removeListener(&m_listener); + rt->removeAllViewports(); + } + m_createdTargets.clear(); + m_query.each([this](flecs::entity, WaterPlane &wp, + TransformComponent &) { + wp.renderTarget = nullptr; + wp.reflectionViewport = nullptr; + wp.refractionViewport = nullptr; + }); +} void EditorWaterPlaneSystem::update(float deltaTime, Ogre::Camera *camera) { @@ -137,6 +158,7 @@ void EditorWaterPlaneSystem::rebuildWaterPlane( // Set listener to hide water during RTT m_listener.waterMesh = wp.manualObject; wp.renderTarget->addListener(&m_listener); + m_createdTargets.push_back(wp.renderTarget); // Create cameras with their own nodes wp.reflectionCamera = m_sceneMgr->createCamera(baseName + "ReflectionCam"); @@ -199,6 +221,10 @@ void EditorWaterPlaneSystem::cleanupWaterPlane(WaterPlane &wp) if (wp.renderTarget) { wp.renderTarget->removeListener(&m_listener); wp.renderTarget->removeAllViewports(); + m_createdTargets.erase( + std::remove(m_createdTargets.begin(), + m_createdTargets.end(), wp.renderTarget), + m_createdTargets.end()); wp.renderTarget = nullptr; } wp.reflectionViewport = nullptr; diff --git a/src/features/editScene/systems/EditorWaterPlaneSystem.hpp b/src/features/editScene/systems/EditorWaterPlaneSystem.hpp index 037c3db..325b6ec 100644 --- a/src/features/editScene/systems/EditorWaterPlaneSystem.hpp +++ b/src/features/editScene/systems/EditorWaterPlaneSystem.hpp @@ -45,6 +45,10 @@ private: Ogre::SceneManager *m_sceneMgr; flecs::query m_query; WaterRenderTargetListener m_listener; + /* Every render target that currently has m_listener registered; + * the destructor detaches from all of them regardless of whether + * the owning entities are still alive. */ + std::vector m_createdTargets; float m_underwaterTime = 0.0f; bool m_compositorAdded = false; Ogre::Viewport *m_compositorViewport = nullptr; diff --git a/src/features/editScene/systems/RoadRegionStore.cpp b/src/features/editScene/systems/RoadRegionStore.cpp index ba027d4..e579cfb 100644 --- a/src/features/editScene/systems/RoadRegionStore.cpp +++ b/src/features/editScene/systems/RoadRegionStore.cpp @@ -69,6 +69,9 @@ RoadRegionStore::PageData &RoadRegionStore::loadPage(long pageX, long pageY) n.z = jn.value("z", 0.0); n.verticalOffset = jn.value( "verticalOffset", 0.0f); + n.generatedGroupId = jn.value( + "generatedGroupId", + (uint32_t)0); page.data.nodes.push_back(n); } } @@ -105,6 +108,39 @@ RoadRegionStore::PageData &RoadRegionStore::loadPage(long pageX, long pageY) e.sidewalkRightMode = je.value( "sidewalkRightMode", e.sidewalkRight ? 0 : 2); + /* Phase 2.5: marking mode + * (absent = Inherit) and the + * decal list. */ + e.markingsMode = je.value( + "markingsMode", 0); + if (je.contains("decals") && + je["decals"].is_array()) { + for (const auto &jd : + je["decals"]) { + RoadRegionDecal d; + d.edgeT = jd.value( + "edgeT", + 0.5f); + d.lateralOffset = jd.value( + "lateralOffset", + 0.0f); + d.size = jd.value( + "size", + 1.0f); + if (jd.contains( + "uv") && + jd["uv"].is_array() && + jd["uv"].size() == + 4) { + d.uvX = jd["uv"][0]; + d.uvY = jd["uv"][1]; + d.uvW = jd["uv"][2]; + d.uvH = jd["uv"][3]; + } + e.decals.push_back( + d); + } + } if (je.contains("sidewalkPasses") && je["sidewalkPasses"] .is_array()) { @@ -193,6 +229,8 @@ bool RoadRegionStore::saveRegion(long px, long py, const RoadRegionData &data) jn["y"] = n.y; jn["z"] = n.z; jn["verticalOffset"] = n.verticalOffset; + if (n.generatedGroupId != 0) + jn["generatedGroupId"] = n.generatedGroupId; j["nodes"].push_back(jn); } j["edges"] = nlohmann::json::array(); @@ -210,6 +248,21 @@ bool RoadRegionStore::saveRegion(long px, long py, const RoadRegionData &data) je["sidewalkRight"] = e.sidewalkRight; je["sidewalkLeftMode"] = e.sidewalkLeftMode; je["sidewalkRightMode"] = e.sidewalkRightMode; + if (e.markingsMode != 0) + je["markingsMode"] = e.markingsMode; + if (!e.decals.empty()) { + nlohmann::json dj = nlohmann::json::array(); + for (const RoadRegionDecal &d : e.decals) { + nlohmann::json jd; + jd["edgeT"] = d.edgeT; + jd["lateralOffset"] = d.lateralOffset; + jd["size"] = d.size; + jd["uv"] = { d.uvX, d.uvY, d.uvW, + d.uvH }; + dj.push_back(jd); + } + je["decals"] = dj; + } if (!e.sidewalkPasses.empty()) { nlohmann::json pj = nlohmann::json::array(); for (const RoadRegionSidewalkPass &p : @@ -265,3 +318,50 @@ void RoadRegionStore::removeRegionFile(long px, long py) std::string(e.what())); } } + +std::string RoadRegionStore::streetsFilePath(const std::string &rootDir) +{ + return rootDir + "/roads/streets.json"; +} + +bool RoadRegionStore::loadStreets(nlohmann::json *out) +{ + if (m_rootDir.empty() || !out) + return false; + const std::string path = streetsFilePath(m_rootDir); + try { + std::ifstream file(path); + if (!file.is_open()) + return false; + file >> *out; + return true; + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "RoadRegionStore: streets load error: " + + std::string(e.what())); + return false; + } +} + +bool RoadRegionStore::saveStreets(const nlohmann::json &j) +{ + if (m_rootDir.empty()) + return false; + try { + std::filesystem::create_directories(m_rootDir + "/roads"); + const std::string path = streetsFilePath(m_rootDir); + std::ofstream file(path); + if (!file.is_open()) { + Ogre::LogManager::getSingleton().logMessage( + "RoadRegionStore: failed to write " + path); + return false; + } + file << j.dump(4); + return true; + } catch (const std::exception &e) { + Ogre::LogManager::getSingleton().logMessage( + "RoadRegionStore: streets save error: " + + std::string(e.what())); + return false; + } +} diff --git a/src/features/editScene/systems/RoadRegionStore.hpp b/src/features/editScene/systems/RoadRegionStore.hpp index 1dd5249..c730446 100644 --- a/src/features/editScene/systems/RoadRegionStore.hpp +++ b/src/features/editScene/systems/RoadRegionStore.hpp @@ -5,6 +5,8 @@ #include #include +#include + /* * One road node stored in a region file. Positions are absolute world * space doubles so regions work at any distance from the origin @@ -16,6 +18,9 @@ struct RoadRegionNode { double y = 0.0; double z = 0.0; float verticalOffset = 0.0f; + /* Phase 3: procedural generator group (0 = manual). Absent in + * legacy files = manual. */ + uint32_t generatedGroupId = 0; }; struct RoadRegionEdgePrefab { @@ -31,6 +36,18 @@ struct RoadRegionSidewalkPass { float width = 6.0f; }; +/* One decal quad painted on an edge (phase 2.5); semantics match + * RoadEdgeDecal. */ +struct RoadRegionDecal { + float edgeT = 0.5f; + float lateralOffset = 0.0f; + float size = 1.0f; + float uvX = 0.0f; + float uvY = 0.0f; + float uvW = 0.5f; + float uvH = 0.5f; +}; + /* * One road edge stored in a region file. Endpoints are referenced by * the region-local node ids of this file. Field semantics match @@ -51,6 +68,10 @@ struct RoadRegionEdge { * (true -> Inherit, false -> Off). */ int sidewalkLeftMode = 0; int sidewalkRightMode = 0; + /* Per-edge marking tri-state (RoadMarkingMode as int, phase 2.5). + * Absent in legacy files = Inherit (0). */ + int markingsMode = 0; + std::vector decals; std::vector sidewalkPasses; RoadRegionEdgePrefab prefabLeft; RoadRegionEdgePrefab prefabRight; @@ -97,6 +118,13 @@ public: void removeRegionFile(long px, long py); void clearCache(); + /* Streets (named edge sequences, phase 3) live in a single file + * per terrain: "/roads/streets.json". loadStreets returns + * false when the file does not exist (leaving *out untouched). */ + bool loadStreets(nlohmann::json *out); + bool saveStreets(const nlohmann::json &j); + static std::string streetsFilePath(const std::string &rootDir); + private: struct PageData { RoadRegionData data; diff --git a/src/features/editScene/systems/RoadSystem.cpp b/src/features/editScene/systems/RoadSystem.cpp index 510409a..0840263 100644 --- a/src/features/editScene/systems/RoadSystem.cpp +++ b/src/features/editScene/systems/RoadSystem.cpp @@ -10,11 +10,14 @@ #include "../components/Lod.hpp" #include "../components/PhysicsCollider.hpp" #include "../components/EditorMarker.hpp" +#include "../components/WaterPhysics.hpp" #include "../physics/physics.h" #include "../roadlib/RoadGeometryLib.hpp" +#include "../roadlib/RoadNetworkGen.hpp" #include "PrefabSystem.hpp" #include #include +#include #include #include #include @@ -51,18 +54,21 @@ void RoadSystem::createManualObjects() m_nodeMarkers = m_sceneMgr->createManualObject("RoadNodeMarkers"); m_edgeLines = m_sceneMgr->createManualObject("RoadEdgeLines"); + m_lotOutlines = m_sceneMgr->createManualObject("RoadLotOutlines"); m_widthIndicators = m_sceneMgr->createManualObject("RoadWidthIndicators"); m_selectionHighlight = m_sceneMgr->createManualObject( "RoadSelectionHighlight"); m_visualNode->attachObject(m_nodeMarkers); m_visualNode->attachObject(m_edgeLines); + m_visualNode->attachObject(m_lotOutlines); m_visualNode->attachObject(m_widthIndicators); m_visualNode->attachObject(m_selectionHighlight); /* Draw in the overlay queue so the aids are never hidden by terrain. */ m_nodeMarkers->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); m_edgeLines->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); + m_lotOutlines->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); m_widthIndicators->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); m_selectionHighlight->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); @@ -72,6 +78,12 @@ void RoadSystem::createManualObjects() m_debugWedgeObject->setRenderQueueGroup( Ogre::RENDER_QUEUE_MAIN); m_debugWedgeObject->setVisible(false); + + /* Generator preview lives outside m_visualNode so it is visible + * without road edit mode. */ + m_genPreview = m_sceneMgr->createManualObject("RoadGenPreview"); + m_sceneMgr->getRootSceneNode()->attachObject(m_genPreview); + m_genPreview->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY); } void RoadSystem::destroyManualObjects() @@ -100,9 +112,11 @@ void RoadSystem::destroyManualObjects() destroyManual(m_nodeMarkers); destroyManual(m_edgeLines); + destroyManual(m_lotOutlines); destroyManual(m_widthIndicators); destroyManual(m_selectionHighlight); destroyManual(m_debugWedgeObject); + destroyManual(m_genPreview); } void RoadSystem::setTerrainEntity(flecs::entity entity) @@ -115,6 +129,7 @@ void RoadSystem::setTerrainEntity(flecs::entity entity) m_lastGraphVersion = 0; resetRegionState(); clearPageGeometry(); + clearGenPreview(); rebuildVisualAids(); } @@ -126,6 +141,7 @@ void RoadSystem::clear() m_lastGraphVersion = 0; resetRegionState(); clearPageGeometry(); + clearGenPreview(); rebuildVisualAids(); } @@ -266,6 +282,13 @@ void RoadSystem::update(float deltaTime) } syncPages(); + /* Phase 2.5: overlay materials/textures and the road atlas + * texture unit. ProceduralMaterialSystem restyles the road + * material in place and wipes extra texture units, so the atlas + * unit is re-attached here whenever it is missing. */ + ensureOverlayMaterials(tc.roadGraph.config); + syncRoadAtlasTextureUnit(tc.roadGraph.config); + /* Mesh assembly (M5.8): rebuild dirty pages, then finalize pages * whose chunk meshes ProceduralMeshSystem has since created. */ for (auto &kv : m_pageGeometry) { @@ -279,18 +302,25 @@ void RoadSystem::update(float deltaTime) continue; bool allCreated = true; - for (flecs::entity e : pg.bufferEntities) { - if (!e.is_alive() || - !e.has()) { - allCreated = false; - break; - } + auto chunkCreated = [](flecs::entity e) { + if (!e.is_alive() || !e.has()) + return false; const auto &tb = e.get(); - if (!tb.meshCreated || !tb.ogreEntity) { + return tb.meshCreated && tb.ogreEntity; + }; + for (flecs::entity e : pg.bufferEntities) { + if (!chunkCreated(e)) { allCreated = false; break; } } + if (allCreated) + for (flecs::entity e : pg.overlayEntities) { + if (!chunkCreated(e)) { + allCreated = false; + break; + } + } if (!allCreated) continue; @@ -306,6 +336,26 @@ void RoadSystem::update(float deltaTime) .markChanged(); createPageCollider(pg, e, tb.meshName); } + /* Overlay chunks (phase 2.5): rendering distance, renderable + * pointer and the shared marking/decal material by name — + * no collider, no navmesh dirtying. */ + for (flecs::entity e : pg.overlayEntities) { + auto &tb = e.get_mut(); + tb.ogreEntity->setRenderingDistance( + tc.roadGraph.config.roadVisibilityDistance); + const std::string &matName = + tb.meshName.find("_decals") != + std::string::npos + ? tc.roadGraph.config.decalMaterialName + : tc.roadGraph.config.markingMaterialName; + tb.ogreEntity->setMaterialName(matName); + if (e.has()) + e.get_mut().entity = + tb.ogreEntity; + if (e.has()) + e.get_mut() + .markChanged(); + } markNavMeshDirty(); pg.meshFinalized = true; } @@ -637,6 +687,14 @@ void RoadSystem::syncRegions() if (root != m_regionRoot) { m_regionRoot = root; m_regionStore.setRootDirectory(root); + /* Streets live in one file per terrain. Pages are not + * all in the graph yet (and may never be at once while + * streaming), so keep entries whose edges are currently + * missing — lookups only ever match existing edges. */ + nlohmann::json sj; + if (m_regionStore.loadStreets(&sj)) + RoadNetworkGen::streetsFromJson(graph, sj, + /*dropMissing=*/false); } Ogre::TerrainGroup *group = ts->getTerrainGroup(); @@ -714,6 +772,40 @@ void RoadSystem::syncRegions() m_streamActivity = true; } +/* Phase 2.5: decal list conversion between RoadEdge and + * RoadRegionEdge, shared by the migrate/load/build paths. */ +static void copyDecalsToRegion(const RoadEdge &src, RoadRegionEdge &dst) +{ + dst.decals.clear(); + for (const RoadEdgeDecal &d : src.decals) { + RoadRegionDecal rd; + rd.edgeT = d.edgeT; + rd.lateralOffset = d.lateralOffset; + rd.size = d.size; + rd.uvX = d.uvX; + rd.uvY = d.uvY; + rd.uvW = d.uvW; + rd.uvH = d.uvH; + dst.decals.push_back(rd); + } +} + +static void copyDecalsFromRegion(const RoadRegionEdge &src, RoadEdge &dst) +{ + dst.decals.clear(); + for (const RoadRegionDecal &rd : src.decals) { + RoadEdgeDecal d; + d.edgeT = rd.edgeT; + d.lateralOffset = rd.lateralOffset; + d.size = rd.size; + d.uvX = rd.uvX; + d.uvY = rd.uvY; + d.uvW = rd.uvW; + d.uvH = rd.uvH; + dst.decals.push_back(d); + } +} + void RoadSystem::migrateGraphToRegions(RoadGraph &graph) { TerrainSystem *ts = TerrainSystem::getInstance(); @@ -797,6 +889,8 @@ void RoadSystem::migrateGraphToRegions(RoadGraph &graph) re.sidewalkRight = e.sidewalkRight; re.sidewalkLeftMode = (int)e.sidewalkLeftMode; re.sidewalkRightMode = (int)e.sidewalkRightMode; + re.markingsMode = (int)e.markingsMode; + copyDecalsToRegion(e, re); for (const RoadSidewalkPass &p : e.sidewalkPasses) { RoadRegionSidewalkPass rp; rp.rightSide = p.rightSide; @@ -867,6 +961,8 @@ void RoadSystem::loadRegion(long px, long py, RoadGraph &graph) remap[rn.id] = graph.addNode( ts->worldToRender(rn.x, rn.y, rn.z), rn.verticalOffset); + graph.findNodeById(remap[rn.id])->generatedGroupId = + rn.generatedGroupId; } /* Second pass: edges whose endpoints both resolved. */ @@ -891,6 +987,8 @@ void RoadSystem::loadRegion(long px, long py, RoadGraph &graph) e.sidewalkRight = re.sidewalkRight; e.sidewalkLeftMode = (RoadSidewalkMode)re.sidewalkLeftMode; e.sidewalkRightMode = (RoadSidewalkMode)re.sidewalkRightMode; + e.markingsMode = (RoadMarkingMode)re.markingsMode; + copyDecalsFromRegion(re, e); for (const RoadRegionSidewalkPass &rp : re.sidewalkPasses) { RoadSidewalkPass p; p.rightSide = rp.rightSide; @@ -942,6 +1040,7 @@ void RoadSystem::buildRegionData(long px, long py, const RoadGraph &graph, rn.y = ts->renderToWorldY(n.position.y); rn.z = wz; rn.verticalOffset = n.verticalOffset; + rn.generatedGroupId = n.generatedGroupId; out.nodes.push_back(rn); } @@ -958,6 +1057,7 @@ void RoadSystem::buildRegionData(long px, long py, const RoadGraph &graph, rn.y = ts->renderToWorldY(n->position.y); rn.z = ts->renderToWorldZ(n->position.z); rn.verticalOffset = n->verticalOffset; + rn.generatedGroupId = n->generatedGroupId; out.nodes.push_back(rn); }; @@ -982,6 +1082,8 @@ void RoadSystem::buildRegionData(long px, long py, const RoadGraph &graph, re.sidewalkRight = e.sidewalkRight; re.sidewalkLeftMode = (int)e.sidewalkLeftMode; re.sidewalkRightMode = (int)e.sidewalkRightMode; + re.markingsMode = (int)e.markingsMode; + copyDecalsToRegion(e, re); for (const RoadSidewalkPass &p : e.sidewalkPasses) { RoadRegionSidewalkPass rp; rp.rightSide = p.rightSide; @@ -1031,6 +1133,11 @@ void RoadSystem::flushRegionStore() m_regionStore.saveRegion(kv.second.first, kv.second.second, data); } + + /* Streets (phase 3) go alongside the region files. */ + nlohmann::json sj; + RoadNetworkGen::streetsToJson(graph, sj); + m_regionStore.saveStreets(sj); } void RoadSystem::onRenderOriginChanged(const Ogre::Vector3 &delta) @@ -1122,6 +1229,15 @@ uint64_t RoadSystem::computePageSignature(uint64_t key) const fnvHashPod(h, cfg.apronSkirtDepth); fnvHashString(h, cfg.roadMeshTemplate); fnvHashString(h, cfg.sidewalkMeshTemplate); + /* Phase 2.5: markings/decal overlay + atlas UV config. */ + fnvHashPod(h, cfg.markingsEnabled); + fnvHashPod(h, cfg.markingLineWidth); + fnvHashPod(h, cfg.markingDashLength); + fnvHashPod(h, cfg.markingDashGap); + fnvHashPod(h, cfg.markingStopWidth); + fnvHashPod(h, cfg.markingStopSetback); + fnvHashPod(h, cfg.markingEdgeInset); + fnvHashPod(h, cfg.textureTileLength); /* Seed nodes of this page's wedges/segments. */ std::set nodeIds; @@ -1186,6 +1302,16 @@ uint64_t RoadSystem::computePageSignature(uint64_t key) const fnvHashPod(h, slot->lateralOffset); fnvHashPod(h, slot->yOffset); } + fnvHashPod(h, e.markingsMode); + for (const RoadEdgeDecal &d : e.decals) { + fnvHashPod(h, d.edgeT); + fnvHashPod(h, d.lateralOffset); + fnvHashPod(h, d.size); + fnvHashPod(h, d.uvX); + fnvHashPod(h, d.uvY); + fnvHashPod(h, d.uvW); + fnvHashPod(h, d.uvH); + } } return h; } @@ -1336,6 +1462,105 @@ void RoadSystem::buildPageMeshes(RoadPageGeometry &pg) } flushChunk(); + /* + * Markings and decals (phase 2.5): separate overlay chunk buffers, + * same chunking rule but WITHOUT collider, navmesh contribution or + * LOD set. Markings are built per wedge/segment (each page owns + * its seed nodes' pieces, so no duplication); decals are per edge + * with the halves attributed to the page seeding the owning node + * (edgeT <= 0.5 -> nodeA) so cross-page edges paint once. + */ + std::vector > markingChunks; + std::vector > decalChunks; + if (!chunks.empty()) { + std::shared_ptr mcur = + std::make_shared(); + std::shared_ptr dcur = + std::make_shared(); + auto flushOverlay = [](std::shared_ptr &c, + std::vector > &dst) { + if (!c->getIndices().empty()) + dst.push_back(c); + c = std::make_shared(); + }; + auto appendOverlay = [&](std::shared_ptr &c, + std::vector > &dst, + const Procedural::TriangleBuffer &piece) { + if (piece.getIndices().empty()) + return; + if (!c->getIndices().empty() && + c->getVertices().size() + piece.getVertices().size() > + ROAD_PAGE_CHUNK_MAX_VERTICES) + flushOverlay(c, dst); + c->append(piece); + }; + + for (const RoadWedge &w : pg.wedges) { + RoadGeometryLib::RoadMarkingBuffers mb; + RoadGeometryLib::buildWedgeMarkings(w, rg, mb); + appendOverlay(mcur, markingChunks, mb.centerline); + appendOverlay(mcur, markingChunks, mb.laneLines); + appendOverlay(mcur, markingChunks, mb.edgeLines); + appendOverlay(mcur, markingChunks, mb.stopLines); + } + for (const RoadStraightSegment &s : pg.segments) { + RoadGeometryLib::RoadMarkingBuffers mb; + RoadGeometryLib::buildSegmentMarkings(s, rg, mb); + appendOverlay(mcur, markingChunks, mb.centerline); + appendOverlay(mcur, markingChunks, mb.laneLines); + appendOverlay(mcur, markingChunks, mb.edgeLines); + appendOverlay(mcur, markingChunks, mb.stopLines); + } + flushOverlay(mcur, markingChunks); + + /* Debug: ROAD_DUMP_MARKINGS= writes every marking + * triangle (9 floats per line) for offline analysis. */ + if (const char *dumpPath = getenv("ROAD_DUMP_MARKINGS")) { + std::ofstream dump(dumpPath, std::ios::app); + for (const auto &mc : markingChunks) { + const auto &vv = mc->getVertices(); + const auto &ii = mc->getIndices(); + for (size_t t = 0; t + 2 < ii.size(); t += 3) { + const Ogre::Vector3 &a = + vv[ii[t]].mPosition; + const Ogre::Vector3 &b = + vv[ii[t + 1]].mPosition; + const Ogre::Vector3 &c = + vv[ii[t + 2]].mPosition; + dump << a.x << " " << a.y << " " << a.z + << " " << b.x << " " << b.y << " " + << b.z << " " << c.x << " " << c.y + << " " << c.z << "\n"; + } + } + } + + std::set seedIds; + for (const RoadWedge &w : pg.wedges) + seedIds.insert(w.nodeId); + for (const RoadStraightSegment &s : pg.segments) + seedIds.insert(s.nodeId); + std::set edgeSet; + for (int id : seedIds) + for (size_t ei : rg.findEdgeIndicesForNode(id)) + edgeSet.insert((int)ei); + for (int ei : edgeSet) { + const RoadEdge &e = rg.edges[(size_t)ei]; + if (e.decals.empty()) + continue; + Procedural::TriangleBuffer piece; + int owners[2] = { e.nodeA, e.nodeB }; + for (int oi = 0; oi < 2; ++oi) { + if (!seedIds.count(owners[oi])) + continue; + RoadGeometryLib::buildEdgeDecals(rg, (size_t)ei, + piece, + owners[oi]); + } + appendOverlay(dcur, decalChunks, piece); + } + flushOverlay(dcur, decalChunks); + } if (chunks.empty()) { /* No road content seeded on this page. */ @@ -1416,6 +1641,45 @@ void RoadSystem::buildPageMeshes(RoadPageGeometry &pg) lod.settingsEntity = ensureRoadLodSettings(rg.config); e.set(lod); } + + /* + * Overlay chunk entities (phase 2.5): marking strips and decal + * quads. Deliberately no NavMeshGeometrySource, no LodComponent + * and no materialEntity — the finalize pass assigns the shared + * marking/decal material by name (picked from the "_markings"/ + * "_decals" mesh-name infix) and never builds colliders for them. + */ + auto createOverlayChunks = [&](const std::vector > &ochunks, + const std::string &infix) { + for (size_t i = 0; i < ochunks.size(); ++i) { + std::string meshName = baseName + infix; + if (ochunks.size() > 1) + meshName += "_c" + std::to_string(i); + + flecs::entity e = m_world.entity(); + + TransformComponent xf; + xf.node = m_sceneMgr->getRootSceneNode() + ->createChildSceneNode(meshName + + "_node"); + e.set(xf); + + RenderableComponent rend; + rend.meshName = meshName; + e.set(rend); + + pg.overlayEntities.push_back(e); + + TriangleBufferComponent tb; + tb.buffer = ochunks[i]; + tb.proceduralContent = true; + tb.meshName = meshName; + tb.dirty = true; + e.set(tb); + } + }; + createOverlayChunks(markingChunks, "_markings"); + createOverlayChunks(decalChunks, "_decals"); } void RoadSystem::destroyPageMeshes(RoadPageGeometry &pg) @@ -1459,6 +1723,43 @@ void RoadSystem::destroyPageMeshes(RoadPageGeometry &pg) e.destruct(); } pg.bufferEntities.clear(); + + /* Overlay chunks (phase 2.5 markings/decals): same teardown, minus + * the navmesh bookkeeping they never contributed to. */ + for (flecs::entity e : pg.overlayEntities) { + if (!e.is_alive()) + continue; + + if (e.has()) { + auto &tb = e.get_mut(); + if (tb.ogreEntity) { + try { + m_sceneMgr->destroyEntity(tb.ogreEntity); + } catch (...) { + } + tb.ogreEntity = nullptr; + } + if (!tb.meshName.empty()) { + try { + Ogre::MeshManager::getSingleton() + .remove(tb.meshName); + } catch (...) { + } + } + tb.meshCreated = false; + } + + if (e.has()) { + auto &xf = e.get_mut(); + if (xf.node) { + m_sceneMgr->destroySceneNode(xf.node); + xf.node = nullptr; + } + } + + e.destruct(); + } + pg.overlayEntities.clear(); pg.meshFinalized = false; if (contributedToNavMesh) @@ -1522,6 +1823,236 @@ flecs::entity RoadSystem::ensureRoadLodSettings(const RoadConfig &cfg) return m_lodSettingsEntity; } +/* ------------------------------------------------------------------ */ +/* Phase 2.5 overlay materials and atlas textures */ +/* ------------------------------------------------------------------ */ + +void RoadSystem::createRoadAtlasTextures() +{ + Ogre::TextureManager &tm = Ogre::TextureManager::getSingleton(); + const Ogre::String &grp = + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME; + + /* Road surface atlas: U bands 0-0.5 asphalt, 0.5-0.75 sidewalk, + * 0.75-1.0 apron; V tiles along the road (TAM_WRAP). The asphalt + * noise is elongated along image Y (the along-road V axis), so the + * grain reads as wheel-track streaks along the lane instead of + * isotropic speckle; mipmaps stay enabled (default) so distant + * minification blurs instead of throwing cross-road moire straps. + */ + if (!tm.resourceExists("RoadAtlasTexture")) { + const int W = 256, H = 256; + std::vector data((size_t)W * H * 3); + for (int y = 0; y < H; ++y) { + for (int x = 0; x < W; ++x) { + float u = ((float)x + 0.5f) / (float)W; + /* Cheap deterministic value noise so the + * bands do not look flat; asphalt quantizes Y + * into 16-px runs (streaks along the lane). + * The coordinates are avalanched separately + * before mixing — a bare multiply-xor hash + * leaves a lattice correlation that moires + * into a giant checkerboard on the road. */ + int ys = (u < 0.5f) ? y / 16 : y; + unsigned hx = (unsigned)x * 0x8da6b343u; + hx ^= hx >> 15; + unsigned hy = (unsigned)ys * 0xd8163841u; + hy ^= hy >> 15; + unsigned s = hx ^ hy; + s *= 0xcb1ab31fu; + s ^= s >> 16; + float n = (float)(s & 0xff) / 255.0f - 0.5f; + int r, g, b; + if (u < 0.5f) { + /* Asphalt: dark gray, fine noise. */ + int c = 58 + (int)(n * 24.0f); + r = g = b = c; + } else if (u < 0.75f) { + /* Sidewalk: light gray paving. */ + int c = 150 + (int)(n * 18.0f); + r = g = b = c; + } else { + /* Apron: warm tan gravel. */ + r = 128 + (int)(n * 20.0f); + g = 112 + (int)(n * 18.0f); + b = 92 + (int)(n * 16.0f); + } + size_t idx = ((size_t)y * W + x) * 3; + data[idx + 0] = (Ogre::uchar)std::max( + 0, std::min(255, r)); + data[idx + 1] = (Ogre::uchar)std::max( + 0, std::min(255, g)); + data[idx + 2] = (Ogre::uchar)std::max( + 0, std::min(255, b)); + } + } + Ogre::Image img; + img.loadDynamicImage(data.data(), W, H, 1, Ogre::PF_BYTE_RGB); + tm.loadImage("RoadAtlasTexture", grp, img, Ogre::TEX_TYPE_2D, + Ogre::MIP_DEFAULT, 1.0f); + } + + /* + * Decal atlas (RGBA, transparent background): four quadrant + * presets, selected per decal via RoadEdgeDecal uv* — + * [0, 0]-[0.5, 0.5] manhole cover (dark disc + ring) + * [0.5, 0]-[1, 0.5] direction arrow (white, +V forward) + * [0, 0.5]-[0.5, 1] road damage patch (dark blotch) + * [0.5, 0.5]-[1, 1] parking bars (white horizontal lines) + * Placeholder art until a real atlas lands in the asset pipeline. + */ + if (!tm.resourceExists("RoadDecalAtlas")) { + const int W = 256, H = 256; + std::vector data((size_t)W * H * 4, 0); + auto px = [&](int x, int y, int r, int g, int b, int a) { + size_t idx = ((size_t)y * W + x) * 4; + data[idx + 0] = (Ogre::uchar)r; + data[idx + 1] = (Ogre::uchar)g; + data[idx + 2] = (Ogre::uchar)b; + data[idx + 3] = (Ogre::uchar)a; + }; + for (int y = 0; y < H; ++y) { + for (int x = 0; x < W; ++x) { + /* Local quadrant coordinates 0..1. */ + float qx = (x % 128 + 0.5f) / 128.0f; + float qy = (y % 128 + 0.5f) / 128.0f; + bool right = x >= 128; + bool bottom = y >= 128; + if (!right && !bottom) { + /* Manhole: dark disc with a ring. */ + float dx = qx - 0.5f, dy = qy - 0.5f; + float rr = std::sqrt(dx * dx + dy * dy); + if (rr < 0.42f) { + int c = 40; + if (rr > 0.36f || rr < 0.1f) + c = 70; + px(x, y, c, c, c, 255); + } + } else if (right && !bottom) { + /* Arrow: shaft + head, forward +V. */ + bool shaft = std::fabs(qx - 0.5f) < 0.06f && + qy > 0.15f && qy < 0.7f; + float hd = std::fabs(qx - 0.5f) + + (qy - 0.7f); + bool head = qy >= 0.7f && qy < 0.95f && + hd < 0.35f; + if (shaft || head) + px(x, y, 230, 230, 230, 255); + } else if (!right && bottom) { + /* Damage patch: irregular blotch. */ + float dx = qx - 0.5f, dy = qy - 0.55f; + float rr = std::sqrt(dx * dx + dy * dy); + unsigned s = (unsigned)(x * 2654435761u) ^ + (unsigned)(y * 40503u); + float n = ((s >> 13) & 0xff) / 255.0f; + if (rr < 0.3f + 0.12f * n) + px(x, y, 26, 24, 22, + 150 + (int)(60 * n)); + } else { + /* Parking bars: white horizontals. */ + float fy = qy * 5.0f; + bool bar = (fy - std::floor(fy)) < 0.45f && + qx > 0.15f && qx < 0.85f; + if (bar) + px(x, y, 235, 235, 235, 255); + } + } + } + Ogre::Image img; + img.loadDynamicImage(data.data(), W, H, 1, Ogre::PF_BYTE_RGBA); + tm.loadImage("RoadDecalAtlas", grp, img, Ogre::TEX_TYPE_2D, 0, + 1.0f); + } +} + +void RoadSystem::ensureOverlayMaterials(const RoadConfig &cfg) +{ + Ogre::MaterialManager &mm = Ogre::MaterialManager::getSingleton(); + const Ogre::String &grp = + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME; + + if (!mm.resourceExists(cfg.markingMaterialName)) { + /* + * Unlit white paint. RTSS ignores material colours in a + * lighting-disabled pass and renders it white — exactly + * what road paint needs. Culling is disabled so the + * paper-thin quads render from any angle; depth write + * stays on (the strips hover 2 cm above the road top, so + * no bias is needed). + */ + Ogre::MaterialPtr mat = + mm.create(cfg.markingMaterialName, grp); + Ogre::Technique *tech = mat->getTechnique(0); + Ogre::Pass *pass = tech->getNumPasses() > 0 ? + tech->getPass(0) : + tech->createPass(); + pass->setLightingEnabled(false); + pass->setCullingMode(Ogre::CULL_NONE); + } + + if (!mm.resourceExists(cfg.decalMaterialName)) { + createRoadAtlasTextures(); + Ogre::MaterialPtr mat = mm.create(cfg.decalMaterialName, grp); + Ogre::Technique *tech = mat->getTechnique(0); + Ogre::Pass *pass = tech->getNumPasses() > 0 ? + tech->getPass(0) : + tech->createPass(); + pass->setLightingEnabled(false); + pass->setCullingMode(Ogre::CULL_NONE); + pass->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA); + pass->setDepthWriteEnabled(false); + Ogre::TextureUnitState *tu = + pass->createTextureUnitState("RoadDecalAtlas"); + tu->setTextureAddressingMode(Ogre::TextureUnitState::TAM_CLAMP); + } +} + +void RoadSystem::syncRoadAtlasTextureUnit(const RoadConfig &cfg) +{ + if (cfg.textureTileLength <= 1e-6f) + return; + + Ogre::MaterialManager &mm = Ogre::MaterialManager::getSingleton(); + if (!mm.resourceExists(cfg.roadMaterialName)) + return; + createRoadAtlasTextures(); + + /* + * The road material is restyled in place by + * ProceduralMaterialSystem (which removes all passes whenever the + * material component is dirty), so attach the atlas texture unit + * whenever it is missing. Both axes wrap: U never leaves + * [0, 1] but V tiles once per textureTileLength meters. + */ + Ogre::MaterialPtr mat = mm.getByName( + cfg.roadMaterialName, + Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME); + if (!mat) + return; + for (unsigned ti = 0; ti < mat->getNumTechniques(); ++ti) { + Ogre::Technique *tech = mat->getTechnique(ti); + for (unsigned pi = 0; pi < tech->getNumPasses(); ++pi) { + Ogre::Pass *pass = tech->getPass(pi); + bool has = false; + for (unsigned ui = 0; ui < pass->getNumTextureUnitStates(); + ++ui) { + if (pass->getTextureUnitState(ui)->getTextureName() == + "RoadAtlasTexture") { + has = true; + break; + } + } + if (!has) { + Ogre::TextureUnitState *tu = + pass->createTextureUnitState( + "RoadAtlasTexture"); + tu->setTextureAddressingMode( + Ogre::TextureUnitState::TAM_WRAP); + } + } + } +} + void RoadSystem::markNavMeshDirty() { /* While streaming, page churn dirties the navmesh repeatedly; @@ -1656,12 +2187,13 @@ static void addBox(Ogre::ManualObject *mo, const Ogre::Vector3 ¢er, void RoadSystem::rebuildVisualAids() { - if (!m_nodeMarkers || !m_edgeLines || !m_widthIndicators || - !m_selectionHighlight) + if (!m_nodeMarkers || !m_edgeLines || !m_lotOutlines || + !m_widthIndicators || !m_selectionHighlight) return; m_nodeMarkers->clear(); m_edgeLines->clear(); + m_lotOutlines->clear(); m_widthIndicators->clear(); m_selectionHighlight->clear(); @@ -1680,6 +2212,7 @@ void RoadSystem::rebuildVisualAids() buildNodeMarkers(); buildEdgeLines(); + buildLotOutlines(); buildWidthIndicators(); buildSelectionHighlight(); @@ -1750,6 +2283,40 @@ void RoadSystem::buildEdgeLines() m_edgeLines->end(); } +void RoadSystem::buildLotOutlines() +{ + flecs::entity terrain = getTerrainEntity(); + if (!terrain.is_alive() || !terrain.has()) + return; + + const RoadGraph &rg = m_world.entity(m_terrainEntityId).get().roadGraph; + + /* Potential building lots (RoadNetworkGen::findLots with the + * default 45 m / 8-edge filter and edge-fallback squares) drawn as + * dark grey outline loops, lifted slightly above the road surface + * like the other helper geometry. */ + std::vector lots = + RoadNetworkGen::findLots(rg); + + m_lotOutlines->begin("Ogre/AxisGizmo", + Ogre::RenderOperation::OT_LINE_LIST); + m_lotOutlines->colour(Ogre::ColourValue(0.1f, 0.1f, 0.1f)); + + for (const RoadNetworkGen::RoadLot &lot : lots) { + size_t n = lot.polygon.size(); + for (size_t i = 0; i < n; ++i) { + Ogre::Vector3 a = lot.polygon[i]; + Ogre::Vector3 b = lot.polygon[(i + 1) % n]; + a.y += 0.25f; + b.y += 0.25f; + m_lotOutlines->position(a); + m_lotOutlines->position(b); + } + } + + m_lotOutlines->end(); +} + void RoadSystem::buildWidthIndicators() { flecs::entity terrain = getTerrainEntity(); @@ -1966,6 +2533,131 @@ int RoadSystem::relaxImpossibleCorners() return (int)m_impossibleCorners.size(); } +uint32_t RoadSystem::generateRoadNetwork( + const RoadNetworkGen::GridParams ¶ms, std::string *error) +{ + flecs::entity terrain = getTerrainEntity(); + if (!terrain.is_alive() || !terrain.has()) { + if (error) + *error = "no terrain entity"; + return 0; + } + RoadGraph &graph = + terrain.get_mut().roadGraph; + + /* Heights come from the terrain; the water level from the + * scene's WaterPhysics entity (default -0.1 when absent). */ + TerrainSystem *ts = TerrainSystem::getInstance(); + RoadNetworkGen::HeightSampler sampler; + if (ts) + sampler = [ts](float x, float z) { + return ts->getHeightAt(Ogre::Vector3(x, 0.0f, z)); + }; + RoadNetworkGen::GridParams p = params; + if (p.avoidWater) { + float waterY = -0.1f; + m_world.each([&waterY](WaterPhysics &wp) { + waterY = wp.waterSurfaceY; + }); + p.waterLevel = waterY; + } + + uint32_t groupId = + RoadNetworkGen::generateGrid(graph, p, sampler, error); + if (groupId == 0) + return 0; + m_impossibleCornersVersion = ~0ull; + clearGenPreview(); + rebuildVisualAids(); + return groupId; +} + +void RoadSystem::updateGenPreview(const RoadNetworkGen::GridParams ¶ms) +{ + if (!m_genPreview) + return; + m_genPreview->clear(); + + /* Generate into a throwaway graph — the real graph is never + * touched; only the would-be edges are drawn as lines. */ + RoadGraph temp; + flecs::entity terrain = getTerrainEntity(); + if (terrain.is_alive() && terrain.has()) + temp.config = terrain.get().roadGraph.config; + + /* Same height/water inputs as generateRoadNetwork(). */ + TerrainSystem *ts = TerrainSystem::getInstance(); + RoadNetworkGen::HeightSampler sampler; + if (ts) + sampler = [ts](float x, float z) { + return ts->getHeightAt(Ogre::Vector3(x, 0.0f, z)); + }; + RoadNetworkGen::GridParams p = params; + if (p.avoidWater) { + float waterY = -0.1f; + m_world.each([&waterY](WaterPhysics &wp) { + waterY = wp.waterSurfaceY; + }); + p.waterLevel = waterY; + } + + if (RoadNetworkGen::generateGrid(temp, p, sampler, nullptr) == 0) + return; + + m_genPreview->begin("Ogre/AxisGizmo", + Ogre::RenderOperation::OT_LINE_LIST); + m_genPreview->colour(Ogre::ColourValue(1.0f, 0.65f, 0.0f)); + for (const RoadEdge &e : temp.edges) { + const RoadNode *na = temp.findNodeById(e.nodeA); + const RoadNode *nb = temp.findNodeById(e.nodeB); + if (!na || !nb) + continue; + m_genPreview->position(na->position + + Ogre::Vector3(0.0f, 0.3f, 0.0f)); + m_genPreview->position(nb->position + + Ogre::Vector3(0.0f, 0.3f, 0.0f)); + } + m_genPreview->end(); +} + +void RoadSystem::clearGenPreview() +{ + if (m_genPreview) + m_genPreview->clear(); +} + +int RoadSystem::resolveCrossingsNow(std::string *error) +{ + flecs::entity terrain = getTerrainEntity(); + if (!terrain.is_alive() || !terrain.has()) { + if (error) + *error = "no terrain entity"; + return -1; + } + RoadGraph &graph = + terrain.get_mut().roadGraph; + int created = RoadNetworkGen::resolveAllCrossings(graph, error); + if (created > 0) { + m_impossibleCornersVersion = ~0ull; + rebuildVisualAids(); + } + return created; +} + +bool RoadSystem::clearGenerated(uint32_t groupId) +{ + flecs::entity terrain = getTerrainEntity(); + if (!terrain.is_alive() || !terrain.has()) + return false; + RoadGraph &graph = + terrain.get_mut().roadGraph; + if (!RoadNetworkGen::removeGroup(graph, groupId)) + return false; + m_impossibleCornersVersion = ~0ull; + rebuildVisualAids(); + return true; +} + const Procedural::TriangleBuffer & RoadSystem::getRoadTemplate(const RoadConfig &cfg) { diff --git a/src/features/editScene/systems/RoadSystem.hpp b/src/features/editScene/systems/RoadSystem.hpp index 5546e5d..d682e27 100644 --- a/src/features/editScene/systems/RoadSystem.hpp +++ b/src/features/editScene/systems/RoadSystem.hpp @@ -18,6 +18,7 @@ #include "../components/RoadGraph.hpp" #include "../roadlib/RoadGeometryLib.hpp" +#include "../roadlib/RoadNetworkGen.hpp" #include "RoadRegionStore.hpp" namespace Ogre { @@ -49,6 +50,14 @@ struct RoadPageGeometry { */ std::vector bufferEntities; + /** + * Overlay chunk entities of this page (phase 2.5): painted marking + * strips and decal quads hovering slightly above the road top. + * Purely visual — no physics collider, no navmesh contribution, no + * LOD set; they share the chunking rule of bufferEntities. + */ + std::vector overlayEntities; + /** * Jolt bodies of this page's static road chunk colliders (M5.9): * one body per chunk mesh, created from the chunk's render mesh @@ -185,6 +194,57 @@ public: */ int relaxImpossibleCorners(); + /** + * Phase 3: procedural road network services (RoadNetworkGen). + * + * generateRoadNetwork emits a seeded settlement grid into the + * terrain road graph (heights sampled from the terrain, water + * level from the scene's WaterPhysics entity) and returns the + * generatedGroupId stamped on the new nodes (0 on failure). + * resolveCrossingsNow turns every edge crossing in the graph into + * a proper junction and returns the number created (-1 when the + * pass limit was exhausted). clearGenerated removes every node + * (and cascading edges) of one generated group. + */ + uint32_t generateRoadNetwork(const RoadNetworkGen::GridParams ¶ms, + std::string *error = nullptr); + int resolveCrossingsNow(std::string *error = nullptr); + bool clearGenerated(uint32_t groupId); + + /** + * Generator origin picking: while armed, the next road-edit-mode + * terrain click (EditorUISystem::handleRoadEditClick) is consumed + * as the generator origin instead of a tool action; the terrain + * panel polls takePickedOrigin() to pick it up. + */ + void armOriginPick(bool armed) { m_originPickArmed = armed; } + bool isOriginPickArmed() const { return m_originPickArmed; } + void setPickedOrigin(const Ogre::Vector3 &p) + { + m_pickedOrigin = p; + m_originPicked = true; + } + bool takePickedOrigin(Ogre::Vector3 &out) + { + if (!m_originPicked) + return false; + out = m_pickedOrigin; + m_originPicked = false; + return true; + } + + /** + * Generator preview: updateGenPreview() runs generateGrid() into a + * throwaway graph (same terrain height/water inputs as + * generateRoadNetwork) and draws the would-be edges as orange + * lines, so generator parameters can be tweaked before the actual + * generation; the real graph is never touched. The preview lives + * on its own scene-node attachment, independent of the road-edit + * visual aids. clearGenPreview() removes the lines. + */ + void updateGenPreview(const RoadNetworkGen::GridParams ¶ms); + void clearGenPreview(); + /** Wedge-debug mode for geometry inspection. */ bool getDebugWedgeEnabled() const { return m_debugWedgeEnabled; } void setDebugWedgeEnabled(bool v); @@ -489,6 +549,7 @@ private: void buildNodeMarkers(); void buildEdgeLines(); + void buildLotOutlines(); void buildWidthIndicators(); void buildSelectionHighlight(); @@ -528,6 +589,14 @@ private: void destroyPageMeshes(RoadPageGeometry &pg); flecs::entity ensureRoadMaterial(const RoadConfig &cfg); flecs::entity ensureRoadLodSettings(const RoadConfig &cfg); + + /* Overlay materials/textures (phase 2.5): the unlit white marking + * material, the alpha-blended decal material and the procedurally + * generated road/decal atlas textures. All created lazily via + * Ogre::MaterialManager/TextureManager when missing. */ + void ensureOverlayMaterials(const RoadConfig &cfg); + void createRoadAtlasTextures(); + void syncRoadAtlasTextureUnit(const RoadConfig &cfg); void markNavMeshDirty(); void applyNavMeshDirty(); @@ -562,6 +631,12 @@ private: std::string m_connectError; bool m_connectErrorPending = false; + /* Generator origin picking: while armed the next road-edit terrain + * click is captured as the generator origin for the panel. */ + bool m_originPickArmed = false; + bool m_originPicked = false; + Ogre::Vector3 m_pickedOrigin = Ogre::Vector3::ZERO; + /* Page geometry state (M5.4). m_wedgeBuckets holds the enumerated * wedges/segments bucketed by seed-node page for ALL pages (including * unloaded ones); m_pageGeometry only tracks loaded pages. */ @@ -621,8 +696,10 @@ private: Ogre::SceneNode *m_visualNode = nullptr; Ogre::ManualObject *m_nodeMarkers = nullptr; Ogre::ManualObject *m_edgeLines = nullptr; + Ogre::ManualObject *m_lotOutlines = nullptr; Ogre::ManualObject *m_widthIndicators = nullptr; Ogre::ManualObject *m_selectionHighlight = nullptr; + Ogre::ManualObject *m_genPreview = nullptr; }; #endif // EDITSCENE_ROADSYSTEM_HPP diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 2ba080d..6e31c86 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -26,6 +26,7 @@ #include "../components/CharacterSlots.hpp" #include "../components/CharacterIdentity.hpp" #include "CharacterRegistry.hpp" +#include "../roadlib/RoadNetworkGen.hpp" #include "../components/AnimationTree.hpp" #include "AnimationTreeRegistry.hpp" #include "AnimationTreeRegistry.hpp" @@ -1181,6 +1182,13 @@ bool SceneSerializer::instantiatePrefab(flecs::entity instanceEntity, // Restore main scene entity map m_entityMap = savedMap; + /* Resolve procedural texture references queued while + * deserializing the prefab (a self-contained prefab carries + * both the texture and the material referencing it); at + * runtime nothing else calls this — it only runs at the end + * of a full scene load. */ + resolveProceduralTextureReferences(); + /* Run any scene scripts contained in the prefab subtree * (pending-only, so repeat instantiations are cheap), then * notify listeners that this instance finished loading. */ @@ -4814,10 +4822,33 @@ nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity) tc.roadGraph.config.apronSkirtDepth; roadConfigJson["cornerFilletLength"] = tc.roadGraph.config.cornerFilletLength; + roadConfigJson["crossingSnapDistance"] = + tc.roadGraph.config.crossingSnapDistance; + roadConfigJson["crossingHeightThreshold"] = + tc.roadGraph.config.crossingHeightThreshold; roadConfigJson["prefabSpawnDistance"] = tc.roadGraph.config.prefabSpawnDistance; roadConfigJson["prefabDespawnDistance"] = tc.roadGraph.config.prefabDespawnDistance; + /* Phase 2.5: markings + road atlas texture. */ + roadConfigJson["markingsEnabled"] = tc.roadGraph.config.markingsEnabled; + roadConfigJson["markingLineWidth"] = + tc.roadGraph.config.markingLineWidth; + roadConfigJson["markingDashLength"] = + tc.roadGraph.config.markingDashLength; + roadConfigJson["markingDashGap"] = tc.roadGraph.config.markingDashGap; + roadConfigJson["markingStopWidth"] = + tc.roadGraph.config.markingStopWidth; + roadConfigJson["markingStopSetback"] = + tc.roadGraph.config.markingStopSetback; + roadConfigJson["markingEdgeInset"] = + tc.roadGraph.config.markingEdgeInset; + roadConfigJson["markingMaterialName"] = + tc.roadGraph.config.markingMaterialName; + roadConfigJson["decalMaterialName"] = + tc.roadGraph.config.decalMaterialName; + roadConfigJson["textureTileLength"] = + tc.roadGraph.config.textureTileLength; json["roadConfig"] = roadConfigJson; /* Streaming terrains persist the road network in per-page region @@ -4832,6 +4863,8 @@ nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity) nj["position"] = { n.position.x, n.position.y, n.position.z }; nj["verticalOffset"] = n.verticalOffset; + if (n.generatedGroupId != 0) + nj["generatedGroupId"] = n.generatedGroupId; roadNodesJson.push_back(nj); } json["roadNodes"] = roadNodesJson; @@ -4850,6 +4883,21 @@ nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity) ej["sidewalkRight"] = e.sidewalkRight; ej["sidewalkLeftMode"] = (int)e.sidewalkLeftMode; ej["sidewalkRightMode"] = (int)e.sidewalkRightMode; + if ((int)e.markingsMode != 0) + ej["markingsMode"] = (int)e.markingsMode; + if (!e.decals.empty()) { + nlohmann::json dj = nlohmann::json::array(); + for (const RoadEdgeDecal &d : e.decals) { + nlohmann::json jd; + jd["edgeT"] = d.edgeT; + jd["lateralOffset"] = d.lateralOffset; + jd["size"] = d.size; + jd["uv"] = { d.uvX, d.uvY, d.uvW, + d.uvH }; + dj.push_back(jd); + } + ej["decals"] = dj; + } if (!e.sidewalkPasses.empty()) { nlohmann::json pj = nlohmann::json::array(); for (const RoadSidewalkPass &p : @@ -4877,6 +4925,10 @@ nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity) roadEdgesJson.push_back(ej); } json["roadEdges"] = roadEdgesJson; + + /* Phase 3: named streets (streaming terrains store them + * in roads/streets.json via RoadSystem instead). */ + RoadNetworkGen::streetsToJson(tc.roadGraph, json); } /* Persist binary heightmap, blend maps, and aux maps alongside the JSON. */ @@ -4993,10 +5045,35 @@ void SceneSerializer::deserializeTerrain(flecs::entity entity, rcj.value("apronSkirtDepth", 2.0f); tc.roadGraph.config.cornerFilletLength = rcj.value("cornerFilletLength", 12.0f); + tc.roadGraph.config.crossingSnapDistance = + rcj.value("crossingSnapDistance", 5.0f); + tc.roadGraph.config.crossingHeightThreshold = + rcj.value("crossingHeightThreshold", 10.0f); tc.roadGraph.config.prefabSpawnDistance = rcj.value("prefabSpawnDistance", 150.0f); tc.roadGraph.config.prefabDespawnDistance = rcj.value("prefabDespawnDistance", 250.0f); + /* Phase 2.5: markings + road atlas texture. */ + tc.roadGraph.config.markingsEnabled = + rcj.value("markingsEnabled", true); + tc.roadGraph.config.markingLineWidth = + rcj.value("markingLineWidth", 0.15f); + tc.roadGraph.config.markingDashLength = + rcj.value("markingDashLength", 3.0f); + tc.roadGraph.config.markingDashGap = + rcj.value("markingDashGap", 3.0f); + tc.roadGraph.config.markingStopWidth = + rcj.value("markingStopWidth", 0.4f); + tc.roadGraph.config.markingStopSetback = + rcj.value("markingStopSetback", 1.0f); + tc.roadGraph.config.markingEdgeInset = + rcj.value("markingEdgeInset", 0.25f); + tc.roadGraph.config.markingMaterialName = + rcj.value("markingMaterialName", "RoadMarkingMaterial"); + tc.roadGraph.config.decalMaterialName = + rcj.value("decalMaterialName", "RoadDecalMaterial"); + tc.roadGraph.config.textureTileLength = + rcj.value("textureTileLength", 0.0f); } if (json.contains("roadNodes") && json["roadNodes"].is_array()) { @@ -5004,6 +5081,8 @@ void SceneSerializer::deserializeTerrain(flecs::entity entity, RoadNode node; node.id = nj.value("id", 0); node.verticalOffset = nj.value("verticalOffset", 0.0f); + node.generatedGroupId = + nj.value("generatedGroupId", (uint32_t)0); if (nj.contains("position") && nj["position"].is_array() && nj["position"].size() >= 3) { node.position.x = nj["position"][0]; @@ -5033,6 +5112,28 @@ void SceneSerializer::deserializeTerrain(flecs::entity entity, "sidewalkLeftMode", edge.sidewalkLeft ? 0 : 2); edge.sidewalkRightMode = (RoadSidewalkMode)ej.value( "sidewalkRightMode", edge.sidewalkRight ? 0 : 2); + /* Phase 2.5: marking tri-state (absent = Inherit) and + * the decal list. */ + edge.markingsMode = + (RoadMarkingMode)ej.value("markingsMode", 0); + if (ej.contains("decals") && ej["decals"].is_array()) { + for (const auto &jd : ej["decals"]) { + RoadEdgeDecal d; + d.edgeT = jd.value("edgeT", 0.5f); + d.lateralOffset = + jd.value("lateralOffset", 0.0f); + d.size = jd.value("size", 1.0f); + if (jd.contains("uv") && + jd["uv"].is_array() && + jd["uv"].size() == 4) { + d.uvX = jd["uv"][0]; + d.uvY = jd["uv"][1]; + d.uvW = jd["uv"][2]; + d.uvH = jd["uv"][3]; + } + edge.decals.push_back(d); + } + } if (ej.contains("sidewalkPasses") && ej["sidewalkPasses"].is_array()) { for (const auto &jp : ej["sidewalkPasses"]) { @@ -5102,6 +5203,11 @@ void SceneSerializer::deserializeTerrain(flecs::entity entity, } } + /* Phase 3: named streets (absent in legacy scenes; streaming + * terrains load them from roads/streets.json via RoadSystem). */ + if (json.contains("streets")) + RoadNetworkGen::streetsFromJson(tc.roadGraph, json); + tc.roadGraph.bumpVersion(); entity.set(tc); diff --git a/src/features/editScene/systems/TerrainPrefabSpawnerSystem.cpp b/src/features/editScene/systems/TerrainPrefabSpawnerSystem.cpp index af2ada6..2e3d7bf 100644 --- a/src/features/editScene/systems/TerrainPrefabSpawnerSystem.cpp +++ b/src/features/editScene/systems/TerrainPrefabSpawnerSystem.cpp @@ -34,16 +34,20 @@ TerrainPrefabSpawnerSystem::TerrainPrefabSpawnerSystem( { s_instance = this; - m_world.observer( - "TerrainPrefabSpawnerCleanup") - .event(flecs::OnRemove) - .each([this](flecs::entity e, TerrainPrefabSpawnerComponent &) { - despawn(e); - }); + m_cleanupObserver = + m_world.observer( + "TerrainPrefabSpawnerCleanup") + .event(flecs::OnRemove) + .each([this](flecs::entity e, + TerrainPrefabSpawnerComponent &) { + despawn(e); + }); } TerrainPrefabSpawnerSystem::~TerrainPrefabSpawnerSystem() { + if (m_cleanupObserver.is_alive()) + m_cleanupObserver.destruct(); for (auto &pair : m_spawners) { if (pair.second.spawned.is_alive()) destroyInstanceRecursive(pair.second.spawned); diff --git a/src/features/editScene/systems/TerrainPrefabSpawnerSystem.hpp b/src/features/editScene/systems/TerrainPrefabSpawnerSystem.hpp index ca75d24..32552c7 100644 --- a/src/features/editScene/systems/TerrainPrefabSpawnerSystem.hpp +++ b/src/features/editScene/systems/TerrainPrefabSpawnerSystem.hpp @@ -163,6 +163,9 @@ private: Ogre::Vector3 getCameraPosition() const; flecs::world &m_world; + /* Kept so the destructor can unregister it: the observer must not + * fire into a destroyed system during world teardown (ecs_fini). */ + flecs::entity m_cleanupObserver; Ogre::SceneManager *m_sceneMgr; EditorCameraSystem *m_cameraSystem; EditorUISystem *m_uiSystem; diff --git a/src/features/editScene/systems/TerrainSystem.cpp b/src/features/editScene/systems/TerrainSystem.cpp index 660aef0..4294748 100644 --- a/src/features/editScene/systems/TerrainSystem.cpp +++ b/src/features/editScene/systems/TerrainSystem.cpp @@ -1,5 +1,6 @@ #include "TerrainSystem.hpp" #include "RoadSystem.hpp" +#include "TownGenSystem.hpp" #include "../components/Terrain.hpp" #include "../components/Transform.hpp" #include "../physics/physics.h" @@ -3012,6 +3013,11 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform, m_roadSystem->setTerrainEntity(m_world.entity(m_terrainEntityId)); m_roadSystem->setVisualAidsVisible(m_roadEditMode); + /* Town generation builds on the road graph; it holds no OGRE + * objects of its own, so no teardown is needed on deactivate. */ + m_townGenSystem = std::make_unique(m_world, + m_sceneMgr); + if (m_physics) m_physics->setBodyDrawFilter(&mBodyDrawFilter); @@ -3112,6 +3118,7 @@ void TerrainSystem::deactivate() /* Destroy road system visual aids before terrain teardown. */ m_roadSystem.reset(); + m_townGenSystem.reset(); deactStep("roadSystem reset"); removeAllColliders(); diff --git a/src/features/editScene/systems/TerrainSystem.hpp b/src/features/editScene/systems/TerrainSystem.hpp index 8f82790..140a7b0 100644 --- a/src/features/editScene/systems/TerrainSystem.hpp +++ b/src/features/editScene/systems/TerrainSystem.hpp @@ -32,6 +32,7 @@ class JoltPhysicsWrapper; class RoadSystem; +class TownGenSystem; class TerrainSystem { public: @@ -406,6 +407,11 @@ public: return m_roadSystem.get(); } + TownGenSystem *getTownGenSystem() const + { + return m_townGenSystem.get(); + } + /* Physics wrapper used for terrain/road colliders (may be null). * Exposed for headless tests that verify collider interaction. */ JoltPhysicsWrapper *getPhysics() const @@ -527,6 +533,7 @@ public: private: std::unique_ptr m_roadSystem; + std::unique_ptr m_townGenSystem; void activate(struct TerrainComponent &tc, struct TransformComponent &xform, flecs::entity_t terrainEntityId); diff --git a/src/features/editScene/systems/TownGenSystem.cpp b/src/features/editScene/systems/TownGenSystem.cpp new file mode 100644 index 0000000..20943eb --- /dev/null +++ b/src/features/editScene/systems/TownGenSystem.cpp @@ -0,0 +1,516 @@ +#include "TownGenSystem.hpp" + +#include "TerrainSystem.hpp" +#include "RoadSystem.hpp" +#include "../components/Terrain.hpp" +#include "../components/Transform.hpp" +#include "../components/EntityName.hpp" +#include "../components/EditorMarker.hpp" +#include "../components/Primitive.hpp" +#include "../components/TriangleBuffer.hpp" +#include "../components/RigidBody.hpp" +#include "../components/PhysicsCollider.hpp" +#include "../components/ProceduralMaterial.hpp" +#include "../components/ProceduralTexture.hpp" +#include "../components/WaterPhysics.hpp" +#include "../components/RoadGraph.hpp" +#include "../roadlib/RoadNetworkGen.hpp" + +#include +#include + +/* Building cell metrics: 2 m cells, 3 m floor height. */ +static const float TOWN_CELL_SIZE = 2.0f; +static const float TOWN_CELL_HEIGHT = 3.0f; +/* Foundation reaches this far below the lowest terrain corner. */ +static const float FOUNDATION_MARGIN = 0.5f; +/* Stairs step geometry. */ +static const float STAIR_RISE = 0.18f; +static const float STAIR_TREAD = 0.30f; +static const float STAIR_WIDTH = 1.4f; + +TownGenSystem::TownGenSystem(flecs::world &world, Ogre::SceneManager *sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) +{ + m_terrainSystem = TerrainSystem::getInstance(); +} + +int TownGenSystem::countTowns() const +{ + int count = 0; + m_world.query().each( + [&count](flecs::entity, EntityNameComponent &name, + TownComponent &) { + if (name.name.rfind("TownGen ", 0) == 0) + ++count; + }); + return count; +} + +int TownGenSystem::clear() +{ + std::vector towns; + m_world.query().each( + [&towns](flecs::entity e, EntityNameComponent &name, + TownComponent &) { + if (name.name.rfind("TownGen ", 0) == 0) + towns.push_back(e); + }); + for (flecs::entity e : towns) + e.destruct(); + return (int)towns.size(); +} + +int TownGenSystem::generate(const TownGen::TownParams ¶ms, + std::string *error) +{ + TerrainSystem *ts = m_terrainSystem ? + m_terrainSystem : + TerrainSystem::getInstance(); + if (!ts || !ts->getRoadSystem()) { + if (error) + *error = "no terrain system"; + return 0; + } + flecs::entity terrain = ts->getRoadSystem()->getTerrainEntity(); + if (!terrain.is_alive() || !terrain.has()) { + if (error) + *error = "no active terrain"; + return 0; + } + const RoadGraph &graph = + terrain.get().roadGraph; + + TownGen::TownParams p = params; + float waterY = -FLT_MAX; + m_world.each([&waterY](WaterPhysics &wp) { + waterY = wp.waterSurfaceY; + }); + p.waterLevel = waterY; + + RoadNetworkGen::HeightSampler sampler; + if (ts) + sampler = [ts](float x, float z) { + return ts->getHeightAt(Ogre::Vector3(x, 0.0f, z)); + }; + + TownGen::TownResult result = TownGen::generateTown(graph, p, sampler); + + /* Replace any previously generated town. */ + clear(); + + if (result.buildings.empty()) { + if (error) + *error = "no suitable lots"; + return 0; + } + + const TownGen::DistrictParams &dp = + TownGen::districtParams(p.district); + + /* Container entity: holds the shared material reference + * (TownComponent) so buildings pick it up via the CellGrid + * parent-chain material lookup. */ + flecs::entity container = m_world.entity(); + std::string containerName = + std::string("TownGen ") + dp.name + " seed " + + std::to_string(p.seed); + container.set( + EntityNameComponent(containerName)); + container.add(); + TransformComponent containerXform; + containerXform.node = + m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + containerXform.position = Ogre::Vector3::ZERO; + containerXform.rotation = Ogre::Quaternion::IDENTITY; + containerXform.scale = Ogre::Vector3::UNIT_SCALE; + containerXform.applyToNode(); + container.set(containerXform); + + flecs::entity texture = + createSettlementResources(container, dp, p.seed); + + flecs::entity material = m_world.entity(); + material.child_of(container); + material.set( + EntityNameComponent(containerName + " material")); + material.add(); + ProceduralMaterialComponent mat; + mat.materialName = containerName + " material"; + mat.diffuseTextureId = + texture.get().textureId; + mat.diffuseTextureEntity = texture; + material.set(mat); + + TownComponent town; + town.townName = containerName; + town.proceduralMaterialEntity = material; + town.proceduralMaterialEntityId = std::to_string(material.id()); + container.set(town); + + for (size_t i = 0; i < result.buildings.size(); ++i) + createBuilding(container, material, texture, + result.buildings[i], result.pois[i]); + + return (int)result.buildings.size(); +} + +flecs::entity +TownGenSystem::createSettlementResources(flecs::entity container, + const TownGen::DistrictParams &dp, + uint32_t seed) +{ + flecs::entity texture = m_world.entity(); + texture.child_of(container); + texture.set(EntityNameComponent( + "towngen_tex_" + std::to_string(seed))); + texture.add(); + + ProceduralTextureComponent tex; + tex.textureName = "towngen_tex_" + std::to_string(seed); + + /* Whole atlas tinted with the district wall color; specific + * rects get their own colors. */ + Ogre::ColourValue wall(dp.wallColor[0], dp.wallColor[1], + dp.wallColor[2]); + for (int i = 0; i < ProceduralTextureComponent::RECT_COUNT; ++i) + tex.setColor(i, wall); + tex.setColor(1, Ogre::ColourValue(0.55f, 0.30f, 0.25f)); /* roof */ + tex.setColor(2, Ogre::ColourValue(0.55f, 0.55f, 0.55f)); /* found. */ + tex.setColor(3, Ogre::ColourValue(0.35f, 0.22f, 0.12f)); /* door */ + tex.setColor(4, Ogre::ColourValue(0.90f, 0.90f, 0.88f)); /* winframe */ + tex.setColor(5, Ogre::ColourValue(0.75f, 0.70f, 0.62f)); /* floor */ + tex.setColor(6, Ogre::ColourValue(0.92f, 0.92f, 0.92f)); /* ceiling */ + tex.addNamedRect("wall", 0); + tex.addNamedRect("roof", 1); + tex.addNamedRect("foundation", 2); + tex.addNamedRect("door", 3); + tex.addNamedRect("winframe", 4); + tex.addNamedRect("floor", 5); + tex.addNamedRect("ceiling", 6); + texture.set(tex); + + return texture; +} + +void TownGenSystem::fillBuildingCells(const TownGen::TownBuilding &b, + CellGridComponent &grid) +{ + int cellsX = std::max(2, (int)std::lround(b.width / TOWN_CELL_SIZE)); + int cellsZ = std::max(2, (int)std::lround(b.depth / TOWN_CELL_SIZE)); + + grid.width = cellsX; + grid.height = b.floors; + grid.depth = cellsZ; + grid.cellSize = TOWN_CELL_SIZE; + grid.cellHeight = TOWN_CELL_HEIGHT; + grid.generationMode = "exteriorOnly"; + + /* Exterior shell only; the entrance door frame is generated but + * the leaf is not (doorsEnabled=false). The scene-switch door + * config is preset for Phase 8 — flipping doorsEnabled wires the + * interior scene switch. */ + grid.doorsEnabled = false; + grid.doorSceneSwitchPath = "town_interior.json"; + + grid.floorRectName = "floor"; + grid.ceilingRectName = "ceiling"; + grid.extWallRectName = "wall"; + grid.intWallRectName = "wall"; + grid.extDoorFrameRectName = "door"; + grid.intDoorFrameRectName = "door"; + grid.extWindowFrameRectName = "winframe"; + grid.intWindowFrameRectName = "winframe"; + grid.roofTopRectName = "roof"; + grid.roofSideRectName = "roof"; + grid.doorRectName = "door"; + + /* Front (street-facing) side is local +Z; the door sits in the + * center cell of the ground floor front. */ + int doorX = cellsX / 2; + + for (int y = 0; y < b.floors; ++y) { + bool top = (y == b.floors - 1); + for (int x = 0; x < cellsX; ++x) { + for (int z = 0; z < cellsZ; ++z) { + Cell &c = grid.getOrCreateCell(x, y, z); + c.setFlag(CellFlags::Floor); + if (top) + c.setFlag(CellFlags::Ceiling); + + bool xMin = (x == 0), xMax = (x == cellsX - 1); + bool zMin = (z == 0), zMax = (z == cellsZ - 1); + bool corner = (xMin || xMax) && (zMin || zMax); + + if (y == 0 && zMax && x == doorX) { + c.setFlag(CellFlags::DoorZPos); + c.setFlag(CellFlags::IntDoorZPos); + if (xMin) + c.setFlag(CellFlags::WallXNeg); + if (xMax) + c.setFlag(CellFlags::WallXPos); + continue; + } + + /* Non-corner perimeter cells get + * windows; corners stay plain walls. */ + if (xMin) + c.setFlag(corner ? CellFlags::WallXNeg : + CellFlags::WindowXNeg); + if (xMax) + c.setFlag(corner ? CellFlags::WallXPos : + CellFlags::WindowXPos); + if (zMin) + c.setFlag(corner ? CellFlags::WallZNeg : + CellFlags::WindowZNeg); + if (zMax) + c.setFlag(corner ? CellFlags::WallZPos : + CellFlags::WindowZPos); + } + } + } + + grid.markDirty(); +} + +void TownGenSystem::createBuilding(flecs::entity container, + flecs::entity material, flecs::entity texture, + const TownGen::TownBuilding &b, + const TownGen::TownPoi &poi) +{ + TerrainSystem *ts = m_terrainSystem ? + m_terrainSystem : + TerrainSystem::getInstance(); + + flecs::entity building = m_world.entity(); + building.child_of(container); + building.set(EntityNameComponent(b.address)); + building.add(); + + CellGridComponent grid; + fillBuildingCells(b, grid); + + /* cellToWorld() puts cell (x, y, z) at x*cellSize, so the shell + * spans [0, width*cellSize] from the entity node — shift the + * node so the shell is CENTERED on TownBuilding::center and the + * front face (local +Z) sits at shellD/2 facing the street. + * Foundation/stairs/POI children use the same convention. */ + float shellW = grid.width * TOWN_CELL_SIZE; + float shellD = grid.depth * TOWN_CELL_SIZE; + Ogre::Vector3 shellCenter((grid.width - 1) * TOWN_CELL_SIZE * 0.5f, + 0.0f, + (grid.depth - 1) * TOWN_CELL_SIZE * 0.5f); + Ogre::Quaternion yaw(Ogre::Radian(b.yaw), Ogre::Vector3::UNIT_Y); + + /* TownGen works in render space (the height sampler is + * TerrainSystem::getHeightAt); store the authoritative world + * position so render-origin rebases keep the node exact. */ + Ogre::Vector3 renderPos = b.center - yaw * shellCenter; + + TransformComponent xform; + xform.node = m_sceneMgr->getRootSceneNode()->createChildSceneNode(); + xform.position = renderPos; + xform.rotation = yaw; + xform.scale = Ogre::Vector3::UNIT_SCALE; + if (ts) { + xform.worldX = ts->renderToWorldX(renderPos.x); + xform.worldY = ts->renderToWorldY(renderPos.y); + xform.worldZ = ts->renderToWorldZ(renderPos.z); + xform.hasWorldPosition = true; + } + xform.applyToNode(); + building.set(xform); + + building.set(grid); + + /* Flat roof over the whole footprint — buildRoofs() only + * processes RoofComponent children of the grid entity. posY at + * one cell above the top floor puts the slab on top of the + * walls; its trim covers the top-floor ceiling seam. */ + flecs::entity roof = m_world.entity(); + roof.child_of(building); + roof.set(EntityNameComponent("roof")); + roof.add(); + RoofComponent rc; + rc.type = RoofComponent::Flat; + rc.posX = 0; + rc.posY = b.floors; + rc.posZ = 0; + rc.sizeX = grid.width; + rc.sizeZ = grid.depth; + roof.set(rc); + + createFoundation(building, b, grid, material, texture); + if (b.stairs) + createStairs(building, b, grid, material, texture); + + /* Curb pickup POI (name-convention marker entity), parented to + * the building with a local transform. */ + flecs::entity poiEnt = m_world.entity(); + poiEnt.child_of(building); + poiEnt.set(EntityNameComponent(poi.name)); + poiEnt.add(); + TransformComponent poiXform; + poiXform.node = xform.node->createChildSceneNode(); + poiXform.position = + xform.rotation.Inverse() * (poi.position - renderPos); + poiXform.rotation = Ogre::Quaternion::IDENTITY; + poiXform.scale = Ogre::Vector3::UNIT_SCALE; + poiXform.applyToNode(); + poiEnt.set(poiXform); +} + +void TownGenSystem::createFoundation(flecs::entity building, + const TownGen::TownBuilding &b, + const CellGridComponent &grid, + flecs::entity material, + flecs::entity texture) +{ + float foundationH = b.floorY - (b.minGroundY - FOUNDATION_MARGIN); + + /* The building node sits half a shell off the intended + * footprint center (cells start at the node, see + * createBuilding) — the block centers on the shell. */ + float shellW = grid.width * TOWN_CELL_SIZE; + float shellD = grid.depth * TOWN_CELL_SIZE; + Ogre::Vector3 shellCenter((grid.width - 1) * TOWN_CELL_SIZE * 0.5f, + 0.0f, + (grid.depth - 1) * TOWN_CELL_SIZE * 0.5f); + + flecs::entity foundation = m_world.entity(); + foundation.child_of(building); + foundation.set(EntityNameComponent("foundation")); + foundation.add(); + + /* Child of the building node: identity local transform, the + * buffer box hangs below the shell base (floor level). */ + TransformComponent xform; + Ogre::SceneNode *buildingNode = building.get().node; + xform.node = buildingNode->createChildSceneNode(); + xform.position = Ogre::Vector3::ZERO; + xform.rotation = Ogre::Quaternion::IDENTITY; + xform.scale = Ogre::Vector3::UNIT_SCALE; + xform.applyToNode(); + foundation.set(xform); + + TriangleBufferComponent tb; + tb.textureEntity = texture; + tb.textureRectName = "foundation"; + tb.materialEntity = material; + tb.meshName = b.address + "_foundation"; + foundation.set(tb); + + RigidBodyComponent rb; + rb.bodyType = RigidBodyComponent::BodyType::Static; + foundation.set(rb); + + flecs::entity box = m_world.entity(); + box.child_of(foundation); + box.set(EntityNameComponent("block")); + TransformComponent boxXform; + boxXform.node = xform.node->createChildSceneNode(); + boxXform.position = + shellCenter + Ogre::Vector3(0.0f, + -foundationH * 0.5f + 0.05f, 0.0f); + boxXform.rotation = Ogre::Quaternion::IDENTITY; + boxXform.scale = Ogre::Vector3::UNIT_SCALE; + boxXform.applyToNode(); + box.set(boxXform); + + PrimitiveComponent prim; + prim.type = PrimitiveComponent::Type::Box; + prim.boxSizeX = shellW + 0.15f; + prim.boxSizeY = foundationH; + prim.boxSizeZ = shellD + 0.15f; + box.set(prim); + + PhysicsColliderComponent collider; + collider.shapeType = PhysicsColliderComponent::ShapeType::Box; + collider.parameters = Ogre::Vector3((shellW + 0.15f) * 0.5f, + foundationH * 0.5f, + (shellD + 0.15f) * 0.5f); + box.set(collider); +} + +void TownGenSystem::createStairs(flecs::entity building, + const TownGen::TownBuilding &b, + const CellGridComponent &grid, + flecs::entity material, flecs::entity texture) +{ + int steps = (int)std::ceil(b.stairsDrop / STAIR_RISE); + if (steps < 2) + return; + float rise = b.stairsDrop / (float)steps; + + /* Shell convention (see createBuilding): cells start at the + * node, walls extend cellSize/2 past the outer cell centers. + * The door sits in the center cell of the front (Z+) face; + * the steps descend from there. */ + float doorX = (grid.width / 2) * TOWN_CELL_SIZE; + float frontZ = (grid.depth - 0.5f) * TOWN_CELL_SIZE; + + flecs::entity stairs = m_world.entity(); + stairs.child_of(building); + stairs.set(EntityNameComponent("stairs")); + stairs.add(); + + TransformComponent xform; + Ogre::SceneNode *parentNode = + building.get().node; + xform.node = parentNode->createChildSceneNode(); + xform.position = Ogre::Vector3::ZERO; + xform.rotation = Ogre::Quaternion::IDENTITY; + xform.scale = Ogre::Vector3::UNIT_SCALE; + xform.applyToNode(); + stairs.set(xform); + + TriangleBufferComponent tb; + tb.textureEntity = texture; + tb.textureRectName = "foundation"; + tb.materialEntity = material; + tb.meshName = b.address + "_stairs"; + stairs.set(tb); + + RigidBodyComponent rb; + rb.bodyType = RigidBodyComponent::BodyType::Static; + stairs.set(rb); + + /* Solid staircase descending from the door (local +Z, front + * face at grid.depth * cellSize) to the ground at -stairsDrop; + * the ground itself is the last step. */ + for (int i = 0; i < steps - 1; ++i) { + float surfaceY = -((float)i + 1.0f) * rise; + float height = surfaceY - (-b.stairsDrop); + if (height < 0.01f) + continue; + + flecs::entity step = m_world.entity(); + step.child_of(stairs); + step.set( + EntityNameComponent("step " + std::to_string(i))); + + TransformComponent stepXform; + stepXform.node = xform.node->createChildSceneNode(); + stepXform.position = Ogre::Vector3( + doorX, (-b.stairsDrop + surfaceY) * 0.5f, + frontZ + ((float)i + 0.5f) * STAIR_TREAD); + stepXform.rotation = Ogre::Quaternion::IDENTITY; + stepXform.scale = Ogre::Vector3::UNIT_SCALE; + stepXform.applyToNode(); + step.set(stepXform); + + PrimitiveComponent prim; + prim.type = PrimitiveComponent::Type::Box; + prim.boxSizeX = STAIR_WIDTH; + prim.boxSizeY = height; + prim.boxSizeZ = STAIR_TREAD; + step.set(prim); + + PhysicsColliderComponent collider; + collider.shapeType = PhysicsColliderComponent::ShapeType::Box; + collider.parameters = Ogre::Vector3( + STAIR_WIDTH * 0.5f, height * 0.5f, STAIR_TREAD * 0.5f); + step.set(collider); + } +} diff --git a/src/features/editScene/systems/TownGenSystem.hpp b/src/features/editScene/systems/TownGenSystem.hpp new file mode 100644 index 0000000..6e7f24a --- /dev/null +++ b/src/features/editScene/systems/TownGenSystem.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include + +#include "../roadlib/TownGen.hpp" +#include "../components/CellGrid.hpp" + +class TerrainSystem; + +/** + * Town generation system (demo-taxi phase 4). + * + * Turns the lots of the terrain's road graph into a procedural + * settlement of CellGrid buildings (roadlib/TownGen does the pure + * placement math): one exteriorOnly CellGrid shell per lot, seated on + * a foundation block (PrimitiveComponent box) with entrance stairs + * when the door-side ground drops below the floor, plus a named + * pickup POI child at the curb. Every building carries its + * scene-switch door config with doorsEnabled=false so Phase 8 only + * has to enable and wire the interiors. + * + * Generated entities are ordinary scene entities (EditorMarker) under + * a container entity named "TownGen seed " holding a + * TownComponent that points at the shared settlement material, so + * they stay editable and serialize with the scene. generate() clears + * any previous generated town first; clear() removes it. + * + * Placement convention: `CellGridComponent::cellToWorld()` puts cell + * (x, y, z) at x*cellSize, so a shell spans [0, width*cellSize] from + * its node. createBuilding() therefore shifts the building node so + * the shell is CENTERED on TownBuilding::center (front face at local + * +Z facing the street); the foundation/stairs children position + * themselves in the same shell coordinates. + */ +class TownGenSystem +{ +public: + TownGenSystem(flecs::world &world, Ogre::SceneManager *sceneMgr); + + /** Generate a settlement on the active terrain's road graph. + * Returns the number of buildings placed (0 on failure, @p error + * set). Clears the previously generated town first. */ + int generate(const TownGen::TownParams ¶ms, std::string *error); + + /** Destroy every generated town (all "TownGen " containers). */ + int clear(); + + /** Live "TownGen " container entities. */ + int countTowns() const; + +private: + flecs::entity createSettlementResources(flecs::entity container, + const TownGen::DistrictParams &dp, + uint32_t seed); + void createBuilding(flecs::entity container, flecs::entity material, + flecs::entity texture, const TownGen::TownBuilding &b, + const TownGen::TownPoi &poi); + void createFoundation(flecs::entity building, + const TownGen::TownBuilding &b, + const CellGridComponent &grid, + flecs::entity material, flecs::entity texture); + void createStairs(flecs::entity building, const TownGen::TownBuilding &b, + const CellGridComponent &grid, + flecs::entity material, flecs::entity texture); + static void fillBuildingCells(const TownGen::TownBuilding &b, + CellGridComponent &grid); + + flecs::world &m_world; + Ogre::SceneManager *m_sceneMgr; + TerrainSystem *m_terrainSystem = nullptr; +}; diff --git a/src/features/editScene/tests/road_geometry_overlap_test.cpp b/src/features/editScene/tests/road_geometry_overlap_test.cpp index c3592c9..f92c567 100644 --- a/src/features/editScene/tests/road_geometry_overlap_test.cpp +++ b/src/features/editScene/tests/road_geometry_overlap_test.cpp @@ -21,8 +21,10 @@ * Runs the built-in regression configurations (flat, raised/ * lowered corner node, raised endpoints) plus junction probes * that combine all wedges of 3-way/4-way nodes into one buffer - * to catch adjacent-wedge overlap. Exit code 0 when no piece - * self-intersects, 1 otherwise. + * to catch adjacent-wedge overlap, plus the phase 2.5 probes + * (marking strip placement/gating, stop-line clear zones, + * decals, road-atlas UV bands and V tiling). Exit code 0 when + * no piece self-intersects, 1 otherwise. * road_geometry_overlap_test Ax Ay Az Bx By Bz Cx Cy Cz * Analyse a single custom configuration (same checks). */ @@ -32,6 +34,8 @@ #include "components/RoadGraph.hpp" #include "roadlib/RoadGeometryLib.hpp" +#include "roadlib/RoadNetworkGen.hpp" +#include "roadlib/TownGen.hpp" #include #include @@ -1080,6 +1084,133 @@ static bool runRegionProbe(const char *path, float laneWidth = 0.0f, return ok; } +/* Markings extent scan: build the marking overlays of every wedge and + * straight segment and measure how far the paint lands from its node. + * Exploded curb offsets at nasty corners (impossible corners, pin-zone + * failures) show up here as extents far beyond the wedge footprint — + * in game they render as "markings to the sky" spikes. */ +static bool runRegionMarkingsProbe(const char *path, float laneWidth = 0.0f) +{ + RoadGraph graph; + double ox = 0.0, oy = 0.0, oz = 0.0; + if (!loadRegionGraph(path, graph, ox, oy, oz, nullptr, laneWidth, + 0.0f, 0.0f)) + return false; + if (graph.nodes.empty()) + return true; + graph.config.markingsEnabled = true; + + std::vector wedges; + std::vector segs; + enumerateWedges(graph, wedges, segs); + + bool ok = true; + long badWedges = 0, badSegs = 0, impossible = 0; + std::vector > extents; /* overshoot */ + std::vector > spans; /* (extent, span) */ + float maxDy = 0.0f; + auto scanBuf = [&](const Procedural::TriangleBuffer &buf, + const Vector3 &origin, float limit, float &maxD) { + for (const auto &v : buf.getVertices()) { + const Vector3 &p = v.mPosition; + if (std::isnan(p.x) || std::isnan(p.y) || + std::isnan(p.z) || std::isinf(p.x) || + std::isinf(p.y) || std::isinf(p.z)) { + maxD = FLT_MAX; + return true; + } + float dx = p.x - origin.x, dy = p.y - origin.y, + dz = p.z - origin.z; + maxD = std::max(maxD, + std::sqrt(dx * dx + dy * dy + + dz * dz)); + maxDy = std::max(maxDy, std::fabs(dy)); + } + return maxD > limit; + }; + for (const auto &w : wedges) { + if (w.degenerate) + continue; + const RoadNode *node = graph.findNodeById(w.nodeId); + if (!node) + continue; + float ps = 0.0f, pe = 0.0f; + bool imp = false; + RoadGeometryLib::filletPinZone(w, graph, ps, pe, nullptr, + &imp); + if (imp) + ++impossible; + float halfW = graph.config.laneWidth * + std::max(1, graph.config.lanesPerDirection); + float span = w.first.halfLength + w.second.halfLength; + float limit = span + 4.0f * halfW + 5.0f; + RoadGeometryLib::RoadMarkingBuffers mb; + RoadGeometryLib::buildWedgeMarkings(w, graph, mb); + float maxD = 0.0f; + bool bad = scanBuf(mb.centerline, node->position, limit, + maxD) || + scanBuf(mb.laneLines, node->position, limit, + maxD) || + scanBuf(mb.edgeLines, node->position, limit, + maxD) || + scanBuf(mb.stopLines, node->position, limit, + maxD); + extents.push_back(std::make_pair(maxD - span, + (uint64_t)w.nodeId)); + spans.push_back(std::make_pair(maxD, span)); + if (bad) { + ++badWedges; + printf(" wedge node %llu: paint extent %.1f m " + "(span %.1f, limit %.1f)%s\n", + (unsigned long long)w.nodeId, maxD, span, + limit, imp ? " [IMPOSSIBLE CORNER]" : ""); + ok = false; + } + } + for (const auto &s : segs) { + const RoadNode *node = graph.findNodeById(s.nodeId); + if (!node) + continue; + float halfW = graph.config.laneWidth * + std::max(1, graph.config.lanesPerDirection); + float limit = s.halfEdge.halfLength + 4.0f * halfW + 5.0f; + RoadGeometryLib::RoadMarkingBuffers mb; + RoadGeometryLib::buildSegmentMarkings(s, graph, mb); + float maxD = 0.0f; + bool bad = scanBuf(mb.centerline, node->position, limit, + maxD) || + scanBuf(mb.laneLines, node->position, limit, + maxD) || + scanBuf(mb.edgeLines, node->position, limit, + maxD) || + scanBuf(mb.stopLines, node->position, limit, + maxD); + if (bad) { + ++badSegs; + printf(" segment node %llu: paint extent %.1f m " + "(limit %.1f)\n", (unsigned long long)s.nodeId, + maxD, limit); + ok = false; + } + } + printf(" markings scan: %ld bad wedges, %ld bad segments, " + "%ld impossible corners, max |dy| from node %.2f m\n", + badWedges, badSegs, impossible, maxDy); + std::sort(extents.begin(), extents.end(), + [](const std::pair &a, + const std::pair &b) { + return a.first > b.first; + }); + for (size_t i = 0; i < extents.size() && i < 8; ++i) + printf(" extent node %llu: %.1f m (span %.1f, " + "overshoot %+.1f)\n", + (unsigned long long)extents[i].second, + spans[i].first, spans[i].second, extents[i].first); + if (!ok) + printf(" *** region markings probe FAILED ***\n"); + return ok; +} + /* Single-node probe: rebuild every wedge of one junction node into ONE * combined buffer (the way a page assembles them) and run the full * coplanar/crossing analysis — catches adjacent-wedge z-fighting at the @@ -2782,6 +2913,889 @@ static bool runPinZonePrecision() return ok; } +/* Pin-zone memoization regression (demo-taxi exterior, nodes 3/16): + * filletMiterPinZoneRaw caches the scan per corner; the cached entry + * round-tripped only the pin point's X/Z, leaving Y uninitialized on + * cache hits. The pinning curb offset (K - center with + * K = node position + pinPoint) could then point straight down with a + * garbage length, and marking rows landing exactly on a pin-zone + * boundary spiked to y ~ -1.5e6 at runtime (the geometry builder + * warmed the cache before the marking builder ran). Asserts: two + * consecutive filletPinZone calls (miss then hit) return identical + * zones and pin points, even when the hit call's output pinPoint is + * pre-filled with a sentinel. Corner geometry replicates demo node 3 + * (60 degrees, 17.8/15.8 m half-arms, 2+2 lanes of 5 m). */ +static bool runPinZoneCacheProbe() +{ + printf("pin-zone cache round-trip probe (tight corner, 2+2 " + "lanes)\n"); + + RoadGraph graph; + graph.config.laneWidth = 5.0f; + graph.config.lanesPerDirection = 2; + graph.config.roadThickness = 0.3f; + graph.config.cornerFilletLength = 12.0f; + graph.config.sidewalkEnabled = false; + graph.config.apronEnabled = false; + + Ogre::Vector3 d1(1.0f, 0.0f, 0.0f); + Ogre::Vector3 d2(0.5f, 0.0f, 0.8660254f); /* 60 degrees */ + int idA = graph.addNode(d1 * 35.6f, 0.0f); + int idB = graph.addNode(Vector3(0, 0, 0), 0.0f); + int idC = graph.addNode(d2 * 31.6f, 0.0f); + graph.addEdge(idB, idA); + graph.addEdge(idC, idB); + + std::vector wedges; + std::vector segs; + enumerateWedges(graph, wedges, segs); + + for (const auto &w : wedges) { + if (w.nodeId != idB || w.degenerate) + continue; + float ps1 = 0.0f, pe1 = 0.0f, ps2 = 0.0f, pe2 = 0.0f; + Ogre::Vector3 p1(0.0f, 0.0f, 0.0f); + if (!RoadGeometryLib::filletPinZone(w, graph, ps1, pe1, &p1, + nullptr)) + continue; /* not a pinning wedge */ + Ogre::Vector3 p2(-999.0f, -12345.0f, -999.0f); + bool has2 = RoadGeometryLib::filletPinZone(w, graph, ps2, + pe2, &p2, nullptr); + if (!has2 || ps1 != ps2 || pe1 != pe2 || p1.x != p2.x || + p1.y != p2.y || p1.z != p2.z) { + printf(" *** cache hit mismatch: [%.3f, %.3f] " + "(%.3f, %.3f, %.3f) vs [%.3f, %.3f] " + "(%.3f, %.3f, %.3f) ***\n", + ps1, pe1, p1.x, p1.y, p1.z, ps2, pe2, p2.x, + p2.y, p2.z); + return false; + } + printf(" zone [%.2f, %.2f] pin (%.3f, %.3f, %.3f) " + "round-trips\n", + ps1, pe1, p1.x, p1.y, p1.z); + return true; + } + printf(" *** no pinning wedge found ***\n"); + return false; +} + +/* ---------------------------------------------------------------- + * Phase 3 probes: road-network generator graph services + * (RoadNetworkGen): crossing resolution, lots, streets, grid + * generation. All pure graph math, no geometry building. + * ---------------------------------------------------------------- */ + +/* Crossing resolution: X crossing -> 4-way junction, end snap reuses + * the existing node, too-short edges collapse, overpasses (> 10 m Y at + * the closest points) are NOT crossings, the graph stays sane after + * every case. */ +static bool runCrossingResolutionProbe() +{ + printf("crossing resolution probe (junction/snap/collapse/" + "overpass)\n"); + using namespace RoadNetworkGen; + bool ok = true; + + /* Plain X crossing: both edges split, one shared junction node. */ + { + RoadGraph graph; + graph.config.laneWidth = 3.0f; + int a1 = graph.addNode(Vector3(0, 0, 0), 0.0f); + int a2 = graph.addNode(Vector3(40, 0, 0), 0.0f); + int b1 = graph.addNode(Vector3(20, 0, -20), 0.0f); + int b2 = graph.addNode(Vector3(20, 0, 20), 0.0f); + graph.addEdge(a1, a2); + graph.addEdge(b1, b2); + + std::vector cs = findCrossings(graph); + if (cs.size() != 1) { + printf(" *** X: %zu crossings, want 1 ***\n", + cs.size()); + ok = false; + } else { + std::string err; + int n = resolveAllCrossings(graph, &err); + const RoadNode *j = graph.findNodeById(cs[0].nodeA1); + (void)j; + int centerDegree = 0; + for (const RoadNode &node : graph.nodes) { + float d = std::sqrt( + node.position.x * node.position.x + + node.position.z * node.position.z); + if (std::fabs(node.position.x - 20.0f) < 1.0f && + std::fabs(node.position.z) < 1.0f) + centerDegree = + (int)graph.findEdgeIndicesForNode( + node.id) + .size(); + (void)d; + } + std::string serr; + if (n != 1 || graph.nodes.size() != 5 || + graph.edges.size() != 4 || centerDegree != 4 || + !checkSane(graph, &serr) || + !findCrossings(graph).empty()) { + printf(" *** X: n=%d nodes=%zu edges=%zu " + "deg=%d sane=%s (%s) ***\n", + n, graph.nodes.size(), + graph.edges.size(), centerDegree, + serr.empty() ? "yes" : "NO", + serr.c_str()); + ok = false; + } + } + } + + /* End snap: the crossing lies within crossingSnapDistance of an + * edge end — the existing end node becomes the junction (no split + * of that edge). */ + { + RoadGraph graph; + graph.config.laneWidth = 3.0f; + graph.config.crossingSnapDistance = 5.0f; + int a1 = graph.addNode(Vector3(0, 0, 0), 0.0f); + int a2 = graph.addNode(Vector3(40, 0, 0), 0.0f); + int b1 = graph.addNode(Vector3(38, 0, -20), 0.0f); + int b2 = graph.addNode(Vector3(38, 0, 20), 0.0f); + graph.addEdge(a1, a2); + graph.addEdge(b1, b2); + + std::string err; + int n = resolveAllCrossings(graph, &err); + /* Edge A kept whole (end node reused, moved onto the + * crossing), edge B split: 4 nodes, 3 edges, degree-3 + * junction. */ + std::string serr; + int maxDeg = 0; + for (const RoadNode &node : graph.nodes) + maxDeg = std::max( + maxDeg, + (int)graph.findEdgeIndicesForNode(node.id) + .size()); + if (n != 1 || graph.nodes.size() != 4 || + graph.edges.size() != 3 || maxDeg != 3 || + !checkSane(graph, &serr)) { + printf(" *** snap: n=%d nodes=%zu edges=%zu " + "maxDeg=%d sane=%s ***\n", + n, graph.nodes.size(), graph.edges.size(), + maxDeg, serr.empty() ? "yes" : "NO"); + ok = false; + } + } + + /* Collapse: an edge too short to split collapses into the end + * nearer to the crossing. */ + { + RoadGraph graph; + graph.config.laneWidth = 3.0f; + graph.config.crossingSnapDistance = 0.2f; + int a1 = graph.addNode(Vector3(0, 0, 0), 0.0f); + int a2 = graph.addNode(Vector3(1.5f, 0, 0), 0.0f); + int b1 = graph.addNode(Vector3(0.75f, 0, -10), 0.0f); + int b2 = graph.addNode(Vector3(0.75f, 0, 10), 0.0f); + graph.addEdge(a1, a2); + graph.addEdge(b1, b2); + + std::string err; + int n = resolveAllCrossings(graph, &err); + std::string serr; + if (n != 1 || graph.nodes.size() != 3 || + graph.edges.size() != 2 || !checkSane(graph, &serr) || + !findCrossings(graph).empty()) { + printf(" *** collapse: n=%d nodes=%zu edges=%zu " + "sane=%s ***\n", + n, graph.nodes.size(), graph.edges.size(), + serr.empty() ? "yes" : "NO"); + ok = false; + } + } + + /* Overpass: same XZ crossing with 15 m of Y separation is not a + * crossing. */ + { + RoadGraph graph; + graph.config.laneWidth = 3.0f; + int a1 = graph.addNode(Vector3(0, 0, 0), 0.0f); + int a2 = graph.addNode(Vector3(40, 0, 0), 0.0f); + int b1 = graph.addNode(Vector3(20, 15, -20), 0.0f); + int b2 = graph.addNode(Vector3(20, 15, 20), 0.0f); + graph.addEdge(a1, a2); + graph.addEdge(b1, b2); + if (!findCrossings(graph).empty()) { + printf(" *** overpass flagged as crossing ***\n"); + ok = false; + } + } + + printf(" %s\n", ok ? "all cases passed" : "FAILURES above"); + return ok; +} + +/* Lot detection: a 2-block grid yields exactly two 50x50 enclosed lots; + * a spur poking into one block rejects it; fallback squares grow off + * free edge sides; minimum-size and boundary-edge-count filters. */ +static bool runLotProbe() +{ + printf("lot detection probe (2-block grid, spur, fallback " + "squares, filters)\n"); + using namespace RoadNetworkGen; + bool ok = true; + RoadLotFilter noFb; + noFb.edgeFallback = false; + + auto buildGrid = [&](RoadGraph &graph, bool spur, float cell) { + int ids[3][2]; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 2; ++j) + ids[i][j] = graph.addNode( + Vector3(i * cell, 0, j * cell), 0.0f); + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 2; ++j) { + if (i < 2) + graph.addEdge(ids[i][j], + ids[i + 1][j]); + if (j < 1) + graph.addEdge(ids[i][j], + ids[i][j + 1]); + } + if (spur) { + int s = graph.addNode( + Vector3(cell * 0.5f, 0, cell * 0.5f), 0.0f); + graph.addEdge(ids[1][0], s); + } + }; + + RoadGraph graph; + buildGrid(graph, false, 50.0f); + std::vector lots = findLots(graph, noFb); + if (lots.size() != 2) { + printf(" *** grid: %zu lots, want 2 ***\n", lots.size()); + ok = false; + } else { + for (const RoadLot &lot : lots) + if (std::fabs(lot.area - 2500.0) > 1.0) { + printf(" *** lot area %.1f, want " + "2500 ***\n", + lot.area); + ok = false; + } + } + + /* Fallback squares (default filter): one 50x50 square off every + * free edge side — the middle vertical edge is covered on both + * sides, the other six each grow one outward square. */ + { + std::vector lotsFb = findLots(graph); + int fb = 0; + for (const RoadLot &l : lotsFb) + if (l.edgeFallback) + ++fb; + if (lotsFb.size() != 8 || fb != 6) { + printf(" *** grid fallback: %zu lots (%d " + "fallback), want 8 (6) ***\n", + lotsFb.size(), fb); + ok = false; + } + } + + RoadGraph graph2; + buildGrid(graph2, true, 50.0f); + std::vector lots2 = findLots(graph2, noFb); + if (lots2.size() != 1) { + printf(" *** spur: %zu lots, want 1 ***\n", lots2.size()); + ok = false; + } else if (std::fabs(lots2[0].centroid.x - 75.0f) > 1.0f) { + printf(" *** spur: wrong lot survived (cx=%.1f, want " + "75) ***\n", + lots2[0].centroid.x); + ok = false; + } + /* With fallback: the pierced block spawns no inward squares + * (the spur node sits strictly inside them) and the spur edge + * (~35 m) is too short — only the six outward squares survive. */ + { + std::vector lotsFb2 = findLots(graph2); + if (lotsFb2.size() != 7) { + printf(" *** spur fallback: %zu lots, want 7 " + "***\n", + lotsFb2.size()); + ok = false; + } + } + + /* Minimum size: a 30 m grid produces no lots at all (faces + * cannot fit 45x45, and 30 m edges spawn no fallback squares). */ + { + RoadGraph graphSmall; + buildGrid(graphSmall, false, 30.0f); + if (!findLots(graphSmall).empty()) { + printf(" *** small grid: %zu lots, want 0 ***\n", + findLots(graphSmall).size()); + ok = false; + } + } + + /* Boundary-edge cap: a 10-sided ring (edge ~49 m) is a single + * 10-edge face — rejected by maxBoundaryEdges. */ + { + RoadGraph ring; + int prev = -1, first = -1; + for (int i = 0; i < 10; ++i) { + float ang = (float)i * 0.628319f; + int id = ring.addNode( + Vector3(80.0f * std::cos(ang), 0, + 80.0f * std::sin(ang)), + 0.0f); + if (prev >= 0) + ring.addEdge(prev, id); + else + first = id; + prev = id; + } + ring.addEdge(prev, first); + if (!findLots(ring, noFb).empty()) { + printf(" *** ring10: %zu lots, want 0 ***\n", + findLots(ring, noFb).size()); + ok = false; + } + } + + /* An isolated 50 m edge spawns a fallback square on each side. */ + { + RoadGraph lone; + int a = lone.addNode(Vector3(0, 0, 0), 0.0f); + int b = lone.addNode(Vector3(50, 0, 0), 0.0f); + lone.addEdge(a, b); + std::vector loneLots = findLots(lone); + if (loneLots.size() != 2 || !loneLots[0].edgeFallback || + !loneLots[1].edgeFallback) { + printf(" *** lone edge: %zu lots, want 2 " + "fallback ***\n", + loneLots.size()); + ok = false; + } + } + + printf(" %s\n", ok ? "passed" : "FAILURES above"); + return ok; +} + +/* Streets: naming an edge sequence, reassignment, JSON round trip and + * unique sequential addresses (unnamed fallback). */ +static bool runStreetProbe() +{ + printf("street naming/addressing probe\n"); + using namespace RoadNetworkGen; + bool ok = true; + + RoadGraph graph; + int ids[3][2]; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 2; ++j) + ids[i][j] = graph.addNode( + Vector3(i * 50.0f, 0, j * 50.0f), 0.0f); + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 2; ++j) { + if (i < 2) + graph.addEdge(ids[i][j], ids[i + 1][j]); + if (j < 1) + graph.addEdge(ids[i][j], ids[i][j + 1]); + } + + std::vector > mainSt = { + std::make_pair(ids[0][1], ids[1][1]), + std::make_pair(ids[1][1], ids[2][1]) + }; + if (nameStreet(graph, "Main St", mainSt) < 0) { + printf(" *** nameStreet failed ***\n"); + ok = false; + } + if (!findStreetForEdge(graph, ids[0][1], ids[1][1]) || + findStreetForEdge(graph, ids[0][0], ids[1][0])) { + printf(" *** findStreetForEdge wrong ***\n"); + ok = false; + } + + /* Reassigning an edge to another street drops it from the + * first. */ + nameStreet(graph, "Side St", + { std::make_pair(ids[0][1], ids[1][1]) }); + const RoadStreet *ms = findStreetForEdge(graph, ids[1][1], + ids[2][1]); + const RoadStreet *ss = findStreetForEdge(graph, ids[0][1], + ids[1][1]); + if (!ms || ms->name != "Main St" || !ss || ss->name != "Side St") { + printf(" *** reassignment wrong ***\n"); + ok = false; + } + + /* JSON round trip into an identical graph. */ + RoadGraph graph2; + int ids2[3][2]; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 2; ++j) + ids2[i][j] = graph2.addNode( + Vector3(i * 50.0f, 0, j * 50.0f), 0.0f); + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 2; ++j) { + if (i < 2) + graph2.addEdge(ids2[i][j], ids2[i + 1][j]); + if (j < 1) + graph2.addEdge(ids2[i][j], ids2[i][j + 1]); + } + nlohmann::json j; + streetsToJson(graph, j); + streetsFromJson(graph2, j); + if (graph2.streets.size() != graph.streets.size()) { + printf(" *** round trip: %zu streets, want %zu ***\n", + graph2.streets.size(), graph.streets.size()); + ok = false; + } + + /* Addresses: unique, sequential along the street, unnamed + * fallback. (Enclosed faces only — fallback squares would add + * lots with no street.) */ + RoadLotFilter noFb; + noFb.edgeFallback = false; + std::vector lots = findLots(graph, noFb); + std::vector addr = assignLotAddresses(graph, lots); + if (addr.size() != 2 || addr[0] == addr[1]) { + printf(" *** addresses not unique ***\n"); + ok = false; + } + RoadGraph graph3; + int i0 = graph3.addNode(Vector3(0, 0, 0), 0.0f); + int i1 = graph3.addNode(Vector3(50, 0, 0), 0.0f); + int i2 = graph3.addNode(Vector3(50, 0, 50), 0.0f); + int i3 = graph3.addNode(Vector3(0, 0, 50), 0.0f); + graph3.addEdge(i0, i1); + graph3.addEdge(i1, i2); + graph3.addEdge(i2, i3); + graph3.addEdge(i3, i0); + std::vector lots3 = findLots(graph3, noFb); + std::vector addr3 = assignLotAddresses(graph3, lots3); + if (addr3.size() != 1 || addr3[0] != "Lot 1") { + printf(" *** unnamed fallback wrong (%s) ***\n", + addr3.empty() ? "" : addr3[0].c_str()); + ok = false; + } + + printf(" %s\n", ok ? "passed" : "FAILURES above"); + return ok; +} + +/* Town generation (demo-taxi phase 4): placement on lots, district + * presets, foundation heights, stairs, frontage-street addresses, + * determinism, water rejection. */ +static bool runTownGenProbe() +{ + printf("town generation probe\n"); + bool ok = true; + + /* 2x2 blocks of 100 m: 4 enclosed lots + 8 edge-fallback + * squares (one per perimeter edge) = 12 lots. */ + auto buildTown = [&](RoadGraph &graph) { + int ids[3][3]; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + ids[i][j] = graph.addNode( + Vector3(i * 100.0f, 0, j * 100.0f), + 0.0f); + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) { + if (i < 2) + graph.addEdge(ids[i][j], + ids[i + 1][j]); + if (j < 2) + graph.addEdge(ids[i][j], + ids[i][j + 1]); + } + }; + + RoadNetworkGen::HeightSampler flat = [](float, float) { + return 0.0f; + }; + + /* CityCenter density 1.0: every lot gets a building. */ + TownGen::TownParams cc; + cc.district = TownGen::District::CityCenter; + cc.seed = 7; + { + RoadGraph graph; + buildTown(graph); + TownGen::TownResult res = + TownGen::generateTown(graph, cc, flat); + if (res.buildings.size() != 12 || res.pois.size() != 12 || + res.skippedTooSmall != 0 || res.skippedWater != 0) { + printf(" *** city center: %zu buildings %zu pois " + "%d tooSmall %d water, want 12/12/0/0 ***\n", + res.buildings.size(), res.pois.size(), + res.skippedTooSmall, res.skippedWater); + ok = false; + } + for (const TownGen::TownBuilding &b : res.buildings) { + if (b.floors < 4 || b.floors > 8) { + printf(" *** floors %d out of city-center " + "range [4,8] ***\n", b.floors); + ok = false; + } + if (b.floorY < b.minGroundY - 0.001f) { + printf(" *** floorY %.2f below minGroundY " + "%.2f ***\n", b.floorY, b.minGroundY); + ok = false; + } + if (b.address.rfind("Lot ", 0) != 0) { + printf(" *** unnamed-street address '%s', " + "want 'Lot N' ***\n", b.address.c_str()); + ok = false; + } + if (b.stairs) { + printf(" *** stairs on flat ground ***\n"); + ok = false; + } + } + + /* Determinism: identical params -> identical output. */ + TownGen::TownResult res2 = + TownGen::generateTown(graph, cc, flat); + if (res2.buildings.size() != res.buildings.size()) { + printf(" *** determinism: count mismatch ***\n"); + ok = false; + } else { + for (size_t i = 0; i < res.buildings.size(); ++i) { + const TownGen::TownBuilding &a = + res.buildings[i]; + const TownGen::TownBuilding &b2 = + res2.buildings[i]; + if (a.center != b2.center || a.yaw != b2.yaw || + a.floors != b2.floors || + a.width != b2.width || + a.address != b2.address) { + printf(" *** determinism: building " + "%zu differs ***\n", i); + ok = false; + break; + } + } + } + + /* Buildings keep clear of the road body: every building + * footprint corner must stay at least the road + * half-width off every edge centerline segment. */ + auto segDistXZ = [](const Vector3 &p, const Vector3 &a, + const Vector3 &b) { + float abx = b.x - a.x, abz = b.z - a.z; + float apx = p.x - a.x, apz = p.z - a.z; + float len2 = abx * abx + abz * abz; + float t = len2 > 0.0f ? + (apx * abx + apz * abz) / len2 : + 0.0f; + t = std::max(0.0f, std::min(1.0f, t)); + float dx = p.x - (a.x + abx * t); + float dz = p.z - (a.z + abz * t); + return std::sqrt(dx * dx + dz * dz); + }; + for (const TownGen::TownBuilding &b : res.buildings) { + float hw = b.width * 0.5f; + float hd = b.depth * 0.5f; + Ogre::Quaternion rot(Ogre::Radian(b.yaw), + Vector3::UNIT_Y); + for (int ci = 0; ci < 4; ++ci) { + Vector3 local(ci < 2 ? -hw : hw, 0.0f, + ci % 2 == 0 ? -hd : hd); + Vector3 corner = b.center + rot * local; + for (const RoadEdge &e : graph.edges) { + const RoadNode *na = + graph.findNodeById(e.nodeA); + const RoadNode *nb = + graph.findNodeById(e.nodeB); + int la, lb; + graph.resolveLaneCounts(e, e.nodeA, + la, lb); + float half = (la + lb) * + graph.config.laneWidth * + 0.5f; + float d = segDistXZ(corner, + na->position, + nb->position); + if (d < half - 0.1f) { + printf(" *** clearance: " + "'%s' corner %.2f m off " + "edge %d-%d, want >= " + "%.2f ***\n", + b.address.c_str(), d, + e.nodeA, e.nodeB, half); + ok = false; + } + } + } + } + } + + /* Water rejection: everything below the water level. */ + { + RoadGraph graph; + buildTown(graph); + TownGen::TownParams wet = cc; + wet.waterLevel = 5.0f; + TownGen::TownResult res = + TownGen::generateTown(graph, wet, flat); + if (!res.buildings.empty() || res.skippedWater != 12) { + printf(" *** water: %zu buildings %d skipped, want " + "0/12 ***\n", res.buildings.size(), + res.skippedWater); + ok = false; + } + } + + /* Sloped ground rising with +z: door-front lots facing downhill + * get stairs, floorY stays the highest corner. */ + { + RoadGraph graph; + buildTown(graph); + RoadNetworkGen::HeightSampler slope = [](float, float z) { + return z * 0.2f; + }; + TownGen::TownResult res = + TownGen::generateTown(graph, cc, slope); + if (res.buildings.size() != 12) { + printf(" *** slope: %zu buildings, want 12 ***\n", + res.buildings.size()); + ok = false; + } + int withStairs = 0; + for (const TownGen::TownBuilding &b : res.buildings) { + if (b.stairs) { + ++withStairs; + if (b.stairsDrop <= 0.35f) { + printf(" *** stairs with drop %.2f " + "***\n", b.stairsDrop); + ok = false; + } + } + if (b.floorY < b.minGroundY - 0.001f) { + printf(" *** slope: floorY %.2f below " + "minGroundY %.2f ***\n", + b.floorY, b.minGroundY); + ok = false; + } + } + if (withStairs == 0 || withStairs == (int)res.buildings.size()) { + printf(" *** slope: %d/%zu buildings with stairs, " + "want some but not all ***\n", + withStairs, res.buildings.size()); + ok = false; + } + } + + /* Street addresses: buildings on a named street get sequential + * " N" addresses; the rest keep "Lot N". */ + { + RoadGraph graph; + buildTown(graph); + const RoadEdge &e0 = graph.edges[0]; + RoadNetworkGen::nameStreet( + graph, "Main Street", + { std::make_pair(e0.nodeA, e0.nodeB) }); + TownGen::TownResult res = + TownGen::generateTown(graph, cc, flat); + std::map seen; + int onMain = 0; + for (const TownGen::TownBuilding &b : res.buildings) { + if (b.address.rfind("Main Street ", 0) == 0) { + ++onMain; + if (++seen[b.address] > 1) { + printf(" *** duplicate address '%s' " + "***\n", b.address.c_str()); + ok = false; + } + } + } + if (onMain != 2) { + printf(" *** %d buildings on Main Street, want 2 " + "***\n", onMain); + ok = false; + } + } + + /* Village preset: single-storey, density leaves some lots empty + * but stays deterministic. */ + { + RoadGraph graph; + buildTown(graph); + TownGen::TownParams vill; + vill.district = TownGen::District::Village; + vill.seed = 3; + TownGen::TownResult res = + TownGen::generateTown(graph, vill, flat); + for (const TownGen::TownBuilding &b : res.buildings) + if (b.floors != 1) { + printf(" *** village floors %d, want 1 ***\n", + b.floors); + ok = false; + } + if (res.buildings.empty()) { + printf(" *** village placed no buildings ***\n"); + ok = false; + } + TownGen::TownResult res2 = + TownGen::generateTown(graph, vill, flat); + if (res2.buildings.size() != res.buildings.size()) { + printf(" *** village determinism: %zu vs %zu ***\n", + res2.buildings.size(), res.buildings.size()); + ok = false; + } + } + + printf(" %s\n", ok ? "passed" : "FAILURES above"); + return ok; +} + +/* Grid generator: determinism, counts, water/slope rejection, group + * connect/remove, two settlements in one graph. */ +static bool runGeneratorProbe() +{ + printf("grid generator probe\n"); + using namespace RoadNetworkGen; + bool ok = true; + + GridParams p; + p.origin = Vector3(0, 0, 0); + p.blocksX = 3; + p.blocksZ = 3; + p.blockSizeX = 50.0f; + p.blockSizeZ = 50.0f; + p.jitter = 3.0f; + p.seed = 42; + p.avoidWater = false; + + /* Determinism: same seed, identical positions. */ + RoadGraph g1, g2; + generateGrid(g1, p, HeightSampler(), nullptr); + generateGrid(g2, p, HeightSampler(), nullptr); + if (g1.nodes.size() != 16 || g2.nodes.size() != 16) { + printf(" *** node counts %zu/%zu, want 16 ***\n", + g1.nodes.size(), g2.nodes.size()); + ok = false; + } else { + for (size_t i = 0; i < g1.nodes.size(); ++i) + if (g1.nodes[i].position != g2.nodes[i].position) { + printf(" *** not deterministic at " + "node %zu ***\n", + i); + ok = false; + break; + } + } + std::string serr; + RoadLotFilter noFb; + noFb.edgeFallback = false; + if (g1.edges.size() != 24 || !checkSane(g1, &serr) || + !findCrossings(g1).empty() || findLots(g1, noFb).size() != 9) { + printf(" *** grid graph: edges=%zu sane=%s lots=%zu " + "crossings=%zu ***\n", + g1.edges.size(), serr.empty() ? "yes" : "NO", + findLots(g1, noFb).size(), findCrossings(g1).size()); + ok = false; + } + /* With fallback squares the twelve perimeter edges each grow one + * outward square (the unjittered boundary is exactly 50 m). */ + if (findLots(g1).size() != 21) { + printf(" *** grid fallback: %zu lots, want 21 ***\n", + findLots(g1).size()); + ok = false; + } + + /* Water rejection: x > 75 is water -> only the x = 0/50 columns + * survive (8 nodes; 6 vertical edges plus the 4 edges between the + * two surviving columns). */ + GridParams pw = p; + pw.jitter = 0.0f; + pw.avoidWater = true; + RoadGraph gw; + HeightSampler sampler = [](float x, float z) { + (void)z; + return x > 75.0f ? -5.0f : 0.0f; + }; + generateGrid(gw, pw, sampler, nullptr); + if (gw.nodes.size() != 8 || gw.edges.size() != 10) { + printf(" *** water: nodes=%zu edges=%zu, want 8/10 ***\n", + gw.nodes.size(), gw.edges.size()); + ok = false; + } + + /* Slope rejection: y = x rises 50 per 50 m block (slope 1), so + * all X-direction edges are skipped; Z-direction edges stay. */ + RoadGraph gs; + HeightSampler slope = [](float x, float z) { + (void)z; + return x; + }; + generateGrid(gs, pw, slope, nullptr); + if (gs.nodes.size() != 16 || gs.edges.size() != 12) { + printf(" *** slope: nodes=%zu edges=%zu, want 16/12 " + "***\n", + gs.nodes.size(), gs.edges.size()); + ok = false; + } + + /* Two settlements in one graph: independent groups; removing one + * leaves the other sane. */ + RoadGraph gt; + GridParams pa = p; + pa.jitter = 0.0f; + GridParams pb = p; + pb.jitter = 0.0f; + pb.origin = Vector3(1000, 0, 1000); + uint32_t ga = generateGrid(gt, pa, HeightSampler(), nullptr); + uint32_t gb = generateGrid(gt, pb, HeightSampler(), nullptr); + if (ga == 0 || gb == 0 || ga == gb || gt.nodes.size() != 32) { + printf(" *** settlements: groups %u/%u nodes=%zu ***\n", + ga, gb, gt.nodes.size()); + ok = false; + } else { + removeGroup(gt, gb); + std::string serr2; + if (gt.nodes.size() != 16 || !checkSane(gt, &serr2)) { + printf(" *** removeGroup: nodes=%zu sane=%s " + "***\n", + gt.nodes.size(), + serr2.empty() ? "yes" : "NO"); + ok = false; + } + } + + /* Connect a manual node to a group "as if it were a road node": + * the closest group node is picked automatically. */ + RoadGraph gc; + uint32_t grp = generateGrid(gc, pa, HeightSampler(), nullptr); + int manual = gc.addNode(Vector3(-30, 0, 10), 0.0f); + std::string cerr; + int ei = connectGroupToNode(gc, grp, manual, 0.0f, &cerr); + std::string serr3; + if (ei < 0 || !findCrossings(gc).empty() || + !checkSane(gc, &serr3)) { + printf(" *** connect: ei=%d crossings=%zu sane=%s (%s) " + "***\n", + ei, findCrossings(gc).size(), + serr3.empty() ? "yes" : "NO", cerr.c_str()); + ok = false; + } else { + /* The connection must land on the grid corner (0, 0). */ + const RoadEdge &e = gc.edges[(size_t)ei]; + int other = e.nodeA == manual ? e.nodeB : e.nodeA; + const RoadNode *n = gc.findNodeById(other); + if (!n || std::fabs(n->position.x) > 1.0f || + std::fabs(n->position.z) > 1.0f) { + printf(" *** connect: wrong group node ***\n"); + ok = false; + } + } + + printf(" %s\n", ok ? "passed" : "FAILURES above"); + return ok; +} + /* Lane-transition taper regression (demo-taxi exterior, node 31): * an edge with 2 lanes per direction (10 m half-width) meets a 1-lane * fallback edge (5 m) at a near-straight node (turn ~0.95 deg, so the @@ -3033,6 +4047,810 @@ static bool runApronSkirt() return ok; } +/* ---------------------------------------------------------------- + * Phase 2.5 probes: road markings, atlas UVs, decals + * ---------------------------------------------------------------- */ + +/* XZ coverage of the paint overlay strips: marking/decal quads are + * horizontal but wind downward (the overlay materials render + * double-sided), so accept both facings. */ +static bool paintCovers(const Procedural::TriangleBuffer &buf, float x, + float z) +{ + const auto &verts = buf.getVertices(); + const auto &indices = buf.getIndices(); + for (size_t i = 0; i + 2 < indices.size(); i += 3) { + const Vector3 &a = verts[(size_t)indices[i]].mPosition; + const Vector3 &b = verts[(size_t)indices[i + 1]].mPosition; + const Vector3 &c = verts[(size_t)indices[i + 2]].mPosition; + Vector3 n = (b - a).crossProduct(c - a); + float l = n.length(); + if (l < 1e-8f || std::fabs(n.y) / l < 0.5f) + continue; + float s0 = (b.x - a.x) * (z - a.z) - (b.z - a.z) * (x - a.x); + float s1 = (c.x - b.x) * (z - b.z) - (c.z - b.z) * (x - b.x); + float s2 = (a.x - c.x) * (z - c.z) - (a.z - c.z) * (x - c.x); + if ((s0 >= -1e-3f && s1 >= -1e-3f && s2 >= -1e-3f) || + (s0 <= 1e-3f && s1 <= 1e-3f && s2 <= 1e-3f)) + return true; + } + return false; +} + +static size_t paintTris(const Procedural::TriangleBuffer &buf) +{ + return buf.getIndices().size() / 3; +} + +/* Marking content on dead-end straight segments: dashed centerline + * for 1+1, double solid for wider, N-1 dashed lane lines per side, + * solid edge lines at both curbs, no stop lines. The per-edge + * tri-state gating is honored both ways. */ +static bool runMarkingSegmentProbe() +{ + printf("marking segment probe (dead-end content + gating)\n"); + bool ok = true; + + auto makeGraph = [](int lanes, RoadMarkingMode mode, bool global, + int &idA, int &idB) { + RoadGraph graph; + graph.config.laneWidth = 5.0f; + graph.config.lanesPerDirection = 1; + graph.config.roadThickness = 0.3f; + graph.config.sidewalkEnabled = false; + graph.config.markingsEnabled = global; + idA = graph.addNode(Vector3(0, 0, 0), 0.0f); + idB = graph.addNode(Vector3(40, 0, 0), 0.0f); + int e = graph.addEdge(idA, idB); + graph.edges[(size_t)e].lanesAtoB = lanes; + graph.edges[(size_t)e].lanesBtoA = lanes; + graph.edges[(size_t)e].markingsMode = mode; + return graph; + }; + auto buildAt = [](RoadGraph &graph, int nodeId, + RoadGeometryLib::RoadMarkingBuffers &mb) { + std::vector wedges; + std::vector segs; + enumerateWedges(graph, wedges, segs); + bool any = false; + for (const auto &s : segs) { + if (s.nodeId != nodeId) + continue; + any |= RoadGeometryLib::buildSegmentMarkings(s, graph, + mb); + } + return any; + }; + auto checkPaintHeight = [](const Procedural::TriangleBuffer &buf, + float want) { + for (const auto &v : buf.getVertices()) + if (std::fabs(v.mPosition.y - want) > 1e-3f) + return false; + return true; + }; + + int idA, idB; + { + RoadGraph g = makeGraph(1, RoadMarkingMode::Inherit, true, + idA, idB); + RoadGeometryLib::RoadMarkingBuffers mb; + if (!buildAt(g, idA, mb)) { + printf(" *** 1+1: segment markings missing ***\n"); + ok = false; + } + /* 1+1: dashed single centerline, no lane lines, edge + * lines at both curbs, no stop lines. */ + if (paintTris(mb.stopLines) != 0) { + printf(" *** 1+1: stop lines on a dead end ***\n"); + ok = false; + } + if (paintTris(mb.laneLines) != 0) { + printf(" *** 1+1: unexpected lane lines ***\n"); + ok = false; + } + if (paintTris(mb.centerline) == 0 || + !paintCovers(mb.centerline, 1.0f, 0.0f) || + paintCovers(mb.centerline, 4.5f, 0.0f)) { + printf(" *** 1+1: dashed centerline wrong " + "(dash/gap) ***\n"); + ok = false; + } + if (paintTris(mb.edgeLines) == 0 || + !paintCovers(mb.edgeLines, 1.0f, 4.675f) || + !paintCovers(mb.edgeLines, 1.0f, -4.675f)) { + printf(" *** 1+1: edge lines missing at a " + "curb ***\n"); + ok = false; + } + if (!checkPaintHeight(mb.centerline, 0.17f) || + !checkPaintHeight(mb.edgeLines, 0.17f)) { + printf(" *** 1+1: paint height != road top + " + "lift ***\n"); + ok = false; + } + RoadGeometryLib::RoadMarkingBuffers mbB; + if (!buildAt(g, idB, mbB) || + paintTris(mbB.centerline) == 0) { + printf(" *** 1+1: far segment markings " + "missing ***\n"); + ok = false; + } + } + { + RoadGraph g = makeGraph(2, RoadMarkingMode::Inherit, true, + idA, idB); + RoadGeometryLib::RoadMarkingBuffers mb; + if (!buildAt(g, idA, mb)) { + printf(" *** 2+2: segment markings missing ***\n"); + ok = false; + } + /* 2+2: double solid centerline (bare gap at lat 0), one + * dashed lane line per side at lat +/-5, edge lines at + * +/-9.675. */ + if (!paintCovers(mb.centerline, 1.0f, 0.15f) || + !paintCovers(mb.centerline, 4.5f, 0.15f) || + paintCovers(mb.centerline, 1.0f, 0.0f)) { + printf(" *** 2+2: double solid centerline " + "wrong ***\n"); + ok = false; + } + if (!paintCovers(mb.laneLines, 1.0f, 5.0f) || + paintCovers(mb.laneLines, 4.5f, 5.0f) || + !paintCovers(mb.laneLines, 1.0f, -5.0f)) { + printf(" *** 2+2: dashed lane lines wrong ***\n"); + ok = false; + } + if (!paintCovers(mb.edgeLines, 1.0f, 9.675f) || + !paintCovers(mb.edgeLines, 1.0f, -9.675f)) { + printf(" *** 2+2: edge lines missing ***\n"); + ok = false; + } + } + { + RoadGraph g = makeGraph(4, RoadMarkingMode::Inherit, true, + idA, idB); + RoadGeometryLib::RoadMarkingBuffers mb; + if (!buildAt(g, idA, mb)) { + printf(" *** 4+4: segment markings missing ***\n"); + ok = false; + } + /* 4+4: three dashed lane lines per side. */ + const float lats[6] = { 5.0f, 10.0f, 15.0f, + -5.0f, -10.0f, -15.0f }; + for (float lat : lats) + if (!paintCovers(mb.laneLines, 1.0f, lat)) { + printf(" *** 4+4: lane line at lat %.1f " + "missing ***\n", lat); + ok = false; + } + if (paintCovers(mb.laneLines, 1.0f, 18.0f)) { + printf(" *** 4+4: unexpected 4th lane line ***\n"); + ok = false; + } + if (!paintCovers(mb.edgeLines, 1.0f, 19.675f)) { + printf(" *** 4+4: edge line missing ***\n"); + ok = false; + } + } + { + RoadGraph g = makeGraph(1, RoadMarkingMode::Off, true, + idA, idB); + RoadGeometryLib::RoadMarkingBuffers mb; + if (buildAt(g, idA, mb) || paintTris(mb.centerline) || + paintTris(mb.edgeLines)) { + printf(" *** per-edge Off ignored ***\n"); + ok = false; + } + } + { + RoadGraph g = makeGraph(1, RoadMarkingMode::On, false, + idA, idB); + RoadGeometryLib::RoadMarkingBuffers mb; + if (!buildAt(g, idA, mb) || + !paintCovers(mb.centerline, 1.0f, 0.0f)) { + printf(" *** per-edge On with global off " + "ignored ***\n"); + ok = false; + } + } + if (!ok) + printf(" *** marking segment probe FAILED ***\n"); + return ok; +} + +/* Junction markings: a stop bar on the incoming half of every arm, no + * paint inside the junction clear zone, longitudinal lines present + * past the clear zone, and no self-overlap in the combined overlay + * buffer (the stop bar must not z-fight the lines it crosses). */ +static bool runJunctionMarkingCase(const char *name, + const std::vector &neighbors, + int lanesPerDirection) +{ + printf("junction markings %s (%zu arms, %d+%d lanes)\n", name, + neighbors.size(), lanesPerDirection, lanesPerDirection); + + RoadGraph graph; + graph.config.laneWidth = 5.0f; + graph.config.lanesPerDirection = lanesPerDirection; + graph.config.roadThickness = 0.3f; + graph.config.sidewalkEnabled = false; + int idO = graph.addNode(Vector3(0, 0, 0), 0.0f); + std::vector dirs; + for (const Vector3 &p : neighbors) { + int idN = graph.addNode(p, 0.0f); + graph.addEdge(idO, idN); + Vector3 d = p; + d.y = 0.0f; + d.normalise(); + dirs.push_back(d); + } + + std::vector wedges; + std::vector segs; + enumerateWedges(graph, wedges, segs); + + bool ok = true; + float lw = graph.config.laneWidth; + float halfW = (float)lanesPerDirection * lw; /* per-side width */ + float setback = graph.config.markingStopSetback; + float sw = graph.config.markingStopWidth; + + RoadGeometryLib::RoadMarkingBuffers all; + std::vector armClear(neighbors.size(), 0.0f); + float minClear = FLT_MAX, maxClear = 0.0f; + int wedgeCount = 0; + for (const auto &w : wedges) { + if (w.nodeId != idO || w.degenerate) + continue; + ++wedgeCount; + RoadGeometryLib::WedgeFillet f = + RoadGeometryLib::wedgeFillet(w, graph); + float c0 = (f.active ? f.f1 : halfW) + setback; + float c1 = (f.active ? f.f2 : halfW) + setback; + minClear = std::min(minClear, std::min(c0, c1)); + maxClear = std::max(maxClear, std::max(c0, c1)); + for (size_t i = 0; i < dirs.size(); ++i) + if (dirs[i].dotProduct(w.second.direction) > 0.999f) + armClear[i] = c1; + RoadGeometryLib::RoadMarkingBuffers mb; + RoadGeometryLib::buildWedgeMarkings(w, graph, mb); + all.centerline.append(mb.centerline); + all.laneLines.append(mb.laneLines); + all.edgeLines.append(mb.edgeLines); + all.stopLines.append(mb.stopLines); + } + if (wedgeCount != (int)neighbors.size()) { + printf(" *** expected %zu wedges, got %d ***\n", + neighbors.size(), wedgeCount); + ok = false; + } + + /* One stop bar per arm (one quad = 2 tris each). */ + size_t stopTris = paintTris(all.stopLines); + if (stopTris != 2 * neighbors.size()) { + printf(" *** expected %zu stop-line tris, got %zu ***\n", + 2 * neighbors.size(), stopTris); + ok = false; + } + + if (minClear < 4.5f) { + printf(" *** clear zone %.2f too small for the probe " + "***\n", minClear); + ok = false; + } + + float halfArm = neighbors[0].length() * 0.5f; + for (size_t i = 0; i < dirs.size(); ++i) { + Vector3 perp = RoadGeometryLib::roadRightVec(dirs[i]); + + /* (a) No paint inside the clear zone. */ + const float dists[2] = { 2.0f, 4.0f }; + const float lats[3] = { -halfW + 0.5f, 0.0f, halfW - 0.5f }; + for (float dist : dists) + for (float lat : lats) { + Vector3 p = dirs[i] * dist + perp * lat; + if (paintCovers(all.centerline, p.x, p.z) || + paintCovers(all.laneLines, p.x, p.z) || + paintCovers(all.edgeLines, p.x, p.z) || + paintCovers(all.stopLines, p.x, p.z)) { + printf(" *** arm %zu: paint inside " + "the clear zone at (%.2f, " + "%.2f) ***\n", i, p.x, p.z); + ok = false; + } + } + + /* (b) Lines resume past the clear zone. */ + float s = (maxClear + halfArm) * 0.5f; + Vector3 p = dirs[i] * s; + float edgeLat = halfW - 0.325f; + Vector3 pe1 = p + perp * edgeLat; + Vector3 pe2 = p - perp * edgeLat; + if (!paintCovers(all.edgeLines, pe1.x, pe1.z) || + !paintCovers(all.edgeLines, pe2.x, pe2.z)) { + printf(" *** arm %zu: edge lines missing past " + "the clear zone ***\n", i); + ok = false; + } + auto scanCovers = [&](const Procedural::TriangleBuffer &buf, + float lat) { + for (float q = maxClear + 0.3f; + q < halfArm - 1e-3f; q += 0.2f) { + Vector3 pq = dirs[i] * q + perp * lat; + if (paintCovers(buf, pq.x, pq.z)) + return true; + } + return false; + }; + /* Centerline past the clear zone. A dashed 1+1 + * centerline sits at lat 0; the double solid strip of a + * multi-lane road leaves lat 0 as the gap between the + * two solid lines, so scan inside one strip. */ + float scanLat = lanesPerDirection >= 2 ? + graph.config.markingLineWidth : + 0.0f; + if (!scanCovers(all.centerline, scanLat)) { + printf(" *** arm %zu: centerline missing past " + "the clear zone ***\n", i); + ok = false; + } + if (lanesPerDirection >= 2) { + /* Double solid centerline: continuous at s. */ + Vector3 pc = p + perp * 0.15f; + if (!paintCovers(all.centerline, pc.x, pc.z)) { + printf(" *** arm %zu: double centerline " + "not solid ***\n", i); + ok = false; + } + for (int k = 1; k < lanesPerDirection; ++k) { + if (!scanCovers(all.laneLines, k * lw) || + !scanCovers(all.laneLines, -k * lw)) { + printf(" *** arm %zu: lane line " + "%d missing ***\n", i, k); + ok = false; + } + } + } + + /* (c) A stop bar crosses the incoming half of this + * arm. */ + bool stop = false; + float c1 = armClear[i]; + for (float q = c1; q <= c1 + sw + 1e-4f && !stop; + q += 0.1f) + for (float lat = -halfW; lat <= halfW + 1e-4f; + lat += 0.2f) { + Vector3 pq = dirs[i] * q + perp * lat; + if (paintCovers(all.stopLines, pq.x, + pq.z)) { + stop = true; + break; + } + } + if (!stop) { + printf(" *** arm %zu: stop line missing ***\n", + i); + ok = false; + } + } + + /* Combined overlay: no self-overlap (catches the stop bar + * z-fighting the lines it crosses). */ + Procedural::TriangleBuffer comb; + comb.append(all.centerline); + comb.append(all.laneLines); + comb.append(all.edgeLines); + comb.append(all.stopLines); + ok &= checkPiece(paintTris(comb) > 0, "junction markings combined", + comb, nullptr); + + /* Road slab + overlay: paint must not z-fight the road top. */ + Procedural::TriangleBuffer combRoad = comb; + for (const auto &w : wedges) { + if (w.nodeId != idO || w.degenerate) + continue; + RoadGeometryLib::buildWedgeGeometry(w, graph, combRoad); + } + ok &= checkPiece(true, "junction road+markings", combRoad, nullptr); + if (!ok) + printf(" *** junction markings %s FAILED ***\n", name); + return ok; +} + +/* 2-way continuation: markings follow the corner through a degree-2 + * node (no clear zone) and no stop lines are emitted. A nearly + * straight node keeps the fillet inactive so the paint passes right + * through the node point. */ +static bool runMarkingTwoWayProbe() +{ + printf("marking 2-way continuation probe\n"); + + RoadGraph graph; + graph.config.laneWidth = 5.0f; + graph.config.lanesPerDirection = 1; + graph.config.roadThickness = 0.3f; + graph.config.sidewalkEnabled = false; + int idO = graph.addNode(Vector3(0, 0, 0), 0.0f); + int idA = graph.addNode(Vector3(-60, 0, 0.5f), 0.0f); + int idB = graph.addNode(Vector3(60, 0, 0.5f), 0.0f); + graph.addEdge(idO, idA); + graph.addEdge(idO, idB); + + std::vector wedges; + std::vector segs; + enumerateWedges(graph, wedges, segs); + + bool ok = true; + RoadGeometryLib::RoadMarkingBuffers all; + int wedgeCount = 0; + for (const auto &w : wedges) { + if (w.nodeId != idO || w.degenerate) + continue; + ++wedgeCount; + RoadGeometryLib::RoadMarkingBuffers mb; + RoadGeometryLib::buildWedgeMarkings(w, graph, mb); + all.centerline.append(mb.centerline); + all.laneLines.append(mb.laneLines); + all.edgeLines.append(mb.edgeLines); + all.stopLines.append(mb.stopLines); + } + if (wedgeCount != 2) { + printf(" *** expected 2 wedges, got %d ***\n", wedgeCount); + ok = false; + } + if (paintTris(all.stopLines) != 0) { + printf(" *** stop lines at a 2-way node ***\n"); + ok = false; + } + + /* The centerline runs through the node with no junction clear + * zone. Dashes phase from the edge midpoint (wedge path d = 0), + * so up to one dash gap next to the node may legitimately be + * unpainted — but a junction-style clear cut (half-width + + * setback) would leave much more. Find the first painted point + * along each arm and bound that gap. */ + Vector3 dirA(-60, 0, 0.5f); + dirA.normalise(); + Vector3 dirB(60, 0, 0.5f); + dirB.normalise(); + auto firstCover = [&](const Vector3 &dir) { + for (float dd = 0.0f; dd <= 8.0f; dd += 0.2f) { + Vector3 p = dir * dd; + if (paintCovers(all.centerline, p.x, p.z)) + return dd; + } + return FLT_MAX; + }; + float maxGap = graph.config.markingDashGap + 0.6f; + float gapA = firstCover(dirA); + float gapB = firstCover(dirB); + if (gapA > maxGap || gapB > maxGap) { + printf(" *** centerline cut back %.2f/%.2f m at the 2-way " + "node (clear zone?) ***\n", gapA, gapB); + ok = false; + } + /* Edge lines on both curbs, both sides of the node. */ + Vector3 pA = dirA * 0.5f; + Vector3 pB = dirB * 0.5f; + Vector3 perpA = RoadGeometryLib::roadRightVec(dirA); + const float sides[2] = { -4.675f, 4.675f }; + for (float side : sides) { + Vector3 qA = pA + perpA * side; + Vector3 qB = pB + perpA * side; + if (!paintCovers(all.edgeLines, qA.x, qA.z) || + !paintCovers(all.edgeLines, qB.x, qB.z)) { + printf(" *** edge line broken at the 2-way node " + "(side %.2f) ***\n", side); + ok = false; + } + } + + Procedural::TriangleBuffer comb; + comb.append(all.centerline); + comb.append(all.laneLines); + comb.append(all.edgeLines); + comb.append(all.stopLines); + ok &= checkPiece(paintTris(comb) > 0, "2-way markings combined", + comb, nullptr); + if (!ok) + printf(" *** marking 2-way probe FAILED ***\n"); + return ok; +} + +/* Decal quads: placement from edgeT + lateral offset, the piecewise + * surface height with the midpoint kink, atlas rect UVs and the + * per-node ownership split (a cross-page edge paints each half's + * decals on one page only). */ +static bool runDecalProbe() +{ + printf("decal probe (placement, UVs, ownership)\n"); + + RoadGraph graph; + graph.config.laneWidth = 5.0f; + graph.config.lanesPerDirection = 1; + graph.config.roadThickness = 0.3f; + graph.config.sidewalkEnabled = false; + int idA = graph.addNode(Vector3(0, 0, 0), 0.0f); + int idB = graph.addNode(Vector3(40, 4, 0), 0.0f); + int e = graph.addEdge(idA, idB); + + RoadEdgeDecal d1; + d1.edgeT = 0.25f; + d1.lateralOffset = 1.0f; + d1.size = 2.0f; + d1.uvX = 0.1f; + d1.uvY = 0.2f; + d1.uvW = 0.3f; + d1.uvH = 0.4f; + RoadEdgeDecal d2; + d2.edgeT = 0.75f; + d2.lateralOffset = -1.5f; + d2.size = 1.0f; + d2.uvX = 0.5f; + d2.uvY = 0.5f; + d2.uvW = 0.25f; + d2.uvH = 0.25f; + graph.edges[(size_t)e].decals.push_back(d1); + graph.edges[(size_t)e].decals.push_back(d2); + + bool ok = true; + Procedural::TriangleBuffer buf; + RoadGeometryLib::buildEdgeDecals(graph, (size_t)e, buf, -1); + if (paintTris(buf) != 4) { + printf(" *** expected 4 decal tris, got %zu ***\n", + paintTris(buf)); + ok = false; + } + + /* Expected corners: position (sloped edge, midpoint kink at + * yMid=2, decal lift 0.025 over the road top) paired with its + * atlas UV. */ + const Vector3 wantP[8] = { + Vector3(9.0f, 1.175f, 0.0f), Vector3(9.0f, 1.175f, 2.0f), + Vector3(11.0f, 1.175f, 0.0f), Vector3(11.0f, 1.175f, 2.0f), + Vector3(29.5f, 3.175f, -2.0f), Vector3(29.5f, 3.175f, -1.0f), + Vector3(30.5f, 3.175f, -2.0f), Vector3(30.5f, 3.175f, -1.0f) + }; + const Vector2 wantUV[8] = { + Vector2(0.1f, 0.2f), Vector2(0.1f, 0.6f), + Vector2(0.4f, 0.2f), Vector2(0.4f, 0.6f), + Vector2(0.5f, 0.5f), Vector2(0.5f, 0.75f), + Vector2(0.75f, 0.5f), Vector2(0.75f, 0.75f) + }; + for (int k = 0; k < 8; ++k) { + bool found = false; + for (const auto &v : buf.getVertices()) { + if (v.mPosition.distance(wantP[k]) < 1e-3f && + v.mUV.distance(wantUV[k]) < 1e-4f) { + found = true; + break; + } + } + if (!found) { + printf(" *** decal corner %d at " + "(%.2f,%.3f,%.2f) uv (%.2f,%.2f) missing " + "***\n", k, wantP[k].x, wantP[k].y, + wantP[k].z, wantUV[k].x, wantUV[k].y); + ok = false; + } + } + + /* Ownership split: decal 1 (edgeT 0.25) belongs to nodeA, + * decal 2 (0.75) to nodeB. */ + Procedural::TriangleBuffer bufA, bufB; + RoadGeometryLib::buildEdgeDecals(graph, (size_t)e, bufA, idA); + RoadGeometryLib::buildEdgeDecals(graph, (size_t)e, bufB, idB); + if (paintTris(bufA) != 2 || paintTris(bufB) != 2) { + printf(" *** ownership split: %zu/%zu tris (want 2/2) " + "***\n", paintTris(bufA), paintTris(bufB)); + ok = false; + } + ok &= checkPiece(paintTris(buf) > 0, "decals", buf, nullptr); + if (!ok) + printf(" *** decal probe FAILED ***\n"); + return ok; +} + +/* Atlas UVs: U selects the band (road top 0-0.5, sidewalk 0.5-0.75, + * apron 0.75-1.0) and V tiles once per textureTileLength meters, + * phase-continuous across the mid-edge seam. textureTileLength 0 + * keeps the legacy meter UVs. */ +static bool runUVAtlasProbe() +{ + printf("UV atlas probe (bands, V tiling, legacy)\n"); + bool ok = true; + + auto makeGraph = [](float tile, int &idA, int &idB) { + RoadGraph graph; + graph.config.laneWidth = 5.0f; + graph.config.lanesPerDirection = 1; + graph.config.roadThickness = 0.3f; + graph.config.sidewalkEnabled = true; + graph.config.sidewalkWidth = 1.5f; + graph.config.sidewalkHeight = 0.15f; + graph.config.sidewalkThickness = 0.3f; + graph.config.apronEnabled = true; + graph.config.apronWidth = 2.0f; + graph.config.apronDrop = 1.0f; + graph.config.textureTileLength = tile; + idA = graph.addNode(Vector3(0, 0, 0), 0.0f); + idB = graph.addNode(Vector3(40, 0, 0), 0.0f); + graph.addEdge(idA, idB); + return graph; + }; + auto uRange = [](const Procedural::TriangleBuffer &buf, float &mn, + float &mx) { + mn = FLT_MAX; + mx = -FLT_MAX; + for (const auto &v : buf.getVertices()) { + mn = std::min(mn, v.mUV.x); + mx = std::max(mx, v.mUV.x); + } + }; + /* V range over the top face only (skirt bottoms shift V). */ + auto vRangeTop = [](const Procedural::TriangleBuffer &buf, float &mn, + float &mx) { + mn = FLT_MAX; + mx = -FLT_MAX; + for (const auto &v : buf.getVertices()) { + if (v.mNormal.y < 0.9f) + continue; + mn = std::min(mn, v.mUV.y); + mx = std::max(mx, v.mUV.y); + } + }; + + int idA, idB; + RoadGraph g = makeGraph(4.0f, idA, idB); + std::vector wedges; + std::vector segs; + enumerateWedges(g, wedges, segs); + + float vA0 = 0.0f, vA1 = 0.0f, vB0 = 0.0f, vB1 = 0.0f; + bool gotA = false, gotB = false; + for (const auto &s : segs) { + Procedural::TriangleBuffer road, sw, ap; + RoadGeometryLib::buildSegmentGeometry(s, g, road); + RoadGeometryLib::buildSegmentSidewalkGeometry(s, g, sw); + RoadGeometryLib::buildSegmentApronGeometry(s, g, ap); + float u0, u1; + uRange(road, u0, u1); + if (u0 < -1e-3f || u1 > 0.5f + 1e-3f) { + printf(" *** road U band [%.3f, %.3f] outside " + "[0, 0.5] ***\n", u0, u1); + ok = false; + } + uRange(sw, u0, u1); + if (u0 < 0.5f - 1e-3f || u1 > 0.75f + 1e-3f) { + printf(" *** sidewalk U band [%.3f, %.3f] " + "outside [0.5, 0.75] ***\n", u0, u1); + ok = false; + } + uRange(ap, u0, u1); + if (u0 < 0.75f - 1e-3f || u1 > 1.0f + 1e-3f) { + printf(" *** apron U band [%.3f, %.3f] outside " + "[0.75, 1] ***\n", u0, u1); + ok = false; + } + float v0, v1; + vRangeTop(road, v0, v1); + if (s.nodeId == idA) { + vA0 = v0; + vA1 = v1; + gotA = true; + } else { + vB0 = v0; + vB1 = v1; + gotB = true; + } + } + if (!gotA || !gotB) { + printf(" *** segments missing ***\n"); + ok = false; + } + /* 20 m half-edge + 0.05 seam at 4 m/tile: span 5.0125; the two + * halves meet at V=5 at the edge midpoint. */ + if (std::fabs(vA1 - vA0 - 5.0125f) > 0.02f) { + printf(" *** segment A V span %.4f (want 5.0125) ***\n", + vA1 - vA0); + ok = false; + } + if (std::fabs(vB1 - vB0 - 5.0125f) > 0.02f || + std::fabs(vB0 - vA1) > 0.02f) { + printf(" *** segment B V [%.4f, %.4f] breaks phase " + "continuity with A [%.4f, %.4f] ***\n", vB0, vB1, + vA0, vA1); + ok = false; + } + + /* Wedge pieces of a 2-way straight node: the road stays in the + * asphalt band (each wedge maps its centerline-to-curb half to + * [0.25, 0.5] with symmetric 1+1 lanes), sidewalk/apron in + * their bands, V tiles along the arms. */ + RoadGraph gw; + gw.config.laneWidth = 5.0f; + gw.config.lanesPerDirection = 1; + gw.config.roadThickness = 0.3f; + gw.config.sidewalkEnabled = true; + gw.config.sidewalkWidth = 1.5f; + gw.config.sidewalkHeight = 0.15f; + gw.config.sidewalkThickness = 0.3f; + gw.config.apronEnabled = true; + gw.config.apronWidth = 2.0f; + gw.config.apronDrop = 1.0f; + gw.config.textureTileLength = 4.0f; + int idO = gw.addNode(Vector3(0, 0, 0), 0.0f); + int idW1 = gw.addNode(Vector3(-40, 0, 0), 0.0f); + int idW2 = gw.addNode(Vector3(40, 0, 0), 0.0f); + gw.addEdge(idO, idW1); + gw.addEdge(idO, idW2); + std::vector ww; + std::vector ws; + enumerateWedges(gw, ww, ws); + Procedural::TriangleBuffer wroad, wsw, wap; + for (const auto &w : ww) { + if (w.nodeId != idO || w.degenerate) + continue; + RoadGeometryLib::buildWedgeGeometry(w, gw, wroad); + RoadGeometryLib::buildSidewalkGeometry(w, gw, wsw); + RoadGeometryLib::buildWedgeApronGeometry(w, gw, wap); + } + float wu0, wu1; + uRange(wroad, wu0, wu1); + if (wu0 < 0.25f - 1e-3f || wu1 > 0.5f + 1e-3f) { + printf(" *** wedge road U [%.3f, %.3f] outside " + "[0.25, 0.5] ***\n", wu0, wu1); + ok = false; + } + uRange(wsw, wu0, wu1); + if (wu0 < 0.5f - 1e-3f || wu1 > 0.75f + 1e-3f) { + printf(" *** wedge sidewalk U [%.3f, %.3f] outside " + "band ***\n", wu0, wu1); + ok = false; + } + uRange(wap, wu0, wu1); + if (wu0 < 0.75f - 1e-3f || wu1 > 1.0f + 1e-3f) { + printf(" *** wedge apron U [%.3f, %.3f] outside band " + "***\n", wu0, wu1); + ok = false; + } + float wv0, wv1; + vRangeTop(wroad, wv0, wv1); + if (wv1 - wv0 < 4.9f) { + printf(" *** wedge road V span %.3f does not tile " + "***\n", wv1 - wv0); + ok = false; + } + + /* Legacy meter UVs with textureTileLength == 0. */ + RoadGraph gl = makeGraph(0.0f, idA, idB); + std::vector lwg; + std::vector lsg; + enumerateWedges(gl, lwg, lsg); + for (const auto &s : lsg) { + if (s.nodeId != idA) + continue; + Procedural::TriangleBuffer road; + RoadGeometryLib::buildSegmentGeometry(s, gl, road); + float u0, u1, v0, v1; + uRange(road, u0, u1); + vRangeTop(road, v0, v1); + if (u1 < 19.9f) { + printf(" *** legacy road U max %.3f not meter " + "based ***\n", u1); + ok = false; + } + if (v1 < 9.9f) { + printf(" *** legacy road V max %.3f not meter " + "based ***\n", v1); + ok = false; + } + } + if (!ok) + printf(" *** UV atlas probe FAILED ***\n"); + return ok; +} + int main(int argc, char **argv) { bool ok = true; @@ -3040,6 +4858,14 @@ int main(int argc, char **argv) if (argc == 3 && strcmp(argv[1], "--region") == 0) return runRegionProbe(argv[2]) ? 0 : 1; + if (argc == 3 && strcmp(argv[1], "--region-markings") == 0) + return runRegionMarkingsProbe(argv[2]) ? 0 : 1; + + if (argc == 4 && strcmp(argv[1], "--region-markings") == 0) + return runRegionMarkingsProbe(argv[2], atof(argv[3])) ? + 0 : + 1; + if (argc == 6 && strcmp(argv[1], "--region") == 0) return runRegionProbe(argv[2], atof(argv[3]), atof(argv[4]), atof(argv[5])) ? @@ -3427,8 +5253,34 @@ int main(int argc, char **argv) Vector3(12, 4, 0)); ok &= runPinZonePrecision(); + ok &= runPinZoneCacheProbe(); + ok &= runCrossingResolutionProbe(); + ok &= runLotProbe(); + ok &= runStreetProbe(); + ok &= runGeneratorProbe(); + ok &= runTownGenProbe(); ok &= runLaneTransitionTaper(); ok &= runApronSkirt(); + + ok &= runMarkingSegmentProbe(); + ok &= runJunctionMarkingCase( + "4-way 1+1", + { Vector3(60, 0, 0), Vector3(0, 0, 60), + Vector3(-60, 0, 0), Vector3(0, 0, -60) }, + 1); + ok &= runJunctionMarkingCase( + "4-way 2+2", + { Vector3(60, 0, 0), Vector3(0, 0, 60), + Vector3(-60, 0, 0), Vector3(0, 0, -60) }, + 2); + ok &= runJunctionMarkingCase( + "3-way T 1+1", + { Vector3(60, 0, 0), Vector3(-60, 0, 0), + Vector3(0, 0, 60) }, + 1); + ok &= runMarkingTwoWayProbe(); + ok &= runDecalProbe(); + ok &= runUVAtlasProbe(); } else { fprintf(stderr, "usage: %s [Ax Ay Az Bx By Bz Cx Cy Cz]\n" diff --git a/src/features/editScene/ui/TerrainEditor.hpp b/src/features/editScene/ui/TerrainEditor.hpp index 0417d35..73ef266 100644 --- a/src/features/editScene/ui/TerrainEditor.hpp +++ b/src/features/editScene/ui/TerrainEditor.hpp @@ -8,6 +8,7 @@ #include "../components/EntityName.hpp" #include "../systems/TerrainSystem.hpp" #include "../systems/RoadSystem.hpp" +#include "../systems/TownGenSystem.hpp" #include "../systems/TerrainPrefabSpawnerSystem.hpp" #include "../systems/PrefabSystem.hpp" #include "NavigationPanel.hpp" @@ -21,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -807,10 +809,278 @@ private: "(aprons start at the sidewalk's " "outer top edge)"); } + + ImGui::SeparatorText("Markings"); + cfgChanged |= ImGui::Checkbox( + "Markings Enabled", &rg.config.markingsEnabled); + if (rg.config.markingsEnabled) { + cfgChanged |= ImGui::SliderFloat( + "Line Width", + &rg.config.markingLineWidth, 0.05f, + 0.5f, "%.2f"); + cfgChanged |= ImGui::SliderFloat( + "Dash Length", + &rg.config.markingDashLength, 0.5f, + 10.0f, "%.1f"); + cfgChanged |= ImGui::SliderFloat( + "Dash Gap", &rg.config.markingDashGap, + 0.5f, 10.0f, "%.1f"); + cfgChanged |= ImGui::SliderFloat( + "Stop Line Width", + &rg.config.markingStopWidth, 0.1f, + 1.0f, "%.2f"); + cfgChanged |= ImGui::SliderFloat( + "Stop Setback", + &rg.config.markingStopSetback, 0.0f, + 5.0f, "%.1f"); + cfgChanged |= ImGui::SliderFloat( + "Edge Line Inset", + &rg.config.markingEdgeInset, 0.0f, + 1.0f, "%.2f"); + } + + ImGui::SeparatorText("Road Texture"); + cfgChanged |= ImGui::SliderFloat( + "Tile Length (0 = off)", + &rg.config.textureTileLength, 0.0f, 50.0f, + "%.1f m"); + if (rg.config.textureTileLength > 0.0f) + ImGui::TextDisabled( + "(atlas UVs: asphalt/sidewalk/apron " + "bands, V tiles along the road)"); if (cfgChanged) rg.bumpVersion(); ImGui::TreePop(); } + + /* Phase 3: procedural settlement generator, crossing + * resolution and generated-group management (RoadNetworkGen). */ + if (ImGui::TreeNode("Road Network Generator")) { + static int s_blocksX = 3, s_blocksZ = 3; + static float s_blockSizeX = 100.0f, s_blockSizeZ = 100.0f; + static float s_jitter = 0.0f; + static int s_seed = 1; + static float s_origin[2] = { 0.0f, 0.0f }; + static bool s_avoidWater = true; + static float s_maxSlope = 0.35f; + static bool s_resolve = true; + static bool s_pickArmed = false; + static bool s_preview = false; + static std::string s_genResult; + + bool paramsChanged = + ImGui::InputFloat2("Origin XZ", s_origin); + if (!s_pickArmed) { + if (ImGui::Button("Pick Origin on Terrain")) { + /* The pick click is handled by road + * edit mode. */ + if (ts) + ts->setRoadEditMode(true); + rs->armOriginPick(true); + s_pickArmed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Arm a terrain click that sets " + "the generator origin XZ (uses " + "road edit mode)."); + } else { + ImGui::TextDisabled( + "Click the terrain to set the " + "origin..."); + ImGui::SameLine(); + if (ImGui::SmallButton("Cancel")) { + rs->armOriginPick(false); + s_pickArmed = false; + } + } + Ogre::Vector3 pickedOrigin; + if (rs->takePickedOrigin(pickedOrigin)) { + s_origin[0] = pickedOrigin.x; + s_origin[1] = pickedOrigin.z; + s_pickArmed = false; + paramsChanged = true; + } + paramsChanged |= ImGui::SliderInt("Blocks X", &s_blocksX, 1, + 20); + paramsChanged |= ImGui::SliderInt("Blocks Z", &s_blocksZ, 1, + 20); + paramsChanged |= ImGui::SliderFloat("Block Size X", + &s_blockSizeX, + 30.0f, 500.0f, + "%.0f m"); + paramsChanged |= ImGui::SliderFloat("Block Size Z", + &s_blockSizeZ, + 30.0f, 500.0f, + "%.0f m"); + paramsChanged |= ImGui::SliderFloat("Jitter", &s_jitter, + 0.0f, 20.0f, + "%.1f m"); + paramsChanged |= ImGui::InputInt("Seed", &s_seed); + paramsChanged |= ImGui::Checkbox("Avoid Water", + &s_avoidWater); + paramsChanged |= ImGui::SliderFloat("Max Edge Slope", + &s_maxSlope, 0.0f, + 1.0f, "%.2f"); + ImGui::Checkbox("Resolve Crossings", &s_resolve); + + auto buildParams = [&]() { + RoadNetworkGen::GridParams p; + p.origin = Ogre::Vector3(s_origin[0], 0.0f, + s_origin[1]); + p.blocksX = s_blocksX; + p.blocksZ = s_blocksZ; + p.blockSizeX = s_blockSizeX; + p.blockSizeZ = s_blockSizeZ; + p.jitter = s_jitter; + p.seed = (uint32_t)s_seed; + p.avoidWater = s_avoidWater; + p.maxSlope = s_maxSlope; + p.runCrossingResolution = s_resolve; + return p; + }; + + if (ImGui::Checkbox("Preview Grid", &s_preview)) { + if (!s_preview) + rs->clearGenPreview(); + else + paramsChanged = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Draw the would-be grid as orange lines " + "without touching the road graph; " + "refreshes on every parameter change."); + if (s_preview && paramsChanged) + rs->updateGenPreview(buildParams()); + + if (ImGui::Button("Generate Grid")) { + s_preview = false; + RoadNetworkGen::GridParams p = buildParams(); + std::string err; + uint32_t gid = rs->generateRoadNetwork(p, &err); + char buf[256]; + if (gid == 0) + snprintf(buf, sizeof(buf), + "Generation failed: %s", + err.c_str()); + else + snprintf(buf, sizeof(buf), + "Generated group %u (%zu nodes, " + "%zu edges in the graph).", + gid, rg.nodes.size(), + rg.edges.size()); + s_genResult = buf; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Emit a deterministic seeded settlement grid. " + "All generated nodes are stamped with a group id " + "and can only be removed as a whole group."); + if (ImGui::Button("Resolve Crossings Now")) { + std::string err; + int created = rs->resolveCrossingsNow(&err); + char buf[256]; + if (created < 0) + snprintf(buf, sizeof(buf), + "Crossing resolution failed: %s", + err.c_str()); + else + snprintf(buf, sizeof(buf), + "Crossing resolution: %d " + "junction(s) created.", + created); + s_genResult = buf; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Turn every crossing of road edges into a " + "proper junction (edges closer than " + "crossingHeightThreshold in Y; larger gaps " + "are overpasses and left alone)."); + if (!s_genResult.empty()) + ImGui::TextWrapped("%s", s_genResult.c_str()); + + /* Generated groups present in the graph; each is + * removed as a whole. */ + std::map groupCounts; + for (const RoadNode &n : rg.nodes) + if (n.generatedGroupId != 0) + ++groupCounts[n.generatedGroupId]; + for (const auto &kv : groupCounts) { + ImGui::PushID((int)kv.first); + ImGui::Text("Group %u (%d nodes)", kv.first, + kv.second); + ImGui::SameLine(); + if (ImGui::SmallButton("Remove")) { + rs->clearGenerated(kv.first); + ImGui::PopID(); + break; + } + ImGui::PopID(); + } + ImGui::TreePop(); + } + + /* Phase 4: procedural town generation on the road lots + * (TownGenSystem + roadlib/TownGen). */ + if (ImGui::TreeNode("Town Generator")) { + static int s_district = 0; + static int s_townSeed = 1; + static float s_density = 1.0f; + static std::string s_townResult; + const char *districts[] = { + "Village", "Town", "City Residential", + "City Center" + }; + + TownGenSystem *tgs = ts->getTownGenSystem(); + if (!tgs) { + ImGui::TextDisabled( + "Town generation is unavailable."); + } else { + + ImGui::Combo("District", &s_district, districts, 4); + ImGui::InputInt("Seed##town", &s_townSeed); + ImGui::SliderFloat("Density##town", &s_density, 0.0f, 1.0f, + "%.2f"); + + if (ImGui::Button("Generate Town")) { + TownGen::TownParams p; + p.district = (TownGen::District)s_district; + p.seed = (uint32_t)s_townSeed; + p.densityScale = s_density; + std::string err; + int placed = tgs->generate(p, &err); + char buf[256]; + if (placed == 0) + snprintf(buf, sizeof(buf), + "Town generation failed: %s", + err.c_str()); + else + snprintf(buf, sizeof(buf), + "Placed %d buildings.", + placed); + s_townResult = buf; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Place one building per suitable road lot " + "(deterministic under the seed). Replaces " + "the previously generated town."); + ImGui::SameLine(); + if (ImGui::Button("Clear Town")) { + int removed = tgs->clear(); + char buf[128]; + snprintf(buf, sizeof(buf), + "Removed %d town(s).", removed); + s_townResult = buf; + } + if (!s_townResult.empty()) + ImGui::TextWrapped("%s", s_townResult.c_str()); + } + ImGui::TreePop(); + } /* Toolbar — visible only in road edit mode. */ if (ts->getRoadEditMode()) { /* Tool selector: Move Node (default) / Add Node / @@ -831,10 +1101,28 @@ private: ts->setRoadEditTool( TerrainSystem::RoadEditTool::Connect); - if (ImGui::Button("Remove Selected Node") && - rs->getSelectedNodeId() >= 0) { - rg.removeNode(rs->getSelectedNodeId()); - rs->setSelectedNodeId(-1); + { + const RoadNode *selNode = + rs->getSelectedNodeId() >= 0 ? + rg.findNodeById( + rs->getSelectedNodeId()) : + nullptr; + bool selGrouped = selNode && + selNode->generatedGroupId != 0; + ImGui::BeginDisabled(selGrouped); + if (ImGui::Button("Remove Selected Node") && + rs->getSelectedNodeId() >= 0) { + rg.removeNode(rs->getSelectedNodeId()); + rs->setSelectedNodeId(-1); + } + ImGui::EndDisabled(); + if (selGrouped && + ImGui::IsItemHovered( + ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip( + "Generated-group node: remove the " + "whole group in the Road Network " + "Generator section."); } static std::string s_roadValidationError; @@ -1038,6 +1326,9 @@ private: for (const auto &n : rg.nodes) { bool selected = (n.id == selectedNodeId); std::string label = "Node " + std::to_string(n.id); + if (n.generatedGroupId != 0) + label += " [gen " + + std::to_string(n.generatedGroupId) + "]"; bool showConnect = selectedNodeId >= 0 && selectedNodeId != n.id; float rowWidth = ImGui::GetContentRegionAvail().x; @@ -1089,6 +1380,12 @@ private: std::string label = "Edge " + std::to_string(i) + ": " + std::to_string(e.nodeA) + " <-> " + std::to_string(e.nodeB); + /* Generated-group edges are unsplittable/unremovable: + * the group is managed as a whole. */ + const RoadNode *ea = rg.findNodeById(e.nodeA); + const RoadNode *eb = rg.findNodeById(e.nodeB); + bool eGrouped = (ea && ea->generatedGroupId != 0) || + (eb && eb->generatedGroupId != 0); float rowWidth = ImGui::GetContentRegionAvail().x; rowWidth -= ImGui::CalcTextSize("Split").x + ImGui::GetStyle().FramePadding.x * 2.0f + @@ -1105,6 +1402,7 @@ private: rs->setSelectedNodeId(-1); } ImGui::SameLine(); + ImGui::BeginDisabled(eGrouped); if (ImGui::SmallButton("Split")) { int newId = rg.splitEdge(i, 0.5f); /* The midpoint Y is interpolated between the @@ -1126,6 +1424,14 @@ private: rs->setSelectedEdgeIndex(-1); } } + ImGui::EndDisabled(); + if (eGrouped && + ImGui::IsItemHovered( + ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip( + "Generated-group edge: split/remove is " + "disabled; manage the group in the Road " + "Network Generator section."); ImGui::PopID(); } @@ -1417,6 +1723,154 @@ private: } } + /* Markings tri-state (phase 2.5): Inherit + * follows RoadConfig::markingsEnabled, On/Off + * force this edge. */ + { + const char *mitems[3] = { "Inherit", "On", + "Off" }; + int mcur = (int)e.markingsMode; + if (ImGui::Combo("Markings", &mcur, + mitems, 3)) { + e.markingsMode = + (RoadMarkingMode)mcur; + edgeChanged = true; + } + } + + /* Street assignment (phase 3): an edge belongs + * to at most one named street; streets give + * lots their addresses. */ + { + const RoadStreet *st = + RoadNetworkGen::findStreetForEdge( + rg, e.nodeA, e.nodeB); + static char s_streetBuf[128] = {}; + if (st) + ImGui::Text("Street: %s", + st->name.c_str()); + else + ImGui::TextDisabled( + "Street: (none)"); + ImGui::SetNextItemWidth(160.0f); + ImGui::InputText("##streetname", + s_streetBuf, + sizeof(s_streetBuf)); + ImGui::SameLine(); + if (ImGui::SmallButton("Assign") && + s_streetBuf[0]) { + RoadNetworkGen::nameStreet( + rg, s_streetBuf, + { std::make_pair(e.nodeA, + e.nodeB) }); + edgeChanged = true; + } + if (st) { + ImGui::SameLine(); + if (ImGui::SmallButton("Unassign")) { + std::pair key( + std::min(e.nodeA, + e.nodeB), + std::max(e.nodeA, + e.nodeB)); + for (RoadStreet &s : + rg.streets) { + auto &edges = + s.edges; + edges.erase( + std::remove( + edges.begin(), + edges.end(), + key), + edges.end()); + } + for (size_t si = + rg.streets.size(); + si-- > 0;) + if (rg.streets[si] + .edges + .empty()) + rg.streets.erase( + rg.streets.begin() + + si); + edgeChanged = true; + } + } + } + + /* Decals (phase 2.5): atlas-textured quads + * painted on the road surface. The preset + * combo selects one of the four quadrants of + * the shared decal atlas; the uv rect stays + * directly editable for custom atlases. */ + if (ImGui::TreeNode("Decals")) { + static const char *s_presets[4] = { + "Manhole", "Arrow", "Damage", + "Parking Bars" + }; + static const float s_presetUv[4][4] = { + { 0.0f, 0.0f, 0.5f, 0.5f }, + { 0.5f, 0.0f, 0.5f, 0.5f }, + { 0.0f, 0.5f, 0.5f, 0.5f }, + { 0.5f, 0.5f, 0.5f, 0.5f } + }; + int removeDecal = -1; + for (size_t di = 0; di < e.decals.size(); + ++di) { + RoadEdgeDecal &d = e.decals[di]; + ImGui::PushID((int)di + 1000); + int preset = -1; + for (int k = 0; k < 4; ++k) + if (d.uvX == s_presetUv[k][0] && + d.uvY == s_presetUv[k][1] && + d.uvW == s_presetUv[k][2] && + d.uvH == s_presetUv[k][3]) + preset = k; + ImGui::SetNextItemWidth(110.0f); + if (ImGui::Combo( + "##preset", &preset, + s_presets, 4)) { + d.uvX = s_presetUv[preset][0]; + d.uvY = s_presetUv[preset][1]; + d.uvW = s_presetUv[preset][2]; + d.uvH = s_presetUv[preset][3]; + edgeChanged = true; + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(90.0f); + edgeChanged |= ImGui::SliderFloat( + "T", &d.edgeT, 0.0f, 1.0f); + ImGui::SameLine(); + if (ImGui::SmallButton("X")) + removeDecal = (int)di; + ImGui::SetNextItemWidth(150.0f); + edgeChanged |= ImGui::SliderFloat( + "Lateral (m)", + &d.lateralOffset, -15.0f, + 15.0f); + ImGui::SetNextItemWidth(150.0f); + edgeChanged |= ImGui::SliderFloat( + "Size (m)", &d.size, 0.3f, + 8.0f); + ImGui::PopID(); + } + if (removeDecal >= 0) { + e.decals.erase(e.decals.begin() + + removeDecal); + edgeChanged = true; + } + if (ImGui::SmallButton("Add Decal")) { + RoadEdgeDecal d; + d.uvX = s_presetUv[0][0]; + d.uvY = s_presetUv[0][1]; + d.uvW = s_presetUv[0][2]; + d.uvH = s_presetUv[0][3]; + e.decals.push_back(d); + edgeChanged = true; + } + ImGui::TreePop(); + } + /* Edge prefab slots (improvement plan §3): * left/right curb anchors plus one centerline * midpoint prefab. */