/** * Clip areas (#260): a red bar below a 100×100 clip area is cut at the area's edge in the scene * view and in the game, and the cut-off part cannot be clicked. Selected, the bar shows that part * faintly and can be grabbed there. A turn of the real mouse wheel over the game reaches `On mouse * wheel`. * * Everything it adds carries a `cl58` id and goes again at the end. */ import { MAKE_OBJECT } from './lib/scaffold.mjs'; const BAR = 'cl58-bar'; const AREA = 'cl58-clip'; export async function clipAreas(t) { const { check, json, evaluate, mutate, click, sleep, shot, stop, wheel, showTab } = t; await showTab('scene'); // On mouse wheel → Log(delta), with no owner: it hears the whole game view. const graph = { nodes: [ { id: 'cl58-wheel', type: 'OnMouseWheel', x: 0, y: 0, params: {} }, { id: 'cl58-log', type: 'Log', x: 320, y: 0, params: {} }, ], edges: [ { id: 'cl58-e1', from: { node: 'cl58-wheel', port: 'exec' }, to: { node: 'cl58-log', port: 'exec' } }, { id: 'cl58-e2', from: { node: 'cl58-wheel', port: 'delta' }, to: { node: 'cl58-log', port: 'message' }, }, ], }; // Inside the camera's frame, so the game shows it too, but off the camera's own icon and in front // of what earlier steps left there. await mutate(` const p = __gm.state.project; const scene = __gm.state.activeScene; const cam = scene.objects.find((o) => o.id === 'o3').position; p.assets.push({ id: 'cl58-area', type: 'clip', name: 'Clip 58', props: { enabled: true, width: 100, height: 100 } }); p.assets.push({ id: 'cl58-scroll', type: 'behavior', name: 'Wheel 58', props: { enabled: true, runInEditor: false }, variables: [], graph: ${JSON.stringify(graph)} }); ${MAKE_OBJECT} const sprite = p.assets.find((a) => a.type === 'sprite'); const area = make('${AREA}', 'cl58-area', '', 0, 'Clip 58'); Object.assign(area.position, { x: cam.x - 180, y: cam.y - 110, z: 1000 }); const bar = make('${BAR}', sprite.id, '${AREA}', 0, 'Bar 58'); Object.assign(bar.position, { x: 50, y: 0, z: 0 }); bar.overrides = { width: 160, height: 40, color: 0xff0000, texture: '' }; const logic = make('cl58-logic', 'cl58-scroll', '', 0, 'Wheel 58'); Object.assign(logic.position, { x: cam.x + 200, y: cam.y - 150 }); scene.objects.push(area, bar, logic); `); await evaluate('__gm.state.select(null)'); await json(`(() => { const cam = __gm.state.activeScene.objects.find((o) => o.id === 'o3').position; __gm.lookAt(cam.x, cam.y); })()`); await sleep(300); const channels = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)); /** * Red across the bar's middle, split at the area's right edge: full red inside, and how far the * red goes past it. A few pixels either side of the edge are left out – they blend – and the row * runs below the middle, clear of the handles a selection puts on the sides. */ const across = async (view) => { const area = await json(`__gm.${view}(${JSON.stringify(AREA)})`); const bar = await json(`__gm.${view}(${JSON.stringify(BAR)})`); if (!bar) return null; const left = Math.round(bar.x - bar.w / 2 + 3); const edge = area ? area.x + area.w / 2 : bar.x - bar.w / 2 + (bar.w * 80) / 160; const row = await json( `__gm.pixelRow(${left}, ${Math.round(bar.y + bar.h / 4)}, ${Math.round(bar.w - 6)})`, ); const inside = row.slice(0, Math.max(0, Math.round(edge - left - 3))).map(channels); const outside = row.slice(Math.round(edge - left + 3)).map(channels); const red = (px) => px.filter(([r, g, b]) => r > 200 && g < 60 && b < 60).length; const tinted = (px) => px.filter(([r, g, b]) => r - Math.max(g, b) > 30).length; return { edge: Math.round(edge), inside: inside.length, redInside: red(inside), outside: outside.length, redOutside: red(outside), tintedOutside: tinted(outside), }; }; // 58a. The scene view cuts the bar at the area's edge. const cut = await across('objectRect'); await shot('58-scene-cut'); check( 'the scene view draws a child only inside its clip area', cut !== null && cut.redInside > 0.9 * cut.inside && cut.tintedOutside === 0 && cut.outside > 20, JSON.stringify(cut), ); // 58b. A click on the cut-off part does not pick the bar; one on the part left does. const bar = await json(`__gm.objectRect(${JSON.stringify(BAR)})`); const cutPoint = { x: Math.round(cut.edge + (bar.x + bar.w / 2 - cut.edge) / 2), y: Math.round(bar.y) }; const keptPoint = { x: Math.round(bar.x - bar.w / 2 + 10), y: Math.round(bar.y) }; const primary = () => json("__gm.state.selection?.kind === 'object' ? __gm.state.selection.objectId : null"); const stackCut = await json(`__gm.sceneStack(${cutPoint.x}, ${cutPoint.y})`); await click(cutPoint.x, cutPoint.y); await sleep(150); const afterCut = await primary(); await evaluate('__gm.state.select(null)'); await click(keptPoint.x, keptPoint.y); await sleep(150); const afterKept = await primary(); check( 'the cut-off part cannot be clicked, the part left can', !stackCut.includes(BAR) && afterCut !== BAR && afterKept === BAR, JSON.stringify({ stackCut, afterCut, afterKept }), ); // 58c. Selected, the cut-off part shows faintly and can be grabbed there. const open = await across('objectRect'); const stackOpen = await json(`__gm.sceneStack(${cutPoint.x}, ${cutPoint.y})`); await shot('58-scene-open'); check( 'a selected child shows its cut-off part faintly and can be grabbed there', open.redInside > 0.9 * open.inside && open.tintedOutside > 0.9 * open.outside && open.redOutside === 0 && stackOpen.includes(BAR), JSON.stringify({ open, stackOpen }), ); await evaluate('__gm.state.select(null)'); // 58d. The game cuts the same way. await t.play(); await sleep(600); const played = await across('gameRect'); await shot('58-game'); check( 'the running game draws the child only inside its clip area', played !== null && played.redInside > 0.9 * played.inside && played.tintedOutside === 0, JSON.stringify(played), ); // 58e. A notch of the real wheel over the game reaches On mouse wheel as delta 1. const logFrom = t.consoleLog.length; const centre = await json(`__gm.gameRect(${JSON.stringify(BAR)})`); await wheel(Math.round(centre.x), Math.round(centre.y), 100); await sleep(300); const logged = t.consoleLog .slice(logFrom) .filter((line) => line.startsWith('[behavior Wheel 58]')) .map((line) => line.split(']').pop().trim()); check( 'a notch of the mouse wheel over the game runs On mouse wheel with delta 1', logged.includes('1'), JSON.stringify(logged), ); await stop(); // Clean up. await evaluate('__gm.state.select(null)'); await mutate(` const p = __gm.state.project; const scene = __gm.state.activeScene; scene.objects = scene.objects.filter((o) => !o.id.startsWith('cl58-')); p.assets = p.assets.filter((a) => !a.id.startsWith('cl58-')); `); }