gap: you walk and drive straight through a level crossing #234
Labels
No labels
agent
agent:ci
agent:done
agent:failed
agent:needs-input
agent:refined
agent:refining
agent:running
agent:shipped
agent:skip
autonomous
autopilot
driven
local
plan
proposal
qa
qa-gap
research
retro
ship
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jeroen/cartopolis#234
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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_geometryorients 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 thatreach the collider index are
CellPlaces::furnitureandCellPlaces::vehicles(
crates/cartopolis/src/systems/map/world_places.rs:27-33);insert_cellbuildsits
propslist 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) andcollision::sync_prop_colliderscopies it intoBuildingColliders(
crates/cartopolis/src/systems/player/collision.rs:423-440). A crossing is aLevelCrossing, never aFurnitureInstance, so it enters none of that. Both themodule note and the standing note say so outright, as a decision:
crates/geo/src/crossings.rs:55-58anddocs/notes/prorail-level-crossings.md:225-227.The precedent those two cite does not carry to this layer.
prop_radiusgives amoored 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 theopposite 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 squareand 3.1 m tall (
MAST_HALF_W,CrossingKind::spec—crates/geo/src/crossings.rs:109,220-225) and it stands on a verge beside astreet,
VERGE_M= 0.5 m outside the kerb (crossings.rs:104-107). It is the lampcase, not the buoy case.
Two corrections to the issue as filed, both from the current tree:
0a9836dstood it up; the bands nowstack in y from
BOOM_Y= 1.05 m upward at a fixed position beside the mast(
crossings.rs:434-453), pinned bya_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.
systems/nav/traffic.rscontainsno reference to
BuildingCollidersor toWorldPlacesat all — vehicles followDriveLines and consult no collider, prop or building. Making crossings solidchanges 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_Macross the road,±clearance_m(tracks)along it,rotated into tile-local metres by
push_box'splaceclosure(
crossings.rs:390-415,377-382,crates/geo/src/furniture.rs:1142-1152). Nothing outside the mesh builder can askfor 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 aFurnitureKindvariant with a mesh recipe nothing uses (the crossings have theirown builder), plus arms in
prop_name/prop_interactable/prop_radius(
world_places.rs:176-247) that exist only to say "never reached". Publishingthrough
BuildingColliders::set_feature_cellis also wrong:street_detailownsthat 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.crates/geo/src/crossings.rs— lift the per-installation placement out ofpush_crossingso the mesh and the collider read one set of numbers, and exposethe result: a public function returning the two mast centres of one
LevelCrossingin tile-local metres (the frameLevelCrossing::x/zis in),derived from the same
road_half.unwrap_or(fallback),VERGE_Mandclearance_mas the boxes, and rotated by the same object→tile mappingpush_boxuses (furniture.rs:1151).push_crossingmust call the sharedhelper 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.
crates/cartopolis/src/systems/map/world_places.rs— add a third list toCellPlaces, e.g.pub colliders: Vec<(Vec2, f32)>(tile-local centre, radiusin 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_cellconverts each entry to the anchoredworld frame at the cell origin and pushes it into
props, beside the furnitureand vehicle circles.
crates/cartopolis/src/systems/map/map_geometry.rs:729-754— afterplace_crossings, extendcell_places.colliderswith one circle per mast atradius 0.25 m, the lamp column's. Name the radius as a constant next to
prop_radiusinworld_places.rs, so every collider radius in the client staysin one file. The comment block at
map_geometry.rs:715-733currently states "nocollider" 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.
this gap survived a settled shot with
level_crossings = 3on screen. Add onefield to
ShotMetrics— a count ofWorldPlaces::props()circles — besidelevel_crossings(crates/cartopolis/src/systems/dev/shot_harness.rs:651,2659-2665, CSV header at846), which is the project's stated way to make anew 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.
docs/notes/prorail-level-crossings.md:225-227and the module note atcrates/geo/src/crossings.rs:55-58both record "no colliders" as a standingdecision. Both stop being true and are edited in place (the format rule in
docs/notes/README.md), keeping the navigation marks' own answer intact andsaying what separates the two cases.
Acceptance criteria
cartopolis_geoexposes the mast centres of aLevelCrossingin tile-localmetres, and
push_crossingplaces its mast boxes from the same helper — nosecond copy of
road_half + VERGE_M/clearance_m/ the rotation.crossing at
yaw != 0and asserts the helper agrees, in the style of theexisting
acrosshelper (crates/geo/src/crossings.rs:518-543). A test thatonly 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).
CrossingKinds produce two mast colliders —Lightsincluded; ithas masts and lamp heads (
crossings.rs:408-426).CellPlacescarries the bare colliders;WorldPlaces::insert_cellconvertsthem to the anchored world frame at the cell origin and includes them in
props().Interactable:WorldPlaces::nearestat amast returns
Nonefor a cell whose only content is crossing colliders.(
world_places.rs:353-361) — a client test in the shape ofretiring_a_cell_removes_its_places(world_places.rs:484-506).new gate written: crossings are only placed under
wants.furniture(
map_geometry.rs:628,755) andsync_prop_collidersalready clears propswhen the layer is off (
collision.rs:429-439). Confirm this holds ratherthan adding a second check.
prop_radius, with thelamp-column reasoning cited.
crates/geo/src/crossings.rs's module note anddocs/notes/prorail-level-crossings.mdno longer claim crossings get nocollider, and both state what makes a mast different from a buoy.
shot stateline, the--csvheader and--dump-state, and is assertable with--expect.Verification
Read-only refinement pass; nothing below was run here.
Headless, on a machine that can render (per CLAUDE.md the
--shotharness runswindowless on lavapipe; geometry, placement and visibility are trustworthy there,
colour and lighting are not):
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') againstlevel_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_CELLis 200 (crossings.rs:69) so the worst caseis 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
(
systems/nav/traffic.rsreferences neitherBuildingCollidersnorWorldPlaces), so they will still drive through a crossing. That is a separategap and needs its own ticket if it is wanted.
one. Only the crossings' sentence changes.
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 orre-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.
three are recorded refusals in
docs/notes/prorail-level-crossings.md:219-229and none is touched.
world_places.rs:215-218).Open questions
None.
Branch:
fix/234-crossing-mast-collidersOriginal 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_geometrysetscell_places.surveyed.6 = crossings.len()and extendsmeshes, andthat is all; the surveyed lampposts, benches and bins go into
cell_places.furniture,which is what
WorldPlaces::props()andcollision::sync_prop_collidersread. So acrossing 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 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.rssays crossings get no collider "exactly as the navigationmarks 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_radiusgivesa 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 andmeshed — they would need to publish their masts as props, or
CellPlacesneeds a secondlist of bare colliders.
world_places::prop_radiusfor the radius: 0.25 m, the same asthe lamp column, and
prop_interactableshould answerfalsefor the same reason a lampdoes.
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.
🤖 Promoted into the build lane by the 7-day retrospective (#279) —
autonomous+ship.Why this one.
collision::sync_prop_collidersor it does not; the answer is a count, not a picture— value 6.
correctly.
crossings.rssays a crossing gets no collider "exactly as the navigationmarks do not", and the marks have that rule because a buoy stands on water nobody can
walk on — the same reason
prop_radiusgives a moored boat a radius of 0. A crossingmast stands beside a street the avatar walks along, which is the case
prop_radiusalready argues for the lamp column in as many words.
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.rswhere the crossings areplaced, and
world_places::prop_radius/prop_interactable. Masts only — not theboom, 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, instand_on_terrainrather than in the placement. Whichever of the two is picked up secondmust be cut from a
mainthat already holds the first — two branches from one parent,each green alone, is what put
mainin the red on9e5ed895last week (#276).🤖 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.
🤖 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:skipstops me.🤖 viberfox issue agent on the VPS — 33 turns · $2.84 · session
63e17760-59dc-4a6a-ac01-7fea5bf7d231. Reply with @agent to continue this session.🤖 Picked this up — working on branch
fix/234-crossing-mast-collidersin a fresh worktree on the VPS. I'll report back here when I'm done.Label an issue
agent:skipto keep me off it, ordrivento keep it in a session you are driving.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— addedmast_centres, which returns a crossing's two mast positions in tile-local metres. The mesh builderpush_crossingnow places its boxes from the sameLayoutstruct and through the same rotation helper, so there is one copy ofroad_half + VERGE_M,clearance_mand the object-to-tile turn, not two.crates/geo/src/furniture.rs— pulled the object-to-world rotation out ofpush_boxasplace_groundso the collider and the box share it.crates/cartopolis/.../world_places.rs— addedCellPlaces::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 isCROSSING_MAST_RADIUS= 0.25 m, sitting besideprop_radiuswith the lamp-column reasoning.crates/cartopolis/.../map_geometry.rs— feeds one circle per mast into that list.ShotMetrics::prop_colliderscounts the collider circles, in the log line, the CSV and--dump-state, so a headless run can assertprop_colliders>0besidelevel_crossings.docs/notes/prorail-level-crossings.mdand 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) andcargo test -p cartopolis(832 passed), both clean, pluscargo fmt --checkon 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)🤖 viberfox issue agent on the VPS — 49 turns · $5.24 · session
dc0cd9da-73d3-49f0-b338-595b7d663d92. Reply with @agent to continue this session.🤖 Marked
agent:donewithout a CI verdict onb11c098e— no workflow reported on this commit. The branch is waiting for review; check the run yourself before landing it.🤖 Merged into
mainas pull request #285 (b11c098e).🤖 Deployed to the public services.