Canvas: gruppe-drag, grid-persistering, ZOOM_MIN 5%

- Lasso-seleksjon → dra flytter alle valgte paneler sammen
- Grid on/off lagres i workspace-metadata (huskes mellom besøk)
- Zoom lagres allerede via kameraposisjon (x, y, zoom)
- ZOOM_MIN senket til 5% for spredte layouts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-19 06:18:28 +00:00
parent 79371c20ac
commit fa85d29c35
2 changed files with 33 additions and 10 deletions

View file

@ -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<string, { x: number; y: number }>();
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,15 +263,19 @@
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;
// 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?.(dragTargetId, newX, newY);
onObjectMove?.(sid, newX, newY);
}
// Edge-pan: scroll canvas when dragging near edges
const edgeMargin = 40;
@ -455,7 +471,7 @@
<button
class="canvas-toolbar-btn"
class:canvas-toolbar-active={grid.enabled}
onclick={() => (grid = { ...grid, enabled: !grid.enabled })}
onclick={() => { grid = { ...grid, enabled: !grid.enabled }; onGridChange?.(grid.enabled); }}
title="Snap-to-grid (G)"
aria-label="Snap-to-grid"
>#</button>

View file

@ -77,10 +77,14 @@
if (res.metadata) {
loadThemeFromMetadata(res.metadata as Record<string, unknown>);
// Load saved camera position
const cam = (res.metadata as Record<string, unknown>).camera as Camera | undefined;
const meta = res.metadata as Record<string, unknown>;
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<WorkspaceLayout>({ panels: [] });
let layoutInitialized = $state(false);
let savedCamera = $state<Camera>({ 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 @@
<Canvas
objects={canvasObjects}
onObjectMove={handleObjectMove}
grid={{ enabled: false, size: 20 }}
grid={{ enabled: gridEnabled, size: 20 }}
initialCamera={savedCamera}
onCameraChange={(cam) => { savedCamera = cam; persistMetadata(); }}
onGridChange={(enabled) => { gridEnabled = enabled; persistMetadata(); }}
>
{#snippet renderObject(obj)}
{@const trait = obj.id}