{"version":3,"file":"usePlanHistory.mjs","names":[],"sources":["../../../../package/common/roomDrawer/usePlanHistory.ts"],"sourcesContent":["import { useCallback, useRef, useState } from 'react';\n\nimport type { FloorPlan } from '../floorPlan';\nimport type { RoomDrawerChangeReason } from './roomDrawer.types';\n\n/**\n * Undo/redo over plan snapshots.\n *\n * Snapshots rather than command objects: every mutator already returns a fresh\n * immutable plan with structural sharing, so a snapshot costs a handful of pointers.\n * Commands would mean writing and testing an inverse for each of fifteen operations,\n * and an `addWall` that split three other walls on its way has no cheap inverse at all.\n *\n * The gesture API is what keeps a 200-move drag to ONE entry: `begin` stashes the plan\n * as it was, `update` replaces the present without pushing, and `end` pushes that single\n * stashed snapshot. A gesture that was aborted, or that ended where it started, pushes\n * nothing.\n */\n\n/** Discrete edits of the same kind on the same target merge if they land within this. */\nconst MERGE_WINDOW_MS = 500;\n\nexport interface PlanHistory {\n  present: FloorPlan;\n  canUndo: boolean;\n  canRedo: boolean;\n\n  /** Stashes the current plan as the snapshot a later `endGesture` would restore to. */\n  beginGesture: () => void;\n  /** Replaces the present mid-gesture. Pushes nothing. */\n  updateGesture: (plan: FloorPlan) => void;\n  /** Pushes the stashed snapshot, if the gesture actually changed anything. */\n  endGesture: () => boolean;\n  /** Discards the gesture and restores the stashed snapshot. */\n  cancelGesture: () => FloorPlan | null;\n\n  /** A discrete edit: pushes and replaces atomically. */\n  commit: (plan: FloorPlan, reason: RoomDrawerChangeReason, targetIds?: string[]) => void;\n  /** Replaces the present without touching history, for an externally driven value. */\n  replace: (plan: FloorPlan) => void;\n\n  undo: () => FloorPlan | null;\n  redo: () => FloorPlan | null;\n  clearHistory: () => void;\n  isGestureActive: () => boolean;\n}\n\nexport function usePlanHistory(initial: FloorPlan, limit = 50): PlanHistory {\n  const [present, setPresent] = useState<FloorPlan>(initial);\n  const pastRef = useRef<FloorPlan[]>([]);\n  const futureRef = useRef<FloorPlan[]>([]);\n  const gestureStartRef = useRef<FloorPlan | null>(null);\n  const presentRef = useRef<FloorPlan>(initial);\n  const lastCommitRef = useRef<{ reason: string; targets: string; at: number } | null>(null);\n  const [, forceCounter] = useState(0);\n\n  const setBoth = useCallback((plan: FloorPlan) => {\n    presentRef.current = plan;\n    setPresent(plan);\n  }, []);\n\n  const push = useCallback(\n    (snapshot: FloorPlan) => {\n      pastRef.current = [...pastRef.current, snapshot].slice(-limit);\n      futureRef.current = [];\n    },\n    [limit],\n  );\n\n  const beginGesture = useCallback(() => {\n    gestureStartRef.current = presentRef.current;\n  }, []);\n\n  const updateGesture = useCallback(\n    (plan: FloorPlan) => {\n      setBoth(plan);\n    },\n    [setBoth],\n  );\n\n  const endGesture = useCallback(() => {\n    const start = gestureStartRef.current;\n    gestureStartRef.current = null;\n    if (!start || start === presentRef.current) return false;\n    push(start);\n    lastCommitRef.current = null;\n    forceCounter((n) => n + 1);\n    return true;\n  }, [push]);\n\n  const cancelGesture = useCallback(() => {\n    const start = gestureStartRef.current;\n    gestureStartRef.current = null;\n    if (!start) return null;\n    setBoth(start);\n    return start;\n  }, [setBoth]);\n\n  const commit = useCallback(\n    (plan: FloorPlan, reason: RoomDrawerChangeReason, targetIds: string[] = []) => {\n      if (plan === presentRef.current) return;\n      const targets = targetIds.join(',');\n      const now = Date.now();\n      const last = lastCommitRef.current;\n      // Typing a room name would otherwise flood the stack with one entry per keystroke.\n      const mergeable =\n        last !== null &&\n        last.reason === reason &&\n        last.targets === targets &&\n        now - last.at < MERGE_WINDOW_MS &&\n        pastRef.current.length > 0;\n\n      if (!mergeable) push(presentRef.current);\n      else futureRef.current = [];\n\n      lastCommitRef.current = { reason, targets, at: now };\n      setBoth(plan);\n      forceCounter((n) => n + 1);\n    },\n    [push, setBoth],\n  );\n\n  const replace = useCallback(\n    (plan: FloorPlan) => {\n      setBoth(plan);\n    },\n    [setBoth],\n  );\n\n  const undo = useCallback(() => {\n    const past = pastRef.current;\n    if (past.length === 0) return null;\n    const previous = past[past.length - 1];\n    pastRef.current = past.slice(0, -1);\n    futureRef.current = [presentRef.current, ...futureRef.current];\n    lastCommitRef.current = null;\n    setBoth(previous);\n    forceCounter((n) => n + 1);\n    return previous;\n  }, [setBoth]);\n\n  const redo = useCallback(() => {\n    const future = futureRef.current;\n    if (future.length === 0) return null;\n    const next = future[0];\n    futureRef.current = future.slice(1);\n    pastRef.current = [...pastRef.current, presentRef.current].slice(-limit);\n    lastCommitRef.current = null;\n    setBoth(next);\n    forceCounter((n) => n + 1);\n    return next;\n  }, [limit, setBoth]);\n\n  const clearHistory = useCallback(() => {\n    pastRef.current = [];\n    futureRef.current = [];\n    lastCommitRef.current = null;\n    forceCounter((n) => n + 1);\n  }, []);\n\n  const isGestureActive = useCallback(() => gestureStartRef.current !== null, []);\n\n  return {\n    present,\n    canUndo: pastRef.current.length > 0,\n    canRedo: futureRef.current.length > 0,\n    beginGesture,\n    updateGesture,\n    endGesture,\n    cancelGesture,\n    commit,\n    replace,\n    undo,\n    redo,\n    clearHistory,\n    isGestureActive,\n  };\n}\n"],"mappings":"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"}