import { openTab } from './lib/scaffold.mjs'; /** * Render on change: the editor draws what changed, not sixty pictures a second. * * This is the one thing the rest of the suite cannot see. `pixelAt` re-renders the stage on * demand, so every colour check would pass just as well on a screen that froze a minute ago – * only the drawn-frame counter tells the difference between "drawn" and "merely correct". * * The step runs last and leaves the editor as it found it. */ export async function renderOnChange(t) { const { check, sleep, evaluate, select, click, locate, play, stop, shot, waitUntil } = t; const stats = () => evaluate('__gm.renderStats()'); /** Frames drawn over `ms` of doing nothing at all. */ const drawnDuring = async (ms) => { const before = await stats(); await sleep(ms); const after = await stats(); return after.drawn - before.drawn; }; /** * Waits for the trailing window of the last event to run out. Measuring without this counts the * frames the previous click is still entitled to and calls them a leak. * * The autosave is waited out first. It fires ~1 s after the last model change and paints the * status twice – `saving`, then `saved` – so an earlier step's last edit lands in this step's * quiet window and looks like a renderer that will not go to sleep. Those frames are earned: * the status text really did change. The step has to stop measuring before them, not call them * a leak. */ const settle = async (timeoutMs = 5000) => { await waitUntil("!['dirty', 'saving'].includes(__gm.state.saveStatus)", { timeout: timeoutMs, label: 'the autosave finished', }); for (let waited = 0; waited < timeoutMs; waited += 100) { if (!(await stats()).awake) return true; await sleep(100); } return false; }; // 33a. A tab whose picture stands still: the scene viewport may wear a Time shader, and the // particles and animation previews are moving pictures by definition. await openTab(t, 'graph'); await select(null); const quiet = await settle(); const idle = await drawnDuring(700); check( 'an editor nobody touches draws no frames at all', quiet && idle === 0, `settled=${quiet}, ${idle} frames drawn`, ); // 33b. A model change that arrives without any input – no click, no key – still reaches the // screen. This is the wake-up that a strict dirty flag on the widgets would have missed. const beforeChange = (await stats()).drawn; await evaluate(`__gm.state.setNotice('render check', 'info')`); await sleep(150); const afterChange = (await stats()).drawn; check( 'a change nobody clicked for is drawn anyway', afterChange > beforeChange, `${beforeChange} → ${afterChange}`, ); // 33c. Input wakes it, and it goes back to sleep on its own afterwards. await settle(); const beforeClick = (await stats()).drawn; const spot = await locate('Behavior'); await click(spot.x, spot.y); const afterClick = (await stats()).drawn; check('a click draws again', afterClick > beforeClick, `${beforeClick} → ${afterClick}`); const settled = await settle(); const backToSleep = await drawnDuring(700); check( 'and it stops on its own once the click is dealt with', settled && backToSleep === 0, `settled=${settled}, ${backToSleep} frames`, ); // 33d. While the game runs, the frame cannot come from an event: the world moves by itself. await play(); const playing = await drawnDuring(600); const fps = await evaluate('__gm.fps()'); check( 'a running game keeps getting frames without being touched', playing > 10, `${playing} frames in 600 ms`, ); check('and the fps readout keeps measuring them', typeof fps === 'number' && fps > 0, `fps=${fps}`); await shot('01-playing'); await stop(); // 33e. Stopping ends the reason to draw. Back off the scene tab first: a Time shader left on // screen by an earlier step is a legitimate reason to keep drawing, and would mask this check. await openTab(t, 'graph'); const stopped = await settle(); const afterStop = await drawnDuring(700); check( 'stopping puts it back to sleep', stopped && afterStop === 0, `settled=${stopped}, ${afterStop} frames`, ); await evaluate(`__gm.state.setNotice('', 'info')`); await sleep(200); }