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

+46
View File
@@ -0,0 +1,46 @@
/**
* A sprite's preview in the inspector: magnified by whole device pixels with its corner on one, so
* pixel art stays sharp; shrunk freely when it does not fit, and with room for a shader's padding.
*/
import { describe, expect, it } from 'vitest';
import { previewFit } from '../../src/editor/inspector/previewFit';
const room = { w: 300, h: 140 };
const options = { margin: 8, maxZoom: 8, ratio: 1 };
/** Whether `v` device pixels is a whole one, within float noise. */
const whole = (v: number) => Math.abs(v - Math.round(v)) < 1e-9;
describe('previewFit', () => {
it('magnifies a small sprite by a whole factor and centres it on whole pixels', () => {
// 124 / 15 = 8.27 is capped at 8; the 120-px picture leaves 90 and 10 at the sides.
const fit = previewFit(15, 15, room, options);
expect(fit).toEqual({ zoom: 8, x: 90, y: 10 });
});
it('rounds the zoom down to whole device pixels', () => {
// 124 / 48 = 2.58: two pixels per unit, not a blur between two and three.
expect(previewFit(48, 48, room, options).zoom).toBe(2);
// At a device ratio of 1.5 the same room holds 3.875 device pixels per unit: three, = 2 panel px.
const fit = previewFit(48, 48, room, { ...options, ratio: 1.5 });
expect(fit.zoom).toBe(2);
expect(whole(fit.x * 1.5) && whole(fit.y * 1.5)).toBe(true);
});
it('puts the corner of an odd size on a whole device pixel', () => {
const fit = previewFit(15, 9, room, { ...options, ratio: 1.25 });
expect(whole(fit.zoom * 1.25)).toBe(true);
expect(whole(fit.x * 1.25) && whole(fit.y * 1.25)).toBe(true);
});
it('shrinks a sprite bigger than the room to fit, without rounding to nothing', () => {
const fit = previewFit(800, 200, room, options);
expect(fit.zoom).toBeCloseTo(284 / 800);
expect(800 * fit.zoom).toBeLessThanOrEqual(room.w - 16);
});
it("keeps room for a shader's padding", () => {
// 10 % on each side: 124 / (15 * 1.2) = 6.9 → 6 rather than 8.
expect(previewFit(15, 15, room, { ...options, pad: { u: 0.1, v: 0.1 } }).zoom).toBe(6);
});
});
+1
View File
@@ -351,6 +351,7 @@ describe('the groups of the inspector', () => {
'[TRANSFORM]',
'[EDITOR OFFSET (SCENE VIEW ONLY)]',
'[PROPERTIES]',
'[PREVIEW]',
]);
});