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:
parent
79371c20ac
commit
fa85d29c35
2 changed files with 33 additions and 10 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue