import { describe, expect, it } from 'vitest'; import { GAME_CREATION_RESOURCE_LAYOUT_MAX_COORDINATE, GAME_CREATION_RESOURCE_LAYOUT_MIN_COORDINATE, isSafeProjectResourceCanvasCoordinate, } from '../../../packages/shared/src/contracts/gameCreationApp'; import { createEmptyResourceCanvasLayout, createResourceCanvasCardSizeByResourceId, DEFAULT_RESOURCE_CANVAS_CARD_SIZE, fitResourceCanvasViewportToContent, moveResourceCanvasPosition, normalizeInfiniteResourceCanvasViewport, reconcileResourceCanvasLayout, RESOURCE_CANVAS_CARD_HEIGHT, RESOURCE_CANVAS_CARD_WIDTH, RESOURCE_CANVAS_COLUMN_GAP, RESOURCE_CANVAS_DEPENDENCY_COLUMN_GAP, RESOURCE_CANVAS_DEPENDENCY_ROW_GAP, RESOURCE_CANVAS_INITIAL_FIT_MAX_SCALE, type ResourceCanvasCardSize, resourceCanvasContentBounds, resourceCanvasImageCardSize, type ResourceCanvasItem, type ResourceCanvasLayoutTopology, resourceCanvasSectionExtent, } from '../src/view/project-development/resourceCanvasLayoutModel'; function resource( id: string, category: ResourceCanvasItem['category'], dependencyDepth = 0, ): ResourceCanvasItem { return { id, category, subtype: 'default', dependencyDepth, label: id, mediaType: category === 'art' ? 'image/png' : 'text/markdown', }; } function dependencyTopology( referenceEdges: ResourceCanvasLayoutTopology['referenceEdges'] = [], taskFlows: ResourceCanvasLayoutTopology['taskFlows'] = [], ): ResourceCanvasLayoutTopology { return { referenceEdges, taskFlows }; } function positionById( layout: ReturnType['layout'], resourceId: string, ) { const position = layout.positions.find( (candidate) => candidate.resourceId === resourceId, ); expect(position).toBeDefined(); return position!; } describe('resource canvas layout model', () => { it('allows infinite background panning while keeping zoom within shared limits', () => { expect( normalizeInfiniteResourceCanvasViewport({ x: 2_000, y: 2_000, scale: 1, }), ).toEqual({ x: 2_000, y: 2_000, scale: 1 }); expect( normalizeInfiniteResourceCanvasViewport({ x: -2_000, y: -2_000, scale: 0.001, }), ).toEqual({ x: -2_000, y: -2_000, scale: 0.025 }); }); it('does not pull the viewport back when resource extents change', () => { const before = normalizeInfiniteResourceCanvasViewport({ x: 12_000, y: -9_000, scale: 0.75, }); const after = normalizeInfiniteResourceCanvasViewport(before); expect(after).toEqual(before); }); it('repairs invalid viewport coordinates without adding a canvas boundary', () => { expect( normalizeInfiniteResourceCanvasViewport({ x: Number.NaN, y: Number.POSITIVE_INFINITY, scale: Number.NaN, }), ).toEqual({ x: 0, y: 0, scale: 1 }); }); it('creates non-overlapping defaults and keeps the two modes independent', () => { const resources = [ resource('a', 'art', 0), resource('b', 'art', 0), resource('c', 'art', 1), ]; const dependency = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-1', 'dependency'), resources, ).layout; const type = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-1', 'type'), resources, ).layout; expect(dependency.positions).toMatchObject([ { resourceId: 'a', x: 0, y: 0, manuallyPlaced: false }, { resourceId: 'c', x: RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_DEPENDENCY_COLUMN_GAP, y: 0, manuallyPlaced: false, }, { resourceId: 'b', x: 0, y: RESOURCE_CANVAS_CARD_HEIGHT + RESOURCE_CANVAS_DEPENDENCY_ROW_GAP, manuallyPlaced: false, }, ]); expect(type.positions).toMatchObject([ { resourceId: 'a', x: 0, y: 0 }, { resourceId: 'b', x: RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP, y: 0, }, { resourceId: 'c', x: 2 * (RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP), y: 0, }, ]); }); it('preserves existing positions, appends new resources, and removes stale ids', () => { const base = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-1', 'type'), [resource('a', 'document'), resource('stale', 'document')], ).layout; const moved = moveResourceCanvasPosition( base, 'a', 'document', 333.4, 41.6, ); const reconciled = reconcileResourceCanvasLayout(moved, [ resource('a', 'document'), resource('new', 'document'), ]); expect(reconciled.changed).toBe(true); expect(reconciled.layout.positions).toContainEqual({ resourceId: 'a', section: 'document', x: 333, y: 42, manuallyPlaced: true, }); expect( reconciled.layout.positions.some( (position) => position.resourceId === 'stale', ), ).toBe(false); expect( reconciled.layout.positions.find( (position) => position.resourceId === 'new', )?.manuallyPlaced, ).toBe(false); }); it('preserves negative manual positions within the symmetric canvas range', () => { const base = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-negative', 'type'), [resource('a', 'document')], ).layout; const moved = moveResourceCanvasPosition( base, 'a', 'document', -123.4, -45.6, ); expect(moved.positions[0]).toMatchObject({ x: -123, y: -46 }); expect( isSafeProjectResourceCanvasCoordinate( GAME_CREATION_RESOURCE_LAYOUT_MIN_COORDINATE, ), ).toBe(true); expect( isSafeProjectResourceCanvasCoordinate( GAME_CREATION_RESOURCE_LAYOUT_MIN_COORDINATE - 1, ), ).toBe(false); }); it('drops a persisted position whose section no longer matches the resource', () => { const layout = createEmptyResourceCanvasLayout('project-1', 'dependency'); layout.positions = [ { resourceId: 'asset-a', section: 'audio', x: 10, y: 10, manuallyPlaced: true, }, ]; const reconciled = reconcileResourceCanvasLayout(layout, [ resource('asset-a', 'art'), ]); expect(reconciled.layout.positions).toEqual([ { resourceId: 'asset-a', section: 'art', x: 0, y: 0, manuallyPlaced: false, }, ]); }); it('sorts type defaults by subtype before media type and label with an id fallback', () => { const typeLayout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-type-order', 'type'), [ { ...resource('ui-prototype', 'art'), subtype: 'ui-prototype', mediaType: 'image/png', label: '甲界面', }, { ...resource('art-spritesheet-b', 'art'), subtype: 'art-spritesheet', mediaType: 'image/png', label: '乙图集', }, { ...resource('art-spritesheet-a', 'art'), subtype: 'art-spritesheet', mediaType: 'image/png', label: '乙图集', }, ], ).layout; expect( typeLayout.positions.map(({ resourceId, x, y }) => ({ resourceId, x, y, })), ).toEqual([ { resourceId: 'art-spritesheet-a', x: 0, y: 0 }, { resourceId: 'art-spritesheet-b', x: RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP, y: 0, }, { resourceId: 'ui-prototype', x: 2 * (RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP), y: 0, }, ]); }); it('keeps connected art cards close while placing separate components before isolates', () => { const layout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-clusters', 'dependency'), [ resource('art:canvas', 'art', 0), resource('art:wireframe', 'art', 1), resource('art:character', 'art', 0), resource('art:screen', 'art', 1), resource('art:unrelated', 'art', 0), ], dependencyTopology([ { sourceResourceId: 'art:canvas', targetResourceId: 'art:wireframe', }, { sourceResourceId: 'art:character', targetResourceId: 'art:screen', }, ]), ).layout; const canvas = positionById(layout, 'art:canvas'); const wireframe = positionById(layout, 'art:wireframe'); const character = positionById(layout, 'art:character'); const screen = positionById(layout, 'art:screen'); const unrelated = positionById(layout, 'art:unrelated'); expect(wireframe.y).toBe(canvas.y); expect(screen.y).toBe(character.y); expect(character.y - canvas.y).toBeGreaterThanOrEqual( RESOURCE_CANVAS_CARD_HEIGHT + RESOURCE_CANVAS_DEPENDENCY_ROW_GAP, ); expect(unrelated.y).toBeGreaterThan(screen.y); }); it('ignores cross-category references and task flows when forming layout clusters', () => { const layout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout( 'project-no-cross-category', 'dependency', ), [ resource('art:a', 'art', 0), resource('art:b', 'art', 0), resource('document:a', 'document', 0), ], dependencyTopology( [ { sourceResourceId: 'document:a', targetResourceId: 'art:a', }, ], [ { sourceResourceIds: ['document:a'], targetResourceIds: ['art:a'], }, ], ), ).layout; expect(positionById(layout, 'art:a').y).toBe(0); expect(positionById(layout, 'art:b').y).toBe( RESOURCE_CANVAS_CARD_HEIGHT + RESOURCE_CANVAS_DEPENDENCY_ROW_GAP, ); expect(positionById(layout, 'document:a')).toMatchObject({ section: 'document', y: 0, }); }); it('uses bounded layer scans to align multi-edge targets with their upstream neighbors', () => { const layout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-crossings', 'dependency'), [ resource('source:bottom', 'art', 0), resource('source:top', 'art', 0), resource('target:first', 'art', 1), resource('target:second', 'art', 1), ], dependencyTopology( [ { sourceResourceId: 'source:bottom', targetResourceId: 'target:second', }, { sourceResourceId: 'source:top', targetResourceId: 'target:first', }, ], [ { sourceResourceIds: ['source:bottom', 'source:top'], targetResourceIds: ['target:first', 'target:second'], }, ], ), ).layout; expect(positionById(layout, 'source:bottom').y).toBeLessThan( positionById(layout, 'source:top').y, ); expect(positionById(layout, 'target:second').y).toBeLessThan( positionById(layout, 'target:first').y, ); }); it('centers narrow dependency layers so diamond edges have balanced lengths', () => { const layout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-balanced-diamond', 'dependency'), [ resource('source', 'art', 0), resource('middle:upper', 'art', 1), resource('middle:lower', 'art', 1), resource('target', 'art', 2), ], dependencyTopology([ { sourceResourceId: 'source', targetResourceId: 'middle:upper' }, { sourceResourceId: 'source', targetResourceId: 'middle:lower' }, { sourceResourceId: 'middle:upper', targetResourceId: 'target' }, { sourceResourceId: 'middle:lower', targetResourceId: 'target' }, ]), ).layout; const dependencySlotHeight = RESOURCE_CANVAS_CARD_HEIGHT + RESOURCE_CANVAS_DEPENDENCY_ROW_GAP; const dependencySlotWidth = RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_DEPENDENCY_COLUMN_GAP; expect(positionById(layout, 'middle:lower')).toMatchObject({ x: dependencySlotWidth, y: 0, }); expect(positionById(layout, 'middle:upper')).toMatchObject({ x: dependencySlotWidth, y: dependencySlotHeight, }); expect(positionById(layout, 'source')).toMatchObject({ x: 0, y: dependencySlotHeight / 2, }); expect(positionById(layout, 'target')).toMatchObject({ x: dependencySlotWidth * 2, y: dependencySlotHeight / 2, }); }); it('keeps cycles contiguous, preserves their Rust depth, and leaves cross-section cards in place', () => { const layout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-cycles', 'dependency'), [ resource('art:cycle-a', 'art', 2), resource('art:cycle-b', 'art', 2), resource('art:after-cycle', 'art', 3), resource('art:self', 'art', 0), resource('document:brief', 'document', 0), ], dependencyTopology([ { sourceResourceId: 'art:cycle-a', targetResourceId: 'art:cycle-b' }, { sourceResourceId: 'art:cycle-b', targetResourceId: 'art:cycle-a' }, { sourceResourceId: 'art:cycle-b', targetResourceId: 'art:after-cycle', }, { sourceResourceId: 'art:self', targetResourceId: 'art:self' }, { sourceResourceId: 'document:brief', targetResourceId: 'art:cycle-a', }, ]), ).layout; const cycleA = positionById(layout, 'art:cycle-a'); const cycleB = positionById(layout, 'art:cycle-b'); const afterCycle = positionById(layout, 'art:after-cycle'); expect(cycleA.x).toBe( 2 * (RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_DEPENDENCY_COLUMN_GAP), ); expect(cycleB.x).toBe(cycleA.x); expect(Math.abs(cycleA.y - cycleB.y)).toBe( RESOURCE_CANVAS_CARD_HEIGHT + RESOURCE_CANVAS_DEPENDENCY_ROW_GAP, ); expect(afterCycle.x).toBe( 3 * (RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_DEPENDENCY_COLUMN_GAP), ); expect(positionById(layout, 'document:brief').section).toBe('document'); }); it('keeps same-depth cycle members consecutive when another related card shares the layer', () => { const layout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-cycle-contiguity', 'dependency'), [ resource('art:cycle-a', 'art', 2), resource('art:cycle-b', 'art', 2), resource('art:sibling', 'art', 2), resource('art:after', 'art', 3), ], dependencyTopology([ { sourceResourceId: 'art:cycle-a', targetResourceId: 'art:cycle-b' }, { sourceResourceId: 'art:cycle-b', targetResourceId: 'art:cycle-a' }, { sourceResourceId: 'art:cycle-b', targetResourceId: 'art:after' }, { sourceResourceId: 'art:sibling', targetResourceId: 'art:after' }, ]), ).layout; const cycleRows = ['art:cycle-a', 'art:cycle-b'] .map((resourceId) => positionById(layout, resourceId).y) .sort((left, right) => left - right); const siblingRow = positionById(layout, 'art:sibling').y; expect(cycleRows[1] - cycleRows[0]).toBe( RESOURCE_CANVAS_CARD_HEIGHT + RESOURCE_CANVAS_DEPENDENCY_ROW_GAP, ); expect(siblingRow).toBeGreaterThan(cycleRows[1]); }); it('is deterministic, does not modify type placement, and avoids historical manual positions', () => { const resources = [ resource('art:source', 'art', 0), resource('art:target', 'art', 1), resource('art:other', 'art', 1), ]; const topology = dependencyTopology([ { sourceResourceId: 'art:source', targetResourceId: 'art:target' }, ]); const source = createEmptyResourceCanvasLayout( 'project-stable', 'dependency', ); source.positions = [ { resourceId: 'art:other', section: 'art', x: RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP, y: 0, manuallyPlaced: true, }, ]; const first = reconcileResourceCanvasLayout( source, resources, topology, ).layout; const second = reconcileResourceCanvasLayout( source, resources, topology, ).layout; const type = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-stable', 'type'), resources, topology, ).layout; expect(second.positions).toEqual(first.positions); expect(positionById(first, 'art:other')).toMatchObject({ x: RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP, y: 0, manuallyPlaced: true, }); expect(positionById(first, 'art:target').y).toBe( RESOURCE_CANVAS_CARD_HEIGHT + RESOURCE_CANVAS_DEPENDENCY_ROW_GAP, ); expect(positionById(type, 'art:other')).toMatchObject({ x: 0, y: 0 }); }); it('saturates super-deep dependency columns and avoids collisions within the coordinate contract', () => { const resources = [ resource('art:depth-4385', 'art', 4_385), resource('art:depth-4386-a', 'art', 4_386), resource('art:depth-4386-b', 'art', 4_386), resource('art:depth-9000', 'art', 9_000), ]; const layout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-deep-dependency', 'dependency'), resources, ).layout; const repeatedLayout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-deep-dependency', 'dependency'), resources, ).layout; const lastLegalColumn = Math.floor( GAME_CREATION_RESOURCE_LAYOUT_MAX_COORDINATE / (RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_DEPENDENCY_COLUMN_GAP), ) * (RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_DEPENDENCY_COLUMN_GAP); const deepPositions = [ positionById(layout, 'art:depth-4386-a'), positionById(layout, 'art:depth-4386-b'), positionById(layout, 'art:depth-9000'), ]; expect(lastLegalColumn).toBe(999_780); expect( isSafeProjectResourceCanvasCoordinate( GAME_CREATION_RESOURCE_LAYOUT_MAX_COORDINATE, ), ).toBe(true); expect( isSafeProjectResourceCanvasCoordinate( GAME_CREATION_RESOURCE_LAYOUT_MAX_COORDINATE + 1, ), ).toBe(false); expect( isSafeProjectResourceCanvasCoordinate( GAME_CREATION_RESOURCE_LAYOUT_MIN_COORDINATE, ), ).toBe(true); expect( isSafeProjectResourceCanvasCoordinate( GAME_CREATION_RESOURCE_LAYOUT_MIN_COORDINATE - 1, ), ).toBe(false); expect(deepPositions.map((position) => position.x)).toEqual([ lastLegalColumn, lastLegalColumn, lastLegalColumn, ]); expect(new Set(deepPositions.map((position) => position.y)).size).toBe(3); expect(repeatedLayout.positions).toEqual(layout.positions); expect( layout.positions.every( (position) => isSafeProjectResourceCanvasCoordinate(position.x) && isSafeProjectResourceCanvasCoordinate(position.y), ), ).toBe(true); expect(resources.map((resource) => resource.dependencyDepth)).toEqual([ 4_385, 4_386, 4_386, 9_000, ]); }); it.each(['dependency', 'type'] as const)( 'reconciles 4096 resources in %s mode within the bounded layout budget', (mode) => { const resources = Array.from({ length: 4096 }, (_, index) => ({ ...resource(`resource-${index.toString().padStart(4, '0')}`, 'art'), dependencyDepth: index % 64, mediaType: `image/type-${index % 16}`, })); const startedAt = performance.now(); const layout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('project-performance', mode), resources, ).layout; const elapsedMs = performance.now() - startedAt; expect(layout.positions).toHaveLength(4096); expect( new Set( layout.positions.map((position) => `${position.x}:${position.y}`), ).size, ).toBe(4096); expect(elapsedMs).toBeLessThan(2000); }, ); it('keeps dependency clustering within the 4096-resource layout budget', () => { const resources = Array.from({ length: 4096 }, (_, index) => resource( `resource-${index.toString().padStart(4, '0')}`, 'art', index % 64, ), ); const topology = dependencyTopology( resources.slice(1).map((target, index) => ({ sourceResourceId: resources[index]!.id, targetResourceId: target.id, })), ); const startedAt = performance.now(); const layout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout( 'project-cluster-performance', 'dependency', ), resources, topology, ).layout; expect(layout.positions).toHaveLength(4096); expect(performance.now() - startedAt).toBeLessThan(2000); }); }); describe('resource canvas variable card geometry', () => { it.each([ [1, 1, { width: 180, height: 180 }], [16, 9, { width: 220, height: 124 }], [9, 16, { width: 101, height: 180 }], [2, 3, { width: 120, height: 180 }], [3, 2, { width: 220, height: 147 }], [100, 1, { width: 220, height: 110 }], [1, 100, { width: 96, height: 180 }], ])( 'bounds a %d:%d image inside the shared card limits', (pixelWidth, pixelHeight, expected) => { expect(resourceCanvasImageCardSize({ pixelWidth, pixelHeight })).toEqual( expected, ); }, ); it('keeps non-images and images without metadata at the fixed fallback size', () => { const sizes = createResourceCanvasCardSizeByResourceId( [resource('image', 'art'), resource('document', 'document')], new Map([ ['image', { pixelWidth: 1920, pixelHeight: 1080 }], ['document', { pixelWidth: 1920, pixelHeight: 1080 }], ]), ); expect(sizes.get('image')).toEqual({ width: 220, height: 124 }); expect(sizes.get('document')).toEqual(DEFAULT_RESOURCE_CANVAS_CARD_SIZE); expect( createResourceCanvasCardSizeByResourceId( [resource('pending-image', 'art')], new Map(), ).get('pending-image'), ).toEqual(DEFAULT_RESOURCE_CANVAS_CARD_SIZE); }); it('uses actual widths and heights for type placement collision checks', () => { const resources = [resource('wide-a', 'art'), resource('wide-b', 'art')]; const cardSizes = new Map([ ['wide-a', { width: 220, height: 180 }], ['wide-b', { width: 220, height: 180 }], ]); const layout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('variable-type', 'type'), resources, undefined, cardSizes, ).layout; expect(positionById(layout, 'wide-a')).toMatchObject({ x: 0, y: 0 }); expect(positionById(layout, 'wide-b')).toMatchObject({ x: 2 * (RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP), y: 0, }); }); it('uses actual dimensions for dependency columns, rows, and section extent', () => { const resources = [ resource('source-a', 'art', 0), resource('source-b', 'art', 0), resource('target', 'art', 1), ]; const cardSizes = new Map([ ['source-a', { width: 220, height: 180 }], ['source-b', { width: 96, height: 180 }], ['target', { width: 180, height: 128 }], ]); const layout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('variable-dependency', 'dependency'), resources, undefined, cardSizes, ).layout; expect(positionById(layout, 'source-a')).toMatchObject({ x: 0, y: 0 }); expect(positionById(layout, 'source-b')).toMatchObject({ x: 0, y: 180 + RESOURCE_CANVAS_DEPENDENCY_ROW_GAP, }); expect(positionById(layout, 'target')).toMatchObject({ x: 220 + RESOURCE_CANVAS_DEPENDENCY_COLUMN_GAP, y: 0, }); expect(resourceCanvasSectionExtent(layout.positions, cardSizes)).toEqual({ x: 0, y: 0, width: 620, height: 416, }); }); it('includes negative manual coordinates in the fit bounds', () => { const positions: ProjectResourceCanvasPosition[] = [ { resourceId: 'negative-card', section: 'art', x: -240, y: -160, manuallyPlaced: true, }, { resourceId: 'positive-card', section: 'art', x: 480, y: 320, manuallyPlaced: true, }, ]; const extent = resourceCanvasSectionExtent( positions, new Map([ ['negative-card', { width: 180, height: 128 }], ['positive-card', { width: 220, height: 180 }], ]), ); expect(extent).toEqual({ x: -256, y: -176, width: 972, height: 692, }); }); it('fits the exact resource card bounds to the reset viewport', () => { const positions: ProjectResourceCanvasPosition[] = [ { resourceId: 'negative-card', section: 'art', x: -240, y: -160, manuallyPlaced: true, }, { resourceId: 'positive-card', section: 'art', x: 480, y: 320, manuallyPlaced: true, }, ]; const bounds = resourceCanvasContentBounds( positions, new Map([ ['negative-card', { width: 180, height: 128 }], ['positive-card', { width: 220, height: 180 }], ]), ); expect(bounds).toEqual({ x: -240, y: -160, width: 940, height: 660, }); const viewport = fitResourceCanvasViewportToContent({ bounds, canvasSize: { width: 800, height: 600 }, }); expect(viewport.scale).toBeCloseTo(768 / 940, 8); expect(bounds.width * viewport.scale).toBeCloseTo(768, 8); expect(viewport.x + bounds.x * viewport.scale).toBeCloseTo(16, 8); }); it.each(['document', 'art', 'audio', 'code', 'version'])( 'caps the initial fit for every resource category (%s) while keeping explicit zoom overrides available', () => { const viewport = fitResourceCanvasViewportToContent({ bounds: { x: 0, y: 0, width: 180, height: 128 }, canvasSize: { width: 800, height: 600 }, }); const explicitViewport = fitResourceCanvasViewportToContent({ bounds: { x: 0, y: 0, width: 180, height: 128 }, canvasSize: { width: 800, height: 600 }, maxScale: 2.5, }); expect(viewport.scale).toBe(RESOURCE_CANVAS_INITIAL_FIT_MAX_SCALE); expect(explicitViewport.scale).toBe(2.5); }, ); it('accepts card sizes directly on resource items for hook integration', () => { const wide = { ...resource('wide', 'art', 0), cardSize: { width: 220, height: 180 }, }; const target = { ...resource('target', 'art', 1), cardSize: { width: 180, height: 128 }, }; const layout = reconcileResourceCanvasLayout( createEmptyResourceCanvasLayout('item-size', 'dependency'), [wide, target], ).layout; expect(positionById(layout, 'target').x).toBe( wide.cardSize.width + RESOURCE_CANVAS_DEPENDENCY_COLUMN_GAP, ); }); });