Skip to content

TrailView ground plan: junction records and the stop-mouth route rule - #3186

Merged
timohueser merged 13 commits into
ground-planfrom
claude/gp-junction-record
Nov 7, 2026
Merged

timohueser merged 13 commits into
ground-planfrom
claude/gp-junction-record

Conversation

@timohueser

@timohueser timohueser commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner

What

Each records block gets a junction record (JR) per junction that a label names. A stop mouth uses it: the route keeps its true edge, and the stop look no longer searches the bin.

  • The record. records::encode writes the JR: the node, and per arm its record line, its samples from the node to the first sample past its seam, its unit direction at the node and two flags (host, stop). Head word 12 is the offset of the JR table and word 13 the JR count (HEAD = 14). A junction label's word 1 holds its JR index: label → table → JR, two reads. Junction labels have no host and other lines; records::owner gives the checks and the mesh the graph's junction id.
  • The readers. heights::arms_of and h_arms_of read a junction's arms from the JR. corner_look and model_point read a junction's frame lines from the JR (hosts; near also the material line; soil every arm) and their segments from the bin, as before the JR. The bins keep the paint segments of the junction's arm lines. So the frame equals the base's by construction. mouthLabels reads the stop flag from the JR.
  • The rule (terrain/mouth.wgsl, near and far). In a junction face with a stop arm, the route arms divide the plane about the node into sectors. In a sector with a stop arm, a point keeps the junction's look on a route arm's stroke (flat end at the node) or in the mitre where the two route arms that bound the sector meet (behind both node lines, limit 4 h), within 2 cm. Every other point of the sector wears the nearest stop arm of that sector, in that arm's frame. The bin search for the mouth, STOP_ARM, other_line, m_arm_line, m_other and the dead branch for three host lines go.
  • Tests. The JR of a T junction with a stop arm (arms, flags, ranges, directions, label → JR). The stop rule with the reviewer's radius-0 steps case. One test of the route rule: a CPU port of mouth.wgsl as of 8946986 (test code only) keeps the mitre's corner and each arm's own edge beside a mouth at a bend. A vitest for splitMouths.
  • MAGIC 0x4f42_520d. ground-plan's history used 5209 to 520b; the walls pull request ground-plan GP3 follow-up: walls from a slope budget (draft, not ready) #3185 takes 520c. The one that lands second takes the next value at its rebase. The commits before 2d24c46 still write 0x4f42_5209.

Commits

  1. 19ce4ac52 the record, its writer and readers.
  2. 5a8240c43, 9cfc02ed5 frame fixes from the first pixel compare. 9174776ab replaces both (see below).
  3. 73009d72a the route rule.
  4. 2d24c464c MAGIC 0x4f42_520d.
  5. Review fixes: 9174776ab the frame from the bin's segments of the JR's lines; 89469869c the stop look from the sector's own stop arms, and the route-rule test.
  6. 3c711fd76 a c11 taper, reverted by 0053ed227: its straight chords leave a curved arm's edge (delta review: up to 1.30 m, spikes). 0053ed227 is the reviewed state 89469869c.
  7. 6838a13ed stops() from table values (special case 1, see below).
  8. 8d1a797bf the trail look from the line's class, not from PATH_HALF_M (special case 2, see below).
  9. e2e561277 PATH moves to bit 14 (M_PATH 16384): bit 11 is the walls branch's WALL, and water TrailView ground plan GP14 slice 1: line water (records, level, banks, culvert zones) #3177 plans bits 12 and 13. A const assert fails the build where two fields of a line's finish word overlap; a new flag joins its list.
  10. f227fcc70 c11 widening (owner approved): the narrower route arm of a stop mouth widens along its own curve (cherry-pick of 9707268 from claude/gp-junction-taper).

Special cases removed (commits 7 and 8)

stops() from table values. A guest arm gets ARM_STOP where its pair's mouth in plan.json is stop, its corners are round, and either its look (finish and material) differs from the best host's, or it is a soil guest whose finish is in the new plan.json list tread_finishes (gravel, dirt, grass, path). The hard-coded unpaved list in code goes.

  • Owner decision: stop pairs only. A reach pair keeps the junction's look; it can join later if renders show the need.
  • Stop arms on the fixture: 292 → 386 (+94, none lost): 61 asphalt cycleways at gravel tracks, 6 paving-stone paths at service roads, 4 asphalt cycleways at grass tracks, 3 paver footways at residential roads, and others.
  • Why tread_finishes: without it, 13 same-look soil guests (gravel path at a gravel service road, dirt path at a dirt track) lose the flag. They then take the interpolated corner frames, and their treads tear (fault 2.2 of the diagnosis). With the list, those guests are pixel-equal to the base.
  • c09 (pics/c09-service-path-T.jpg, pics/f1-c09-top.jpg): the paving-stone paths keep pavers up to the service road's edge.
  • p07 does not change (pics/p07.jpg): its hosts are gravel footways and its guest an asphalt footway, a path/path pair with mouth reach. A trial with the reach pairs (stop arms 488) fixed p07 but cut soil treads with straight sector edges at p06, p08, p09 (pics/reach-*.jpg, labelled base | trial).
  • c01: an asphalt footway at a secondary keeps its look to the road's edge; the edge lines end at the mouth (174 px).

The trail look from the class. A line of a path class (group path or steps in roads.json) has the flag PATH (bit 14) in its finish word. line_look reads it, as lookOf of roads/section.ts reads the line's kind. PATH_HALF_M = 1.2 and the half-width guess go from plan/corners.wgsl and plan/mouth-in.wgsl. The width guess still lives in ground/roads.wgsl (the drawn ground, outside the plan). On the fixture 2 ways change look, both dirt with a half-width of 1.5 m: a cycleway and a path with a width tag. They now wear a trail's tread, not a track's ruts (pics/t2-cycleway-dirt.jpg, pics/t1-path-dirt-wide.jpg).

Why the first frame changed pixels

Commit 1 first read a junction's frame from the arm ranges in the JR (node to the first sample past the seam). The base read it from the bin, which lists every segment of the frame lines that can be nearest to a point of the bin: the nearest point of the whole line. A point whose nearest point on the line lies past the range got a round frame there. The far paint shows it most, because it paints texels outside the face with the junction's label: a dirt path broke at two stub junctions near conn-path-off-road, and the culvert of mouth-stream had a notch. 9174776ab reads the frame from the bin again.

Checks

On 8d1a797 (the head):

  • Fixture counts equal to 8946986 (= 0053ed2) exactly: faces and the heights scan (steps 5,606, the same rules, border, unnamed and inside points). The stop arms are the only intended change and are not in these counts.
  • cargo test -p obc-ground-plan: 102 passed, on both commits. cargo clippy -p obc-ground-plan --all-targets -- -D warnings: clean. cargo fmt --all.
  • check-bytes.sh: 256 of 256 (the checker again hung at exit after it printed).
  • GPU = Rust probe: v14 11,310 points max 0.058 mm; 49,741 points max 0.119 mm; 0 kinds differ.
  • npx vitest run src/lib/trailview: 408 passed. npm run check: 0 errors. npm run check:wgsl: 69 valid.
  • WASM size guard: raw 1,800,971 B against 1,802,240 B: 1,269 B left (0053ed2: 1,800,658 B, 1,582 B left; the tread_finishes parse is most of the +313 B). Gzipped 691,166 B against 704,512 B.
  • Shots (36 views: the census, c09, p01 to p10), head against 0053ed2: 30 equal. Differ: c09, f1-c09-top, c01 (intended); c14-eye 114 px (distant mouths in town), c16-eye 2,485 px (skyline noise), p01 6 px (a sign's shadow, noise). c08, c12, c14, c15, w598 and v14-top are pixel-equal to the base; v14 stays on the owner's line (pics/v14-top-overlay-base-c2.png). The 5 same-look soil views (l1..l5): 5 of 5 equal.
  • v14-pass, one pair, terrain median: 0053ed2 2.29 ms, head 2.35 ms.

On 8946986 (the review fixes; the taper changes only mouth.wgsl):

  • Fixture counts equal to ground-plan c3f5693 exactly: faces (BentBorder 92, Fragment 4, kinks 18, short features 162, no crashes, no partition or check-2 faults) and the heights scan (steps 5,606, the same rules, border, unnamed, inside and host-missing points).
  • cargo test -p obc-ground-plan: 102 passed. cargo clippy -p obc-ground-plan --all-targets -- -D warnings: clean. cargo fmt --all.
  • check-bytes.sh: 256 of 256.
  • GPU = Rust probe: v14 11,310 points max 0.058 mm; 49,741 points max 0.119 mm; 0 kinds differ (as ground-plan).
  • npx vitest run src/lib/trailview: 408 passed. npm run check: 0 errors. npm run check:wgsl: all valid, the mouth composition too.
  • WASM size guard: raw 1,800,658 B against the budget of 1,802,240 B: 1,582 B (0.09 %) left (ground-plan 1,798,510 B, so +2,148 B). Gzipped 691,511 B against 704,512 B (ground-plan 692,074 B).
  • Frame probe (fixture, every junction-face point on a 0.25 m grid, near and far records): the frame from the bin's segments of the JR's lines against the nearest segment of those whole lines: 1,949,430 points for the paved frame and as many for the soil frame, 0 differ.
    Left out: a full obc test affected; develop shots; road-views-far.json.

Measurements

  • Records over the fixture: near 15,855,809 words (ground-plan 15,835,684, +20,125, +0.13 %); far 13,178,354 (13,158,229, +20,125, +0.15 %). The bins keep the junction paint segments; the JR adds 1,478 records, 4,567 arms.

  • eps = 2 cm is a chosen value: the face's edge lies up to about 2 cm outside a stroke (snap and chords). The fixture's hairline counts (stop-look points with no other stop point 6 cm away, 249 owned stop junctions, 5 cm grid) fall steadily, with no knee: 289 at 0 cm, 122 at 1 cm, 88 at 2 cm, 56 at 3 cm, 45 at 5 cm, 24 at 10 cm.

  • Frame cost, flight.mjs --gpu, terrain pass median (ms), ground-plan | 2d24c46, 3 alternating runs:

    flight ground-plan c3f5693 2d24c46 difference
    v14-pass (mouths) 2.29 / 2.32 / 2.30 2.30 / 2.30 / 2.32 +0.00
    track-pass (mouths) 2.32 / 2.37 / 2.29 2.35 / 2.39 / 2.27 +0.03
    walk (ordinary) 2.99 / 3.01 / 2.99 2.98 / 3.00 / 3.02 +0.00
    valley-drive (ordinary) 2.55 / 2.53 / 2.55 2.57 / 2.54 / 2.38 within noise

    One v14-pass run on 8946986: 2.36 (ground-plan 2.33 in the same pair).

Pictures

/media/timo/OSM1/tmp/ground-plan/junctions/impl/pics/ (INDEX.md has a verdict per picture). Commits 7 and 8: /media/timo/OSM1/tmp/ground-plan/junctions/sc/pics/ (base | head crops).

  • v14-top on the owner's line, with flat ends (the head): middle panel of v14-top-overlay-base-flat-taper.png, and c2/v14-top-diff.png.
  • c08, c12, c14, c15 and w598: the tabs, steps and the half corner arc are gone. c07's bump is gone.
  • Commit 1 with the bin frame against ground-plan (a commit-1-equivalent build, diff/c1eq/; 47 views: the reviewer's 21 eye views against their control base3, and the earlier differing top and mouth views): 39 equal. The far-paint path break and the culvert notch are gone (pics/cpor-far150-base-c1c-fix.jpg). Left: c16-eye, footway-eye, conn-road-into-track-top and side-15m are noise (a second ground-plan run differs the same way); hp-15m is 13 px noise and 7 px real. Small, real, cause not found: b-steps-eye 68 px on a distant ridge (the same in every branch build), bridge-eye 38 px (single pixels on the parapets), v14-far150 4 px.
  • The head against ground-plan differs more in views with a stop mouth (crossing-close, hp-15m, path-edge-*): the route's edge is now the stroke, not ground-plan GP4: a stop mouth wears its unpaved arm's look #3183's smooth union, along the whole junction face. A second run of the head gives the same pixels.

c11 (owner approved: widening, commit 9)

c11: two roads of 2 m and 3 m half-width meet at the node with gravel tracks on both sides. Flat stroke ends gave a ~1 m square step on each side of the node. Now, where the facing edge corner of one route arm lies past the other's facing edge line, the other arm's stroke widens on the stop side by that much at the node, back to its own width at its seam, along its own centre line; the mitre takes the widened edges. Where the two arms are as wide, nothing widens. The Rust test port has the rule and a case of two widths.

  • Checks on f227fcc: 103 crate tests, clippy, check:wgsl 69 valid, vitest 408, probe as above (0.119 mm, 0 kinds differ), size unchanged (WGSL only: raw 1,800,971 B, 1,269 B left).
  • Census views against 8d1a797: c08, c12, c14, c15, w598-top, w598 and v14-top pixel-equal; c11 changes (6,483 px): the step is gone (sc/pics/c11.jpg). v14 stays on the owner's line.
  • Earlier on the taper branch: the reviewer's probe gave 0 changed points for equal widths, 7,206 for unequal (max 0.95 m); v14-pass 2.33 vs 2.28.

Open

Public docs: unchanged.

Mode: change #3182
Requirements: none

🤖 Generated with Claude Code

timohueser and others added 5 commits November 6, 2026 23:17
A junction label's owner word names its junction record: the node and, per arm, its record line, its
samples from the node to the first past its seam, its direction at the node and two flags (host, stop).
The corners, the far paint, the heights' arms and the stop flag of the mouth read the record; junction
labels have no host and other lines and no paint segments in the bins. MAGIC 0x4f42_5209.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ines

A host line that goes on through the junction as a guest arm keeps its frame there, and a junction
whose material line is a guest's frames its corners by that line too, as the bin search did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The paint of a junction face reaches past the face's seam; there the host line runs on straight as its
own segment, not round its last sample.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…record

In a junction face with a stop arm, the route arms divide the plane about the node into sectors. In a
sector with a stop arm a point keeps the junction's look on a route arm's stroke (flat at the node) or
in the bounded mitre where the two route arms that bound the sector meet, within 2 cm; elsewhere it
wears the nearest stop arm's look in that arm's frame. The rule reads the junction record, near and
far, and replaces the search of the bin's segments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8d4fb0ae-ce4c-4ce7-aa55-2779202c7c3b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

timohueser and others added 2 commits November 7, 2026 02:38
…ines

The record names the frame lines (hosts, near also the material line; every arm for soil), and the bin
gives their segments, as before the record: the bins list every segment of a junction's arm lines that
can be nearest to a point of the bin, so the frame equals the base's by construction. The arm ranges
and their extra segment no longer give frames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sector test already finds the stop arms between the two route arms; the frame and the look take the
nearest of those. A test keeps the route rule's mitre corner and the arms' own edges beside a mouth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
timohueser and others added 2 commits November 7, 2026 03:19
…stop mouth

Each route arm's facing edge at the node runs straight to the other arm's facing edge at its seam. Two
arms of different widths (c11) meet without a step; where both are as wide, the taper lies within the
strokes and the mitre.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hs at a stop mouth"

This reverts commit 3c711fd. The taper's straight chords leave a curved arm's edge (delta review): c11
stays an owner decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@timohueser

Copy link
Copy Markdown
Owner Author

From the plan after the 2026-11-07 review of #3096 (special cases): stops() becomes a table value. This is J1.

Problem. stops() (faces/records/mod.rs:55 on this branch) flags a guest as a stop arm only if its finish is unpaved. A pavers path is Finish::Road with Material::Pavers, so its arm gets no ARM_STOP, and m_mouth keeps the junction's look for the whole face: the dark asphalt mouths of paved paths (c09 and three more places). p07 is the same fault from the other side: an asphalt cycleway turns gravel at its seam with a gravel host. Pictures: /media/timo/OSM1/tmp/ground-plan/junction-look-diag/pics/ (f1-c09-top.jpg, p07-gravel-hosts-paved-guest.jpg).

Proposal. No finish list in code. A guest is a stop arm when it is not a host, its pair's mouth is not join, its corner radius is above 0, and its look (finish and material, from the tables) differs from the best host's. This is the owner's general rule: a guest whose look differs from the route's keeps its own look up to the route's edge, paved or not. A guest with the same look gets no flag, so nothing changes and nothing costs there. If a pair needs another answer, that is a value in plan.json, not a branch.

Effect on the fixture (1,041 junctions): stop arms 294 → 388 with the stop pairs (66 paved cycleways at gravel tracks, 7 pavers paths at service roads, 4 pavers footways at residential roads, …); about 503 if the reach pairs join too. The owner decides whether reach pairs join.

Proof. One predicate and its test (a pavers path at a service road, an asphalt cycleway at a gravel track); the census shots of the changed junctions, top and eye height, ground-plan | new; the two mouth flights (v14-pass, track-pass) with GP0's method (this PR measured the rule at about +0.03 ms with 294 arms). About half a day.

Also on this PR: PATH_HALF_M = 1.2 (plan/corners.wgsl) infers "path" from the half-width; the record's line takes the path look from its class instead, and Rust and WGSL read the same bit.

Out of scope here: R2 and R3 of the junction-look diagnosis (every arm keeps its own look; no frame interpolated across arms). They are one later slice on the same functions.

timohueser and others added 3 commits November 7, 2026 14:44
A guest stops at its host where its pair's mouth in plan.json is stop,
its corners are round, and its finish or material differs from the best
host's or its finish is in tread_finishes of plan.json. The hard-coded
unpaved list goes: paving-stone paths at a service road and asphalt
cycleways at a gravel track keep their own look up to the route's edge.
A paved guest of the host's look needs no mouth; a soil one keeps its
tread as before. A reach pair keeps the junction's look.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A line of a path class (group path or steps) carries PATH in its finish
word. line_look reads that flag: a dirt or grass path wears a trail's
tread and a dirt or grass track wears dirt, as lookOf of section.ts.
The corners and the stop mouth no longer guess "path" from a half-width
under PATH_HALF_M = 1.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… curve

Where the facing edge corner of one route arm at the node lies past the other's facing edge line (two
arms of different widths that run on straight), the other arm's stroke widens on the stop side by that
much at the node, back to its own width at its seam, along its own centre line; the mitre takes the
widened edges. Where the two arms are as wide, nothing widens. The test port has the rule, with a case
of two widths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@timohueser
timohueser marked this pull request as ready for review November 7, 2026 15:50
…fields apart

Bit 11 is the walls branch's WALL flag, and water plans bits 12 and 13.
A const assert fails the build where two fields of a line's finish word
overlap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@timohueser
timohueser enabled auto-merge November 7, 2026 16:53
@timohueser
timohueser merged commit 7a38730 into ground-plan Nov 7, 2026
26 checks passed
@timohueser
timohueser deleted the claude/gp-junction-record branch November 7, 2026 17:07
timohueser added a commit that referenced this pull request Nov 7, 2026
The junction record (#3186) and the load order (#3204) join the outline slopes. The head keeps the
junction table at words 12 and 13; the outline edges move to word 14 (HEAD 15). WALL (bit 11) joins
the finish word's disjoint fields beside PATH (bit 14). MAGIC 0x4f42_5211.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot mentioned this pull request Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant