1/**2 * Shared seam types between the generation core (pure, testable) and the3 * platform (rendering, DOM). Plain data only — no Three.js classes.4 */56/** Yaw in 90° steps: 0=+x, 1=+z, 2=-x, 3=-z (right-handed, y up). */7export type Dir = 0 | 1 | 2 | 3;89export type Vec3 = readonly [number, number, number];1011/** Parametric centerline segment, in whatever frame the container declares. */12export type CurveSeg =13 | { kind: "line"; a: Vec3; b: Vec3 }14 | {15 /** Circular arc around a vertical axis; y lerps a0→a1 (covers helixes). */16 kind: "arc";17 center: Vec3;18 radius: number;19 /** Start/end angles in radians, world convention: angle θ → (cos θ, sin θ) in xz. */20 a0: number;21 a1: number;22 y0: number;23 y1: number;24 }25 | { kind: "cubic"; p: readonly [Vec3, Vec3, Vec3, Vec3] };2627export type PlacementRole = "track" | "support" | "decor";2829export interface Placement {30 /** Piece id == GLB node name, e.g. "curve-large", "nature:tree_oak". */31 pieceId: string;32 /** World position in grid units (piece origin is its authored bottom-center). */33 pos: Vec3;34 rotation: Dir;35 /** Render with scale(-1,1,1); registry pre-mirrors ports/curves. */36 mirrored?: boolean;37 role: PlacementRole;38 /** Trackside fan: accelerate the marble while it's within ~1.2 units. */39 boost?: number;40 /** Uniform scale, decor only (track pieces are never scaled). */41 scale?: number;42 /** Constant spin for fan blades etc. */43 spin?: { axis: Vec3; radPerSec: number };44 /** Wind-capsule tuning hint (endless fan field, M16). The client45 * derives a capsule of moving air from the fan's geometry + rotation46 * (src/client/fanField.ts); these override the family defaults. All47 * values are WORLD units — endless placements are world-scaled, and48 * this field rides through untouched. `offset` nudges the derived49 * capsule origin (e.g. the boost fan's wash parks over the channel50 * while its housing stays on the ledge). */51 wind?: {52 strength?: number;53 length?: number;54 radius?: number;55 offset?: Vec3;56 /** Pulsed gust (M19a gust walls): the capsule blows only during the57 * duty window of each period, cosine-ramped at the edges. Seconds of58 * SIM time (the advanceTime clock) — never wall time, so replays and59 * headless runs agree. `phase` is an offset in seconds; banks derive60 * travelling waves and opposed pairs derive anti-phase gates purely61 * by phase arithmetic. */62 pulse?: { period: number; duty: number; phase: number };63 /** Vortex column (M19a): force is TANGENTIAL around the capsule axis64 * instead of along it — marbles arc around the column center. */65 swirl?: boolean;66 };67 /** Floating void decor (M18b): client-side bob + drift hint, WORLD68 * units/seconds — the `wind` model: generator-authored, client-69 * implemented, physics-invisible (void decor never grows proxies).70 * amp = vertical bob amplitude; period/phase in sim seconds; drift =71 * slow velocity applied over segment-local time (bounded walk). */72 float?: { amp: number; period: number; phase: number; drift?: Vec3 };73 /** Lap-line gate: renderer restyles this placement distinctly. */74 goal?: boolean;75 /** Endless pipe-color chapter (the island id whose reach this track76 * piece is in) — the client recolors the pipe swatch per chapter. */77 chapter?: number;78 /** Planner zone this placement's window carries ("icy": the client79 * paints the pipe ice-pale so the surface READS — zones must read). */80 zone?: string;81 /** Bouncer pin (M13): the physics stream adds a high-restitution82 * cylinder proxy at this placement (the mushroom bumper on wide island83 * decks — plinko pins; recapture is the wide channel's walls). */84 bumper?: boolean;85}8687/** A pierced bridge deck: the marble MAY drop through onto the channel88 * below (deterministic per lap, client-side). Both points share (x,z). */89export interface FallPoint {90 /** Hole center at the upper channel floor. */91 pos: Vec3;92 /** Landing point on the lower channel floor. */93 landPos: Vec3;94 /** Always taken when crossed (takeable spur drains — end-hole law);95 * default is the ~50% per-lap chance-fall. */96 guaranteed?: boolean;97}9899export interface LoopSegment {100 /** Index into placements, or −1 for a segment whose mesh is shared with101 * another window (a grade crossing's second visit). */102 placementIndex: number;103 /** World-space centerline through this placement, entry → exit. */104 curve: readonly CurveSeg[];105 length: number;106 /** A diamond's other branch (same endpoints) — laps alternate. With107 * `spur` set the branch is a takeable spur instead (endless respawn108 * variety): a ONE-WAY line out over an end-hole diving board that109 * ends diving through the hole onto the receiver channel below —110 * railForSegment then skips the main hook and resumes there. With111 * `braid` set the branch is a braid's alt LANE (endless): the curve112 * runs from this fork window's entry port through the whole wandering113 * alt lane to the MERGE window's exit port cells later —114 * railForSegment then skips the main windows under the braid and115 * resumes at the window starting at that port. */116 alt?: {117 curve: readonly CurveSeg[];118 length: number;119 spur?: boolean;120 braid?: boolean;121 /** Per-curve-part zone tags (M19b braid lane mods): aligned with122 * `curve`; a tag overlays the host window's rail mod at those123 * stations ("icy" | "boost"). Absent = the host mod rides the whole124 * chain (diamonds/spurs, and untagged braids, unchanged). */125 zones?: readonly (string | null)[];126 /** Braid V3: this chain CONTINUES a fork from an earlier segment —127 * it enters at the segment boundary (no fork junction here; the128 * proxy builder must NOT cut a fork mouth into the entry host). */129 contBraid?: boolean;130 /** Braid V3: this chain leaves the segment OPEN — it exits at the131 * boundary (no merge junction here; the next segment's contBraid132 * chain continues it). */133 openBraid?: boolean;134 };135 /** A braid's merge-junction window (endless): the host placement136 * mutated straight → split facing the arriving alt lane; the ribbon137 * proxies stop at its ports and the junction composite carries the138 * marble across (the fork carries the braid's `alt`). */139 braidMerge?: boolean;140 /** Floor-pierce centers at floor height (same space as `curve`): the141 * collision ribbon opens the kit's round r=0.25-grid aperture at each142 * (T2 — the generic ribbon used to cap straight-hole shut). Scale143 * these wherever `curve` is scaled. */144 holes?: readonly Vec3[];145 /** Grade-crossing windows (T2): "cross" (the mesh-carrying visit)146 * stops the ribbon at its ports and emits the authored 4-way junction147 * composite — deck + corner wall stubs, all four ports open; "quiet"148 * (the noMesh second visit) stops the ribbon and emits NOTHING (the149 * deck belongs to the first visit — two overlapping same-height150 * ribbons walled each other's route off). */151 junction?: "cross" | "quiet";152}153154export interface LoopPath {155 segments: readonly LoopSegment[];156 totalLength: number;157}158159export interface TrackStats {160 theme: string;161 paletteIndex: 0 | 1 | 2;162 setting: string;163 pieceCount: number;164 loopCells: number;165 maxDeckY: number;166 /** Distinct piece ids used, for coverage accounting. */167 families: readonly string[];168}169170export interface Track {171 seed: string;172 placements: readonly Placement[];173 loop: LoopPath;174 /** Bridge holes the marble may fall through (chance per lap). */175 falls?: readonly FallPoint[];176 stats: TrackStats;177}178179// ---------------------------------------------------------------------------180// Asset manifest (pipeline output)181// ---------------------------------------------------------------------------182183export interface PieceClassification {184 family: string;185 solid: boolean;186 hole: boolean;187 wide: boolean;188 long: boolean;189 short: boolean;190 double: boolean;191 color: boolean;192 high: boolean;193 low: boolean;194 /** Height/severity tier for ramp/slant/bump/wave/s-curve-short. */195 tier: "a" | "b" | "c" | "d" | null;196 size: "medium" | "large" | null;197 chirality: "left" | "right" | null;198 /** Helix sweep; full helixes carry no token. */199 span: "quarter" | "half" | "full" | null;200 /** Support post count / stack segment. */201 count: "single" | "two" | "four" | null;202 segment: "bottom" | "middle" | "top" | "top-double" | null;203}204205export interface ManifestPiece extends PieceClassification {206 id: string;207 bboxMin: Vec3;208 bboxMax: Vec3;209 /** Grid cells occupied, derived from bbox at rotation 0. */210 footprint: readonly [number, number];211 vertices: number;212 triangles: number;213}214215export interface PackManifest {216 version: 1;217 pack: string;218 slug: string;219 license: string;220 glbPath: string;221 glbBytes: number;222 pieceCount: number;223 pieces: readonly ManifestPiece[];224}