diff --git a/frontend/src/lib/components/canvas/Canvas.svelte b/frontend/src/lib/components/canvas/Canvas.svelte index 036eb53..9b2bd83 100644 --- a/frontend/src/lib/components/canvas/Canvas.svelte +++ b/frontend/src/lib/components/canvas/Canvas.svelte @@ -11,6 +11,7 @@ clampZoom, ZOOM_MIN, ZOOM_MAX, + ZOOM_FIT_MIN, } from './types.js'; /** @@ -211,21 +212,23 @@ return; } - // Left button on empty space = lasso or deselect - if (!spaceHeld) { - if (e.shiftKey) { - // Start lasso - isLassoing = true; - lassoStart = { x: sx, y: sy }; - lassoEnd = { x: sx, y: sy }; - } else { - // Deselect all - selectedIds = new Set(); - onSelectionChange?.([]); - } - containerEl.setPointerCapture(e.pointerId); - e.preventDefault(); + // Left button on empty space = pan (or lasso with shift) + if (e.shiftKey) { + // Shift+drag = lasso selection + isLassoing = true; + lassoStart = { x: sx, y: sy }; + lassoEnd = { x: sx, y: sy }; + } else { + // Normal drag on empty space = pan + isPanning = true; + panStart = { x: e.clientX, y: e.clientY }; + cameraStart = { x: camera.x, y: camera.y }; + // Deselect all + selectedIds = new Set(); + onSelectionChange?.([]); } + containerEl.setPointerCapture(e.pointerId); + e.preventDefault(); } } diff --git a/frontend/src/lib/components/canvas/types.ts b/frontend/src/lib/components/canvas/types.ts index f0698f9..7edb1a2 100644 --- a/frontend/src/lib/components/canvas/types.ts +++ b/frontend/src/lib/components/canvas/types.ts @@ -113,8 +113,10 @@ export function visibleObjects( } /** Clamp zoom to allowed range */ -export const ZOOM_MIN = 0.1; +export const ZOOM_MIN = 0.05; export const ZOOM_MAX = 3.0; +/** Lower bound for zoomToFit — allows fitting very spread-out layouts */ +export const ZOOM_FIT_MIN = 0.02; export function clampZoom(zoom: number): number { return Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, zoom)); diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 0eea6e5..6142bed 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -8,7 +8,7 @@ // Canvas + BlockShell import Canvas from '$lib/components/canvas/Canvas.svelte'; - import type { CanvasObject } from '$lib/components/canvas/types.js'; + import type { CanvasObject, Camera } from '$lib/components/canvas/types.js'; import BlockShell from '$lib/components/blockshell/BlockShell.svelte'; // Workspace layout @@ -76,6 +76,11 @@ // Load theme preferences if (res.metadata) { loadThemeFromMetadata(res.metadata as Record); + // Load saved camera position + const cam = (res.metadata as Record).camera as Camera | undefined; + if (cam && typeof cam.x === 'number') { + savedCamera = cam; + } } }) .catch((err) => { @@ -97,6 +102,7 @@ let layout = $state({ panels: [] }); let layoutInitialized = $state(false); + let savedCamera = $state({ x: 0, y: 0, zoom: 1.0 }); // When workspace node appears in store (after creation), load its layout $effect(() => { @@ -141,6 +147,7 @@ metadata: { ...currentMeta, workspace_layout: layout, + camera: savedCamera, preferences: { ...(currentMeta.preferences ?? {}), theme: { hueBg: themeHueBg, hueSurface: themeHueSurface, hueAccent: themeHueAccent }, @@ -778,6 +785,8 @@ objects={canvasObjects} onObjectMove={handleObjectMove} grid={{ enabled: false, size: 20 }} + initialCamera={savedCamera} + onCameraChange={(cam) => { savedCamera = cam; persistMetadata(); }} > {#snippet renderObject(obj)} {@const trait = obj.id}