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>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-23 15:57:13 +02:00
1 parent ceaa44e5b8
commit fd6af84474
5 files changed
+179 -46

No files matched your search

+50
View File
@@ -298,6 +298,56 @@ export async function shapes(t) {
)) === true,
JSON.stringify({ two, left }),
);
// 42c-ii. A sheet full of outlines stays smooth (#220): dragging a corner redraws only the
// edited frame, a pan moves the drawn outlines instead of drawing them anew, and a
// change to an unselected frame still reaches the picture.
const drawInfo = () => json('__gm.texture.drawInfo()');
const beforeDrag = await drawInfo();
const corner = await json('__gm.texture.pointAt(32, 0)');
const aside = await json('__gm.texture.pointAt(24, 0)');
await drag(corner.x, corner.y, aside.x, aside.y, 8);
const afterDrag = await drawInfo();
await undo();
check(
'dragging a corner does not redraw the frames around it',
afterDrag.sheetBuilds === beforeDrag.sheetBuilds,
JSON.stringify({ before: beforeDrag.sheetBuilds, after: afterDrag.sheetBuilds }),
);
await select({ kind: 'asset', assetId: texture?.id });
await settle();
const unselected = await drawInfo();
await undo();
await settle();
const restored = await drawInfo();
await t.redo();
await settle();
check(
'undo on an unselected frame redraws its outlines',
unselected.sheetOutlines === 1 && restored.sheetOutlines === 2 && (await drawInfo()).sheetOutlines === 1,
JSON.stringify({ unselected: unselected.sheetOutlines, restored: restored.sheetOutlines }),
);
const area = await json('__gm.texture.rect()');
const beforePan = await drawInfo();
const [px, py] = [Math.round(area.x + area.w / 2), Math.round(area.y + area.h / 2)];
await drag(px, py, px + 40, py + 25, { steps: 8, button: 'middle' });
const afterPan = await drawInfo();
const origin = await json('__gm.texture.pointAt(0, 0)');
const label = afterPan.labels.find((l) => l.text === 'Wedge');
check(
'a pan moves the drawn outlines and names along without drawing them anew',
afterPan.sheetBuilds === beforePan.sheetBuilds &&
Math.abs(afterPan.origin.x - beforePan.origin.x - 40) < 0.01 &&
Math.abs(afterPan.origin.y - beforePan.origin.y - 25) < 0.01 &&
Math.abs(afterPan.origin.x - origin.x) < 0.01 &&
Math.abs(afterPan.origin.y - origin.y) < 0.01 &&
Boolean(label) &&
Math.abs(label.x - origin.x - 5) < 0.01 &&
Math.abs(label.y - origin.y - 5) < 0.01,
JSON.stringify({ beforePan, afterPan, origin }),
);
await select({ kind: 'frame', assetId: texture?.id, frameId: 'sh-frame' });
await settle();
await clickText('Collision');
await showTab('scene');
await settle();