Commit Graph
20 Commits
Author SHA1 Message Date
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 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 14972d057f Refactoring round 8: hide data files, safe saves, Esc as Cancel (#85)
game-maker / gate (push) Successful in 3m53s
game-maker / suite (push) Successful in 11m55s
Recurring cleanup round over the code since round 7 (bc688f3). The
issue now also asks to fix bugs and small inconsistencies found on the
way, so unlike the rounds before, this one changes behaviour in a few
places. Duplicated code in api went from 23 clones with 228 lines to
none (jscpd, at least 50 tokens). In src it stays at 22 clones with
271 lines, the same before and after.

Bugs:
- nginx served project and snapshot files as they were, so anyone who
  knew a project id could read it without signing in. Every file the
  API writes now goes through AtomicFile (temp file, mode, rename).
  Projects, snapshots, releases, accounts and sessions are 0640, and
  nginx answers 403 for them.
- Any signed-in account could delete a shared image, font or sound,
  which projects of other accounts may wear. Deleting one is now for
  administrators only. The editor never deleted a file, so the
  client's delete is gone.
- Two saves of one project could run at once. The lock file was
  removed after each save, so a waiting save and the next one locked
  different files. The lock file now stays, and deleting a project
  takes the lock as well.
- Bytes that are no picture got a 500 instead of a 422, because the
  error handler also threw for warnings silenced with @.
- A session file was rewritten in place. A request reading it at that
  moment found it empty and was signed out. It is replaced atomically.
- Delete did nothing for a shape picked in the vector art list. Delete
  now goes through removeSelection, one case per kind of selection,
  and a kind without a case fails the type check.

Inconsistencies:
- Escape on a question does what its Cancel does. Cancel on "Delete
  project?" and "Restore snapshot?" leads back to the picker or the
  list like the other questions did, and so does deleting a project.
- Editor texts use American spelling throughout, like the port names:
  48 British spellings in 45 lines (colour, centre, initialised).
- objectName and groupName replace eleven copies of "own name, else
  the asset's" and three of "Group". One copy fell back to the object
  id instead of "?".

Duplicates, dead code, comments:
- routeFiles serves images, fonts and sounds instead of three copies.
  HashedFileStore holds what the three stores shared: keeping a file
  under its hash with its name, listing and deleting. The stores keep
  only which bytes they take and what a record says.
- AtomicFile replaces nine copies of temp file plus rename.
- removeShapes is the one way the canvas, the inspector button and
  Delete take shapes out.
- GraphInterpreter.fire was called by tests only. The 54 calls use a
  helper in tests/helpers.ts now.
- FileStore.delete, HttpFileStore.delete and the PluginApi type were
  unused. A doc comment in actions.ts sat above the wrong function.

Checked and left as is:
- solidCells is used by tests only. It is the per-cell view before the
  merge into rectangles, the one place the largest-scale rule shows.
- An undo checker ran all 124 commands of the unit tests and compared
  the project before and after undo. The only differences are empty
  ports, groups and notes lists where there were none, which the file
  format drops anyway.

Docs:
- README: the check sum.
- Wiki, own commit: API (admin-only deletes, 422, file modes, lock
  file), Home (the "Not yet" list named three things that exist),
  Concepts, Keyboard, Vector art, Architecture and Development, and
  the 38 characters Gitea flagged as ambiguous on eight pages.
- .gitignore: the lock files, and pasted-*.png in any folder, which
  keeps the screenshot archive out.

Tests:
- ./check.sh: 195 files, 2817 tests, exit 0 (+2 for the name helpers).
- PHP: accounts 52 in the web container and 53 in the CI image with
  the race test (+2), snapshots 15 (+1), releases 20.
- Full e2e run: ALL PASSED, 1586 checks (+6 in 12, 47, 49 and 50).
- Smokes jumprun, tetris, actor: 24, 19 and 21 checks. The example
  project files were restored byte for byte afterwards.
- The file stores of HEAD and of this commit give byte-identical
  output for the live folders and for store, list and delete in a
  throwaway folder.
- Counter-checks, all fail as expected: a 500 for bad bytes fails 12,
  no admin check fails 49, removing the lock file again loses saves
  in 3 of 3 runs (151 to 170 of 321), Delete ignoring shapes fails 47,
  Escape that only closes fails 50, and dropping the shape case fails
  the type check (TS2366).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:12:55 +02:00
adminandClaude Opus 5.5 a5ae21481b Type point values, snap and scrub the preview in the curve tab (#248)
game-maker / gate (push) Successful in 3m23s
game-maker / suite (push) Successful in 10m9s
Building a curve by mouse alone was fiddly: there was no way to put a
point on an exact value, no grid, and the running preview could not be
stopped at a chosen moment. The hand cursor over points also looked
like a link instead of a drawing tool.

- Number fields t and v in the header show the selected point. Typing
  or dragging on them goes through the same rules as a mouse drag, so
  points never overtake and handles stay clamped. One undo step each.
  t is greyed out on the two end points.
- The magnet button from Vector art snaps points and handles to a
  0.05 grid in t and v, when placing on free space and when dragging.
- Points and handles keep the default arrow in the curve tab and in
  Vector art. The pixel art palette dropped pointer as well, which is
  beyond the issue. Collider corners never had it.
- The preview now starts off. A vertical time line with a knob under
  the t axis sets t when dragged sideways. Dragging the dot on the
  rail jumps to the first t where the curve reaches that value, or to
  the closest one if it never does.
- The rules are pure functions in curveView.ts (withCurvePointSet,
  snappedToGrid, shareAtValue, previewSecondsAt), the canvas only
  calls them.

Tests:
- ./check.sh: 180 files, 2616 tests, CHECK EXIT 0
- GM_STEPS=1,47,53: ALL PASSED, 87 checks
- 4 new unit tests in curve-view.test.ts, 4 new e2e checks in 53
  (53h-53k) and 1 in 47, all with real mouse and keyboard
- Counter-probes bite: without "first t" 1 unit test fails, with
  pointer, no snap, no field commit, no scrub or preview on 5 browser
  checks fail, pointer in Vector art fails the new 47 check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:45:53 +02:00
adminandClaude Opus 5.5 0e8585665a Load the 500 weight and draw missing glyphs as SVG icons (#214)
game-maker / gate (push) Successful in 2m47s
game-maker / suite (push) Successful in 8m41s
The theme asks for Source Sans 3 at weight 500, but fonts.ts only
loaded 400, 600 and 700. The browser synthesised the 500 text. The
issue comment also asked for the symbols the font lacks to become SVG
icons. A scan of src/ against the font's cmap found 20 such characters
in 30 places.

- fonts.ts loads source-sans-3-latin-500-normal.woff2 as well.
- Menus draw their ticks and the "current" dot as icons. The dot is a
  new MenuItem.current field and no longer part of the label text.
- Inspector action buttons (reset, apply, fit width/height) are
  icons. The menu arrow behind them is the drawn Dropdown chevron.
- Kind marks come from a table in markIcons.ts. Picture kinds use an
  icon, # and Aa stay text because the font has them.
- Scene marks in the asset panel, sound play/stop and the transition
  arrows are icons too.
- Apply is an icon although the font has the arrow. It sits next to
  the reset icon and would look different as a glyph.
- A new Icon widget draws on redraw like Button, so Node tests need no
  DOMParser.
- Tests find icons through the SVG id. The new hook
  __gm.locateIcons(id) and the harness helpers findIcon/clickIcon
  replace locate() on the old characters.

Findings:
- Weight 700 is loaded but the theme never uses it. Left as is.
- The old scene-mark check in module 35 accepted any play glyph on
  screen. It now checks the mark on the start scene's row.
- Module 44 needs the textures from module 37 and fails without it.

Tests:
- ./check.sh: 145 files, 2187 tests, exit 0 (before 144 / 2143).
- New tests/ui/fonts.test.ts: every theme weight is loaded, and every
  character in src/ exists in every shipped face. The cmap reader
  matches fontTools (231 characters).
- Icon tests cover the 19 new files. Each kind mark is either an
  icon or text, never both.
- e2e 1-16,22,30,34,35,43,44: 586 green, 0 FAIL, then 44 stopped
  for lack of module 37. e2e 1-16,37,44,47,48: 618 green.
  e2e 1-14 on the final build: 454 green.
- Two new e2e checks: loaded weights in 14, a reset icon pixel in 04.
- Counter-probes all fail as expected: 500 removed, a play glyph put
  back, the struct icon removed (one unit test each). In the browser,
  500 removed and reset icon hidden fail exactly the two new checks.
- Editor-suite check sum 1312 (1310 measured by CI at 056a622, +2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 08:13:22 +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 6e3e8d23c5 Combining vector shapes keeps the curves it did not cut (#208)
game-maker / gate (push) Successful in 2m44s
game-maker / suite (push) Successful in 8m42s
Until now every boolean operation on vector art threw the curves away.
The shapes were flattened into line segments, the operation ran on those
segments, and the result came back as a polygon. A circle that nothing
even touched came out of an Exclude with 40 corners instead of its four
points and handles. Now the result carries the original curves again,
and only the edges the operation really drew stay straight.

- While flattening, every sample point remembers which curve it came
  from and where on that curve it sits. The operation runs on the same
  segments as before, so nothing about the boolean result changes.
- When a run of result points walks along one of those curves, that
  piece is cut out of the original curve again with de Casteljau. This
  is an exact cut, not a fit through the points.
- An edge whose two ends sit on the same curve but whose middle does
  not follow it stays straight. Without that rule a cut-off cap would
  bulge back into the shape.
- A closed contour keeps at least three points, so a lens shape does
  not collapse into two.
- The rule lives in its own module, src/core/model/vectorCurves.ts.
  vectorBoolean.ts only hands it the table of sampled points.

Findings:

- The two intersection points themselves sit up to 0.09 canvas pixels
  off the original curve. Clipper cuts the flattened segments, not the
  curves, so this was the case before as well and stays inside the
  0.2 px sampling tolerance.
- Refining those points with Newton steps on the curve was measured and
  gave nothing back (0.02952 px instead of 0.02978 px), because the
  error is in the intersection itself and not in the estimate. The code
  was removed again.
- Theme.ts rowHeight 22 -> 24 is in this commit. The change is not mine,
  it was already in the working tree; gate and module 47 are green with
  it.

Tests:

- ./check.sh: 143 files, 2133 tests, exit 0 (was 2127)
- e2e module 47 on a fresh seed: all passed, 59 checks (was 57)
- 6 new unit tests: untouched circle, untouched side plus a straight cut
  edge, a cut in the middle of a curve, a chord crossing one curve
  twice, two overlapping circles, the three point minimum
- 2 new e2e checks in block 47p: point count and handles from the model,
  plus a pixel pair on the round edge
- Four counter-proofs, all bite: no reconstruction -> 5 unit tests and
  the e2e check fail with 35 points and nothing curved; no chord test ->
  the cap bulges back to 28.28; no three point rule -> the lens test
  fails; handles halved -> the pixel check reads background

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 05:31:05 +02:00
adminandClaude Opus 5 bbbf34109b Lists duplicate, rename and edit values right in their rows
game-maker / gate (push) Successful in 2m20s
game-maker / suite (push) Successful in 8m25s
Name, opacity and Z each sat in a separate field below or beside
the list. You had to select a row first and then look elsewhere to
change it. Duplicating was a separate button or not possible at all.
Now the row itself carries everything.

- SortableList gets three optional callbacks. onDuplicate shows a
  copy button between the eye and the delete button. onRename opens
  a text field on double click, Enter or a click outside commits,
  Esc cancels, an empty name is ignored. value puts a NumberField
  into the row.
- Dragging on the number changes the value and never moves the row.
  A whole drag is one undo step, the canvas follows live.
- Copy and delete only show on the hovered and the selected row.
  Their space stays reserved, so nothing jumps on hover.
- Pixel layers: copy right above with the same pixels on all
  frames, rename, opacity in percent. The Name and Opacity fields
  below the list are gone.
- Pixel frames: copy right after, replaces the Duplicate button.
- Tilemap layers: copy with cells, Z and visibility, rename, Z in
  the row. The Name and Z fields in the panel are gone.
- Vector shapes: copy at the same place and select it, rename.
- New in core: DuplicateVectorShapeCommand,
  DuplicateTilemapLayerCommand, withLayerDuplicated and copyName
  ("X copy", "X copy 2"). copyName lives in src/core/util/names.ts
  because the model never imports from commands/.

Findings:

- Double clicking a shape that is not selected yet only selects it.
  The first click moves the list up, so the second click misses the
  name. Module 47 selects the row first.

Tests:

- ./check.sh: 138 files, 2055 tests, CHECK EXIT 0 (before 2052).
- GM_STEPS=47,48 (fresh, twice): ALL PASSED, 91 checks (before 86).
- GM_STEPS=39,40,47,48: module 40 green, 47 died on the rename
  check, which was a test fault and is fixed.
- Counter probes: a constant copyName fails the shape and pixel
  layer tests. A dead copy hit fails the copy check in 47.
- Check total 1272 in README and wiki, computed (1266 + 6).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 06:02:47 +02:00
adminandClaude Opus 5 ae0247d86f Lists add and delete items right in the list header and rows
The layer, frame and shape lists still had their add and delete
buttons scattered below the list, each in its own layout. They now
live in the shared SortableList, so every list works the same way.

- SortableList gets an optional header with the title on the left
  and a + on the right, plus an optional x per row next to the eye.
- The keepOne option hides the x on the last remaining row, so a
  pixel art image and a tilemap always keep one layer.
- Pixel art layers and frames and tilemap layers use header, + and
  x. Only Merge down and Duplicate stay below the lists.
- Vector art shapes get the x but no +. Add shape stays below the
  list and Delete shapes stays for a multi-selection.
- The section headings in the pixel art column use the same style
  as the list titles and the inspector.

Findings:

- The wait loop added to check 47i for #196 only waited for the
  right half of the shape. If the texture landed between the two
  reads, the left half was still blue and the check failed. It now
  waits for both halves.

Tests:

- ./check.sh: 138 files, 2052 tests, exit 0.
- GM_STEPS=39,40,47,48 (fresh): ALL PASSED, 156 checks (was 154).
- GM_STEPS=47,48 (fresh, final): ALL PASSED, 86 checks.
- Two new e2e checks: x on a shape (47) and x on a frame (48). The
  tilemap check in 40 now uses the x instead of Delete layer.
- Counter-check: with the x not reporting to its owner, exactly the
  two new checks fail.
- Check total 1266 in README and wiki is computed (1264 + 2).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 21:18:26 +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 1c8a20fcac Texture fill e2e checks pick the right image in a full run (#186)
game-maker / gate (push) Successful in 2m19s
game-maker / suite (push) Successful in 8m5s
CI run 217 for 592ea94 failed three checks in block 47i. In the full
run the texture list is longer than in a single module run, so the
picker click missed "halves" and the texture stayed empty.

- The check now types "halves" into the menu search field and picks
  it with Enter, independent of the list length.
- FIND_HALVES looks the image up by name. Matching only by size had
  picked the 32 px image "wedge" in the full run.
- The texture fill check forces a redraw before reading pixels. After
  the work of earlier modules the vector tab does not redraw once the
  texture has loaded. That bug is tracked in #196, and the forced
  redraw goes away with it.

Tests:
- ./check.sh: CHECK EXIT 0
- npm run build: BUILD EXIT 0
- GM_STEPS=47 (fresh seed): ALL PASSED, 54 checks
- Full run not repeated locally. CI decides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 14:59:06 +02:00
adminandClaude Opus 5 592ea942d9 Vector shapes can fill with gradients and textures (#186)
game-maker / gate (push) Successful in 2m14s
game-maker / suite (push) Failing after 7m14s
A vector shape could only be filled and outlined with one flat colour.
Shading, glows and patterned surfaces had to be faked with stacks of
shapes. Every shape now has a paint type for its fill and its outline.

- Fill type: solid colour, linear gradient, radial gradient or
  texture. Line type offers the same without texture.
- Colour stops are edited on a bar in the inspector, as agreed in the
  issue. A click on the bar adds a stop in the colour the gradient
  already has there, so adding never changes the picture. At least two
  stops remain.
- The new Paint tool in the tab shows two handles for the selected
  shape: axis start and end, centre and radius, or the corners of one
  texture tile. "Handles: Fill / Outline" picks which paint is edited.
- Moving a whole shape moves its gradients and tile with it. Every
  gesture is one undo step.
- A texture fill always tiles, because Pixi turns a non-repeating
  pattern into a repeating one anyway. When switched on, one tile
  covers the whole shape.
- Only uploaded textures and frames can be a fill, never a drawing.
  A drawing filled with itself would rasterise itself mid-raster.
- The fingerprint of a drawing includes the texture and frame a fill
  points to, so the picture redraws when the image is swapped.

Findings:

- graphics.destroy(true) also destroyed the textures of the fills.
  With a texture fill it deleted the tile slice and the next redraw
  crashed on reading 'scaleMode' of null. It is now destroy().
- The gradient cache destroyed old gradients itself, which gave 20
  Pixi warnings "destroyed while still bound". They are now handed to
  Pixi's texture GC with autoGarbageCollect switched on. The console
  of module 47 is empty again, the last CI log had 0 of them.

Tests:

- ./check.sh: 135 files, 2011 tests, exit 0 (before 134 / 1986)
- npm run build: exit 0
- GM_STEPS=47, freshly seeded: ALL PASSED, 54 checks (before 44),
  0 console warnings
- 25 new unit tests, 10 new e2e checks in block 47i
- Four counter-checks bite, originals restored and verified with cmp
- Check sum 1232 in README and wiki, calculated (1222 + 10)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 13:05:02 +02:00
adminandClaude Opus 5 1116818011 Vector shapes can be combined by union, subtract and more (#187)
game-maker / gate (push) Successful in 2m13s
game-maker / suite (push) Successful in 7m51s
Building a shape out of simpler ones meant drawing the result point by
point. The Vector Art tab now selects several shapes at once and
combines them with a boolean operation into one ordinary shape.

- Ctrl/Cmd+click in the tab and Ctrl or Shift+click in the shape list
  add a shape to the selection. Shift stays pan in the canvas.
- A COMBINE block in the inspector offers Union, Subtract, Intersect
  and Exclude. Subtract removes every upper shape from the lowest one.
- The result replaces the lowest selected shape and keeps its name,
  colours, stroke and place in the order. It is one undo step.
- Curves are flattened to 0.2 canvas pixels, a circle gets about 48
  points. Open lines and single shapes cannot be combined, the buttons
  are off and a hint gives the reason. An empty result is rejected.
- Delete now removes every selected shape, and the selection drops
  shapes that no longer exist.
- The geometry comes from clipper2-ts 2.0.1, pinned. It has no
  dependencies and speaks even-odd directly. Only the engine module
  is imported, the bundle grows by 55 kB instead of 110 kB.

Findings:

- clipper2-js was tried and dropped. It pulls in Angular 15 with a
  high npm audit finding and no fix.
- Pixi finds holes by containment, not by even-odd depth. An island
  inside a hole would be drawn as a hole again. Result rings are
  emitted outer ring before its islands, and a test mirrors Pixi's
  rule.
- A depth sort turned out to be dead code in a counter-check and was
  removed. A pixel check in 47h stayed green without the subtract
  and now checks both colours.

Tests:

- ./check.sh: 134 files, 1986 tests, exit 0 (before 133 / 1968)
- npm run build: exit 0
- GM_STEPS=47, freshly seeded: ALL PASSED, 44 checks (before 39)
- 18 new unit tests, 5 new e2e checks in block 47h
- Five counter-checks bite, originals restored and verified with cmp
- Check sum 1222 in README and wiki, calculated (1217 + 5)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 11:56:41 +02:00
adminandClaude Opus 5 3ff7e91dbf Vector art points have four types and the pen draws whole curves (#195)
game-maker / gate (push) Successful in 2m10s
game-maker / suite (push) Successful in 7m29s
The pen only pulled out the outgoing handle of a new point, because
the opposite handle was mirrored only if it already existed. The
first segment of every dragged point was therefore half a curve.
Beyond that bug, a point had no way to say how its handles relate.

- A vector point has an optional mode: straight, corner, smooth or
  symmetric. Missing means corner. The mode is not part of the
  fingerprint, because it changes no pixel, only the next drag.
- Dragging a handle follows the mode. Corner leaves the other handle
  alone, smooth turns it along and keeps its length, symmetric
  mirrors it exactly.
- The pen pulls out both handles mirrored and marks the point
  symmetric. A plain click still sets a point without handles.
- The tab header has a "Point: ..." menu for the last clicked or set
  point. That point is drawn filled. Changing its type is one undo
  step. Text instead of four icons, because the UI kit has no
  tooltips.
- Switching a point without handles to a curved type first grows
  handles along its neighbours. Smooth and symmetric line the
  handles up and keep the rough direction of the curve.
- A point inserted on a curve is smooth. Symmetric neighbours become
  smooth, because the split shortens one of their handles.
- Alt-click still toggles between straight and symmetric.

Found on the way: the old Alt rounding took the point at the other
end of an open line as a neighbour, so its handles came out skewed.
Fixed and held by a unit test.

Existing points with handles now count as corner. Before, every drag
mirrored. No pixel changes, only the next drag behaves differently.

Tests:
- ./check.sh: 129 files, 1917 tests, exit 0 (before 1903)
- GM_STEPS=47, freshly seeded: ALL PASSED, 39 checks (before 32)
- Check sum 1195 in README and wiki is computed (1188 + 7), CI
  measures it
- Counter-checks: pen without 'symmetric' fails 3 checks, no smooth
  branch fails 1 unit test and 1 e2e check

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 06:51:35 +02:00
adminandClaude Opus 5 90627ccf47 Snap is a magnet button in every graph and vector art tab (#194)
game-maker / gate (push) Successful in 2m13s
game-maker / suite (push) Successful in 7m33s
The snap toggle was a checkbox with the word "Snap" next to Fit, and
the vector art tab had its own "Grid" button for the same idea. Both
now share one square icon button, so snapping looks and works the
same wherever it exists.

- SnapToggle is a square toggle button with a magnet icon and no
  text. It lights up blue when snapping is on, like the other toggle
  buttons.
- The magnet is drawn with cubic curves instead of arcs, because the
  Pixi SVG parser draws arcs as jagged edges.
- The vector art tab drops its Grid button and uses the same
  SnapToggle. It still snaps to 8 px, the graph tabs to 24 px.
- SnapToggle accepts anything with setSnapping(on). The vector art
  canvas method is renamed from setSnap to setSnapping for that.
- Every tab exposes the button as __gm.<tab>.snapButton(). Modules
  06, 26 and 36 click through that hook instead of the label "Snap"
  plus 30 px.

Tests:
- ./check.sh: 129 files, 1903 tests (was 128 / 1897)
- 3 new tests in tests/editor/snap-toggle.test.ts, 3 in the icon tests
- GM_STEPS=1-6,26,36,47 fresh: ALL PASSED, 346 checks
- GM_STEPS=47: ALL PASSED, 32 checks (was 29)
- Counter-checks: toggle not switching its target fails 2 unit
  tests, the canvas ignoring the switch fails module 47
- Check total 1188 in README and wiki is computed (1184 + 4), the
  full run is left to CI

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 06:25:56 +02:00
adminandClaude Opus 5 ad62a0f525 A vector art asset can be made and used without touching the model (#129)
game-maker / gate (push) Successful in 2m9s
game-maker / suite (push) Successful in 7m24s
Stage 5 closes the loop that the earlier stages left open: every check so
far put the shapes into the project with mutate, because the tab that
draws them came later. Block 47f now walks the path a user walks - the +
menu, the asset panel, the tab, the picture picker - so a regression in
any of those four doors is caught by the suite instead of by you.

- The + menu offers "Vector art" among the types that only live in the
  library, and the entry makes an asset that is selected and stands in
  the panel under its own heading.
- A double-click on the row opens the Vector art tab on that asset,
  which is what stage 3 built and no check measured until now.
- The Texture row of a sprite offers the drawing by name, sorted among
  the other pictures, and picking it there makes the sprite wear it -
  measured at the pixel in the scene, not at the row.

One finding: the picker listed three entries where I expected more, and
the reason is sound - a fresh fixture holds one texture and the drawing,
"None" being the third. The check names all three instead of counting,
so a lost entry says which one it lost.

Tests:
  ./check.sh                       128 files, 1897 tests, exit 0
  node tools/e2e.mjs (all modules) ALL PASSED - 1184 checks (was 1179)
  GM_STEPS=47 (fresh seed)         ALL PASSED - 29 checks (was 24)
  Counter-checks: both hold - no generator registered fails the picker
  check, and opening the tab without the asset fails the double-click
  check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 05:42:07 +02:00
adminandClaude Opus 5 78595ec80f A selected vector shape has its own inspector rows (#129)
game-maker / gate (push) Successful in 2m12s
game-maker / suite (push) Successful in 7m45s
Stage 4 of #129. Until now the inspector showed the rows of the asset
when a shape was selected, so there was no way to give a shape a colour,
an outline or a place in the drawing order without editing the project
file. The rows for a shape now sit where every other selection puts its
rows.

- SHAPES lists every shape of the asset, topmost row drawn last, the
  same way a layer list reads. Selecting the asset itself shows the same
  list below its own rows, with no shape selected.
- Each row paints its shape with the commands the rasterizer uses
  (shapeDrawing plus paintDrawing) into a 22 px swatch, the whole canvas
  scaled in, so the row also says where on the canvas the shape sits.
- The four order buttons from the issue (forward, to front, backward, to
  back) sit in two rows of two, and a row can be dragged to another
  place with a marker line. They are disabled when there is nothing to
  move.
- VECTOR SHAPE carries name, fill, opacity and an outline checkbox, the
  checkbox adding and removing the stroke instead of hiding it; line
  colour and width appear with it. A dragged colour slider is one undo
  step, the style rows going through previewingHandlers like every other
  row.
- A line width of 0 is no stroke, which the command rejects, so the row
  clamps at 0.1.

Findings:

- locate('Outline') aims beside the control, because it gives the centre
  of the label text and a wider label pushes a fixed offset out of the
  field. __gm.inspectorField(label) knows the centre of the control.
- A click into a text field sets the cursor and selects nothing, so a
  typed hex landed appended to the old one and parseHex returned null
  without anything failing - typeText only checks that the text is
  contained. A Ctrl+A goes first now.
- Four buttons side by side cut "Backward" to "Backw...", which only the
  screenshot showed. Two rows of two now, as in the tilemap panel.

Tests: ./check.sh 128 files, 1897 tests (was 127/1883). Full e2e suite
locally instead of only CI because the change sits in the inspector and
touches every selection: ALL PASSED - 1179 checks (was 1172), module 47
at 24 checks (was 17). Three counter-checks hold: not reversing the list
fails 1 unit test and 2 e2e checks, dropping the gap correction when
dropping a row fails 2 unit tests and the drag check, and a constant
signature kills module 47 because the outline rows never appear.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 22:29:40 +02:00
admin 3d7ad65f34 Vector art has a tab to draw in, which is stage 3 of #129
game-maker / gate (push) Canceled after 37s
game-maker / suite (push) Canceled after 37s
Stages 1 and 2 gave the asset a model and a picture, but the only way
to put a shape into it was a test calling mutate.  The tab closes that
gap: what you draw there is what the sprite in the scene carries, with
no extra step, because the fingerprint changes and the picture registry
of #185 redraws.

- The tab has a header with Select, Pen, Grid and Fit, a chequerboard
  behind the canvas and the canvas itself as a frame.  It paints with
  the same commands as the rasterizer, paintDrawing being exported
  from viewport/vectorArtPicture.ts for it, so the tab cannot drift
  away from the picture.
- Pen sets points on a click and pulls the handles out on a drag, both
  of them mirrored; a click on the first point closes the ring and Esc
  ends an open line.
- Select drags a point, a handle or a whole shape, a click on an edge
  puts a point there, a double click takes one away, and Alt turns a
  corner round and back.  Delete removes the selected shape.
- The rules grew inside polygonEdit.ts instead of a second module, as
  the plan says, so a collider polygon and a frame outline get the
  same behaviour where it applies.
- A point on a curved edge splits the curve by de Casteljau instead of
  cutting it: both halves together draw exactly what was there before.
- A curved edge is walked instead of measured over its chord, or a
  click on the bow of a curve would grab nothing; the same walk carries
  the test for the inside of a ring.
- One gesture is one undo step.  The preview writes the model directly
  and the old state is put back on release before the command runs,
  which is the pattern the frame outlines of #141 already use.
- state.ts gained a selection kind "shape".  The inspector shows the
  rows of the asset for it, the shape list with colour, opacity and
  stroke being stage 4.

One counter-check was no counter-check: disabling the pen's closing
tolerance by multiplying it with 0 left the run green, and rightly so,
because the closing click lands on exactly the screen coordinate point
0 came from, so the distance is 0 and <= 0 is true.  Only an
unsatisfiable condition really switched it off, and then 2 checks fail.
A second trap on the way: a false && in front of the guard breaks the
TS narrowing, npm run build bails out, and two runs went against the
old bundle.

Tests: check.sh 127 files, 1878 tests, EXIT 0 (was 126 / 1860).
Full e2e suite ALL PASSED - 1172 checks, module 47 alone 17 (was 10).
18 new unit tests in tests/editor/ring-edit.test.ts and 7 new e2e
checks in blocks 47c and 47d.  Two counter-checks hold: no de
Casteljau fails 1 unit test, measuring a curve over its chord fails
another.  Originals checked back with cmp and rebuilt.
2026-09-20 21:34:14 +02:00
adminandClaude Opus 5 6371576d37 A vector art asset is a picture now, which is stage 2 of #129 and the first stage that shows something in the browser: the asset sits in the picture picker, a sprite carries it in its Texture row and what the sprite shows has been rasterized from the shapes, the generator of #185 being registered for the type vectorart - installVectorArtPictures(app.renderer) hooks the source (core) and the generator (viewport) in once per page from App.ts, by a call and not by a module side effect, so a test may install it or leave it out; the rule lives in core and the painting in viewport - the new core/model/vectorPath.ts turns a shape into plain drawing commands (move/line/curve/close), closed rings being filled while all rings are stroked, a segment staying a line as long as neither of its ends carries a handle - a single handle is enough for a curve, the missing control point sitting on its own end, so a corner may stand next to a round side - and the way back to the first point is only drawn when it is curved, close doing it otherwise; holes go through one door only, Pixi recognising them from a GraphicsPath with checkForHoles, which tests containment rather than even-odd depth, which is enough for a ring inside a ring while without the flag the inner ring simply paints over the outer one (measured, see the counter-checks); the oversampling is the 2x from the plan - RenderTexture.create({resolution: 2}) puts 512 px on the GPU for a 256 canvas while texture.width stays the canvas size, which is the measure the sprite hangs on, and the render texture is the clip itself, so what juts out over the canvas is not in it and needs no mask; drawn pictures get the same sampler state as uploaded ones, setPictureFactory wrapping the factory for it, mirroring keep(); two measuring traps cost me runs - __gm.objectRect(id) gives the centre of the object and not its corner, so the first pass read four pixels next to the sprite and reported background while the rasterizer was correct, and the grid lines of the scene cross exactly in the centre point, so the hole check read #363940 and failed for the right reason, measuring beside it now; renderer.render() runs inside the picture request, which is safe here because every caller asks during its own widget redraw and the frame is drawn on the ticker afterwards, a nested render inside a running render pass would not be - that warning stands in the module head and in the notes; new module 47 with 8 own checks in its own lane (the picture on the GPU at double resolution measuring the canvas, the sprite showing the fill, the inner ring being a hole with the scene shining through, the later shape on top, the stroke running along the ring, a changed drawing getting a new key with the old one freed, and the sprite showing the new colour), the shapes being put into the model by mutate because the tab that draws them is stage 3, and 21 new unit tests in tests/core/vector-path.test.ts; three counter-checks hold: oversampling back to 1 fails the GPU check, checkForHoles off fails the hole check, and not drawing the curved way back fails a unit test; the full suite ran locally instead of only in CI because the change sits in TextureLibrary and touches every picture: ALL PASSED - 1165 checks, measured and carried into README and the wiki (#129)
game-maker / gate (push) Successful in 2m9s
game-maker / suite (push) Successful in 7m26s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:51:54 +02:00