The game view took whatever camera came first in the scene, ignoring the switch, so `enabled = false` on a camera did nothing and a second camera never got its turn. `World.activeCamera()` now decides: of the cameras switched on – the whole parent chain counted, as for any object – the first in hierarchy order, and none on renders nothing at all. It is asked per frame, so a graph switching cameras hands the view over immediately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
83 lines
3.2 KiB
TypeScript
83 lines
3.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { CreateAssetCommand } from '../../src/core/commands/assetCommands';
|
||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||
import { treeOrder } from '../../src/core/model/hierarchy';
|
||
import { makeSpriteScene, place, startWorld } from '../helpers';
|
||
|
||
/** The fixture's scene with a camera asset, and switching a placed object off the way the inspector does. */
|
||
function sceneWithCamera() {
|
||
const fixture = makeSpriteScene();
|
||
const camera = new CreateAssetCommand('camera', 'Cam');
|
||
fixture.stack.execute(camera);
|
||
return {
|
||
...fixture,
|
||
cameraId: camera.asset.id,
|
||
switchOff: (objectId: string) =>
|
||
fixture.stack.execute(new SetOverrideCommand(fixture.sceneId, objectId, 'enabled', false)),
|
||
};
|
||
}
|
||
|
||
describe('the camera the game is seen through', () => {
|
||
it('skips a camera that is switched off, so nothing is rendered while it is the only one', () => {
|
||
const f = sceneWithCamera();
|
||
const cam = place(f, f.cameraId, { x: 0, y: 0 });
|
||
const { world } = startWorld(f);
|
||
expect(world.activeCamera()?.objectId).toBe(cam);
|
||
|
||
// Switching it off reaches the running world through the same sync an inspector edit uses.
|
||
f.switchOff(cam);
|
||
world.syncFromModel();
|
||
expect(world.activeCamera()).toBeUndefined();
|
||
|
||
f.stack.undo();
|
||
world.syncFromModel();
|
||
expect(world.activeCamera()?.objectId).toBe(cam);
|
||
});
|
||
|
||
it('is off as well when a parent above it is off', () => {
|
||
const f = sceneWithCamera();
|
||
const holder = place(f, f.spriteId, { x: 0, y: 0 });
|
||
const cam = place(f, f.cameraId, { x: 0, y: 0 }, holder);
|
||
const { world } = startWorld(f);
|
||
expect(world.activeCamera()?.objectId).toBe(cam);
|
||
|
||
f.switchOff(holder);
|
||
world.syncFromModel();
|
||
expect(world.activeCamera()).toBeUndefined();
|
||
});
|
||
|
||
it('takes the first switched-on one, and hands the view over when that one goes off', () => {
|
||
const f = sceneWithCamera();
|
||
const first = place(f, f.cameraId, { x: 0, y: 0 });
|
||
const second = place(f, f.cameraId, { x: 500, y: 0 });
|
||
const { world } = startWorld(f);
|
||
expect(world.activeCamera()?.objectId).toBe(first);
|
||
|
||
f.switchOff(first);
|
||
world.syncFromModel();
|
||
expect(world.activeCamera()?.objectId).toBe(second);
|
||
|
||
// And back: the order decides again as soon as both are on.
|
||
f.stack.undo();
|
||
world.syncFromModel();
|
||
expect(world.activeCamera()?.objectId).toBe(first);
|
||
|
||
f.switchOff(second);
|
||
world.syncFromModel();
|
||
expect(world.activeCamera()?.objectId).toBe(first);
|
||
});
|
||
|
||
it('reads "first" in hierarchy order – what the panel shows – not in storage order', () => {
|
||
const f = sceneWithCamera();
|
||
const holder = place(f, f.spriteId, { x: 0, y: 0 });
|
||
const stored = place(f, f.cameraId, { x: 500, y: 0 });
|
||
const nested = place(f, f.cameraId, { x: 0, y: 0 }, holder);
|
||
// The nested camera is stored last but listed first, right below its parent.
|
||
expect(f.sceneOf().objects.map((o) => o.id)).toEqual([holder, stored, nested]);
|
||
expect(treeOrder(f.sceneOf()).map(({ object }) => object.id)).toEqual([holder, nested, stored]);
|
||
|
||
const { world } = startWorld(f);
|
||
expect(world.activeCamera()?.objectId).toBe(nested);
|
||
});
|
||
});
|