Document frame collision and tile surfaces (#193)

- Colliders: the shape "Frame collision", how it is measured and
  when it turns red.
- Physics, Tiles, Tilemaps: friction and bounciness on a tile, and
  why cells of different surfaces never merge.
- Textures: an outline drawn on a frame can serve as a collider.
- Architecture: frameOutline.ts and the surface grouping.
- Development: check sum 1198.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
adminandClaude Opus 5 committed 2026-09-21 07:29:28 +02:00
1 parent 102ab47430
commit a79833f4bb
7 files changed
+33 -7

No files matched your search

+1 -1
@@ -68,7 +68,7 @@
``` ```
web/index.html Vite source page (dev) index.html built page (generated, served) web/index.html Vite source page (dev) index.html built page (generated, served)
src/core/model/ Project (object holders), Asset + AssetTypes, SceneObject, Properties (value types), 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), generated (pictures that are drawn rather than uploaded, #185: the key `gen:<assetId>@<revision>` and the register of readers that tell the picture path an asset's canvas size and revision), 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) + 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), 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, 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)), 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), graphLookup (what a graph sees of the project: behaviors to run, state machines to read), Scope (where a state machine's current state lives: per instance or one for the run — variables are shared by living on a Variables asset instead), 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/model/ Project (object holders), Asset + AssetTypes, SceneObject, Properties (value types), 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), generated (pictures that are drawn rather than uploaded, #185: the key `gen:<assetId>@<revision>` and the register of readers that tell the picture path an asset's canvas size and revision), 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) + 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), 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)), 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), graphLookup (what a graph sees of the project: behaviors to run, state machines to read), Scope (where a state machine's current state lives: per instance or one for the run — variables are shared by living on a Variables asset instead), 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 (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 (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/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) 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)
src/core/commands/ undoable commands, CommandStack, SetBagValueCommand base, RenameCommand base (undone by renaming back: variable, struct field, port), one import command shape for texture/font/sound, require.ts (validated lookups, and what an instance may not change), naming.ts src/core/commands/ undoable commands, CommandStack, SetBagValueCommand base, RenameCommand base (undone by renaming back: variable, struct field, port), one import command shape for texture/font/sound, require.ts (validated lookups, and what an instance may not change), naming.ts
+13 -2
@@ -1,4 +1,4 @@
A **collider** is an asset (`shape` box, circle, capsule or polygon — see A **collider** is an asset (`shape` box, circle, capsule, polygon or frame collision — see
[Shapes](#shapes)) placed like any [Shapes](#shapes)) placed like any
other object — typically as a child of the sprite it belongs to, so its position is the offset other object — typically as a child of the sprite it belongs to, so its position is the offset
from that *body* and it moves with it. Right-click an object in **Scene objects** → *Add from that *body* and it moves with it. Right-click an object in **Scene objects** → *Add
@@ -28,6 +28,17 @@ its slant.
A polygon may be concave: it is cut into convex pieces (ear clipping, then the cuts that are not A polygon may be concave: it is cut into convex pieces (ear clipping, then the cuts that are not
needed are joined again), which is what collision works with. One that crosses itself or has no needed are joined again), which is what collision works with. One that crosses itself or has no
area is drawn **red** and collides with nothing. area is drawn **red** and collides with nothing.
- **Frame collision** (#193) — wears the collision outlines of a texture frame, the ones drawn in the
Texture tab for tiles (see [Textures](Textures#collision-outline)). The **Frame** row picks the
frame like a sprite's texture. The collider stays **linked**: redraw the outline and every collider
wearing that frame blocks with the new one, in the editor and in the next Play. The frame is
centred on the collider's origin, one image pixel to one unit, so a collider on a sprite of that
frame at its own size lies exactly over the picture. It turns, grows and mirrors with the object
like any shape. A frame with several outlines blocks with all of them, each cut into convex pieces
like a polygon. The corners are edited in the Texture tab, not in the scene view. Without a frame,
with a whole texture instead of a frame, or on a frame nobody outlined, the collider is drawn
**red** as a box and collides with nothing: the frame's box when there is one, else its own
`width` × `height`.
Box against box, circle against circle and box against circle keep their own exact tests. Every Box against box, circle against circle and box against circle keep their own exact tests. Every
pair with a capsule or a polygon goes one common way (`core/properties/convex.ts`): each shape is a pair with a capsule or a polygon goes one common way (`core/properties/convex.ts`): each shape is a
@@ -55,7 +66,7 @@ which a turned parent turns as well — until one runs into a solid collider; th
flush (`exitDistance` in `collider.ts`: the travel at which the shapes stop overlapping). What is left flush (`exitDistance` in `collider.ts`: the travel at which the shapes stop overlapping). What is left
of the move then goes on **along the surface**: the part into it is dropped, and the part along it of the move then goes on **along the surface**: the part into it is dropped, and the part along it
is shortened by **friction × the part into it** (Coulomb; the pair's friction is the average of both is shortened by **friction × the part into it** (Coulomb; the pair's friction is the average of both
colliders, a tile counts 0.5). The contact's normal decides which way "along" is, so a circle that colliders, a solid tile brings its own, see [Physics](Physics#the-material)). The contact's normal decides which way "along" is, so a circle that
runs into an edge rolls up over it, an object pressed onto a slope slides down it unless the pair runs into an edge rolls up over it, an object pressed onto a slope slides down it unless the pair
is rough enough to hold it (friction ≥ tan of the slope), and one pressed against a wall slides is rough enough to hold it (friction ≥ tan of the slope), and one pressed against a wall slides
along it the slower the harder it presses. Up to four contacts are followed per move (a corner along it the slower the harder it presses. Up to four contacts are followed per move (a corner
+1 -1
@@ -13,7 +13,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
npm run build # writes dist/ and regenerates index.html npm run build # writes dist/ and regenerates index.html
./build.sh # the same under `set -o pipefail`, for callers that pipe the output ./build.sh # the same under `set -o pipefail`, for callers that pipe the output
php tools/make-user.php <name> <password> # an account; every tool signs in (see Accounts) php tools/make-user.php <name> <password> # an account; every tool signs in (see Accounts)
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1195 editor checks, screenshots in /tmp/gm-e2e-*.png node tools/seed-e2e.mjs && node tools/e2e.mjs # 1198 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 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 start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result ./e2e.sh status # live progress, or the last run's result
+8 -2
@@ -41,10 +41,16 @@ Every collider has three more rows, read wherever a rigidbody takes part:
A trigger (`isTrigger`) weighs nothing and never pushes; it only reports. A trigger (`isTrigger`) weighs nothing and never pushes; it only reports.
A **solid tile** has Friction and Bounciness too (#193), with the same defaults and the same
mixing, shown in the inspector once *Solid* is on. It has no Density: a map never moves, so what its
cells would weigh counts for nothing. Tiles of different surfaces never merge into one rectangle,
so an ice patch in a stone floor is its own shape, and `Move` slides along each cell by the tile it
belongs to (see [Tilemaps](Tilemaps#collision)). The scene view still outlines the floor as one.
## Shapes in the engine ## Shapes in the engine
A capsule collider is the engine's capsule; a polygon is one convex polygon per piece it is cut A capsule collider is the engine's capsule; a polygon, and a *Frame collision* collider, is one convex polygon
into (see [Colliders](Colliders#shapes)), all on the same body, so a concave shape rests and rolls per piece it is cut into (see [Colliders](Colliders#shapes)), all on the same body, so a concave shape rests and rolls
as the shape it is — a crate dropped into a U lands on its inner floor, not on the lid across the as the shape it is — a crate dropped into a U lands on its inner floor, not on the lid across the
notch. A rigidbody's fixed **Mass** is shared out among the pieces by area, and density weighs each notch. A rigidbody's fixed **Mass** is shared out among the pieces by area, and density weighs each
by its own area. by its own area.
+2 -1
@@ -74,7 +74,8 @@ wearing *any* of its frames, not just the ones naming the texture directly.
## Collision outline ## Collision outline
A frame can carry **outlines** a tile made of it blocks with (#141; see A frame can carry **outlines** a tile made of it blocks with (#141; see
[Tilemaps](Tilemaps#outlined-tiles)). Several, because an autotile's block holds several shapes – [Tilemaps](Tilemaps#outlined-tiles)), and a collider of the shape *Frame collision* too (#193; see
[Colliders](Colliders#shapes)). Several, because an autotile's block holds several shapes –
its bordered tile and its notches. Switch **Collision** on in the Texture tab's header: a press its bordered tile and its notches. Switch **Collision** on in the Texture tab's header: a press
then edits the selected frame's outlines instead of the frames. then edits the selected frame's outlines instead of the frames.
+6
@@ -150,6 +150,12 @@ that blocks (#173): where two rectangles or the pieces of outlined cells meet, n
so a wall of several rectangles or a lake of many pieces shows as one shape. Collision itself keeps so a wall of several rectangles or a lake of many pieces shows as one shape. Collision itself keeps
the pieces. the pieces.
A solid tile's **Friction** and **Bounciness** (#193) are what a cell of it offers, read by `Move`
and by the physics engine like a collider's (see [Physics](Physics#the-material)). Cells of tiles
with a different surface are never merged into one rectangle, the same way one-way tiles are kept
apart from walls. A tile with a smaller collision scale therefore does not stay flush with a
neighbour of another surface: towards it, it is drawn in like towards an open side.
A placed tilemap carries **Layer** and **Collides with** like a collider does, for the whole map A placed tilemap carries **Layer** and **Collides with** like a collider does, for the whole map
(see [Colliders](Colliders#collision-layers)): ground only some things walk on is a map on its own (see [Colliders](Colliders#collision-layers)): ground only some things walk on is a map on its own
layer. Per layer of the map or per tile it is not — a cell belongs to the map. layer. Per layer of the map or per tile it is not — a cell belongs to the map.
+2
@@ -65,6 +65,8 @@ asks for; a picture that does not fit is kept and named, never silently dropped.
platform one jumps through from below (#155); see [Tilemaps](Tilemaps#one-way-tiles). platform one jumps through from below (#155); see [Tilemaps](Tilemaps#one-way-tiles).
- **One-way angle** — 45 by default, 0 to 90, shown while *One-way* is on. How far a contact may - **One-way angle** — 45 by default, 0 to 90, shown while *One-way* is on. How far a contact may
lean from straight up and still land on the tile (#159). lean from straight up and still land on the tile (#159).
- **Friction** and **Bounciness** — 0.5 and 0 by default, shown while *Solid* is on. The surface a
cell offers to what meets it, mixed like two colliders' (#193); see [Physics](Physics#the-material).
- **Collision scale** — 1 by default. How big a solid cell blocks against its square, drawn in or - **Collision scale** — 1 by default. How big a solid cell blocks against its square, drawn in or
out only on the sides without a blocking neighbour (#140); see out only on the sides without a blocking neighbour (#140); see
[Tilemaps](Tilemaps#collision-scale). [Tilemaps](Tilemaps#collision-scale).