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>
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>
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>
A short list opened after a long one drew all its rows stacked on one
point. The fault was not in the list tab: rebuild() handed the row stack
to setContent() while it was still empty, so the content was sized to
max(rows, viewport) = the viewport. The rows arrived after that, and for
a short list the same formula yields the same height, so resize() saw no
change and skipped layout(). A long list changes the height and never
showed it.
relayoutContent() now runs whenever the content changed, rather than
leaning on resize()'s change detection - that detection is the wrong
instrument for "the children moved".
The list tab was the only affected caller: the panels use ListView,
which positions its rows itself, and the inspector fills its body
before setContent.
Also drops the comma line under the list. Every section that can carry a
list supplies openList, so the comma fallback in fieldFactory was
unreachable; listToText/listFromText go with it. The list-value test now
covers coerceListItem through coerceValue instead, so the boolean
spelling stays guarded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both stores carried their own ~28-line request<T>, and formatBytes existed
twice – the second copy had lost the "B" branch, so a small sprite would have
read as 0 KB had its caller ever passed one under a kilobyte.
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.
The context menu entry is gone; dragging replaces it entirely. The drop
target is the view under the pointer, not the active tab, so the same
gesture works for scenes and for container templates.
While dragging, an outline shows the footprint the object will have plus
a crosshair on its origin, turning red when the drop would be refused.
Preview and command share one function (placementRejection), so the
outline cannot promise something the drop then rejects.
A left-press used to select the list row immediately. Since the container
tab follows the selection, it switched to the very container being dragged
and every drop would have nested it inside itself. The list now selects on
release without a drag: picking something up is not the same as choosing it.
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.
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.
`decimals` only reached `formatNumber`, so it changed the label and nothing else: typing 640.5 into
a field with `decimals: 0` stored 640.5 and displayed 641. The two existing users got away with it
because rounding happened outside the widget (`normalizeFrameRect`, the slice dialog), which their
comments had to point out.
`NumberField.clampValue` now rounds before it clamps, and every path that sets a value goes through
it (typing, scrubbing, arrow keys, the constructor), so the stored number matches the shown one and
`min`/`max` stay exact. The rounding is `roundTo` in math.ts, which `snapToStep` already did inline.
With that safe, camera `View width`/`View height` and text `Font size` become integers – they were
showing 640.000 and 32.000. Everything else that looked integral is not: positions, sprite and
collider sizes and zoom are all scrubbed, and `z` is a summed coordinate (walk.ts), not a layer.
Verified remotely against the RTX 4070 host: 270 checks, all passed (step 10 gained a check that the
field text has no decimal point and survives a reopen). `npm run check` green, 407 tests.
The aspect button now carries two offers, so it becomes a menu: fit one
side to the texture ratio, or set both sides to the exact pixel size as a
single undo step.
Dropping a row on the outer quarter of another row inserts it into that
gap instead of parenting into it, reordering among the siblings there.
Order is the stored order, so ReorderObjectCommand is a splice; equal z
falls back to tree order, which makes this the way to draw one object in
front of another. Rejected on container content like the other structural
edits.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Container tab reuses SceneViewport against any object holder, so a
container template is edited exactly like a scene. Right-click no longer
changes the selection, which previously made nesting impossible: opening
the menu on a container loaded it, and "Place in container" then targeted
itself.
Template edits (move, rename, override) now reach already-placed instances
per slot, through a shared templateSync used by both the apply path and the
plain object commands. Copies the user has touched keep their own value.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
- 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>
Number fields derived their display precision from `step`, so `step: 1`
(X/Y/Z, width, height, font size) rendered a stored 100.234 as "100" — and
because `onChange` parses the text, touching the field wrote the rounded
value back into the project. `formatNumber` now renders every number with
three decimals, independent of `step`, which is left to control the scrub
and arrow-key increment only.
Dragging now snaps to the increment's grid (`snapToStep`), so a scrub at
`step: 1` always lands on whole numbers even from a fractional start, while
Alt (x0.1) and the 0.1-step fields keep their precision. Typed values and
arrow keys are untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Menu entries are left-aligned and live in a ScrollContainer capped at 320 px, so a
long list (the parent picker holds every object of the scene) scrolls instead of
running off the screen. `Menu(items, { searchable: true })` puts a filter box above
the scroll area; Enter takes the first match. Filtering forces a relayout because
two queries with the same hit count leave the height unchanged, which would keep
the stale row positions.
Fields that open a menu are a new `Dropdown` widget: the value sits flush left and
the chevron is drawn flush right instead of being written into the label. A menu
opened below a field is never narrower than that field.
New debug hooks `overlayTexts()` / `overlayRect()` make the popup contents
assertable; e2e step 11 covers the cap, the scrolling and the search box.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
- Inspector rows compute their reset/apply entries through providers, so an
entry that would not change anything is dropped; a single remaining entry
renders as a direct button instead of a menu, none removes it entirely.
- Bold and buttons now follow the same rule (deviatesFromAsset /
deviatesFromTemplate in core/properties/resolve.ts).
- Hierarchy panel is a collapsible tree: chevrons, arrow keys, double click,
expand/collapse all. Everything starts collapsed; selecting an object
expands its ancestors and scrolls it into view.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- TextField: new onSubmit callback (Enter only, independent of value change)
- NodePalette: use onSubmit instead of onChange, add per-open picked guard
- e2e: new check 13a (type + click row -> exactly one node), wait for
Chromium exit in finally with SIGKILL escalation after 5s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Canvas-only 2D game editor/runtime built on PixiJS v8 + Vite + TypeScript.
- core: asset model (Scene/Camera/Sprite), scene objects with sparse
overrides, property schema + resolve, command stack (undo/redo),
serialization and project store interface (16 unit tests)
- ui: custom in-canvas widget toolkit (panels, fields, color picker,
list/scroll, popup) with a hidden <input> keyboard bridge
- editor: asset/hierarchy panels, schema-driven inspector, toolbar
- viewport: box rendering, camera frame, selection gizmo, drag/zoom/pan
- runtime: world/entity instantiation for play mode, input, camera follow
- api: PHP JSON project store with revision counter (409 on conflict)
- tools: CDP-based e2e smoke test
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>