Preview a sprite at the end of its inspector, placed or as an asset

A sprite's inspector listed its rows but never showed what they add up
to. The picture, its frame, nine-slice, tint and shader could only be
judged in the scene, often far from the object being edited.

- SpritePreview draws the sprite through syncSpriteMesh, the function
  the scene draws it with. Slices, padding, tint and shader come out
  the same without a second path. syncSpriteMesh now takes only the
  three fields of a DrawItem it reads.
- The subject is resolved by previewPicture from the Shader tab, so a
  placed sprite shows its own overrides.
- previewFit is the rule for size and place: whole device pixels per
  unit from 1 on, at most 8, and the corner on a whole device pixel.
  Pixel art stays sharp. A sprite larger than the group shrinks freely.
- A shader with a Time node keeps the preview redrawing once a frame
  while it is on screen. A folded group or a hidden panel stops it.
- New test hook __gm.inspectorSpritePreview().

Finding: the preview redrew on a model change in a microtask at first.
A drawn picture whose revision moved on is retried once, in a
microtask queued by the scene's synchronous redraw. That retry ran
before the preview had let go, and the old pixel art sheet stayed
waiting (e2e 48l-iii read waiting [1,1,1]). The preview now redraws
at once on a model change, as the scene does.

Tests:
- unit: 238 files, 3592 tests (+5 in preview-fit.test.ts), and the
  group list of a placed sprite in sections.test.ts ends in PREVIEW
- e2e 48: ALL PASSED, 47 checks (+2: 48l-i asset preview with
  the frame, 48l-iii placed preview with its shader values)
- counter-checks: without whole-pixel zoom 2 unit tests fail; with
  the microtask redraw 48l-iii fails with waiting [1,1,1]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-04 14:27:35 +02:00
1 parent 1665576fb9
commit 3f12a77aea
10 files changed
+239 -4

No files matched your search

+35 -2
View File
@@ -675,6 +675,26 @@ export async function pixelArt(t) {
JSON.stringify({ spriteRed, spriteBlue }),
);
// 48l-i. The inspector previews the selected sprite as the scene draws it, frame and all.
const previewSpots = async () => {
const rect = await json('__gm.inspectorSpritePreview()');
await settle();
if (!rect) return null;
const at = (px, py) =>
json(
`__gm.pixelAt(${Math.round(rect.x + ((px + 0.5) / 32) * rect.w)}, ${Math.round(rect.y + ((py + 0.5) / 32) * rect.h)})`,
);
return { rect, red: await at(5, 2), blue: await at(10, 10) };
};
const assetPreview = await previewSpots();
check(
'the inspector previews the sprite asset with the frame it wears',
Boolean(assetPreview) &&
colourDistance(assetPreview.red, HEX[RED]) < 12 &&
colourDistance(assetPreview.blue, HEX[BLUE]) < 12,
JSON.stringify(assetPreview),
);
// 48l-ii. Nine-slice on pixel art (#308): 12-pixel borders at scale 2 draw the corner at 2 units a
// pixel instead of 4, so red (5, 2) and blue (10, 10) move to units (11, 5) and (21, 21). The same
// spots are read before the borders are set, so a sprite that ignored them could not pass.
@@ -710,7 +730,8 @@ export async function pixelArt(t) {
// once and the cut with it, and Pixi warned for the source and its sampler every time.
await mutate(`
Object.assign(__gm.state.activeScene.objects.find((o) => o.id === 'pa-object').overrides,
{ shader: 'adjust-colors', saturation: 0 })`);
{ shader: 'adjust-colors', saturation: 0 });
__gm.state.select({ kind: 'object', holderId: __gm.state.activeScene.id, objectId: 'pa-object', objectIds: ['pa-object'] })`);
await settle();
await sleep(300);
const boundLogFrom = t.consoleLog.length;
@@ -730,10 +751,22 @@ export async function pixelArt(t) {
boundWarnings.length === 0 && waiting.every((n) => n === 0),
JSON.stringify({ warnings: boundWarnings.length, waiting }),
);
// The preview of the placed object wears its shader and its own saturation of 0: no colour left.
const objectPreview = await previewSpots();
const grey = (c) => {
const [r, g, b] = [1, 3, 5].map((i) => parseInt(c.slice(i, i + 2), 16));
return Math.max(r, g, b) - Math.min(r, g, b) <= 6;
};
check(
"the preview of a placed sprite draws its shader with the object's own values",
Boolean(objectPreview) && grey(objectPreview.red) && grey(objectPreview.blue),
JSON.stringify(objectPreview),
);
await mutate(`
const o = __gm.state.activeScene.objects.find((o) => o.id === 'pa-object');
delete o.overrides.shader;
delete o.overrides.saturation;`);
delete o.overrides.saturation;
__gm.state.select({ kind: 'asset', assetId: 'pa-sprite' })`);
await settle();
const textureRow = await json("__gm.inspectorField('Texture', true)");