'use client' import { type AnyNode, type AnyNodeId, type FenceNode, getClampedWallCurveOffset, getMaxWallCurveOffset, getWallCurveLength, type MaterialSchema, normalizeWallCurveOffset, useScene, } from '@pascal-app/core' import { useViewer } from '@pascal-app/viewer' import { Move, Spline } from 'lucide-react' import { useCallback } from 'react' import { sfxEmitter } from '../../../lib/sfx-bus' import useEditor from '../../../store/use-editor' import { ActionButton, ActionGroup } from '../controls/action-button' import { MaterialPicker } from '../controls/material-picker' import { PanelSection } from '../controls/panel-section' import { SegmentedControl } from '../controls/segmented-control' import { SliderControl } from '../controls/slider-control' import { PanelWrapper } from './panel-wrapper' type FenceStyleValue = 'slat' | 'rail' | 'privacy' type FenceBaseStyleValue = 'grounded' | 'floating' const FENCE_STYLE_OPTIONS: { label: string; value: FenceStyleValue }[] = [ { label: 'Slat', value: 'slat' }, { label: 'Rail', value: 'rail' }, { label: 'Privacy', value: 'privacy' }, ] const FENCE_BASE_STYLE_OPTIONS: { label: string; value: FenceBaseStyleValue }[] = [ { label: 'Grounded', value: 'grounded' }, { label: 'Floating', value: 'floating' }, ] export function FencePanel() { const selectedId = useViewer((s) => s.selection.selectedIds[0]) const selectedCount = useViewer((s) => s.selection.selectedIds.length) const setSelection = useViewer((s) => s.setSelection) const updateNode = useScene((s) => s.updateNode) const setMovingNode = useEditor((s) => s.setMovingNode) const setCurvingFence = useEditor((s) => s.setCurvingFence) const node = useScene((s) => selectedId ? (s.nodes[selectedId as AnyNode['id']] as FenceNode | undefined) : undefined, ) const handleUpdate = useCallback( (updates: Partial) => { if (!selectedId) return updateNode(selectedId as AnyNode['id'], updates) useScene.getState().dirtyNodes.add(selectedId as AnyNodeId) }, [selectedId, updateNode], ) const handleUpdateLength = useCallback( (newLength: number) => { if (!node || newLength <= 0) return const dx = node.end[0] - node.start[0] const dz = node.end[1] - node.start[1] const currentLength = Math.sqrt(dx * dx + dz * dz) if (currentLength === 0) return const dirX = dx / currentLength const dirZ = dz / currentLength const newEnd: [number, number] = [ node.start[0] + dirX * newLength, node.start[1] + dirZ * newLength, ] handleUpdate({ end: newEnd }) }, [node, handleUpdate], ) const handleClose = useCallback(() => { setSelection({ selectedIds: [] }) }, [setSelection]) const handleMove = useCallback(() => { if (!node) return sfxEmitter.emit('sfx:item-pick') setMovingNode(node) setSelection({ selectedIds: [] }) }, [node, setMovingNode, setSelection]) const handleCurve = useCallback(() => { if (!node) return sfxEmitter.emit('sfx:item-pick') setCurvingFence(node) setSelection({ selectedIds: [] }) }, [node, setCurvingFence, setSelection]) const handleMaterialPresetChange = useCallback( (materialPreset: string) => { handleUpdate({ materialPreset, material: undefined }) }, [handleUpdate], ) const handleCustomMaterialChange = useCallback( (material: MaterialSchema) => { handleUpdate({ material, materialPreset: undefined }) }, [handleUpdate], ) if (!(node && node.type === 'fence' && selectedId && selectedCount === 1)) return null const length = getWallCurveLength(node) const curveOffset = getClampedWallCurveOffset(node) const maxCurveOffset = getMaxWallCurveOffset(node) return ( handleUpdate({ style: value })} options={FENCE_STYLE_OPTIONS} value={node.style} /> handleUpdate({ baseStyle: value })} options={FENCE_BASE_STYLE_OPTIONS} value={node.baseStyle} /> handleUpdate({ curveOffset: normalizeWallCurveOffset(node, value) })} precision={2} step={0.1} unit="m" value={Math.round(curveOffset * 100) / 100} /> handleUpdate({ height: Math.max(0.4, value) })} precision={2} step={0.05} unit="m" value={node.height} /> handleUpdate({ thickness: Math.max(0.03, value) })} precision={3} step={0.005} unit="m" value={node.thickness} /> handleUpdate({ baseHeight: Math.max(0.04, value) })} precision={3} step={0.01} unit="m" value={node.baseHeight} /> handleUpdate({ topRailHeight: Math.max(0.01, value) })} precision={3} step={0.005} unit="m" value={node.topRailHeight} /> handleUpdate({ postSpacing: Math.max(0.2, value) })} precision={2} step={0.05} unit="m" value={node.postSpacing} /> handleUpdate({ postSize: Math.max(0.01, value) })} precision={3} step={0.005} unit="m" value={node.postSize} /> handleUpdate({ groundClearance: Math.max(0, value) })} precision={3} step={0.005} unit="m" value={node.groundClearance} /> handleUpdate({ edgeInset: Math.max(0.005, value) })} precision={3} step={0.005} unit="m" value={node.edgeInset} /> } label="Move" onClick={handleMove} /> } label="Curve" onClick={handleCurve} /> ) }