Play through the first camera that is switched on

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>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-09 16:13:30 +02:00
1 parent 75be133374
commit 37fff5dabf
8 files changed
+227 -13

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
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);
});
});