Describe sprite preview, machine ports and shared local machines

The wiki catches up with the last four commits in the code repo.

- Concepts: new paragraph Sprite preview. Architecture lists
  SpritePreview.ts and previewFit.ts.
- State-machines: a Start input or a Variables node can name the
  machine it carries. A called behavior now shares the caller's
  local machines instead of starting empty.
- Behaviors: the warning at Run behavior for a different machine,
  the shared local machines and Get world position in the data nodes.
- Scene-editing: a half sentence on Get world position.
- Development: e2e sum 1744.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
adminandClaude Opus 5.5 committed 2026-10-04 18:44:21 +02:00
1 parent 1d9e589d07
commit 70697cce9a
6 files changed
+41 -15

No files matched your search

+3 -1
@@ -281,7 +281,9 @@ src/editor/inspector/ Inspector panel, host (what a section builds into, and `e
every variable), shapeSection (the shapes of a vector drawing: the list, the four order every variable), shapeSection (the shapes of a vector drawing: the list, the four order
buttons and the style of the picked one, #129; Fill type and Line type with the stop bar, buttons and the style of the picked one, #129; Fill type and Line type with the stop bar,
the picked stop's rows or the texture, #186), portSection, aspectAction, the picked stop's rows or the texture, #186), portSection, aspectAction,
objectQueries/vectorRows/parentRow, cutSection (the Nine-slice and Sorting rows, shared by the frame and the drawing inspectors, #308) objectQueries/vectorRows/parentRow, cutSection (the Nine-slice and Sorting rows, shared by the frame and the drawing inspectors, #308),
SpritePreview (a sprite as `syncSpriteMesh` draws it in the scene) + previewFit (whole device
pixels per unit, the corner on one)
src/editor/panels/ AssetPanel, assetGroups (groups by type and their order, #176), assetIcons (one SVG per src/editor/panels/ AssetPanel, assetGroups (groups by type and their order, #176), assetIcons (one SVG per
asset type in icons/, shared by the panel, the + menu, the tabs and Scene objects, asset type in icons/, shared by the panel, the + menu, the tabs and Scene objects,
plus the List, Game, Plugins and User management tabs' own four, which stand for no asset), plus the List, Game, Plugins and User management tabs' own four, which stand for no asset),
+8 -4
@@ -220,7 +220,7 @@ the inspector).
the given object, nearer levels first, the order `Child by name` searches in), `Children` (the the given object, nearer levels first, the order `Child by name` searches in), `Children` (the
children of an object; with **recursive** every object below it, level by level) and children of an object; with **recursive** every object below it, level by level) and
`Objects of asset` (every instance of the chosen asset, spawned ones included), `Objects of asset` (every instance of the chosen asset, spawned ones included),
`Variables`, `Properties` (every property row of the target's type, the transform rows `scale`, `rotation`, `mirrorX`, `mirrorY` included — how a graph turns a sprite), `Get position` (local and world), `Get Z` (the object's own `z` and `world z`, summed up with its parents'), `Is enabled`, literals `Number`/`Text`/`Boolean`/`Texture`/`State machine`/`Sound`/`Scene`/`Animation`/`Asset`, `Texture size` (a texture's, frame's or pixel art's own size in pixels, 0 for none — wired into a sprite's `width`/`height`, a picture shows pixel for pixel), `Add/Subtract/Multiply/Divide`, `Variables`, `Properties` (every property row of the target's type, the transform rows `scale`, `rotation`, `mirrorX`, `mirrorY` included — how a graph turns a sprite), `Get position` (local and world), `Get world position` (only the world `x`/`y`), `Get Z` (the object's own `z` and `world z`, summed up with its parents'), `Is enabled`, literals `Number`/`Text`/`Boolean`/`Texture`/`State machine`/`Sound`/`Scene`/`Animation`/`Asset`, `Texture size` (a texture's, frame's or pixel art's own size in pixels, 0 for none — wired into a sprite's `width`/`height`, a picture shows pixel for pixel), `Add/Subtract/Multiply/Divide`,
`Min/Max/Abs/Sqrt/Clamp`, `Lerp` (from a to b as t runs from 0 to 1, t held in 0…1; the setting `Min/Max/Abs/Sqrt/Clamp`, `Lerp` (from a to b as t runs from 0 to 1, t held in 0…1; the setting
*Ease* bends the way: *Linear*, *In* starts slow, *Out* ends slow, *In-out* both, all quadratic, *Ease* bends the way: *Linear*, *In* starts slow, *Out* ends slow, *In-out* both, all quadratic,
or *Curve*, which follows the [curve asset](Curves) picked in the *Curve* row and may overshoot b; or *Curve*, which follows the [curve asset](Curves) picked in the *Curve* row and may overshoot b;
@@ -372,7 +372,8 @@ the inspector).
removing takes the edges hanging on it in *every* graph that calls the behavior. A call is a removing takes the edges hanging on it in *every* graph that calls the behavior. A call is a
**call, not an inlining**: the nested graph runs in its own pulse cache with a fresh copy of its **call, not an inlining**: the nested graph runs in its own pulse cache with a fresh copy of its
variable defaults, so values travel in and out through the ports and nothing else. That makes a variable defaults, so values travel in and out through the ports and nothing else. That makes a
call repeatable and keeps a nested behavior free of hidden state. A behavior that would run call repeatable and keeps a nested behavior free of hidden state. The one exception are local
state machines, which belong to the instance running the call (see *Keep* below). A behavior that would run
itself raises an error instead of looping; there is one start and one end node per behavior. itself raises an error instead of looping; there is one start and one end node per behavior.
A behavior **without a start node** is called from its **On start** nodes instead, all of them in A behavior **without a start node** is called from its **On start** nodes instead, all of them in
graph order, as when it starts on its own. It takes no inputs then, but its end node still hands graph order, as when it starts on its own. It takes no inputs then, but its end node still hands
@@ -427,7 +428,8 @@ the inspector).
a call inside a call has one per outer node. It follows today's declarations: a variable added a call inside a call has one per outer node. It follows today's declarations: a variable added
since starts at its default, a removed one is dropped, and one whose kind changed starts over. since starts at its default, a removed one is dropped, and one whose kind changed starts over.
The kept value lives as long as the object does in the run and is never saved. Local state The kept value lives as long as the object does in the run and is never saved. Local state
machines of the called behavior are not kept, they start in no state every call. For state machines need no Keep: a call reads and moves the states of the instance running it, so they
carry over from call to call anyway (see *State machines*). For state
shared between objects, use a Variables asset or a behavior object in the scene (see shared between objects, use a Variables asset or a behavior object in the scene (see
*Cross-object state* below). *Cross-object state* below).
Three ways in and out of a call: **drag** a behavior out of the asset panel onto the graph and it Three ways in and out of a call: **drag** a behavior out of the asset panel onto the graph and it
@@ -437,7 +439,9 @@ the inspector).
stays in the same tab, and the trail belongs to that tab: a behavior opened any other way comes stays in the same tab, and the trail belongs to that tab: a behavior opened any other way comes
up in a tab of its own, without a trail, and switching back brings the trail back with it. And a call that cannot up in a tab of its own, without a trail, and switching back brings the trail back with it. And a call that cannot
do anything **says so on the node** — no behavior picked, the behavior deleted, do anything **says so on the node** — no behavior picked, the behavior deleted,
or neither a start node nor an On start in it. The last one is the one worth naming: without or neither a start node nor an On start in it. It also warns when a State machine input is set
to another machine than the one its Start input carries (see *State machines*). The missing
start is the one worth naming: without
either there is no way in, so the call is skipped and the flow simply carries on at its `exec` output. A missing either there is no way in, so the call is skipped and the flow simply carries on at its `exec` output. A missing
*end* node is not a defect — a call is always synchronous, "done" is the nested flow running out, *end* node is not a defect — a call is always synchronous, "done" is the nested flow running out,
and the end node only decides what comes back. and the end node only decides what comes back.
+7
@@ -288,6 +288,13 @@ without rows is left out.
with a header of its own (*Layers*, *Frames*, *Shapes*) folds with a click on that header, clear with a header of its own (*Layers*, *Frames*, *Shapes*) folds with a click on that header, clear
of its plus, and takes the notes and buttons under it along. of its plus, and takes the notes and buttons under it along.
**Sprite preview.** A sprite, placed or as an asset, ends its inspector with a **Preview** group:
the sprite as the scene draws it, with its picture or frame, nine-slice, tint and shader, on the
scene's background. A placed sprite shows its own overrides. An edit shows at once, also while a
value is dragged, and a shader with a Time node keeps moving while the group is open. The picture
is fitted into the group and magnified by whole device pixels at most 8 times, so pixel art stays
sharp; a sprite larger than the group is shrunk to fit.
The asset panel and **Scene objects** share the left column; the divider between them drags The asset panel and **Scene objects** share the left column; the divider between them drags
the height from one to the other, and dragged all the way either keeps its header. The split is a the height from one to the other, and dragged all the way either keeps its header. The split is a
share of the column, so a taller or shorter window keeps it. share of the column, so a taller or shorter window keeps it.
+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) # 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/snapshots.test.php # the snapshot store (#231), the same way
php tests/php/releases.test.php # the release store (#266), 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 # 1742 editor checks, screenshots in /tmp/gm-e2e-*.png node tools/seed-e2e.mjs && node tools/e2e.mjs # 1744 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
+2 -1
@@ -179,7 +179,8 @@ scene opens fully collapsed — which rows are open is view state of the panel,
the project — and selecting an object anywhere (viewport, `Add collider…`, `Create container`) the project — and selecting an object anywhere (viewport, `Add collider…`, `Create container`)
unfolds the way down to it and scrolls it into view, so a selection is never hidden. In unfolds the way down to it and scrolls it into view, so a selection is never hidden. In
play mode entities carry the same `parent` link: moving a parent entity moves its subtree, play mode entities carry the same `parent` link: moving a parent entity moves its subtree,
`Destroy` removes it. `Get position` exposes both the local and the world coordinates. `Destroy` removes it. `Get position` exposes both the local and the world coordinates,
`Get world position` only the world ones.
**Groups in the hierarchy (#241).** A group is a folder for top-level objects — 100 crates under **Groups in the hierarchy (#241).** A group is a folder for top-level objects — 100 crates under
one foldable row, say. It is editor looks only: parents, positions and behaviors stay exactly as one foldable row, say. It is editor looks only: parents, positions and behaviors stay exactly as
+20 -8
@@ -95,15 +95,25 @@ state lives in the *machine* — so nothing cleans up there, and the edge stays
lost, though: the behavior's node shows the key as a **ghost port** with the edge dashed in the lost, though: the behavior's node shows the key as a **ghost port** with the edge dashed in the
warning colour, and *Disconnect* on its right-click menu removes it. Put the state back and the warning colour, and *Disconnect* on its right-click menu removes it. Put the state back and the
edge works again. Which machine the edge works again. Which machine the
ports belong to is read off the graph, not off a running value — the inline pick, or a ports belong to is read off the graph, not off a running value — the inline pick, a
`State machine` literal wired in; a variable or an `Item at` feeding the input yields no state `State machine` literal wired in, or an input of the behavior's **Start** node of the kind
ports, exactly as `Run behavior` knows its behavior only as a setting. That machine is still not State machine with a machine picked next to its name in the inspector. A variable of that kind
read by a `Variables` node works the same way: the machine is picked next to the variable's row.
The pick only names the states, the value the variable holds at run time is not checked against
it. An `Item at` or anything else feeding the input yields no state ports, exactly as
`Run behavior` knows its behavior only as a setting. That machine is still not
stuck: `Set state by name` names the state as text, `State branch` leaves along `else`, and stuck: `Set state by name` names the state as text, `State branch` leaves along `else`, and
`Current state` answers as always. The price is spelled out, not hidden — a name is typed, so a `Current state` answers as always. The price is spelled out, not hidden — a name is typed, so a
typo is possible where a port made it impossible, which is what `not found` is there for. typo is possible where a port made it impossible, which is what `not found` is there for.
Picking another machine Picking another machine
orphans the edges of the old states, the way any setting that decides a node's ports does. orphans the edges of the old states, the way any setting that decides a node's ports does.
A Start input with a machine picked has that machine **picked in advance** on every node running
the behavior. Another pick there is allowed, but the node warns (`"machine" expects "Doors"`):
the called graph asks for the states of its own machine, so with another one every state would
read as never entered. A **local** machine handed in is the caller's: the called graph reads and
moves the very state the calling instance holds (see *The current state while playing* below).
Two more read it as **data** rather than as a flow, both pure readers (no exec ports, answered Two more read it as **data** rather than as a flow, both pure readers (no exec ports, answered
whenever they are asked): whenever they are asked):
@@ -148,9 +158,9 @@ The rules, all of them worth knowing before wiring a graph on them:
whose events keep moving the machine stops the run with an endless-loop error, like the step limit. whose events keep moving the machine stops the run with an endless-loop error, like the step limit.
- Who hears it follows the **scope**: a local machine is heard by the instance that moved it alone, - Who hears it follows the **scope**: a local machine is heard by the instance that moved it alone,
a global one by **every** instance with a matching event node — which is the point of sharing a a global one by **every** instance with a matching event node — which is the point of sharing a
state. A move a nested `Run behavior` makes to a *local* machine dies with the call and is not state. A move made inside a `Run behavior`, however deep, counts as a move of the instance that
reported, even where the called behavior keeps variables (see *Behaviors*); a move it makes to a runs the call: its own `On state entered` and `On state exited` hear it. The event nodes of the
global one is a move like any other. called graph itself stay silent, like every event there.
## The current state while playing ## The current state while playing
@@ -164,8 +174,10 @@ gone the moment the run stops. Play repaints the tab the way it repaints the Beh
**A local machine's current state is the behavior instance's own**, kept beside its variables **A local machine's current state is the behavior instance's own**, kept beside its variables
(`NodeContext.states`, machine id → state id): two crates running the same behavior are in (`NodeContext.states`, machine id → state id): two crates running the same behavior are in
states of their own, and a behavior run through `Run behavior` starts in no state, states of their own. A behavior run through `Run behavior` has no states of its own: it reads and
and again with every call. A **global** one is kept for the run instead (`SharedState.states`, built moves those of the instance that runs the call, however deep, so a state one call sets is still set
in the next one, and the call begins in the start state like the instance does. Two characters
running the same behavior therefore stay apart, each with its calls. A **global** one is kept for the run instead (`SharedState.states`, built
once per run in `core/graph/sharedState.ts`) and every instance reads and moves that one — which once per run in `core/graph/sharedState.ts`) and every instance reads and moves that one — which
bag a node touches is decided by the machine's scope, not by the node (`stateBag`). Neither bag a node touches is decided by the machine's scope, not by the node (`stateBag`). Neither
survives a stop: both begin the next Play at the start state again, or in no state where there is survives a stop: both begin the next Play at the start state again, or in no state where there is