import { useMemo } from 'react'; import type { ElementModel, StagedDocument, Strategy, StrategyKind } from '@/lib/engine'; import { STRATEGIES, STRATEGY_ORDER, eligibleStrategies, strategySupportsAtomic, type ParamControl, } from '@/lib/engine'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Slider } from '@/components/ui/slider'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '@/components/ui/accordion'; import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Button } from '@/components/ui/button'; import { setElementStrategy, setElementParams, setParamsForSelection, setPlanningForSelection, setHole, } from './staging-actions'; import { computeHoleMap, netFillArea } from '@/lib/engine'; interface Props { doc: StagedDocument; selectedIds: Set; reporters: string[]; update: (fn: (doc: StagedDocument) => StagedDocument) => void; } function paramsOf(strategy: Strategy): Record { return strategy.kind === 'skip' ? {} : ((strategy as Extract).params as unknown as Record< string, unknown >); } function ControlRow({ control, value, reporters, onChange, }: { control: ParamControl; value: unknown; reporters: string[]; onChange: (key: string, value: unknown) => void; }) { const label = ( } > {control.label} {control.tooltip} ); if (control.kind === 'slider') { const v = typeof value === 'number' ? value : control.min; return (
{label} {v} {control.unit ? ` ${control.unit}` : ''}
{ const raw = Array.isArray(vals) ? vals[0] : (vals as number); onChange(control.key, raw); }} aria-label={control.label} />
); } if (control.kind === 'switch') { return (
{label} onChange(control.key, c)} aria-label={control.label} />
); } // select — directional field gets dynamic options let options = control.options; if (control.key === 'field') { options = [ { value: '', label: 'insert scaffold' }, ...reporters.map((r) => ({ value: r, label: `@${r}` })), ]; } const current = control.key === 'field' ? ((value as string | null) ?? '') : String(value ?? ''); return (
{label}
); } function HolePanel({ el, update }: { el: ElementModel; update: Props['update'] }) { if (el.rings.length < 2) return null; return ( Holes ({el.rings.length} rings)
{el.rings.map((_, i) => { const h = el.holeMap[i]; return (
ring {i} · depth {h?.depth ?? 0} · {h?.orientation ?? 'ccw'} { const v = vals[0]; if (v) update((d) => setHole(d, el.id, i, v === 'hole')); }} > Solid Hole
); })}
); } function StrategySelect({ geomType, role, value, hasGradient, onChange, }: { geomType: ElementModel['geomType']; role: ElementModel['role']; value: StrategyKind; hasGradient: boolean; onChange: (k: StrategyKind) => void; }) { const eligible = new Set(eligibleStrategies(geomType, role, hasGradient)); const order = role === 'relation' ? [value] : STRATEGY_ORDER; return ( ); } function PlanningControls({ elements, selectedIds, update, }: { elements: ElementModel[]; selectedIds: Set; update: Props['update']; }) { const atomicSupported = elements.every((element) => strategySupportsAtomic(element.strategy.kind), ); const atomic = atomicSupported && elements.every((element) => element.atomic); const barrier = elements.every((element) => element.planBarrierBefore); return (
update((doc) => setPlanningForSelection(doc, selectedIds, { atomic: checked })) } aria-label="keep operation atomic during planning" />
update((doc) => setPlanningForSelection(doc, selectedIds, { planBarrierBefore: checked }), ) } aria-label="planner barrier before operation" />
{!atomicSupported && (

Atomic grouping is unavailable for recipes that declare helpers or change thread colour.

)}
); } export default function Inspector({ doc, selectedIds, reporters, update }: Props) { const selected = useMemo( () => doc.operations.filter((operation) => selectedIds.has(operation.id)), [doc.operations, selectedIds], ); // Nothing selected → document defaults hint. if (selected.length === 0) { return (
Select an element to edit its stitch strategy. Document defaults: fabric{' '} {doc.fabric}, geometry tolerance{' '} {doc.geometryToleranceMM} mm.
); } // Multiple selected. if (selected.length > 1) { const kinds = new Set(selected.map((e) => e.strategy.kind)); if (kinds.size > 1) { return (
Editing {selected.length} elements Assign a single strategy to edit parameters. operation.sourceGradient !== null)} onChange={(k) => update((d) => setElementStrategy(d, selectedIds, k))} />
); } const kind = [...kinds][0]; const def = STRATEGIES[kind]; const shared = paramsOf(selected[0].strategy); return (
Editing {selected.length} elements Changes apply to all of them. operation.sourceGradient !== null)} onChange={(k) => update((d) => setElementStrategy(d, selectedIds, k))} />
{def.controls.map((c) => ( update((d) => setParamsForSelection(d, selectedIds, { [key]: value })) } /> ))}
); } // Single selection. const el = selected[0]; const def = STRATEGIES[el.strategy.kind]; const params = paramsOf(el.strategy); return (
{el.name} update((d) => setElementStrategy(d, new Set([el.id]), k))} /> {def.controls.map((c) => ( update((d) => setElementParams(d, el.id, { [key]: value }))} /> ))}
); }