USER: raw physics fell into the void on curves. The sim now runs the actual control law per fixed step — nearest station on the plot's rail, autopilotForce with per-marble personalities — and fallen marbles respawn at the entry, so the piece carries continuous traffic for the whole 15s run.
- commit
- d255a424051d6c4e8b7561cb7a3a8233a7ba43af
- parent
- 6854fdd05be0b10a963c0fd8e3c35d08095773ff
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 69 | ++++++++++++++++++++++++------ |
@@ -38,6 +38,7 @@ import {
3838 PhysicsEngine,3939 type PhysicsModule,4040 } from "./physics/engine.ts";41+import { autopilotForce, pilotFor } from "./physics/autopilot.ts";4142 import type { Dir } from "../shared/types.ts";4243 4344 interface Plot {@@ -338,11 +339,26 @@ export function buildPieceBench(ctx: SceneCtx, assets: Assets): void {
338339 color: 0x9adcff,339340 roughness: 0.3,340341 });342+ const simNearest: number[] = [];341343 const clearSim = () => {342344 for (const m of simMarbles) root.remove(m);343345 simMarbles.length = 0;346+ simNearest.length = 0;344347 simPlot = null;345348 };349+ const simSpawn = (i: number) => {350+ if (!engine || !simPlot) return;351+ const rail = simPlot.rail;352+ const t0 = rail.tans[0];353+ const lane = (i - 2) * 0.12;354+ const nl = Math.hypot(t0[2], t0[0]) || 1;355+ engine.spawnMarble(i, [356+ rail.pts[0][0] + (t0[2] / nl) * lane,357+ rail.pts[0][1] + 0.3 + i * 0.05,358+ rail.pts[0][2] + (-t0[0] / nl) * lane,359+ ], [t0[0] * 2.2, 0, t0[2] * 2.2]);360+ simNearest[i] = 0;361+ };346362 mkBtn("simulate", async () => {347363 if (!selected) {348364 status.textContent = "select a piece first";@@ -364,16 +380,9 @@ export function buildPieceBench(ctx: SceneCtx, assets: Assets): void {
364380 engine = new PhysicsEngine(physModule);365381 // Proxies live in plot-local space — the engine world IS this plot.366382 engine.addSegment(0, selected.proxies);367- const rail = selected.rail;368- const t0 = rail.tans[0];383+ simPlot = selected;369384 for (let i = 0; i < 5; i++) {370- const lane = (i - 2) * 0.12;371- const nl = Math.hypot(t0[2], t0[0]) || 1;372- engine.spawnMarble(i, [373- rail.pts[0][0] + (t0[2] / nl) * lane,374- rail.pts[0][1] + 0.3 + i * 0.05,375- rail.pts[0][2] + (-t0[0] / nl) * lane,376- ], [t0[0] * 3, 0, t0[2] * 3]);385+ simSpawn(i);377386 const mesh = new THREE.Mesh(378387 new THREE.SphereGeometry(MARBLE_RADIUS, 14, 10),379388 marbleMat,@@ -381,7 +390,6 @@ export function buildPieceBench(ctx: SceneCtx, assets: Assets): void {
381390 root.add(mesh);382391 simMarbles.push(mesh);383392 }384- simPlot = selected;385393 simT = 0;386394 status.textContent = `simulating ${selected.label}`;387395 });@@ -476,13 +484,48 @@ export function buildPieceBench(ctx: SceneCtx, assets: Assets): void {
476484 onPick(plot);477485 });478486 479- // Sim stepping + marble mirroring.487+ // Sim stepping: the REAL wishdir autopilot steers each marble across488+ // the piece (USER: raw physics just fell into the void on curves) —489+ // nearest station on the plot's rail, autopilotForce, per-marble490+ // personality. Fallen marbles respawn at the entry, so the piece gets491+ // continuous traffic for the whole run.480492 ctx.onFrame.add((dt) => {481493 if (!engine || !simPlot) return;482494 simT += dt;483- engine.step(Math.min(dt, 0.05));495+ const rail = simPlot.rail;496+ engine.step(Math.min(dt, 0.05), () => {497+ for (let i = 0; i < simMarbles.length; i++) {498+ const st = engine!.state(i);499+ let best = simNearest[i] ?? 0;500+ let bestD = Infinity;501+ const lo = Math.max(0, best - 20);502+ const hi = Math.min(rail.pts.length - 1, best + 30);503+ for (let k = lo; k <= hi; k++) {504+ const r = rail.pts[k];505+ const d = (r[0] - st.pos[0]) ** 2 + (r[1] - st.pos[1]) ** 2 +506+ (r[2] - st.pos[2]) ** 2;507+ if (d < bestD) {508+ bestD = d;509+ best = k;510+ }511+ }512+ simNearest[i] = best;513+ const f = autopilotForce(514+ rail,515+ best,516+ st.pos,517+ st.vel,518+ engine!.massOf(i),519+ null,520+ pilotFor("bench", i),521+ );522+ if (f) engine!.force(i, f[0], f[1], f[2]);523+ }524+ });525+ const floorY = rail.pts.reduce((a, p) => Math.min(a, p[1]), Infinity);484526 for (let i = 0; i < simMarbles.length; i++) {485527 const st = engine.state(i);528+ if (st.pos[1] < floorY - 4) simSpawn(i);486529 simMarbles[i].position.set(487530 simPlot.origin.x + st.pos[0],488531 simPlot.origin.y + st.pos[1],@@ -490,7 +533,7 @@ export function buildPieceBench(ctx: SceneCtx, assets: Assets): void {
490533 );491534 simMarbles[i].quaternion.set(...st.quat);492535 }493- if (simT > 12) clearSim();536+ if (simT > 15) clearSim();494537 });495538 496539 // 'x' toggles collision ('c' belongs to the camera-mode toggle).