Painting on the 60x60 map of a project with fifty objects dropped the
editor to 20 to 39 fps. A profile of a brush stroke showed four causes,
none of them the brush itself.
- Each frame of a stroke previews the model. The asset and hierarchy
panels then built every row anew, so every label became a new text
and a new upload to the GPU. One run spent 46 % of the main thread
in those uploads. ListView.setItems now keeps a row whose item draws
the same (sameListItem) and builds only the rows that changed.
- The brush drew the whole scene on every pointer event, on top of the
preview. Now the preview draws what the stroke changed, once a frame.
The rectangle tool asks for one drawing a frame for its box.
- An animated tile made the view draw in the tick and the preview drew
it again in the same frame. animate now asks just before the frame
is drawn and stays out when the view was drawn since the tick began.
- layoutSymbols ran on every drawing and costs up to the cube of the
symbols. SymbolLayoutMemo keeps the spots while no anchor moves.
- A painted cell made the map read every "x,y" key again, and once
more for each of the eight neighbours of every cell. layerCells reads
a layer once per content into an index by place. layerTiles and the
blocking cells take their neighbours from there (neighboursIn).
parseCellKey reads a key without split, with the same results.
The test hook sceneRedraws counts how often the scene view was drawn.
Not done: a painted cell still works out the tiles and blocking cells
of the whole map again, so that part grows with the map.
Tests:
- Brush stroke on a copy of the project (stein2, Chrome): 20 to 39 fps
before, 47.5 after, 55 to 57 without painting. Labels drawn again
during the stroke 2012 before, none after. Scene drawings per frame
1.32 before, 1 after.
- One painted cell, headless: 28 to 16 ms on 60x60, 109 to 49 ms on
the same map doubled to 120x120.
- frame-preview: 2 new, red with the old pointer redraw.
- tilemap: 3 new (key reading, index, refresh), list-ping 1 new,
symbol-layout 1 new. 5 single reversals turned each red.
- e2e 39,40 (melektaus-atx): ALL PASSED, 107 checks (1 new, a fast
stroke: 6 drawings in 6 frames, no label drawn again). With the row
reuse and the pointer redraw reverted it fails: 24 drawings in
3 frames, and the panels draw their labels again.
- Gate (melektaus-atx): CHECK EXIT 0, 260 files, 4038 tests.
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>
Recurring cleanup round over the code since round 6 (056a622), 105
commits. Behaviour is unchanged. Duplicated code in src went from 29
clones with 229 lines (0.32 %) to 14 clones with 109 lines (0.15 %),
measured with jscpd and the same settings as round 6.
- BehaviorSystem.ts shrinks from 941 to 589 lines. Collecting the
pulses (keys, timers, collisions, animations) moved to
behaviorPulses.ts. The NodeContext the nodes receive moved to
behaviorContext.ts. Imports from outside stay the same, TimeScaled
is still exported from BehaviorSystem.ts.
- PointKindButton and drawPointGrip replace the "Point:" button with
its menu and the point and handle drawing that the curve and the
vector art tab each had on their own.
- lerpVec, cubicAt and splitCubic in src/core/util/vec.ts replace four
copies of the cubic curve and two de Casteljau splits.
- mapNested in Properties.ts is the one walk over lists and structs
that liveObjects and objectRefs did separately. withFieldRenamed
and withoutField replace three copies of renaming a struct field
and two of removing one.
- jsonBody and staleRevision build the JSON body and turn a 409 into
StaleRevisionError for all stores. The auth store turns a 401 into
its error in one place.
- Smaller helpers: structAssetFrom for the two "Make struct type"
commands, shownObject in the inspector, selectedItem for the size
gizmo and the polygon corners, solidTiles in the tilemap collision,
requireVariablesAsset and requireBehaviorVariable.
- Left as is on purpose: the plain forwarding from the collision and
physics systems to Solids, the pointer wiring of the canvases and
the identical last lines of the two make commands.
- Removed unused code: tilemapTileIds and tilemapsUsing were never
called since #128 and #131. EditorState.runtimeProject became unused
with GameRun in #266. The assetId getter of the make commands was
read only by a test.
- 60 values used only in their own file lost their export. The shader
and particle node catalogues stay exported as in round 6.
- Five comments that told history now describe the current state. The
doc comment of BehaviorSystem sits on the class again.
- README lists the struct, clip area and curve assets and mentions the
game link from #266.
Findings:
- Deleting a tile that is painted on a tilemap leaves its cells in
place, and they draw nothing. The delete dialog does not mention the
tilemaps. That was probably the job of the unused tilemapsUsing.
Left as is, because fixing it changes behaviour.
- The solid filter for tile surfaces was untested. A bent filter
stayed green. A new test in tests/core/surface.test.ts now fails
against it.
Tests:
- ./check.sh: 195 files, 2815 tests, exit 0 (one test removed with
tilemapsUsing, one added for tile surfaces).
- Full e2e run: ALL PASSED, 1580 checks.
- Smokes jumprun, tetris, actor: 24, 19 and 21 checks, all passed.
The example project files were restored byte for byte afterwards.
- Counter-checks, 12, all fail as expected: 10 in unit tests (among
them mapNested, splitCubic, cubicAt, staleRevision, the stay pulse,
the late timer and the time scale), 2 in the browser (Point button
always enabled fails 47, selectedItem without a hit fails 29).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 0.02 texel the lines between tiles of a nearest tilemap were gone at
the zooms tried first, but still showed at others. The user raised the
inset to 0.1 by hand and checked it in the browser.
- A tenth of a texel is still far below half a texel, so the texel
picked at every pixel centre stays the same.
- The comment names the new value.
Tests:
- tilemap.test.ts follows the value: the inset test expects 0.1/96,
the sliver test allows up to 0.25 texel from the edge, and the quad
test checks its uvs to 2 digits instead of 3.
- ./check.sh: 156 files, 2314 tests, CHECK EXIT 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At zoom levels that do not land quarter edges on whole screen pixels,
a tilemap whose texture uses Nearest showed one-pixel lines in the
wrong colour. The float uv of a pixel centre right at a quad edge
could round onto the first texel of the neighbouring quarter in the
sheet, which is usually a different tile. Terrain sheets showed it
most, because every tile is four quads there.
- Nearest now pulls the uvs 0.02 texel inside each quarter. That is
far below half a texel, so every picked texel stays the same and
pixel art keeps its size.
- Linear keeps its half-texel inset.
- The first draft used 0.01. The user raised it to 0.02 after
checking the result in the browser.
Tests:
- ./check.sh: 156 files, 2299 tests, CHECK EXIT 0 (before 2298).
- New unit test: every nearest uv sits strictly inside its quarter,
by less than 0.05 texel. Two older tests now expect the inset.
- Counter-check: with no inset for nearest the new test and the
inset test fail.
- No e2e check. The user checked the result in the browser.
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>
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>