Texture tab: one masked container for everything the view moves (#82)
Only the picture was masked, so the checkerboard, the frame outlines, the handles and the name plates were drawn over the panel next door once the sheet was zoomed past the tab. The dark block in the report is the checkerboard's base: checker.ts fills the whole image rect and clips only the light squares to the widget. Everything that pans and zooms now sits in one masked container, the pattern WorldViewport and AnimationCanvas already use. A check samples pixels outside the tab and names the frames drawn past it; a guard in front of it asserts that zooming really pushes the sheet over every edge, or the pixel probes would measure nothing and stay green forever. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fe036db8f2
commit
4f240a50fd
4 files changed
+58
-17
No files matched your search
@@ -315,6 +315,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
pickerWidth: () => textureEditor.picker.w,
|
||||
/** Where an image pixel of the shown sheet sits on screen (frames are dragged in image coordinates). */
|
||||
pointAt: (x: number, y: number) => textureEditor.imageToScreen(x, y),
|
||||
rect: () => textureEditor.canvasRect(),
|
||||
},
|
||||
animation: {
|
||||
/** Frames, the one on screen, play state and where the preview picture is drawn. */
|
||||
|
||||
@@ -61,8 +61,9 @@ interface NewFrame {
|
||||
*/
|
||||
export class TextureCanvas extends Widget {
|
||||
private readonly bg = new Graphics();
|
||||
/** Everything the view can move: panned or zoomed, all of it reaches past the widget. */
|
||||
private readonly content = new Container();
|
||||
private readonly checker = new Graphics();
|
||||
private readonly imageLayer = new Container();
|
||||
private readonly image = new Sprite();
|
||||
private readonly overlay = new Graphics();
|
||||
private readonly labels = new Map<string, Text>();
|
||||
@@ -79,20 +80,10 @@ export class TextureCanvas extends Widget {
|
||||
|
||||
constructor(private readonly state: EditorState) {
|
||||
super();
|
||||
this.imageLayer.addChild(this.image);
|
||||
this.addChild(
|
||||
this.bg,
|
||||
this.checker,
|
||||
this.imageLayer,
|
||||
this.overlay,
|
||||
this.labelLayer,
|
||||
this.hint,
|
||||
this.clip,
|
||||
);
|
||||
this.imageLayer.mask = this.clip;
|
||||
this.imageLayer.eventMode = 'none';
|
||||
this.overlay.eventMode = 'none';
|
||||
this.labelLayer.eventMode = 'none';
|
||||
this.content.addChild(this.checker, this.image, this.overlay, this.labelLayer);
|
||||
this.addChild(this.bg, this.content, this.hint, this.clip);
|
||||
this.content.mask = this.clip;
|
||||
this.content.eventMode = 'none';
|
||||
this.hint.eventMode = 'none';
|
||||
this.focusable = true;
|
||||
|
||||
|
||||
@@ -23,6 +23,12 @@ export class TextureEditor extends AssetTabEditor<TextureAsset, TextureCanvas> {
|
||||
return this.canvas.imageToScreen(x, y);
|
||||
}
|
||||
|
||||
/** The drawing area without the header, so a check can ask what lies outside it. */
|
||||
canvasRect(): { x: number; y: number; w: number; h: number } {
|
||||
const p = this.canvas.getGlobalPosition();
|
||||
return { x: p.x, y: p.y, w: this.canvas.w, h: this.canvas.h };
|
||||
}
|
||||
|
||||
protected override selectedId(): string {
|
||||
return this.state.selectedTextureId ?? '';
|
||||
}
|
||||
|
||||
+45
-2
@@ -34,7 +34,7 @@ function nearestColour(sample) {
|
||||
}
|
||||
|
||||
export async function frames(t) {
|
||||
const { check, sleep, json, evaluate, shot, clickText, drag, press, undo } = t;
|
||||
const { check, sleep, json, evaluate, shot, clickText, drag, press, undo, wheel } = t;
|
||||
|
||||
// 13a. The tab exists and shows the texture the selection points at.
|
||||
await press('Escape');
|
||||
@@ -156,7 +156,50 @@ export async function frames(t) {
|
||||
check('and numbers them in reading order', sliced[0]?.startsWith('Frame 1 '), JSON.stringify(sliced[0]));
|
||||
await shot('36-sliced-sheet');
|
||||
|
||||
// 13f. A sprite pointed at a frame slicing removed falls back to its colour.
|
||||
// 13f. Zoomed in, the sheet is larger than the tab: nothing of it may be drawn next to the tab.
|
||||
// The bug this guards against drew the checkerboard's base rectangle, the frame outlines
|
||||
// and the name plates over the Assets panel, because only the picture itself was masked.
|
||||
await evaluate("__gm.showTab('texture')");
|
||||
await sleep(400);
|
||||
const area = await json('__gm.texture.rect()');
|
||||
// Points on the panel left of the tab and on the tab row above it – both were painted over.
|
||||
const probes = [
|
||||
...[0.2, 0.4, 0.6, 0.8].map((f) => ({ x: Math.round(area.x - 40), y: Math.round(area.y + f * area.h) })),
|
||||
...[0.25, 0.5, 0.75].map((f) => ({ x: Math.round(area.x + f * area.w), y: Math.round(area.y - 10) })),
|
||||
];
|
||||
const readProbes = async () => {
|
||||
const out = [];
|
||||
for (const p of probes) out.push(await json(`__gm.pixelAt(${p.x}, ${p.y})`));
|
||||
return out;
|
||||
};
|
||||
const quiet = await readProbes();
|
||||
|
||||
const centre = { x: Math.round(area.x + area.w / 2), y: Math.round(area.y + area.h / 2) };
|
||||
for (let i = 0; i < 6; i++) await wheel(centre.x, centre.y, -240);
|
||||
await sleep(500);
|
||||
const topLeft = await json('__gm.texture.pointAt(0, 0)');
|
||||
// Without this the probes below would prove nothing: they need a sheet that reaches past the edge.
|
||||
check(
|
||||
'zooming in pushes the sheet past the tab on every side',
|
||||
topLeft.x < area.x && topLeft.y < area.y,
|
||||
JSON.stringify({ topLeft, area }),
|
||||
);
|
||||
await shot('36b-zoomed-sheet');
|
||||
|
||||
const loud = await readProbes();
|
||||
const changed = probes
|
||||
.map((p, i) => ({ ...p, was: quiet[i], now: loud[i] }))
|
||||
.filter((p) => p.was !== p.now);
|
||||
check('a zoomed sheet paints nothing outside the tab', changed.length === 0, JSON.stringify(changed));
|
||||
check(
|
||||
'and no frame name is drawn past the tab either',
|
||||
(await json('__gm.textOverflows()')).length === 0,
|
||||
JSON.stringify(await json('__gm.textOverflows()')),
|
||||
);
|
||||
await clickText('Fit');
|
||||
await sleep(400);
|
||||
|
||||
// 13g. A sprite pointed at a frame slicing removed falls back to its colour.
|
||||
await evaluate("__gm.showTab('scene')");
|
||||
await sleep(700);
|
||||
if (box) {
|
||||
|
||||
Reference in new issue
Block a user