import { memo, type PointerEvent } from 'react' import type { Plant } from '@/lib/plants' import { computeDerivedCount, type EditorPlanting } from '@/lib/plantings' import { SELECT_COLOR } from './shared' // Semantic-zoom thresholds in px/cm (DESIGN § Editor / rendering), tuned by feel: // < FAR — flat color patch, no text ("what's planted where" at a glance) // FAR..NEAR — colored circle + plant emoji // ≥ NEAR — circle + emoji + plant name + count export const SEMANTIC_FAR = 0.75 export const SEMANTIC_NEAR = 3 const DEFAULT_COLOR = '#6aa84f' /** * One plop, rendered in its parent object's local frame (the caller wraps it in * the object's translate+rotate group, so the plop tracks the bed). A circle in * the plant's color; emoji and name/count fade in with zoom. The count is * computed live from the current radius so it updates while resizing. memo'd so a * pan/zoom that only changes the world transform doesn't re-render every plop. */ export const PlopMarker = memo(function PlopMarker({ plop, plant, scale, selected, onSelect, }: { plop: EditorPlanting plant?: Plant scale: number selected: boolean onSelect: (id: number) => void }) { const color = plant?.color ?? DEFAULT_COLOR const r = Math.max(0, plop.radiusCm) const showIcon = scale >= SEMANTIC_FAR && !!plant?.icon const showText = scale >= SEMANTIC_NEAR && !!plant const count = plop.count ?? (plant ? computeDerivedCount(plop.radiusCm, plant.spacingCm) : plop.derivedCount) // Keep the tap target at least ~44px across even when the plop draws tiny at // low zoom (fill=transparent still receives pointer events, unlike fill=none). const hitR = Math.max(r, 22 / scale) function down(e: PointerEvent) { e.stopPropagation() onSelect(plop.id) } return ( {showIcon && ( {plant!.icon} )} {showText && ( {plant!.name} · {count} )} ) })