Files
feuerwerk/src/viewport/jointLinks.ts
T
adminandClaude Opus 5.5 f1e3bd8378
game-maker / gate (push) Successful in 3m4s
game-maker / suite (push) Successful in 14m2s
Let a joint glide on a rail and drive it with a motor (#154)
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>
2026-10-03 10:09:45 +02:00

56 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { hasConnectedAnchor, JOINT_CONNECTED_KEY } from '../core/model/joint';
import type { Mat } from '../core/model/matrix';
import { referenceCandidates, referencedId } from '../core/model/objectRefs';
import type { Vec2 } from '../core/model/Properties';
import { jointFarEnd, jointSettings, sliderAxis, sliderRail } from '../core/properties/joint';
import type { DrawItem } from './items';
/** What the scene view draws of a joint (#154): from where it sits to what it holds on to. */
export interface JointLink {
id: string;
/** Where the joint sits – the point of its own object it holds. */
from: Vec2;
/**
* Where the line ends: a spring's or a rope's connected anchor, for a hinge and a fixed joint the
* connected object itself. Equal to `from` when such a joint is held to the scene.
*/
to: Vec2;
/** The object it is connected to as the view shows it; '' for the scene, or for one that is gone. */
connectedId: string;
/** Whether `to` is a point of its own, which can be dragged. */
anchored: boolean;
/** The frame the connected anchor is counted in; null for the scene's. */
frame: Mat | null;
/** A slider's rail through where it sits; `limited` when its ends are the limits, which get a tick. */
rail: { from: Vec2; to: Vec2; limited: boolean } | null;
}
/** The item a joint's reference names, looked up from where the joint sits (see `objectRefs.ts`). */
function connectedItem(item: DrawItem, byId: ReadonlyMap<string, DrawItem>): DrawItem | null {
const ref = referencedId(item.props[JOINT_CONNECTED_KEY]);
for (const id of referenceCandidates(item.id, ref)) {
const found = byId.get(id);
if (found) return found;
}
return null;
}
export function jointLinkOf(item: DrawItem, byId: ReadonlyMap<string, DrawItem>): JointLink | null {
if (item.type !== 'joint') return null;
const settings = jointSettings(item.props);
const connected = connectedItem(item, byId);
const from = { x: item.x, y: item.y };
const anchored = hasConnectedAnchor(settings.type);
const frame = connected?.m ?? null;
const to = anchored
? jointFarEnd(settings, from, frame)
: connected
? { x: connected.x, y: connected.y }
: from;
const rail =
settings.type === 'slider'
? { ...sliderRail(settings, from, sliderAxis(item.m)), limited: settings.distanceLimits !== null }
: null;
return { id: item.id, from, to, connectedId: connected?.id ?? '', anchored, frame, rail };
}