The performance panel showed Physics and Editor as the large parts of
a frame in a scene of 222 entities. Nearly all of it came from a
tilemap that only stands there.
- Whoever writes cells in place now says so with cellsChanged. Only
then does cellsVersion compare the content. The comparison ran about
60 times per frame and was the largest single item at 14 percent.
- The four writers call it: the painter during and at the end of a
stroke, PaintTilesCommand on execute and on undo. A new writer that
forgets it stays invisible.
- The game view is drawn once per frame in play. With an animated tile
in view it was drawn twice, once by the run and once by animate().
animate() now gets the redraw count from the start of the tick.
- TilemapField builds the geometry of a mesh only when its source
changes: the cells, the tile size, the origin or the image. For that
groupByTile and layerRows return the same objects for the same list.
- PhysicsSystem skips a tilemap whose source and switch are unchanged.
The switch is kept because a map that starts switched off would
otherwise have dead colliders once it is switched on.
- World.syncToModel looks entities and assets up in a map per pass
instead of a linear search per object.
Measured on stein2 with a copy of the reference project, ms per frame:
Physics 2.64 to 0.97, Drawing 6.89 to 2.56, Editor 1.95 to 1.31.
Left open: syncToModel still mirrors every entity every frame. That
is its own issue.
Tests:
- Gate: 198 files, 2887 tests (2883 + 4), exit 0
- e2e 1-17,28,33,39-43,59: 810 checks passed, 40 has +3
- Smokes: 24 / 19 / 21, exit 0
- Three unit tests wrote cells directly and now call cellsChanged
- Eleven counter-probes fail. Two did not at first: the stroke needed
a check of its own, the physics test has to start with the map
switched off. That last one was repeated on stein2 only.
- e2e total 1613 is computed, not measured in a full run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deleting a tile left the cells painted with it in the tilemaps. They
drew nothing, blocked nothing and did not show in the delete dialog,
but still read as a tile for "Tile at" and stopped a fill.
- DeleteAssetCommand empties the cells of a deleted tile on every
layer of every tilemap in the same step. Undo puts the tile and its
cells back together.
- The delete dialog counts them, for example "3 cell(s) on 1
tilemap(s) are painted with it and become empty." (cellsPainted).
- presentTile treats a cell naming a missing tile as empty. "Tile at"
(World.tileAt) and floodFill use it, so cells already in existing
files read and fill as empty. There is no migration.
- The brush, the rectangle and the fill check the tile in hand. A
tile deleted after it was picked no longer paints new invisible
cells, the editor says "Choose a tile to paint with." instead.
- Unchanged on purpose: the picker still reports such a cell as
naming a tile that no longer exists, and erasing removes it.
Tests:
- ./check.sh: 195 files, 2823 tests, CHECK EXIT 0 (+6).
- GM_STEPS=39,40: ALL PASSED, 100 checks (4 new in 40t).
- Full e2e sum is now 1590 (1586 confirmed by CI on round 8, plus 4).
- Counter-checks, all fail as expected: 5 in unit tests (delete
without emptying, undo without cells, fill and "Tile at" without
presentTile, wrong map count) and 3 in the browser (dialog without
count, delete without emptying, brush without the check).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The suite job failed on every push since 6015869. Each module passed on
its own, but the full run showed two kinds of fault.
- e2e 40 still expected the object menu and the empty-cell menu from
before #240 and #241. It now expects Copy, Paste, Group... and Remove
from group, and Paste alone on an empty cell.
- e2e 46g read the outline of the first collider in the scene. In the
full run that is a leftover of module 43, off screen, so it read as
hidden before any switch. The check now places a collider of its own
next to its tilemap and removes both at the end.
- e2e 51 copied the seed's red and blue crate. In the full run module 09
has packed both into a container. The module now places two crates
with colliders of its own and removes them at the end.
- e2e 52 compared whole row lists against the seed scene. In the full
run that scene holds about 50 more rows. The module now works in a
scene of its own, which it removes at the end. It had never run in CI
because 51 died before it.
The check in e2e 41 that failed once (task 347, a dragged body 15 px
off in its first sample) is left alone. It failed in 1 of the last 60
logs, while a local e2e run loaded the same host.
Tests:
- full e2e run in the game-maker-ci image with GPU, as the runner does:
ALL PASSED, 1452 checks, E2E EXIT 0 (the README sum is now measured)
- GM_STEPS=1,46,51,52 on the host: ALL PASSED, 51 checks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A texture was fetched only when it was drawn for the first time. Until
the file arrived, the tilemap skipped the cell for that frame. An
animated tile whose next step sat in another picture therefore blinked
away once on its first cycle, and sprite animations did the same.
- FileLibrary gets preload(files). It requests every file that is not
held, loading or known to have failed. Each library reports what it
holds through holds().
- src/core/model/projectFiles.ts lists the files a project names per
library: uploaded textures, fonts and pixel art cels.
- App.ts runs the preload on every project event. That covers opening
a project, new assets and edits from a collaborator.
- A new test hook __gm.heldPictures() reports which textures of the
project are in memory.
Not done on purpose:
- The tilemap keeps no fallback frame while a step is still loading.
- Sounds still load on first play.
Not checked: the flicker itself was not reproduced in the browser, the
e2e check measures "held in memory". Modules 42, 44 and 47 read
textures from the project and were not run locally.
Tests:
- ./check.sh: 174 files, 2518 tests, CHECK EXIT 0
- e2e 39 + 40: ALL PASSED, 96 checks
- 3 new unit tests for preload and the project file list
- New e2e check 40k-ii: an unplaced picture is held after a reload
- Counter-probe: without the preload 40k-ii fails with
"Autotiles: held false, unworn: held false"
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tilemap layer was one piece in the draw order, so a figure stood
either in front of a whole layer of trees or behind all of it. For a
top-down game the trees in front of the figure's feet have to cover
it and the ones behind must not.
- A new "Sort by Y" box under the layer list sets `sortByY` on the
painted layer. Every change is its own undo step.
- Such a layer comes as one draw slot per row. On its `z`, the rows
and all objects on that `z` are sorted by where they stand: the
bottom of an object's box, the bottom of a row. Ties keep the
hierarchy order, and other `z` values are untouched.
- A layer drawn as one piece and the map's own node stay behind the
sorted ones on the same `z`, since neither has a single place to
stand. Ground belongs on a lower `z` or stays unsorted.
- A new tile property "Sort offset" (whole rows, -16 to 16) lets a
crown sort with the trunk below it.
- `TilemapField` draws a container per row for such a layer, and
picking takes the row into account, so a click picks what is drawn
in front. Scene view, game view and play share the same rule.
Limits: on a rotated map "down" is world Y. A layer sorted by Y costs
about one draw call per row and tile instead of one per tile.
Tests:
- ./check.sh: 160 files, 2350 tests, CHECK EXIT 0 (before 2340).
- 10 new unit tests in tests/viewport/sort-by-y.test.ts.
- e2e 39-43: ALL PASSED, 148 checks. New check 40p-iii ticks the box
and finds the slot of row 0, undo brings back the whole layer.
- Counter checks: without the Y sort 5 unit tests fail, with the
offset ignored 5, with no row in picking 1. If TilemapField draws
no rows, exactly 40p-iii fails in e2e.
- The e2e sum in the README is 1378, counted, not yet seen in CI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
A behavior could move sprites around but had no way to look at or
change the tilemap it was standing on. Doors, breakable blocks and
"is there ground ahead?" checks all need that.
- New nodes Tile at, Set tile, World to cell and Cell to world. The
conversion honours position, rotation, scale and mirroring of the
placed map. A small Tile literal node lets a graph compare what it
read against a fixed tile.
- The layer is picked by name from all tilemap layers in the project
through a new field kind. "First layer" takes the map's first one.
Because layer is an input, a text can also be wired in.
- Each placement keeps only its edited cells in liveTilemap.ts,
layered over the asset's cells. A second placement of the same map
stays untouched, and the asset itself is never written.
- A read sees a Set tile from earlier in the same flow. Drawing picks
up the edit at once and neighbouring autotile edges adapt.
- Solids see the edits straight away, so a Move in the same frame
already hits the new cell. Rapier rebuilds its shapes on the next
step.
- After Stop the map is back to what was painted. The editor run shows
the edit in the scene view while it lasts and does not touch the
project.
- An unwired tilemap input means no map, like Get position. Cells
outside the grid are refused and done reports false.
Tests:
- ./check.sh: 155 files, 2271 tests, CHECK EXIT 0 (before 2245).
- tests/core/tile-access.test.ts (14), one physics test for a floor
set at runtime, catalog entries for the five nodes.
- e2e 39,40: all passed, 86 checks (before 81), five new in 40s.
- Counter-checks: collision ignoring edits fails 3 unit tests, the
solids cache missing same-tick edits fails only the "at once" test,
drawing ignoring edits fails the 2 drawing checks, the editor run
not reporting a new tile fails only its check.
- 40s uses a still tile. The animated fixture tile redraws every frame
and hid the last counter-check at first.
- Check total 1351 + 5 = 1356, computed. CI confirmed 1351 at cb32b4b.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Add collider" and "Add rigidbody" could only create a fresh asset.
Reusing one meant creating a copy by hand, and a new box collider
never matched the sprite it was added to.
- "Add collider…" and "Add rigidbody…" open a second menu at the same
spot: a search field, "Create new …", a separator, then every asset
of that kind sorted by name. Enter takes the first match.
- An existing asset is placed as a shared child, not copied. An asset
the object already carries is greyed out.
- The rule for when the entries are available lives in one place in
the collider commands. An object without a resolvable asset is
greyed out again, as with the old canAddCollider.
- "Fit to sprite" in the menu of a placed collider sizes it to its
sprite and centres it (position 0,0, z kept) as one undo step.
- Fit writes overrides of that placement, not the asset, because a
collider can now sit on several sprites. A value equal to the asset
value clears the override instead.
- Fit removes the collider's own scale, swaps width and height near
90 degrees and gives a circle the radius that fits (min/2). It is
greyed out for polygon and frame outlines and off a sprite.
- Menu has no submenus, so a second menu opens instead, similar to
"Add Component" in Unity. That costs one click more than before.
Tests:
- ./check.sh: 152 files, 2234 tests, CHECK EXIT 0 (before 151/2228).
- 6 new unit tests in tests/core/collider-parts.test.ts.
- 3 new e2e checks in 08a: submenu with search and greying, an
existing collider via search and Enter, Fit on oc2.
- 09, 40 and 41 follow the new labels.
- e2e 1-16,39-41 on the final build: ALL PASSED, 596 checks.
- Counter-checks: no greying, existing asset ignored and fit without
centring each fail at their own check. Breaking the override rule
fails 2 unit tests.
- CI confirmed 1345 checks at 2cf7202 (task 265). New total 1348,
calculated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rectangle drawn over cells that already hold exactly that tile
pushed a command that changed nothing. Undo then seemed to do nothing
and the project was marked as modified. PaintTilesCommand.changes()
was meant to prevent that, but nothing called it since #128.
- New paintTiles() in tilemapCommands.ts returns the command for a
finished stroke or fill, or null when no cell changes. It is the
first caller of PaintTilesCommand.changes().
- All three paint paths in the Scene View go through it: brush and
eraser, rectangle (both in endStroke) and flood fill (fillFrom).
- Flood fill was not affected, because floodFill already returns []
for the same tile. The guard is there anyway, as the issue asked.
- A rectangle that changes even one cell stays a normal undo step.
Tests:
- ./check.sh: 145 files, 2190 tests, CHECK EXIT 0 (before 2187).
- 3 new unit tests in tests/core/tilemap.test.ts: same rectangle
twice gives no command, a wider rectangle still does, a fill that
finds nothing gives no command.
- 1 new e2e check in 40d: the same rectangle again, then one undo
must take 9 cells to 0. A redo restores them for later blocks.
- GM_STEPS=39,40 on the final build: ALL PASSED, 81 checks.
- Counter-probe with changes(...) || true: exactly the two unit
tests fail, and in the browser exactly the new check fails with
{"afterUndo":9}. Original restored via cmp (IDENTICAL), rebuilt.
- Editor suite check total: CI measured 1312 at 0e85856, new total
1313 (calculated) in README and wiki.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The object menu lived only on the hierarchy rows. Now a right-click in
the scene view opens the same menu for the object under the cursor.
- The menu moves out of HierarchyPanel into src/editor/objectMenu.ts.
Hierarchy and scene view call the same function, so their entries
cannot drift apart. Only the hierarchy adds "Expand all" and
"Collapse all".
- Which object the menu is for follows the press rule from #203. A
selected object under the cursor, even a hidden one, keeps the
selection and the menu acts on all of it. Otherwise the topmost
object is selected first.
- The rule is the pure function menuTargetOf in src/viewport/pick.ts,
next to pressOn.
- Empty space, including an empty tilemap cell, opens no menu.
- The container tab gets the same, because ContainerCanvas extends the
scene view.
Tests:
- ./check.sh: 142 files, 2094 tests, exit 0 (was 2090)
- e2e 39,40 fresh: ALL PASSED, 80 checks
- e2e 1-16,39,40 fresh: ALL PASSED, 565 checks
- New block 40r (3 checks): menu on a hidden selected crate, topmost
object without a selection, no menu on an empty cell.
- Counter-check: a right-click that always takes the topmost object
fails 2 unit tests and the hidden-crate e2e check.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pixi's own hit test decided what a click in the scene view selected.
A tilemap caught clicks over its whole area, even on empty cells and
over sprites sorted above one of its layers. A press always grabbed
the topmost object, so a hidden selected object could not be dragged,
and there was no way to reach an object underneath another one.
- New pure rule in src/viewport/pick.ts. It lists the objects under
the cursor front to back, in the order they are drawn.
- A tilemap counts only where it has a tile. Its depth there is the Z
of the highest layer with a tile at that spot. An empty cell is
empty space, so a click clears the selection or starts a marquee.
- Pressing on any selected object, hidden ones included, drags the
selection. Otherwise the press selects the topmost object and drags
it, as before.
- A click without drag on a selected object selects the next one
underneath. After the lowest it starts again at the top.
- New debug hook __gm.sceneStack(x, y) returns that list.
Decisions:
- With a paint tool the selected tilemap paints anywhere on its grid,
even under a sprite. With empty cells as empty space an empty cell
could not be painted otherwise.
- Icons such as colliders and cameras stay in front. A second click
now reaches the sprite under a collider.
Tests:
- ./check.sh: 142 files, 2090 tests, exit 0 (was 141 / 2080)
- 10 new unit tests in tests/viewport/pick.test.ts
- GM_STEPS=39,40: ALL PASSED, 77 checks
- GM_STEPS=1-16,29,39-43: ALL PASSED, 626 checks
- 4 new checks in block 40q: sprite over a lower layer, empty cell,
cycling crate, map, crate, dragging the hidden selected crate.
- Counter test: whole-area tilemap hits and always-topmost presses
fail 4 unit tests and all 4 new checks.
- Check total 1290 (1286 + 4), computed, not yet counted by CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
Reordering shapes by dragging worked well in the vector tab. Pixel
art layers and frames and tilemap layers still used up/down buttons.
All three lists now share one widget built from the shape list.
- SortableList in the UI toolkit, with the drag rules next to it in
src/ui/sortable.ts. A row has an optional thumbnail, a name, a hint
on the right and an eye toggle. Every drop is one undo step.
- The shape list is a thin wrapper around it. shapeRows.ts is gone,
its tests moved to the new rules.
- Pixel art layers show the layer alone on the current frame, frames
show the whole frame. Up/Down, Hide, Earlier and Later are gone.
- Tilemap layers get the eye instead of the checkbox. The per-row
up, down and delete buttons are replaced by Delete layer next to
Add layer.
- A row is selected on release instead of press. Selecting rebuilt
the inspector or tilemap panel and cancelled a drag that started
on a row not yet selected.
Tests:
- ./check.sh: 138 files, 2052 tests, exit 0 (before 137 / 2051).
- GM_STEPS=39,40,47,48 (fresh seed): ALL PASSED, 154 checks.
- 5 new e2e checks. Check total 1264 in the README is computed
(1259 + 5), CI never measured 1259.
- Counter-checks bite: dropMove ignoring reversed fails a unit test,
a tilemap drop not applied fails the drag check, a layer thumbnail
showing all layers fails the thumbnail check.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Issue #193 asked where friction, bounciness and density of a tile
live. Tiles had a fixed surface (friction 0.5, bounciness 0) and a
collider could not use the outline drawn on a texture frame (#141).
Both gaps are closed here.
- A collider has a fifth shape "Frame collision" with a Frame row.
The frame is picked like a sprite's texture and stored as the
value 'frame'.
- The shape is linked, not copied. Redrawing the outline in the
texture tab changes every collider using that frame, in the editor
and on the next play.
- The frame centre sits on the collider origin, one image pixel is
one unit. The shape turns, scales and mirrors like any other.
- Every outline ring blocks. Each ring is split into convex parts the
same way a polygon is.
- Without a frame, with a whole texture or with a frame that has no
outline, the collider is drawn red as a box and blocks nothing.
- Move, Rapier, raycast, detection, hit test, drawing and footprint
all resolve the frame. At runtime an entity looks the frame up
live in the current project.
- A tile has Friction and Bounciness next to Solid and One-way. The
rows only show when Solid is on. Defaults and mixing rules match
the collider. Density is left out because a tilemap never moves.
- Tilemap cells are grouped by surface as well as by one-way angle.
Ice and stone never merge into one rectangle, and every cell brings
its surface to Move and Rapier.
Findings:
- A tile with a collision scale below 1 is no longer flush with a
neighbour of another surface. It is inset there like at an open
side, the same as next to one-way tiles today.
- A bounce test first measured 0.84. The ball had landed on the hero
at the origin, not on the tile. On the tile it measures 0.993.
Tests:
- ./check.sh: 130 files, 1931 tests, exit 0 (was 129 / 1917).
- 14 new unit tests in tests/core/surface.test.ts. The Move tests run
against the editor run and the physics system.
- 3 new e2e checks: 40l (surface rows follow Solid), 42e (collider
has frame size, a walker climbs the slope in the frame).
- GM_STEPS=1-8,39-43 freshly seeded: ALL PASSED, 376 checks.
- Three counter-probes bite: Move ignores the tile surface (2 fail),
Rapier ignores it (1 fails), frame shape falls back to a box
(5 fail).
- Check sum 1198 is computed (1195 + 3), the full run belongs to CI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>