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;
|
onCameraChange?: (camera: Camera) => void;
|
||||||
/** Callback when selection changes */
|
/** Callback when selection changes */
|
||||||
onSelectionChange?: (ids: string[]) => void;
|
onSelectionChange?: (ids: string[]) => void;
|
||||||
|
/** Callback when grid is toggled */
|
||||||
|
onGridChange?: (enabled: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
|
@ -45,6 +47,7 @@
|
||||||
initialCamera = { x: 0, y: 0, zoom: 1.0 },
|
initialCamera = { x: 0, y: 0, zoom: 1.0 },
|
||||||
onObjectMove,
|
onObjectMove,
|
||||||
onCameraChange,
|
onCameraChange,
|
||||||
|
onGridChange,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
|
@ -62,6 +65,7 @@
|
||||||
let dragTargetId: string | null = null;
|
let dragTargetId: string | null = null;
|
||||||
let dragStartWorld = { x: 0, y: 0 };
|
let dragStartWorld = { x: 0, y: 0 };
|
||||||
let dragObjectStart = { 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 panStart = { x: 0, y: 0 };
|
||||||
let cameraStart = { x: 0, y: 0 };
|
let cameraStart = { x: 0, y: 0 };
|
||||||
let lassoStart = { x: 0, y: 0 };
|
let lassoStart = { x: 0, y: 0 };
|
||||||
|
|
@ -124,6 +128,7 @@
|
||||||
// 'g' toggles grid snap
|
// 'g' toggles grid snap
|
||||||
if (e.code === 'KeyG' && !e.ctrlKey && !e.metaKey) {
|
if (e.code === 'KeyG' && !e.ctrlKey && !e.metaKey) {
|
||||||
grid = { ...grid, enabled: !grid.enabled };
|
grid = { ...grid, enabled: !grid.enabled };
|
||||||
|
onGridChange?.(grid.enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onKeyUp(e: KeyboardEvent) {
|
function onKeyUp(e: KeyboardEvent) {
|
||||||
|
|
@ -207,6 +212,13 @@
|
||||||
}
|
}
|
||||||
onSelectionChange?.([...selectedIds]);
|
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);
|
containerEl.setPointerCapture(e.pointerId);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
|
|
@ -251,15 +263,19 @@
|
||||||
const sx = e.clientX - rect.left;
|
const sx = e.clientX - rect.left;
|
||||||
const sy = e.clientY - rect.top;
|
const sy = e.clientY - rect.top;
|
||||||
const world = screenToWorld(sx, sy, camera);
|
const world = screenToWorld(sx, sy, camera);
|
||||||
let newX = dragObjectStart.x + (world.x - dragStartWorld.x);
|
const dx = world.x - dragStartWorld.x;
|
||||||
let newY = dragObjectStart.y + (world.y - dragStartWorld.y);
|
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) {
|
if (grid.enabled) {
|
||||||
newX = snap(newX, grid.size);
|
newX = snap(newX, grid.size);
|
||||||
newY = snap(newY, grid.size);
|
newY = snap(newY, grid.size);
|
||||||
}
|
}
|
||||||
|
onObjectMove?.(sid, newX, newY);
|
||||||
onObjectMove?.(dragTargetId, newX, newY);
|
}
|
||||||
|
|
||||||
// Edge-pan: scroll canvas when dragging near edges
|
// Edge-pan: scroll canvas when dragging near edges
|
||||||
const edgeMargin = 40;
|
const edgeMargin = 40;
|
||||||
|
|
@ -455,7 +471,7 @@
|
||||||
<button
|
<button
|
||||||
class="canvas-toolbar-btn"
|
class="canvas-toolbar-btn"
|
||||||
class:canvas-toolbar-active={grid.enabled}
|
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)"
|
title="Snap-to-grid (G)"
|
||||||
aria-label="Snap-to-grid"
|
aria-label="Snap-to-grid"
|
||||||
>#</button>
|
>#</button>
|
||||||
|
|
|
||||||
|
|
@ -77,10 +77,14 @@
|
||||||
if (res.metadata) {
|
if (res.metadata) {
|
||||||
loadThemeFromMetadata(res.metadata as Record<string, unknown>);
|
loadThemeFromMetadata(res.metadata as Record<string, unknown>);
|
||||||
// Load saved camera position
|
// 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') {
|
if (cam && typeof cam.x === 'number') {
|
||||||
savedCamera = cam;
|
savedCamera = cam;
|
||||||
}
|
}
|
||||||
|
if (typeof meta.gridEnabled === 'boolean') {
|
||||||
|
gridEnabled = meta.gridEnabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|
@ -103,6 +107,7 @@
|
||||||
let layout = $state<WorkspaceLayout>({ panels: [] });
|
let layout = $state<WorkspaceLayout>({ panels: [] });
|
||||||
let layoutInitialized = $state(false);
|
let layoutInitialized = $state(false);
|
||||||
let savedCamera = $state<Camera>({ x: 0, y: 0, zoom: 1.0 });
|
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
|
// When workspace node appears in store (after creation), load its layout
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|
@ -148,6 +153,7 @@
|
||||||
...currentMeta,
|
...currentMeta,
|
||||||
workspace_layout: layout,
|
workspace_layout: layout,
|
||||||
camera: savedCamera,
|
camera: savedCamera,
|
||||||
|
gridEnabled,
|
||||||
preferences: {
|
preferences: {
|
||||||
...(currentMeta.preferences ?? {}),
|
...(currentMeta.preferences ?? {}),
|
||||||
theme: { hueBg: themeHueBg, hueSurface: themeHueSurface, hueAccent: themeHueAccent },
|
theme: { hueBg: themeHueBg, hueSurface: themeHueSurface, hueAccent: themeHueAccent },
|
||||||
|
|
@ -784,9 +790,10 @@
|
||||||
<Canvas
|
<Canvas
|
||||||
objects={canvasObjects}
|
objects={canvasObjects}
|
||||||
onObjectMove={handleObjectMove}
|
onObjectMove={handleObjectMove}
|
||||||
grid={{ enabled: false, size: 20 }}
|
grid={{ enabled: gridEnabled, size: 20 }}
|
||||||
initialCamera={savedCamera}
|
initialCamera={savedCamera}
|
||||||
onCameraChange={(cam) => { savedCamera = cam; persistMetadata(); }}
|
onCameraChange={(cam) => { savedCamera = cam; persistMetadata(); }}
|
||||||
|
onGridChange={(enabled) => { gridEnabled = enabled; persistMetadata(); }}
|
||||||
>
|
>
|
||||||
{#snippet renderObject(obj)}
|
{#snippet renderObject(obj)}
|
||||||
{@const trait = obj.id}
|
{@const trait = obj.id}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue