From 319e731d0e52a64d04c385aaf6d7b0f90dbe4326 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 04:20:35 +0000 Subject: [PATCH] =?UTF-8?q?Workers=20P2:=20Szene=20bekommt=20flush()=20f?= =?UTF-8?q?=C3=BCr=20die=20R=C3=BCckkehr=20in=20den=20Tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Bühne beruhigt die Regie beim Zurückkehren (alles zu Ende gespielt, alle heim, Aufzug und Effekte leer); im Standbild zeichnet flush() ein Bild. Co-Authored-By: Claude Sonnet 5.5 --- src/components/workers/engine/scene.js | 13 ++++++-- tests/unit/workersScene.test.mjs | 43 +++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/components/workers/engine/scene.js b/src/components/workers/engine/scene.js index 4273652..79d12c9 100644 --- a/src/components/workers/engine/scene.js +++ b/src/components/workers/engine/scene.js @@ -35,7 +35,7 @@ const bindGlobal = (name) => (typeof globalThis[name] === 'function' ? globalThi function inertHandle() { const nop = () => {} return { - destroy: nop, setState: nop, pushEvents: nop, select: nop, setPaused: nop, resize: nop, zoomBy: nop, resetView: nop, + destroy: nop, setState: nop, pushEvents: nop, select: nop, setPaused: nop, flush: nop, resize: nop, zoomBy: nop, resetView: nop, locate: () => null, stats: () => ({ frames: 0, avgDrawMs: 0, zoom: 1 }), debug: () => ({ pending: 0, agents: [], lift: 'idle', fx: 0 }), @@ -43,12 +43,14 @@ function inertHandle() { } /* mountTower(canvas, { onSelect, reducedMotion = false, coarsePointer = false, fontsReady, dpr = 1, raf, caf, now }) - → { destroy(), setState(state), pushEvents(events), select(workerId|null), setPaused(bool), resize(), zoomBy(faktor), + → { destroy(), setState(state), pushEvents(events), select(workerId|null), setPaused(bool), flush(), resize(), zoomBy(faktor), resetView(), locate(workerId): { x, y } | null, stats(): { frames, avgDrawMs, zoom }, debug() } Rasterweite je Bild: k = Canvas-Breite / (320 / zoom). Reihenfolge je Bild: Weiß, Außenwelt, Turm, Räume, Aufzug hinten, Figuren, Aufzug vorn, Effekte, Pausenschleier. onSelect(id | null) kommt nur bei Klicks des Nutzers, nie bei select(). reducedMotion (und fehlendes raf): keine Schleife; setState, pushEvents, select, resize und die Kamera zeichnen je ein Standbild, pushEvents spielt die Regie sofort zu Ende. fontsReady (Promise): nach dem Laden der Schrift wird einmal neu gezeichnet. + flush() beruhigt die Regie auf einen Schlag (alles zu Ende gespielt, alle Figuren heim, Aufzug und Effekte leer): die Bühne ruft es + beim Zurückkehren in den Tab, damit nichts aus der Zeit davor nachläuft. locate liefert die Mitte der Trefferfläche in CSS-Pixeln relativ zur Leinwand (null: unbekannt oder außerhalb der Ansicht). Nach destroy() ist jeder Aufruf wirkungslos. */ export function mountTower(canvas, options = {}) { @@ -318,6 +320,13 @@ export function mountTower(canvas, options = {}) { } }, + flush() { + if (destroyed) return + director.flush() + if (still) calm() + draw() + }, + setPaused(value) { if (destroyed) return const next = !!value diff --git a/tests/unit/workersScene.test.mjs b/tests/unit/workersScene.test.mjs index ac5d17e..05ce9fd 100644 --- a/tests/unit/workersScene.test.mjs +++ b/tests/unit/workersScene.test.mjs @@ -14,7 +14,7 @@ const open = async (o = {}, extra = {}) => { test('Handle hat die Methoden aus Übersicht §7 und E4; Mount zeichnet sofort', async () => { const { sb, h } = await open() - for (const m of ['destroy', 'setState', 'pushEvents', 'select', 'setPaused', 'resize', 'zoomBy', 'resetView', 'locate', 'stats', 'debug']) { + for (const m of ['destroy', 'setState', 'pushEvents', 'select', 'setPaused', 'flush', 'resize', 'zoomBy', 'resetView', 'locate', 'stats', 'debug']) { assert.equal(typeof h[m], 'function', m) } assert.ok(sb.counts.fillRect > 100) @@ -575,3 +575,44 @@ test('debug() und stats(): Form', async () => { assert.ok(s.frames > 25) h.destroy() }) + +test('flush(): Rückkehr in den Tab beruhigt die Regie (alles heim, Aufzug und Effekte leer) und zeichnet; nach destroy wirkungslos', async () => { + const { sb, h } = await open() + h.setState(makeState()) + const arne = () => h.debug().agents.find((a) => a.id === 'arne') + const home = { x: arne().x, y: arne().y, floor: arne().floor } + h.pushEvents([ev('uebergabe', 'arne', { to: 'bea' })]) + sb.tick(3) + assert.ok(h.debug().pending > 0) + h.setPaused(true) + const n = sb.counts.fillRect + h.setPaused(false) + h.flush() + assert.ok(sb.counts.fillRect > n) + assert.equal(h.debug().pending, 0) + assert.equal(h.debug().lift, 'idle') + assert.equal(h.debug().fx, 0) + assert.deepEqual({ x: arne().x, y: arne().y, floor: arne().floor }, home) + h.pushEvents([ev('uebergabe', 'arne', { to: 'bea' })]) + assert.equal(h.debug().pending, 1) + h.destroy() + const m = sb.counts.fillRect + h.flush() + assert.equal(sb.counts.fillRect, m) + assert.equal(h.debug().pending, 1) +}) + +test('flush() im Standbild zeichnet ein Standbild ohne Bildrückruf; ohne 2D-Kontext tut es nichts', async () => { + const { sb, h } = await open({}, { reducedMotion: true }) + h.setState(makeState()) + const n = sb.counts.fillRect + h.flush() + assert.ok(sb.counts.fillRect > n) + assert.equal(sb.rafPending(), 0) + h.destroy() + const bare = await createEngineSandbox() + bare.canvas.getContext = () => null + const g = bare.mountTower(bare.canvas, bare.opts({})) + g.flush() + assert.equal(bare.counts.fillRect, 0) +})