Hide inapplicable door and window panel settings

This commit is contained in:
sudhir
2026-05-12 00:05:41 +05:30
parent be41511474
commit d78308b366
2 changed files with 167 additions and 138 deletions
@@ -127,8 +127,11 @@ export function DoorPanel() {
const handleUpdate = useCallback( const handleUpdate = useCallback(
(updates: Partial<DoorNode>) => { (updates: Partial<DoorNode>) => {
if (!(selectedId && node)) return if (!(selectedId && node)) return
const liveNode = useScene.getState().nodes[selectedId as AnyNodeId]
if (liveNode?.type !== 'door') return
const hasChange = Object.entries(updates).some(([key, value]) => { const hasChange = Object.entries(updates).some(([key, value]) => {
const currentValue = node[key as keyof DoorNode] const currentValue = liveNode[key as keyof DoorNode]
return !isSameDoorValue(currentValue, value) return !isSameDoorValue(currentValue, value)
}) })
if (!hasChange) return if (!hasChange) return
@@ -137,7 +140,9 @@ export function DoorPanel() {
useInteractive.getState().removeDoorOpenState(selectedId as AnyNodeId) useInteractive.getState().removeDoorOpenState(selectedId as AnyNodeId)
} }
updateNode(selectedId as AnyNode['id'], updates) updateNode(selectedId as AnyNode['id'], updates)
useScene.getState().dirtyNodes.add(selectedId as AnyNodeId) const scene = useScene.getState()
scene.dirtyNodes.add(selectedId as AnyNodeId)
if (liveNode.parentId) scene.dirtyNodes.add(liveNode.parentId as AnyNodeId)
}, },
[selectedId, node, updateNode], [selectedId, node, updateNode],
) )
@@ -348,7 +353,9 @@ export function DoorPanel() {
const isRollupGarageDoor = doorType === 'garage-rollup' const isRollupGarageDoor = doorType === 'garage-rollup'
const isTiltupGarageDoor = doorType === 'garage-tiltup' const isTiltupGarageDoor = doorType === 'garage-tiltup'
const typeMode = isOpening ? 'opening' : isGarageDoor ? 'garage' : 'door' const typeMode = isOpening ? 'opening' : isGarageDoor ? 'garage' : 'door'
const supportsHandleSide = isSwingDoor const supportsHingeSide = doorType === 'hinged'
const supportsHandleSide = doorType === 'hinged'
const supportsTopShape = !isGarageDoor
const maxDoorWidth = isGarageDoor ? 6 : 3 const maxDoorWidth = isGarageDoor ? 6 : 3
const setOpeningTopRadius = (index: number, value: number, commit = false) => { const setOpeningTopRadius = (index: number, value: number, commit = false) => {
@@ -395,6 +402,7 @@ export function DoorPanel() {
handleSide: 'right', handleSide: 'right',
trackStyle: 'visible', trackStyle: 'visible',
operationState: Math.max(node.operationState ?? 0, 0.65), operationState: Math.max(node.operationState ?? 0, 0.65),
threshold: false,
contentPadding: [0.03, 0.04], contentPadding: [0.03, 0.04],
segments: foldingDoorSegments, segments: foldingDoorSegments,
} }
@@ -411,6 +419,7 @@ export function DoorPanel() {
trackStyle: 'pocket', trackStyle: 'pocket',
slideDirection: node.slideDirection ?? 'left', slideDirection: node.slideDirection ?? 'left',
operationState: node.operationState ?? 0, operationState: node.operationState ?? 0,
threshold: false,
contentPadding: [0.035, 0.045], contentPadding: [0.035, 0.045],
segments: foldingDoorSegments, segments: foldingDoorSegments,
} }
@@ -427,6 +436,7 @@ export function DoorPanel() {
trackStyle: 'visible', trackStyle: 'visible',
slideDirection: node.slideDirection ?? 'left', slideDirection: node.slideDirection ?? 'left',
operationState: node.operationState ?? 0, operationState: node.operationState ?? 0,
threshold: false,
contentPadding: [0.035, 0.045], contentPadding: [0.035, 0.045],
segments: foldingDoorSegments, segments: foldingDoorSegments,
} }
@@ -443,6 +453,7 @@ export function DoorPanel() {
trackStyle: 'visible', trackStyle: 'visible',
slideDirection: node.slideDirection ?? 'left', slideDirection: node.slideDirection ?? 'left',
operationState: node.operationState ?? 0, operationState: node.operationState ?? 0,
threshold: false,
contentPadding: [0.03, 0.04], contentPadding: [0.03, 0.04],
segments: frenchDoorSegments, segments: frenchDoorSegments,
} }
@@ -456,6 +467,7 @@ export function DoorPanel() {
...dimensionUpdates, ...dimensionUpdates,
handle: false, handle: false,
threshold: false, threshold: false,
openingShape: 'rectangle',
trackStyle: 'overhead', trackStyle: 'overhead',
operationState: 0, operationState: 0,
garagePanelCount: Math.max(3, Math.min(8, node.garagePanelCount ?? 4)), garagePanelCount: Math.max(3, Math.min(8, node.garagePanelCount ?? 4)),
@@ -472,6 +484,7 @@ export function DoorPanel() {
...dimensionUpdates, ...dimensionUpdates,
handle: false, handle: false,
threshold: false, threshold: false,
openingShape: 'rectangle',
trackStyle: 'overhead', trackStyle: 'overhead',
operationState: 0, operationState: 0,
garagePanelCount: 4, garagePanelCount: 4,
@@ -488,6 +501,7 @@ export function DoorPanel() {
...dimensionUpdates, ...dimensionUpdates,
handle: false, handle: false,
threshold: false, threshold: false,
openingShape: 'rectangle',
trackStyle: 'overhead', trackStyle: 'overhead',
operationState: 0, operationState: 0,
garagePanelCount: 4, garagePanelCount: 4,
@@ -738,7 +752,7 @@ export function DoorPanel() {
/> />
</PanelSection> </PanelSection>
{!isOpening && ( {!isOpening && supportsTopShape && (
<PanelSection title="Top Shape"> <PanelSection title="Top Shape">
<div className="flex flex-col gap-2 px-1 pb-1"> <div className="flex flex-col gap-2 px-1 pb-1">
<SegmentedControl <SegmentedControl
@@ -990,19 +1004,21 @@ export function DoorPanel() {
{isSwingDoor && ( {isSwingDoor && (
<PanelSection title="Swing"> <PanelSection title="Swing">
<div className="flex flex-col gap-2 px-1 pb-1"> <div className="flex flex-col gap-2 px-1 pb-1">
<div className="space-y-1"> {supportsHingeSide && (
<span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider"> <div className="space-y-1">
Hinges Side <span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
</span> Hinges Side
<SegmentedControl </span>
onChange={(v) => handleUpdate({ hingesSide: v })} <SegmentedControl
options={[ onChange={(v) => handleUpdate({ hingesSide: v })}
{ label: 'Left', value: 'left' }, options={[
{ label: 'Right', value: 'right' }, { label: 'Left', value: 'left' },
]} { label: 'Right', value: 'right' },
value={node.hingesSide} ]}
/> value={node.hingesSide}
</div> />
</div>
)}
<div className="space-y-1"> <div className="space-y-1">
<span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider"> <span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
Direction Direction
@@ -16,7 +16,6 @@ import { cn } from '../../../lib/utils'
import { sfxEmitter } from '../../../lib/sfx-bus' import { sfxEmitter } from '../../../lib/sfx-bus'
import useEditor from '../../../store/use-editor' import useEditor from '../../../store/use-editor'
import { ActionButton, ActionGroup } from '../controls/action-button' import { ActionButton, ActionGroup } from '../controls/action-button'
import { MetricControl } from '../controls/metric-control'
import { PanelSection } from '../controls/panel-section' import { PanelSection } from '../controls/panel-section'
import { SegmentedControl } from '../controls/segmented-control' import { SegmentedControl } from '../controls/segmented-control'
import { SliderControl } from '../controls/slider-control' import { SliderControl } from '../controls/slider-control'
@@ -81,14 +80,16 @@ const windowTypeOptions: Array<{ label: string; value: WindowNode['windowType']
{ label: 'Louvered', value: 'louvered' }, { label: 'Louvered', value: 'louvered' },
] ]
const rectangleOnlyWindowTypes = new Set<WindowNode['windowType']>([ const shapedWindowTypes = new Set<WindowNode['windowType']>([
'sliding', 'fixed',
'single-hung', 'casement',
'double-hung', 'awning',
'bay', 'hopper',
'bow', 'louvered',
]) ])
const silllessWindowTypes = new Set<WindowNode['windowType']>(['bay', 'bow'])
export function WindowPanel() { export function WindowPanel() {
const selectedId = useViewer((s) => s.selection.selectedIds[0]) const selectedId = useViewer((s) => s.selection.selectedIds[0])
const setSelection = useViewer((s) => s.setSelection) const setSelection = useViewer((s) => s.setSelection)
@@ -110,14 +111,19 @@ export function WindowPanel() {
const handleUpdate = useCallback( const handleUpdate = useCallback(
(updates: Partial<WindowNode>) => { (updates: Partial<WindowNode>) => {
if (!(selectedId && node)) return if (!(selectedId && node)) return
const liveNode = useScene.getState().nodes[selectedId as AnyNodeId]
if (liveNode?.type !== 'window') return
const hasChange = Object.entries(updates).some(([key, value]) => { const hasChange = Object.entries(updates).some(([key, value]) => {
const currentValue = node[key as keyof WindowNode] const currentValue = liveNode[key as keyof WindowNode]
return !isSameWindowValue(currentValue, value) return !isSameWindowValue(currentValue, value)
}) })
if (!hasChange) return if (!hasChange) return
updateNode(selectedId as AnyNode['id'], updates) updateNode(selectedId as AnyNode['id'], updates)
useScene.getState().dirtyNodes.add(selectedId as AnyNodeId) const scene = useScene.getState()
scene.dirtyNodes.add(selectedId as AnyNodeId)
if (liveNode.parentId) scene.dirtyNodes.add(liveNode.parentId as AnyNodeId)
}, },
[selectedId, node, updateNode], [selectedId, node, updateNode],
) )
@@ -316,6 +322,9 @@ export function WindowPanel() {
node.windowType === 'single-hung' || node.windowType === 'single-hung' ||
node.windowType === 'double-hung' || node.windowType === 'double-hung' ||
node.windowType === 'louvered' node.windowType === 'louvered'
const supportsWindowShape = shapedWindowTypes.has(node.windowType ?? 'fixed')
const supportsGrid = node.windowType === 'fixed'
const supportsSill = !silllessWindowTypes.has(node.windowType)
const setOperationState = (value: number) => { const setOperationState = (value: number) => {
useInteractive.getState().cancelWindowAnimation(node.id) useInteractive.getState().cancelWindowAnimation(node.id)
@@ -464,10 +473,10 @@ export function WindowPanel() {
handleUpdate({ handleUpdate({
windowType: option.value, windowType: option.value,
...(option.value === 'awning' ? { awningDirection } : {}), ...(option.value === 'awning' ? { awningDirection } : {}),
...(rectangleOnlyWindowTypes.has(option.value) ...(!shapedWindowTypes.has(option.value)
? { openingShape: 'rectangle' } ? { openingShape: 'rectangle' }
: {}), : {}),
...(option.value === 'bay' || option.value === 'bow' ? { sill: false } : {}), ...(silllessWindowTypes.has(option.value) ? { sill: false } : {}),
}) })
} }
type="button" type="button"
@@ -597,7 +606,7 @@ export function WindowPanel() {
/> />
</PanelSection> </PanelSection>
{!isOpening && !rectangleOnlyWindowTypes.has(node.windowType) && ( {!isOpening && supportsWindowShape && (
<PanelSection title="Corner Shape"> <PanelSection title="Corner Shape">
<SegmentedControl <SegmentedControl
onChange={(value) => onChange={(value) =>
@@ -814,128 +823,132 @@ export function WindowPanel() {
/> />
</PanelSection> </PanelSection>
<PanelSection title="Grid"> {supportsGrid && (
<SliderControl <PanelSection title="Grid">
label="Columns" <SliderControl
max={8} label="Columns"
min={1} max={8}
onChange={(v) => { min={1}
const n = Math.max(1, Math.min(8, Math.round(v))) onChange={(v) => {
handleUpdate({ columnRatios: Array(n).fill(1 / n) }) const n = Math.max(1, Math.min(8, Math.round(v)))
}} handleUpdate({ columnRatios: Array(n).fill(1 / n) })
precision={0} }}
step={1} precision={0}
value={numCols} step={1}
/> value={numCols}
<SliderControl />
label="Rows" <SliderControl
max={8} label="Rows"
min={1} max={8}
onChange={(v) => { min={1}
const n = Math.max(1, Math.min(8, Math.round(v))) onChange={(v) => {
handleUpdate({ rowRatios: Array(n).fill(1 / n) }) const n = Math.max(1, Math.min(8, Math.round(v)))
}} handleUpdate({ rowRatios: Array(n).fill(1 / n) })
precision={0} }}
step={1} precision={0}
value={numRows} step={1}
/> value={numRows}
/>
{numCols > 1 && ( {numCols > 1 && (
<div className="mt-2 flex flex-col gap-1"> <div className="mt-2 flex flex-col gap-1">
<div className="mb-1 px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider"> <div className="mb-1 px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
Col Widths Col Widths
</div>
{normCols.map((ratio, i) => (
<SliderControl
key={`c-${i}`}
label={`C${i + 1}`}
max={95}
min={5}
onChange={(v) => setColumnRatio(i, v / 100)}
precision={1}
step={1}
unit="%"
value={Math.round(ratio * 100 * 10) / 10}
/>
))}
<div className="mt-1 border-border/50 border-t pt-1">
<SliderControl
label="Divider"
max={0.1}
min={0.005}
onChange={(v) => handleUpdate({ columnDividerThickness: v })}
precision={3}
step={0.01}
unit="m"
value={Math.round((node.columnDividerThickness ?? 0.03) * 1000) / 1000}
/>
</div>
</div> </div>
{normCols.map((ratio, i) => ( )}
{numRows > 1 && (
<div className="mt-2 flex flex-col gap-1">
<div className="mb-1 px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
Row Heights
</div>
{normRows.map((ratio, i) => (
<SliderControl
key={`r-${i}`}
label={`R${i + 1}`}
max={95}
min={5}
onChange={(v) => setRowRatio(i, v / 100)}
precision={1}
step={1}
unit="%"
value={Math.round(ratio * 100 * 10) / 10}
/>
))}
<div className="mt-1 border-border/50 border-t pt-1">
<SliderControl
label="Divider"
max={0.1}
min={0.005}
onChange={(v) => handleUpdate({ rowDividerThickness: v })}
precision={3}
step={0.01}
unit="m"
value={Math.round((node.rowDividerThickness ?? 0.03) * 1000) / 1000}
/>
</div>
</div>
)}
</PanelSection>
)}
{supportsSill && (
<PanelSection title="Sill">
<ToggleControl
checked={node.sill}
label="Enable Sill"
onChange={(checked) => handleUpdate({ sill: checked })}
/>
{node.sill && (
<div className="mt-1 flex flex-col gap-1">
<SliderControl <SliderControl
key={`c-${i}`} label="Depth"
label={`C${i + 1}`} min={0}
max={95} onChange={(v) => handleUpdate({ sillDepth: v })}
min={5}
onChange={(v) => setColumnRatio(i, v / 100)}
precision={1}
step={1}
unit="%"
value={Math.round(ratio * 100 * 10) / 10}
/>
))}
<div className="mt-1 border-border/50 border-t pt-1">
<SliderControl
label="Divider"
max={0.1}
min={0.005}
onChange={(v) => handleUpdate({ columnDividerThickness: v })}
precision={3} precision={3}
step={0.01} step={0.01}
unit="m" unit="m"
value={Math.round((node.columnDividerThickness ?? 0.03) * 1000) / 1000} value={Math.round(node.sillDepth * 1000) / 1000}
/> />
</div>
</div>
)}
{numRows > 1 && (
<div className="mt-2 flex flex-col gap-1">
<div className="mb-1 px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
Row Heights
</div>
{normRows.map((ratio, i) => (
<SliderControl <SliderControl
key={`r-${i}`} label="Thickness"
label={`R${i + 1}`} min={0}
max={95} onChange={(v) => handleUpdate({ sillThickness: v })}
min={5}
onChange={(v) => setRowRatio(i, v / 100)}
precision={1}
step={1}
unit="%"
value={Math.round(ratio * 100 * 10) / 10}
/>
))}
<div className="mt-1 border-border/50 border-t pt-1">
<SliderControl
label="Divider"
max={0.1}
min={0.005}
onChange={(v) => handleUpdate({ rowDividerThickness: v })}
precision={3} precision={3}
step={0.01} step={0.01}
unit="m" unit="m"
value={Math.round((node.rowDividerThickness ?? 0.03) * 1000) / 1000} value={Math.round(node.sillThickness * 1000) / 1000}
/> />
</div> </div>
</div> )}
)} </PanelSection>
</PanelSection> )}
<PanelSection title="Sill">
<ToggleControl
checked={node.sill}
label="Enable Sill"
onChange={(checked) => handleUpdate({ sill: checked })}
/>
{node.sill && (
<div className="mt-1 flex flex-col gap-1">
<SliderControl
label="Depth"
min={0}
onChange={(v) => handleUpdate({ sillDepth: v })}
precision={3}
step={0.01}
unit="m"
value={Math.round(node.sillDepth * 1000) / 1000}
/>
<SliderControl
label="Thickness"
min={0}
onChange={(v) => handleUpdate({ sillThickness: v })}
precision={3}
step={0.01}
unit="m"
value={Math.round(node.sillThickness * 1000) / 1000}
/>
</div>
)}
</PanelSection>
</> </>
)} )}