Add pixel-size fit and drag-to-reorder in the hierarchy

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>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 22:20:46 +02:00
1 parent f40b8a0523
commit 4568c6581b
17 files changed
+416 -85

No files matched your search

+27 -15
View File
@@ -106,7 +106,10 @@ shown on the node and editable in the inspector.
**Editor symbols are always on top.** The viewport draws scene content (sprites) in an
`objects` layer and editor symbols (camera icon, labels) in an `overlay` layer above it, so a
sprite can never hide a symbol regardless of z or hierarchy order.
sprite can never hide a symbol regardless of z or hierarchy order. A viewport label keeps a
constant size on screen however far you zoom, and both kinds of them – the object names of the
Scene tab and the frame names of the Texture tab – read that size from one token
(`Theme.viewportLabelSize`); the two `viewportLabel` styles differ in colour only.
**Z-order is part of the position.** `position.z` is the draw order of a scene object: higher
`z` is drawn on top; objects with equal `z` keep their hierarchy (tree) order. It is edited in the
@@ -120,6 +123,11 @@ subtree back. Re-parenting keeps the world position by recomputing the local one
are rejected. In the **Scene objects** panel children are indented below their parent; drag a
row onto another row to parent it, onto the empty space below the rows (or right-click →
Unparent) to move it to the top level; the inspector offers the same as a **Parent** menu.
Dragging into the **gap** above or below a row – its outer quarter, marked with a line instead of
an outline – orders the object among *that* row's siblings rather than putting it inside, and
re-parents it on the way when the gap belongs to another parent. Since equal `z` falls back to
this order, that is also how two objects at the same depth are put in front of each other. A drag
that lands where it started changes nothing and costs no undo step.
Rows with children **fold**: click the chevron (or press ←/→, or double-click the row) to open
and close a subtree, right-click → **Expand all** / **Collapse all** for the whole scene. A
scene opens fully collapsed – which rows are open is view state of the panel, never stored in
@@ -225,13 +233,17 @@ plain colour and comes back by itself on undo. Deleting the whole texture counts
wearing *any* of its frames, not just the ones naming the texture directly.
**Fitting the sprite to the picture.** The *Width* and *Height* rows of a sprite carry a ↔ / ↕
button that sets that one side from the other and the picture's proportions — the frame's
rectangle when it wears a frame, the whole image otherwise. Like ↺ and ↑ the buttons exist only
when they would do something: no picture, an image not yet measured, or a sprite already in
proportion means no button at all. Fitting is an ordinary property write, so on an object it
becomes an override and it is one undo step. The comparison has a relative tolerance rather than
being exact — of the 40 000 picture sizes up to 200 × 200, 3135 do not survive the round trip
through a division, and an exact test would leave the button standing after its own click.
button offering two things: **Fit to 64 × 48** sets that one side from the other and the picture's
proportions — the frame's rectangle when it wears a frame, the whole image otherwise — and **Set
to 64 × 48 px** puts both sides at the picture's own pixel size. Like ↺ and ↑ the button exists
only when it would do something, and follows the same rule for how many: no offer means no button,
one is a direct click, both make it the menu form ↔ ▾. So a sprite without a picture, or one
already in proportion *and* at pixel size, carries nothing. Either action is an ordinary property
write, so on an object it becomes an override, and setting both sides is still one undo step. The
proportions are compared with a relative tolerance rather than exactly — of the 40 000 picture
sizes up to 200 × 200, 3135 do not survive the round trip through a division, and an exact test
would leave the button standing after its own click. The pixel size is compared exactly, because
those two numbers are written as they are.
Rendering a frame is a texture matrix on the fill, not a Pixi sub-texture — a Graphics fill
ignores a `Texture`'s `frame` (measured, see `viewport/textureFill.ts`). One consequence is
@@ -346,7 +358,7 @@ selected and switched back on.
```
web/index.html Vite source page (dev) index.html built page (generated, served)
src/core/model/ Project (object holders), Asset, SceneObject, hierarchy, walk (generic parent/child walks), instance (container content resolved from its template)
src/core/model/ Project (object holders), Asset, SceneObject, hierarchy, walk (generic parent/child walks), instance (container content resolved from its template), container (the cycle guard)
src/core/properties/ PropertySchema, resolve (the only override merge), collider/camera/text geometry, enabled
src/core/commands/ undoable commands, CommandStack, SetBagValueCommand base, require.ts (validated lookups, and what an instance may not change), naming.ts
src/core/graph/ graph model, NodeRegistry/NodeContext/values, interpreter; nodes/<category>/<Type>.ts
@@ -362,6 +374,7 @@ src/editor/inspector/ Inspector panel, field factory, previewHandlers (scrub
src/editor/panels/ AssetPanel, HierarchyPanel, Toolbar
src/editor/graph/ GraphEditor (header + picker), GraphCanvas, NodeView, NodePalette
src/editor/container/ ContainerEditor (header + picker), ContainerCanvas (a SceneViewport on a template)
src/editor/texture/ TextureEditor (header + picker), TextureCanvas (picture + frames), SliceDialog, sheetView (the grid maths)
api/ PHP backend data/projects/ persisted projects
tests/ Vitest unit tests tools/ seed-*, e2e (steps in tools/e2e/), smoke-jumprun, lib/
```
@@ -390,8 +403,8 @@ npm run lint # ESLint, type-aware; --fix for the fixable half
npm run format # Prettier over src, tests and tools
npm run check # typecheck + lint + format:check + tests, what CI would run
npm run build # writes dist/ and regenerates index.html
node tools/seed-e2e.mjs && node tools/e2e.mjs # 140 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs # the example game, played end to end
node tools/seed-e2e.mjs && node tools/e2e.mjs # 258 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs # 24 checks: the example game, played end to end
```
**Prettier owns the formatting** (110 columns, single quotes, `.prettierrc.json`); Markdown is
@@ -459,9 +472,8 @@ object in the selection (hierarchy rows too).
## Not yet
Reordering the hierarchy, several scenes in one run (a `Load scene` node – screens are containers
Several scenes in one run (a `Load scene` node – screens are containers
today), sound, auth, touch-optimised layout, graph comments/groups. A frame is always stretched to
the sprite's `width` × `height`: no animation (nothing steps through a sheet's frames yet) and no nine-slice. ↔ / ↕ fit the
proportions, but nothing sets a sprite to the picture's pixel size outright. Slicing cuts a
regular grid only — no detection of where the sprites actually
the sprite's `width` × `height`: no animation (nothing steps through a sheet's frames yet) and no
nine-slice. Slicing cuts a regular grid only — no detection of where the sprites actually
sit on the sheet. Unreferenced image files are never collected.