gap: you walk and drive straight through a level crossing #234

Closed
opened 2026-08-30 10:27:45 +00:00 by viberfox-agent · 8 comments
Collaborator

Problem

A level crossing's masts are drawn but are solid to nothing. The avatar walks
through the mast, the lamp head and the standing boom.

The placement path ends at the mesh. map_geometry orients the register's points,
counts them and extends the mesh list — and that is all it does with them
(crates/cartopolis/src/systems/map/map_geometry.rs:740-754). The two things that
reach the collider index are CellPlaces::furniture and CellPlaces::vehicles
(crates/cartopolis/src/systems/map/world_places.rs:27-33); insert_cell builds
its props list from exactly those two
(crates/cartopolis/src/systems/map/world_places.rs:301-306, 342-344), props()
hands that list out (world_places.rs:388-390) and
collision::sync_prop_colliders copies it into BuildingColliders
(crates/cartopolis/src/systems/player/collision.rs:423-440). A crossing is a
LevelCrossing, never a FurnitureInstance, so it enters none of that. Both the
module note and the standing note say so outright, as a decision:
crates/geo/src/crossings.rs:55-58 and
docs/notes/prorail-level-crossings.md:225-227.

The precedent those two cite does not carry to this layer. prop_radius gives a
moored boat 0 for a stated reason — "a collider there is an invisible wall in the
middle of a canal" (world_places.rs:238-245) — and a lamp column 0.25 for the
opposite one: "genuinely in the way … one of those small things that tells you the
world is a drawing" (world_places.rs:231-233). A crossing mast is 0.22 m square
and 3.1 m tall (MAST_HALF_W, CrossingKind::spec —
crates/geo/src/crossings.rs:109, 220-225) and it stands on a verge beside a
street, VERGE_M = 0.5 m outside the kerb (crossings.rs:104-107). It is the lamp
case, not the buoy case.

Two corrections to the issue as filed, both from the current tree:

  • The boom is no longer drawn lowered. 0a9836d stood it up; the bands now
    stack in y from BOOM_Y = 1.05 m upward at a fixed position beside the mast
    (crossings.rs:434-453), pinned by
    a_raised_boom_stands_clear_of_the_road (crossings.rs:764-800). So this is not
    "walking through a closed barrier"; it is walking through a mast, its lamp head
    and the raised bar's vertical column.
  • Ambient traffic will not be fixed by this. systems/nav/traffic.rs contains
    no reference to BuildingColliders or to WorldPlaces at all — vehicles follow
    DriveLines and consult no collider, prop or building. Making crossings solid
    changes what the avatar can walk through and nothing about the cars.

Where the masts actually stand is already computed, once, inside push_crossing:
mast_x = road_half + VERGE_M across the road, ±clearance_m(tracks) along it,
rotated into tile-local metres by push_box's place closure
(crossings.rs:390-415, 377-382,
crates/geo/src/furniture.rs:1142-1152). Nothing outside the mesh builder can ask
for those positions today.

Approach

Give the crossings a bare collider channel — the second option the issue names
— rather than making them FurnitureInstances. A furniture form would need a
FurnitureKind variant with a mesh recipe nothing uses (the crossings have their
own builder), plus arms in prop_name/prop_interactable/prop_radius
(world_places.rs:176-247) that exist only to say "never reached". Publishing
through BuildingColliders::set_feature_cell is also wrong: street_detail owns
that key space and uses the same z14 grid (street_detail.rs:74,
vector_tiles.rs:260), so two producers would clobber each other's cells.

  1. crates/geo/src/crossings.rs — lift the per-installation placement out of
    push_crossing so the mesh and the collider read one set of numbers, and expose
    the result: a public function returning the two mast centres of one
    LevelCrossing in tile-local metres (the frame LevelCrossing::x/z is in),
    derived from the same road_half.unwrap_or(fallback), VERGE_M and
    clearance_m as the boxes, and rotated by the same object→tile mapping
    push_box uses (furniture.rs:1151). push_crossing must call the shared
    helper rather than keeping a second copy of the arithmetic — a duplicated
    expression here is a collider that drifts from its mast on the next edit to
    either.
  2. crates/cartopolis/src/systems/map/world_places.rs — add a third list to
    CellPlaces, e.g. pub colliders: Vec<(Vec2, f32)> (tile-local centre, radius
    in metres), documented as solid and silent: it never becomes an
    Interactable, for the reason a lamp gets no prompt
    (world_places.rs:203-213). insert_cell converts each entry to the anchored
    world frame at the cell origin and pushes it into props, beside the furniture
    and vehicle circles.
  3. crates/cartopolis/src/systems/map/map_geometry.rs:729-754 — after
    place_crossings, extend cell_places.colliders with one circle per mast at
    radius 0.25 m, the lamp column's. Name the radius as a constant next to
    prop_radius in world_places.rs, so every collider radius in the client stays
    in one file. The comment block at map_geometry.rs:715-733 currently states "no
    collider" for the marks and the crossings in one breath — the marks keep that
    answer (they stand on water); the crossings' half has to be rewritten to say why
    they no longer share it.
  4. Instrument it. The collider index is invisible to a capture, which is why
    this gap survived a settled shot with level_crossings = 3 on screen. Add one
    field to ShotMetrics — a count of WorldPlaces::props() circles — beside
    level_crossings (crates/cartopolis/src/systems/dev/shot_harness.rs:651,
    2659-2665, CSV header at 846), which is the project's stated way to make a
    new fact inspectable in all three sinks at once (CLAUDE.md, "Sweeps and
    metrics"). This is the only part of the change a headless run can assert; drop
    it if you disagree, and the change is then unit-test-only.
  5. Docs. docs/notes/prorail-level-crossings.md:225-227 and the module note at
    crates/geo/src/crossings.rs:55-58 both record "no colliders" as a standing
    decision. Both stop being true and are edited in place (the format rule in
    docs/notes/README.md), keeping the navigation marks' own answer intact and
    saying what separates the two cases.

Acceptance criteria

  • cartopolis_geo exposes the mast centres of a LevelCrossing in tile-local
    metres, and push_crossing places its mast boxes from the same helper — no
    second copy of road_half + VERGE_M / clearance_m / the rotation.
  • A geo test reads the mast positions back off the built mesh for a
    crossing at yaw != 0 and asserts the helper agrees, in the style of the
    existing across helper (crates/geo/src/crossings.rs:518-543). A test that
    only checks yaw 0 cannot catch a rotation applied the wrong way round, which
    is the failure mode this module has already paid for twice (the door-normal
    lesson in CLAUDE.md).
  • All four CrossingKinds produce two mast colliders — Lights included; it
    has masts and lamp heads (crossings.rs:408-426).
  • CellPlaces carries the bare colliders; WorldPlaces::insert_cell converts
    them to the anchored world frame at the cell origin and includes them in
    props().
  • A crossing mast is never an Interactable: WorldPlaces::nearest at a
    mast returns None for a cell whose only content is crossing colliders.
  • Retiring the cell removes them, on the same path as everything else
    (world_places.rs:353-361) — a client test in the shape of
    retiring_a_cell_removes_its_places (world_places.rs:484-506).
  • Turning the Furniture layer off removes them from the collider index, with no
    new gate written: crossings are only placed under wants.furniture
    (map_geometry.rs:628, 755) and sync_prop_colliders already clears props
    when the layer is off (collision.rs:429-439). Confirm this holds rather
    than adding a second check.
  • The radius is 0.25 m, named as a constant beside prop_radius, with the
    lamp-column reasoning cited.
  • crates/geo/src/crossings.rs's module note and
    docs/notes/prorail-level-crossings.md no longer claim crossings get no
    collider, and both state what makes a mast different from a buoy.
  • If item 4 is taken: the new count appears in the shot state line, the
    --csv header and --dump-state, and is assertable with --expect.

Verification

Read-only refinement pass; nothing below was run here.

cargo test -p cartopolis_geo crossings          # placement helper vs meshed geometry
cargo test -p cartopolis world_places           # solid, silent, retired with the cell
cargo fmt --check -p cartopolis_geo -p cartopolis   # the gate is the owned crates

Headless, on a machine that can render (per CLAUDE.md the --shot harness runs
windowless on lavapipe; geometry, placement and visibility are trustworthy there,
colour and lighting are not):

# over the Peizerweg cell, the one z14 cell the note measures at 1 crossing
# (docs/notes/prorail-level-crossings.md:32, cell 8490/5321 — its north-west
# corner is lat 53.2102, crates/cartopolis/src/systems/map/crossings.rs:436)
cargo run -p cartopolis -- --shot /tmp/xing.png --at <lat>,<lng>,60 --look=-10,0 \
    --dump-state /tmp/xing.json --expect 'level_crossings>=1'

Only a walk-up can show that the mast now stops the avatar; that is a workstation
check, not one this container or a still frame can make. With item 4 in place the
capture can at least assert the circles exist (--expect '<prop_count>>0') against
level_crossings, which is the pair that separates "drawn" from "solid".

Not measured here, and not mine to measure: how many collider circles a dense
cell gains. MAX_CROSSINGS_PER_CELL is 200 (crossings.rs:69) so the worst case
is 400 circles per cell, but the densest cell the register actually holds is 8
(same doc comment), i.e. 16 — negligible against the linear scan in
BuildingColliders::in_prop (collision.rs:248-254).

Out of scope

  • Ambient traffic. Cars do not consult the collider index at all
    (systems/nav/traffic.rs references neither BuildingColliders nor
    WorldPlaces), so they will still drive through a crossing. That is a separate
    gap and needs its own ticket if it is wanted.
  • The navigation marks. They keep their zero, for the reason a moored boat has
    one. Only the crossings' sentence changes.
  • A separate collider for the raised bar. One 0.25 m circle on the mast centre
    leaves the bar's inner face uncovered: the bar sits MAST_HALF_W + BOOM_HALF_FACE
    = 0.24 m inboard of the mast centre and is 0.13 m wide either side
    (crossings.rs:109-118, 442), so its inner face is 0.37 m in. Widening or
    re-centring the circle to swallow that 0.12 m was considered and left: the issue
    names the lamp's radius at the mast, and the colliders are height-free circles
    while the bar starts 1.05 m off the ground.
  • Boom animation, the St Andrew's cross, a Layers toggle of their own — all
    three are recorded refusals in docs/notes/prorail-level-crossings.md:219-229
    and none is touched.
  • Polygon colliders. Props are circles by design (world_places.rs:215-218).

Open questions

None.


Branch: fix/234-crossing-mast-colliders

Original request

What I did

Walked the placement path from the register to the collider index, after the settled
Peizerweg capture confirmed the installations are on screen (level_crossings = 3).

What happened

Nothing on a level crossing is solid — not the mast, not the lamp head, not the boom.

Crossings are meshed into the furniture group but are never FurnitureInstances.
map_geometry sets cell_places.surveyed.6 = crossings.len() and extends meshes, and
that is all; the surveyed lampposts, benches and bins go into cell_places.furniture,
which is what WorldPlaces::props() and collision::sync_prop_colliders read. So a
crossing reaches the collider index by no route at all.

Combined with the boom being drawn lowered (filed separately), the avatar and the
streamed traffic drive through a closed barrier at chest height.

What a user would expect

The codebase already argues this case, about the lamppost that may be standing ten metres
from the crossing — world_places::prop_radius:

A lamp column is slim and genuinely in the way — walking through one is one of those
small things that tells you the world is a drawing.

A crossing mast is that object: 0.22 m square, 3.1 m tall, beside a street the avatar
walks along.

Why the cited precedent does not carry

crates/geo/src/crossings.rs says crossings get no collider "exactly as the navigation
marks do not". The marks have that answer for a reason that is specific to them: a buoy
stands on water the avatar cannot walk on — which is the same reason prop_radius gives
a moored boat a radius of 0, in as many words ("a collider there is an invisible wall
in the middle of a canal"). A level crossing stands on a road. The rule the marks are
following says the opposite here.

Where the seam is

crates/cartopolis/src/systems/map/map_geometry.rs, where the crossings are placed and
meshed — they would need to publish their masts as props, or CellPlaces needs a second
list of bare colliders. world_places::prop_radius for the radius: 0.25 m, the same as
the lamp column, and prop_interactable should answer false for the same reason a lamp
does.

Filed from the QA pass on #216 (#230).

🤖 Refined by the viberfox issue agent. Reply with @agent refine and what is wrong to have this rewritten.

## Problem A level crossing's masts are drawn but are solid to nothing. The avatar walks through the mast, the lamp head and the standing boom. The placement path ends at the mesh. `map_geometry` orients the register's points, counts them and extends the mesh list — and that is all it does with them (`crates/cartopolis/src/systems/map/map_geometry.rs:740-754`). The two things that reach the collider index are `CellPlaces::furniture` and `CellPlaces::vehicles` (`crates/cartopolis/src/systems/map/world_places.rs:27-33`); `insert_cell` builds its `props` list from exactly those two (`crates/cartopolis/src/systems/map/world_places.rs:301-306`, `342-344`), `props()` hands that list out (`world_places.rs:388-390`) and `collision::sync_prop_colliders` copies it into `BuildingColliders` (`crates/cartopolis/src/systems/player/collision.rs:423-440`). A crossing is a `LevelCrossing`, never a `FurnitureInstance`, so it enters none of that. Both the module note and the standing note say so outright, as a decision: `crates/geo/src/crossings.rs:55-58` and `docs/notes/prorail-level-crossings.md:225-227`. The precedent those two cite does not carry to this layer. `prop_radius` gives a moored boat 0 for a stated reason — "a collider there is an invisible wall in the middle of a canal" (`world_places.rs:238-245`) — and a lamp column 0.25 for the opposite one: "genuinely in the way … one of those small things that tells you the world is a drawing" (`world_places.rs:231-233`). A crossing mast is 0.22 m square and 3.1 m tall (`MAST_HALF_W`, `CrossingKind::spec` — `crates/geo/src/crossings.rs:109`, `220-225`) and it stands on a verge beside a street, `VERGE_M` = 0.5 m outside the kerb (`crossings.rs:104-107`). It is the lamp case, not the buoy case. Two corrections to the issue as filed, both from the current tree: * **The boom is no longer drawn lowered.** `0a9836d` stood it up; the bands now stack in y from `BOOM_Y` = 1.05 m upward at a fixed position beside the mast (`crossings.rs:434-453`), pinned by `a_raised_boom_stands_clear_of_the_road` (`crossings.rs:764-800`). So this is not "walking through a closed barrier"; it is walking through a mast, its lamp head and the raised bar's vertical column. * **Ambient traffic will not be fixed by this.** `systems/nav/traffic.rs` contains no reference to `BuildingColliders` or to `WorldPlaces` at all — vehicles follow `DriveLine`s and consult no collider, prop or building. Making crossings solid changes what the avatar can walk through and nothing about the cars. Where the masts actually stand is already computed, once, inside `push_crossing`: `mast_x = road_half + VERGE_M` across the road, `±clearance_m(tracks)` along it, rotated into tile-local metres by `push_box`'s `place` closure (`crossings.rs:390-415`, `377-382`, `crates/geo/src/furniture.rs:1142-1152`). Nothing outside the mesh builder can ask for those positions today. ## Approach Give the crossings a **bare collider channel** — the second option the issue names — rather than making them `FurnitureInstance`s. A furniture form would need a `FurnitureKind` variant with a mesh recipe nothing uses (the crossings have their own builder), plus arms in `prop_name`/`prop_interactable`/`prop_radius` (`world_places.rs:176-247`) that exist only to say "never reached". Publishing through `BuildingColliders::set_feature_cell` is also wrong: `street_detail` owns that key space and uses the same z14 grid (`street_detail.rs:74`, `vector_tiles.rs:260`), so two producers would clobber each other's cells. 1. **`crates/geo/src/crossings.rs`** — lift the per-installation placement out of `push_crossing` so the mesh and the collider read one set of numbers, and expose the result: a public function returning the two mast centres of one `LevelCrossing` in **tile-local metres** (the frame `LevelCrossing::x/z` is in), derived from the same `road_half.unwrap_or(fallback)`, `VERGE_M` and `clearance_m` as the boxes, and rotated by the same object→tile mapping `push_box` uses (`furniture.rs:1151`). `push_crossing` must call the shared helper rather than keeping a second copy of the arithmetic — a duplicated expression here is a collider that drifts from its mast on the next edit to either. 2. **`crates/cartopolis/src/systems/map/world_places.rs`** — add a third list to `CellPlaces`, e.g. `pub colliders: Vec<(Vec2, f32)>` (tile-local centre, radius in metres), documented as *solid and silent*: it never becomes an `Interactable`, for the reason a lamp gets no prompt (`world_places.rs:203-213`). `insert_cell` converts each entry to the anchored world frame at the cell origin and pushes it into `props`, beside the furniture and vehicle circles. 3. **`crates/cartopolis/src/systems/map/map_geometry.rs:729-754`** — after `place_crossings`, extend `cell_places.colliders` with one circle per mast at radius **0.25 m**, the lamp column's. Name the radius as a constant next to `prop_radius` in `world_places.rs`, so every collider radius in the client stays in one file. The comment block at `map_geometry.rs:715-733` currently states "no collider" for the marks *and* the crossings in one breath — the marks keep that answer (they stand on water); the crossings' half has to be rewritten to say why they no longer share it. 4. **Instrument it.** The collider index is invisible to a capture, which is why this gap survived a settled shot with `level_crossings = 3` on screen. Add one field to `ShotMetrics` — a count of `WorldPlaces::props()` circles — beside `level_crossings` (`crates/cartopolis/src/systems/dev/shot_harness.rs:651`, `2659-2665`, CSV header at `846`), which is the project's stated way to make a new fact inspectable in all three sinks at once (CLAUDE.md, "Sweeps and metrics"). This is the only part of the change a headless run can assert; drop it if you disagree, and the change is then unit-test-only. 5. **Docs.** `docs/notes/prorail-level-crossings.md:225-227` and the module note at `crates/geo/src/crossings.rs:55-58` both record "no colliders" as a standing decision. Both stop being true and are edited in place (the format rule in `docs/notes/README.md`), keeping the navigation marks' own answer intact and saying what separates the two cases. ## Acceptance criteria - [ ] `cartopolis_geo` exposes the mast centres of a `LevelCrossing` in tile-local metres, and `push_crossing` places its mast boxes from the same helper — no second copy of `road_half + VERGE_M` / `clearance_m` / the rotation. - [ ] A geo test reads the mast positions back **off the built mesh** for a crossing at `yaw != 0` and asserts the helper agrees, in the style of the existing `across` helper (`crates/geo/src/crossings.rs:518-543`). A test that only checks yaw 0 cannot catch a rotation applied the wrong way round, which is the failure mode this module has already paid for twice (the door-normal lesson in CLAUDE.md). - [ ] All four `CrossingKind`s produce two mast colliders — `Lights` included; it has masts and lamp heads (`crossings.rs:408-426`). - [ ] `CellPlaces` carries the bare colliders; `WorldPlaces::insert_cell` converts them to the anchored world frame at the cell origin and includes them in `props()`. - [ ] A crossing mast is **never** an `Interactable`: `WorldPlaces::nearest` at a mast returns `None` for a cell whose only content is crossing colliders. - [ ] Retiring the cell removes them, on the same path as everything else (`world_places.rs:353-361`) — a client test in the shape of `retiring_a_cell_removes_its_places` (`world_places.rs:484-506`). - [ ] Turning the Furniture layer off removes them from the collider index, with no new gate written: crossings are only placed under `wants.furniture` (`map_geometry.rs:628`, `755`) and `sync_prop_colliders` already clears props when the layer is off (`collision.rs:429-439`). Confirm this holds rather than adding a second check. - [ ] The radius is 0.25 m, named as a constant beside `prop_radius`, with the lamp-column reasoning cited. - [ ] `crates/geo/src/crossings.rs`'s module note and `docs/notes/prorail-level-crossings.md` no longer claim crossings get no collider, and both state what makes a mast different from a buoy. - [ ] If item 4 is taken: the new count appears in the `shot state` line, the `--csv` header and `--dump-state`, and is assertable with `--expect`. ## Verification Read-only refinement pass; nothing below was run here. ```bash cargo test -p cartopolis_geo crossings # placement helper vs meshed geometry cargo test -p cartopolis world_places # solid, silent, retired with the cell cargo fmt --check -p cartopolis_geo -p cartopolis # the gate is the owned crates ``` Headless, on a machine that can render (per CLAUDE.md the `--shot` harness runs windowless on lavapipe; geometry, placement and visibility are trustworthy there, colour and lighting are not): ```bash # over the Peizerweg cell, the one z14 cell the note measures at 1 crossing # (docs/notes/prorail-level-crossings.md:32, cell 8490/5321 — its north-west # corner is lat 53.2102, crates/cartopolis/src/systems/map/crossings.rs:436) cargo run -p cartopolis -- --shot /tmp/xing.png --at <lat>,<lng>,60 --look=-10,0 \ --dump-state /tmp/xing.json --expect 'level_crossings>=1' ``` Only a walk-up can show that the mast now stops the avatar; that is a workstation check, not one this container or a still frame can make. With item 4 in place the capture can at least assert the circles exist (`--expect '<prop_count>>0'`) against `level_crossings`, which is the pair that separates "drawn" from "solid". Not measured here, and not mine to measure: how many collider circles a dense cell gains. `MAX_CROSSINGS_PER_CELL` is 200 (`crossings.rs:69`) so the worst case is 400 circles per cell, but the densest cell the register actually holds is 8 (same doc comment), i.e. 16 — negligible against the linear scan in `BuildingColliders::in_prop` (`collision.rs:248-254`). ## Out of scope * **Ambient traffic.** Cars do not consult the collider index at all (`systems/nav/traffic.rs` references neither `BuildingColliders` nor `WorldPlaces`), so they will still drive through a crossing. That is a separate gap and needs its own ticket if it is wanted. * **The navigation marks.** They keep their zero, for the reason a moored boat has one. Only the crossings' sentence changes. * **A separate collider for the raised bar.** One 0.25 m circle on the mast centre leaves the bar's inner face uncovered: the bar sits `MAST_HALF_W + BOOM_HALF_FACE` = 0.24 m inboard of the mast centre and is 0.13 m wide either side (`crossings.rs:109-118`, `442`), so its inner face is 0.37 m in. Widening or re-centring the circle to swallow that 0.12 m was considered and left: the issue names the lamp's radius at the mast, and the colliders are height-free circles while the bar starts 1.05 m off the ground. * **Boom animation, the St Andrew's cross, a Layers toggle of their own** — all three are recorded refusals in `docs/notes/prorail-level-crossings.md:219-229` and none is touched. * **Polygon colliders.** Props are circles by design (`world_places.rs:215-218`). ## Open questions None. --- Branch: `fix/234-crossing-mast-colliders` <details><summary>Original request</summary> ## What I did Walked the placement path from the register to the collider index, after the settled Peizerweg capture confirmed the installations are on screen (`level_crossings = 3`). ## What happened Nothing on a level crossing is solid — not the mast, not the lamp head, not the boom. Crossings are meshed into the furniture *group* but are never `FurnitureInstance`s. `map_geometry` sets `cell_places.surveyed.6 = crossings.len()` and extends `meshes`, and that is all; the surveyed lampposts, benches and bins go into `cell_places.furniture`, which is what `WorldPlaces::props()` and `collision::sync_prop_colliders` read. So a crossing reaches the collider index by no route at all. Combined with the boom being drawn lowered (filed separately), the avatar and the streamed traffic drive through a closed barrier at chest height. ## What a user would expect The codebase already argues this case, about the lamppost that may be standing ten metres from the crossing — `world_places::prop_radius`: > A lamp column is slim and genuinely in the way — walking through one is one of those > small things that tells you the world is a drawing. A crossing mast is that object: 0.22 m square, 3.1 m tall, beside a street the avatar walks along. ## Why the cited precedent does not carry `crates/geo/src/crossings.rs` says crossings get no collider "exactly as the navigation marks do not". The marks have that answer for a reason that is specific to them: a buoy stands on water the avatar cannot walk on — which is the same reason `prop_radius` gives a moored boat a radius of **0**, in as many words ("a collider there is an invisible wall in the middle of a canal"). A level crossing stands on a road. The rule the marks are following says the opposite here. ## Where the seam is `crates/cartopolis/src/systems/map/map_geometry.rs`, where the crossings are placed and meshed — they would need to publish their masts as props, or `CellPlaces` needs a second list of bare colliders. `world_places::prop_radius` for the radius: 0.25 m, the same as the lamp column, and `prop_interactable` should answer `false` for the same reason a lamp does. <sub>Filed from the QA pass on #216 (#230).</sub> </details> <sub>🤖 Refined by the viberfox issue agent. Reply with **@agent refine** and what is wrong to have this rewritten.</sub>
Author
Collaborator

🤖 Promoted into the build lane by the 7-day retrospective (#279) — autonomous + ship.

Why this one.

  • A machine can judge it exactly. A mast either reaches
    collision::sync_prop_colliders or it does not; the answer is a count, not a picture
    — value 6.
  • The precedent is in the tree, and the ticket disposes of the counter-precedent
    correctly.
    crossings.rs says a crossing gets no collider "exactly as the navigation
    marks do not", and the marks have that rule because a buoy stands on water nobody can
    walk on — the same reason prop_radius gives a moored boat a radius of 0. A crossing
    mast stands beside a street the avatar walks along, which is the case prop_radius
    already argues for the lamp column in as many words.
  • #231 has landed since this was filed, so the remaining defect is sharper than the
    ticket says.
    The booms now stand up. What is left is that you walk and drive straight
    through a 3.1 m steel post.

Bounds. crates/cartopolis/src/systems/map/map_geometry.rs where the crossings are
placed, and world_places::prop_radius / prop_interactable. Masts only — not the
boom, which moves, and not the lamp head. Not interactable, for the reason #259 is
open: a prop that is solid is not thereby a prop worth pressing, and there is nothing to
do with a level crossing.

One caution. #268 was promoted in the same pass and also edits map_geometry.rs, in
stand_on_terrain rather than in the placement. Whichever of the two is picked up second
must be cut from a main that already holds the first — two branches from one parent,
each green alone, is what put main in the red on 9e5ed895 last week (#276).

🤖 **Promoted into the build lane** by the 7-day retrospective (#279) — `autonomous` + `ship`. **Why this one.** - **A machine can judge it exactly.** A mast either reaches `collision::sync_prop_colliders` or it does not; the answer is a count, not a picture — value 6. - **The precedent is in the tree, and the ticket disposes of the counter-precedent correctly.** `crossings.rs` says a crossing gets no collider "exactly as the navigation marks do not", and the marks have that rule because a buoy stands on water nobody can walk on — the same reason `prop_radius` gives a moored boat a radius of 0. A crossing mast stands beside a street the avatar walks along, which is the case `prop_radius` already argues for the lamp column in as many words. - **#231 has landed since this was filed, so the remaining defect is sharper than the ticket says.** The booms now stand up. What is left is that you walk and drive straight through a 3.1 m steel post. **Bounds.** `crates/cartopolis/src/systems/map/map_geometry.rs` where the crossings are placed, and `world_places::prop_radius` / `prop_interactable`. **Masts only** — not the boom, which moves, and not the lamp head. **Not interactable**, for the reason #259 is open: a prop that is solid is not thereby a prop worth pressing, and there is nothing to do with a level crossing. **One caution.** #268 was promoted in the same pass and also edits `map_geometry.rs`, in `stand_on_terrain` rather than in the placement. Whichever of the two is picked up second must be cut from a `main` that already holds the first — two branches from one parent, each green alone, is what put `main` in the red on `9e5ed895` last week (#276).
Author
Collaborator

🤖 Refining this before anything gets built — reading the code, no branch, no commits. If it turns out I'd have to guess at the requirements I'll stop and ask instead.

🤖 Refining this before anything gets built — reading the code, no branch, no commits. If it turns out I'd have to guess at the requirements I'll stop and ask instead.
Author
Collaborator

🤖 Refined. The description above is now the spec; I'll pick it up for implementation on the next poll.

If the direction is wrong, say so now — @agent refine with a correction rewrites it, and agent:skip stops me.


🤖 viberfox issue agent on the VPS — 33 turns · $2.84 · session 63e17760-59dc-4a6a-ac01-7fea5bf7d231. Reply with @agent to continue this session.

🤖 **Refined.** The description above is now the spec; I'll pick it up for implementation on the next poll. If the direction is wrong, say so now — **@agent refine** with a correction rewrites it, and `agent:skip` stops me. --- <sub>🤖 viberfox issue agent on the VPS — 33 turns · $2.84 · session `63e17760-59dc-4a6a-ac01-7fea5bf7d231`. Reply with **@agent** to continue this session.</sub>
Author
Collaborator

🤖 Picked this up — working on branch fix/234-crossing-mast-colliders in a fresh worktree on the VPS. I'll report back here when I'm done.

Label an issue agent:skip to keep me off it, or driven to keep it in a session you are driving.

🤖 Picked this up — working on branch `fix/234-crossing-mast-colliders` in a fresh worktree on the VPS. I'll report back here when I'm done. <sub>Label an issue `agent:skip` to keep me off it, or `driven` to keep it in a session you are driving.</sub>
Author
Collaborator

A level crossing's two masts are now solid: the avatar stops at the post instead of walking through it. Everything else on an installation stays walkable.

What changed

  • crates/geo/src/crossings.rs — added mast_centres, which returns a crossing's two mast positions in tile-local metres. The mesh builder push_crossing now places its boxes from the same Layout struct and through the same rotation helper, so there is one copy of road_half + VERGE_M, clearance_m and the object-to-tile turn, not two.
  • crates/geo/src/furniture.rs — pulled the object-to-world rotation out of push_box as place_ground so the collider and the box share it.
  • crates/cartopolis/.../world_places.rs — added CellPlaces::colliders, a list of plain circles that reach the collider index and can never become a walk-up prompt (a mast has nothing to do with, same as a lamppost). The radius is CROSSING_MAST_RADIUS = 0.25 m, sitting beside prop_radius with the lamp-column reasoning.
  • crates/cartopolis/.../map_geometry.rs — feeds one circle per mast into that list.
  • ShotMetrics::prop_colliders counts the collider circles, in the log line, the CSV and --dump-state, so a headless run can assert prop_colliders>0 beside level_crossings.
  • docs/notes/prorail-level-crossings.md and the module comment no longer say crossings get no collider; both now say what separates a mast (on a verge beside a street) from a buoy (on water nobody walks on).

Verified here: cargo test -p cartopolis_geo (247 passed) and cargo test -p cartopolis (832 passed), both clean, plus cargo fmt --check on the five owned crates. New tests: the mast centres read back off the built mesh at four different rotations agree with the helper (a rotation-0 test would pass with the sign flipped); all four crossing forms produce two circles; a mast is solid but never promptable; retiring a cell removes its circles.

Not verified: that the mast visibly stops you when you walk into it. That is a walk-up on a workstation — this container cannot render, and the collider index does not appear in a still frame.

One thing to decide: the raised bar's inner face is 0.37 m in from the mast centre and the circle reaches 0.25 m, so about 12 cm of bar is not covered. I left it that way — the bar starts 1.05 m off the ground and these colliders are height-free circles, so widening the circle would also block ground the bar is not on. Ambient traffic is untouched: cars consult no collider at all and still drive through a crossing.

Branch fix/234-crossing-mast-colliders (pushed)

b11c098 fix(geo): make a level crossing's masts solid

🤖 viberfox issue agent on the VPS — 49 turns · $5.24 · session dc0cd9da-73d3-49f0-b338-595b7d663d92. Reply with @agent to continue this session.

A level crossing's two masts are now solid: the avatar stops at the post instead of walking through it. Everything else on an installation stays walkable. **What changed** - `crates/geo/src/crossings.rs` — added `mast_centres`, which returns a crossing's two mast positions in tile-local metres. The mesh builder `push_crossing` now places its boxes from the same `Layout` struct and through the same rotation helper, so there is one copy of `road_half + VERGE_M`, `clearance_m` and the object-to-tile turn, not two. - `crates/geo/src/furniture.rs` — pulled the object-to-world rotation out of `push_box` as `place_ground` so the collider and the box share it. - `crates/cartopolis/.../world_places.rs` — added `CellPlaces::colliders`, a list of plain circles that reach the collider index and can never become a walk-up prompt (a mast has nothing to do with, same as a lamppost). The radius is `CROSSING_MAST_RADIUS` = 0.25 m, sitting beside `prop_radius` with the lamp-column reasoning. - `crates/cartopolis/.../map_geometry.rs` — feeds one circle per mast into that list. - `ShotMetrics::prop_colliders` counts the collider circles, in the log line, the CSV and `--dump-state`, so a headless run can assert `prop_colliders>0` beside `level_crossings`. - `docs/notes/prorail-level-crossings.md` and the module comment no longer say crossings get no collider; both now say what separates a mast (on a verge beside a street) from a buoy (on water nobody walks on). **Verified here:** `cargo test -p cartopolis_geo` (247 passed) and `cargo test -p cartopolis` (832 passed), both clean, plus `cargo fmt --check` on the five owned crates. New tests: the mast centres read back off the built mesh at four different rotations agree with the helper (a rotation-0 test would pass with the sign flipped); all four crossing forms produce two circles; a mast is solid but never promptable; retiring a cell removes its circles. **Not verified:** that the mast visibly stops you when you walk into it. That is a walk-up on a workstation — this container cannot render, and the collider index does not appear in a still frame. **One thing to decide:** the raised bar's inner face is 0.37 m in from the mast centre and the circle reaches 0.25 m, so about 12 cm of bar is not covered. I left it that way — the bar starts 1.05 m off the ground and these colliders are height-free circles, so widening the circle would also block ground the bar is not on. Ambient traffic is untouched: cars consult no collider at all and still drive through a crossing. **Branch** `fix/234-crossing-mast-colliders` (pushed) ``` b11c098 fix(geo): make a level crossing's masts solid ``` --- <sub>🤖 viberfox issue agent on the VPS — 49 turns · $5.24 · session `dc0cd9da-73d3-49f0-b338-595b7d663d92`. Reply with **@agent** to continue this session.</sub>
Author
Collaborator

🤖 Marked agent:done without a CI verdict on b11c098e — no workflow reported on this commit. The branch is waiting for review; check the run yourself before landing it.

🤖 Marked `agent:done` **without a CI verdict** on `b11c098e` — no workflow reported on this commit. The branch is waiting for review; check the run yourself before landing it.
viberfox-agent 2026-09-03 01:38:01 +00:00
Author
Collaborator

🤖 Merged into main as pull request #285 (b11c098e).

🤖 **Merged into `main`** as pull request #285 (`b11c098e`).
Author
Collaborator

🤖 Deployed to the public services.

main is at 6473ddb1
  waiting for CI on 6473ddb1
  success    wasm & android targets
  success    test cartopolis

dispatching the simulator
  run 704 started
  the simulator: success

verifying the running simulator (expecting protocol 21)
  serving protocol 21

dispatching the web client
  run 705 started
  the web client: success

deployed 6473ddb1
🤖 **Deployed to the public services.** ``` main is at 6473ddb1 waiting for CI on 6473ddb1 success wasm & android targets success test cartopolis dispatching the simulator run 704 started the simulator: success verifying the running simulator (expecting protocol 21) serving protocol 21 dispatching the web client run 705 started the web client: success deployed 6473ddb1 ```
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jeroen/cartopolis#234
No description provided.