import { create } from 'zustand' import type { Viewport } from '@/lib/geometry' import type { Plant } from '@/lib/plants' import type { EditorPlanting } from '@/lib/plantings' 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 // The editor's one primary activity (#99). On mobile this is the segmented // control at the bottom of the screen; it decides which tools dock there — // placing fixtures, placing plants, the journal, or the assistant — so the four // activities stop competing for the same cramped strip. Desktop keeps its // side-column layout and treats this as a lighter hint. export type EditorMode = 'fixtures' | 'plants' | 'journal' | 'assistant' interface EditorState { viewport: Viewport setViewport: (next: Viewport | ((prev: Viewport) => Viewport)) => void // The primary editor mode (mobile mode bar). Ephemeral — which tool you were // last using is not a property of the garden. mode: EditorMode setMode: (mode: EditorMode) => void // The selected object OR plop (mutually exclusive; selecting one clears the // other). selectedId is a garden object; selectedPlantingId is a plop. selectedId: number | null select: (id: number | null) => void selectedPlantingId: number | null selectPlanting: (id: number | null) => void focusedObjectId: number | null setFocusedObject: (id: number | null) => void // Which rail tab is showing, or null when the rail is closed. Selecting an // object switches this to 'inspector' (see GardenEditorPage), so editing never // starts with a click on the rail itself. railTab: string | null setRailTab: (tab: string | null) => void // Which season the canvas is showing: null is "now" (live and editable), a // year is a read-only view of what was in the ground that year. Ephemeral — // which year you were last looking at is not a property of the garden. seasonYear: number | null setSeasonYear: (year: number | null) => void // Which bed the journal tab is filtered to, or null for the whole garden. // Separate from selectedId deliberately: you can scope the journal to a bed // and then select something else without the list moving under you. journalObjectId: number | null setJournalObjectId: (id: number | null) => void // The plant armed for placing plops (set after the PlantPicker choice); stays // armed for repeat-placement until cleared (Escape / done). null = not placing. armedPlant: Plant | null // Which seed lot placements should be attributed to, when the armed plant has // one worth naming. Cleared with the plant. armedLotId: number | null setArmedPlant: (p: Plant | null, lotId?: 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 same, for a plop being moved/resized. livePlanting: EditorPlanting | null setLivePlanting: (p: EditorPlanting | 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 // Clear all transient (non-persisted) editor state at once — selection, focus, // armed plant/kind, and any in-flight live geometry — when entering a garden // view fresh (e.g. the public read-only page on mount). The viewport is left // alone; the canvas re-fits it from the loaded garden. resetTransient: () => void } export const useEditorStore = create((set) => ({ viewport: { tx: 0, ty: 0, scale: 1 }, setViewport: (next) => set((s) => ({ viewport: typeof next === 'function' ? next(s.viewport) : next })), mode: 'fixtures', setMode: (mode) => set({ mode }), selectedId: null, select: (id) => set({ selectedId: id, selectedPlantingId: null }), selectedPlantingId: null, selectPlanting: (id) => set({ selectedPlantingId: id, selectedId: null }), focusedObjectId: null, setFocusedObject: (id) => set({ focusedObjectId: id }), railTab: null, setRailTab: (tab) => set({ railTab: tab }), seasonYear: null, setSeasonYear: (year) => set({ seasonYear: year }), journalObjectId: null, setJournalObjectId: (id) => set({ journalObjectId: id }), armedPlant: null, armedLotId: null, setArmedPlant: (p, lotId = null) => set({ armedPlant: p, armedLotId: p ? lotId : null }), liveObject: null, setLiveObject: (o) => set({ liveObject: o }), livePlanting: null, setLivePlanting: (p) => set({ livePlanting: p }), armedKind: null, setArmedKind: (kind) => set({ armedKind: kind }), objectDragging: false, setObjectDragging: (v) => set({ objectDragging: v }), resetTransient: () => set({ selectedId: null, selectedPlantingId: null, focusedObjectId: null, armedPlant: null, armedLotId: null, armedKind: null, liveObject: null, livePlanting: null, railTab: null, seasonYear: null, journalObjectId: null, mode: 'fixtures', }), }))