Physics: joints tie two objects together (#154)

Physics has a new section Joints for the hinge, the spring, the rope and the fixed joint. Concepts, Behaviors and Architecture name the new asset type and the node Add joint. Development counts 1691 e2e checks.
admin committed 2026-10-02 21:43:18 +02:00
1 parent 6e7ecb0260
commit 0665b682e2
5 files changed
+66 -9

No files matched your search

+3 -2
@@ -117,7 +117,7 @@ web/play.html a released game (#266) play.html built pag
src/main.ts, play.ts the two entries; page.ts is the canvas both are (the WebGL context, root, resize)
src/core/model/ Project (object holders), Asset + AssetTypes, SceneObject (and what a list calls one:
`objectName`, `groupName`), Properties (value types, and the walks over a nested value: `mapNested`, a struct field taken out or renamed), hierarchy, walk (generic parent/child walks), instance (container content resolved from its template), usages (where an asset is used), container (the cycle guard), placement (what may be dropped where), texture (frames, the slice grid, a frame's collision outlines), frameOutline (#193: a frame's outlines as a collider wears them, centred, a pixel to a unit, cut into pieces once per drawn list), traceCollision (#161: a frame's outlines read off its picture — the pixels from an alpha threshold on are solid, each 4-connected island is walked round along the pixel edges and cut down to a few corners without ever crossing itself; crumbs below 1 % of the frame drop out), generated (pictures that are drawn rather than uploaded, #185: the key `gen:<assetId>@<revision>`, the register of readers that tell the picture path an asset's canvas size and revision, and the `fingerprint` pixel art and vector art make their revision with), vectorart (#129: the canvas as a property, which ring is worth drawing, and the revision as a fingerprint of the drawing rather than a counter, a texture fill's picture included) + vectorPath (what a shape draws, as plain commands: closed rings fill, every ring strokes, a segment is a line while neither of its ends carries a handle; a paint resolved to sorted stops or the picture it names) + vectorPaint (#186: gradients and texture fills — where a paint lies when it is switched on, the colour between two stops, adding, moving and removing a stop, a paint moved with its shape, and the rule that a texture fill takes uploaded pictures only) + vectorBoolean (#187: union, subtract, intersect and exclude on the sampled outlines, each shape read by the even-odd rule; the result rings in the order Pixi's hole detection needs, every outline followed by its holes and preceded by the islands inside them) + vectorCurves (#208: every sampled point remembers its cubic and its `t`, so a run of result points along one cubic becomes that cubic again, cut with de Casteljau), pixelart (#130: the palette a picture is painted in, how the layers of a frame come together, the sheet of all frames, a frame id read as a cut of it, and the revision) + pixelCel (the file of one layer on one frame, and its SHA-256 name worked out in the browser) + pixelDraw (what the tools do to the indices: brush, line, rectangle, ellipse, fill, resize about an anchor, merge, a palette colour taken out) + pixelEdit (every edit of layers, frames and cels as a function from one state to the next), polygon (an outline checked, cut into convex pieces and clipped to a rectangle), tile (the three kinds and their block shapes, reading a tile's pictures, the animation step), autotile (which quarter tile a neighbourhood asks for, the 47/16 tiles a block produces, 1 for a plain tile), tilemap (the cells of a layer, which neighbours a cell is joined to, the cells a stroke or a rectangle covers), tilemapCollision (the solid cells of a map, drawn in or out by their tile's collision scale and merged into rectangles, one group per one-way angle and per surface (`tileMaterials`, #193), the outlined cells as polygons — an autotile's outlines cut into its quarters and pieced together per cell like its picture —, where one sits in the world, and which cells of a placed map a world rectangle can reach, in cells (`cellsWithin`, #184)), tilemapMemo (what a map's cells work out to, kept while they hold the same: a version per cell map compared cell by cell, since strokes paint in place), animation (which frame at time t, a row's value there, what a row may aim at, the players on an object; KeyRef and its lookups) + animationPreview (the root at a frame, written into copies), curve (#247: a drawn easing evaluated at t — each segment a cubic solved for t by bisection, held to its rules by `sanitizedCurve`: ends at 0 and 1, no point overtaking another, no handle past its neighbour — and which keys and `Lerp` nodes use one) + curvePresets (the built-in eases traced as curves, a segment halved only where it strays), graphLookup (what a graph sees of the project: behaviors to run, state machines to read, curves to ease along), structTypes (#253: Struct assets as types — every typed struct variable, struct field and `Pack`/`Expand` found and brought in line with its asset, a renamed field carried through every value first, the cycle rule), Scope (how long a shared value lives: per instance, one for the run, or one per scene and rebuilt with every scene that loads, #164 — for a state machine's current state and for the values of a Variables asset), collisionLayers (which colliders may meet: the project's named layers, a collider's layer and mask, and the one rule `layersMeet` the engine, Solids and the event pair search all answer by)
src/core/properties/ PropertySchema, kinds (one row per value kind), listItems (an item's kind, read off its value), resolve (the only override merge), deviation (what is gold), collider/camera/bounds geometry, clip (the region clip areas cut each object to, a convex polygon, #260) (with the side a one-way collider blocks from, and the rectangle around a shape a broad phase looks at first, #184), convex (capsules and polygons: rounded convex pieces against each other), ray (where a ray meets a shape, in double precision), rigidbody (what the engine reads off a rigidbody, a collider's material and the scene), aspect, enabled, editorRun, assetRef/textureRef (how a reference reads), declaredRows (the row a Property node declares in a shader or particle graph, and the params that node has in both)
src/core/properties/ PropertySchema, kinds (one row per value kind), listItems (an item's kind, read off its value), resolve (the only override merge), deviation (what is gold), collider/camera/bounds geometry, clip (the region clip areas cut each object to, a convex polygon, #260) (with the side a one-way collider blocks from, and the rectangle around a shape a broad phase looks at first, #184), convex (capsules and polygons: rounded convex pieces against each other), ray (where a ray meets a shape, in double precision), rigidbody (what the engine reads off a rigidbody, a collider's material and the scene), joint (what it reads off a joint: its settings, where its far end is, its length and a hinge's limits, #154), aspect, enabled, editorRun, assetRef/textureRef (how a reference reads), declaredRows (the row a Property node declares in a shader or particle graph, and the params that node has in both)
src/core/util/ list (take out, put back and move an item — what a command's undo is made of), vec (dot, the closest point on a segment — shared by the collider maths, the outline hit area and the polygon grips; a cubic Bezier at `t` and cut in two at `t` — shared by vector art, its Boolean operations and the curve grips), color (`mixColor`, channel by channel — keys of an animation, stops of a gradient, the particles' Mix colour), recorder (#253: assignments taken back last first — the undo of an edit whose reach is not known in advance)
src/core/commands/ undoable commands (pixelArtCommands: one command runs any pixel art edit and keeps the state before it, #130), CommandStack, SetBagValueCommand base, RenameCommand base (undone by renaming back: variable, struct field, port), MoveCommand base (undone by moving back: tile picture, tilemap layer, vector shape), vectorArtCommands (#129: the shapes of a drawing, one gesture one command), one import command shape for texture/font/sound, require.ts (validated lookups, and what an instance may not change), naming.ts
src/core/graph/ graph model, NodeRegistry/NodeContext/values (a travelling list may hold live objects, `storedList`
@@ -161,7 +161,8 @@ src/runtime/physics/ engine (loads Rapier once, reports its state, converts be
what the physics nodes reach through `NodeContext.physics`), oneWay (the rule itself:
whether a body lands on, is held by or passes a one-way collider — pure, so it is measured
without the engine), bodyGeometry (a world shape as the engine colliders of a body, and the
poses handed between the two)
poses handed between the two), joints (one engine joint per joint entity whose object has
a rigidbody, and a body of the joint's own for an end that has none, #154)
src/ui/ widget toolkit (generic, knows nothing about assets); fonts (the faces the editor ships:
Source Sans 3 in 400, 500 and 600, Source Code Pro 400, Latin only, #202/#214/#273), icons +
Icon (the menu's tick boxes and current-entry dot as SVG files, and a widget that shows one file
+1 -1
@@ -231,7 +231,7 @@ playing use the same two words.
`Key was pressed` and `Key was released` (true for the one frame in which the key went down or came up, the frame `On key down` and `On key up` fire in; a key that stays held is not pressed again),
`Mouse position`, `Mouse button`, `Is hovered`, `Is touching`, `Layer of` (the collision layer of an object by name and number, see [Colliders](Colliders)), `Animation playing` (whether an animation runs on an object, its progress and time left) and `Animation length` (see [Animations](Animations)), `Get velocity`, `Current state` (name and id), `Is in state` (one boolean per state) ·
actions `Move` (dx, dy, scale, dt — connect an event's dt for per-second movement; in the object's own axes, which a turned parent turns too; with *Collide with solids* it slides along what it hits, less friction, and outputs `blocked x/y`, `hit` and the contact `normal x/y`, see [Colliders](Colliders)), `Set position`, `Spawn`
(asset input + x/y/z, optional parent → outputs the new object; the asset is picked on the node or arrives over an edge, from an `Asset` node, a variable or a list item. A pick saved while it was still a setting reads as that input's value, so older projects spawn as before; the same holds for the asset of `Is asset`), `Destroy` (removes the object with everything below it; with **keep children** on, only the object goes: its children move up to its parent (top level when it had none) and keep their place, rotation and size in the world, and the output `children` is the list of them, nearest level only. Its behaviors and its rigidbody go with it either way. In the editor run, children that are content of a placed container move up for the run only: their place belongs to the template, so the scene does not learn it), `Emit` (starts a particle source over, see *Particles*),
(asset input + x/y/z, optional parent → outputs the new object; the asset is picked on the node or arrives over an edge, from an `Asset` node, a variable or a list item. A pick saved while it was still a setting reads as that input's value, so older projects spawn as before; the same holds for the asset of `Is asset`), `Destroy` (removes the object with everything below it; with **keep children** on, only the object goes: its children move up to its parent (top level when it had none) and keep their place, rotation and size in the world, and the output `children` is the list of them, nearest level only. Its behaviors, its rigidbody and its joints go with it either way. In the editor run, children that are content of a placed container move up for the run only: their place belongs to the template, so the scene does not learn it), `Add joint` (ties an object to another while the game runs: the picked Joint asset is placed on *object* at x/y and connected to *connected*, the scene when nothing arrives; outputs the new joint, which `Destroy` lets go again, see [Physics](Physics#joints)), `Emit` (starts a particle source over, see *Particles*),
`Play sound` / `Stop sound` (see [Sounds](Sounds): the asset says how loud and whether it loops, the node's
`volume` multiplies that and its `loop` either leaves it alone or overrules it in either
direction; every play is a voice of its own, so two in a row overlap, and the voice ends with the
+3 -2
@@ -20,6 +20,7 @@
| **Collider** | A shape (box or circle) for collision detection, placed as a **child** of the object it belongs to (its *body*). Drawn as a green outline in edit mode only; its area is tinted only while the collider itself is selected. |
| **Collision layer** | One of up to 16 named layers of the project (**menu → Project settings…**). A collider or tilemap names the layer it is on and the layers it **collides with**; two of them meet only when each one names the layer of the other. A new project has one layer, *Default*, which every collider is on and meets — so nothing collides differently until a layer is named. See [Colliders](Colliders#collision-layers). |
| **Rigidbody** | Placed as a **child** of an object, it hands that object to the physics engine while playing: it falls with the scene's gravity, bounces and rests on colliders, pushed by what it meets. At most one per object. See [Physics](Physics). |
| **Joint** | Placed as a **child** of an object with a rigidbody, it ties that object to another one or to the scene while playing: a hinge, a spring, a rope or a fixed joint. Where it sits is the point it holds. See [Physics](Physics#joints). |
| **Override** | A per-object value that replaces the asset's value for one property. Only changed keys are stored; the inspector paints their label gold and offers a reset (↺) and an **apply** (↑) that writes the value into the asset instead. Both appear only where the value really differs from its base — an override carrying the asset's own value is none. |
| **Default** | Every property has a schema default. Asset values that differ from it are marked gold in the inspector and can be reset (↺) the same way an override can. The asset's name in the **Assets** panel carries the same gold while any of them does (`assetDeviates`, `src/core/properties/deviation.ts` — the inspector marks the rows of that same schema, so the list cannot claim a deviation the inspector then fails to show). A behavior's variables are not in it: their default is declared on the variable itself, so the asset value cannot differ from it. |
| **Instance** | An object resolved from a placed container's template. Its **values** can be edited — position, name, property overrides — and only what actually deviates is stored, as a sparse `instanceEdits` entry on the container object keyed by the template path. Everything else keeps following the template. Its **structure** cannot: it can not be removed, re-parented or given a collider, and nothing of the scene can be parented into it. ↺ ▾ resets to the nearest base, which inside a container is the **template as the containers in between leave it**; ↑ ▾ offers **Apply to container** (write the value into the template, where every other instance still at the old value picks it up) and/or **Apply to asset** (which drops the template's override as well). With one base left the menu collapses to a plain ↺ / ↑, with none the buttons disappear. Gold marks the same thing the buttons offer. |
@@ -195,8 +196,8 @@ Right-clicking a row offers *Set as active scene*, *Set as start scene* / *Clear
*Create sprite from texture*, *Create animation from texture* and *Delete…*. In
**Scene objects** it offers *Select asset*, *Open asset* (the same as double-clicking the asset
in the asset panel; greyed out for a kind that has no editor of its own, such as a sprite or a
camera), *Add collider…* and *Add rigidbody…* (each opens a searchable menu: a new one or one
the project has, see [Colliders](Colliders)), *Fit to sprite* on a collider, *Create container*,
camera), *Add collider…*, *Add rigidbody…* and *Add joint…* (each opens a searchable menu: a new one
or one the project has, see [Colliders](Colliders) and [Physics](Physics#joints)), *Fit to sprite* on a collider, *Create container*,
*Unpack container*, *Unparent*, *Expand all* / *Collapse all* and *Remove*. The scene view's right-click menu is the same.
Both menus also offer **one *Open …* entry per asset the values point at** (#221): a sprite
wearing a picture and a shader has *Open texture* and *Open shader*, in the asset panel at the
+1 -1
@@ -18,7 +18,7 @@ php tests/php/accounts.test.php # the API's accoun
# has and the web server's container has not (SKIP there)
php tests/php/snapshots.test.php # the snapshot store (#231), the same way
php tests/php/releases.test.php # the release store (#266), the same way
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1687 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1691 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result
+58 -3
@@ -161,6 +161,61 @@ object above it) takes it out of the simulation for as long as the mouse holds i
pointer as if it had no rigidbody, and gravity does not pull it. Let go, it starts again at rest,
with no velocity.
## Joints
A **joint** ties the object it sits on to another one (#154). It is an asset placed as a child of
the object it holds, like a collider or a rigidbody: right-click the object in **Scene objects** →
*Add joint…* and pick **Create new joint** or one the project has already. An object takes as many
as it needs; a collider, a behavior, a rigidbody, a tilemap or another joint takes none. In the
scene view it is the icon of two ends on a bar, and it is not drawn in the game.
**Where the joint sits is the point it holds its object at** — move the joint and that point moves.
Its rows:
| Row | Default | What it does |
|---|---|---|
| Type | Hinge | **Hinge**: the object turns round the point where the joint sits (a door, a pendulum, a wheel). **Spring**: pulled towards a length, swinging about it. **Rope**: free up to a length, held beyond it. **Fixed**: kept exactly as the two were placed, moving as one. |
| Connected object | None | The other end, picked from the objects of the scene or with the eyedropper. None is a point of the scene that never moves. |
| Connected anchor X / Y | 0, 0 | Spring and rope only: where the other end is, in the connected object's own units (0, 0 is its origin, and it turns and scales with it) — with no connected object, in the scene's. A hinge and a fixed joint have one point only, the one the joint sits on. |
| Length (0 = as placed) | 0 | Spring and rope only: the length the spring rests at or the rope allows, in world units. 0 takes the distance between the two ends as they were placed. |
| Stiffness | 50 | Spring only: the force per 100 units of stretch (N/m). A body of 1 kg on 40 swings once in 0.99 s. |
| Damping | 1 | Spring only: how fast the swinging dies down; 0 swings on. |
| Limit angle | off | Hinge only. On: **Min angle** and **Max angle** say how far the object turns either way, in degrees, counted from how it was placed. |
| Collide with connected | off | Off: the two objects of the joint pass through each other. On: they collide like any two. |
**The object needs a rigidbody**, its own or one of an object above it. Without one the joint holds
nothing and the console says so once: `Physics: "Lamp joint" sits on an object without a rigidbody,
so it holds nothing.`
**The connected object need not have one.** With a rigidbody the two pull on each other. Without
one it is a moving anchor: nothing pulls it, but the joint follows it wherever a behavior or an
animation puts it — a lamp on a platform that drives along. A connected object that is destroyed
while the game runs makes the joint let go (`… lost its connected object, so it holds nothing.`);
removed in the editor, the row goes back to None like any reference to it (#269), so the joint then
holds on to the scene. A joint connected to the object it sits on holds nothing either.
A reference follows its object as every object's place does (see [Behaviors](Behaviors)): pasted
together, the copy of the joint names the copy of the other object, and inside a container every
placement holds on to its own copy.
In the **scene view** a line leads from the joint to what it holds on to: to the connected anchor
of a spring or a rope, to the connected object of a hinge or a fixed joint. The held point is
ringed, since the icon stands aside when it shares the spot with a collider's. A selected spring or
rope has a round **handle** on its far end: drag it to move the connected anchor (one undo step).
A joint that is switched off lets go, and holds again when it is switched back on. `Destroy` on a
joint lets go for good. **`Add joint`** ties two objects while the game runs: it places the picked
Joint asset on *object* at (x, y) of that object, connects it to *connected* (nothing there: the
scene) and outputs the new joint. The asset is the template for the type and every value;
`Properties` with Type *Joint* changes them on a placed joint.
Joints hold while the game is **played**. The editor run simulates nothing (#148), so there they do
nothing, and the inspector of a placed joint says so.
Measured in the unit tests: a pendulum of 100 units crosses the middle after 0.50 s (2.007 s the
whole period), a spring of 40 N/m on 1 kg turns round after 0.50 s, a rope of 150 lets a body fall
to 150 and no further, and a hinge limited to 30 degrees stops between 27 and 33.
## Play and the editor run
The editor loads the engine right after opening the project; Play waits for it if it is not there
@@ -179,6 +234,6 @@ are placed, because what the editor run moves is saved. The nodes above do nothi
crates), kept a gap of up to 1.7 units and knows no friction; `Move` works out its contacts itself,
in double precision, and slides over the same seams without catching (#150).
- The simulation in the editor run (#148).
- Joints (#154). Raycasts (#153) with their layer filter (#158), `Raycast all` with every hit on the
line (#157), one-way platforms (#155) and the collision layers themselves (#152) are there,
see [Colliders](Colliders#raycast).
- A slider joint, and motors on a hinge (#154, second step). Raycasts (#153) with their layer
filter (#158), `Raycast all` with every hit on the line (#157), one-way platforms (#155) and the
collision layers themselves (#152) are there, see [Colliders](Colliders#raycast).