import { describe, expect, it } from "vitest"; import { createHydrationModelAsset } from "../model-import/model-persistence-hydration-test-support"; import { defineToolcraft } from "../schema/define-toolcraft"; import { createToolcraftState } from "../state/create-template-state"; import { getToolcraftSceneElementRect, outwardRoundToolcraftSceneRect, resolveToolcraftProductSceneBounds, resolveToolcraftSceneBounds, unionToolcraftSceneRects, } from "./scene-bounds"; function emptyState() { return createToolcraftState( defineToolcraft({ canvas: { enabled: true }, panels: {} }), ); } describe("scene bounds", () => { it("resolves one exact product frame before runtime contributors are added", () => { expect( resolveToolcraftProductSceneBounds([ { height: 20, width: 40, x: -10, y: -5 }, { height: 30, width: 10, x: 35, y: 15 }, ]), ).toEqual({ bounds: { height: 50, width: 55, x: -10, y: -5 }, ok: true, }); expect(resolveToolcraftProductSceneBounds([])).toMatchObject({ code: "empty-scene", ok: false, }); expect( resolveToolcraftProductSceneBounds([ { height: 20, width: 40, x: -10, y: -5 }, { height: 30, width: Number.NaN, x: 35, y: 15 }, ]), ).toMatchObject({ code: "scene-bounds-unavailable", ok: false }); }); it("projects center-anchored frames and unions negative coordinates", () => { expect( getToolcraftSceneElementRect({ position: { x: 10, y: 5 }, size: { height: 20, unit: "px", width: 40 }, }), ).toEqual({ height: 20, width: 40, x: -10, y: -5 }); expect( unionToolcraftSceneRects([ { height: 20, width: 40, x: -10, y: 5 }, { height: 30, width: 10, x: 20, y: -5 }, ]), ).toEqual({ height: 30, width: 40, x: -10, y: -5 }); }); it("returns typed empty and invalid-provider failures", () => { expect(resolveToolcraftSceneBounds(emptyState())).toMatchObject({ code: "empty-scene", ok: false, }); expect( resolveToolcraftSceneBounds(emptyState(), [ { height: 20, width: Number.NaN, x: 0, y: 0 }, ]), ).toMatchObject({ code: "scene-bounds-unavailable", ok: false }); }); it("unions visible runtime and product contributors in world coordinates", () => { const state = createToolcraftState( defineToolcraft({ canvas: { enabled: true }, panels: {} }), { layers: [ { displayName: "Image", id: "image-layer", kind: "layer", name: "image", visible: true, }, { displayName: "Model", id: "model-layer", kind: "layer", name: "model", visible: true, }, ], mediaAssets: [ { assetKind: "image", dataUrl: "data:image/png;base64,test", fileName: "image.png", id: "image", layerId: "image-layer", mimeType: "image/png", position: { x: 0, y: 0 }, size: { height: 40, unit: "px", width: 100 }, transform: { rotationDeg: 90 }, }, createHydrationModelAsset("document:model", { id: "model", layerId: "model-layer", lifecycle: "clean", position: { x: 100, y: 25 }, size: { height: 30, unit: "px", width: 20 }, sourceTarget: "source.model", }), ], }, ); expect( resolveToolcraftSceneBounds(state, [ { height: 10, width: 10, x: -100, y: 100 }, ]), ).toEqual({ bounds: { height: 160, width: 210, x: -100, y: -50 }, ok: true, }); }); it("honors layer and composition visibility for images and models", () => { const state = createToolcraftState( defineToolcraft({ canvas: { enabled: true }, panels: {} }), { layers: [ { collapsed: false, displayName: "Hidden", id: "hidden-group", kind: "group", name: "hidden", visible: false, }, { displayName: "Image", id: "image-layer", kind: "layer", name: "image", parentGroupId: "hidden-group", visible: true, }, { displayName: "Model", id: "model-layer", kind: "layer", name: "model", visible: true, }, ], mediaAssets: [ { assetKind: "image", dataUrl: "data:image/png;base64,test", fileName: "hidden.png", id: "image", layerId: "image-layer", mimeType: "image/png", position: { x: 0, y: 0 }, size: { height: 20, unit: "px", width: 20 }, }, createHydrationModelAsset("document:model", { id: "model", layerId: "model-layer", lifecycle: "clean", position: { x: 50, y: 60 }, size: { height: 40, unit: "px", width: 30 }, sourceTarget: "source.model", }), ], }, ); expect( resolveToolcraftSceneBounds(state, [], { renderDefaultImages: false, suppressedModelTargets: [], }), ).toEqual({ bounds: { height: 40, width: 30, x: 35, y: 40 }, ok: true, }); expect( resolveToolcraftSceneBounds(state, [], { renderDefaultImages: true, suppressedModelTargets: ["source.model"], }), ).toMatchObject({ code: "empty-scene", ok: false }); }); it("excludes unavailable images from runtime scene bounds", () => { const state = createToolcraftState( defineToolcraft({ canvas: { enabled: true }, panels: {} }), { layers: [ { displayName: "Ready", id: "ready-layer", kind: "layer", name: "ready", visible: true, }, { displayName: "Unavailable", id: "unavailable-layer", kind: "layer", name: "unavailable", visible: true, }, ], mediaAssets: [ { assetKind: "image", fileName: "ready.png", id: "ready", layerId: "ready-layer", lifecycle: "ready", mimeType: "image/png", position: { x: 0, y: 0 }, resourceRef: `media:image:sha256:${"a".repeat(64)}`, size: { height: 20, unit: "px", width: 20 }, }, { assetKind: "image", error: { code: "binary-resource-unavailable", message: "The image resource is unavailable.", }, fileName: "unavailable.png", id: "unavailable", layerId: "unavailable-layer", lifecycle: "unavailable", mimeType: "image/png", position: { x: 1_000, y: 1_000 }, resourceRef: `media:image:sha256:${"b".repeat(64)}`, size: { height: 400, unit: "px", width: 400 }, }, ], }, ); expect(resolveToolcraftSceneBounds(state)).toEqual({ bounds: { height: 20, width: 20, x: -10, y: -10 }, ok: true, }); }); it("outward-rounds fractional and large finite bounds without clamping", () => { expect( outwardRoundToolcraftSceneRect({ height: 100.2, width: 20.1, x: -1_000_000.25, y: 2_000_000.75, }), ).toEqual({ height: 101, width: 21, x: -1_000_001, y: 2_000_000, }); }); });