Fragment-side coordinate nodes instead of vertex nodes: the quad stays the
quad, a UV read plus a sample at a computed coordinate covers flip, scroll,
wave, pixelate and scanlines. Time is a uniform only when a Time node reaches
the Output; a viewport that drew a Time shader redraws from the ticker so the
Scene tab animates without a running world. Edge (Mirror/Repeat/Clamp) is a
second texture variant in TextureLibrary because WebGL binds no TextureStyle
resource of the shader. The compiler now emits #version 300 es first: Pixi
only compiles as ES 3.00 when the fragment says so, and textureSize needs it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A sprite's picture is now a Mesh (unit quad, sibling of its Graphics node, which keeps
only the hit area); a worn shader replaces Pixi's fragment in that draw, so there is no
intermediate texture and no extra render pass. Measured on the HD 630: 400 shaded
sprites hold 60 fps (was 34), 800 hold 56 (was 20).
Two Pixi facts the code leans on: a mesh's texture is bound only for Pixi's own shader,
so GraphShader.wear hands it over; and MeshPipe caches whether a mesh batches and
re-checks it only in a frame without structure changes, so switching a shader on or
off calls mesh.unload().
The console panel mirrors console.group/groupCollapsed/groupEnd and fills the %s %d
%o %c directives, so Pixi's deprecation warning shows its head, not only its stack.
E2E 12 guards against the addChild-on-Graphics warning, 28 counts shaders instead of
filters.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The wiki carried an estimate ("fine for dozens, not for thousands"). Measured
on the Intel HD 630 that CI uses, 48x48 sprites in a 640x360 camera, neutral
values, three runs agreeing to 1 fps:
sprites 25 50 100 200 400 800
fps 58 56 54 48 34 20
The same scene without shaders holds 60 fps at every one of those counts, 800
included. Quadrupling the sprite area (96x96) measures the same, so the cost is
the render target switch, not the pixels: roughly 20 us per pass up to 200,
40 us at 800.
tools/perf-shaders.mjs drives it (not run in CI, frame times are machine
dependent). New hook __gm.filterCount() counts visible nodes carrying a filter
and proves one pass per sprite; e2e 28 checks it.
Two traps the tool guards against, both cost a wrong answer first: the median
frame time is useless here (the distribution is bimodal under vsync, so the
rate comes from frames/duration), and --disable-gpu-vsync has no effect in
headless, so what is measured is the point where the frame rate breaks down.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The + Property menu of the Properties node offered every shader's params
for every sprite. Each shader row now carries its shader name, and a name
shared by two shaders lists both: "brightness (Adjust colors, Neon glow)".
Sprite-owned rows stay unlabelled.
The note hangs off a map keyed by the def object, not by the name -- a
sprite property and a shader param may share a name (#63), and keying by
name would label the wrong row.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A frame drawn straight from the sheet showed a hairline of the neighbouring
cell along any edge that fell inside a screen pixel: with antialias on, a
partly covered pixel is still evaluated at its centre, past the edge. On top
of that Pixi rewrites a Graphics fill texture from clamp-to-edge to repeat at
draw time (generateTextureFillMatrix.mjs), so the top row of a sheet read its
bottom row.
TextureLibrary.get(ref) now cuts the rectangle out of the loaded sheet with an
OffscreenCanvas on first use and uploads the cut (key file|x,y,w,h, kept for
the session). Address mode is mirror-repeat: Pixi only rewrites clamp-to-edge,
and within a texel of the border mirror behaves like clamp, for linear as well
as nearest. Loaded sheets get it too, since whole pictures wrapped at their own
borders the same way. viewport/textureFill.ts and its test are gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dropdown labels are now Linear (default) and Nearest; the stored values
linear/nearest are unchanged, old project files load as before.
e2e 12c-ii measures in Play at camera zoom 1.37 (screen scale 1.434): a
Nearest edge sampled across seven pixels is byte-identical with and without
a neutral Adjust colors shader, and brightness 0.5 on the same path darkens,
so the pass really runs there. New hook __gm.gameRect(id).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The check compared the position after key-up with a sample taken before
shot(); the player keeps walking during the screenshot, so the check
measured screenshot duration on the runner (22/30/40 px on CI runs 11-13)
and failed at exactly 40.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
No input, one Value param (step 0.05, drawn on the node), output `value`
as a float literal via floatLiteral (2 -> 2.0, NaN -> 0.0). No uniform,
no sprite row - that is the difference to Property.
The e2e negative check for "no behavior node in the palette" now uses
Boolean as its marker, and step 28 types the palette search first
because the palette scrolls from twelve entries on.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One rule for every asset type: a click on an asset row selects it, a
double-click (or Enter, or creating it via +) opens its tab. Selection
alone still switches tabs only for nodes, frames and template objects.
- state.openAsset(id) = select + 'open' event; the editor maps the asset
type to its tab via TAB_OF_ASSET. The two special clauses for state
machine and shader assets in the selection handler are gone.
- Creating via + and importing a texture from the picker open the tab;
paste/drop of an image only selects.
- 3 unit tests (event order, re-open of a selected asset, selection never
fires open), 14 e2e checks in step 4.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Output gains a number input `alpha`; unconnected it reads the texel's
alpha, like `color` reads the texel. The epilogue premultiplies with the
resolved alpha. New `Texture alpha` node exposes texel.a for maths.
PortDef.inline=false keeps the alpha port without an inline field: every
number port is inline by default, and a typed default of 0 would have made
every shaded sprite invisible. The compiler's texel fallback is typed:
colour ports get rgb, number ports get texel.a.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
check() appended the detail by string concatenation, so an object
argument showed up as "[object Object]" in the log (27-console.mjs).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The ninth tab (Shader) pushed the row past the centre column: List
ended at 962 while the column ended at 925, so the last buttons sat
under the inspector and clickText('Container') hit the List tab
instead (CI runs 5 and 6, module 16). Tab buttons now use padding 4
via a new Button padding option; e2e step 14b guards the row width
using the widget box rather than getBounds(), which sees only the
label on an untoggled ghost button.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The shipped shader graph gains a `brightness - 1` offset node, so 1 stays
neutral and brightness lifts every channel by the same amount; contrast keeps
multiplying, saturation keeps mixing with the luma. Fixture, seeds, unit and
e2e expectations follow (twelve nodes, 1.25 -> #80c0ff).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A shader is an asset carrying a node graph (Texture color, Output, Property,
Color, Add/Subtract/Multiply/Divide, Mix, Luminance) that compiles to the GLSL
fragment of a Pixi filter. The Shader tab reuses the Behavior canvas; a sprite
wears a shader through its Shader picker, and the rows below it are the
graph's Property nodes. Property schema, resolution and deviation take the
project because a sprite's rows now live in another asset. The built-in
registry of the prototype is gone; "Adjust colors" ships as a graph in the
fixture and the three example games.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Order per instance: On start -> On enable -> On update ... -> On disable
-> On destroy. On enable fires right after On start and whenever the
behavior comes back on (itself and its parents); On disable in the frame
it goes off, the instance stays frozen. On destroy fires when the entity
leaves the world (Destroy node, child of a destroyed object) and when the
world is torn down on stop/restart, preceded by On disable if still on.
A new On start node added while playing still does not fire for a
running instance (kept as is, documented in the wiki).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The twelve sections of the 992-line README are now wiki pages
(https://git.nockal.com/meleki/game-maker/wiki, wiki commit 9be1294),
content unchanged. The README keeps what it is, the quick start and
an index of the pages.
Every push runs the gate, the editor suite and the three games on the Gitea
runner, so a local full run only costs four minutes of waiting.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The job image (tools/ci/Dockerfile, label game-maker-ci) carries chromium with the Vulkan
drivers, php-cli and the host's fonts; the render device itself comes from the runner's
container.options, so a job container gets the Intel GPU instead of llvmpipe.
tools/ci/serve.sh puts the checkout behind /game-maker/ with php -S and writes the project
shells the seeders need; every tool now takes its server from GAME_MAKER_BASE_URL rather than
from a project id repeated in shell scripts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A log layer (src/editor/console/log.ts) keeps a 500-line ring buffer and
wraps console.log/info/debug/warn/error, still calling the original. Notices
and behaviour runtime errors go straight to the layer instead of through
console, so editor tests stay quiet and errors can render red. world.log is
untouched: it is per-run structured state that runtime tests and e2e read
directly, not a text stream.
The panel sits under the visible tab, resizable with Splitter('y'), with a
CONSOLE header and a clear button. Each line carries its wall-clock time in
its own dimmed column, so locate() still finds the message text.
The taller centre area drops the graph fit zoom to 0.60, which broke six e2e
checks that assumed one screen pixel is one graph unit and that a fixed
offset from the canvas centre is empty. They now ask __gm.emptySpot(),
__gm.graph.emptySpot() and __gm.graph.zoom() instead of guessing.
locate/locateAll added the unscaled Pixi text width to the global position,
so click points in the scaled graph canvas landed too far right; they use
getBounds() now.
check.sh: 831 tests. Full e2e: ALL PASSED - 671 checks. Smokes 24/19/20.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A state machine is an asset whose graph holds one `State` node per state
(no ports, name as title), edited in a new "State machine" tab that reuses
the graph canvas: right-click places "State n", snap toggle shared with the
Behavior tab via SnapToggle. `statemachine` is a value kind like texture
(port, literal node, variable, struct field, tagged list item).
Behaviors use it through `State branch` (one exec output per state) and
`Set state` (one exec input per state; the interpreter now hands `run` the
entry port). States live per behavior instance in NodeContext.states, with
no initial state. Ports derive from the inline picker or a wired
`State machine` literal; BehaviorLookup became GraphLookup with
behavior(id) and machine(id).
The Graph tab is labelled "Behavior" (key unchanged). Unit tests
(state-machine, kinds, catalog, ports), e2e step 26 and README added.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- applyCommands sit on ObjectEditCommand with a revert hook instead of
five copies of the same preamble and undo
- actionIf replaces six identical Reset/Apply menu blocks, addNameField
the three name rows, assetLink the two asset links
- requireStructuralObject replaces ten two-line preambles and drops the
duplicate holder lookup
- __gm.objectsOf had no caller; the SMB remnant in e2e.sh is gone
Proven: check.sh 797 tests, full e2e 633 checks, smokes 24/19/20 against
the same bundle.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- `.gitignore` said `index.html` unanchored, which also caught the Vite source page;
a fresh clone could neither `dev` nor `build`. Anchored to `/index.html`, page added.
- Removed GM_REMOTE (tools/remote/gm-browser.ps1 and the ssh/scp branch in browser.mjs):
the Windows test machine is gone and the only flaky check that lived on it is closed (#37).
- Removed `npm run preview`, undocumented and unused.
- Comments: wrong number in collider.test.ts (250 → 200), the "declarations used to…"
history in listItems.ts and friends, the 24 px/28 contradiction, section banners in
nodes/index.ts, node-catalog.test.ts and api/index.php, name paraphrases, and the stale
step numbers in tools/e2e/11-…23- from the old global count.
- README: asset types include `animation`, kind order Number/Text/Boolean, tagged colour
and texture items, 6/5 kinds, the `Texture` node, `./build.sh`, PROJECT_FORMAT_VERSION,
and the file tree (kinds.ts, listItems.ts, propertyPorts.ts, Breadcrumb.ts, beads.ts,
tools/assets/).
- tests/helpers.ts takes the duplicated `findObject` and `sceneWithCamera`.
Proof: ./check.sh 797 tests, full e2e ALL PASSED 633 checks against this bundle.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
All 50 notes are now issues #1-#50 in meleki/game-maker, open ones open
and finished ones closed. Every issue keeps the note verbatim and adds a
footer with the commit it was filed in, the commit that finished it and
who raised it; the 37 notes the user wrote are posted as `admin`, the 13
I raised as `meleki`. Label `todo-note` marks the whole batch.
todo-notes/ is gone - the tracker is the backlog from here on. README
says where they live now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A kind like colour or texture used to be spelled out in five parallel
lists, its port type, its empty value, its glyph, its label and its list
tag - 22 literal places for texture alone. core/properties/kinds.ts now
holds one row per kind and the rest is derived from it, so adding a kind
is one row. The five literal nodes collapse into literals.ts, the six
kind menus into one helper.
Number, Text, Boolean is now the one order everywhere; the variable and
port menus used to list Boolean before Text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- `Texture` and `Color` can be picked as a variable kind, a struct field
kind and a declared port kind; a new `Texture` node hands a picture id
into the graph. Declared, a texture is its bare id – the `$texture` tag
stays a list-only thing.
- The `String` node is labelled "Text" like its kind; the type id stays.
- Whole-number inputs (`Item at`, `Set item`, `Sub list`, `Fill`,
`Repeat`, `Substring`, `To text`) show no decimals (`int()` in shared.ts).
- All server projects are reseeded, so `legacyNodes.ts`,
`migrateDeclaredLists` and the `ownerId` fallback go, with their tests.
- Note `reseed-because-of-list` moved to done/; new note `refactor-types`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A list no longer says what its items are. ListDef.items, ListDef.fields,
ListFieldDef.items and SetListItemsCommand are gone; the single seam is
src/core/properties/listItems.ts. JSON already tells number, text, boolean
and struct apart, so only two kinds need a tag: {"$color": n} and the new
{"$texture": id}. Field names cannot start with "$", so a tag never
collides with a struct field. On a data link a texture item arrives as
its bare id; the graph has no images.
List tab: the mark in front of each row is the kind picker, "+ Item"
asks for the kind, both are ordinary value edits (one undo step). A list
inside a struct field only offers the flat kinds because the depth rule
forbids a struct item there (ListAccess.kinds); the menu used to offer
it and silently do nothing.
Legacy lists that declared items: 'color' are tagged on load.
Actor demo: the four walk animations and four standing frames live in
two list variables of texture items indexed by facing (Item at), so the
graph can be rebuilt in the editor. The stand frames get their grid ids
back; 44 nodes instead of 46.
Also: the user's edits to on-start-during-play.md and the new note
reseed-because-of-list.md.
Verified: check.sh 794 tests, full e2e 631 checks, smokes 24/19/20.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tools/seed-actor.mjs builds the project Actor demo: it uploads
tools/assets/Actor1.png, slices the 576x384 sheet into the 96 cells of a
12x8 grid (48x48 each, no remainder) and wears the character in the top
left corner. Walking is four ping-pong animations of three frames, which
is the original RPG-Maker walk cycle; standing is the middle frame of the
row, not an animation.
The graph pastes walk- or idle- in front of the direction faced and wears
the result, so eight cases need no branch tree - the List node only holds
numbers. Which way it looks is the key pressed last, corrected whenever
only one axis is still held, or letting go of one key of a diagonal would
walk backwards. Axis input already normalizes its vector.
tools/lib/projects.mjs gains uploadTexture, since data/textures/ is
git-ignored and a seed that ships its picture has to send it. The harness
gains w/a/s/d and keyDown/keyUp halves, because holdKey cannot express two
presses that overlap - which is what a diagonal is.
tools/smoke-actor.mjs plays it in 19 checks and reads the drawn pixels at
the camera edge, where the bounds hold him walking on the spot: three
distinct frames walking, one standing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An animation asset is a list of pictures (whole textures or frames) with
frames per second and Loop/Once/Ping-pong playback. A sprite wears it
through its texture property, so the picker, instance overrides and the
Properties node work unchanged; there is no dedicated play node.
A different animation starts from its first frame, the same one set again
runs on, so a graph may choose Run or Idle every frame. The clock lives on
the entity, not in props, which the world writes back as overrides. The
AnimationSystem ticks before behaviors, and On animation finished fires
once when a Once animation reaches its end.
The Animation tab plays a preview above a frame strip: reorder by dragging,
Delete, duplicate, + Frames, pause/resume, and wheel zoom about the pointer
with pan and Fit. Scene and Container tabs show the first frame; edit mode
has no clock. Right-clicking a texture creates an animation from its frames.
Button now measures its label with the bold font it draws in, which cut
the Animation tab and red dialog buttons short.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eleven notes collected from the README's "Not yet" section, known
limits found in recent rounds, and open technical points: sprite
animation, graph comments/groups, a load-scene node, sound, nine-slice,
template changes reaching placed instances, exact matches in the
palette, On start added during play, render-on-change, gzip in nginx
and the flaky pixel-art filtering test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Splitting the bundle would only silence Vite's 500 kB warning. The real
saving is transport compression: nginx serves the 590 kB bundle without
gzip (184 kB compressed), and that setting lives in the shared web-stack
config, not in this repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The graph was only locked by five edit-mode guards in GraphCanvas. The
running game reads the graph by reference each frame, so a new node, edge,
changed value, disconnect or undo takes effect from the next frame. Stop
discards these edits like everything else changed in play. A new On start
node does not fire for a behavior that has already started.
During play every data output shows the value it last produced, in the
highlight colour and refreshed ten times a second. Values are kept per
graph, because node ids are only unique within one; with several
instances the most recent value wins. The Variables node reads the live
variable bag, so only its own outputs are kept, as a snapshot.
Each time an exec edge fires, a bead runs along it in 0.3 s, at most one
every 0.15 s per edge, following the same curve the edge is drawn with.
Beads run on game time and freeze while paused; the game logic is
unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A reference search over every export and method in src/ found code that
only tests still called, or that was never called at all:
- RemoveSceneObjectCommand: the product only removes through the
multi-object command; the tests now use that one too.
- NodeRegistry.requireNodeDef: a second function of the same name in
commands/require.ts raises a CommandError instead of throwing, and is
the one the product uses. The twin invited the wrong import.
- isTextureAsset, listContainers, AssetTabEditor.show,
FocusManager.destroy, KeyboardBridge.selectAll and the getters
ColorField.value, ListView.selected, NumberField.number.
Also: Theme.controlActive now matches the resting control colour, and a
new note asks what editing the graph during play would take.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collapsed, the node reads "Set 2 variables", "Get width" or "No properties";
expanded it is always "Variables" / "Properties". The Properties node shows
its object type (and behavior) as a line under the title instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Get/Set property, Get/Set properties, Set enabled and Set text are gone.
A single Properties node takes a fixed "object" input; every chosen
property is an output that reads the current value, and one switched to
Set is also an input written when the flow arrives. Exec pins only exist
while at least one entry writes.
The node carries a type (Sprite, Text, Collider, Camera, Container,
Behavior, Any) that decides what "+ Property" offers: the fields of that
type, the variables of the chosen behavior, or for Any a kind to pick
and a name to type. Each port takes the default and field of its type,
so a Camera width starts at 640. Switching the type keeps the ports and
marks those the new type lacks. At runtime the type is only an editor
aid; the node writes to whatever object it gets.
Old project files are converted on load. The type is inferred from the
keys: the first object type that has them all, else the one behavior
that declares them all, else Any. enabled belongs to every object, so a
converted Set enabled lands on Any.
The jump-run and tetris seeders use Properties throughout; draw-piece
switches its cell on in the same node that sets the colour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A single node now lists any number of variables. Every entry is an output
that reads the current value whenever it is pulled; an entry switched to
Set is also an input and is written when the flow arrives. Exec pins only
exist while at least one entry writes, and all inputs are read before any
write, so one node can swap two variables.
Get/Set variable, Get/Set variables and Variable are gone. Old project
files are converted on load: each old node becomes a Variables node with
the same variables, edges and typed values.
A written port without a typed value now shows the zero of its kind on
the node body, matching what the inspector shows and what the run writes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Get properties / Set properties work like their variable counterparts:
one port per chosen property, an unconnected input writes its inline
value. A port chosen from the built-in list keeps that property's field
and default (height starts at 100, color is a colour field). "object" is
reserved for the fixed input, checked by the command and on load.
The property picker names the owning type ("Sprite width"), and text
fields can now carry a suggestion list; Get/Set property use it.
Double-clicking a call node shows the path of opened behaviors under the
graph header. Clicking an entry jumps back and trims the path; choosing
a behavior any other way hides it. Reopening a behavior already on the
path steps back to it instead of repeating it. The bar shows before the
graph switches, otherwise it clips the graph that was just fitted.
The "Add node..." header button is gone; nodes come from the context menu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Get/Set variable nodes now title themselves "Get <name>" / "Set <name>", and
the new Variable node just "<name>"; the title is the same folded or open,
and a collapsed node shrinks to it. The redundant "Variable: <name>" row is
gone unless the variable was deleted ("<name>?" is the only hint then).
The Variable node sets its value when an exec flow arrives and exposes the
value at any time. Its output is read when a downstream node pulls it, not
frozen at the write, so count = count + 1 is one Variable node plus an Add.
Get variable keeps reading once per pulse: switching it would change the
behaviour of existing games.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adding a collider now lives only in the Scene objects context menu, deleting a node on
the Delete key and the node context menu. The e2e checks that pinned the inspector button
now drive the hierarchy menu instead, including that its entry is disabled on template
content and on a placed container without raising a notice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured click-to-paint was 72-77 ms, of which the editor itself spends
under 4 ms; the rest is the finished frame waiting in the compositor
queue. A desynchronized context bypasses it (16-18 ms in the same probe).
Pixi has no option for it, so main.ts builds the WebGL2 context itself.
The mode may tear on some drivers; ?vsync falls back to Pixi's own
context.
__gm.lowLatency() reports the attributes the browser actually granted,
and a graph-step check pins that the mode stays on.
Also adds the todo note graph-node-collapse-width.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Button, Checkbox, the color swatch and list rows took a hard-coded
'pointer'; they now read Theme.hoverCursor, which is 'default'.
Also adds the todo note for a breadcrumb when drilling into a
sub-behavior in the graph.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Split GraphCanvas (1076 -> 831 lines) into EdgeLayer and graphMenus;
the port and marker menus were the same list and are now one function.
Value specs of the inspector and the list tab share valueSpecs.ts, and
the remaining copies (compound commands, type glyphs, typed asset
lookup, name rule, play/editor world setup, marquee) are merged.
Four copies had already drifted apart; merging them fixes:
- "Disconnect N links" in the port menu is one undo step, not N
- "+ Add collider" is hidden on a placed container instance, as in
the hierarchy menu, instead of always failing
- a bool property coerced from the text "false" becomes false
- a port command on an unknown node type reports instead of crashing
Comment-only edits leave the bundle byte-identical. README corrected in
35 places (step limit, jump-run clock, suite duration, 409 body, ...).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sprite takes the whole image at its pixel size and the texture's name,
numbered like any other clash. CreateAssetCommand now accepts starting
properties; they go through the schema in execute, so an unknown key or a
bad value is refused before the project is touched, and one undo removes
the asset.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A chevron in front of the title toggles it without touching the selection,
and right-click offers Collapse node / Expand node for the whole selection
as one undo step. A collapsed node draws its title bar alone; every link of
one side meets at a single point on it, and hidden links there share one
mark per side, since per-port marks would stack on the same pixels. It has
no ports to grab, so a link dropped on it connects nothing.
Like hiding a link this is drawing only: collapsed is saved on the node and
an expanded node leaves no trace in the project file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
buildStructBlock only covered structs, so a placed instance drew numbers,
text, bools and lists through createFieldRow on a 64 px name column while
its struct block sat on the asset's grid below it. variableRows.ts now
builds the row for every variable kind on both sides; the edit option
decides what the owner may change (asset: rename, x, item kind; instance:
revert and push). Label and Button take a fixed width so the mark column
no longer shifts names when a list shows its item-kind picker.
The two structFields calls that became dead with 9f48257 are gone. The
e2e steps locate the Enabled toggle through __gm.inspectorField instead
of clicking a fixed offset from its label.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The instance inspector drew its own, flatter version of a struct: one row per
field, no bundle head. It now draws the same block as the asset side, because
there is only one implementation left - both call buildStructBlock. What the
instance does not get is everything that touches the shape: renaming a field,
'+ Field', and the per-field '×'. The shape belongs to the asset; a placed
object overrides values. The revert and override marks sit on the head rather
than on a field, since an override covers the whole bundle at once.
Underneath this, editing a single field of an unoverridden struct used to blank
its siblings: structFieldsOf filled the untouched fields from spec.read, which
is object.overrides[key] and therefore undefined until something is overridden.
structOf then handed back the empty shape. PreviewedValue carries 'current' now
- the resolved value the row actually shows - and the fields come from that.
Without it the old path stays, which is right for the asset side, whose prop
bag is dense.
The port editor gets 'From variable' next to '+ Field': pick a struct variable
of the behavior and its fields land as ports. It only adds - a field the node
already declares stays as it is, because it may carry an edge - and the entry
is disabled once there is nothing left to add. A list of structs counts too:
it declares the shape of every item, which is exactly the bundle a Pack builds
for it. A color field arrives as a number port; the graph has no color type.
AddPortsCommand is new underneath: adding five ports had to be one undo step,
and it checks every key before it appends a single one, so a collision does
not leave half the list behind. AddPortCommand is a three-line subclass of it,
which leaves its sixteen call sites untouched.