import { useEffect, useRef, type PointerEvent as ReactPointerEvent, type RefObject } from 'react' import { screenToWorld, snapLocalToBedGrid, worldToLocal, type Point } from '@/lib/geometry' import { useUpdatePlanting } from '@/lib/objects' import type { EditorPlanting } from '@/lib/plantings' import { HANDLE_PX, MIN_RADIUS_CM, SELECT_COLOR, objectTransform } from './shared' import { useEditorStore } from './store' import type { EditorObject } from './types' const MAX_RADIUS_CM = 10_000 /** * Move/resize handles for the selected plop, rendered inside its parent object's * translate+rotate group so the plop stays in the object's local frame. Drag the * body to move (clamped to the object's bounds); drag the edge handle to resize * the radius. Each gesture updates livePlanting for instant feedback and fires * one PATCH on release — the same contract as SelectionOverlay (#11). */ export function PlopOverlay({ plop, object, gardenId, svgRef, snap, gridCm, }: { plop: EditorPlanting object: EditorObject gardenId: number svgRef: RefObject // The parent bed's grid: when snap is true, moving the plop snaps it to the // bed grid (radius stays free either way). snap: boolean gridCm: number }) { const setLivePlanting = useEditorStore((s) => s.setLivePlanting) const setObjectDragging = useEditorStore((s) => s.setObjectDragging) const scale = useEditorStore((s) => s.viewport.scale) const update = useUpdatePlanting(gardenId) const cleanupRef = useRef<(() => void) | null>(null) useEffect( () => () => { cleanupRef.current?.() cleanupRef.current = null }, [], ) const handleCm = HANDLE_PX / scale const halfW = object.widthCm / 2 const halfH = object.heightCm / 2 const center: Point = { x: object.xCm, y: object.yCm } const rot = object.rotationDeg // Pointer (client) → the object's local frame, snapshotting the svg rect once // per gesture (the object doesn't move during a plop drag). const makePointerLocal = () => { const rect = svgRef.current?.getBoundingClientRect() return (e: { clientX: number; clientY: number }): Point => { const vp = useEditorStore.getState().viewport const canvas = rect ? { x: e.clientX - rect.left, y: e.clientY - rect.top } : { x: e.clientX, y: e.clientY } return worldToLocal(screenToWorld(canvas, vp), center, rot) } } const begin = ( e: ReactPointerEvent, onMove: (e: PointerEvent) => EditorPlanting, fields: (final: EditorPlanting) => Parameters[0], ) => { e.stopPropagation() e.preventDefault() setObjectDragging(true) const move = (ev: PointerEvent) => setLivePlanting(onMove(ev)) const detach = () => { window.removeEventListener('pointermove', move) window.removeEventListener('pointerup', finish) window.removeEventListener('pointercancel', finish) } const finish = () => { detach() cleanupRef.current = null const final = useEditorStore.getState().livePlanting setObjectDragging(false) setLivePlanting(null) if (final) update.mutate(fields(final)) } cleanupRef.current = () => { detach() setObjectDragging(false) setLivePlanting(null) } window.addEventListener('pointermove', move) window.addEventListener('pointerup', finish) window.addEventListener('pointercancel', finish) } const base = { ...plop } const startMove = (e: ReactPointerEvent) => { const ptr = makePointerLocal() const start = ptr(e.nativeEvent) begin( e, (ev) => { const p = ptr(ev) const moved: Point = { x: base.xCm + (p.x - start.x), y: base.yCm + (p.y - start.y) } // snapLocalToBedGrid clamps either way; step 0 (snapping off) makes it a // pure clamp, so both paths go through one helper. const next = snapLocalToBedGrid(moved, snap ? gridCm : 0, halfW, halfH) return { ...base, xCm: next.x, yCm: next.y } }, (f) => ({ id: base.id, version: base.version, xCm: f.xCm, yCm: f.yCm }), ) } const startResize = (e: ReactPointerEvent) => { const ptr = makePointerLocal() begin( e, (ev) => { const p = ptr(ev) const r = Math.max(MIN_RADIUS_CM, Math.min(MAX_RADIUS_CM, Math.hypot(p.x - base.xCm, p.y - base.yCm))) return { ...base, radiusCm: r } }, (f) => ({ id: base.id, version: base.version, radiusCm: f.radiusCm }), ) } const r = plop.radiusCm return ( {/* Transparent body: drag to move (at least handle-sized so tiny plops stay grabbable). */} {/* Selection ring. */} {/* Radius handle at the local +x edge. */} ) }