The issue asked for Set render layer, Set sees and Render layer of. As with #211, a probe showed that writing already works. A Properties node of the object's type writes Render layer during a run, Inherit included, and one of type Camera writes Sees. The next picture follows both. As agreed, only Render layer of is built, and writing stays with Properties. - Render layer of is a data node with the input object and the outputs name and number. It answers the layer that holds, so an object without a choice of its own names its parent's. - NodeContext.renderLayerOf answers the number with the same rule the drawing uses (renderLayerAlong). The new field renderLayers on GraphLookup brings the names to the node. - hasRenderLayer in PropertySchema says which types are drawn. - Decided here: for an object that is not drawn (a camera, a behavior, a collider) the name is empty and the number -1, as Layer of does without a collider. Changing the render layer or Sees during a run had no test so far. Three unit tests now hold the proof from the issue. Tests: - Gate: 231 files, 3534 tests (3526 + 8), exit 0 - e2e 6: 149 checks passed, e2e 63: 13 checks passed (+1) - Unit: the coin moves into the overlay with Set and back with Inherit, a HUD container takes its sprite along, the main camera sees a second layer once its Sees is set, and Render layer of names the inherited layer, and none for a behavior - Node catalog: four cases for Render layer of - 63c3 moves a sprite onto UI in a Play run: the main camera no longer draws it, and the log reads "UI" and "1" - Three counter-probes fail 1, 1 and 2 tests: without inheriting in the node, without the check for a drawn type, and without inheriting in the drawing - "layer of" in the palette still finds Layer of first (6z-x) - The e2e sum is 1731 (1730 plus 1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
469 lines
21 KiB
JavaScript
469 lines
21 KiB
JavaScript
/**
|
||
* Render layers and overlay cameras (#302). The main camera sees only `Default`, an overlay camera
|
||
* 2000 units to the right sees only `UI`. Each draws its own picture, the overlay's laid on top:
|
||
* a green sprite of the overlay with z -10 covers a red one of the main camera with z 1000, and
|
||
* what a camera does not see stays away – a yellow `UI` sprite in front of the main camera, a blue
|
||
* `Default` one in front of the overlay. Switched to no overlay, the second camera draws nothing.
|
||
* `Is hovered` and `Mouse position` find the mouse through the overlay camera too (#304), and a
|
||
* graph moves a sprite onto another layer while the game runs (#305).
|
||
* Then the main camera wears a shader over its picture (#299), in the Game tab and, switched on in
|
||
* the eye, in the scene view.
|
||
*
|
||
* Everything it adds carries an `rl63` id and goes again at the end.
|
||
*/
|
||
import { MAKE_OBJECT } from './lib/scaffold.mjs';
|
||
|
||
const OVERLAY = 'rl63-cam';
|
||
|
||
export async function renderLayers(t) {
|
||
const { check, json, evaluate, mutate, sleep, shot, showTab, play, stop, clickText, press } = t;
|
||
await showTab('scene');
|
||
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
const main = scene.objects.find((o) => o.id === 'o3');
|
||
window.__rl63 = { layers: p.renderLayers, overrides: structuredClone(main.overrides) };
|
||
p.renderLayers = ['Default', 'UI'];
|
||
main.overrides = { ...main.overrides, sees: 0b01 };
|
||
${MAKE_OBJECT}
|
||
const sprite = p.assets.find((a) => a.type === 'sprite');
|
||
const square = (id, name, dx, z, size, color, layer, from) => {
|
||
const o = make(id, sprite.id, '', 0, name);
|
||
Object.assign(o.position, { x: from.x + dx, y: from.y, z });
|
||
o.overrides = { width: size, height: size, color, texture: '', shader: '', renderLayer: layer };
|
||
return o;
|
||
};
|
||
const overlay = structuredClone(main);
|
||
Object.assign(overlay, { id: '${OVERLAY}', name: 'Overlay 63', instanceEdits: {} });
|
||
overlay.position = { ...main.position, x: main.position.x + 2000 };
|
||
overlay.overrides = { ...main.overrides, overlay: true, sees: 0b10 };
|
||
scene.objects.push(
|
||
overlay,
|
||
square('rl63-a', 'A 63', 70, 1000, 60, 0xff0000, -1, main.position),
|
||
square('rl63-d', 'D 63', -70, 1000, 60, 0xffff00, 1, main.position),
|
||
square('rl63-b', 'B 63', 0, -10, 40, 0x00ff00, 1, overlay.position),
|
||
square('rl63-c', 'C 63', 70, 1000, 60, 0x0000ff, 0, overlay.position),
|
||
);
|
||
`);
|
||
await evaluate('__gm.state.select(null)');
|
||
await showTab('game');
|
||
await sleep(300);
|
||
|
||
const channels = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
||
const is = (hex, [r, g, b]) => {
|
||
const [pr, pg, pb] = channels(hex);
|
||
return Math.abs(pr - r) < 30 && Math.abs(pg - g) < 30 && Math.abs(pb - b) < 30;
|
||
};
|
||
/** The three points of the picture the squares sit on: the centre and 70 units either side. */
|
||
const picture = async () => {
|
||
const { frame, scale } = await json('__gm.gameScale()');
|
||
const cx = Math.round(frame.x + frame.w / 2);
|
||
const cy = Math.round(frame.y + frame.h / 2);
|
||
const at = (dx) => json(`__gm.pixelAt(${Math.round(cx + dx * scale)}, ${cy})`);
|
||
return {
|
||
centre: await at(0),
|
||
right: await at(70),
|
||
left: await at(-70),
|
||
main: await json('__gm.gameDrawOrder()'),
|
||
overlays: await json('__gm.gameOverlays()'),
|
||
};
|
||
};
|
||
const GREEN = [0, 255, 0];
|
||
const RED = [255, 0, 0];
|
||
const YELLOW = [255, 255, 0];
|
||
const stacked = (seen) =>
|
||
seen.overlays.length === 1 &&
|
||
seen.overlays[0].camera === OVERLAY &&
|
||
seen.overlays[0].drawOrder.includes('rl63-b') &&
|
||
!seen.overlays[0].drawOrder.includes('rl63-c') &&
|
||
seen.main.includes('rl63-a') &&
|
||
!seen.main.includes('rl63-d') &&
|
||
!seen.main.includes('rl63-b') &&
|
||
is(seen.centre, GREEN) &&
|
||
is(seen.right, RED) &&
|
||
!is(seen.left, YELLOW);
|
||
|
||
// 63a. The preview of the Game tab: each camera draws what it sees, the overlay on top.
|
||
const preview = await picture();
|
||
await shot('63-overlay-preview');
|
||
check(
|
||
'each camera draws only its layers, and the overlay lies on top whatever the z',
|
||
stacked(preview),
|
||
JSON.stringify(preview),
|
||
);
|
||
|
||
// 63b. Without Overlay the second camera is a plain one after the first: it draws nothing.
|
||
await mutate(`
|
||
const cam = __gm.state.activeScene.objects.find((o) => o.id === '${OVERLAY}');
|
||
cam.overrides = { ...cam.overrides, overlay: false };
|
||
`);
|
||
await sleep(200);
|
||
const plain = await picture();
|
||
check(
|
||
'two plain cameras switched on draw through the first one only',
|
||
plain.overlays.length === 0 && !is(plain.centre, GREEN) && is(plain.right, RED),
|
||
JSON.stringify(plain),
|
||
);
|
||
await mutate(`
|
||
const cam = __gm.state.activeScene.objects.find((o) => o.id === '${OVERLAY}');
|
||
cam.overrides = { ...cam.overrides, overlay: true };
|
||
`);
|
||
|
||
// 63c. The running game stacks them the same way.
|
||
await play();
|
||
await sleep(400);
|
||
const running = await picture();
|
||
check(
|
||
'the running game lays the overlay camera on top the same way',
|
||
stacked(running),
|
||
JSON.stringify(running),
|
||
);
|
||
await stop();
|
||
|
||
// 63c2. (#304) The mouse through the overlay camera, zoomed 2x: 30 units right of the centre of
|
||
// the picture it lies 15 units right of the overlay's centre, on the 40 wide square B. Through
|
||
// the main camera nothing is there. Logged each tick: hovered through it, without it, its x.
|
||
const node = (id, type, x, params = {}) => ({ id, type, x, y: 0, params });
|
||
const edge = (id, from, fromPort, to, toPort) => ({
|
||
id,
|
||
from: { node: from, port: fromPort },
|
||
to: { node: to, port: toPort },
|
||
});
|
||
const hoverGraph = {
|
||
nodes: [
|
||
node('rl63-tick', 'OnUpdate', 0),
|
||
node('rl63-b-find', 'ObjectByName', 0, { name: 'B 63' }),
|
||
node('rl63-cam-find', 'ObjectByName', 0, { name: 'Overlay 63' }),
|
||
node('rl63-through', 'IsHovered', 200),
|
||
node('rl63-plain', 'IsHovered', 200),
|
||
node('rl63-mouse', 'MousePosition', 200),
|
||
node('rl63-log1', 'Log', 400),
|
||
node('rl63-log2', 'Log', 600),
|
||
node('rl63-log3', 'Log', 800),
|
||
],
|
||
edges: [
|
||
edge('rl63-e1', 'rl63-tick', 'exec', 'rl63-log1', 'exec'),
|
||
edge('rl63-e2', 'rl63-log1', 'exec', 'rl63-log2', 'exec'),
|
||
edge('rl63-e3', 'rl63-log2', 'exec', 'rl63-log3', 'exec'),
|
||
edge('rl63-e4', 'rl63-b-find', 'object', 'rl63-through', 'object'),
|
||
edge('rl63-e5', 'rl63-cam-find', 'object', 'rl63-through', 'camera'),
|
||
edge('rl63-e6', 'rl63-b-find', 'object', 'rl63-plain', 'object'),
|
||
edge('rl63-e7', 'rl63-cam-find', 'object', 'rl63-mouse', 'camera'),
|
||
edge('rl63-e8', 'rl63-through', 'result', 'rl63-log1', 'message'),
|
||
edge('rl63-e9', 'rl63-plain', 'result', 'rl63-log2', 'message'),
|
||
edge('rl63-e10', 'rl63-mouse', 'x', 'rl63-log3', 'message'),
|
||
],
|
||
};
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
p.assets.push({ id: 'rl63-hover', type: 'behavior', name: 'Hover 63', props: { enabled: true, runInEditor: false }, variables: [], graph: ${JSON.stringify(hoverGraph)} });
|
||
${MAKE_OBJECT}
|
||
scene.objects.push(make('rl63-logic', 'rl63-hover', '', 0, 'Hover 63'));
|
||
const cam = scene.objects.find((o) => o.id === '${OVERLAY}');
|
||
window.__rl63.overlay = structuredClone(cam.overrides);
|
||
cam.overrides = { ...cam.overrides, zoom: 2 };
|
||
`);
|
||
await play();
|
||
await sleep(300);
|
||
const game = await json('__gm.gameScale()');
|
||
const centre = { x: game.frame.x + game.frame.w / 2, y: game.frame.y + game.frame.h / 2 };
|
||
await t.mouse('mouseMoved', centre.x + 30 * game.scale, centre.y);
|
||
await sleep(300);
|
||
const ran = await json('__gm.world()');
|
||
const hovered = ran?.log.slice(-3) ?? [];
|
||
const overlayX = ran?.entities.find((e) => e.objectId === OVERLAY)?.worldX;
|
||
await stop();
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
scene.objects = scene.objects.filter((o) => o.id !== 'rl63-logic');
|
||
p.assets = p.assets.filter((a) => a.id !== 'rl63-hover');
|
||
scene.objects.find((o) => o.id === '${OVERLAY}').overrides = window.__rl63.overlay;
|
||
`);
|
||
check(
|
||
'Is hovered through the overlay camera finds what only it shows, through the main one not',
|
||
hovered[0] === 'true' && hovered[1] === 'false' && Math.abs(Number(hovered[2]) - (overlayX + 15)) < 1,
|
||
JSON.stringify({ hovered, overlayX }),
|
||
);
|
||
|
||
// 63c3. (#305) A graph moves the red square A onto `UI` with a Properties node once the game runs:
|
||
// the main camera no longer draws it, and `Render layer of` names its new layer.
|
||
const switchGraph = {
|
||
nodes: [
|
||
node('rl63-start', 'OnStart', 0),
|
||
node('rl63-a-find', 'ObjectByName', 0, { name: 'A 63' }),
|
||
{
|
||
...node('rl63-set', 'Properties', 200, { objectType: 'sprite', renderLayer: 1 }),
|
||
ports: [{ key: 'renderLayer', kind: 'number', write: true }],
|
||
},
|
||
node('rl63-of', 'RenderLayerOf', 200),
|
||
node('rl63-name', 'Log', 400),
|
||
node('rl63-number', 'Log', 600),
|
||
],
|
||
edges: [
|
||
edge('rl63-s1', 'rl63-start', 'exec', 'rl63-set', 'exec'),
|
||
edge('rl63-s2', 'rl63-set', 'exec', 'rl63-name', 'exec'),
|
||
edge('rl63-s3', 'rl63-name', 'exec', 'rl63-number', 'exec'),
|
||
edge('rl63-s4', 'rl63-a-find', 'object', 'rl63-set', 'object'),
|
||
edge('rl63-s5', 'rl63-a-find', 'object', 'rl63-of', 'object'),
|
||
edge('rl63-s6', 'rl63-of', 'name', 'rl63-name', 'message'),
|
||
edge('rl63-s7', 'rl63-of', 'number', 'rl63-number', 'message'),
|
||
],
|
||
};
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
p.assets.push({ id: 'rl63-switch', type: 'behavior', name: 'Switch 63', props: { enabled: true, runInEditor: false }, variables: [], graph: ${JSON.stringify(switchGraph)} });
|
||
${MAKE_OBJECT}
|
||
scene.objects.push(make('rl63-switcher', 'rl63-switch', '', 0, 'Switch 63'));
|
||
`);
|
||
await play();
|
||
await sleep(400);
|
||
const switched = await picture();
|
||
const named = (await json('__gm.world()'))?.log.slice(-2) ?? [];
|
||
await stop();
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
scene.objects = scene.objects.filter((o) => o.id !== 'rl63-switcher');
|
||
p.assets = p.assets.filter((a) => a.id !== 'rl63-switch');
|
||
`);
|
||
check(
|
||
'a graph moves a sprite onto UI while the game runs, and Render layer of names it',
|
||
!switched.main.includes('rl63-a') && !is(switched.right, RED) && JSON.stringify(named) === '["UI","1"]',
|
||
JSON.stringify({ switched, named }),
|
||
);
|
||
|
||
// 63d. The project settings list the render layers, apart from the collision layers.
|
||
await t.clickToolbar('menu');
|
||
await clickText('Project settings…');
|
||
const opened = await json('__gm.renderLayers()');
|
||
await clickText('Add render layer');
|
||
const added = await json('__gm.renderLayers()');
|
||
const collision = await json('__gm.collisionLayers()');
|
||
await press('Escape');
|
||
await sleep(150);
|
||
check(
|
||
'the project settings list the render layers and add one at the end',
|
||
JSON.stringify(opened.dialog) === JSON.stringify(['Default', 'UI']) &&
|
||
JSON.stringify(added.dialog) === JSON.stringify(['Default', 'UI', 'Layer 2']) &&
|
||
!collision.dialog.includes('UI'),
|
||
JSON.stringify({ opened, added, collision }),
|
||
);
|
||
|
||
// 63e. (#299) The main camera wears a shader over its whole picture. One that changes nothing
|
||
// draws the picture byte for byte as before, the right way up: a blue square 100 units above
|
||
// the centre stays above it. At 100 % so the picture's pixels sit on the screen's.
|
||
await showTab('game');
|
||
const scaleBefore = await json('__gm.state.gameScale');
|
||
await mutate(`
|
||
${MAKE_OBJECT}
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
const main = scene.objects.find((o) => o.id === 'o3');
|
||
const sprite = p.assets.find((a) => a.type === 'sprite');
|
||
const o = make('rl63-e', sprite.id, '', 0, 'E 63');
|
||
Object.assign(o.position, { x: main.position.x, y: main.position.y - 100, z: 0 });
|
||
o.overrides = { width: 30, height: 30, color: 0x3366ff, texture: '', shader: '', renderLayer: 0 };
|
||
scene.objects.push(o);
|
||
__gm.state.setGameScale(1);
|
||
`);
|
||
const shaderOf = (id, nodes, edges) => ({
|
||
id,
|
||
type: 'shader',
|
||
name: id,
|
||
props: {},
|
||
graph: { nodes, edges },
|
||
});
|
||
const link = (from, out, to, port) => ({ from: { node: from, port: out }, to: { node: to, port } });
|
||
const PASS = shaderOf('rl63-pass', [{ id: 'out', type: 'ShaderOutput', x: 0, y: 0, params: {} }], []);
|
||
const FLAT_RED = shaderOf(
|
||
'rl63-red',
|
||
[
|
||
{ id: 'red', type: 'ShaderColor', x: 0, y: 0, params: { value: 0xff0000 } },
|
||
{ id: 'one', type: 'ShaderNumber', x: 0, y: 80, params: { value: 1 } },
|
||
{ id: 'out', type: 'ShaderOutput', x: 200, y: 0, params: {} },
|
||
],
|
||
[link('red', 'color', 'out', 'color'), link('one', 'value', 'out', 'alpha')],
|
||
);
|
||
// Red is the width, green the height, both in thousandths: 640 × 360 comes out as 163 / 92.
|
||
const SIZE = shaderOf(
|
||
'rl63-size',
|
||
[
|
||
{ id: 'size', type: 'ShaderTextureSize', x: 0, y: 0, params: {} },
|
||
{ id: 'red', type: 'ShaderColor', x: 0, y: 80, params: { value: 0xff0000 } },
|
||
{ id: 'green', type: 'ShaderColor', x: 0, y: 160, params: { value: 0x00ff00 } },
|
||
{ id: 'milli', type: 'ShaderNumber', x: 0, y: 240, params: { value: 0.001 } },
|
||
{ id: 'one', type: 'ShaderNumber', x: 0, y: 320, params: { value: 1 } },
|
||
{ id: 'w', type: 'ShaderMultiply', x: 200, y: 0, params: {} },
|
||
{ id: 'h', type: 'ShaderMultiply', x: 200, y: 160, params: {} },
|
||
{ id: 'sum', type: 'ShaderAdd', x: 400, y: 0, params: {} },
|
||
{ id: 'scaled', type: 'ShaderMultiply', x: 600, y: 0, params: {} },
|
||
{ id: 'out', type: 'ShaderOutput', x: 800, y: 0, params: {} },
|
||
],
|
||
[
|
||
link('red', 'color', 'w', 'a'),
|
||
link('size', 'width', 'w', 'b'),
|
||
link('green', 'color', 'h', 'a'),
|
||
link('size', 'height', 'h', 'b'),
|
||
link('w', 'result', 'sum', 'a'),
|
||
link('h', 'result', 'sum', 'b'),
|
||
link('sum', 'result', 'scaled', 'a'),
|
||
link('milli', 'value', 'scaled', 'b'),
|
||
link('scaled', 'result', 'out', 'color'),
|
||
link('one', 'value', 'out', 'alpha'),
|
||
],
|
||
);
|
||
/** Puts `shader` on the main camera (null takes it off), with `resolution` as its row. */
|
||
const wear = (shader, resolution = 'screen') =>
|
||
mutate(`
|
||
const p = __gm.state.project;
|
||
p.assets = p.assets.filter((a) => !a.id.startsWith('rl63-'));
|
||
const shader = ${JSON.stringify(shader)};
|
||
if (shader) p.assets.push(shader);
|
||
const main = __gm.state.activeScene.objects.find((o) => o.id === 'o3');
|
||
main.overrides = { ...main.overrides, shader: shader ? shader.id : '', shaderResolution: '${resolution}' };
|
||
`);
|
||
/** Two rows of the picture, through the centre and through the blue square, and the square's spot. */
|
||
const rows = async () => {
|
||
const { frame, scale } = await json('__gm.gameScale()');
|
||
const cx = Math.round(frame.x + frame.w / 2);
|
||
const cy = Math.round(frame.y + frame.h / 2);
|
||
const row = (dy) =>
|
||
json(`__gm.pixelRow(${Math.round(frame.x)}, ${Math.round(cy + dy * scale)}, ${Math.round(frame.w)})`);
|
||
return {
|
||
centre: await row(0),
|
||
above: await row(-100),
|
||
blueAbove: await json(`__gm.pixelAt(${cx}, ${Math.round(cy - 100 * scale)})`),
|
||
blueBelow: await json(`__gm.pixelAt(${cx}, ${Math.round(cy + 100 * scale)})`),
|
||
pixels: await json('__gm.cameraShaderPixels()'),
|
||
};
|
||
};
|
||
const same = (a, b) => JSON.stringify([a.centre, a.above]) === JSON.stringify([b.centre, b.above]);
|
||
const unshaded = await rows();
|
||
await wear(PASS);
|
||
const handedThrough = await rows();
|
||
const differing = handedThrough.above.filter((hex, i) => hex !== unshaded.above[i]).length;
|
||
check(
|
||
'a camera shader that changes nothing draws the game picture byte for byte, the right way up',
|
||
unshaded.pixels === null &&
|
||
handedThrough.pixels !== null &&
|
||
is(unshaded.blueAbove, [0x33, 0x66, 0xff]) &&
|
||
!is(unshaded.blueBelow, [0x33, 0x66, 0xff]) &&
|
||
same(handedThrough, unshaded),
|
||
JSON.stringify({ differing, pixels: handedThrough.pixels, blue: handedThrough.blueAbove }),
|
||
);
|
||
|
||
// 63f. A flat red fills the main camera's whole picture, corners included; the overlay camera's
|
||
// UI square lies on top of it in its own green (the proof moved here from #302).
|
||
await wear(FLAT_RED);
|
||
const { frame } = await json('__gm.gameScale()');
|
||
const corners = await json(
|
||
`__gm.pixels(${JSON.stringify([
|
||
{ x: frame.x + 2, y: frame.y + 2 },
|
||
{ x: frame.x + frame.w - 3, y: frame.y + 2 },
|
||
{ x: frame.x + 2, y: frame.y + frame.h - 3 },
|
||
{ x: frame.x + frame.w - 3, y: frame.y + frame.h - 3 },
|
||
])})`,
|
||
);
|
||
const reddened = await picture();
|
||
await shot('63-camera-shader-red');
|
||
check(
|
||
'a flat red fills the whole game picture, and the overlay camera’s UI keeps its colour',
|
||
corners.every((hex) => is(hex, RED)) && is(reddened.left, RED) && is(reddened.centre, GREEN),
|
||
JSON.stringify({ corners, reddened }),
|
||
);
|
||
|
||
// 63g. `Texture size` is the camera's raster in either resolution – a pixel counts as a camera
|
||
// pixel (the user's choice, #299) – while Screen pixels draws the picture with as many pixels
|
||
// as it covers on screen. Fitted, so that is not 640 by chance.
|
||
await mutate(`__gm.state.setGameScale('fit')`);
|
||
const measured = {};
|
||
for (const resolution of ['screen', 'camera']) {
|
||
await wear(SIZE, resolution);
|
||
measured[resolution] = {
|
||
colour: (await picture()).left,
|
||
pixels: await json('__gm.cameraShaderPixels()'),
|
||
};
|
||
}
|
||
const fitted = await json('__gm.gameScale()');
|
||
const ratio = (await json('__gm.pixelGrid()')).resolution;
|
||
// Tight: counted in screen pixels the fitted 670 would come out as 171 / 96, close to the real thing.
|
||
const exactly = (hex, rgb) => channels(hex).every((v, i) => Math.abs(v - rgb[i]) <= 2);
|
||
check(
|
||
'Texture size is View width × View height in both resolutions, and only Camera pixels draws 640 × 360',
|
||
['screen', 'camera'].every((r) => exactly(measured[r].colour, [163, 92, 0])) &&
|
||
measured.camera.pixels?.w === 640 &&
|
||
measured.camera.pixels?.h === 360 &&
|
||
measured.screen.pixels?.w === Math.round(fitted.frame.w * ratio) &&
|
||
measured.screen.pixels?.w !== 640,
|
||
JSON.stringify({ measured, frame: fitted.frame, ratio }),
|
||
);
|
||
|
||
// 63h. Taken off again, the camera draws as before and lays no picture over its view.
|
||
await mutate(`__gm.state.setGameScale(1)`);
|
||
await wear(null);
|
||
const back = await rows();
|
||
check(
|
||
'a camera without a shader draws as it did before',
|
||
back.pixels === null && same(back, unshaded),
|
||
JSON.stringify({ pixels: back.pixels }),
|
||
);
|
||
|
||
// 63i. The scene view lays the shader over the main camera's frame only while the eye's Shader
|
||
// switch is on – a real click on it – and never past the frame.
|
||
await wear(FLAT_RED);
|
||
await showTab('scene');
|
||
await evaluate(`(() => {
|
||
const main = __gm.state.activeScene.objects.find((o) => o.id === 'o3');
|
||
__gm.lookAt(main.position.x, main.position.y);
|
||
})()`);
|
||
await sleep(150);
|
||
const sceneSpots = async () => {
|
||
const r = await json('__gm.objectRect("o3")');
|
||
const [inside, outside] = await json(
|
||
`__gm.pixels(${JSON.stringify([
|
||
{ x: Math.round(r.x - r.w * 0.3), y: Math.round(r.y - r.h * 0.3) },
|
||
{ x: Math.round(r.x - r.w * 0.7), y: Math.round(r.y - r.h * 0.3) },
|
||
])})`,
|
||
);
|
||
return { inside, outside, pixels: await json(`__gm.cameraShaderPixels('scene')`) };
|
||
};
|
||
const switchedOff = await sceneSpots();
|
||
const eye = await json(`__gm.sceneHeaderAt('visibility')`);
|
||
await t.click(eye.x, eye.y);
|
||
const shaderSwitch = (await json('__gm.visibilityPopup()')).find(
|
||
(e) => e.type === 'camera' && e.part === 'shader',
|
||
);
|
||
await t.click(shaderSwitch.x, shaderSwitch.y);
|
||
await press('Escape');
|
||
await sleep(150);
|
||
const switchedOn = await sceneSpots();
|
||
await shot('63-camera-shader-scene');
|
||
check(
|
||
'the scene view shows the camera shader over the frame only while the eye switch is on',
|
||
shaderSwitch.on === false &&
|
||
switchedOff.pixels === null &&
|
||
!is(switchedOff.inside, RED) &&
|
||
switchedOn.pixels !== null &&
|
||
is(switchedOn.inside, RED) &&
|
||
!is(switchedOn.outside, RED),
|
||
JSON.stringify({ switchedOff, switchedOn, shaderSwitch }),
|
||
);
|
||
|
||
// Clean up.
|
||
await showTab('scene');
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
scene.objects = scene.objects.filter((o) => !o.id.startsWith('rl63-'));
|
||
scene.objects.find((o) => o.id === 'o3').overrides = window.__rl63.overrides;
|
||
p.renderLayers = window.__rl63.layers;
|
||
p.assets = p.assets.filter((a) => !a.id.startsWith('rl63-'));
|
||
__gm.state.setSceneVisibility('camera', 'shader', false);
|
||
__gm.state.setGameScale(${JSON.stringify(scaleBefore)});
|
||
`);
|
||
}
|