Turns the canvas foundation into the real editor, loading a garden from the server and editing it with optimistic sync. Completes Phase 3. - lib/objects.ts: react-query over GET /gardens/:id/full + optimistic create/update/delete. Update applies to the cache up front and PATCHes with the row version; a 409 adopts the server's `current` row and toasts "updated elsewhere". One PATCH per gesture. - editor/store.ts: adds liveObject (in-gesture geometry, rendered instantly), armedKind (palette tap-to-place), and objectDragging (so the pan gesture stands down while an object drag owns the pointer). - editor/Palette.tsx + kinds.ts: the seven kinds with icons + default sizes; tap a kind, tap the canvas to place (works desktop + touch). - editor/SelectionOverlay.tsx: move (drag body), resize (corner handles, opposite corner fixed via the object-local frame so rotation is honored), rotate (knob, snaps to 15°, free with Shift). Each updates liveObject and fires one PATCH on release. - editor/Inspector.tsx: name, dimensions/position (unit-aware), rotation, color override (+ clear), plantable, notes, two-step delete. Commits per field; re-syncs from the object unless a field is focused. - GardenCanvas: placement, live-drag merge, z-ordered render, selection overlay, and ?focus=<id> frames an object on load. GardenEditorPage loads /full and lays out palette | canvas | inspector (bottom sheet on mobile). - components/ui/toast.tsx + Toaster in AppShell. Verified in a real browser against the embedded binary: place a bed → move, resize, rotate (rotate snapped to 60°) → set its name in the inspector → reload, all persisted; each gesture was exactly one PATCH (version 1→2→3→4→5); an out-of-band edit made the next drag 409 → rollback to the server's value + toast. tsc + 17 vitest tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
57 lines
1.9 KiB
TypeScript
57 lines
1.9 KiB
TypeScript
import { create } from 'zustand'
|
|
import type { Viewport } from '@/lib/geometry'
|
|
import type { EditorObject } from './types'
|
|
|
|
// Ephemeral editor state only (per DESIGN § State): the viewport, the current
|
|
// selection, the object being live-edited mid-gesture, and the palette kind
|
|
// armed for tap-to-place. All server state stays in react-query.
|
|
|
|
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
|
|
|
|
// During a move/resize/rotate, the object's live geometry is held here so the
|
|
// canvas renders it instantly; the PATCH fires only on gesture end.
|
|
liveObject: EditorObject | null
|
|
setLiveObject: (o: EditorObject | null) => void
|
|
|
|
// The palette kind armed for tap-to-place (mobile-friendly; also set while
|
|
// dragging a kind from the palette). null when nothing is armed.
|
|
armedKind: string | null
|
|
setArmedKind: (kind: string | null) => void
|
|
|
|
// True while an object move/resize/rotate is in progress, so the viewport's
|
|
// pan gesture stands down (both listen on the same svg).
|
|
objectDragging: boolean
|
|
setObjectDragging: (v: boolean) => 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 }),
|
|
|
|
liveObject: null,
|
|
setLiveObject: (o) => set({ liveObject: o }),
|
|
|
|
armedKind: null,
|
|
setArmedKind: (kind) => set({ armedKind: kind }),
|
|
|
|
objectDragging: false,
|
|
setObjectDragging: (v) => set({ objectDragging: v }),
|
|
}))
|