Final fix in the chain: the drop-cup bottom pan must deliver the marble ONTO the receiver ribbon sheet from slightly above - arriving even 0.03 below a one-sided mesh means catching its edge forever (the parked-in-cup stall). With that, all survivability seeds pass on the ribbon: continuous welded channel mesh with identifyEdges, boundary-port extrapolation, crease rounding, tall containment walls, and curvature-aware braking in the shared assist law (fast chute arrivals brake into curves instead of launching over the walls). 65 tests green, ribbon is the default.
- commit
- aa81e760df15fdb4c0641e36fc7ff59c24cef5eb
- parent
- 6235f79f240b59462d4f0eb1ccd6b552f858f9bc
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 14 | ++++++-- |
| M | src/ | 9 | ++++- |
| M | src/ | 53 | +++++++++++++++++++++++++++--- |
| M | tests/ | 14 | ++++++-- |
@@ -551,11 +551,19 @@ class Session {
551551 if (!rail) return;552552 const t = rail.tans[this.nearest.idx] as [number, number, number];553553 let climbTy = t[1];554- for (let k = 1; k <= 6; k++) {554+ let curveAhead = 0;555+ for (let k = 1; k <= 8; k++) {555556 const look = rail.tans[this.nearest.idx + k];556- if (look) climbTy = Math.max(climbTy, look[1]);557+ const prev = rail.tans[this.nearest.idx + k - 1];558+ if (!look || !prev) break;559+ climbTy = Math.max(climbTy, look[1]);560+ const dot = Math.min(561+ 1,562+ look[0] * prev[0] + look[1] * prev[1] + look[2] * prev[2],563+ );564+ curveAhead += Math.acos(Math.max(-1, dot));557565 }558- const f = assistForce(st.vel, t, engine.mass, climbTy);566+ const f = assistForce(st.vel, t, engine.mass, climbTy, curveAhead);559567 if (f) engine.force(f[0], f[1], f[2]);560568 });561569 const st = engine.state();@@ -86,6 +86,10 @@ export function assistForce(
8686 * sized for the ramp AHEAD, not the base station, or the marble cycles8787 * at the foot of every ramp (under-powered mid-climb, rolls back). */8888 climbTy: number = tangent[1],89+ /** Radians of heading change over the next ~4 units of rail — fast90+ * arrivals into tight curves launch over the walls; brake to a safe91+ * entry speed instead (the driver-side speed envelope). */92+ curveAhead = 0,8993 ): [number, number, number] | null {9094 // Near-vertical rail (drop shafts): the fall is gravity's job — an9195 // assist here just grinds the marble into the cup.@@ -98,8 +102,9 @@ export function assistForce(
98102 // envelope) and banked/taller walls, not a bigger constant.99103 const TARGET = 2.6;100104 const MAX = 7.5;101- if (vAlong > MAX) {102- const f = mass * 1.8;105+ const safe = Math.max(TARGET, MAX - curveAhead * 4.5);106+ if (vAlong > safe) {107+ const f = mass * Math.min(6, 2 + (vAlong - safe) * 1.5);103108 return [-tangent[0] * f, -tangent[1] * f, -tangent[2] * f];104109 }105110 const climbing = tangent[1] > 0.12 || climbTy > 0.12;@@ -51,7 +51,8 @@ export type CollisionProxy = SlabProxy | BoxProxy | MeshProxy;
5151 * slalom scale: flat floor, gutter fillets, near-vertical walls with a5252 * slight outward flare — the shape a marble rolls smoothly in. */5353 const PROFILE: readonly (readonly [number, number])[] = [54- [-0.88, 0.66],54+ [-0.92, 1.1],55+ [-0.86, 0.66],5556 [-0.80, 0.52],5657 [-0.74, 0.16],5758 [-0.62, 0.03],@@ -60,7 +61,8 @@ const PROFILE: readonly (readonly [number, number])[] = [
6061 [0.62, 0.03],6162 [0.74, 0.16],6263 [0.80, 0.52],63- [0.88, 0.66],64+ [0.86, 0.66],65+ [0.92, 1.1],6466 ];6567 6668 /** Sampling step along the centerline, world units. */@@ -105,7 +107,7 @@ interface Station {
105107 * guide tube opens toward the receiver's exit heading). */106108 export function channelProxies(107109 path: LoopPath,108- useRibbon = false,110+ useRibbon = true,109111 ): CollisionProxy[] {110112 const stations: Station[] = [];111113 for (const seg of path.segments) {@@ -229,9 +231,11 @@ export function channelProxies(
229231 });230232 }231233 // Bottom pan: the full shaft cross-section at receiver-floor level.234+ // Its top sits a hair ABOVE the receiver ribbon sheet — arriving235+ // even 0.03 below a one-sided mesh means catching its edge forever.232236 out.push({233237 kind: "slab",234- center: [px, botY - FLOOR_HALF_T - SINK, pz],238+ center: [px, botY - FLOOR_HALF_T + 0.03, pz],235239 half: [0.75, FLOOR_HALF_T, 0.75],236240 yaw: st.yaw,237241 pitch: 0,@@ -312,11 +316,50 @@ function ribbonOf(
312316 ],313317 };314318 };315- const source = [319+ const raw = [316320 ...(extendStart ? [extrapolate(strip[0], -1)] : []),317321 ...strip,318322 ...(extendEnd ? [extrapolate(strip[strip.length - 1], 1)] : []),319323 ];324+ // Valley/crest rounding: a sharp pitch (or yaw) crease between rings is325+ // a KICKER — a marble arriving fast reflects off the V and launches326+ // over the wall (chute valleys threw marbles laterally; slabs had327+ // incidentally rounded these with overlaps). Subdivide any step whose328+ // frame turns faster than MAX_TURN into blended rings.329+ const source: Station[] = [];330+ const angleDelta = (a: number, b: number): number => {331+ let d = b - a;332+ while (d > Math.PI) d -= 2 * Math.PI;333+ while (d < -Math.PI) d += 2 * Math.PI;334+ return d;335+ };336+ for (let i = 0; i < raw.length; i++) {337+ if (i === 0) {338+ source.push(raw[i]);339+ continue;340+ }341+ const a = raw[i - 1];342+ const b = raw[i];343+ const dPitch = b.pitch - a.pitch;344+ const dYaw = angleDelta(a.yaw, b.yaw);345+ const steps = Math.max(346+ 1,347+ Math.ceil(Math.max(Math.abs(dPitch), Math.abs(dYaw)) / MAX_TURN),348+ );349+ for (let j = 1; j <= steps; j++) {350+ const t = j / steps;351+ source.push({352+ ...b,353+ p: [354+ a.p[0] + (b.p[0] - a.p[0]) * t,355+ a.p[1] + (b.p[1] - a.p[1]) * t,356+ a.p[2] + (b.p[2] - a.p[2]) * t,357+ ],358+ yaw: a.yaw + dYaw * t,359+ pitch: a.pitch + dPitch * t,360+ });361+ }362+ }320363 for (const st of source) {321364 if (322365 last &&@@ -141,11 +141,19 @@ function driveWith(
141141 }142142 const t = rail.tans[nearest];143143 let climbTy = t[1];144- for (let k2 = 1; k2 <= 6; k2++) {144+ let curveAhead = 0;145+ for (let k2 = 1; k2 <= 8; k2++) {145146 const look = rail.tans[nearest + k2];146- if (look) climbTy = Math.max(climbTy, look[1]);147+ const prev = rail.tans[nearest + k2 - 1];148+ if (!look || !prev) break;149+ climbTy = Math.max(climbTy, look[1]);150+ const dot = Math.min(151+ 1,152+ look[0] * prev[0] + look[1] * prev[1] + look[2] * prev[2],153+ );154+ curveAhead += Math.acos(Math.max(-1, dot));147155 }148- const f = assistForce(st.vel, t, engine.mass, climbTy);156+ const f = assistForce(st.vel, t, engine.mass, climbTy, curveAhead);149157 if (f) engine.force(f[0], f[1], f[2]);150158 });151159 st = engine.state();