import { describe, expect, it } from 'vitest'; import { type CanvasLayer, createCanvasExportModel, createMinimapModel, fitViewportToLayers, getWorldPointFromClient, moveLayersFromDrag, panViewportFromWheel, resizeCanvasLayerBounds, resolvePointerSelection, resolveViewportFromWheel, scaleViewportFromScreenPoint, selectLayersInsideMarquee, transformCanvasLayers, } from './index'; function layer(id: string, overrides: Partial = {}): CanvasLayer { return { id, resourceId: `resource-${id}`, title: id, src: `media:${id}`, x: 0, y: 0, width: 100, height: 80, originalWidth: 100, originalHeight: 80, zIndex: 1, sourceType: 'uploaded', ...overrides, }; } describe('shared image canvas core', () => { it('keeps the world anchor stable while zooming and converts coordinates', () => { const viewport = { x: 20, y: 30, scale: 0.5 }; const anchor = { x: 240, y: 180 }; const before = getWorldPointFromClient({ clientX: anchor.x, clientY: anchor.y, rect: { left: 0, top: 0, right: 800, bottom: 600 }, viewport, }); const next = scaleViewportFromScreenPoint({ viewport, nextScale: 1.25, screenPoint: anchor, }); const after = getWorldPointFromClient({ clientX: anchor.x, clientY: anchor.y, rect: { left: 0, top: 0, right: 800, bottom: 600 }, viewport: next, }); expect(after).toEqual(before); expect(panViewportFromWheel({ viewport, deltaX: 12, deltaY: -8 })).toEqual({ x: 8, y: 38, scale: 0.5, }); }); it('shares pan, shift-pan and modifier-zoom wheel semantics', () => { const viewport = { x: 20, y: 30, scale: 1 }; expect( resolveViewportFromWheel({ viewport, deltaX: 4, deltaY: 10, shiftKey: false, ctrlKey: false, metaKey: false, screenPoint: { x: 100, y: 100 }, }), ).toEqual({ mode: 'pan', viewport: { x: 16, y: 20, scale: 1 } }); expect( resolveViewportFromWheel({ viewport, deltaX: 0, deltaY: 10, shiftKey: true, ctrlKey: false, metaKey: false, screenPoint: { x: 100, y: 100 }, }), ).toEqual({ mode: 'pan', viewport: { x: 10, y: 30, scale: 1 } }); expect( resolveViewportFromWheel({ viewport, deltaX: 0, deltaY: -10, shiftKey: false, ctrlKey: true, metaKey: false, screenPoint: { x: 100, y: 100 }, }).mode, ).toBe('zoom'); }); it('supports additive selection, marquee and multi-layer movement', () => { expect( resolvePointerSelection({ targetId: 'b', selectedIds: ['a'], isMultiSelectGesture: true, }), ).toEqual(['a', 'b']); expect( resolvePointerSelection({ targetId: 'a', selectedIds: ['a'], isMultiSelectGesture: true, }), ).toEqual(['a']); expect( resolvePointerSelection({ targetId: 'a', selectedIds: ['a', 'b'], isMultiSelectGesture: true, }), ).toEqual(['b']); const layers = [ layer('a', { x: 10, y: 10 }), layer('b', { x: 140, y: 20 }), ]; expect( selectLayersInsideMarquee({ layers, marquee: { pointerId: 1, startX: 0, startY: 0, currentX: 0, currentY: 0, }, currentPoint: { x: 260, y: 120 }, viewport: { x: 0, y: 0, scale: 1 }, }), ).toEqual(['a', 'b']); const moved = moveLayersFromDrag({ layers, dragState: { kind: 'layer', pointerId: 1, layerId: 'a', layerIds: ['a', 'b'], startClientX: 0, startClientY: 0, startLayerX: 10, startLayerY: 10, startLayers: [ { id: 'a', x: 10, y: 10 }, { id: 'b', x: 140, y: 20 }, ], startScale: 1, }, pointer: { x: 20, y: 30 }, }); expect(moved.layers.map(({ x, y }) => ({ x, y }))).toEqual([ { x: 30, y: 40 }, { x: 160, y: 50 }, ]); }); it('resizes layers and preserves locked layers during transforms', () => { expect( resizeCanvasLayerBounds({ initial: { x: 10, y: 20, width: 200, height: 100 }, handle: 'top-left', deltaX: -40, deltaY: -20, preserveAspectRatio: true, }), ).toEqual({ x: -30, y: 0, width: 240, height: 120 }); const locked = layer('locked', { locked: true }); expect( transformCanvasLayers( [locked], new Map([['locked', { x: 99, width: 300 }]]), )[0], ).toBe(locked); }); it('fits content, projects the minimap and creates a host-neutral export', () => { const layers = [ layer('a', { x: 100, y: 200 }), layer('b', { x: 400, y: 500, width: 200, height: 120, zIndex: 2 }), ]; const viewport = fitViewportToLayers({ layers, canvasSize: { width: 900, height: 640 }, }); expect(viewport).not.toBeNull(); expect( createMinimapModel({ layers, viewport: viewport!, canvasSize: { width: 900, height: 640 }, })?.layers, ).toHaveLength(2); expect( createCanvasExportModel({ layers, backgroundColor: '#ffffff' }), ).toMatchObject({ bounds: { x: 100, y: 200, width: 500, height: 420 }, backgroundColor: '#ffffff', }); }); });