diff --git a/src/editor/debugHook.ts b/src/editor/debugHook.ts index ef6ca890..4e8f73e2 100644 --- a/src/editor/debugHook.ts +++ b/src/editor/debugHook.ts @@ -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. */ diff --git a/src/editor/texture/TextureCanvas.ts b/src/editor/texture/TextureCanvas.ts index e625345d..92ace0bc 100644 --- a/src/editor/texture/TextureCanvas.ts +++ b/src/editor/texture/TextureCanvas.ts @@ -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(); @@ -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; diff --git a/src/editor/texture/TextureEditor.ts b/src/editor/texture/TextureEditor.ts index 78ccc68f..a3753448 100644 --- a/src/editor/texture/TextureEditor.ts +++ b/src/editor/texture/TextureEditor.ts @@ -23,6 +23,12 @@ export class TextureEditor extends AssetTabEditor { 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 ?? ''; } diff --git a/tools/e2e/13-frames.mjs b/tools/e2e/13-frames.mjs index fc3fbdf6..2a6d03d7 100644 --- a/tools/e2e/13-frames.mjs +++ b/tools/e2e/13-frames.mjs @@ -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) {