Let a joint glide on a rail and drive it with a motor (#154)
game-maker / gate (push) Successful in 3m4s
game-maker / suite (push) Successful in 14m2s

Step 2 of the joints: a door that opens by itself, a lift and a
piston need a joint that slides along one line and a motor that turns
or pushes. A graph also needs to read how far such a joint stands.

- Slider: a fifth joint type on Rapier's prismatic joint. Its rail is
  the joint's own x axis, so turning the joint turns the rail. Joints
  now carry Scale, Rotation and Mirror like a sprite.
- Limit distance with Min and Max, in world units from where the
  object was placed.
- Motor on hinge and slider: Off, Speed or Target, with Motor speed,
  Motor target and Max force (0 = no limit). Target goes there at no
  more than the speed and slows down for the last stretch.
- Changing speed, target or force keeps the engine joint. Switching
  the motor on or off builds it again.
- New node Joint position: value and speed of a hinge (degrees,
  counting past a full round) or a slider (world units).
- Scene view draws a slider's rail dashed, with a tick at each limit.
- shownFor in the schema takes a list of choices that must all hold.

Findings, measured with a probe against Rapier 0.20:
- A prismatic joint holds both bodies at one angle. Held to the scene
  or to an object without a rigidbody, the joint's own anchor body is
  turned by the placed difference, so nothing jumps. Between two
  rigidbodies the held one is turned to match, and the console says
  so once.
- Rapier reads no joint angle back. The position is worked out from
  the two bodies.
- A hinge motor is weak on a small body at the end of a long arm
  (box 20 on an arm of 100 does not lift itself). More solver
  iterations would help but cost every scene. A door of 100 x 20
  reaches -90 and holds it. This is written in the wiki.

Tests:
- Unit: 16 new in joints.test.ts, 1 in joint-links, 3 catalog cases.
- e2e 41: 27 checks (25 + 2), block 41f with a real mouse in the
  inspector and the motor in play (59.1 of 60 units in 1 s).
- 9 of 9 unit counter-probes fail as they should.
- Gate: 220 files, 3436 tests (3416 + 20), CHECK EXIT 0.
- e2e total 1701 is counted (1699 + 2), the full suite did not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-03 10:09:45 +02:00
1 parent 4716726b6b
commit f1e3bd8378
16 files changed
+738 -34

No files matched your search

+55
View File
@@ -661,6 +661,61 @@ export async function physics(t) {
hints: consoleLog.filter((line) => line.includes('[physics]')).slice(jointHints),
}),
);
// 41f. The same joint made a slider with a motor (#154, step 2): its rows follow the choices, its
// rail is drawn along the joint as it is turned, and in play the motor drives the bob up it.
await selectObject(joint.id);
await pressAt(await inView(t, '__gm.inspectorRect()', "__gm.inspectorField('Type')"), 'the Type field');
await pressAt(await json("__gm.locate('Slider', true)"), 'Slider in the menu');
await pressAt(await inView(t, '__gm.inspectorRect()', "__gm.inspectorField('Motor')"), 'the Motor field');
await pressAt(await json("__gm.locate('Speed', true)"), 'Speed in the menu');
await mutate(`
const o = __gm.state.activeScene.objects.find((o) => o.id === '${joint.id}');
Object.assign(o.overrides, { rotation: 90, motorSpeed: -60 })`);
await settle();
const railLink = (await links()).links.find((l) => l.id === joint.id);
check(
'a slider with a speed motor shows its rows and draws its rail along the turned joint',
(await jointObject())?.overrides?.jointType === 'slider' &&
(await jointObject())?.overrides?.motor === 'speed' &&
(await has('Limit distance')) &&
(await has('Motor speed')) &&
(await has('Max force (0 = no limit)')) &&
!(await has('Motor target')) &&
!(await has('Connected anchor X')) &&
railLink?.rail?.limited === false &&
Math.abs(railLink.rail.from.x) < 0.01 &&
Math.abs(railLink.rail.to.y - railLink.rail.from.y - 120) < 0.01,
JSON.stringify({ railLink, overrides: (await jointObject())?.overrides }),
);
await select(null);
await play();
await evaluate(`(() => {
const out = (window.__sliderSamples = []);
const start = performance.now();
const sample = () => {
const w = __gm.world();
if (!w) return;
const bob = w.entities.find((e) => e.objectId === 'phys-bob');
out.push({ t: performance.now() - start, x: bob?.worldX, y: bob?.worldY });
if (performance.now() - start < 1000) requestAnimationFrame(sample);
};
requestAnimationFrame(sample);
})()`);
await sleep(1300);
const slid = await json('window.__sliderSamples');
await stop();
// From the first sample to the last, at 60 units a second up the rail.
const span = slid.length > 1 ? (slid.at(-1).t - slid[0].t) / 1000 : 0;
const climbed = slid.length > 1 ? slid[0].y - slid.at(-1).y : 0;
check(
'in the played scene the motor drives the bob up its rail at its speed, and not off it',
slid.length >= 5 &&
span > 0.8 &&
Math.abs(climbed - 60 * span) < 6 &&
slid.every((s) => Math.abs(s.x) < 0.5),
JSON.stringify({ frames: slid.length, span, climbed, first: slid[0], last: slid.at(-1) }),
);
await mutate(`
const p = __gm.state.project;
const scene = __gm.state.activeScene;