Data stability improved, town works much better
This commit is contained in:
@@ -1240,9 +1240,18 @@ Per-terrain procedural roads (`RoadGraph` component, geometry built by
|
||||
"Smooth A–B–C" action (`RoadGraph::smoothNode`, position relaxation —
|
||||
no graph mutation).
|
||||
- **Lane counts (0 = unset)**: `RoadEdge::lanesAtoB/lanesBtoA` of 0 mean
|
||||
"unset" and resolve to `RoadConfig::lanesPerDirection` per side
|
||||
(`RoadGraph::resolveLaneCounts()`) — never "no lanes". A 0+0 edge
|
||||
builds as a full default road, not a placeholder path.
|
||||
"unset" and resolve to `RoadConfig::defaultLanesAtoB/defaultLanesBtoA`
|
||||
per direction (0 there falls back to the legacy symmetric
|
||||
`RoadConfig::lanesPerDirection`; `RoadGraph::resolveLaneCounts()`) —
|
||||
never "no lanes". A 0+0 edge builds as a full default road, not a
|
||||
placeholder path.
|
||||
- **Multi-edge selection / batch editing**: ctrl+click edges in the
|
||||
list or in the 3D view toggles membership in
|
||||
`RoadSystem::m_selectedEdgeIndices` (plain click single-selects and
|
||||
replaces the set); "Select Connected" flood-fills the component,
|
||||
node rows have "Sel. Path" (BFS shortest path to the selected node).
|
||||
The Batch Edit Selection panel applies lanes A→B/B→A and sidewalk
|
||||
left/right modes (each with a "keep" choice) to the whole set.
|
||||
- **Roadside prefabs**: three fixed slots per edge (`prefabLeft`,
|
||||
`prefabRight`, `prefabMid` — anchors at left curb / right curb /
|
||||
centerline). Spawning is per edge (`RoadSystem::m_edgePrefabs`, keyed by
|
||||
@@ -1255,7 +1264,10 @@ Per-terrain procedural roads (`RoadGraph` component, geometry built by
|
||||
see `ProceduralRoadGeometry.md` §15); terrain compliance starts its
|
||||
falloff at the sidewalk outer edge. Per edge side the tri-state
|
||||
`RoadEdge::sidewalkLeftMode/RightMode` (Inherit/On/Off) can override
|
||||
the global switch either way; the geometry builders fold modes +
|
||||
the per-side config defaults
|
||||
(`RoadConfig::defaultSidewalkLeftMode/defaultSidewalkRightMode`,
|
||||
themselves tri-states falling back to the legacy global
|
||||
`sidewalkEnabled`); the geometry builders fold modes +
|
||||
global themselves (`RoadGraph::edgeSidewalkEnabled()`), while
|
||||
`resolveSidewalkModes()` caches the resolved `sidewalkLeft/Right`
|
||||
bools for terrain compliance and the editor UI.
|
||||
@@ -1347,7 +1359,13 @@ Per-terrain procedural roads (`RoadGraph` component, geometry built by
|
||||
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
|
||||
manual road via `connectGroupToNode()`. Per-generation road make-up
|
||||
(`GridParams::lanesAtoB/lanesBtoA`, `sidewalkLeftMode/
|
||||
sidewalkRightMode`; 0/Inherit = leave unset, config defaults apply)
|
||||
is stamped on every edge the generation creates — two settlements in
|
||||
one map can differ without touching the RoadConfig defaults, and
|
||||
`splitEdge()` copies the whole `RoadEdge` so crossing-resolution
|
||||
splits preserve the values. 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
|
||||
@@ -1449,8 +1467,13 @@ Per-terrain procedural roads (`RoadGraph` component, geometry built by
|
||||
library. **Town areas** (phase 4.5 item 4): `RoadGraph::townAreas`
|
||||
(`RoadTownArea`: name, square/circle XZ scope, district, seed,
|
||||
densityScale) scope generation per settlement —
|
||||
`TownGen::generateTown()` filters lots by centroid containment with
|
||||
first-match-wins over overlapping areas before any RNG draws;
|
||||
`TownGen::generateTown()` filters lots by centroid containment before
|
||||
any RNG draws, and a contested lot (inside 2+ areas) belongs to
|
||||
exactly one area via `RoadNetworkGen::ownerAreaIndex()` — smallest
|
||||
containing coverage, tie-break lower seed then name; stable under
|
||||
`townAreas` reordering. The Town Areas block has an "Area Ownership
|
||||
Preview" checkbox coloring each lot by its owning area in road edit
|
||||
mode (with a per-area legend and contested-lot X markers);
|
||||
`TownGenSystem::generateAreas()` builds one `"TownGen <area name>"`
|
||||
container per area (the unscoped `generate()` stays for
|
||||
single-settlement scenes and `--generate-town`). Areas persist
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
|
||||
> **Lane counts — the 0-is-unset rule.** `RoadEdge::lanesAtoB` and
|
||||
> `RoadEdge::lanesBtoA` are per-edge overrides; **0 means "unset"** and
|
||||
> resolves to the global `RoadConfig::lanesPerDirection` (default 1) for
|
||||
> resolves to `RoadConfig::defaultLanesAtoB` / `defaultLanesBtoA`
|
||||
> (per-direction defaults; 0 there falls back to the legacy symmetric
|
||||
> `RoadConfig::lanesPerDirection`, default 1) for
|
||||
> that side — it never means "no lanes". An edge stored as 0+0 therefore
|
||||
> builds as a full default road (1+1 lanes with the shipped config), not
|
||||
> as a path/placeholder; there is currently no way to express a true
|
||||
@@ -1091,8 +1093,11 @@ bands stay open where a future wedge would connect.
|
||||
|
||||
Each edge controls its sidewalk per side via the tri-state
|
||||
`RoadEdge::sidewalkLeftMode` / `sidewalkRightMode` (`RoadSidewalkMode`:
|
||||
**Inherit** — the default — follows the global
|
||||
`RoadConfig::sidewalkEnabled`, **On** forces the side even when sidewalks
|
||||
**Inherit** — the default — follows the per-side config defaults
|
||||
`RoadConfig::defaultSidewalkLeftMode` / `defaultSidewalkRightMode`,
|
||||
which are themselves tri-states whose Inherit falls back to the legacy
|
||||
global `RoadConfig::sidewalkEnabled`; **On** forces the side even when
|
||||
sidewalks
|
||||
are globally off, **Off** suppresses it even when globally on).
|
||||
`RoadGraph::resolveSidewalkModes()` folds modes + global switch into the
|
||||
resolved `RoadEdge::sidewalkLeft` / `sidewalkRight` bools (called by
|
||||
@@ -1518,11 +1523,20 @@ 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
|
||||
slope, crossing resolution, per-generation road make-up; 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()`. The per-generation make-up
|
||||
(`GridParams::lanesAtoB/lanesBtoA` and
|
||||
`sidewalkLeftMode/sidewalkRightMode`, phase 5.9 follow-up) is stamped on
|
||||
every edge the generation creates, so two settlements in one map can
|
||||
differ without touching the `RoadConfig` defaults; 0 lanes / Inherit
|
||||
leave the edge fields unset (the config defaults apply at resolve
|
||||
time), and `RoadGraph::splitEdge()` copies the whole `RoadEdge`, so the
|
||||
crossing resolution's splits preserve the stamped values. "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
|
||||
@@ -1539,7 +1553,8 @@ Headless coverage: `road_geometry_overlap_test` probes
|
||||
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).
|
||||
slope rejection, two settlements in one graph, group connect/remove,
|
||||
per-generation lanes/sidewalk stamping + split survival).
|
||||
|
||||
## 20. Procedural Town Generation (taxi demo phase 4)
|
||||
|
||||
@@ -2079,8 +2094,11 @@ struct RoadTownArea {
|
||||
|
||||
`TownGen` filters lots per area by **centroid containment** (simple,
|
||||
deterministic; a lot straddling the boundary belongs to the area
|
||||
containing its centroid — overlapping areas resolve first-match-wins
|
||||
and are flagged in the editor). Lots inside NO area generate nothing
|
||||
containing its centroid — overlapping areas resolve through
|
||||
`RoadNetworkGen::ownerAreaIndex`: smallest containing coverage wins,
|
||||
ties break by lower seed then lexicographic name, so the owner is
|
||||
stable under `townAreas` reordering; contested lots are marked in the
|
||||
editor's area-ownership preview). Lots inside NO area generate nothing
|
||||
(areas are the only generation scope; the old global generate becomes
|
||||
a one-click "create area around origin" convenience). Each area
|
||||
generates into its own `"TownGen <name>"` container. Persistence
|
||||
@@ -2095,8 +2113,17 @@ the unscoped `TownGenSystem::generate()` stays as-is (area filtering
|
||||
is opt-in via the trailing `areas`/`areaIndex` parameters of
|
||||
`TownGen::generateTown()`, so the single-town button, the
|
||||
`--generate-town` headless check and all existing probes are
|
||||
unaffected), and overlapping areas are NOT flagged in the editor —
|
||||
first-match-wins applies silently. `RoadTownArea` lives in
|
||||
unaffected). Overlapping areas were originally first-match-wins and
|
||||
NOT flagged in the editor; the phase 5.9 follow-up replaced that with
|
||||
the order-independent `RoadNetworkGen::ownerAreaIndex()` rule
|
||||
(smallest containing coverage, then lower seed, then name) shared by
|
||||
generation and the editor, and added an "Area Ownership Preview"
|
||||
checkbox in the Town Areas block: `RoadSystem::buildLotOutlines()`
|
||||
then colors every `findLots()` lot by its owning area (color hashed
|
||||
from the area name, stable under reordering), draws area bounds in
|
||||
matching colors, marks contested lots with an X in the winner's
|
||||
color, and a legend lists per-area owned/contested lot counts.
|
||||
`RoadTownArea` lives in
|
||||
`components/RoadGraph.hpp` with its own `RoadTownDistrict` enum
|
||||
(Village..CityCenter, no Inherit, value-compatible with
|
||||
`TownGen::District`, static_asserted in TownGenSystem.cpp);
|
||||
@@ -2105,8 +2132,8 @@ generateAreas()` shares the terrain/water/sampler setup with
|
||||
`generate()` (extracted `setupTownRun`), requires a non-empty
|
||||
`townAreas`, clears once and builds one `"TownGen <area name>"`
|
||||
container per area through the extracted `createTownContainer()`
|
||||
tail. Filtering runs before any RNG draws (centroid inside the
|
||||
area, not inside any earlier area), so per-area towns are
|
||||
tail. Filtering runs before any RNG draws (owner check via
|
||||
`ownerAreaIndex`), so per-area towns are
|
||||
deterministic. Serialization: `RoadNetworkGen::townAreasToJson/
|
||||
FromJson`, inline `townAreas` in the scene JSON next to `streets`
|
||||
(non-streaming), `roads/towns.json` via
|
||||
@@ -2119,10 +2146,14 @@ the two picks don't steal each other's click —, halfSize/radius,
|
||||
district, seed, densityScale, Remove), with Add Area (Square/Circle)
|
||||
and Generate All Areas buttons; `RoadSystem::buildLotOutlines()`
|
||||
draws area outlines in dark blue (0.1,0.1,0.3) next to the dark grey
|
||||
lot loops. Probes in `runTownGenProbe()`: two overlapping areas on
|
||||
the 2x2-block grid (west square 4 lots, east circle 4 lots after
|
||||
first-match-wins, 4 lots outside all areas build nothing), per-area
|
||||
determinism, disjoint lot sets, and the townAreas json round trip.
|
||||
lot loops (or per-area colors when the ownership preview is on).
|
||||
Probes in `runTownGenProbe()`: two overlapping areas on
|
||||
the 2x2-block grid (west square 4 lots — it is the smaller coverage
|
||||
and owns the contested lots —, east circle 4 lots, 4 lots outside all
|
||||
areas build nothing), per-area
|
||||
determinism, disjoint lot sets, ownership identical under reversed
|
||||
area order, `ownerAreaIndex` containment/coverage/seed/name
|
||||
tie-break unit probes, and the townAreas json round trip.
|
||||
|
||||
### 21.6 Implementation order
|
||||
|
||||
|
||||
@@ -31,6 +31,26 @@ static const float ROAD_SIDEWALK_WALL_GAP = 0.002f;
|
||||
* road network owned by a single TerrainComponent. These values are
|
||||
* serialized with the scene and edited through the terrain editor.
|
||||
*/
|
||||
/**
|
||||
* Per-edge-side sidewalk tri-state.
|
||||
*
|
||||
* RoadEdge::sidewalkLeft/Right used to be plain enables ANDed with the
|
||||
* global RoadConfig::sidewalkEnabled, which made it impossible to give a
|
||||
* single edge a sidewalk while the global switch was off. The tri-state
|
||||
* replaces that: Inherit follows the global switch, On/Off force the side
|
||||
* regardless of it. The resolved value is cached in the legacy bools by
|
||||
* RoadGraph::resolveSidewalkModes() for the geometry builders.
|
||||
*
|
||||
* Declared before RoadConfig: the config's per-side default sidewalk
|
||||
* modes use it as a member type.
|
||||
*/
|
||||
enum class RoadSidewalkMode : int {
|
||||
Inherit = 0, /**< Follow RoadConfig::defaultSidewalk*Mode, then
|
||||
* RoadConfig::sidewalkEnabled. */
|
||||
On = 1, /**< Sidewalk on this side even if globally disabled. */
|
||||
Off = 2 /**< No sidewalk on this side even if globally enabled. */
|
||||
};
|
||||
|
||||
struct RoadConfig {
|
||||
/**
|
||||
* Ogre mesh template used to build each unit-length road segment.
|
||||
@@ -62,6 +82,15 @@ struct RoadConfig {
|
||||
*/
|
||||
int lanesPerDirection = 1;
|
||||
|
||||
/**
|
||||
* Per-direction default lane counts (phase 5.9 follow-up).
|
||||
*
|
||||
* An edge's lanesAtoB/lanesBtoA (0 = default) resolve against these;
|
||||
* a 0 here falls back to the legacy symmetric lanesPerDirection.
|
||||
*/
|
||||
int defaultLanesAtoB = 0;
|
||||
int defaultLanesBtoA = 0;
|
||||
|
||||
/**
|
||||
* Vertical thickness of the road surface.
|
||||
*
|
||||
@@ -101,6 +130,17 @@ struct RoadConfig {
|
||||
float sidewalkThickness = 0.3f;
|
||||
std::string sidewalkMeshTemplate;
|
||||
|
||||
/**
|
||||
* Per-side default sidewalk modes (phase 5.9 follow-up).
|
||||
*
|
||||
* An edge's sidewalkLeftMode/sidewalkRightMode Inherit resolves
|
||||
* against these; Inherit here falls back to the legacy symmetric
|
||||
* sidewalkEnabled switch. Sides follow the edge convention:
|
||||
* looking from nodeA toward nodeB, left is UNIT_Y x dir.
|
||||
*/
|
||||
RoadSidewalkMode defaultSidewalkLeftMode = RoadSidewalkMode::Inherit;
|
||||
RoadSidewalkMode defaultSidewalkRightMode = RoadSidewalkMode::Inherit;
|
||||
|
||||
/**
|
||||
* Entry aprons (M5.18). When enabled, a thin sloped band is
|
||||
* generated along every road outer curb: it starts at the road top
|
||||
@@ -314,22 +354,6 @@ struct RoadSidewalkPass {
|
||||
float width = 6.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-edge-side sidewalk tri-state.
|
||||
*
|
||||
* RoadEdge::sidewalkLeft/Right used to be plain enables ANDed with the
|
||||
* global RoadConfig::sidewalkEnabled, which made it impossible to give a
|
||||
* single edge a sidewalk while the global switch was off. The tri-state
|
||||
* replaces that: Inherit follows the global switch, On/Off force the side
|
||||
* regardless of it. The resolved value is cached in the legacy bools by
|
||||
* RoadGraph::resolveSidewalkModes() for the geometry builders.
|
||||
*/
|
||||
enum class RoadSidewalkMode : int {
|
||||
Inherit = 0, /**< Follow RoadConfig::sidewalkEnabled. */
|
||||
On = 1, /**< Sidewalk on this side even if globally disabled. */
|
||||
Off = 2 /**< No sidewalk on this side even if globally enabled. */
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-edge road-marking tri-state (phase 2.5).
|
||||
*
|
||||
@@ -564,7 +588,8 @@ struct RoadEdge {
|
||||
/**
|
||||
* Number of lanes traveling from nodeA to nodeB.
|
||||
*
|
||||
* 0 means "use RoadConfig::lanesPerDirection". Positive values
|
||||
* 0 means "use RoadConfig::defaultLanesAtoB" (which itself falls
|
||||
* back to RoadConfig::lanesPerDirection when 0). Positive values
|
||||
* override the global default for this direction only. Asymmetric
|
||||
* roads are allowed (e.g. lanesAtoB = 2, lanesBtoA = 1).
|
||||
*/
|
||||
@@ -573,7 +598,8 @@ struct RoadEdge {
|
||||
/**
|
||||
* Number of lanes traveling from nodeB to nodeA.
|
||||
*
|
||||
* 0 means "use RoadConfig::lanesPerDirection". See lanesAtoB for
|
||||
* 0 means "use RoadConfig::defaultLanesBtoA" (falling back to
|
||||
* RoadConfig::lanesPerDirection). See lanesAtoB for
|
||||
* asymmetric-lane semantics.
|
||||
*/
|
||||
int lanesBtoA = 0;
|
||||
@@ -865,6 +891,9 @@ struct RoadGraph {
|
||||
{
|
||||
RoadSidewalkMode m = left ? e.sidewalkLeftMode :
|
||||
e.sidewalkRightMode;
|
||||
if (m == RoadSidewalkMode::Inherit)
|
||||
m = left ? config.defaultSidewalkLeftMode :
|
||||
config.defaultSidewalkRightMode;
|
||||
return m == RoadSidewalkMode::On ||
|
||||
(m == RoadSidewalkMode::Inherit &&
|
||||
config.sidewalkEnabled);
|
||||
@@ -1015,9 +1044,11 @@ struct RoadGraph {
|
||||
lb = edge.lanesPerDirectionOverride;
|
||||
}
|
||||
if (la == 0)
|
||||
la = config.lanesPerDirection;
|
||||
la = config.defaultLanesAtoB > 0 ? config.defaultLanesAtoB :
|
||||
config.lanesPerDirection;
|
||||
if (lb == 0)
|
||||
lb = config.lanesPerDirection;
|
||||
lb = config.defaultLanesBtoA > 0 ? config.defaultLanesBtoA :
|
||||
config.lanesPerDirection;
|
||||
|
||||
if (fromNodeId == edge.nodeA) {
|
||||
outLanesFrom = la;
|
||||
|
||||
@@ -3095,6 +3095,50 @@ control for towns, areas and layouts: moved to **Phase 8.5** (they
|
||||
depend on furniture in 6.5, beds/interiors in 8 and the 4.7
|
||||
population/schedule systems).
|
||||
|
||||
**Follow-up requirements (added 2026-10-23, implemented same day)** —
|
||||
road-editing UX and town-area ownership:
|
||||
|
||||
1. **Road defaults + batch edge editing.** `RoadConfig` gained
|
||||
per-direction defaults — `defaultLanesAtoB`/`defaultLanesBtoA`
|
||||
(0 falls back to legacy `lanesPerDirection`) and tri-state
|
||||
`defaultSidewalkLeftMode`/`defaultSidewalkRightMode` (Inherit falls
|
||||
back to legacy `sidewalkEnabled`) — serialized in the scene
|
||||
roadConfig with full backward compatibility, hashed into the road
|
||||
content signature, editable in the Road Config UI; inheriting edges
|
||||
(0 / Inherit) follow them automatically. Multi-edge selection:
|
||||
ctrl+click in the edge list or the 3D view toggles
|
||||
`RoadSystem::m_selectedEdgeIndices` (plain click single-selects),
|
||||
"Select Connected" flood-fills the component, node rows have
|
||||
"Sel. Path" (BFS shortest path), all selected edges highlight in
|
||||
the 3D view; the Batch Edit Selection panel applies lanes A→B/B→A
|
||||
and sidewalk left/right modes (with per-field "keep") to the whole
|
||||
selection.
|
||||
2. **Town-area preview + order-independent lot ownership.** New
|
||||
`RoadNetworkGen::ownerAreaIndex()`: among areas containing the lot
|
||||
centroid the smallest coverage wins (circle πr² vs square
|
||||
4·halfSize²), ties break by lower seed then name — content-keyed,
|
||||
stable under `townAreas` reordering. `TownGen::generateTown()`
|
||||
uses it instead of first-match-wins. Editor: "Area Ownership
|
||||
Preview" checkbox in the Town Areas block colors every lot by its
|
||||
owning area (`areaPreviewColor`, name-hash hue), draws area bounds
|
||||
in matching colors, marks contested lots with an X in the winner's
|
||||
color, and shows a per-area legend with owned/contested lot counts.
|
||||
New headless probes: ownership identical under reversed area order,
|
||||
ownerAreaIndex containment/coverage/seed/name tie-breaks
|
||||
(`road_geometry_overlap_test` PASSED).
|
||||
3. **Per-generation road make-up (2026-10-23).** `GridParams` gained
|
||||
`lanesAtoB`/`lanesBtoA` (0 = leave the edge field unset, the
|
||||
`RoadConfig` defaults apply at resolve time) and
|
||||
`sidewalkLeftMode`/`sidewalkRightMode` (Inherit = unset); the Road
|
||||
Network Generator UI sets them per generation run, so two
|
||||
settlements in one map can have different lane counts/sidewalks
|
||||
without touching the config defaults. `generateGrid()` stamps them
|
||||
on every edge it creates; `RoadGraph::splitEdge()` copies the whole
|
||||
`RoadEdge`, so crossing-resolution splits preserve the values.
|
||||
Headless probes: params stamped on all generated edges, default
|
||||
params leave edges unset, split survival
|
||||
(`runGeneratorProbe`, `road_geometry_overlap_test` PASSED).
|
||||
|
||||
### Phase 6 — Foot traffic NPCs
|
||||
|
||||
This part concerns asset pipeline in assets/blender/characters in addition to the code.
|
||||
|
||||
@@ -1495,15 +1495,15 @@
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 3992.816650390625,
|
||||
"y": 10.998796463012695,
|
||||
"z": 3959.3525390625
|
||||
"x": 3991.31494140625,
|
||||
"y": 10.990023612976074,
|
||||
"z": 3959.019775390625
|
||||
},
|
||||
"rotation": {
|
||||
"w": 0.9888275265693665,
|
||||
"x": -0.0022681253030896187,
|
||||
"y": -0.14902545511722565,
|
||||
"z": 0.0025473441928625107
|
||||
"w": 0.9888135194778442,
|
||||
"x": -0.00225599086843431,
|
||||
"y": -0.1491188406944275,
|
||||
"z": 0.0025476780720055103
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
@@ -1900,15 +1900,15 @@
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4009.110107421875,
|
||||
"y": 11.092273712158203,
|
||||
"z": 3966.238525390625
|
||||
"x": 4007.2353515625,
|
||||
"y": 11.077539443969727,
|
||||
"z": 3966.094970703125
|
||||
},
|
||||
"rotation": {
|
||||
"w": 0.7900381684303284,
|
||||
"x": -0.004521117080003023,
|
||||
"y": 0.6130222082138062,
|
||||
"z": 0.004806817974895239
|
||||
"w": 0.8054168224334717,
|
||||
"x": -0.004673945717513561,
|
||||
"y": 0.5926746129989624,
|
||||
"z": 0.004342386499047279
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
@@ -2122,6 +2122,10 @@
|
||||
"crossingHeightThreshold": 10.0,
|
||||
"crossingSnapDistance": 5.0,
|
||||
"decalMaterialName": "RoadDecalMaterial",
|
||||
"defaultLanesAtoB": 0,
|
||||
"defaultLanesBtoA": 0,
|
||||
"defaultSidewalkLeftMode": 0,
|
||||
"defaultSidewalkRightMode": 0,
|
||||
"laneWidth": 5.0,
|
||||
"lanesPerDirection": 1,
|
||||
"markingDashGap": 3.0,
|
||||
@@ -2254,7 +2258,7 @@
|
||||
0.800000011920929
|
||||
],
|
||||
"sunSphereSize": 5.0,
|
||||
"timeOfDay": 10.760608673095703,
|
||||
"timeOfDay": 20.96595573425293,
|
||||
"timeSpeed": 0.12999999523162842
|
||||
},
|
||||
"transform": {
|
||||
|
||||
@@ -1,33 +1,12 @@
|
||||
{
|
||||
"edges": [
|
||||
{
|
||||
"lanesAtoB": 2,
|
||||
"lanesBtoA": 2,
|
||||
"lanesPerDirectionOverride": 0,
|
||||
"nodeA": 61,
|
||||
"nodeB": 62,
|
||||
"roadLevelA": 0.0,
|
||||
"roadLevelB": 0.0,
|
||||
"sidewalkLeft": true,
|
||||
"sidewalkLeftMode": 1,
|
||||
"sidewalkRight": true,
|
||||
"sidewalkRightMode": 1
|
||||
}
|
||||
],
|
||||
"edges": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 61,
|
||||
"id": 1,
|
||||
"verticalOffset": 0.1762075424194336,
|
||||
"x": 3910.80126953125,
|
||||
"y": 7.148712158203125,
|
||||
"z": 2962.83984375
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"verticalOffset": 0.22691726684570313,
|
||||
"x": 3951.0302734375,
|
||||
"y": 9.944311141967773,
|
||||
"z": 3000.0
|
||||
}
|
||||
],
|
||||
"page": [
|
||||
|
||||
+1055
-148
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
"nodes": [
|
||||
{
|
||||
"generatedGroupId": 2,
|
||||
"id": 42,
|
||||
"id": 57,
|
||||
"verticalOffset": 0.0,
|
||||
"x": 4048.19775390625,
|
||||
"y": 7.354270935058594,
|
||||
|
||||
@@ -16,6 +16,51 @@
|
||||
"village"
|
||||
],
|
||||
"radius": 500.0,
|
||||
"seed": 1,
|
||||
"town": "Pensville"
|
||||
},
|
||||
{
|
||||
"center": [
|
||||
4664.90185546875,
|
||||
11.98272705078125,
|
||||
3714.66748046875
|
||||
],
|
||||
"circular": false,
|
||||
"densityScale": 1.0,
|
||||
"district": 0,
|
||||
"halfSize": 250.0,
|
||||
"name": "Area 2",
|
||||
"preferredTypes": [
|
||||
"house",
|
||||
"village"
|
||||
],
|
||||
"radius": 500.0,
|
||||
"seed": 1,
|
||||
"town": "Pensville"
|
||||
},
|
||||
{
|
||||
"center": [
|
||||
3401.02001953125,
|
||||
21.564054489135742,
|
||||
4221.458984375
|
||||
],
|
||||
"circular": false,
|
||||
"densityScale": 1.0,
|
||||
"district": 0,
|
||||
"halfSize": 250.0,
|
||||
"name": "Area 3",
|
||||
"preferredTypes": [
|
||||
"shop",
|
||||
"commercial"
|
||||
],
|
||||
"radius": 500.0,
|
||||
"seed": 1,
|
||||
"town": "Pensville"
|
||||
}
|
||||
],
|
||||
"towns": [
|
||||
{
|
||||
"name": "Pensville",
|
||||
"seed": 1
|
||||
}
|
||||
]
|
||||
|
||||
+3
-2
File diff suppressed because one or more lines are too long
@@ -924,7 +924,17 @@ uint32_t generateGrid(RoadGraph &graph, const GridParams ¶ms,
|
||||
if (run < 1e-4f || rise / run > params.maxSlope)
|
||||
return; /* too steep */
|
||||
}
|
||||
graph.addEdge(a, b);
|
||||
int ei = graph.addEdge(a, b);
|
||||
if (ei >= 0) {
|
||||
/* Stamp this generation's road make-up (phase 5.9
|
||||
* follow-up); 0/Inherit leave the edge fields unset so
|
||||
* the RoadConfig defaults apply. */
|
||||
RoadEdge &e = graph.edges[(size_t)ei];
|
||||
e.lanesAtoB = params.lanesAtoB;
|
||||
e.lanesBtoA = params.lanesBtoA;
|
||||
e.sidewalkLeftMode = params.sidewalkLeftMode;
|
||||
e.sidewalkRightMode = params.sidewalkRightMode;
|
||||
}
|
||||
};
|
||||
for (int i = 0; i <= params.blocksX; ++i)
|
||||
for (int j = 0; j <= params.blocksZ; ++j) {
|
||||
@@ -1193,4 +1203,37 @@ uint32_t effectiveAreaSeed(const RoadGraph &graph, const RoadTownArea &area)
|
||||
return area.seed;
|
||||
}
|
||||
|
||||
int ownerAreaIndex(const std::vector<RoadTownArea> &areas,
|
||||
float x, float z)
|
||||
{
|
||||
int best = -1;
|
||||
double bestCoverage = 0.0;
|
||||
for (size_t i = 0; i < areas.size(); ++i) {
|
||||
const RoadTownArea &a = areas[i];
|
||||
if (!a.contains(x, z))
|
||||
continue;
|
||||
double coverage = a.circular ?
|
||||
3.14159265358979323846 * (double)a.radius *
|
||||
(double)a.radius :
|
||||
4.0 * (double)a.halfSize * (double)a.halfSize;
|
||||
if (best < 0 || coverage < bestCoverage) {
|
||||
best = (int)i;
|
||||
bestCoverage = coverage;
|
||||
continue;
|
||||
}
|
||||
if (coverage > bestCoverage)
|
||||
continue;
|
||||
/* Exact coverage tie: lower seed, then name. */
|
||||
const RoadTownArea &b = areas[best];
|
||||
if (a.seed != b.seed) {
|
||||
if (a.seed < b.seed)
|
||||
best = (int)i;
|
||||
continue;
|
||||
}
|
||||
if (a.name < b.name)
|
||||
best = (int)i;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
} /* namespace RoadNetworkGen */
|
||||
|
||||
@@ -200,6 +200,21 @@ struct GridParams {
|
||||
float maxSlope = 0.35f;
|
||||
/** Run resolveAllCrossings after generating. */
|
||||
bool runCrossingResolution = true;
|
||||
|
||||
/**
|
||||
* Per-generation road make-up (phase 5.9 follow-up): the lanes and
|
||||
* sidewalk modes stamped on every edge THIS generation creates, so
|
||||
* two grids in one map can differ without touching the RoadConfig
|
||||
* defaults. A lane count of 0 leaves the edge field unset (it
|
||||
* inherits RoadConfig::defaultLanesAtoB/lanesPerDirection at resolve
|
||||
* time); a sidewalk mode of Inherit leaves the edge mode unset.
|
||||
* RoadGraph::splitEdge copies the whole RoadEdge, so the crossing
|
||||
* resolution's splits preserve the stamped values.
|
||||
*/
|
||||
int lanesAtoB = 0;
|
||||
int lanesBtoA = 0;
|
||||
RoadSidewalkMode sidewalkLeftMode = RoadSidewalkMode::Inherit;
|
||||
RoadSidewalkMode sidewalkRightMode = RoadSidewalkMode::Inherit;
|
||||
};
|
||||
|
||||
/** Terrain height sampler: render-space (x, z) -> surface Y. */
|
||||
@@ -274,6 +289,40 @@ void townAreasFromJson(RoadGraph &graph, const nlohmann::json &j);
|
||||
uint32_t effectiveAreaSeed(const RoadGraph &graph,
|
||||
const RoadTownArea &area);
|
||||
|
||||
/**
|
||||
* Owning area of a lot centroid (phase 5.9 follow-up): a lot belongs
|
||||
* to exactly ONE area. Among the areas containing (x, z) the owner
|
||||
* is the one with the smallest coverage (circle pi*r^2, square
|
||||
* 4*halfSize^2 — the most specific area wins); ties break by lower
|
||||
* seed, then by lexicographic name. All keys are area content, so
|
||||
* the result is stable under townAreas list reordering
|
||||
* (order-independent). Note: a full (coverage, seed) tie makes the
|
||||
* choice name-dependent, so give areas distinct seeds.
|
||||
*
|
||||
* Returns the index into areas, or -1 when no area contains the
|
||||
* point. TownGen::generateTown() and the editor area-ownership
|
||||
* preview both go through this so they never disagree.
|
||||
*/
|
||||
int ownerAreaIndex(const std::vector<RoadTownArea> &areas,
|
||||
float x, float z);
|
||||
|
||||
/**
|
||||
* Deterministic preview color for an area (editor area-ownership
|
||||
* preview): FNV-1a hash of the area name mapped to an HSB hue, so the
|
||||
* color is stable under townAreas list reordering.
|
||||
*/
|
||||
inline Ogre::ColourValue areaPreviewColor(const std::string &name)
|
||||
{
|
||||
uint32_t h = 2166136261u;
|
||||
for (char c : name) {
|
||||
h ^= (uint8_t)c;
|
||||
h *= 16777619u;
|
||||
}
|
||||
Ogre::ColourValue col;
|
||||
col.setHSB((float)(h % 256) / 255.0f, 0.75f, 1.0f);
|
||||
return col;
|
||||
}
|
||||
|
||||
} /* namespace RoadNetworkGen */
|
||||
|
||||
#endif /* EDITSCENE_ROADNETWORKGEN_HPP */
|
||||
|
||||
@@ -368,25 +368,18 @@ TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
streetNumbers.clear();
|
||||
}
|
||||
|
||||
/* Phase 4.5 item 4: town-area scoping. The lot belongs
|
||||
* to the area containing its centroid (first-match-wins
|
||||
* over overlapping areas); only that area's run builds
|
||||
* it. No RNG draws happen before this point, so
|
||||
* filtering does not disturb determinism. */
|
||||
/* Phase 4.5 item 4 / phase 5.9 follow-up: town-area
|
||||
* scoping. The lot belongs to exactly one area — the
|
||||
* deterministic ownerAreaIndex() winner (smallest
|
||||
* containing coverage, then lower seed, then name;
|
||||
* order-independent). Only the owner's run builds it.
|
||||
* No RNG draws happen before this point, so filtering
|
||||
* does not disturb determinism. */
|
||||
if (areas && areaIndex >= 0 &&
|
||||
areaIndex < (int)areas->size()) {
|
||||
if (!(*areas)[areaIndex].contains(lot.centroid.x,
|
||||
lot.centroid.z))
|
||||
continue;
|
||||
bool ownedEarlier = false;
|
||||
for (int k = 0; k < areaIndex; ++k) {
|
||||
if ((*areas)[k].contains(lot.centroid.x,
|
||||
lot.centroid.z)) {
|
||||
ownedEarlier = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ownedEarlier)
|
||||
if (RoadNetworkGen::ownerAreaIndex(
|
||||
*areas, lot.centroid.x,
|
||||
lot.centroid.z) != areaIndex)
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -2528,8 +2528,15 @@ void EditorUISystem::handleRoadEditClick(const Ogre::Ray &mouseRay)
|
||||
|
||||
int edgeIdx = pickRoadEdge(hit);
|
||||
if (edgeIdx >= 0) {
|
||||
rs->setSelectedEdgeIndex(edgeIdx);
|
||||
rs->setSelectedNodeId(-1);
|
||||
/* Ctrl+click toggles the edge in the batch multi-selection
|
||||
* (phase 5.9 follow-up); plain click single-selects. */
|
||||
if (ImGui::GetIO().KeyCtrl) {
|
||||
rs->setSelectedNodeId(-1);
|
||||
rs->toggleEdgeSelected(edgeIdx);
|
||||
} else {
|
||||
rs->setSelectedEdgeIndex(edgeIdx);
|
||||
rs->setSelectedNodeId(-1);
|
||||
}
|
||||
m_roadGizmo->setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -218,12 +218,130 @@ void RoadSystem::setSelectedNodeId(int id)
|
||||
|
||||
void RoadSystem::setSelectedEdgeIndex(int idx)
|
||||
{
|
||||
if (m_selectedEdgeIndex != idx) {
|
||||
bool setChanged = false;
|
||||
if (idx >= 0)
|
||||
setChanged = m_selectedEdgeIndices != std::set<int>{ idx };
|
||||
else
|
||||
setChanged = !m_selectedEdgeIndices.empty();
|
||||
if (m_selectedEdgeIndex != idx || setChanged) {
|
||||
m_selectedEdgeIndex = idx;
|
||||
m_selectedEdgeIndices.clear();
|
||||
if (idx >= 0)
|
||||
m_selectedEdgeIndices.insert(idx);
|
||||
rebuildVisualAids();
|
||||
}
|
||||
}
|
||||
|
||||
void RoadSystem::toggleEdgeSelected(int idx)
|
||||
{
|
||||
if (idx < 0)
|
||||
return;
|
||||
if (m_selectedEdgeIndices.count(idx)) {
|
||||
m_selectedEdgeIndices.erase(idx);
|
||||
if (m_selectedEdgeIndex == idx)
|
||||
m_selectedEdgeIndex = m_selectedEdgeIndices.empty() ?
|
||||
-1 : *m_selectedEdgeIndices.rbegin();
|
||||
} else {
|
||||
m_selectedEdgeIndices.insert(idx);
|
||||
m_selectedEdgeIndex = idx;
|
||||
}
|
||||
rebuildVisualAids();
|
||||
}
|
||||
|
||||
void RoadSystem::clearEdgeSelection()
|
||||
{
|
||||
if (m_selectedEdgeIndices.empty() && m_selectedEdgeIndex < 0)
|
||||
return;
|
||||
m_selectedEdgeIndices.clear();
|
||||
m_selectedEdgeIndex = -1;
|
||||
rebuildVisualAids();
|
||||
}
|
||||
|
||||
int RoadSystem::selectConnectedComponent(int edgeIdx)
|
||||
{
|
||||
flecs::entity terrain = getTerrainEntity();
|
||||
if (!terrain.is_alive() || !terrain.has<TerrainComponent>())
|
||||
return 0;
|
||||
const RoadGraph &rg = terrain.get<TerrainComponent>().roadGraph;
|
||||
if (edgeIdx < 0 || edgeIdx >= (int)rg.edges.size())
|
||||
return 0;
|
||||
|
||||
std::unordered_map<int, std::vector<int>> byNode;
|
||||
for (size_t i = 0; i < rg.edges.size(); ++i) {
|
||||
byNode[rg.edges[i].nodeA].push_back((int)i);
|
||||
byNode[rg.edges[i].nodeB].push_back((int)i);
|
||||
}
|
||||
|
||||
std::set<int> visited;
|
||||
std::vector<int> queue;
|
||||
queue.push_back(edgeIdx);
|
||||
visited.insert(edgeIdx);
|
||||
while (!queue.empty()) {
|
||||
int ei = queue.back();
|
||||
queue.pop_back();
|
||||
const RoadEdge &e = rg.edges[ei];
|
||||
const int endNodes[2] = { e.nodeA, e.nodeB };
|
||||
for (int n : endNodes) {
|
||||
for (int next : byNode[n]) {
|
||||
if (visited.insert(next).second)
|
||||
queue.push_back(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_selectedEdgeIndices = visited;
|
||||
m_selectedEdgeIndex = *visited.rbegin();
|
||||
rebuildVisualAids();
|
||||
return (int)visited.size();
|
||||
}
|
||||
|
||||
int RoadSystem::selectPathBetweenNodes(int nodeA, int nodeB)
|
||||
{
|
||||
flecs::entity terrain = getTerrainEntity();
|
||||
if (!terrain.is_alive() || !terrain.has<TerrainComponent>())
|
||||
return 0;
|
||||
const RoadGraph &rg = terrain.get<TerrainComponent>().roadGraph;
|
||||
if (nodeA == nodeB || !rg.findNodeById(nodeA) ||
|
||||
!rg.findNodeById(nodeB))
|
||||
return 0;
|
||||
|
||||
/* node -> (neighbor node, edge index) adjacency */
|
||||
std::unordered_map<int, std::vector<std::pair<int, int>>> adj;
|
||||
for (size_t i = 0; i < rg.edges.size(); ++i) {
|
||||
adj[rg.edges[i].nodeA].push_back(
|
||||
std::make_pair(rg.edges[i].nodeB, (int)i));
|
||||
adj[rg.edges[i].nodeB].push_back(
|
||||
std::make_pair(rg.edges[i].nodeA, (int)i));
|
||||
}
|
||||
|
||||
/* BFS shortest path by edge count, parent map per node. */
|
||||
std::unordered_map<int, std::pair<int, int>> parent;
|
||||
std::vector<int> queue;
|
||||
queue.push_back(nodeA);
|
||||
parent[nodeA] = std::make_pair(-1, -1);
|
||||
size_t head = 0;
|
||||
while (head < queue.size() && parent.find(nodeB) == parent.end()) {
|
||||
int cur = queue[head++];
|
||||
for (const auto &next : adj[cur]) {
|
||||
if (parent.find(next.first) != parent.end())
|
||||
continue;
|
||||
parent[next.first] = std::make_pair(cur, next.second);
|
||||
queue.push_back(next.first);
|
||||
}
|
||||
}
|
||||
if (parent.find(nodeB) == parent.end())
|
||||
return 0;
|
||||
|
||||
std::set<int> path;
|
||||
for (int n = nodeB; parent[n].first >= 0; n = parent[n].first)
|
||||
path.insert(parent[n].second);
|
||||
|
||||
m_selectedEdgeIndices = path;
|
||||
m_selectedEdgeIndex = *path.rbegin();
|
||||
rebuildVisualAids();
|
||||
return (int)path.size();
|
||||
}
|
||||
|
||||
void RoadSystem::setDebugWedgeEnabled(bool v)
|
||||
{
|
||||
if (m_debugWedgeEnabled != v) {
|
||||
@@ -1274,8 +1392,12 @@ uint64_t RoadSystem::computePageSignature(uint64_t key) const
|
||||
const RoadConfig &cfg = graph.config;
|
||||
fnvHashPod(h, cfg.laneWidth);
|
||||
fnvHashPod(h, cfg.lanesPerDirection);
|
||||
fnvHashPod(h, cfg.defaultLanesAtoB);
|
||||
fnvHashPod(h, cfg.defaultLanesBtoA);
|
||||
fnvHashPod(h, cfg.roadThickness);
|
||||
fnvHashPod(h, cfg.sidewalkEnabled);
|
||||
fnvHashPod(h, cfg.defaultSidewalkLeftMode);
|
||||
fnvHashPod(h, cfg.defaultSidewalkRightMode);
|
||||
fnvHashPod(h, cfg.sidewalkWidth);
|
||||
fnvHashPod(h, cfg.sidewalkHeight);
|
||||
fnvHashPod(h, cfg.sidewalkThickness);
|
||||
@@ -2326,7 +2448,7 @@ void RoadSystem::buildEdgeLines()
|
||||
if (!na || !nb)
|
||||
continue;
|
||||
|
||||
bool selected = ((int)i == m_selectedEdgeIndex);
|
||||
bool selected = m_selectedEdgeIndices.count((int)i) != 0;
|
||||
Ogre::ColourValue color = selected ?
|
||||
Ogre::ColourValue(1.0f, 0.0f, 1.0f) :
|
||||
Ogre::ColourValue(0.0f, 1.0f, 1.0f);
|
||||
@@ -2350,15 +2472,32 @@ void RoadSystem::buildLotOutlines()
|
||||
/* 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. */
|
||||
* like the other helper geometry. With the area-ownership preview
|
||||
* enabled (phase 5.9 follow-up) each lot instead takes its owning
|
||||
* area's color (ownerAreaIndex: smallest containing coverage, then
|
||||
* seed, then name), and contested lots (inside >= 2 areas) get an
|
||||
* X marker at the centroid in the winner's color. */
|
||||
std::vector<RoadNetworkGen::RoadLot> 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) {
|
||||
int owner = -1;
|
||||
int containers = 0;
|
||||
if (m_areaOwnershipPreview && !rg.townAreas.empty()) {
|
||||
owner = RoadNetworkGen::ownerAreaIndex(
|
||||
rg.townAreas, lot.centroid.x, lot.centroid.z);
|
||||
for (const RoadTownArea &area : rg.townAreas)
|
||||
if (area.contains(lot.centroid.x,
|
||||
lot.centroid.z))
|
||||
++containers;
|
||||
}
|
||||
m_lotOutlines->colour(owner >= 0 ?
|
||||
RoadNetworkGen::areaPreviewColor(
|
||||
rg.townAreas[owner].name) :
|
||||
Ogre::ColourValue(0.1f, 0.1f, 0.1f));
|
||||
size_t n = lot.polygon.size();
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
Ogre::Vector3 a = lot.polygon[i];
|
||||
@@ -2368,12 +2507,25 @@ void RoadSystem::buildLotOutlines()
|
||||
m_lotOutlines->position(a);
|
||||
m_lotOutlines->position(b);
|
||||
}
|
||||
if (containers >= 2 && owner >= 0) {
|
||||
/* Contested lot: X marker at the centroid in the
|
||||
* winner's color. */
|
||||
Ogre::Vector3 c = lot.centroid + Ogre::Vector3(0, 0.4f, 0);
|
||||
const float r = 1.5f;
|
||||
m_lotOutlines->position(c + Ogre::Vector3(-r, 0, -r));
|
||||
m_lotOutlines->position(c + Ogre::Vector3(r, 0, r));
|
||||
m_lotOutlines->position(c + Ogre::Vector3(-r, 0, r));
|
||||
m_lotOutlines->position(c + Ogre::Vector3(r, 0, -r));
|
||||
}
|
||||
}
|
||||
|
||||
/* Town generation areas (phase 4.5 item 4): dark blue loops,
|
||||
* squares as 4 corners, circles as 64-segment polylines. */
|
||||
m_lotOutlines->colour(Ogre::ColourValue(0.1f, 0.1f, 0.3f));
|
||||
* squares as 4 corners, circles as 64-segment polylines — or the
|
||||
* area's preview color when the ownership preview is on. */
|
||||
for (const RoadTownArea &area : rg.townAreas) {
|
||||
m_lotOutlines->colour(m_areaOwnershipPreview ?
|
||||
RoadNetworkGen::areaPreviewColor(area.name) :
|
||||
Ogre::ColourValue(0.1f, 0.1f, 0.3f));
|
||||
if (area.circular) {
|
||||
const int segs = 64;
|
||||
for (int i = 0; i < segs; ++i) {
|
||||
@@ -2467,6 +2619,17 @@ void RoadSystem::buildSelectionHighlight()
|
||||
m_selectionHighlight->position(pos + Ogre::Vector3(0, 2.0f, 0));
|
||||
}
|
||||
|
||||
/* Multi-edge selection (phase 5.9 follow-up): full lines over every
|
||||
* selected edge, plus the direction arrow on the primary edge. */
|
||||
m_selectionHighlight->colour(Ogre::ColourValue(1.0f, 0.0f, 1.0f));
|
||||
for (int ei : m_selectedEdgeIndices) {
|
||||
Ogre::Vector3 a, b;
|
||||
if (!getEdgePositions(ei, a, b))
|
||||
continue;
|
||||
m_selectionHighlight->position(a + Ogre::Vector3(0, 0.4f, 0));
|
||||
m_selectionHighlight->position(b + Ogre::Vector3(0, 0.4f, 0));
|
||||
}
|
||||
|
||||
if (m_selectedEdgeIndex >= 0) {
|
||||
Ogre::Vector3 a, b;
|
||||
if (getEdgePositions(m_selectedEdgeIndex, a, b)) {
|
||||
|
||||
@@ -141,6 +141,56 @@ public:
|
||||
int getSelectedEdgeIndex() const { return m_selectedEdgeIndex; }
|
||||
void setSelectedEdgeIndex(int idx);
|
||||
|
||||
/**
|
||||
* Area-ownership preview (phase 5.9 follow-up): when enabled the
|
||||
* lot-outline overlay colors every findLots() lot by its owning
|
||||
* town area (RoadNetworkGen::ownerAreaIndex), draws area bounds in
|
||||
* matching colors and marks contested lots (inside >= 2 areas)
|
||||
* with an X in the winner's color.
|
||||
*/
|
||||
bool getAreaOwnershipPreview() const
|
||||
{
|
||||
return m_areaOwnershipPreview;
|
||||
}
|
||||
void setAreaOwnershipPreview(bool v)
|
||||
{
|
||||
if (m_areaOwnershipPreview != v) {
|
||||
m_areaOwnershipPreview = v;
|
||||
rebuildVisualAids();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-edge selection for batch editing (phase 5.9 follow-up).
|
||||
*
|
||||
* selectedEdgeIndex remains the "primary" edge whose inspector the
|
||||
* panel shows; the set is what batch operations apply to. Plain
|
||||
* setSelectedEdgeIndex() replaces the set with the single edge (or
|
||||
* clears it for -1); toggleEdgeSelected() ctrl-click style adds or
|
||||
* removes one edge and makes it the primary when added.
|
||||
*/
|
||||
const std::set<int> &getSelectedEdgeIndices() const
|
||||
{
|
||||
return m_selectedEdgeIndices;
|
||||
}
|
||||
bool isEdgeSelected(int idx) const
|
||||
{
|
||||
return m_selectedEdgeIndices.count(idx) != 0;
|
||||
}
|
||||
void toggleEdgeSelected(int idx);
|
||||
void clearEdgeSelection();
|
||||
/**
|
||||
* Select every edge reachable from edgeIdx via shared nodes (BFS).
|
||||
* Returns the number of edges selected.
|
||||
*/
|
||||
int selectConnectedComponent(int edgeIdx);
|
||||
/**
|
||||
* Select the edges of the shortest path (by edge count, BFS)
|
||||
* between two nodes. Returns the number of edges selected, 0 when
|
||||
* no path exists.
|
||||
*/
|
||||
int selectPathBetweenNodes(int nodeA, int nodeB);
|
||||
|
||||
/**
|
||||
* Last connectNodes failure from the 3D Connect tool. The terrain
|
||||
* editor panel consumes it with takeConnectError() and shows it as
|
||||
@@ -627,6 +677,11 @@ private:
|
||||
bool m_visualAidsVisible = true;
|
||||
int m_selectedNodeId = -1;
|
||||
int m_selectedEdgeIndex = -1;
|
||||
/* Multi-edge selection (phase 5.9 follow-up); always contains
|
||||
* m_selectedEdgeIndex when that is >= 0. */
|
||||
std::set<int> m_selectedEdgeIndices;
|
||||
/* Area-ownership preview toggle (phase 5.9 follow-up). */
|
||||
bool m_areaOwnershipPreview = false;
|
||||
uint64_t m_lastGraphVersion = 0;
|
||||
std::string m_connectError;
|
||||
bool m_connectErrorPending = false;
|
||||
|
||||
@@ -4838,6 +4838,8 @@ nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity)
|
||||
roadConfigJson["roadMeshTemplate"] = tc.roadGraph.config.roadMeshTemplate;
|
||||
roadConfigJson["laneWidth"] = tc.roadGraph.config.laneWidth;
|
||||
roadConfigJson["lanesPerDirection"] = tc.roadGraph.config.lanesPerDirection;
|
||||
roadConfigJson["defaultLanesAtoB"] = tc.roadGraph.config.defaultLanesAtoB;
|
||||
roadConfigJson["defaultLanesBtoA"] = tc.roadGraph.config.defaultLanesBtoA;
|
||||
roadConfigJson["roadThickness"] = tc.roadGraph.config.roadThickness;
|
||||
roadConfigJson["roadLodDistance"] = tc.roadGraph.config.roadLodDistance;
|
||||
roadConfigJson["roadVisibilityDistance"] =
|
||||
@@ -4850,6 +4852,10 @@ nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity)
|
||||
tc.roadGraph.config.sidewalkThickness;
|
||||
roadConfigJson["sidewalkMeshTemplate"] =
|
||||
tc.roadGraph.config.sidewalkMeshTemplate;
|
||||
roadConfigJson["defaultSidewalkLeftMode"] =
|
||||
(int)tc.roadGraph.config.defaultSidewalkLeftMode;
|
||||
roadConfigJson["defaultSidewalkRightMode"] =
|
||||
(int)tc.roadGraph.config.defaultSidewalkRightMode;
|
||||
roadConfigJson["apronEnabled"] = tc.roadGraph.config.apronEnabled;
|
||||
roadConfigJson["apronWidth"] = tc.roadGraph.config.apronWidth;
|
||||
roadConfigJson["apronDrop"] = tc.roadGraph.config.apronDrop;
|
||||
@@ -5085,6 +5091,10 @@ void SceneSerializer::deserializeTerrain(flecs::entity entity,
|
||||
tc.roadGraph.config.laneWidth = rcj.value("laneWidth", 3.0f);
|
||||
tc.roadGraph.config.lanesPerDirection =
|
||||
rcj.value("lanesPerDirection", 1);
|
||||
tc.roadGraph.config.defaultLanesAtoB =
|
||||
rcj.value("defaultLanesAtoB", 0);
|
||||
tc.roadGraph.config.defaultLanesBtoA =
|
||||
rcj.value("defaultLanesBtoA", 0);
|
||||
tc.roadGraph.config.roadThickness = rcj.value("roadThickness", 0.3f);
|
||||
tc.roadGraph.config.roadLodDistance = rcj.value("roadLodDistance", 200.0f);
|
||||
tc.roadGraph.config.roadVisibilityDistance =
|
||||
@@ -5101,6 +5111,10 @@ void SceneSerializer::deserializeTerrain(flecs::entity entity,
|
||||
rcj.value("sidewalkThickness", 0.3f);
|
||||
tc.roadGraph.config.sidewalkMeshTemplate =
|
||||
rcj.value("sidewalkMeshTemplate", "");
|
||||
tc.roadGraph.config.defaultSidewalkLeftMode =
|
||||
(RoadSidewalkMode)rcj.value("defaultSidewalkLeftMode", 0);
|
||||
tc.roadGraph.config.defaultSidewalkRightMode =
|
||||
(RoadSidewalkMode)rcj.value("defaultSidewalkRightMode", 0);
|
||||
tc.roadGraph.config.apronEnabled =
|
||||
rcj.value("apronEnabled", false);
|
||||
tc.roadGraph.config.apronWidth = rcj.value("apronWidth", 1.0f);
|
||||
|
||||
@@ -4785,7 +4785,9 @@ static bool runTownGenProbe()
|
||||
}
|
||||
|
||||
/* Phase 4.5 item 4: town areas scope generation by lot centroid,
|
||||
* first-match-wins over overlapping areas. */
|
||||
* deterministic owner over overlapping areas (phase 5.9
|
||||
* follow-up: RoadNetworkGen::ownerAreaIndex — smallest containing
|
||||
* coverage, then lower seed, then name; order-independent). */
|
||||
{
|
||||
RoadGraph graph;
|
||||
buildTown(graph);
|
||||
@@ -4815,8 +4817,10 @@ static bool runTownGenProbe()
|
||||
|
||||
/* A: left enclosed pair + left fallback pair = 4 lots.
|
||||
* B's circle overlaps A on the (50,*) enclosed lots but
|
||||
* loses them first-match-wins: right enclosed pair +
|
||||
* right fallback pair = 4 lots. The top/bottom fallback
|
||||
* loses them — A's square (coverage 4*60^2 = 14400) is
|
||||
* smaller than B's circle (pi*120^2 ~= 45239), so A owns
|
||||
* the contested lots: right enclosed pair + right
|
||||
* fallback pair = 4 lots. The top/bottom fallback
|
||||
* squares lie outside both areas and build nothing. */
|
||||
if (resA.buildings.size() != 4 || resB.buildings.size() != 4) {
|
||||
printf(" *** town areas: A=%zu B=%zu, want 4/4 ***\n",
|
||||
@@ -4839,8 +4843,9 @@ static bool runTownGenProbe()
|
||||
}
|
||||
}
|
||||
|
||||
/* First-match-wins: the two area runs own disjoint lot
|
||||
* sets (the overlap lots belong to A). */
|
||||
/* Disjoint ownership: the two area runs own disjoint lot
|
||||
* sets (the contested lots belong to A, the smaller
|
||||
* containing area). */
|
||||
std::set<int> lotsA, lotsB;
|
||||
for (const TownGen::TownBuilding &b : resA.buildings)
|
||||
lotsA.insert(b.lotIndex);
|
||||
@@ -4857,6 +4862,87 @@ static bool runTownGenProbe()
|
||||
ok = false;
|
||||
}
|
||||
|
||||
/* Phase 5.9 follow-up: ownership is order-independent.
|
||||
* Reversing townAreas must swap the per-area indices but
|
||||
* keep each area's lot set identical. */
|
||||
{
|
||||
RoadGraph graphRev = graph;
|
||||
std::reverse(graphRev.townAreas.begin(),
|
||||
graphRev.townAreas.end());
|
||||
TownGen::TownResult resAr =
|
||||
TownGen::generateTown(graphRev, cc, flat,
|
||||
nullptr,
|
||||
&graphRev.townAreas, 1);
|
||||
TownGen::TownResult resBr =
|
||||
TownGen::generateTown(graphRev, cc, flat,
|
||||
nullptr,
|
||||
&graphRev.townAreas, 0);
|
||||
std::set<int> lotsAr, lotsBr;
|
||||
for (const TownGen::TownBuilding &b :
|
||||
resAr.buildings)
|
||||
lotsAr.insert(b.lotIndex);
|
||||
for (const TownGen::TownBuilding &b :
|
||||
resBr.buildings)
|
||||
lotsBr.insert(b.lotIndex);
|
||||
if (lotsAr != lotsA || lotsBr != lotsB) {
|
||||
printf(" *** town areas: ownership changed "
|
||||
"under reorder (A %zu->%zu, B %zu->%zu) "
|
||||
"***\n",
|
||||
lotsA.size(), lotsAr.size(),
|
||||
lotsB.size(), lotsBr.size());
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ownerAreaIndex unit probes: containment, smallest-
|
||||
* coverage win, seed and name tie-breaks — all
|
||||
* independent of list order. */
|
||||
{
|
||||
const std::vector<RoadTownArea> &areas =
|
||||
graph.townAreas;
|
||||
using RoadNetworkGen::ownerAreaIndex;
|
||||
/* (50,100): inside A (square) and B (circle) —
|
||||
* A is smaller. (-30,100): A only.
|
||||
* (250,100): B only. (500,500): none. */
|
||||
if (ownerAreaIndex(areas, 50.0f, 100.0f) != 0 ||
|
||||
ownerAreaIndex(areas, -30.0f, 100.0f) != 0 ||
|
||||
ownerAreaIndex(areas, 250.0f, 100.0f) != 1 ||
|
||||
ownerAreaIndex(areas, 500.0f, 500.0f) != -1) {
|
||||
printf(" *** ownerAreaIndex: containment/"
|
||||
"coverage ***\n");
|
||||
ok = false;
|
||||
}
|
||||
/* Seed tie-break: identical squares, lower seed
|
||||
* wins regardless of order. */
|
||||
RoadTownArea s1, s2;
|
||||
s1.name = "s1";
|
||||
s2.name = "s2";
|
||||
s1.center = s2.center = Vector3(0, 0, 0);
|
||||
s1.halfSize = s2.halfSize = 50.0f;
|
||||
s1.seed = 7;
|
||||
s2.seed = 3;
|
||||
std::vector<RoadTownArea> v1 = { s1, s2 };
|
||||
std::vector<RoadTownArea> v2 = { s2, s1 };
|
||||
int o1 = ownerAreaIndex(v1, 0.0f, 0.0f);
|
||||
int o2 = ownerAreaIndex(v2, 0.0f, 0.0f);
|
||||
if (o1 != 1 || o2 != 0 ||
|
||||
v1[o1].name != "s2" || v2[o2].name != "s2") {
|
||||
printf(" *** ownerAreaIndex: seed "
|
||||
"tie-break ***\n");
|
||||
ok = false;
|
||||
}
|
||||
/* Name tie-break: identical coverage and seed,
|
||||
* lexicographically smaller name wins. */
|
||||
s2.seed = s1.seed;
|
||||
std::vector<RoadTownArea> v3 = { s2, s1 };
|
||||
int o3 = ownerAreaIndex(v3, 0.0f, 0.0f);
|
||||
if (o3 < 0 || v3[o3].name != "s1") {
|
||||
printf(" *** ownerAreaIndex: name "
|
||||
"tie-break ***\n");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* townAreas json round trip (phase 5.9: towns + preferred
|
||||
* types included). */
|
||||
{
|
||||
@@ -6434,6 +6520,67 @@ static bool runGeneratorProbe()
|
||||
}
|
||||
}
|
||||
|
||||
/* Per-generation lanes/sidewalk stamping (phase 5.9 follow-up):
|
||||
* the params land on every edge this generation creates, and
|
||||
* crossing-resolution splits preserve them (splitEdge copies the
|
||||
* whole RoadEdge). */
|
||||
{
|
||||
GridParams ps = p;
|
||||
ps.jitter = 0.0f;
|
||||
ps.lanesAtoB = 2;
|
||||
ps.lanesBtoA = 1;
|
||||
ps.sidewalkLeftMode = RoadSidewalkMode::On;
|
||||
ps.sidewalkRightMode = RoadSidewalkMode::Off;
|
||||
RoadGraph gStamp;
|
||||
generateGrid(gStamp, ps, HeightSampler(), nullptr);
|
||||
bool stamped = !gStamp.edges.empty();
|
||||
for (const RoadEdge &e : gStamp.edges) {
|
||||
if (e.lanesAtoB != 2 || e.lanesBtoA != 1 ||
|
||||
e.sidewalkLeftMode != RoadSidewalkMode::On ||
|
||||
e.sidewalkRightMode != RoadSidewalkMode::Off) {
|
||||
stamped = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!stamped) {
|
||||
printf(" *** stamping: generated edges lack the "
|
||||
"params ***\n");
|
||||
ok = false;
|
||||
}
|
||||
/* Defaults (0/Inherit) leave the edge fields unset so the
|
||||
* RoadConfig defaults apply. */
|
||||
bool unset = true;
|
||||
for (const RoadEdge &e : g1.edges) {
|
||||
if (e.lanesAtoB != 0 || e.lanesBtoA != 0 ||
|
||||
e.sidewalkLeftMode != RoadSidewalkMode::Inherit ||
|
||||
e.sidewalkRightMode != RoadSidewalkMode::Inherit) {
|
||||
unset = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!unset) {
|
||||
printf(" *** stamping: default params dirtied the "
|
||||
"edges ***\n");
|
||||
ok = false;
|
||||
}
|
||||
/* Split survival: split a stamped edge, both halves must
|
||||
* keep the stamped values. */
|
||||
int splitId = gStamp.splitEdge(0, 0.5f);
|
||||
bool splitOk = splitId > 0;
|
||||
for (const RoadEdge &e : gStamp.edges) {
|
||||
if (e.lanesAtoB != 2 || e.lanesBtoA != 1 ||
|
||||
e.sidewalkLeftMode != RoadSidewalkMode::On ||
|
||||
e.sidewalkRightMode != RoadSidewalkMode::Off) {
|
||||
splitOk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!splitOk) {
|
||||
printf(" *** stamping: split lost the params ***\n");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
printf(" %s\n", ok ? "passed" : "FAILURES above");
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -1319,6 +1319,24 @@ private:
|
||||
cfgChanged |= ImGui::SliderInt(
|
||||
"Lanes Per Direction",
|
||||
&rg.config.lanesPerDirection, 1, 8);
|
||||
cfgChanged |= ImGui::SliderInt(
|
||||
"Default Lanes A->B",
|
||||
&rg.config.defaultLanesAtoB, 0, 8);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Default lane count for the edge A->B "
|
||||
"direction (0 = use Lanes Per "
|
||||
"Direction). Edges with Lanes A->B = 0 "
|
||||
"inherit this.");
|
||||
cfgChanged |= ImGui::SliderInt(
|
||||
"Default Lanes B->A",
|
||||
&rg.config.defaultLanesBtoA, 0, 8);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Default lane count for the edge B->A "
|
||||
"direction (0 = use Lanes Per "
|
||||
"Direction). Edges with Lanes B->A = 0 "
|
||||
"inherit this.");
|
||||
cfgChanged |= ImGui::SliderFloat(
|
||||
"Road Thickness", &rg.config.roadThickness, 0.05f,
|
||||
1.0f, "%.2f");
|
||||
@@ -1342,7 +1360,45 @@ private:
|
||||
ImGui::SeparatorText("Sidewalks");
|
||||
cfgChanged |= ImGui::Checkbox(
|
||||
"Sidewalks Enabled", &rg.config.sidewalkEnabled);
|
||||
if (rg.config.sidewalkEnabled) {
|
||||
{
|
||||
/* Per-side defaults (phase 5.9 follow-up): edges
|
||||
* whose sidewalk mode is Inherit resolve against
|
||||
* these; Inherit here falls back to the legacy
|
||||
* "Sidewalks Enabled" switch. */
|
||||
const char *swModes[3] = { "Inherit", "On",
|
||||
"Off" };
|
||||
int dl = (int)rg.config.defaultSidewalkLeftMode;
|
||||
if (ImGui::Combo("Default Sidewalk Left", &dl,
|
||||
swModes, 3)) {
|
||||
rg.config.defaultSidewalkLeftMode =
|
||||
(RoadSidewalkMode)dl;
|
||||
cfgChanged = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Default for the left side (looking "
|
||||
"from node A to node B) of edges in "
|
||||
"Inherit mode; Inherit here uses "
|
||||
"Sidewalks Enabled.");
|
||||
int dr = (int)rg.config.defaultSidewalkRightMode;
|
||||
if (ImGui::Combo("Default Sidewalk Right", &dr,
|
||||
swModes, 3)) {
|
||||
rg.config.defaultSidewalkRightMode =
|
||||
(RoadSidewalkMode)dr;
|
||||
cfgChanged = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Default for the right side (looking "
|
||||
"from node A to node B) of edges in "
|
||||
"Inherit mode; Inherit here uses "
|
||||
"Sidewalks Enabled.");
|
||||
}
|
||||
if (rg.config.sidewalkEnabled ||
|
||||
rg.config.defaultSidewalkLeftMode ==
|
||||
RoadSidewalkMode::On ||
|
||||
rg.config.defaultSidewalkRightMode ==
|
||||
RoadSidewalkMode::On) {
|
||||
cfgChanged |= ImGui::SliderFloat(
|
||||
"Sidewalk Width",
|
||||
&rg.config.sidewalkWidth, 0.5f, 5.0f);
|
||||
@@ -1453,6 +1509,11 @@ private:
|
||||
static bool s_pickArmed = false;
|
||||
static bool s_preview = false;
|
||||
static std::string s_genResult;
|
||||
/* Per-generation road make-up (phase 5.9 follow-up):
|
||||
* stamped on the edges this generation creates, not
|
||||
* the RoadConfig defaults. 0 lanes = inherit config. */
|
||||
static int s_genLanesAtoB = 0, s_genLanesBtoA = 0;
|
||||
static int s_genSidewalkLeft = 0, s_genSidewalkRight = 0;
|
||||
|
||||
bool paramsChanged =
|
||||
ImGui::InputFloat2("Origin XZ", s_origin);
|
||||
@@ -1510,6 +1571,28 @@ private:
|
||||
1.0f, "%.2f");
|
||||
ImGui::Checkbox("Resolve Crossings", &s_resolve);
|
||||
|
||||
/* Lanes/sidewalks stamped on THIS generation's edges
|
||||
* (0/Inherit leave the edge fields unset so the
|
||||
* RoadConfig defaults apply at resolve time). */
|
||||
paramsChanged |= ImGui::SliderInt("Lanes A->B##gen",
|
||||
&s_genLanesAtoB, 0, 8);
|
||||
paramsChanged |= ImGui::SliderInt("Lanes B->A##gen",
|
||||
&s_genLanesBtoA, 0, 8);
|
||||
const char *genSideItems[3] = { "Inherit", "On", "Off" };
|
||||
paramsChanged |= ImGui::Combo("Sidewalk Left##gen",
|
||||
&s_genSidewalkLeft,
|
||||
genSideItems, 3);
|
||||
paramsChanged |= ImGui::Combo("Sidewalk Right##gen",
|
||||
&s_genSidewalkRight,
|
||||
genSideItems, 3);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Road make-up for the edges this generation "
|
||||
"creates. 0 lanes / Inherit sidewalk leave the "
|
||||
"edge fields unset so the RoadConfig defaults "
|
||||
"apply; nonzero/On/Off stamp every generated "
|
||||
"edge (survives crossing-resolution splits).");
|
||||
|
||||
auto buildParams = [&]() {
|
||||
RoadNetworkGen::GridParams p;
|
||||
p.origin = Ogre::Vector3(s_origin[0], 0.0f,
|
||||
@@ -1523,6 +1606,12 @@ private:
|
||||
p.avoidWater = s_avoidWater;
|
||||
p.maxSlope = s_maxSlope;
|
||||
p.runCrossingResolution = s_resolve;
|
||||
p.lanesAtoB = s_genLanesAtoB;
|
||||
p.lanesBtoA = s_genLanesBtoA;
|
||||
p.sidewalkLeftMode =
|
||||
(RoadSidewalkMode)s_genSidewalkLeft;
|
||||
p.sidewalkRightMode =
|
||||
(RoadSidewalkMode)s_genSidewalkRight;
|
||||
return p;
|
||||
};
|
||||
|
||||
@@ -1831,11 +1920,101 @@ private:
|
||||
ImGui::Separator();
|
||||
/* Phase 4.5 item 4: town areas — named square/circular
|
||||
* generation scopes carried by the road graph. A lot is
|
||||
* built by the area containing its centroid
|
||||
* (first-match-wins over overlapping areas).
|
||||
* built by the area owning its centroid
|
||||
* (RoadNetworkGen::ownerAreaIndex — smallest
|
||||
* containing coverage, then seed, then name;
|
||||
* order-independent).
|
||||
* "Generate All Areas" builds one "TownGen <area>"
|
||||
* container per area. */
|
||||
ImGui::TextUnformatted("Town Areas");
|
||||
/* Area-ownership preview (phase 5.9 follow-up):
|
||||
* viewport overlay coloring each lot by its owning
|
||||
* area, plus a legend with per-area lot counts. */
|
||||
{
|
||||
bool ownPreview = rs->getAreaOwnershipPreview();
|
||||
if (ImGui::Checkbox("Area Ownership Preview",
|
||||
&ownPreview))
|
||||
rs->setAreaOwnershipPreview(ownPreview);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Color every lot by the area that "
|
||||
"owns it and draw area bounds in "
|
||||
"matching colors. Lots inside 2+ "
|
||||
"areas get an X marker in the "
|
||||
"winning area's color.");
|
||||
if (ownPreview && !rg.townAreas.empty()) {
|
||||
static uint64_t s_ownVersion = ~0ull;
|
||||
static size_t s_ownAreaCount = 0;
|
||||
static std::vector<int> s_ownLots;
|
||||
static std::vector<int> s_ownContested;
|
||||
if (s_ownVersion != rg.version ||
|
||||
s_ownAreaCount !=
|
||||
rg.townAreas.size()) {
|
||||
s_ownVersion = rg.version;
|
||||
s_ownAreaCount =
|
||||
rg.townAreas.size();
|
||||
s_ownLots.assign(
|
||||
rg.townAreas.size(), 0);
|
||||
s_ownContested.assign(
|
||||
rg.townAreas.size(), 0);
|
||||
std::vector<RoadNetworkGen::
|
||||
RoadLot>
|
||||
lots = RoadNetworkGen::
|
||||
findLots(rg);
|
||||
for (const auto &lot : lots) {
|
||||
int own = RoadNetworkGen::
|
||||
ownerAreaIndex(
|
||||
rg.townAreas,
|
||||
lot.centroid
|
||||
.x,
|
||||
lot.centroid
|
||||
.z);
|
||||
if (own < 0)
|
||||
continue;
|
||||
s_ownLots[own]++;
|
||||
int cont = 0;
|
||||
for (const auto &a :
|
||||
rg.townAreas)
|
||||
if (a.contains(
|
||||
lot.centroid
|
||||
.x,
|
||||
lot.centroid
|
||||
.z))
|
||||
++cont;
|
||||
if (cont >= 2)
|
||||
s_ownContested
|
||||
[own]++;
|
||||
}
|
||||
}
|
||||
for (size_t ai = 0;
|
||||
ai < rg.townAreas.size(); ++ai) {
|
||||
const RoadTownArea &a =
|
||||
rg.townAreas[ai];
|
||||
Ogre::ColourValue c =
|
||||
RoadNetworkGen::
|
||||
areaPreviewColor(
|
||||
a.name);
|
||||
/* Unique ID scope per legend row:
|
||||
* without it every row's color
|
||||
* button shares "##owncolor" and
|
||||
* ImGui flags an ID conflict. */
|
||||
ImGui::PushID((int)ai);
|
||||
ImGui::ColorButton(
|
||||
"##owncolor",
|
||||
ImVec4(c.r, c.g, c.b,
|
||||
1.0f),
|
||||
ImGuiColorEditFlags_NoTooltip,
|
||||
ImVec2(16, 16));
|
||||
ImGui::SameLine();
|
||||
ImGui::Text(
|
||||
"%s: %d lots (%d contested)",
|
||||
a.name.c_str(),
|
||||
s_ownLots[ai],
|
||||
s_ownContested[ai]);
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
static int s_areaPick = -1;
|
||||
const char *shapes[] = { "Square", "Circle" };
|
||||
int removeArea = -1;
|
||||
@@ -2680,8 +2859,9 @@ private:
|
||||
float rowWidth = ImGui::GetContentRegionAvail().x;
|
||||
if (showConnect)
|
||||
rowWidth -= ImGui::CalcTextSize("Connect").x +
|
||||
ImGui::GetStyle().FramePadding.x * 2.0f +
|
||||
ImGui::GetStyle().ItemSpacing.x;
|
||||
ImGui::CalcTextSize("Sel. Path").x +
|
||||
(ImGui::GetStyle().FramePadding.x * 2.0f +
|
||||
ImGui::GetStyle().ItemSpacing.x) * 2.0f;
|
||||
ImGui::PushID(n.id);
|
||||
if (ImGui::Selectable(label.c_str(), selected, 0,
|
||||
ImVec2(std::max(20.0f, rowWidth),
|
||||
@@ -2711,6 +2891,16 @@ private:
|
||||
"Road Connect Failed");
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Sel. Path"))
|
||||
rs->selectPathBetweenNodes(selectedNodeId,
|
||||
n.id);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Select the edges of the shortest "
|
||||
"path between node %d and this "
|
||||
"node (batch editing).",
|
||||
selectedNodeId);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -2718,11 +2908,32 @@ private:
|
||||
/* Edge list. Same sizing rule as the node list: keep the
|
||||
* selectable clear of the row buttons. Every row has a Split
|
||||
* button (splitting does not require selecting the edge
|
||||
* first); Remove only appears on the selected row. */
|
||||
* first); Remove only appears on the primary selected row.
|
||||
* Ctrl+click toggles the edge in the multi-selection used by
|
||||
* the batch panel below; plain click keeps single-select. */
|
||||
ImGui::Text("Edges: %zu", rg.edges.size());
|
||||
{
|
||||
const std::set<int> &sel = rs->getSelectedEdgeIndices();
|
||||
if (!sel.empty()) {
|
||||
ImGui::Text("Selected: %zu", sel.size());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Select Connected"))
|
||||
rs->selectConnectedComponent(
|
||||
rs->getSelectedEdgeIndex());
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Extend the selection to every edge "
|
||||
"reachable from the primary edge via "
|
||||
"shared nodes.");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Clear Selection"))
|
||||
rs->clearEdgeSelection();
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < rg.edges.size(); ++i) {
|
||||
const auto &e = rg.edges[i];
|
||||
bool selected = ((int)i == rs->getSelectedEdgeIndex());
|
||||
bool inSelection = rs->isEdgeSelected((int)i);
|
||||
bool primary = ((int)i == rs->getSelectedEdgeIndex());
|
||||
std::string label = "Edge " + std::to_string(i) + ": " +
|
||||
std::to_string(e.nodeA) + " <-> " +
|
||||
std::to_string(e.nodeB);
|
||||
@@ -2736,16 +2947,21 @@ private:
|
||||
rowWidth -= ImGui::CalcTextSize("Split").x +
|
||||
ImGui::GetStyle().FramePadding.x * 2.0f +
|
||||
ImGui::GetStyle().ItemSpacing.x;
|
||||
if (selected)
|
||||
if (primary)
|
||||
rowWidth -= ImGui::CalcTextSize("Remove").x +
|
||||
ImGui::GetStyle().FramePadding.x * 2.0f +
|
||||
ImGui::GetStyle().ItemSpacing.x;
|
||||
ImGui::PushID((int)i);
|
||||
if (ImGui::Selectable(label.c_str(), selected, 0,
|
||||
if (ImGui::Selectable(label.c_str(), inSelection, 0,
|
||||
ImVec2(std::max(20.0f, rowWidth),
|
||||
0))) {
|
||||
rs->setSelectedEdgeIndex((int)i);
|
||||
rs->setSelectedNodeId(-1);
|
||||
if (ImGui::GetIO().KeyCtrl) {
|
||||
rs->setSelectedNodeId(-1);
|
||||
rs->toggleEdgeSelected((int)i);
|
||||
} else {
|
||||
rs->setSelectedEdgeIndex((int)i);
|
||||
rs->setSelectedNodeId(-1);
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(eGrouped);
|
||||
@@ -2763,7 +2979,7 @@ private:
|
||||
rs->setSelectedNodeId(newId);
|
||||
rs->setSelectedEdgeIndex(-1);
|
||||
}
|
||||
if (selected) {
|
||||
if (primary) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Remove")) {
|
||||
rg.removeEdge(i);
|
||||
@@ -2781,6 +2997,87 @@ private:
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
/* Batch edit (phase 5.9 follow-up): applies lanes/sidewalk
|
||||
* modes to every edge in the multi-selection. Each field
|
||||
* has a "keep" choice that leaves that property untouched. */
|
||||
if (!rs->getSelectedEdgeIndices().empty()) {
|
||||
ImGui::SeparatorText("Batch Edit Selection");
|
||||
static int s_batchLanesAtoB = -1; /* -1 = keep */
|
||||
static int s_batchLanesBtoA = -1;
|
||||
static int s_batchSidewalkLeft = 0; /* 0 keep,1 inherit,
|
||||
2 on, 3 off */
|
||||
static int s_batchSidewalkRight = 0;
|
||||
/* Field edits only stage the batch; nothing touches the
|
||||
* graph until Apply. */
|
||||
ImGui::SliderInt(
|
||||
"Lanes A->B##batch", &s_batchLanesAtoB, -1, 8,
|
||||
s_batchLanesAtoB < 0 ? "keep" : "%d");
|
||||
ImGui::SliderInt(
|
||||
"Lanes B->A##batch", &s_batchLanesBtoA, -1, 8,
|
||||
s_batchLanesBtoA < 0 ? "keep" : "%d");
|
||||
const char *keepModes[4] = { "keep", "Inherit", "On",
|
||||
"Off" };
|
||||
ImGui::Combo("Sidewalk Left##batch",
|
||||
&s_batchSidewalkLeft,
|
||||
keepModes, 4);
|
||||
ImGui::Combo("Sidewalk Right##batch",
|
||||
&s_batchSidewalkRight,
|
||||
keepModes, 4);
|
||||
bool canApply = s_batchLanesAtoB >= 0 ||
|
||||
s_batchLanesBtoA >= 0 ||
|
||||
s_batchSidewalkLeft > 0 ||
|
||||
s_batchSidewalkRight > 0;
|
||||
ImGui::BeginDisabled(!canApply);
|
||||
if (ImGui::Button("Apply to Selected Edges")) {
|
||||
bool any = false;
|
||||
for (int ei : rs->getSelectedEdgeIndices()) {
|
||||
if (ei < 0 ||
|
||||
(size_t)ei >= rg.edges.size())
|
||||
continue;
|
||||
RoadEdge &e = rg.edges[(size_t)ei];
|
||||
if (s_batchLanesAtoB >= 0) {
|
||||
e.lanesAtoB = s_batchLanesAtoB;
|
||||
any = true;
|
||||
}
|
||||
if (s_batchLanesBtoA >= 0) {
|
||||
e.lanesBtoA = s_batchLanesBtoA;
|
||||
any = true;
|
||||
}
|
||||
if (s_batchSidewalkLeft > 0) {
|
||||
e.sidewalkLeftMode =
|
||||
(RoadSidewalkMode)(
|
||||
s_batchSidewalkLeft -
|
||||
1);
|
||||
any = true;
|
||||
}
|
||||
if (s_batchSidewalkRight > 0) {
|
||||
e.sidewalkRightMode =
|
||||
(RoadSidewalkMode)(
|
||||
s_batchSidewalkRight -
|
||||
1);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
if (any)
|
||||
rg.bumpVersion();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered(
|
||||
ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
ImGui::SetTooltip(
|
||||
"Apply the non-\"keep\" fields to all "
|
||||
"%zu selected edges. Ctrl+click edges in "
|
||||
"the list or in the 3D view to extend "
|
||||
"the selection.",
|
||||
rs->getSelectedEdgeIndices().size());
|
||||
if (ImGui::SmallButton("Reset Batch Fields")) {
|
||||
s_batchLanesAtoB = -1;
|
||||
s_batchLanesBtoA = -1;
|
||||
s_batchSidewalkLeft = 0;
|
||||
s_batchSidewalkRight = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Connect failures (from the 3D Connect tool via RoadSystem,
|
||||
* or the node-list Connect buttons above) are shown as a
|
||||
* modal. Placed after the lists so OpenPopup calls from the
|
||||
|
||||
Reference in New Issue
Block a user