A camera could only show the whole scene, so a HUD drawn in the world would run through a camera shader (#299) with everything else. Render layers say which cameras see an object, and overlay cameras draw their own picture over the main one. - The project has a list of up to 16 render layers, edited in Project settings next to the collision layers. It is a list of its own, read by the same rules (`layerNamesOf`). - Sprites, texts, tilemaps, particle sources and containers carry a row `Render layer` under their switches. Its default `Inherit` takes the parent's layer, at the top the first one. - A camera has `Sees` (all layers by default) and `Overlay`. The main camera is the first one switched on without Overlay, so two plain cameras still draw through the first one only. - Every view through a camera keeps only what that camera sees and lays one see-through view per overlay camera over itself. That view is a `WorldViewport` of its own, handed its view and frame from below. - An overlay is fitted into the main camera's whole picture without distortion. One of the same size and zoom covers it exactly. - `z` sorts within one camera only. The pictures lie on top of each other as a whole, which is what a shader per camera (#299) needs. - The Game tab's preview now collects the scene once per redraw. Before it collected it twice, with the cameras it would have been five times. - `SetCollisionLayersCommand` became `SetLayerNamesCommand`, which takes the list it sets. Follow-ups: #304 (the mouse through a chosen camera) and #305 (change the render layer at run time). Tests: - Gate: 226 files, 3487 tests (3480 + 7) - e2e 63 (new): 4 checks. A green overlay sprite at z -10 covers a red main sprite at z 1000, in the preview and in the running game. A `UI` sprite in front of the main camera and a `Default` one in front of the overlay stay away. Without Overlay the second camera draws nothing. The settings dialog adds a render layer at the end. - Counter-checks: inheriting from parents switched off fails 1 unit test. Overlays never drawn fails the two overlay checks in e2e 63. - e2e total 1720 (1716 + 4), computed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
4.2 KiB
TypeScript
98 lines
4.2 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { CreateAssetCommand, CreateTextureCommand } from '../../src/core/commands/assetCommands';
|
||
import { CommandError } from '../../src/core/commands/Command';
|
||
import { findAsset } from '../../src/core/model/Project';
|
||
import { spriteFromTexture } from '../../src/core/properties/aspect';
|
||
import { makeProject } from '../helpers';
|
||
|
||
const FILE = `${'b'.repeat(64)}.webp`;
|
||
|
||
describe('CreateTextureCommand', () => {
|
||
it('adds a texture asset carrying the uploaded image, and removes it again', () => {
|
||
const { project, stack } = makeProject();
|
||
const command = new CreateTextureCommand('Hero', { file: FILE, width: 64, height: 32 });
|
||
stack.execute(command);
|
||
|
||
const asset = findAsset(project, command.asset.id);
|
||
expect(asset).toMatchObject({ type: 'texture', name: 'Hero', file: FILE, width: 64, height: 32 });
|
||
// The schema default comes along, so the new texture is a complete asset – nothing to fill in later.
|
||
expect(asset?.props).toEqual({ filter: 'linear', sliceScale: 1 });
|
||
|
||
stack.undo();
|
||
expect(project.assets).toHaveLength(0);
|
||
stack.redo();
|
||
expect(findAsset(project, command.asset.id)?.name).toBe('Hero');
|
||
});
|
||
|
||
it('keeps the caller’s name even though the upload result carries one of its own', () => {
|
||
const { project, stack } = makeProject();
|
||
// What `TextureStore.upload` actually answers: the file's original name and its byte count
|
||
// ride along. Copied wholesale they would land in the asset and rename it behind the caller.
|
||
const upload = { file: FILE, name: 'hero-idle.png', width: 64, height: 32, bytes: 900 };
|
||
const command = new CreateTextureCommand('Hero', upload);
|
||
stack.execute(command);
|
||
|
||
const asset = findAsset(project, command.asset.id);
|
||
expect(asset?.name).toBe('Hero');
|
||
expect(asset).not.toHaveProperty('bytes');
|
||
});
|
||
|
||
it('rejects a file name the server cannot have produced – in execute, not in the constructor', () => {
|
||
const { project, stack } = makeProject();
|
||
const command = new CreateTextureCommand('Hero', { file: '../secrets.png', width: 1, height: 1 });
|
||
expect(() => stack.execute(command)).toThrow(CommandError);
|
||
expect(project.assets).toHaveLength(0);
|
||
});
|
||
|
||
it('keeps two assets for the same file apart', () => {
|
||
const { project, stack } = makeProject();
|
||
stack.execute(new CreateTextureCommand('Hero', { file: FILE, width: 8, height: 8 }));
|
||
stack.execute(new CreateTextureCommand('Hero copy', { file: FILE, width: 8, height: 8 }));
|
||
expect(project.assets.map((a) => a.id)).toHaveLength(2);
|
||
expect(new Set(project.assets.map((a) => a.id)).size).toBe(2);
|
||
});
|
||
});
|
||
|
||
describe('CreateAssetCommand with starting props', () => {
|
||
it('creates a sprite wearing the texture at its pixel size, and removes it again', () => {
|
||
const { project, stack } = makeProject();
|
||
const texture = new CreateTextureCommand('Hero', { file: FILE, width: 64, height: 32 });
|
||
stack.execute(texture);
|
||
const sprite = new CreateAssetCommand('sprite', 'Hero 2', spriteFromTexture(project, texture.asset.id));
|
||
stack.execute(sprite);
|
||
|
||
expect(findAsset(project, sprite.asset.id)?.props).toEqual({
|
||
enabled: true,
|
||
keepOnSceneLoad: false,
|
||
scale: 1,
|
||
rotation: 0,
|
||
mirrorX: false,
|
||
mirrorY: false,
|
||
sortWithParent: true,
|
||
renderLayer: -1,
|
||
width: 64,
|
||
height: 32,
|
||
color: 0xffffff,
|
||
texture: texture.asset.id,
|
||
shader: '',
|
||
});
|
||
stack.undo();
|
||
expect(findAsset(project, sprite.asset.id)).toBeUndefined();
|
||
stack.redo();
|
||
expect(findAsset(project, sprite.asset.id)?.props.texture).toBe(texture.asset.id);
|
||
});
|
||
|
||
it('passes each value through the schema', () => {
|
||
const { project, stack } = makeProject();
|
||
const sprite = new CreateAssetCommand('sprite', 'Tiny', { width: -5 });
|
||
stack.execute(sprite);
|
||
expect(findAsset(project, sprite.asset.id)?.props.width).toBe(1);
|
||
});
|
||
|
||
it('rejects a property the type does not have, leaving the project untouched', () => {
|
||
const { project, stack } = makeProject();
|
||
expect(() => stack.execute(new CreateAssetCommand('sprite', 'Odd', { zoom: 2 }))).toThrow(CommandError);
|
||
expect(project.assets).toHaveLength(0);
|
||
});
|
||
});
|