Commit Graph
18 Commits
Author SHA1 Message Date
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 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 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
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
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 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 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