diff --git a/frontend/src/lib/components/canvas/Canvas.svelte b/frontend/src/lib/components/canvas/Canvas.svelte index 9b2bd83..2e1a597 100644 --- a/frontend/src/lib/components/canvas/Canvas.svelte +++ b/frontend/src/lib/components/canvas/Canvas.svelte @@ -36,6 +36,8 @@ onCameraChange?: (camera: Camera) => void; /** Callback when selection changes */ onSelectionChange?: (ids: string[]) => void; + /** Callback when grid is toggled */ + onGridChange?: (enabled: boolean) => void; } let { @@ -45,6 +47,7 @@ initialCamera = { x: 0, y: 0, zoom: 1.0 }, onObjectMove, onCameraChange, + onGridChange, onSelectionChange, }: Props = $props(); @@ -62,6 +65,7 @@ let dragTargetId: string | null = null; let dragStartWorld = { x: 0, y: 0 }; let dragObjectStart = { x: 0, y: 0 }; + let dragGroupStarts = new Map(); let panStart = { x: 0, y: 0 }; let cameraStart = { x: 0, y: 0 }; let lassoStart = { x: 0, y: 0 }; @@ -124,6 +128,7 @@ // 'g' toggles grid snap if (e.code === 'KeyG' && !e.ctrlKey && !e.metaKey) { grid = { ...grid, enabled: !grid.enabled }; + onGridChange?.(grid.enabled); } } function onKeyUp(e: KeyboardEvent) { @@ -207,6 +212,13 @@ } onSelectionChange?.([...selectedIds]); + // Record start positions for all selected objects (group drag) + dragGroupStarts = new Map(); + for (const sid of selectedIds) { + const sobj = objects.find(o => o.id === sid); + if (sobj) dragGroupStarts.set(sid, { x: sobj.x, y: sobj.y }); + } + containerEl.setPointerCapture(e.pointerId); e.preventDefault(); return; @@ -251,16 +263,20 @@ const sx = e.clientX - rect.left; const sy = e.clientY - rect.top; const world = screenToWorld(sx, sy, camera); - let newX = dragObjectStart.x + (world.x - dragStartWorld.x); - let newY = dragObjectStart.y + (world.y - dragStartWorld.y); + const dx = world.x - dragStartWorld.x; + const dy = world.y - dragStartWorld.y; - if (grid.enabled) { - newX = snap(newX, grid.size); - newY = snap(newY, grid.size); + // Move all selected objects together + for (const [sid, start] of dragGroupStarts) { + let newX = start.x + dx; + let newY = start.y + dy; + if (grid.enabled) { + newX = snap(newX, grid.size); + newY = snap(newY, grid.size); + } + onObjectMove?.(sid, newX, newY); } - onObjectMove?.(dragTargetId, newX, newY); - // Edge-pan: scroll canvas when dragging near edges const edgeMargin = 40; const edgeSpeed = 8; @@ -455,7 +471,7 @@ diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 6142bed..2f0b359 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -77,10 +77,14 @@ if (res.metadata) { loadThemeFromMetadata(res.metadata as Record); // Load saved camera position - const cam = (res.metadata as Record).camera as Camera | undefined; + const meta = res.metadata as Record; + const cam = meta.camera as Camera | undefined; if (cam && typeof cam.x === 'number') { savedCamera = cam; } + if (typeof meta.gridEnabled === 'boolean') { + gridEnabled = meta.gridEnabled; + } } }) .catch((err) => { @@ -103,6 +107,7 @@ let layout = $state({ panels: [] }); let layoutInitialized = $state(false); let savedCamera = $state({ x: 0, y: 0, zoom: 1.0 }); + let gridEnabled = $state(false); // When workspace node appears in store (after creation), load its layout $effect(() => { @@ -148,6 +153,7 @@ ...currentMeta, workspace_layout: layout, camera: savedCamera, + gridEnabled, preferences: { ...(currentMeta.preferences ?? {}), theme: { hueBg: themeHueBg, hueSurface: themeHueSurface, hueAccent: themeHueAccent }, @@ -784,9 +790,10 @@ { savedCamera = cam; persistMetadata(); }} + onGridChange={(enabled) => { gridEnabled = enabled; persistMetadata(); }} > {#snippet renderObject(obj)} {@const trait = obj.id}