feat(f1-kit): grandstand-bay tiers/roof/stairs/collision + garage-box floors/tower (Madring) - #14
Open
zugzug1 wants to merge 16 commits into
Open
feat(f1-kit): grandstand-bay tiers/roof/stairs/collision + garage-box floors/tower (Madring)#14zugzug1 wants to merge 16 commits into
zugzug1 wants to merge 16 commits into
Conversation
Keep visual Three.js recipes authoritative and ship a CompiledTopology .vtopo per catalog model (welded hull, repaired manifold, LOD, collision) with GameReadyClaims measured by measureIntegrity. Registry schema v2 emits artifacts and compiled representations. The compiler is not part of published @f1-kit/f1-kit-core.
…t 6.4 The rig STRADDLES the racing surface, so its post-to-post span belongs to the circuit, not the model — but `const span = 6.4` sat inside `rebuild()` and was not on `F1StartLightsConfig`, so no consumer could reach it. On a 14 m road the posts stood well inside the asphalt. Now `span` on the config, plumbed through createModel + configure(), clamped `>= 4`. DEFAULT IS UNCHANGED at 6.4, so every existing caller renders exactly as before; only a caller that passes a span sees a difference. The lamp panel deliberately does NOT scale with it — it keeps its FIA size (5 columns at 0.36 pitch), centred under the beam, which is how the real structure reads. Only the posts, the top chord and the beam follow the span.
…a hard-coded 5.5 Devlo-racing's Madring footbridge needs to sit on a cut-and-cover lid, whose clearance above the racing surface is not the kit's standard 5.5 m catch-fence figure. The rig's ground datum belongs to the consumer, not the model, but `const DECK_H = SPECTATOR_BRIDGE.deckHeight` sat at module scope and every quantity derived from it (girder chord heights, pier heads, stair rise/run/rake, tower cap) was computed once at import time, so nothing downstream could reach it. `deckHeight` is now on `F1SpectatorBridgeConfig`, plumbed through createModel and configure(), clamped to >= 4 (finite required, else falls back). DEFAULT IS UNCHANGED at 5.5, so every existing caller renders exactly as before — proved by re-running the kit's full `.vtopo` regeneration (`f1:compile-topology`): f1-spectator-bridge.vtopo comes out byte-identical (same sha256), and every other model's sidecar is untouched too. The DECK_H-derived module constants are now `let`s recomputed by a new `applyDeckHeight()` at the top of `rebuild()`, mirroring the mutable-derivation pattern this file already used for per-rebuild state; JS's single-threaded synchronous execution means two bridge instances with different deckHeight configs never interleave a build against the other's derived values — each rebuild bakes vertex positions before anything else runs. The piers and the switchback stair towers (and their handrails) scale to reach the new deck height; girder depth and parapet/rail proportions are unchanged, matching how f1-start-lights' span option (44f299b) kept its own proportions fixed. Tests: default build is byte-identical (vertex count + AABB) to an explicit `deckHeight: 5.5`; `deckHeight: 6.5` measurably raises the deck, piers and stair towers; non-finite and sub-4 values are rejected (clamped/fallback) both at construction and via configure().
…n top
Adds tiers: number (1-4, default 1) to F1GrandstandBayConfig. Tier k>=1
stacks a full bowl whose promenade sits at tier k-1's bowlTop + ROOF_CLEAR
and whose front sits at tier k-1's rear edge (stepped back and up, no
cantilever overlap) — both derived from layoutOf's own constants, not new
magic numbers. Each non-top tier gets its own frontage (fascia/hoarding/
guard-rail) and a rear wall stretched to bowlTop + (ROOF_CLEAR - DECK) so
it closes flush against the tier above (no see-through gap into the
concourse). Only the top tier gets the membrane roof + rafters + columns +
fascia, built exactly as before.
tiers: 1 is untouched: tier 0 still emits straight into the bowl/roof
groups with an identity transform, so geometry, part names, and the
compiled .vtopo sidecar stay byte-identical to before this change
(verified by sha256 over world-space vertex data + instance matrices for
{rows:8,width:10} and {rows:24,width:10}, and by recompiling the sidecar
— same bytes, same hash).
No stair/vomitory — out of scope per brief, skipped.
…ding as a kit module Adds \`floors\` (1-3, default 1) and \`tower\` (default false) to f1-garage-box's config so the Madring pit building — 14 garages + two Paddock Club levels + a glass entry tower — is one kit module instead of a bespoke building. - floors:1 stays byte-identical to today's box (same .vtopo hash, same triangle/vertex counts, same AABB) — verified by test and by regenerating the sidecar. - floors 2-3 stack glazed storeys above the garage roof: a mullioned curtain wall, a pit-lane terrace with a guard rail, and a red leading-edge fascia band under each terrace (the catalogue tell). Storey height is solved backward from the circuit's published 18.5 m / 3-floor max (estimate:official-max-height). Top storey caps with a roof slab + parapet. - tower:true adds a one-bay glazed shaft at the model's -X end, mullioned like the floors, rising two storeys above the top floor (estimate:photo). - Glass is a local non-transmissive MeshStandardMaterial (transparent+opacity, same pattern as f1-team-motorhome's glazing) — never \`transmission\`, which the host forbids for perf. The red band uses TOKEN.RED_500 directly rather than the shared kit.red instance, which is reserved for fire equipment.
- Glazing was reading as a see-through glass box; the reference (pit-building-render.png, imagen-paddock.webp) is near-opaque blue-black curtain wall with the mullion grid as the light lines and floor plates behind only faintly visible. Darkened + raised opacity (0x050a10, roughness 0.05, metalness 0.55, opacity 0.9 — still non-transmissive). estimate:render. - Tower rose two storeys above the roof; f1com-YDRAY-foto-pit-building.webp shows about one. TOWER_EXTRA_STOREYS 2 -> 1 (roof deck + 1x STOREY_H + roof slab + parapet). estimate:photo. - floors:1 stays byte-identical (.vtopo SHA unchanged, identity test still passes). Tests + typecheck re-run clean; 9 review PNGs re-rendered.
Tier k's front now cantilevers OVERLAP_ROWS (3, estimate:photo — the Madring S/F stand photo's balcony fronts hang over roughly the last three rows of the tier beneath) rows' worth of tread past tier k-1's rear edge, instead of sitting flush on top of it. Concretely: the z-step between tiers shrinks by OVERLAP_ROWS * TREAD, so tier k's own bowl slides forward over tier k-1's last few rows. No new soffit geometry is needed: each tier's bowl mass is already a closed, full-depth loft (see buildBowl's profile), so the same slab that was already there becomes the soffit over the newly-covered rows once the tier is shifted forward. buildFrontage already runs per tier, so its fascia/hoarding/guard-rail already lands right at the new overhang's leading edge, matching tier 0's frontage. The lower tier's rear wall stays at its existing (already-correct) extended height/position — the tier above's slab is 2*halfD deep vs an OVERLAP_ROWS*TREAD overhang, so it still reaches past the wall and closes flush against it; the wall now sits well inside the covered volume instead of at a visible seam. Headroom is asserted, not assumed: rebuild() computes the clearance from the lowest covered tread (row rows-OVERLAP_ROWS of the tier below) to the tier above's soffit and throws if it undercuts ROOF_CLEAR. The clearance works out to OVERLAP_ROWS*RISE + ROOF_CLEAR - DECK = 2.62 m for any rows (the rows-dependent terms cancel), 0.37 m over the 2.25 m ROOF_CLEAR minimum. tiers: 1 stays byte-identical (re-verified: sha256 over world-space vertex + instance data unchanged for rows:8 and rows:24, .vtopo sidecar recompiles to the same 180828 bytes / hash). Depth for tiers:3 drops by 2 * OVERLAP_ROWS * TREAD = 4.8 m as expected (rows:8: 24.79m -> 19.99m; rows:10: 29.59m -> 24.79m); height is untouched (rows:8: 20.641m; rows:10: 23.281m — the deck-height rule didn't change).
…stairs, plinth/lift
Adds tierSpec (indexed by tier, 0 = bottom) to f1-grandstand-bay so a stacked
stand is never left floating:
- plinth (tier 0) / lift (tiers >=1) make per-tier height configurable
- support / rearSupport ('columns' | 'wall' | 'none') carry each tier's
cantilever front and brace its rear with X cross-bracing, every foot
computed off surfaceY or true ground, never assumed
- stairs (default true, tiers >=1) run a straight flight — or a folded
switchback when it would not fit inside width — from the lower tier's
promenade to this tier's, reusing a shared guard-rail helper
- roof is now per-tier (default: top tier only), gated by a thrown
ROOF_CLEAR check against the tier above
- tiers may be given directly as the tierSpec array (tiers = array.length)
tiers: 1 with default tierSpec stays byte-identical (sha256 vertex hash
locked in a new test); .vtopo recompiles unchanged for the catalog entry.
The stairs were across the front of the bowl. They are now a stair TOWER on the
bay's END face, travelling along Z parallel to the rake and climbing promenade to
promenade — a scaffold cage bolted to the end, the way a temporary stand's stair
tower is built (Madring IMG_2437).
- `tierSpec.stairs: boolean` -> `stairSide: 'left' | 'right' | 'both' | 'none'`
(default `'right'` for tiers >= 1, `'none'` for tier 0) plus `stairWidth`
(default 1.4 m, f1-stairs' own flight width).
- ONE cage per end face serves the whole stack, not one per tier: successive
tiers step back as they stack, so per-tier cages would interleave standards in
the same volume — and a shared cage makes tier k's head landing and tier k+1's
foot landing the SAME landing, which is what they are in a real stand.
- Each promenade-to-promenade leg folds into a switchback whose two lane runs are
fixed by the geometry (runA - runB is the step back less one landing, runA +
runB the nominal run at the FIA going), so the free variable is the step split,
chosen to keep both lanes' derived going near 280 mm.
- Flights are built from f1-stairs' OWN tread pan, stringer stations and channel
stringer — those three are exported from that module (geometry untouched) so
the stand's access stair and the catalogue flight are literally one part. The
tower adds a raking bottom chord and web per stringer, because f1-stairs' 220 mm
channel is sized for a two-metre catalogue flight, not an eight-metre one.
- `getFootprint()` reports the seating module vs the total width including the
towers, so an emitter tiling a run of bays can place towers only at the ends;
bays mid-run pass `stairSide: 'none'`.
- `getCollisionVolumes()` / `parts.collision`: a first cut at what a physics
engine should get for one placed stand — one convex prism per tier's bowl mass,
one box per support line, one box per stair tower. The roof is excluded by
design: nothing that collides reaches it, and its cantilever is exactly what
would push a whole-model AABB out over the racing line. The group is not
parented to `root` (so it can never reach the visual merge or the .vtopo
compile); `debug: { collision: true }` parents it for a capture.
`tiers: 1` is byte-identical — the sha256 vertex proof still holds and both
`.vtopo` sidecars recompile unchanged.
…irs switches
Miguel on 8r-3t: "looks wrong because the canopy up top doesn't extend over the
whole section like it does for rows 24 tiers 1. Fix that. We should also be able
to config the roof on or off, and config stairs or no stairs so we can squeeze
bays together. Make it obvious."
THE CANOPY. A stack used to park a small lid on the top deck. It now spans the
whole SECTION by default: the rear tie stays exactly where the top tier's own
roof tied it (that tier's bowlTop + ROOF_CLEAR at zBack) and the leading edge
runs out to TIER 0's halfD + ROOF_FRONT — the same z a single-tier bay's roof
leads at, to the millimetre (measured: 5.060 m either way). So 8r-3t reads with
the rows-24 single-tier profile at three-tier height: one rafter falling
ROOF_FALL from the rear tie, then the TIP_U curl opening the front out over the
track. Roof z-extent 20.880 m against a stand 21.045 m deep.
Nothing is forked to do it. rafterAt / membranePanel / bayWeb / roofRafters /
roofEdges / buildRoof / buildColumns / buildFascia are already parameterised by
the layout's zBack/zFront/yBack/yFront, so the canopy is the TOP tier's own
layout with zFront pushed forward. tiers:1 takes an early return — today's three
calls on today's layout object, in today's order.
- Intermediate props: a twenty-metre rafter needs more than its leading-edge
columns, so every tier BELOW the top carries a column line on its own
promenade, on the roof's BAY pitch, up to the rafter underside. Every foot is
computed through surfaceY and asserted to be that promenade; gangway stations
are skipped on the same clear-width test seats are.
- Clearance is solved, not assumed: the rafter line is checked against every
tier's EXPOSED mass top (bowlMassTopAt — not surfaceY, which clamps the rear
strip one rise low) at 24 stations across the depth that tier actually shows
to the sky, and yFront is RAISED until it clears ROOF_CLEAR. Raising the front
only opens the canopy further, so the ROOF_FALL/TIP_U/ROOF_LIFT semantics
survive; a solve that would push the leading edge above its own rear tie means
the stack is wrong, and throws. No config tested needs a raise (0.000 m).
THE TWO SWITCHES, documented with a six-line example at the top of the header.
- roof: 'full' | 'top' | 'none' | 'per-tier', default 'full'. 'top' is the old
top-tier-only canopy; 'none' removes membrane, rafters, columns and fascia.
'per-tier' is the ONLY value that reads tierSpec[k].roof — under every other
value that per-tier field is ignored, so the precedence is explicit rather
than magic.
- stairs: 'none' | 'left' | 'right' | 'both' | 'ends', default 'none', so tiled
bays squeeze together at exactly `width` and the EMITTER decides which bay
gets a tower. 'ends' is a documented alias for 'both'. This one is only the
DEFAULT a tier falls back to: tierSpec[k].stairSide still wins when set.
- getFootprint() is unchanged in shape and reports the RESOLVED stairs;
configure() patches both live.
GATE. {rows:8,width:10} is byte-identical — sha256 vertex proof still
3570efa087f65d4981ccb6826b05f41abd494267e920100f1c578521b24b1674 — and the
.vtopo sidecar is untouched. tsc clean, 447/447 f1-kit tests, 77/77 topology.
…dural config contract
The cage's lowest landing was tier 0's own promenade (DECK 0.95 m + plinth), so
the bottom bays carried no stair and the stand had no way in from grade — the
lower third of every tower read as an empty braced shaft.
- An ENTRY flight now runs grade -> tier 0's promenade: f1-stairs' own tread pan,
channel stringer, double rail, toe board and 180/280 pitch, built by the same
`flightGeometry` as every other flight in the cage.
- It rides INSIDE the cage's existing plan. A folded cage is two lanes wide and
the tier 0 -> tier 1 flight leaves the foot landing's rear edge on the OUTBOARD
lane, so the inboard lane's rear is free to grade; the entry climbs it and
switchbacks at tier 0. `getFootprint()`, the tower's zFront/zRear and the
stair-tower collision box are byte-for-byte what they were. The `'front'`
fallback (a cage whose flights all run straight — ~175 rows) hangs off the
landing's front edge instead and lets zFront grow to cover it.
- The entry flight alone is built without `flightGeometry`'s underslung bottom
chord: that chord hangs 0.46 m below the flight datum, and this flight's datum
is grade. It bears on sole boards instead.
- `stairs: 'none'`, the default `{rows:8,width:10}` and the catalogue
`{rows:2,width:3}` stay byte-identical (sha256 vertex proof); .vtopo unchanged.
`floors`/`tower` (f82c6d4 + 0084aab) were built without the preview -> critique loop. Ran it: four fresh critics against the Madring references (pit-building-render, paddock-club-render, imagen-paddock, f1com-YDRAY-foto-pit-building) over `{count:4, floors:3}` and the tower variant. Three highest-impact fixes, one per iteration: 1. Each Paddock Club storey is a CLOSED volume. It had glazing on the pit-lane face only, so the floor slab and plate were open on their other three sides and the stack read as floating trays over voids. Solid end walls and rear wall, held a party-wall thickness inside the slab and plate so the two horizontal bands stand proud as shadow reveals. 2. The roof oversails the pit lane. It capped flush at the facade; it now cantilevers 3.4 m as a blade thinner than the slab it grows out of, on a dark soffit with a bright leading lip. 3. The blade RAKES (hinged at the facade, falling 0.105 rad), and the parapet became an upstand held 0.5 m back from the slab edge with nothing on the pit-lane side — a band wrapping all four edges added itself to the slab and the two read as one thick white lid. 4. Terraces STEP OUT as they rise (1.7 m, then 2.5 m) instead of stacking flush, which is what turns the red leading edge from a stripe on a sheer wall into a real balcony edge. Both later critics asked for this independently. Scores across the pass: 35 -> 34 -> 56 -> 48 -> 38. The signal plateaued into noise, not the model: the last critic scored 38 while asserting the terraces "sit perfectly plumb on one flat face", which the new `terrace-1.max.z > terrace-0.max.z + 0.5` assertion measures as false. Stopped at the plateau per the loop rule; the remaining gap is render/material evidence, not geometry. - `floors: 1` stays byte-identical (identity test green, .vtopo unchanged). - The 18.5 m datum is now asserted directly on the ROOF DECK, plus tests for the storey envelopes, the stepped terraces and the raked canopy oversailing them.
This reverts commit 68636ae.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Approved by Miguel 2026-09-11 after review of the renders in devlo-racing's Madring ledger. Includes #5 (spectator-bridge deckHeight) — merging this supersedes it.
f1-grandstand-bay
tiers(1–4): stacked bowls, each upper tier cantilevered 3 rows over the one below; per-tiertierSpec(rows, plinth, lift, overlapRows, support, rearSupport, stairSide, stairWidth, roof).roof:full(default — one canopy from the top tier's rear over the whole section to tier 0's front, columns landing on each promenade) |top|none|per-tier.stairs:none(default) |left|right|both— scaffold stair towers on the end faces built from f1-stairs parts, folded flights per tier plus a ground flight;getFootprint()reports seating vs total width so an emitter puts towers only at run ends.getCollisionVolumes()/parts.collision: convex wedge per tier bowl, thin box per column line, box per stair tower, roof excluded (topologyRole 'detail', not parented unlessdebug.collision). Header carries the TODO for collision as a first-class procedural config across the kit.{rows:8,width:10}byte-identical (sha256 vertex proof + unchanged .vtopo).f1-garage-box
floors(1–3): dark-glazed storeys with terrace + red fascia band, roof deck at the official 18.5 m;tower: one-storey glass tower at −X.floors:1byte-identical.f1-stairs
gratingTread,stringerStations,channelStringer(geometry untouched) so the tower reuses the real parts.Tests: f1-kit 448/448, topology 77/77. Consumer: devlo-racing PRs #922–#925, #929 (merged).
Follow-ups landed since the review (2026-09-12)
stairstower now has a flight from grade to tier 0's promenade, built from the same f1-stairs parts; foot asserted on ground.f1-grandstand-bayallowlisted, convex volumes, sweep 0 gaps).floors/towerlook stays. Re-run only once a flank/roof reference exists.Tests: f1-kit + topology 525/525.