Commit Graph
38 Commits
Author SHA1 Message Date
adminandClaude Opus 5.5 cf6043a066 Show a wheel step or Fit as a short zoom transition (#309)
The wheel and the Fit button used to jump to the new view. Now the
view takes a short way there, 200 ms by default, everywhere the editor
zooms. The zoom steps stay the same, only the change is shown.

- viewTween: tweenView is the way between two views as a pure
  function. The scale moves by equal ratios, and the world point that
  sits at the same place on screen in both views stays there. After a
  wheel step that is the point under the pointer.
- ViewTween drives it on the ticker. A surface hands it reading,
  writing and drawing its own view and wraps its wheel and Fit code in
  change(). A step during a transition starts from where that one was
  heading, so quick steps add up to exactly as many steps.
- Anything else that moves the view ends the transition. A pan jumps
  to the target first, and saving a view saves the target.
- Wired into FreeViewport (Scene, Container, animation preview),
  SheetCanvas (Texture, Curve, Pixel art, Vector art), GraphCanvas,
  panAndZoom (Tile board, plugin node board) and the timeline.
- Menu > General settings... sets the time in ms, 0 jumps at once. It
  is kept in this browser like the panel sizes, not in the project,
  and it is not an undo step.
- A document that opens still comes up framed at once. The animation
  preview frames a new subject itself instead of calling fit().
- The pixel art readout names the step the view is heading for.

Measured in the scene view at 200 ms on melektaus-atx, where every
frame of the transition is a full redraw:

- 500 sprites: redraw 6.7 ms, 16 frames, mean 18 ms
- 1000 sprites: redraw 13.0 ms, 9 frames, mean 27 ms
- 2000 sprites: redraw 32.2 ms, 5 frames, mean 58 ms

Tests:
- The harness sets 0 ms on every load, so the existing modules read
  the step and not the way.
- e2e 46i samples one wheel step at 1000 ms a frame later: zoom 1.033
  between 1 and 2, the point under the pointer kept at (134, -40). It
  lands on the view a 0 ms jump reaches.
- e2e 46 and 49 expect the new menu entry.
- Gate (atx): 234 files, 3563 tests, CHECK EXIT 0 (+15)
- e2e 46,49 (atx): ALL PASSED, 43 checks (+3, sum 1735)
- Three counter-checks fail view-tween.test.ts as expected. e2e 46i
  fails when the scene view does not carry its centre along.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 21:01:10 +02:00
adminandClaude Opus 5.5 3e05cbbed5 Let pixel art and vector art be nine-sliced and sorted (#308)
Nine-slice and the sort line hung on the texture frame alone. Drawing
and sorting already read both off the TextureRef every picture hands
out, but the generated pictures always said "no slice, no line". So
the feature moves from the frame to the picture, without a second
copy of any rule.

- PictureCut (border, fill, sortLine) is one type. TextureFrame
  extends it, PixelArtAsset and VectorArtAsset carry it as a whole.
  The texture file format does not change, and no migration is needed.
- The rules in texture.ts take a cut and a size: cutSortLine,
  setCutSlice, rawCutSlice and the rest replace the frame* versions.
  serialize.ts reads all three through one readCut.
- cutTarget.ts resolves a target to cut and size: a texture frame, a
  pixel frame's size or the vector canvas. It lives apart from
  texture.ts because vectorart.ts reaches texture.ts itself.
- SetSliceCommand and SetSortLineCommand take that target instead of
  (assetId, frameId). They refuse an asset that draws no picture.
- generatedRef fills slice and sortLine from the drawing. The whole
  strip of a pixel art asset with several frames gets none, it is no
  picture of that size. With one frame asset and frame are the same.
- Nine-slice scale is one shared schema row, now also on pixel art and
  vector art.
- cutSection.ts holds the NINE-SLICE and SORTING rows once. The frame
  inspector and the drawing inspector both build them.
- SheetCanvas draws a drawing's lines that are set, dashed as in the
  Texture tab. They are not dragged there, the painting tools own the
  canvas (as proposed in the issue).
- drawOrder falls back to the picture's height when a ref has no
  rect, or a sort line on vector art would have been ignored.

Tests:
- Gate: 232 files, 3542 tests (+8: drawing-cut.test.ts, and vector
  art in sort-by-y.test.ts). Three counter-proofs bite: no cut on a
  drawing, a cut on the whole strip, no height fallback.
- e2e 48: 44 checks (+1, sum 1732). 48l-ii reads red and blue where
  12-pixel borders at scale 2 put them. Without borders both spots
  read green.
- Looked at: the Pixel art tab with the lines, and the sliced sprite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:53:51 +02:00
adminandClaude Opus 5.5 345289982b Scale, turn and mirror the shapes of a vector drawing (#291)
game-maker / gate (push) Successful in 2m57s
game-maker / suite (push) Successful in 14m25s
A shape could only be moved and reshaped point by point. Whoever
wanted it larger or turned pulled every point alone, and for several
shapes or a group there was no way at all. The answers in the issue
settle the rest: the change is baked into the points, nothing of it
is remembered, and the line width stays.

- With Select the selection stands in a frame of eight squares and a
  knob, drawn like the one of the scene view. It holds one shape,
  several shapes or a group, the same selection that is dragged as
  one.
- A corner square scales from the opposite corner, a square on an
  edge in its own direction. Shift keeps the proportions, Alt scales
  about the middle.
- The knob turns round the middle of the frame, with Shift in steps
  of 15 degrees. After the pull the frame stands upright again.
- The inspector has a band TRANSFORM with X, Y, Width, Height and
  Rotate by, and the buttons Flip horizontal and Flip vertical.
- Points, handles, gradients and texture tiles are taken through the
  same matrix. The rule is a pure function in vectorTransform.ts,
  transformShapesCommand makes one undo step of any number of
  shapes.
- A press goes to a point or a handle first, then to the frame, then
  to a line. shapeFrame.ts holds where the handles are and what a
  pull of each means.

Decided here:

- The frame stands 15 pixels off the shapes. The points of a shape
  lie on its bounds, so a square on the bounds would cover them.
- The knob only turns. In the scene view it scales as well.
- A side cannot be pulled through the opposite one, the frame keeps
  at least 1 pixel.
- Coordinates are rounded to a thousandth of a pixel, so a quarter
  turn is exact.
- A texture tile stays upright under a turn and grows to the bounds
  of the turned tile. The bands of a gradient stay square to its
  axis under an uneven scale.
- The switch Scale stroke of the proposal is not built.

Found on the way:

- The inspector of a shape is now taller than the window. Two checks
  of module 47 no longer found their row or button on the screen and
  use the new hook inspectorButton, which scrolls there first.
- Not solved: with a shape at the top edge of the view the knob lies
  outside the tab. The wiki names it under the limits.

Taken over from #217: the e2e check for two selected shapes without
a group, and unit tests for the snap of the common offset and for
the gradients of the shapes dragged along.

Tests:
- Gate on melektaus-atx: 209 files, 3176 tests (3121 + 55), exit 0
- e2e 47,48 on melektaus-atx: 112 checks, all pass
- No full run and no smokes. The sum of 1664 (1660 + 4) is reckoned,
  1660 is what CI counted for 6d79cf5.
- 55 unit tests: 25 in tests/core/vector-transform.test.ts and 30 in
  tests/editor/shape-frame.test.ts
- Four e2e checks in block 47k of module 47: pull a corner, turn
  with Shift, type width and rotation, drag two shapes without a
  group
- 47 unit counter-probes and four browser counter-probes all fail
  their tests. Four of the unit ones did not at first: two showed
  dead code, which is gone, two got tests.
- Not checked in the browser: the edge squares, Alt, the cursors,
  the flip buttons and the rows X, Y and Height

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:20:55 +02:00
adminandClaude Opus 5.5 32a0af6e2c Put the text and boxes of the interface on device pixels
On a screen scaled to 125 % or 150 % the editor looked soft beside
a page the browser draws itself. The layout counts in whole CSS
pixels, and most of those fall between two device pixels there. The
GPU averages both: text loses its edges, and a 1 px line becomes a
pixel and a half of mixed colour. Now text and boxes are rounded to
device pixels, and a line is one device pixel wide.

- ui/pixelGrid.ts holds the rule. crispText() makes a text with
  Pixi's roundPixels, crisp() does the same for a Graphics of
  upright boxes, and hairline() is the width of one device pixel.
- Every text of the interface is made with crispText() instead of
  new Text().
- crisp() only rounds where the scale is not a whole number. At
  100 % and 200 % nothing changes.
- drawControlBox, drawHeaderBar and drawSectionBand draw their
  lines a hairline wide and flush with the edge. The widgets that
  draw lines of their own do the same.
- Two bands share exactly the line they draw, and a column still
  ends on a whole pixel.
- The stage is nudged by 1/32 px where the scale is not whole. At
  150 % every odd pixel is an exact half, and without the nudge one
  band came out a row shorter than its neighbours.
- Curves, icons and everything of the game stay unrounded. Rounding
  their points would bend them.
- The harness takes deviceScaleFactor as an option, and the hooks
  deviceColumn, edgeContrast and pixelGrid read the frame as the
  screen gets it.

Not in here: the subpixel smoothing a browser gives its own text.
Pixi draws a text into a transparent picture first, and there only
greyscale smoothing is possible.

Measured at 150 %, edge contrast before and after: top bar 31.5 to
47.8, the lists on the left 68.6 to 86.4, inspector 27.6 to 38.4.
At 100 % it is 46.5, 90.1 and 37.9, and the picture there is the
same as before pixel for pixel, apart from the fps display.

Tests:
- Gate on melektaus-atx: 206 files, 3114 tests (3104 + 10), exit 0
- Full run on melektaus-atx: 1655 of 1656 checks passed, smokes
  24/19/21. The one that failed was a condition of the new step
  62 itself: it scrolled an inspector that had nothing to scroll.
  The step now makes the window lower for that reading.
- e2e 61,62 after that fix: 14 checks. The full run was not
  repeated, and CI is switched off for now.
- Ten unit tests, nine in tests/ui/pixel-grid.test.ts
- Eight e2e checks in the new step 62, which runs at 150 %
- Eleven unit counter-probes fail tests
- Four browser counter-probes fail their checks. They ran before
  the fix to step 62 and were not repeated for it.
- Screenshots looked at for 125 % and 150 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 08:42:26 +02:00
adminandClaude Opus 5.5 2ff3a78607 Put the shapes of a vector drawing on layers and in groups (#190)
game-maker / gate (push) Successful in 2m41s
game-maker / suite (push) Failing after 7h24m17s
A vector drawing was one flat list of shapes. With many shapes the
list got hard to read, and several shapes could not be kept or moved
together. Now the shapes stand on layers, as in pixel art, and can be
put into groups that nest.

- A drawing has layers, each with a name and an eye. A hidden layer
  is neither in the tab nor in the picture a sprite wears, and its
  shapes cannot be pressed. One layer is the one worked on: the pen
  and Add shape draw onto it, and SHAPES lists what is on it.
- The inspector has a list LAYERS over the shapes. Its plus adds a
  layer, the cross on a row removes it with all on it, the last one
  stays. A double-click renames, a drag sorts.
- Group (Ctrl+G) puts the selected shapes into a group, Ungroup
  (Ctrl+Shift+G) takes it away. A group stands over its rows in the
  list, the rows one step in, and a row is dragged into a group or
  out of it.
- In the tab a press on a shape takes its outermost group, and a drag
  moves the group as one undo step.
- The model has layers with a tree of shapes and groups instead of a
  flat shapes array. The rules are pure functions in
  core/model/vectorTree.ts. A drawing saved before opens with all its
  shapes on one layer.
- SortableList takes rows with a depth and drops into a tree. The
  flat lists of tilemap and pixel art run as before.

Decided here:

- A group has no transformation of its own and cannot be hidden or
  locked. Rotating and scaling is the follow-up #291.
- A group is never empty: the one that loses its last shape goes
  with it.
- Several selected shapes are dragged together, grouped or not, and
  a click on a shape of the selection keeps the selection. This is
  what #217 asked for.
- Shift+click selects like Ctrl+click. With Select, Shift pans only
  beside the shapes, as in the scene view. The middle button pans
  anywhere, and with the pen and Paint Shift pans as before.
- A row of the list picks one shape of a group alone.
- Shapes change their layer through a row Layer in the inspector,
  which is there from two layers on. Rows are not dragged between
  layers.
- A group cannot span two layers, the button is off then.
- The pen does not draw onto a hidden layer, the hint says so.
- Groups are always shown unfolded.
- The four order buttons work inside the own group or layer.

Tests:
- Gate: 204 files, 3067 tests (3008 + 59), exit 0
- e2e 47,48: 108 checks. e2e 39,40,47,48 before the Shift change:
  209 checks. No full run, the sum of 1645 (1639 + 6) is reckoned
  and CI measures it.
- 59 unit tests: 36 in tests/core/vector-tree.test.ts, 23 in
  tests/editor/vector-layers.test.ts
- Six e2e checks with a real mouse in block 47j
- 47 unit counter-probes for layers and groups and three for the
  Shift click all fail tests, run on stein2
- Three browser counter-probes fail their checks
- Two screenshots looked at (group in the list, hidden layer)
- Only unit tested: the row Layer, sorting and removing layers,
  copying a group, the Shift pan beside the shapes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 23:13:43 +02:00
adminandClaude Opus 5.5 0f85476b93 Add points by double-click and delete the picked one (#288)
game-maker / gate (push) Successful in 2m39s
game-maker / suite (push) Successful in 13m8s
Four editors edit points, and all of them put a point down on a single
press on the line and took one away on a double-click. A stray click
added a point, and there was no way to see which point a key would hit.
All four now work the same way: the Curve tab, the Vector art tab, a
polygon collider in the scene view and a frame's collision outline in
the Texture tab.

- A double-click on a line, edge or segment puts a point there, in the
  Curve tab on free space too. The point is picked and stays in place.
  It can be dragged with the next press, as answered in the issue.
- A click on a point picks it, and it is drawn filled. A double-click
  on a point takes nothing away any more.
- Delete or Backspace takes the picked point away. Without a picked
  point the key removes the shape, the object or the outline as before.
- A right-click on a point picks it and opens a menu with Delete
  point. In the Curve and Vector art tab the four point types stand
  below it. Alt-click stays.
- A polygon keeps three corners and a curve keeps its ends. The entry
  is greyed out then.
- The pick ends with a click beside the points, with another object,
  shape or frame, and after the point is gone.
- editor/pointGestures.ts holds the rule as a pure function, and
  editor/pointMenu.ts builds the menu for all four.

Decided here:

- A single click on a line no longer adds anything. In Vector art it
  drags the shape, on a polygon it drops the pick, and in the Texture
  tab it makes that outline the active one and drops the pick.
- In the Texture tab a click inside the frame still appends a corner,
  and that corner is picked. Delete takes the last corner first. To
  remove the whole outline, click one of its edges before.
- The point the Pen put down last is picked, so Delete takes it and
  not the shape.
- Delete hits the picked point also when the tab has lost the keyboard
  focus, after a click in the shape list or after the menu.
- A vector shape that loses its last point is removed whole. Before,
  an empty shape stayed in the list.

Findings:

- Fixed: the new hint line in the Texture tab was cut off at the edge.
  The shorter text was not looked at in a picture again.
- Not fixed: in the scene view the collider's name label covers a
  corner that sits near the centre. That was so before.

Tests:
- Gate: 200 files, 2967 tests (2944 + 23), exit 0
- e2e, full run: 1629 checks passed (1625 + 4), exit 0
- Smokes: 24 / 19 / 21, exit 0
- After the full run an unused parameter and a comment were tidied.
  For that final state the gate and e2e 42,47,53 (105 checks) ran
  again, the full run did not.
- 23 unit tests in tests/editor/point-gestures.test.ts, for the rule
  and for each of the four editors
- Four e2e checks with a real mouse, two in module 42 and one each in
  47 and 53. The existing checks for adding and removing a point use
  the new gestures.
- 20 unit counter-probes all fail tests, run on stein2
- Two browser counter-probes fail, the build went through both times.
  Without the Delete wiring the key removes the collider. Without the
  right-click hook the menu is missing, and that run stops in module
  42, so 47 and 53 were not reached with it.
- Three screenshots looked at, the picked point is filled. None of
  the Vector art tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:36:13 +02:00
adminandClaude Opus 5.5 bc688f3efc Refactoring round 7: split BehaviorSystem, fewer copies (#85)
game-maker / gate (push) Successful in 3m50s
game-maker / suite (push) Successful in 12m3s
Recurring cleanup round over the code since round 6 (056a622), 105
commits. Behaviour is unchanged. Duplicated code in src went from 29
clones with 229 lines (0.32 %) to 14 clones with 109 lines (0.15 %),
measured with jscpd and the same settings as round 6.

- BehaviorSystem.ts shrinks from 941 to 589 lines. Collecting the
  pulses (keys, timers, collisions, animations) moved to
  behaviorPulses.ts. The NodeContext the nodes receive moved to
  behaviorContext.ts. Imports from outside stay the same, TimeScaled
  is still exported from BehaviorSystem.ts.
- PointKindButton and drawPointGrip replace the "Point:" button with
  its menu and the point and handle drawing that the curve and the
  vector art tab each had on their own.
- lerpVec, cubicAt and splitCubic in src/core/util/vec.ts replace four
  copies of the cubic curve and two de Casteljau splits.
- mapNested in Properties.ts is the one walk over lists and structs
  that liveObjects and objectRefs did separately. withFieldRenamed
  and withoutField replace three copies of renaming a struct field
  and two of removing one.
- jsonBody and staleRevision build the JSON body and turn a 409 into
  StaleRevisionError for all stores. The auth store turns a 401 into
  its error in one place.
- Smaller helpers: structAssetFrom for the two "Make struct type"
  commands, shownObject in the inspector, selectedItem for the size
  gizmo and the polygon corners, solidTiles in the tilemap collision,
  requireVariablesAsset and requireBehaviorVariable.
- Left as is on purpose: the plain forwarding from the collision and
  physics systems to Solids, the pointer wiring of the canvases and
  the identical last lines of the two make commands.
- Removed unused code: tilemapTileIds and tilemapsUsing were never
  called since #128 and #131. EditorState.runtimeProject became unused
  with GameRun in #266. The assetId getter of the make commands was
  read only by a test.
- 60 values used only in their own file lost their export. The shader
  and particle node catalogues stay exported as in round 6.
- Five comments that told history now describe the current state. The
  doc comment of BehaviorSystem sits on the class again.
- README lists the struct, clip area and curve assets and mentions the
  game link from #266.

Findings:
- Deleting a tile that is painted on a tilemap leaves its cells in
  place, and they draw nothing. The delete dialog does not mention the
  tilemaps. That was probably the job of the unused tilemapsUsing.
  Left as is, because fixing it changes behaviour.
- The solid filter for tile surfaces was untested. A bent filter
  stayed green. A new test in tests/core/surface.test.ts now fails
  against it.

Tests:
- ./check.sh: 195 files, 2815 tests, exit 0 (one test removed with
  tilemapsUsing, one added for tile surfaces).
- Full e2e run: ALL PASSED, 1580 checks.
- Smokes jumprun, tetris, actor: 24, 19 and 21 checks, all passed.
  The example project files were restored byte for byte afterwards.
- Counter-checks, 12, all fail as expected: 10 in unit tests (among
  them mapNested, splitCubic, cubicAt, staleRevision, the stay pulse,
  the late timer and the time scale), 2 in the browser (Point button
  always enabled fails 47, selectedItem without a hit fails 29).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:16:37 +02:00
adminandClaude Opus 5.5 3a3401f8ac Let the sort knob win over the corner handle it sits on (#228)
game-maker / gate (push) Successful in 3m19s
game-maker / suite (push) Successful in 9m57s
The CI run for 30de4d3 failed on e2e 37b-ii: dragging the sort knob of
a nine-slice frame resized the frame instead (h 18, lifted null). The
knob's middle sits exactly 8 px left of the frame, which is also the
edge of the corner handle's 8 px tolerance. Until now only the rounding
of the pointer decided which of the two won. The slimmer header bars of
#245 moved that rounding to the corner handle's side. The bug itself
has been there since #228.

- sortLineAt bounds the knob where it is drawn, from a gap to the reach
  left of the frame, instead of by the handle tolerance.
- TextureCanvas passes that gap in image pixels, scaled by the zoom.
- The corner handle stays reachable at the corner itself and to the
  right of and below it.

Tests:
- ./check.sh: 178 files, 2576 tests, CHECK EXIT 0
- GM_STEPS=1,37: ALL PASSED, 26 checks (37b-ii: lifted 6, h 24)
- New unit test: the knob's middle belongs to the knob even inside
  the corner handle's reach. It fails with the old bound.
- Full e2e run not repeated. 37b-ii was the only failure in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:36:40 +02:00
adminandClaude Opus 5.5 89048413f1 Let a texture frame set where a sprite stands for Y sorting (#228)
A sprite sorted by Y stood on the bottom of its box. With a shadow or
empty rows under the feet that is too low, so a figure slipped in
front of a tree it should stand behind. The line belongs to the frame,
not the texture, because one texture can hold several sprite sheets.

- New field `sortLine` on a frame: whole image pixels up from the
  frame's bottom edge, 0 to the frame height. 0 is stored as nothing,
  so old projects stay the same. Shrinking a frame keeps the line
  inside it.
- `feetOf` in drawOrder.ts lifts a sprite by the same share of its
  box. Scene view, game view, play and picking all use it. In an
  animation the line of the frame shown right now counts.
- The texture tab draws the selected frame's line dashed in yellow
  with a knob left of the frame. At 0 the line shares the bottom edge
  with the nine-slice border and a resize handle, so the knob is
  tested first. A line inside the frame can be dragged directly,
  tested after the nine-slice lines.
- The inspector has a new "Sorting" section with a "Sort line"
  field. Dragging or typing is one undo step each.

Limit: frames of one walk cycle need the same value, set by hand.
Otherwise the figure changes its place in the order while walking.

Tests:
- ./check.sh: 160 files, 2356 tests, CHECK EXIT 0 (before 2350).
- 6 new unit tests in tests/viewport/sort-by-y.test.ts.
- e2e 37 and 39-43: ALL PASSED, 162 checks. New check 37b-ii drags
  the knob up 6 px and undoes it, plus the inspector row.
- Counter checks: if feetOf ignores the line, exactly the 3 order
  tests fail. If the knob cannot be grabbed, 37b-ii fails.
- The e2e sum in the README is 1380, counted, not yet seen in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:34:37 +02:00
adminandClaude Opus 5.5 c1a6d63edb Cover which tilemap layer and texture frame get a collision fill
game-maker / gate (push) Successful in 3m27s
game-maker / suite (push) Successful in 9m47s
f4cc681 fills tilemap solids only for the selected map and layer, and
texture collision only for the selected frame. Neither the scene view
wiring nor the texture canvas had a test for that.

- __gm.tilemap.solids() now reports the cells that were filled, and
  __gm.texture.drawInfo() counts the filled outlines.
- e2e 40l-iii: a selected tile fills nothing. The map selected with
  the upper layer fills only cell (4,0), with the lower layer only the
  3x3 block. Two selected objects fill nothing again.
- e2e 42: a selected frame fills its one outline. Deselected it fills
  none, while the outline stays visible.
- README check total goes from 1375 to 1377. CI confirmed 1375, the
  new total is counted, not yet measured.

Tests:
- ./check.sh: 159 files, 2337 tests, CHECK EXIT 0
- e2e 39-43: ALL PASSED, 147 checks (before 145)
- Counter checks: a scene view that fills every map fails only the
  tilemap check. A texture canvas that fills every frame fails only
  the texture check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 13:22:45 +02:00
adminandClaude Opus 5.5 f4cc681df4 Fill texture and tilemap collision only where it is being edited
game-maker / gate (push) Successful in 2m57s
game-maker / suite (push) Failing after 9m24s
The faint fill on every frame outline and on every tilemap made the
scene and the texture editor look green all over. Like the collider
before, the fill now shows only on what is selected, and a bit
stronger, so it marks the thing being worked on.

- Texture collision: all frame outlines stay visible. Only the
  outlines of the selected frame are filled, at alpha 0.3 instead of
  0.15 on every frame.
- Tilemap: the outline still shows where the whole map blocks. The
  fill appears only while the tilemap alone is selected, and only for
  the part the layer chosen in the panel adds. Solid is 0.3 and
  one-way 0.15, before 0.12 and 0.06 on every map.
- New query layerSolids(layerId) in items.ts. It builds the same
  solids for one layer and runs only for the selected map.
- The small landing wedge on one-way edges stays filled. It belongs
  to the edge marker, not to the area.

Not covered by tests: the texture canvas and the scene view wiring
that picks the map and the layer. Neither was looked at in a browser.

Tests:
- ./check.sh: 159 files, 2337 tests, CHECK EXIT 0 (before 2334)
- e2e 1-17,39-43: ALL PASSED, 668 checks
- 3 new unit tests in tests/viewport/tilemap-fill.test.ts
- Counter-check: with layerSolids ignoring the layer, exactly the
  layer test fails. Original restored, checked with cmp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 12:23:13 +02:00
adminandClaude Opus 5.5 fd6af84474 Keep the texture tab smooth with many collision outlines (#220)
game-maker / gate (push) Successful in 3m0s
game-maker / suite (push) Failing after 8m37s
With a large sheet the texture tab stalled on every mouse move. A
profile over a pan with 1728 frames showed two causes. Half of the time
went into convexPieces, which split every outline into convex pieces
only to pick its colour. And every pointer move during a pan rebuilt
and re-tessellated the whole overlay geometry.

- TextureCanvas draws on two layers. `sheet` holds every frame except
  the selected one and is rebuilt only when the zoom, the selection or
  one of those frames changes. `overlay` holds the selected frame, its
  handles and the rubber band of a new frame.
- Both layers are drawn without the pan. A pan only moves the
  container and places the name labels again.
- SheetCanvas has a new hook `panned()`. By default it calls `redraw()`
  as before, so Pixel Art and Vector Art are unchanged.
- The validity of an outline is remembered per outline and comes from
  polygonProblem, with the same result. This works because the model
  replaces an outline list on every change and never edits it in
  place, the same as frameOutline.ts already relies on.
- The name plates stay below the outlines and the texts on top. Checked
  on a screenshot: the corner handle next to a label sits on top.
- New debug hook `__gm.texture.drawInfo()` with the rebuild count, the
  outline count and the screen positions of outlines and names.

Measured with a throwaway probe, 1728 frames, 120 moves, main thread
time per move, before and after in the same browser:

- Middle-click pan: 76.9 ms -> 4.7 ms (4.4 ms without frames)
- Dragging a frame: 51.7 ms -> 10.1 ms
- Wheel zoom: 52.2 ms -> 30.9 ms
- Selecting another frame: 67.6 ms -> 41.0 ms

Findings:

- Wheel zoom and selection still rebuild `sheet` once on purpose. That
  stays a single hitch per notch or click on very large sheets.

Tests:

- ./check.sh: 148 files, 2214 tests, CHECK EXIT 0.
- e2e 1-16,37,39-43 on the final build: ALL PASSED, 641 checks.
- Three new checks in 42c-ii. A corner drag does not rebuild the other
  frames. An undo on an unselected frame reaches the picture (1 -> 2
  outlines). A pan moves outlines and names by exactly 40/25 px without
  a rebuild.
- Counter-checks: a missing outline in the cache key and a pan that
  does not move each fail at their check. The selected frame in the
  key fails alone with 9 rebuilds during an 8-step drag.
- Check total 1333 -> 1336 (confirmed by CI at ceaa44e, task 259).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 15:57:13 +02:00
adminandClaude Opus 5.5 30fb3a2316 Dragging previews once per frame instead of on every move (#213)
game-maker / gate (push) Successful in 2m52s
game-maker / suite (push) Successful in 8m43s
Scene view, texture tab and graph called state.preview() on every
pointer move. Each call rebuilds panels, the editor run and generated
images, so a drag in a busy project cost up to 34 ms per move. The
model is still written on every move. Only the preview now waits for
the next frame.

- New FramePreview in src/editor/framePreview.ts. request() goes into
  the move handler, cancel() into the release. The rule lives in one
  place instead of six copies.
- Scene view: object drag, polygon corners and tilemap strokes use it.
- Texture tab: frames, slice edges and collision corners use it.
- Graph: nodes, groups and comments use it through GraphDrags. That
  covers the state machine, shader and particle tabs as well.
- Vector art is unchanged. Its frame already writes the model since
  #129.

Findings:
- The texture tab paid twice per move (232 project events for 120
  moves). Pixi sends a second pointer event 4 to 8 ms after each real
  move as a hover update. It now lands in the same frame for free.
- Inspector scrubbing and gradient stop dragging still preview on
  every input. Both lie outside the three canvases of the issue.

Measured with a throwaway probe, 120 moves at 8 ms, 120 extra objects:
scene 26.2 -> 18.5 ms/move, texture 34.2 -> 17.9, graph 28.8 -> 17.0.
Project events dropped from 120/232/119 to 48/48/41.

Tests:
- ./check.sh: 147 files, 2204 tests, exit 0 (before 146 / 2199).
- 5 new unit tests in tests/editor/frame-preview.test.ts, including
  the real GraphDrags. Counter-checks (direct preview in the graph,
  no cancel on release, one key per name) each fail the expected test.
- e2e 1-16,29,31,36,37,39-43: 708 checks passed.
- e2e 1-16,26,28: 634 checks passed.
- No new e2e check, the check sum stays at 1314.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 11:08:11 +02:00
adminandClaude Opus 5.5 056a622524 Refactoring round 6: shared sheet canvas, fewer copies (#85)
game-maker / gate (push) Successful in 2m57s
game-maker / suite (push) Successful in 8m55s
Recurring cleanup round. Behaviour is unchanged. Duplicated code in
src went from 27 clones with 299 lines to 6 clones with 56 lines
(jscpd, the two identical OFL licence texts not counted).

- SheetCanvas is the new base of the texture, pixel art and vector art
  canvases. It holds background, mask, hint, panning, wheel zoom, fit,
  view save and restore and the screen mapping. Pixel art keeps its
  whole-pixel panning and fixed zoom steps.
- mixColor in src/core/util/color.ts replaces three copies in
  animation, gradient and particles. It clamps t like the particle one.
- fingerprint now lives once in generated.ts, next to the revision
  contract from #185.
- MoveCommand next to RenameCommand is the base of the three reorder
  commands for tile pictures, tilemap layers and vector shapes.
- The two raycast nodes share inputs, settings and filter via rayOf.
- canvasRect() lives once in AssetTabEditor.
- Removed unused code: findPixelArt, isDrawableShape,
  GeneratedPictures.has and SnapToggle.snapping. Tests now check the
  same things through equivalent paths.
- 38 values used only in their own file lost their export.
- Three comments that told history now describe the current state.
  The bundle stayed md5-identical for those edits.
- README lists vector art, pixel art and palette, and links the two
  wiki pages.

Findings:
- PaintTilesCommand.changes is meant to prevent empty undo steps but
  has not been called since #128. The rectangle tool therefore records
  a no-op undo step when it paints the same tile over itself. Left as
  is, because fixing it changes behaviour.
- No test covered middle-click panning in any sheet tab before. 13f
  and 48a now check it.
- The default values of the ray inputs were untested. Length 1000
  could become 10 without a red test. tests/core/raycast.test.ts now
  covers it.
- CI measured 1308 editor checks at d7c0b7c, not the 1307 counted for
  #164. The sum is now 1310 (1308 + 2), computed.

Tests:
- ./check.sh: 144 files, 2143 tests, exit 0 (before 2142).
- e2e 1-16,37,39-43,47,48 fresh: ALL PASSED, 729 checks.
- e2e 1-13,47,48 with the new checks: ALL PASSED, 518 checks.
- Counter-checks, each restored via cp and cmp: halved panning fails
  both new pan checks; unclamped mix fails 1 test, no blue channel 3;
  coarser hash 3; broken reorder undo 2; triggers setting forced on 2;
  bent ray length 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 06:57:43 +02:00
adminandClaude Opus 5 6a2b06dd4b Texture fills show in the vector tab as soon as they load (#196)
game-maker / gate (push) Successful in 2m21s
game-maker / suite (push) Failing after 7m29s
After signing out and back in, a texture fill stayed in its base
colour until something else redrew the vector tab. The texture
library is a singleton that outlives the editor. Four widgets
subscribed to it and never unsubscribed. After a new sign-in the
first loaded texture called a destroyed canvas, which threw and
stopped the loop before the vector tab heard about the image.

- TextureCanvas, TileCanvas, ShaderPreview and WorldViewport now
  unsubscribe in destroy(). This is the pattern FontPreview and
  VectorArtCanvas already use. WorldViewport leaves both the
  texture and the font library.
- FileLibrary.notifyChange catches errors per listener. A broken
  listener no longer starves the others. The error is still
  logged as "[textures] a listener failed".
- e2e 47i no longer forces a redraw. It waits up to 3 s for the
  tab to show the texture on its own.

Findings:
- The trigger was e2e module 45 (sign out, sign in), not the
  texture fill. My note in #196 that the browser showed no
  exception was wrong. The counter-check shows it clearly.

Tests:
- ./check.sh: 137 files, 2051 tests, exit 0 (before 136 / 2049)
- 2 new unit tests in tests/viewport/file-library.test.ts
- GM_STEPS=45,47 after a fresh seed: ALL PASSED, 71 checks
- Counter-check with the old source and the new check: 1 FAIL
  (shape stays #0000ff) plus the TypeError. Old FileLibrary.ts
  fails the first unit test.
- No full e2e run locally. The check count is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 17:48:10 +02:00
adminandClaude Opus 5 2d79230b55 A frame's collision outline can be generated from its pixels (#161)
game-maker / gate (push) Successful in 2m13s
game-maker / suite (push) Successful in 7m42s
Drawing an outline by hand for every frame of a sheet is slow, and the
outline mostly follows the opaque part of the image anyway. The texture
tab now traces it: one click, a corner budget, and every Frame
collision collider and tile using that frame blocks with the result.

- "Generate collision..." sits in the texture tab header and is off
  while the texture has no frames.
- The dialog has Max points (default 8, 3 to 64), Solid from ... %
  alpha (default 50) and All frames of the texture. A live line says
  what Generate will write, and Generate stays off when nothing is
  solid enough.
- traceCollision walks each island of solid pixels along the pixel
  edges, then drops the least important corners until at most N are
  left and no stair step remains. A slope becomes one straight edge.
- An outline never crosses itself. Where the image would force that,
  the island's convex hull is taken instead.
- Crumbs under 1 % of the frame area are dropped unless they are the
  only island. Holes are not reproduced.
- Existing outlines are replaced. One frame or all frames is a single
  undo step.
- TextureLibrary hands out the sheet's RGBA data, and a pure function
  cuts a frame's region from it.

Findings:
- The wedge in the e2e fixture gets 4 corners, not 3. Its tip is one
  pixel wide in the image, and that real edge stays while corners are
  still free.

Tests:
- ./check.sh: 131 files, 1941 tests, CHECK EXIT 0 (was 130 / 1931).
- 10 new unit tests in tests/core/trace-collision.test.ts, including
  150 random shapes that must all be valid and within the budget.
- GM_STEPS=42, freshly seeded: ALL PASSED, 20 checks (was 16).
- Three counter-probes bite: no hull fallback, no stair threshold,
  dialog ignoring the alpha threshold.
- Check sum 1202 in README is computed (1198 + 4), CI measures it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 08:00:30 +02:00
adminandClaude Opus 5 cc285af10a Tabs are open documents: Scene, Game and Plugins stay as fixed tabs, every other asset opens in its own tab (double click or Enter in the asset panel, on creation, from the list button), several assets of one kind can be open at once, a tab is labelled with its kind dimmed before the name, cut at 160 px and renamed with its asset, closes with its x or a middle click and goes away when its asset is deleted; a tab always shows its own document instead of following the selection, the picker in the tab head is gone, zoom and scroll are kept per document for the session and dropped on close, a double click on a call node stays in the same tab with breadcrumbs per tab; one editor per kind is switched between documents, the placement rules live in the pure module documents.ts, and the test hook showTab stays as a compatibility path; new e2e module 44 documents (#171)
game-maker / gate (push) Successful in 1m59s
game-maker / suite (push) Successful in 6m53s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 15:49:54 +02:00
adminandClaude Opus 5 947a6b1977 Collision outlines for autotiles: a frame holds several outlines (click adds a corner to the active one, Ctrl/Cmd-click starts a new one, clicking another outline makes it active, Delete removes the active outline and never the frame in collision mode), an autotile's outlines are drawn over the whole block, cut at the quarters and each cell takes the pieces of exactly the quarters its image is made of, so edges, outer corners and notches follow automatically; a tile of the block with nothing drawn blocks fully, cells of only full quarters merge back into rectangles, collision scale does not apply to an outlined autotile, and the previous format with one flat outline still loads (#141)
game-maker / gate (push) Successful in 1m53s
game-maker / suite (push) Successful in 6m41s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 01:56:20 +02:00
adminandClaude Opus 5 32057a96fc Physics stage 2: colliders gain the shapes capsule and polygon, a polygon keeps its corners in the property points, turns, scales and mirrors with its object and is edited in the scene view (drag a corner, click an edge to insert one, double-click to delete, at least three stay, each step one undo step), a concave polygon is split into convex parts, a self-intersecting one is drawn red and collides with nothing, Rapier gets a capsule and one convex collider per part on the same body, and Move and the editor run use our own f64 geometry for the new shapes, where on a tie between two normals the one that blocks the move least wins, so a box no longer stops at the foot of a slope (#151); first version of a collision outline per texture frame, edited in the texture tab with the new Collision toggle, which a plain tile blocks with instead of its rectangle (#141, to be reworked)
game-maker / gate (push) Successful in 1m52s
game-maker / suite (push) Successful in 6m41s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:56:25 +02:00
adminandClaude Opus 5 bfbcbe604c The tile list in the tilemap section is a palette of pictures: each tile shows its lone tile (the four convex corners for floor and wall, the picture itself for none) with its name below, laid out with the image-picker grid (#139)
game-maker / gate (push) Successful in 1m41s
game-maker / suite (push) Successful in 6m15s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 10:15:51 +02:00
adminandClaude Opus 5 2a390e52ff The scene view and the animation preview share one camera, the three form dialogs one base, and keys are found through one set of helpers (#85)
game-maker / gate (push) Successful in 1m36s
game-maker / suite (push) Successful in 5m41s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 14:19:53 +02:00
adminandClaude Fable 5.1 cc6db5f350 Three stores, three importers and three pickers that differ only in their type are one of each (#85)
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m46s
Refactoring round one: HttpFontStore/HttpSoundStore/HttpTextureStore collapse into a generic
HttpFileStore over a FileStore contract; fontImport/soundImport/textureImport share assetImport
with an ImportKind per sort; ImagePicker/FontPicker/SoundPicker sit on FilePicker and describe
only their row. The group/comment drag logic leaves GraphCanvas for annotationDrag.ts (with its
own unit test), declaredRows.ts replaces the duplicated row build in particles.ts and shaders.ts,
and dead code in font/sound/compile/emission/transform/actions is gone. README: asset list,
check count 958, the two-job CI, the Scenes wiki page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:38:54 +02:00
adminandClaude Opus 5 be8f2331de A frame that keeps its corners can be stretched to any size (#41)
game-maker / suite (push) Successful in 6m21s
TextureFrame carries optional `border` (left/right/top/bottom) and
`fill` (stretch | repeat); absent keys mean "not sliced", so no format
bump and undo restores the plain frame instead of four zeroes.

The sliced sprite builds its own geometry instead of the shared
UNIT_QUAD: a 4x4 grid, and for `repeat` one quad per tile carrying the
frame cell's real UVs -- tiling in the geometry, not in the sampler, so
a frame out of a sheet cannot pull its neighbour in. Above 64 tiles per
axis it stretches, to bound the buffer.

A sprite narrower than left+right scales both borders down together
until the middle is exactly 0, so the corners meet at a seam and never
overlap.

The texture editor gets four draggable border lines plus the same four
numbers in the inspector. Hit order is resize handles, then the lines,
then the frame body -- handleAt reports 'move' anywhere inside a frame,
so lines placed after it were unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:04:09 +02:00
adminandClaude Opus 5 3b039384c4 A search that ignores what a thing is called is no search, so the name wins (#43)
game-maker / suite (push) Failing after 10m23s
Typing "variable" into the node palette landed on `Run behavior`, because the
filter weighed label, type and description alike and then sorted by category.
`searchRank` in `src/ui/search.ts` now ranks a match: exact label or type, then
prefix, then label, then type, and only last a hit that lives in the
description alone. Category and alphabet order within a rank. The searchable
`Menu` uses the same function, so searching means the same thing everywhere.

The `+` menu grew to fifteen entries in declaration order. It is built in two
alphabetical groups now -- the types that go into a scene, then the ones that
stay in the library -- with a separator between them, the split `isPlaceableType`
already draws. `ASSET_TYPES` itself stays untouched: it is also the
serialisation and check order. A search flattens the groups and drops the lines.

Popups were capped at fixed heights (320 menu, 260 palette, 340 pickers) while
the canvas had room to spare. `Popup.roomFor()` measures its own chrome and
takes the window height minus a tenth at each end. Placement clamps
asymmetrically: the bottom margin is enforced, the top is only a budget, so a
menu anchored near the top stays at its button instead of being pushed off it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:21:20 +02:00
rootandClaude Opus 5 03c4323794 Image picker: tiles are containers, not graphics with children
game-maker / suite (push) Successful in 9m14s
The twin of the font picker fixed earlier today: every tile was a Graphics
carrying its sprite and caption, which Pixi v8 answers with "addChild:
Only Containers will be allowed to add children". The background moves
into its own Graphics inside a Container, and the step watches the console
while the picker opens, as the font one does.

Editor suite: 784 checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 21:17:26 +02:00
rootandClaude Opus 5 4f240a50fd Texture tab: one masked container for everything the view moves (#82)
Only the picture was masked, so the checkerboard, the frame outlines, the
handles and the name plates were drawn over the panel next door once the
sheet was zoomed past the tab. The dark block in the report is the
checkerboard's base: checker.ts fills the whole image rect and clips only
the light squares to the widget.

Everything that pans and zooms now sits in one masked container, the
pattern WorldViewport and AnimationCanvas already use. A check samples
pixels outside the tab and names the frames drawn past it; a guard in
front of it asserts that zooming really pushes the sheet over every edge,
or the pixel probes would measure nothing and stay green forever.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 21:17:21 +02:00
DeveloperandClaude Opus 5 99c5d2ad78 Uploads carry the name the file came with (#83)
An upload sends its file name next to the raw bytes, and the server keeps
it in a sidecar next to the content-addressed file. Both pickers caption
every tile and row with it, and an asset made there is named after the
file instead of "Texture" / "Font". Last upload of the same content wins.

The API's own set_error_handler ignores error_reporting(), so @-silenced
warnings still throw: a missing sidecar took the whole listing down with a
500. Existence is checked instead of suppressed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 20:32:45 +02:00
rootandClaude Opus 5 1135bf4562 Play sprite animations from an animation asset worn through texture
An animation asset is a list of pictures (whole textures or frames) with
frames per second and Loop/Once/Ping-pong playback. A sprite wears it
through its texture property, so the picker, instance overrides and the
Properties node work unchanged; there is no dedicated play node.

A different animation starts from its first frame, the same one set again
runs on, so a graph may choose Run or Idle every frame. The clock lives on
the entity, not in props, which the world writes back as overrides. The
AnimationSystem ticks before behaviors, and On animation finished fires
once when a Once animation reaches its end.

The Animation tab plays a preview above a frame strip: reorder by dragging,
Delete, duplicate, + Frames, pause/resume, and wheel zoom about the pointer
with pan and Fit. Scene and Container tabs show the first frame; edit mode
has no clock. Right-clicking a texture creates an animation from its frames.

Button now measures its label with the bold font it draws in, which cut
the Animation tab and red dialog buttons short.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 23:52:34 +02:00
root 2cf788eedd Let a value be a bundle of named fields
PropertyValue was number | string | boolean | list. A struct joins it, which
costs two things the typechecker found rather than I did.

An object on a data edge used to mean a live entity, so asEntity would have
taken a struct for one and asString would have read .name off it. EntityHandle
now carries a symbol brand – a symbol so no field name can impersonate it, and
it never serializes, which suits a thing that exists only while the game runs.
It is built in exactly one place.

String(value) would have written a struct as [object Object] at 39 call sites.
They go through textOf(), which is identical for primitives.

normalizeGraph dropped everything but primitives from node params, so the List
node lost its value on every save ([3,1,4] round-tripped to undefined).
Variables were spared: they have a schema, params have none.
2026-09-10 08:14:18 +02:00
rootandClaude Opus 5 3b3f374e10 Split the object inspector, and sweep out the section banners
objectSection.ts had grown to 559 lines around three separable parts: the
queries, the vector rows and the parent row. Four object commands repeated the
same capture/restore preamble – applyCommands stays out, its five carry extra
undo state whose restore order matters.

58 dashed banner comments are gone. The nine labels in nodes/index.ts stay:
they are the only structure a flat 90-entry array has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 07:14:45 +02:00
rootandClaude Opus 5 11981b40d0 Give the three asset tabs one base, and move the viewports into the editor
src/viewport/ held three classes only src/editor/ ever used, and framing.ts sat
under graph/ although the scene view wants the same bounds sum. The graph, the
texture and the container tab repeated the same header, subscription and
disposal code with two silent divergences: the graph tab said "nothing shown"
with null instead of '', and the texture tab refell back to the first asset on
a selection change but not on a project event.

These arrive together because GraphCanvas ties them: it reads framing from its
new home, hands its fold marks to folding.ts, and its '' change is what
GraphEditor must agree with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 07:14:38 +02:00
rootandClaude Opus 5 b13b130da7 Share the one HTTP envelope and the one byte formatter
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>
2026-09-10 07:14:23 +02:00
root 88ec367ab0 Offer the pictures the server already holds when adding a texture
"+ → Texture" opened the machine's file dialog, so a picture that was uploaded once
but lost its asset – an undone import, a deleted asset – was unreachable from the
editor while still sitting on the server. Nothing collects those files, by design.

It now opens a grid of every picture the server holds, with thumbnails: pick one and
"Use image" makes a texture asset on that file, "Upload…" is the way in for bytes the
server does not have yet and lands in the same import as a drop. A tile captioned with
an asset name is one this project already wears – picking it selects that asset instead
of making a second one, since two assets on one file would fight over `filter`.

- `GET ?r=textures` now reports `width`/`height`; a texture asset holds the pixel size,
  and reading it off the GPU texture would tie the asset's correctness to load timing.
- `TextureLibrary.load(file)` loads without touching `scaleMode` – a thumbnail must not
  restage the filter of a picture the scene is drawing with.
- The grid geometry lives in `texture/imageGrid.ts` as pure functions; inside the widget
  no test could reach it.
- The part after an upload is now one function shared by the drop import and the picker,
  so "one file = one asset" is stated in a single place.

12 new E2E checks; the strongest one undoes an import, leaving the file on the server
with no asset naming it, and picks exactly that file back out of the dialog.
2026-09-09 15:55:15 +02:00
root 1b16a55ffc Round number fields to the precision they display
`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.
2026-09-09 10:11:06 +02:00
rootandClaude Opus 5 6bedc948be Make the viewport label size one setting
Scene labels used `small` (11 px), the Texture tab used `body` (13 px). Both now
read `Theme.viewportLabelSize` through two tokens that differ only in colour.

Dropping the per-frame `label.style.fill` assignment matters: textStyle() returns
shared cached instances, so with both views on one token it would have recoloured
the scene labels too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 16:59:17 +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
rootandClaude Opus 5 35a1a23af8 Add a Texture tab for defining frames
A new Texture tab next to Scene/Game/Graph shows a texture sheet and lets
frames be drawn on it: drag to create, move, resize via eight handles, edit
name and rect in the inspector, Delete to remove. Every change is a command,
so one gesture is one undo step. "Slice..." cuts a grid from columns and rows
with optional offset and spacing, previewing the resulting frame count and
cell size live.

Sprites pick a frame instead of the whole texture through the same `texture`
property: it now holds either a texture id or a frame id. Frame ids come from
the same generator and are document-wide unique, so one picker, one property
and one set of instance overrides cover both cases, and every sprite written
before this still means the same thing.

A Graphics fill ignores a Pixi sub-texture's frame and draws the whole image,
because it derives its uvs from the shape bounds and the inverted style
matrix. The cutout is therefore a matrix (`viewport/textureFill.ts`), which
keeps a sheet at a single GPU upload no matter how many of its frames are on
screen at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:27:23 +02:00