The CI run for ded71a9 failed three checks in e2e 58 clipAreas. The
cause was the new step 57g, not the editor. It ends in the Variables
tab on purpose, and the clean-up of 57 then deletes the object whose
list that tab shows. The tab closes itself, and the editor falls back
to the last open document instead of the scene view. In the full run
earlier modules had left document tabs open, so 58 measured and
clicked in the wrong tab. Run on its own, 57 had no other document
open, which is why the module run before the push was green.
- 57 switches back to the scene view before its clean-up, with a
comment that says why.
- 58 sets the scene view itself at the start, as 44 and 51 already do.
It no longer depends on what the modules before it left open.
Tests:
- e2e 57,58 with the fix: ALL PASSED, 17 checks.
- Counter-check 57,58 and 54,57,58 without the fix: green, because no
other document was open.
- Counter-check with a graph tab left open before 57, without the fix:
3 FAILED with the same values as in CI. Same setup with the fix:
ALL PASSED, 17 checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
167 lines
7.0 KiB
JavaScript
167 lines
7.0 KiB
JavaScript
/**
|
||
* 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-'));
|
||
`);
|
||
}
|