Add a node that names the render layer of an object (#305)
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>
This commit is contained in:
1 parent
70f3608733
commit
d9c6bbfa4d
12 files changed
+250
-8
No files matched your search
@@ -609,6 +609,28 @@ const CASES: Record<string, Case[]> = {
|
||||
},
|
||||
{ name: 'no object is on no layer', inputs: { object: null }, outputs: { name: '', number: -1 } },
|
||||
],
|
||||
RenderLayerOf: [
|
||||
{
|
||||
name: 'names the render layer the context says the object is drawn on (#305)',
|
||||
inputs: { object: fakeEntity('Coin') },
|
||||
context: { renderLayerOf: (target) => (target.name === 'Coin' ? 1 : null) },
|
||||
lookup: { ...NO_LOOKUP, renderLayers: () => ['Default', 'UI'] },
|
||||
outputs: { name: 'UI', number: 1 },
|
||||
},
|
||||
{
|
||||
name: 'a render layer the project has not named goes by its number',
|
||||
inputs: { object: fakeEntity('Coin') },
|
||||
context: { renderLayerOf: () => 3 },
|
||||
lookup: { ...NO_LOOKUP, renderLayers: () => ['Default'] },
|
||||
outputs: { name: 'Layer 3', number: 3 },
|
||||
},
|
||||
{
|
||||
name: 'an object that is not drawn is on no render layer',
|
||||
inputs: { object: fakeEntity('Cam') },
|
||||
outputs: { name: '', number: -1 },
|
||||
},
|
||||
{ name: 'no object is on no render layer', inputs: { object: null }, outputs: { name: '', number: -1 } },
|
||||
],
|
||||
ObjectsByName: [
|
||||
{
|
||||
name: 'every object so named, in scene order',
|
||||
|
||||
@@ -244,6 +244,7 @@ export function fakeContext(overrides: Partial<NodeContext> = {}): FakeContext {
|
||||
raycast: () => null,
|
||||
raycastAll: () => [],
|
||||
collisionLayerOf: () => null,
|
||||
renderLayerOf: () => null,
|
||||
ignoreOneWay: () => {},
|
||||
isTilemap: () => false,
|
||||
tileAt: () => null,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
|
||||
import { SetLayerNamesCommand } from '../../src/core/commands/projectCommands';
|
||||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||||
import { PlaceAssetCommand, SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||||
import type { GraphNode } from '../../src/core/graph';
|
||||
import { OBJECT_TYPE_PARAM } from '../../src/core/graph/propertyPorts';
|
||||
import type { PropertyBag } from '../../src/core/model/Properties';
|
||||
import {
|
||||
cameraSees,
|
||||
INHERIT_LAYER,
|
||||
@@ -13,10 +16,15 @@ import {
|
||||
} from '../../src/core/model/renderLayers';
|
||||
import { deserializeProject, serializeProject } from '../../src/core/store/serialize';
|
||||
import { settingsChanges } from '../../src/editor/ProjectSettingsDialog';
|
||||
import { cameraOfItems, overlaysOfItems, overlaysOfWorld } from '../../src/viewport/activeCamera';
|
||||
import {
|
||||
cameraOfItems,
|
||||
cameraOfWorld,
|
||||
overlaysOfItems,
|
||||
overlaysOfWorld,
|
||||
} from '../../src/viewport/activeCamera';
|
||||
import { collectHolderItems, collectWorldItems } from '../../src/viewport/items';
|
||||
import { cameraView, overlayView } from '../../src/viewport/view';
|
||||
import { makeSpriteScene, place, startWorld } from '../helpers';
|
||||
import { type GraphEditor, graphEditor, makeBehavior, makeSpriteScene, place, startWorld } from '../helpers';
|
||||
|
||||
function sceneWithCamera() {
|
||||
const fixture = makeSpriteScene();
|
||||
@@ -134,3 +142,116 @@ describe('render layers (#302)', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* A coin on `Default` and a HUD container with a sprite in it, a main camera that sees `Default` and
|
||||
* an overlay that sees `UI`. Behaviors change the layers with Properties nodes, the way a graph does
|
||||
* it (#305): the coin goes to `UI` on `u` and back to Inherit on `i`, the HUD to `UI` on `h`, and the
|
||||
* main camera sees both layers on `s`. On `l` the HUD's sprite asks `Render layer of` of itself, of
|
||||
* the HUD and of its behavior, which is not drawn.
|
||||
*/
|
||||
function layerSwitchScene() {
|
||||
const f = sceneWithCamera();
|
||||
f.stack.execute(new SetLayerNamesCommand('renderLayers', ['Default', 'UI']));
|
||||
const coin = place(f, f.spriteId, { x: 0, y: 0 });
|
||||
const hud = place(f, f.containerId, { x: 0, y: 0 });
|
||||
const inside = place(f, f.spriteId, { x: 0, y: 0 }, hud);
|
||||
const main = place(f, f.cameraId, { x: 0, y: 0 });
|
||||
const over = place(f, f.cameraId, { x: 0, y: 0 });
|
||||
f.set(main, SEES_KEY, 0b01);
|
||||
f.set(over, SEES_KEY, 0b10);
|
||||
f.set(over, OVERLAY_KEY, true);
|
||||
|
||||
const behaviorOn = (objectId: string, name: string) => {
|
||||
const behavior = makeBehavior(f, name);
|
||||
f.stack.execute(new PlaceAssetCommand(f.sceneId, behavior.id, { x: 0, y: 0 }, objectId));
|
||||
return graphEditor(f, behavior);
|
||||
};
|
||||
const setOnKey = (edit: GraphEditor, key: string, type: string, port: string, value: number) => {
|
||||
const set = edit.properties([{ key: port, kind: 'number', write: true }], {
|
||||
[OBJECT_TYPE_PARAM]: type,
|
||||
[port]: value,
|
||||
});
|
||||
edit.connect(edit.addNode('Owner'), 'object', set, 'object');
|
||||
edit.connect(edit.addNode('OnKeyDown', { key }), 'exec', set, 'exec');
|
||||
};
|
||||
const onCoin = behaviorOn(coin, 'Coin');
|
||||
setOnKey(onCoin, 'u', 'sprite', RENDER_LAYER_KEY, 1);
|
||||
setOnKey(onCoin, 'i', 'sprite', RENDER_LAYER_KEY, INHERIT_LAYER);
|
||||
const onHud = behaviorOn(hud, 'Hud');
|
||||
setOnKey(onHud, 'h', 'container', RENDER_LAYER_KEY, 1);
|
||||
const onCamera = behaviorOn(main, 'Cam');
|
||||
setOnKey(onCamera, 's', 'camera', SEES_KEY, 0b11);
|
||||
|
||||
const onInside = behaviorOn(inside, 'Inside');
|
||||
let from = onInside.addNode('OnKeyDown', { key: 'l' });
|
||||
const ask = (source: GraphNode, port: string) => {
|
||||
const of = onInside.addNode('RenderLayerOf');
|
||||
onInside.connect(source, port, of, 'object');
|
||||
for (const output of ['name', 'number']) {
|
||||
const log = onInside.addNode('Log');
|
||||
onInside.connect(of, output, log, 'message');
|
||||
onInside.connect(from, 'exec', log, 'exec');
|
||||
from = log;
|
||||
}
|
||||
};
|
||||
const owner = onInside.addNode('Owner');
|
||||
const parent = onInside.addNode('Parent');
|
||||
onInside.connect(owner, 'object', parent, 'object');
|
||||
ask(owner, 'object');
|
||||
ask(parent, 'parent');
|
||||
ask(onInside.addNode('Self'), 'object');
|
||||
return { ...f, coin, hud, inside };
|
||||
}
|
||||
|
||||
describe('a graph changes render layers and what a camera sees during the run (#305)', () => {
|
||||
const start = () => {
|
||||
const s = layerSwitchScene();
|
||||
const started = startWorld(s, { behaviors: true });
|
||||
const press = (key: string) => {
|
||||
started.input.press(key);
|
||||
started.frame();
|
||||
started.input.release(key);
|
||||
};
|
||||
const seenBy = (camera: { props: PropertyBag } | null) => {
|
||||
const items = collectWorldItems(started.world).items.filter((item) => item.type === 'sprite');
|
||||
const seen = items.filter((item) => camera && cameraSees(camera.props, item.renderLayer));
|
||||
return seen.map((item) => (item.id === s.coin ? 'coin' : item.id === s.inside ? 'inside' : item.id));
|
||||
};
|
||||
const main = () => seenBy(cameraOfWorld(started.world));
|
||||
const overlay = () => seenBy(overlaysOfWorld(started.world)[0]!);
|
||||
started.frame();
|
||||
return { ...s, ...started, press, main, overlay };
|
||||
};
|
||||
|
||||
it('moves the coin into the overlay with Set, and back with Inherit', () => {
|
||||
const r = start();
|
||||
expect([r.main(), r.overlay()]).toEqual([['coin', 'inside'], []]);
|
||||
r.press('u');
|
||||
expect([r.main(), r.overlay()]).toEqual([['inside'], ['coin']]);
|
||||
r.press('i');
|
||||
expect([r.main(), r.overlay()]).toEqual([['coin', 'inside'], []]);
|
||||
});
|
||||
|
||||
it('takes the HUD container’s sprite along when the container changes', () => {
|
||||
const r = start();
|
||||
r.press('h');
|
||||
expect([r.main(), r.overlay()]).toEqual([['coin'], ['inside']]);
|
||||
});
|
||||
|
||||
it('lets the main camera see a second layer once its Sees is set', () => {
|
||||
const r = start();
|
||||
r.press('h');
|
||||
r.press('s');
|
||||
expect(r.main()).toEqual(['coin', 'inside']);
|
||||
});
|
||||
|
||||
it('names the layer that holds, the inherited one too, and none for what is not drawn', () => {
|
||||
const r = start();
|
||||
r.press('l');
|
||||
expect(r.world.log).toEqual(['Default', '0', 'Default', '0', '', '-1']);
|
||||
r.press('h');
|
||||
r.press('l');
|
||||
expect(r.world.log.slice(6)).toEqual(['UI', '1', 'UI', '1', '', '-1']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user