Commit Graph
9 Commits
Author SHA1 Message Date
rootandClaude Opus 5 3ab8d88ad6 Let On key down repeat while the key stays held
A held key fired once and then nothing, so sliding a Tetris piece sideways or
walking a menu meant building a timer next to every key node. Two settings on
the node do it now: Repeat after and Repeat interval, both in milliseconds so
the usual values stay whole numbers (300, 50) instead of 0.3 and 0.05. A delay
of 0 keeps the node exactly as it was, an interval of 0 means every frame.

The browser's own auto-repeat is deliberately not used - Input.press has always
dropped it - because its delay and rate are an operating-system setting of the
player's, not a game's. The repeat is built from the measured hold time instead,
per node, so two nodes on one key keep their own clocks. The rule itself is a
pure function in keyRepeat.ts and is checked without a world.

Both rows stay off the node body while the delay is 0: two lines of "0" on every
key node in every graph would be noise, and the screenshot said so. NodeDef gains
hiddenParams for that; the inspector still shows them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 22:44:00 +02:00
rootandClaude Opus 5 4b0919a224 Let a link be hidden, and shown again by selecting either end
A link can be folded away from the canvas: instead of a curve running clear
across the graph, each end grows a short stub with a mark naming the node at
the far side. The curve comes back while either of its two nodes is selected,
so following a connection is a click rather than a search, and a link carrying
a live pulse is always drawn so the trace keeps its thread.

Marks are collected per port, not per link, because several hidden links on one
port used to stack stubs, dots and labels on identical pixels. A bundle reads
"Move +1" and its dot selects every node the port feeds, as one undo step.

The whole thing is one optional flag on the edge; the interpreter never sees it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 22:19:59 +02:00
rootandClaude Opus 5 c6f907b4e0 Make a call node say why it does nothing, and easier to place
A behavior node that cannot run looked exactly like a healthy one: no
start node, no behavior picked, a deleted behavior or a self-call were
all silently skipped. The call still had to be forgiving at runtime -- a
dangling reference must not abort a game -- so the fix is diagnosis, not
an exception: describeProblem() names the one reason, the node draws it
in red under its title, and the inspector repeats it where you pick the
behavior. A missing end node is deliberately not a problem: a call is
always synchronous, and the end node only decides which values come back.

Two ways in: a behavior dragged from the asset panel lands in the graph
under the pointer, and double-clicking a call node opens the graph it
runs. Both reuse machinery that already existed -- the asset panel
already starts the drag, and list views already report double clicks.

The e2e module needed its assets at the head of the asset list: after 18
modules the list is long enough that a newly appended row scrolls out of
view, and the press then lands on the panel behind it, starting no drag
at all. That looked exactly like a rejected drop and cost two full runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 20:40:56 +02:00
root 6702ff8abe Let a behavior be a node in another graph
A behavior that has a Start and an End node can now be dropped into any
other graph as a node of its own. What is declared on the start node
becomes the inputs of that node, what hangs on the end node its outputs;
the port list is derived from the sub graph, not copied, so declaring a
port in one graph grows the node in the other without touching it. The
node carries the behavior's name and the palette lists behaviors by
name, so a call is one grab instead of a node plus a dropdown.

An embedded behavior has no object and therefore no place for
variables: every call starts on the asset defaults and communicates
only through its ports. That is the conservative direction - state can
still be added later, and shared state already has a home in a behavior
object in the scene. Node ids are unique per graph only, so each call
gets its own pulse cache; without that, two graphs would collide.

Renaming a port used to silently drop the value typed on the calling
node. It is carried along now, project wide, like the edges.
2026-09-09 20:01:49 +02:00
root e0d43e12aa Pan with the middle button, select several nodes, and fit properly
Three things the graph view did differently from the scene view, or not
at all:

Panning moves to the middle button, which works from anywhere – a node
under the pointer included, since NodeView now lets that one button
bubble up. Shift-drag on empty canvas pans too. The left button pulls a
rubber band instead and no longer pans.

Nodes can be selected several at a time: by band, by ctrl/shift-click,
moved together, deleted together, each as one undo step. The selection
model gains `nodeIds` alongside the objects' `objectIds`.

Fit really fits. It used to pin the zoom at 1 and merely centre, so a
graph larger than the view always left nodes hanging out, and it sized
every node with one guessed height although real ones vary with their
port count. Both sums now live in editor/graph/framing.ts as pure
functions, which is what made the defect testable at all.

The band selects any node it overlaps rather than one whose position it
contains, unlike the scene view: nodes are large rectangles, and a band
drawn visibly across one has to catch it.
2026-09-09 14:29:30 +02:00
root 14562cf157 Keep the dragged edge on screen while the palette picks its node
Releasing an edge over empty space opened the node palette but dropped the edge
drawing, so the connection being made was invisible exactly while its target was
being chosen. The finished drag is now held in `pendingDrop` and drawn like a
running one until the palette closes, by pick or by Escape.

The e2e check for the Escape path exposed a real defect underneath: `Popup.open()`
blurs the focused widget, `close()` did not. A field inside the popup therefore kept
the keyboard bridge's DOM focus after the popup was gone, and every later Delete or
Ctrl+Z went into that invisible input. The suite caught it one check later ("Delete
removes the node and its edge" found the node still there). `close()` now blurs,
mirroring `open()`.

Verified remotely on the Windows host: 287 editor checks and 24 jump-and-run smoke
checks, no failures.
2026-09-09 12:42:40 +02:00
root ebad3fd530 Make an edge drag say where it will land, and offer nodes when it lands nowhere
Dragging an edge now rings the port it would connect to and snaps the line
there. A drop on a node's body takes that node's first free compatible port,
or the last compatible one when none is free, replacing its link. A drop into
empty space opens the palette filtered to the node types the edge could reach
at all, and the picked node arrives wired - node and link as one undo step,
so a single undo takes back the whole gesture.

The decision of what fits what lives in core/graph/dropTarget.ts as pure
functions, tested directly; the canvas only draws the answer. The palette
takes an optional filter rather than knowing about drags.

Verified remote on the Windows box (GPU): ALL PASSED, 284 checks.
2026-09-09 12:29:57 +02:00
rootandClaude Opus 5 4d84231b9f Trim needless comments and fix the picker width
Remove comments that only restate the code, keeping the section banners
and the notes that carry a reason, a measured fact or an external
contract.

ButtonOptions.width claimed to be a fixed width but only set minWidth,
so the Graph and Texture pickers grew with the name they showed. It is
now what it promises, with a separate minWidth for the buttons that have
two forms. Narrow buttons shrink their padding, otherwise a 24px icon
button had 8px left for its glyph and truncate dropped it entirely.

__gm.labelsDropped() guards that class: the overflow guard only saw text
that was too wide, never a label truncated to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:52:05 +02:00
rootandClaude Opus 5 f16801aef3 Add a decimals option, fix dropdown overflow and the texture filter
- NumberField takes a decimals option (default 3); frame rects and the
  slice dialog use 0, matching their integer model.
- Button truncates its label to the drawn width; Dropdown reserves the
  chevron slot. NodeView reuses the shared truncate().
- Setting scaleMode leaves the style's shared resource id in place, so
  the old GPU sampler stayed in use until a reload. Call style.update().
- Graph gets a header bar like the Texture tab: GraphEditor is now the
  frame, the former editor is GraphCanvas. A selection the tab does not
  concern no longer unloads the behavior.
- New e2e step 14 guards text overflow across all widgets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:05:28 +02:00