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:
@@ -1,13 +1,31 @@
|
||||
import { getRouteApi } from '@tanstack/react-router'
|
||||
import { PageStub } from '@/components/PageStub'
|
||||
import { GardenCanvas } from '@/editor/GardenCanvas'
|
||||
import type { EditorGarden, EditorObject } from '@/editor/types'
|
||||
|
||||
const routeApi = getRouteApi('/gardens/$gardenId')
|
||||
// Mock scene for the canvas foundation (#9). #11 replaces this with the garden's
|
||||
// real /full payload keyed on the route's gardenId.
|
||||
const mockGarden: EditorGarden = { id: 0, name: 'Demo garden', widthCm: 1200, heightCm: 800, unitPref: 'metric' }
|
||||
|
||||
const mockObjects: EditorObject[] = [
|
||||
{ id: 1, kind: 'bed', name: 'Bed A', shape: 'rect', xCm: 300, yCm: 250, widthCm: 400, heightCm: 120, rotationDeg: 0, zIndex: 1, plantable: true },
|
||||
{ id: 2, kind: 'bed', name: 'Bed B', shape: 'rect', xCm: 300, yCm: 450, widthCm: 400, heightCm: 120, rotationDeg: 0, zIndex: 1, plantable: true },
|
||||
{ id: 3, kind: 'container', name: 'Pot', shape: 'circle', xCm: 800, yCm: 250, widthCm: 120, heightCm: 120, rotationDeg: 0, zIndex: 2, plantable: true },
|
||||
{ id: 4, kind: 'path', name: '', shape: 'rect', xCm: 600, yCm: 400, widthCm: 60, heightCm: 720, rotationDeg: 0, zIndex: 0, plantable: false },
|
||||
{ id: 5, kind: 'tree', name: 'Apple', shape: 'circle', xCm: 980, yCm: 620, widthCm: 200, heightCm: 200, rotationDeg: 0, zIndex: 2, plantable: false },
|
||||
{ id: 6, kind: 'bed', name: 'Angled', shape: 'rect', xCm: 920, yCm: 480, widthCm: 260, heightCm: 100, rotationDeg: 30, zIndex: 1, plantable: true },
|
||||
]
|
||||
|
||||
export function GardenEditorPage() {
|
||||
const { gardenId } = routeApi.useParams()
|
||||
return (
|
||||
<PageStub title={`Garden ${gardenId}`}>
|
||||
The field editor (canvas, palette, plops) arrives across issues #9–#15.
|
||||
</PageStub>
|
||||
<div className="flex h-[calc(100vh-10rem)] flex-col gap-3">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||
<h1 className="text-lg font-semibold tracking-tight">Editor preview</h1>
|
||||
<span className="text-sm text-muted">
|
||||
Mock garden — drag to pan, wheel/pinch to zoom, tap an object to select. Real data + editing land in #11.
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1">
|
||||
<GardenCanvas garden={mockGarden} objects={mockObjects} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user