A sprite or collider could only be sized, turned and scaled through
the inspector. The scene view now draws a size gizmo on the selection,
so the common edits happen where the object is.
- Edges and corners set Width and Height. The opposite side stays
where it was, so the position moves by half the change. Sizes land
on whole units, at least 1.
- A circle collider has edge handles only and stays round. A polygon
or frame collider keeps its own corner editing and gets no size
handles.
- One knob on a stem above the top edge turns or scales. The first
5 px of the drag decide which: out from the centre scales, sideways
turns. The choice holds until the release, so one drag never does
both. Turning and scaling share one handle by the user's choice.
- Shift snaps a turn to 15 degrees. The turn adds up the swept angle,
so it goes on past a half circle instead of jumping back.
- The values are overrides of this placement, like "Fit to sprite".
A value equal to the asset's own clears the override. One drag is
one undo step.
- A handle reaches 7 px outwards but only half that into the object.
Otherwise a press near the edge of a small sprite sizes it instead
of moving it.
- The rules live in src/editor/sizeGizmo.ts without Pixi. The handles
are drawn and driven by src/editor/viewport/sizeGizmoEditor.ts. The
test hook __gm.sizeGizmo() reports them in screen pixels.
Found on the way:
- The first e2e run failed the play-mode drag check in 41b. It grabs
the 40 px crate 6 px inside its bottom edge, and the full grip zone
took that as a resize. The half zone inside fixes it.
- A first gizmo check failed because the sprite's right edge lay
outside the scene view (x 940, view ends at 925). The test was
wrong, the sprite now sits further left.
Tests:
- ./check.sh: 156 files, 2283 tests, CHECK EXIT 0 (before 2273).
- 10 unit tests in tests/editor/size-gizmo.test.ts.
- 6 new e2e checks in 29j: handles present, edge resize with the
opposite edge fixed, undo, knob turn, knob scale that ignores a
later sideways pull, circle radius.
- e2e 1-16,29,39-43 on the final state: ALL PASSED, 663 checks.
- Counter-checks: deciding the knob mode on every move fails the
"does not turn it" check. Not moving the centre on an edge drag
fails both edge checks. A full grip zone inside fails the new unit
test. Originals restored.
- Check total 1357 + 6 = 1363, computed. CI measured 1357 at d257298.
Co-Authored-By: Claude Opus 5.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.
Four new rows on sprite, text, collider and container (block TRANSFORM):
scale, rotation in degrees clockwise, mirrorX, mirrorY. As properties they
get override/reset/apply, loading of old projects, write-back after play and
the Properties node for free; camera and behavior have none.
The hierarchy composes T*R*S per level (composeAlongChain replaces
sumAlongChain), so children rotate and scale with their parent. Reparenting
and grouping keep the look (carriedTransform). Drawing goes through
placeNode, the gizmo is a polygon, cull and objectRect use the AABB, drags
are computed in the parent's system.
Colliders are oriented boxes: SAT for box/box, box-circle in the box frame;
exitDistance replaces axisSeparation/axisCorrection, moveAndSlide moves along
the object's own axes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>