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 { cameraSees, INHERIT_LAYER, OVERLAY_KEY, ownRenderLayer, RENDER_LAYER_KEY, renderLayersOf, SEES_KEY, } 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 { collectHolderItems, collectWorldItems } from '../../src/viewport/items'; import { cameraView, overlayView } from '../../src/viewport/view'; import { makeSpriteScene, place, startWorld } from '../helpers'; function sceneWithCamera() { const fixture = makeSpriteScene(); const camera = new CreateAssetCommand('camera', 'Cam'); fixture.stack.execute(camera); const container = new CreateAssetCommand('container', 'Hud'); fixture.stack.execute(container); return { ...fixture, cameraId: camera.asset.id, containerId: container.asset.id, set: (objectId: string, key: string, value: number | boolean) => fixture.stack.execute(new SetOverrideCommand(fixture.sceneId, objectId, key, value)), items: () => collectHolderItems(fixture.project, fixture.sceneOf(), 0).items, }; } describe('render layers (#302)', () => { it('reads the names like the collision layers, "Default" for a project from before', () => { expect(renderLayersOf(null)).toEqual(['Default']); expect(renderLayersOf({ renderLayers: ['World', '', 'UI'] })).toEqual(['World', 'Layer 1', 'UI']); const f = sceneWithCamera(); const stored = serializeProject(f.project) as Record; delete stored.renderLayers; expect(deserializeProject(stored).renderLayers).toEqual(['Default']); }); it('reads an own choice, and anything unreadable as none', () => { for (const value of [undefined, null, '', ' ', 'x', -1, -5, 16, true]) expect(ownRenderLayer(value)).toBe(INHERIT_LAYER); expect(ownRenderLayer(0)).toBe(0); expect(ownRenderLayer(3)).toBe(3); expect(ownRenderLayer('2')).toBe(2); }); it('puts an object without a choice on its parent’s layer, and the top on layer 0', () => { const f = sceneWithCamera(); const hud = place(f, f.containerId, { x: 0, y: 0 }); const inside = place(f, f.spriteId, { x: 0, y: 0 }, hud); const own = place(f, f.spriteId, { x: 0, y: 0 }, hud); const loose = place(f, f.spriteId, { x: 0, y: 0 }); f.set(hud, RENDER_LAYER_KEY, 1); f.set(own, RENDER_LAYER_KEY, 2); const layerOf = (id: string) => f.items().find((item) => item.id === id)!.renderLayer; expect([layerOf(hud), layerOf(inside), layerOf(own), layerOf(loose)]).toEqual([1, 1, 2, 0]); const { world } = startWorld(f); const running = (id: string) => collectWorldItems(world).items.find((item) => item.id === id)!.renderLayer; expect([running(inside), running(own), running(loose)]).toEqual([1, 2, 0]); }); it('lets a camera without `Sees` see every layer, and one with it only those', () => { expect(cameraSees({}, 0)).toBe(true); expect(cameraSees({}, 15)).toBe(true); expect(cameraSees({ [SEES_KEY]: 0b10 }, 0)).toBe(false); expect(cameraSees({ [SEES_KEY]: 0b10 }, 1)).toBe(true); }); it('keeps the first camera without Overlay as the main one, the overlays apart in hierarchy order', () => { const f = sceneWithCamera(); const over = place(f, f.cameraId, { x: 0, y: 0 }); const main = place(f, f.cameraId, { x: 100, y: 0 }); const plain = place(f, f.cameraId, { x: 200, y: 0 }); const over2 = place(f, f.cameraId, { x: 300, y: 0 }); f.set(over, OVERLAY_KEY, true); f.set(over2, OVERLAY_KEY, true); const { world } = startWorld(f); expect(world.activeCamera()?.objectId).toBe(main); expect(world.overlayCameras().map((c) => c.objectId)).toEqual([over, over2]); expect(overlaysOfWorld(world).map((c) => c.x)).toEqual([0, 300]); expect(cameraOfItems(f.items())?.id).toBe(main); expect(overlaysOfItems(f.items()).map((c) => c.id)).toEqual([over, over2]); // Two plain cameras on draw as before: through the first only, the second is no overlay. expect(world.overlayCameras().map((c) => c.objectId)).not.toContain(plain); f.set(over2, 'enabled', false); world.syncFromModel(); expect(world.overlayCameras().map((c) => c.objectId)).toEqual([over]); }); it('fits an overlay into the main picture, and gives the same camera the very same view', () => { const props = { width: 640, height: 360, zoom: 1 }; const main = cameraView({ x: 0, y: 0, props }, 1280, 720, 2); expect(overlayView({ x: 0, y: 0, props }, main, { w: 640, h: 360 })).toEqual(main); // Narrower and zoomed: the frame is 160x180 world units, fitted to the 720 px height. const narrow = overlayView({ x: 50, y: 7, props: { width: 320, height: 360, zoom: 2 } }, main, { w: 640, h: 360, }); expect(narrow).toEqual({ x: 50, y: 7, scale: 4, rect: { x: 320, y: 0, w: 640, h: 720 } }); }); it('keeps the render layers an undo step and a change of the settings of their own', () => { const f = sceneWithCamera(); f.stack.execute(new SetLayerNamesCommand('renderLayers', ['World', 'UI'])); expect(f.project.renderLayers).toEqual(['World', 'UI']); expect(f.project.collisionLayers).toEqual(['Default']); f.stack.undo(); expect(f.project.renderLayers).toEqual(['Default']); const before = { name: 'P', playFrom: 'start' as const, whilePlaying: 'live' as const, gameLinkZoom: 'auto' as const, collisionLayers: ['Default'], renderLayers: ['Default'], symbolScale: 1, }; expect(settingsChanges(before, { ...before, renderLayers: ['Default'] })).toEqual({}); expect(settingsChanges(before, { ...before, renderLayers: ['Default', 'UI'] })).toEqual({ renderLayers: ['Default', 'UI'], }); }); });