gap: a crossing is sized for a 7 m road, so its booms stop short and its masts stand in the carriageway #233
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#233
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
CrossingKind::specreturns a constant half-carriageway per form, and every form but the mini one uses the same 3.5 m —crates/geo/src/crossings.rs:200-207:push_crossingputs each mast atroad_half + 0.5across the road (crates/geo/src/crossings.rs:340) and runs the boom back toward the centreline from there (crates/geo/src/crossings.rs:376-387). Withroad_half = 3.5that describes a 7 m road, for everyAHOB,HAHOB,AOBand lights installation in the register.The client draws the carriageway from
road_texture::road_width_m(crates/geo/src/road_texture.rs:181-218), which answers 11.0 m forsecondaryand is reached from tile geometry throughvector_tiles::drawn_street(crates/geo/src/vector_tiles.rs:3015-3025). So over the Peizerweg (OSMhighway=secondary) both masts stand ~1.5 m inside the kerb, on the asphalt, and each 4 m boom covers well under half of an 11 m road.It is wrong in the other direction for the commonest small case:
AHOB-MINI(381 features,crates/geo/src/crossings.rs:164) assumes 1.6 m half-width whereroad_width_mgives a cycleway 2.2 m total (half 1.1) and a footway 1.8 m (half 0.9).The width is already reachable on the lookup that produces the yaw.
place_crossingsbuilds aWayFieldwithWayFilter::ExcludingRailand asks it for a direction (crates/geo/src/crossings.rs:263-283);WayField::direction→WayField::nearestreturns the endpoints of the very segment involved (crates/geo/src/furniture.rs:544-577). What the field does not keep is any per-segment attribute —segsisVec<([f32;2],[f32;2])>(crates/geo/src/furniture.rs:394-400) — so the way'skindis read inadmits_featureand then thrown away (crates/geo/src/furniture.rs:374-379).One constraint sits right there and is load-bearing:
WayFilter::Allis documented to cost no tag lookup per way, because that is the path every streamed cell's furniture takes (crates/geo/src/furniture.rs:370-379).Approach
All of it is in
crates/geo—cartopolis_geois Bevy-free and this is pure geometry. No client change is needed:map_geometry.rs:728-742callsplace_crossingsandbuild_crossing_meshesand reads neither the spec nor the width.crates/geo/src/furniture.rs— carry the width off the segment.Give
WayFielda per-segment half-width parallel tosegs, asOption<f32>(None= the way's kind was not read, or is not drawn as a carriageway). Populate it only for filters that already read thekindtag — i.e.ExcludingRail— soWayFilter::Allkeeps the property the doc comment atfurniture.rs:370-379states.pushandpush_line(crates/geo/src/furniture.rs:479-504) take the width alongside the endpoints;nearestreturns it (or the segment index) so bothfacinganddirectionkeep working unchanged. Add adirection_and_width(or widendirection's return) for the crossings caller;facingis untouched.The width itself is
road_texture::road_width_m(kind, flags) * 0.5with the flags read the waydrawn_streetreads them (crates/geo/src/vector_tiles.rs:3020-3024) — reusingdrawn_streetis the shortest path and gives the tunnel refusal for free, at the cost of answeringNonefor a tunnel, which then falls back exactly as an unreadable kind does.crates/geo/src/crossings.rs— carry it to the mesh.LevelCrossing(crossings.rs:228-238) gainsroad_half: Option<f32>, filled inplace_crossings_againstfrom the same segment the yaw came from.push_crossingresolveslet half = crossing.road_half.unwrap_or(spec_half)and derives both numbers from it instead of from the table:half + VERGE_M, whereVERGE_M = 0.5is the existing bare0.5atcrossings.rs:340given a name;HalfBarrier/MiniBarrier:half + VERGE_M— the tip lands on the centreline, which is what today's constants already do (3.5+0.5 = 4.0; 1.6+0.5 = 2.1);FullBarrier:2.0 * half + VERGE_M— the tip lands on the far kerb. At the fallback half-width that is 7.5 m against today's 7.6 m; the 10 cm is an accepted consequence of deriving the number rather than tabulating it.Lightskeeps no boom.Mast heights stay per-form and do not move with the road:
speckeeps its 3.1 / 2.4, and its half-width becomes explicitly the fallback.docs/notes/prorail-level-crossings.md— the orientation section (lines ~140-165) says the field grew aWayFilterto answer the yaw; it gains the second thing that lookup now answers. The census section already asserts "the road's own width is already in the tile" underkarakter; that line becomes true of the code rather than of an argument.Byte accounting does not move:
BOOM_BANDSstays 3, soMAX_BOXES_PER_CROSSINGandCROSSING_MAX_BYTES(crossings.rs:91-99) are unchanged and a longer boom costs no extra vertices.Acceptance criteria
WayFieldbuilt withWayFilter::Allperforms no per-way tag lookup — the rule stated atcrates/geo/src/furniture.rs:370-379still holds after the change, and the furniture call sites (furniture.rs:290,furniture.rs:870) are untouched.kind = secondary(road_width_m→ 11.0) puts each mast at 6.0 m from the register point across the road, not 4.0. Test drives the real path —synth::tagged_line_layer+WayField::build_with(ExcludingRail), asthe_tile_side_filter_reads_the_kind_tag(crossings.rs:588-613) does — so a build that never reads the tag is caught.HalfBarrier/MiniBarrierboom tip lands on the road centreline (within a millimetre) for that road, and eachFullBarrierboom tip lands on the far kerb.AHOB-MINIagainst akind = cyclewayway (2.2 m) puts its masts at 1.6 m either side, not 2.1 — i.e. the mini form gets narrower, not just the big forms wider.kindtag, a kindroad_classrefuses, or the crossing was constructed withroad_half: None), the mast positions are exactly today's: 4.0 m forHalfBarrier/FullBarrier/Lights, 2.1 m forMiniBarrier. Boom reach is likewise today's for the first three forms and 7.5 m forFullBarrier.ORIENT_RADIUS_Mis still dropped rather than drawn —orientation_comes_from_the_road_not_the_track,without_the_filter_the_track_winsanda_crossing_with_no_road_is_droppedstill pass unmodified in substance.the_byte_cap_splits_between_crossingsanda_cap_below_one_crossing_still_draws_it(crossings.rs:556-582) still pass, andCROSSING_MAX_BYTESis not raised.docs/notes/prorail-level-crossings.mdrecords that the width now comes off the same segment as the yaw, and what the fallback is.Verification
Runs here (whoever picks this up, after the label moves — this pass builds nothing):
Needs a renderer, so not this container — the settled capture the issue was filed from, over the Peizerweg AHOB:
level_crossingsin--dump-state(crates/cartopolis/src/systems/dev/shot_harness.rs:651,:2665) must stay non-zero — this change must not drop a crossing that was being drawn. The picture is the judgement: both masts on the verge, the two booms meeting near the centre of the carriageway. Geometry and placement are trustworthy on the software rasteriser (docs/notes/headless-shots-software-renderer.md); colour and lighting are not, and nothing here depends on them. It needs a vectorCARTO_TILE_URLand network reach to the PDOK Spoorwegen service.Out of scope
crossings.rs:273), so the width is exactly as trustworthy as the orientation this layer already ships. Bounding one and not the other would be arbitrary; if it turns out to matter it is a separate ticket aboutORIENT_RADIUS_M.lanes.road_width_mclassifies onkindand the twoStreetFlagsonly (road_texture.rs:181-218); the Peizerweg'slanes=3is not read today and stays unread. This change makes the crossing agree with the ribbon the client draws, not with OSM's lane count.docs/notes/prorail-level-crossings.md, and none of them moves here.--shotcapture itself. It cannot run in this container.Open questions
None.
Branch:
fix/233-crossing-boom-road-widthOriginal request
What I did
The same settled capture over the Peizerweg AHOB in Groningen
(
--at 53.210238,6.549328,25 --look=-90,0), and then read the road's own tags.What happened
CrossingKind::specreturns a constant half-carriageway per form:push_crossingputs the mast atroad_half + 0.5= 4.0 m from the register point andreaches the boom 4.0 m back toward the centreline. That describes a 7 m road, and it is
the same 3.5 m for every
AHOB,HAHOB,AOBand lights installation in the country.The Peizerweg is OSM
highway=secondary,lanes=3.road_texture::road_width_manswers11.0 m for that kind, which is the ribbon this client draws. So both masts land about
a metre and a half inside the kerb, standing on the asphalt, and each boom covers about
4 m of an 11 m carriageway. In the capture the middle of the road is open between the two
bars, and both masts are plainly on the road surface rather than on the verge.
The mismatch is the wrong way round for the commonest case, too:
AHOB-MINI(381features, the footpath and cycleway variant) assumes 1.6 m where
road_width_mgives acycleway 2.2 m and a footway 1.8 m.
What a user would expect
A boom reaches across the road it bars, and its mast stands beside the road, not in it.
Where the seam is
crates/geo/src/crossings.rs::CrossingKind::specandpush_crossing.The width is already within reach on the lookup that gives the yaw:
WayField::nearestfinds the very segment the crossing is oriented against, and
road_texture::road_width_msits directly beside
is_rail_kindin the same module — which is whereis_rail_kindwas put, by this feature, "so the two cannot drift". Carrying the way's
kindout ofWayField::directionalongside the yaw would givepush_crossingthe real half-width;spec's constant then becomes the fallback for a crossing whose road kind is unreadable.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 (#269) —
autonomous+ship.Why this one:
road_texture::road_width_msits directly besideis_rail_kind, and the commit that addedis_rail_kindput it there "so the two cannot drift".WayField::nearestalready finds the very segment the crossing is oriented against, so the width is on a lookup the code performs anyway. This is value 1 — a surveyed width in reach, against a constant 3.5 m guess.CrossingKind::spec,push_crossing), the source of the right number, and the route it travels. Contrast #213, which was promoted by the last retrospective and stalled atagent:needs-inputbecause its fix direction was not settled first.level_crossings_groningen.json,level_crossings_utrecht.json) — acrates/geounit test, no capture.crates/geo/src/crossings.rsand a read ofroad_texture::road_width_m. No wire protocol, no schema, no workflow, no taste.Do the narrow-road case too while the width is in hand:
AHOB-MINIis 381 features and assumes 1.6 m whereroad_width_mgives a cycleway 2.2 m and a footway 1.8 m. Keep the constant as the fallback for a crossing whoseWayField::nearestfinds nothing — value 3, degrade rather than break.Stuck, waiting for you: nonewhile three tickets wait for a person #270🤖 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 — 17 turns · $1.82 · session
95aec176-abcb-4790-af29-914d5681a6e3. Reply with @agent to continue this session.🤖 Picked this up — working on branch
fix/233-crossing-boom-road-widthin 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.Level crossings are now sized for the road they stand on, instead of for a fixed 7-metre road.
What changed
CrossingKind::specreturned the same 3.5 m half-carriageway for every barrier and lights installation in the country. The Peizerweg is taggedhighway=secondary, which this client draws 11 m wide — so both masts stood about 1.5 m inside the kerb, on the asphalt, and each 4 m boom covered under half the road. The small form was wrong the other way:AHOB-MINIassumed 1.6 m where a cycleway is 2.2 m.WayField(crates/geo/src/furniture.rs) now stores a half-carriageway alongside each segment, anddirection_and_widthreturns it with the yaw — the same segment, one lookup.directionis gone, replaced by that.vector_tiles::drawn_street's own number, the one the drawn road ribbon uses, so the installation and the tarmac under it cannot drift apart.WayFilter::All— the path every streamed cell's furniture takes — still reads no tags per way, and answers no width. A new test pins that.crates/geo/src/crossings.rs:LevelCrossingcarriesroad_half: Option<f32>. Masts stand athalf + 0.5 m; a half barrier's boom tip lands on the centreline, a full barrier's on the far kerb.spec's constants are now explicitly the fallback.docs/notes/prorail-level-crossings.mdrecords where the width comes from and what the fallback is.Vertex counts are unchanged —
BOOM_BANDSis still 3, soCROSSING_MAX_BYTESdid not move.Verified
cargo test -p cartopolis_geo— 242 passed.cargo test -p cartopolis— 820 passed.cargo check -p cartopolisclean,cargo fmt --check -p cartopolis_geoclean.Six new tests, five of them measuring the meshed geometry rather than recomputing it: a
secondaryroad driven through a synthetic tile puts the mast at 6.0 m and the boom tip on the centreline; a cycleway puts a mini barrier's mast at 1.6 m; a full barrier reaches the far kerb; and where no width is known, all four forms land exactly where they used to.Not verified
The picture. This container cannot render, so the settled capture over the Peizerweg has not been taken — that is the judgement call on whether the masts now read as standing on the verge:
level_crossingsin the dumped state must stay non-zero — nothing here should drop a crossing, but it is worth confirming.One thing to decide
A full barrier's reach is now 7.5 m where the table said 7.6 — the 10 cm is the cost of deriving the number from the half-width instead of writing it down. The issue accepted that; flagging it since it is the one place the fallback behaviour is not byte-identical.
Housekeeping
The worktree I was handed had no git administrative directory — every git command answered
not a git repository: (null). I made a second worktree on the same branch at.claude/worktrees/233-repair, copied the four changed files across, and committed and pushed from there. Both worktrees are still on disk; the orphaned one can be deleted.No commits were made.
🤖 viberfox issue agent on the VPS — 66 turns · $6.43 · session
a59ef83c-439a-4ca6-ad26-4450cc5a81fd. Reply with @agent to continue this session.🤖 Merged into
mainas pull request #274 (b2b92339).🤖 Merged, but not deployed. The deploy step exited 1:
mainhas the change; the public services are still on the previous build.mainwas already red, and every merge commit was red #282