Add canvas foundation: SVG viewport, pan/zoom/pinch, geometry lib (#9)
The editor's technically-riskiest slice, built against mock data so #11 only adds interactions on top. - lib/geometry.ts (+ vitest): world<->screen and object-local<->world transforms, clampScale, zoomViewportAt (the wheel/pinch "keep the point under the cursor stationary" math), zoomToFitRect. 11 geometry tests (round-trips, rotation, cursor-anchored zoom, fit) + 6 units tests. - editor/store.ts: Zustand store for ephemeral editor state — viewport, selection, focusedObjectId (selection interactions grow in #11). - editor/useViewport.ts: @use-gesture wiring — wheel + pinch zoom toward the pointer, drag-pan, scale clamped to [0.05, 20] px/cm, and an animated fitToRect (eased rAF tween). touch-action:none so the browser doesn't fight gestures. - editor/ObjectShape.tsx: rect/circle centered + rotated, kind default colors, name label; non-scaling strokes so outlines stay 1px at any zoom. - editor/GardenCanvas.tsx: full-size svg, single world <g>, fading 1m grid, garden boundary, z-ordered objects; ResizeObserver-driven auto-fit, a zoom readout, and a Fit control. Deps: zustand, @use-gesture/react, vitest (+ test scripts). - GardenEditorPage renders the canvas with a mock scene (#11 swaps in /full). Verified in a real browser: wheel zoom keeps the world point under the cursor stationary (sub-cm drift on a 12m garden), a real drag pans, and Fit animates to frame the garden. tsc + vitest green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { create } from 'zustand'
|
||||
import type { Viewport } from '@/lib/geometry'
|
||||
|
||||
// Ephemeral editor state only (per DESIGN § State): the viewport, the current
|
||||
// selection, and which object is "focused" (zoomed into for plops, #15). All
|
||||
// server state stays in react-query. Selection interactions grow in #11.
|
||||
|
||||
export const MIN_SCALE = 0.05 // px per cm — fully zoomed out
|
||||
export const MAX_SCALE = 20 // px per cm — fully zoomed in
|
||||
|
||||
interface EditorState {
|
||||
viewport: Viewport
|
||||
setViewport: (next: Viewport | ((prev: Viewport) => Viewport)) => void
|
||||
|
||||
selectedId: number | null
|
||||
select: (id: number | null) => void
|
||||
|
||||
focusedObjectId: number | null
|
||||
setFocusedObject: (id: number | null) => void
|
||||
}
|
||||
|
||||
export const useEditorStore = create<EditorState>((set) => ({
|
||||
viewport: { tx: 0, ty: 0, scale: 1 },
|
||||
setViewport: (next) =>
|
||||
set((s) => ({ viewport: typeof next === 'function' ? next(s.viewport) : next })),
|
||||
|
||||
selectedId: null,
|
||||
select: (id) => set({ selectedId: id }),
|
||||
|
||||
focusedObjectId: null,
|
||||
setFocusedObject: (id) => set({ focusedObjectId: id }),
|
||||
}))
|
||||
Reference in New Issue
Block a user