Dragging from right to left in a canvas text field only moved the
cursor. The hidden input behind the field got every selection sorted
and without a direction. Its selectionchange echo then handed back the
sorted range, and the anchor jumped to the mouse on the first move.
- KeyboardBridge sets the selection with 'backward' when the focus
lies before the anchor, and reads selectionDirection when the input
reports a change.
- TextField passes anchor and focus through unchanged. Its own
sorting is gone.
- Shift+Left runs through the same path. The cursor should now sit at
the moving end there too. This is not tested on its own.
Tests:
- ./check.sh: 167 files, 2435 tests, CHECK EXIT 0
- e2e 2: ALL PASSED, 25 checks. New check 2d-ii drags right to left
in the name field and reads start 0, end 13, direction backward.
- Counter-check without the direction: 2d-ii fails with end 2, the
reported symptom.
- e2e check total in README goes to 1401 (computed, not run in full).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scene view drew its symbols from its own SVGs, each in a type
colour and some on a plate. They looked heavier than the panel icons
and were hard to read on busy ground. Symbols of different objects
also sat on top of each other whenever their positions met.
- Every symbol is now the panel icon of its kind, one file for both
places. The scene-only SVGs in src/viewport/icons are gone.
- Symbols are white in a thin dark outline with no plate behind them.
haloSvg widens each shape of the file by its own stroke and draws
that silhouette dark under the icon, so no second file is needed.
- A butt stroke end would leave its end without outline. The halo caps
such strokes square, and the rigidbody arrow shaft is now a filled
rect so its top edge is outlined exactly.
- Symbols are 18 world units across, stay upright and are clickable.
A click selects the object, a drag moves it.
- symbolLayout places every symbol on its object's position. Symbols
that overlap stand on a ring round the middle of their positions,
evenly spaced one icon apart. A ring that reaches into another
symbol takes it in until nothing overlaps. Ownership plays no part.
- Names sit beside their symbol, on the outside of a ring. Selection
frames and parent links follow the symbol.
- A collider icon no longer shows trigger or invalid state. The
outline colour of the shape still does.
- A lone collider sits on its sprite's middle again. A press there on
an unselected sprite drags the collider, so e2e 02 and 16 now grab
the sprite beside its middle. Hook __gm.container.objectRect helps
16 find that spot.
Tests:
- check.sh: 2245 tests, CHECK EXIT 0 (+8 for layout and halo)
- e2e 1-16,31,39-43: 670 PASS, 2 FAIL. Both were sampling too
strictly for the thinner, smaller icons. 5c looked for the exact
outline colour, which the thin outline only reaches blended at this
zoom. 31n started the ring scan one pixel past the smaller ring.
Both are fixed, and e2e 5,31 then passed with 48 checks.
- 5c checks white with an outline and none of the old colours, 41b
checks the ring positions and that a click on a symbol selects its
object, 31n checks the now white particle ring.
- Counter-checks: without the halo only 5c fails, also with the looser
outline match. Without the layout the ring and click checks in 41b
fail. Originals restored by cmp.
- The e2e check total stays 1351.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The focus outline of a text field was only visible at its corners.
The mask that clips the text covered the whole field, border included,
so the 1 px stroke was cut away along every straight edge. Only the
rounded corners bent inwards far enough to survive. With the larger
radius the new theme tried, this became obvious.
- TextField puts text, selection and caret into their own container.
Only that container is masked. Background and outline are not.
- NumberField and the rename field in lists inherit the fix.
- The theme radius goes back to 2 px.
- New debug hook __gm.focusedRect() returns where the focused widget
sits, so a check can read its border.
Tests:
- ./check.sh: 141 files, 2080 tests, exit 0
- GM_STEPS=2: ALL PASSED, 24 checks (one new)
- The new check in 2c reads the middle of all four edges of the
focused name field. All four must be the focus blue.
- Counter-check with the old full-field mask: the check fails, all
four edges read #24272e.
- Editor check total is now 1286 (1285 + 1), computed, not measured.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suite spent 88% of its step time asleep: 564 s of steps, 496 s of
them in sleep(n) - fixed spans estimated by hand and then copied from
step to step. That is why it was slow, and why it flickered.
The signal is afterFrames(count, timeout) in ui/render.ts, exposed as
__gm.afterFrames(). It wakes the loop itself, because an editor that
draws nothing would never redeem a promise of a frame, and it gives up
after a timeout instead of hanging the run. settle() in the harness
waits two drawn frames (~32 ms) instead of 150-300 ms and falls back to
sleep(150) when the hook is missing (an old build): silently not
waiting would make every later check a coin toss.
Every action goes through it now - click, doubleClick, wheel, drag,
dragAsset, press, typeText, select, mutate, undo, redo - and with that
about 550 sleep() calls behind those actions became redundant, helpers
that are nothing but a mutate in a coat included. What a frame cannot
answer got its own condition instead: waitUntil(expr), play()/stop()
waiting on __gm.state.mode rather than on 300 ms, saved() for the
autosave (the two hand-written polling loops sampled at 300 ms for
something that arrives after a second), and showTab() as a real tool -
47 places called evaluate("__gm.showTab(...)") raw, and 24 of them
waited for nothing at all and read the old panel.
564 s -> 243 s of step time, the waiting share 88% -> 76%. Three kinds
of wait were kept, and each one bit when it was taken away: loading
bytes (textures, fonts), game time that a repaint cannot stand in for
(time scale, shader and particle clocks, the live values a running
graph carries), and the inertia the scroll step measures on purpose.
Two things the new speed uncovered, both real: 25-animation read the
frame menu unfiltered and had frozen an incomplete list (the CI failure
on c990dbc), and 33-render-on-change measured idleness while the
autosave of the step before was still painting - it waits for
saveStatus to leave dirty/saving now, so it measures that nothing
happens any more instead of that it waited long enough.
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>
- `.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>
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>
A rebuild destroys the widget being typed in, and a typed value can change the
very structure that decides whether a rebuild is needed -- a field that says
how many ports a node has. That made such a field unusable: every keystroke
previewed, the preview emitted 'project', refreshValues() saw the signature
change and threw the row away. Dragging the value worked only because a drag
keeps receiving globalpointermove on the destroyed widget.
So refreshValues() now postpones a due rebuild while the focus sits inside the
inspector, and runs it as soon as the field is left. Values are safe in the
meantime: TextField.setValue ignores an edit in progress.
Two handles for that in the toolkit. FocusManager.isWithin(node) also replaces
the parent chain refreshLive() walked by hand, and changeHandlers lets a panel
notice that the focus has gone -- there was no signal for that before.
Nothing in the editor can trigger this today, since the Sequence setting that
did is gone. It is the ground for the List tab, where a row that adds or
deletes an item changes the structure while you type. The e2e check therefore
builds the case out of real parts: type into a field, set the object's parent
from outside -- the parent is part of the structure signature -- and confirm
the input survives and the rebuild follows on Escape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pixi only fills `detail` on the `click` event it synthesises; on
`pointerdown` it is always 0, so every `e.detail === 2` branch was dead
code. Asset rows, hierarchy folding and select-all in text fields never
responded to a double-click.
A shared click counter in src/ui/clicks.ts replaces the three readings of
`e.detail`. A finished double-click ends the run, so two in a row count
2 and 2 rather than 3 and 4.
The harness could only send `clickCount: 1`, which is why no test caught
this: every "activate" check went through Enter. It can send real
double-clicks now, and five checks drive the three paths separately.
An inspector label whose value deviates from its base was drawn bold, which is
easy to miss next to the regular labels and says nothing in the hierarchy, where
a folded subtree could hide a change entirely.
The colour now lives as a theme token (`override`, #f2c84b) used by both the
inspector labels and the hierarchy rows. Whether a row deviates is decided by
`src/core/properties/deviation.ts`: `objectDeviates` compares an object against
what it follows -- the container template for content, the asset otherwise --
and `deviationMarks` carries the mark up to every ancestor, so a folded subtree
still shows that something inside was changed.
`deviationMarks` takes the stored holder, not the resolved one: in the resolved
view container content looks like an ordinary object and `instanceTargetOf`
would compare against the asset, marking every instance.
Covered by 3 unit tests and 5 e2e checks, including one that strips an object's
own overrides to prove the row stays gold for a deviating child and restores
them byte for byte. Full suite remote on the RTX 4070: 275 checks, all passed.
Tooling:
- Prettier as the single formatting authority (110 columns, single quotes,
measured against the existing style); Markdown excluded because table-cell
padding turns the README glossary into 1000-character lines.
- Type-aware ESLint ending in eslint-config-prettier, so a rule always means
"this is a bug", never "this looks different".
- .editorconfig and an `npm run check` that runs typecheck, lint, format and
tests in one go.
Findings fixed on the first run: three more duplicate imports, four
String(unknown) calls (parameter types narrowed instead of casts), two dead
initialisers, two redundant casts, a console.log for a real error. Two
warnings were false here and are disabled with a reason in place.
Tests 132 -> 269:
- node-catalog.test.ts covers every node with a run(), with a guard that fails
when a new node has no row.
- behavior-events.test.ts covers the ten event nodes through a running World,
checking timing rather than existence.
- Input's state machine (press/release/releaseAll) is reachable without DOM
events now; the DOM handlers are thin adapters.
Compare fell back to == without a configured operator although the schema
default is <; aligned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>