ai-chatbot/packages/editor/src/components/ui/panels/roof-segment-panel.tsx
2026-04-23 10:21:33 +05:30

312 lines
9.0 KiB
TypeScript
Executable File

'use client'
import {
type AnyNode,
type AnyNodeId,
type RoofSegmentNode,
RoofSegmentNode as RoofSegmentNodeSchema,
type RoofType,
useScene,
} from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { Copy, Move, Trash2 } 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 { PanelSection } from '../controls/panel-section'
import { SegmentedControl } from '../controls/segmented-control'
import { SliderControl } from '../controls/slider-control'
import { PanelWrapper } from './panel-wrapper'
const ROOF_TYPE_OPTIONS: { label: string; value: RoofType }[] = [
{ label: 'Hip', value: 'hip' },
{ label: 'Gable', value: 'gable' },
{ label: 'Shed', value: 'shed' },
{ label: 'Flat', value: 'flat' },
]
const ROOF_TYPE_OPTIONS_2: { label: string; value: RoofType }[] = [
{ label: 'Gambrel', value: 'gambrel' },
{ label: 'Dutch', value: 'dutch' },
{ label: 'Mansard', value: 'mansard' },
]
export function RoofSegmentPanel() {
const selectedId = useViewer((s) => s.selection.selectedIds[0])
const setSelection = useViewer((s) => s.setSelection)
const updateNode = useScene((s) => s.updateNode)
const setMovingNode = useEditor((s) => s.setMovingNode)
const node = useScene((s) =>
selectedId ? (s.nodes[selectedId as AnyNode['id']] as RoofSegmentNode | undefined) : undefined,
)
const handleUpdate = useCallback(
(updates: Partial<RoofSegmentNode>) => {
if (!selectedId) return
updateNode(selectedId as AnyNode['id'], updates)
},
[selectedId, updateNode],
)
const handleClose = useCallback(() => {
setSelection({ selectedIds: [] })
}, [setSelection])
const handleBack = useCallback(() => {
if (node?.parentId) {
setSelection({ selectedIds: [node.parentId] })
}
}, [node?.parentId, setSelection])
const handleDuplicate = useCallback(() => {
if (!node?.parentId) return
sfxEmitter.emit('sfx:item-pick')
let duplicateInfo = structuredClone(node) as any
delete duplicateInfo.id
duplicateInfo.metadata = { ...duplicateInfo.metadata, isNew: true }
// Offset slightly so it's visible
duplicateInfo.position = [
duplicateInfo.position[0] + 1,
duplicateInfo.position[1],
duplicateInfo.position[2] + 1,
]
try {
const duplicate = RoofSegmentNodeSchema.parse(duplicateInfo)
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
setSelection({ selectedIds: [] })
setMovingNode(duplicate)
} catch (e) {
console.error('Failed to duplicate roof segment', e)
}
}, [node, setSelection, setMovingNode])
const handleMove = useCallback(() => {
if (node) {
sfxEmitter.emit('sfx:item-pick')
setMovingNode(node)
setSelection({ selectedIds: [] })
}
}, [node, setMovingNode, setSelection])
const handleDelete = useCallback(() => {
if (!(selectedId && node)) return
sfxEmitter.emit('sfx:item-delete')
const parentId = node.parentId
useScene.getState().deleteNode(selectedId as AnyNodeId)
if (parentId) {
useScene.getState().dirtyNodes.add(parentId as AnyNodeId)
setSelection({ selectedIds: [parentId] })
} else {
setSelection({ selectedIds: [] })
}
}, [selectedId, node, setSelection])
if (!(node && node.type === 'roof-segment' && selectedId)) return null
return (
<PanelWrapper
icon="/icons/roof.png"
onBack={handleBack}
onClose={handleClose}
title={node.name || 'Roof Segment'}
width={300}
>
<PanelSection title="Roof Type">
<SegmentedControl
onChange={(v) => handleUpdate({ roofType: v })}
options={ROOF_TYPE_OPTIONS}
value={node.roofType}
/>
<SegmentedControl
onChange={(v) => handleUpdate({ roofType: v })}
options={ROOF_TYPE_OPTIONS_2}
value={node.roofType}
/>
</PanelSection>
<PanelSection title="Footprint">
<SliderControl
label="Width"
max={25}
min={0.5}
onChange={(v) => handleUpdate({ width: v })}
precision={2}
step={0.5}
unit="m"
value={Math.round(node.width * 100) / 100}
/>
<SliderControl
label="Depth"
max={25}
min={0.5}
onChange={(v) => handleUpdate({ depth: v })}
precision={2}
step={0.5}
unit="m"
value={Math.round(node.depth * 100) / 100}
/>
</PanelSection>
<PanelSection title="Heights">
<SliderControl
label="Wall"
max={5}
min={0}
onChange={(v) => handleUpdate({ wallHeight: v })}
precision={2}
step={0.1}
unit="m"
value={Math.round(node.wallHeight * 100) / 100}
/>
<SliderControl
label="Roof"
max={15}
min={0}
onChange={(v) => handleUpdate({ roofHeight: v })}
precision={2}
step={0.1}
unit="m"
value={Math.round(node.roofHeight * 100) / 100}
/>
</PanelSection>
<PanelSection title="Structure">
<SliderControl
label="Wall Thick."
max={1}
min={0.05}
onChange={(v) => handleUpdate({ wallThickness: v })}
precision={2}
step={0.05}
unit="m"
value={Math.round(node.wallThickness * 100) / 100}
/>
<SliderControl
label="Deck Thick."
max={0.3}
min={0.04}
onChange={(v) => handleUpdate({ deckThickness: v })}
precision={2}
step={0.01}
unit="m"
value={Math.round(node.deckThickness * 100) / 100}
/>
<SliderControl
label="Overhang"
max={1}
min={0}
onChange={(v) => handleUpdate({ overhang: v })}
precision={2}
step={0.05}
unit="m"
value={Math.round(node.overhang * 100) / 100}
/>
<SliderControl
label="Shingle Thick."
max={0.3}
min={0.02}
onChange={(v) => handleUpdate({ shingleThickness: v })}
precision={2}
step={0.01}
unit="m"
value={Math.round(node.shingleThickness * 100) / 100}
/>
</PanelSection>
<PanelSection title="Position">
<SliderControl
label="X"
max={50}
min={-50}
onChange={(v) => {
const pos = [...node.position] as [number, number, number]
pos[0] = v
handleUpdate({ position: pos })
}}
precision={2}
step={0.05}
unit="m"
value={Math.round(node.position[0] * 100) / 100}
/>
<SliderControl
label="Y"
max={50}
min={-50}
onChange={(v) => {
const pos = [...node.position] as [number, number, number]
pos[1] = v
handleUpdate({ position: pos })
}}
precision={2}
step={0.05}
unit="m"
value={Math.round(node.position[1] * 100) / 100}
/>
<SliderControl
label="Z"
max={50}
min={-50}
onChange={(v) => {
const pos = [...node.position] as [number, number, number]
pos[2] = v
handleUpdate({ position: pos })
}}
precision={2}
step={0.05}
unit="m"
value={Math.round(node.position[2] * 100) / 100}
/>
<SliderControl
label="Rotation"
max={180}
min={-180}
onChange={(degrees) => {
handleUpdate({ rotation: (degrees * Math.PI) / 180 })
}}
precision={0}
step={1}
unit="°"
value={Math.round((node.rotation * 180) / Math.PI)}
/>
<div className="flex gap-1.5 px-1 pt-2 pb-1">
<ActionButton
label="-45°"
onClick={() => {
sfxEmitter.emit('sfx:item-rotate')
handleUpdate({ rotation: node.rotation - Math.PI / 4 })
}}
/>
<ActionButton
label="+45°"
onClick={() => {
sfxEmitter.emit('sfx:item-rotate')
handleUpdate({ rotation: node.rotation + Math.PI / 4 })
}}
/>
</div>
</PanelSection>
<PanelSection title="Actions">
<ActionGroup>
<ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
<ActionButton
icon={<Copy className="h-3.5 w-3.5" />}
label="Duplicate"
onClick={handleDuplicate}
/>
<ActionButton
className="hover:bg-red-500/20"
icon={<Trash2 className="h-3.5 w-3.5 text-red-400" />}
label="Delete"
onClick={handleDelete}
/>
</ActionGroup>
</PanelSection>
</PanelWrapper>
)
}