@pascal-app/editor 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -7
- package/src/components/editor/bottom-sheet.tsx +149 -0
- package/src/components/editor/custom-camera-controls.tsx +75 -7
- package/src/components/editor/editor-layout-mobile.tsx +264 -0
- package/src/components/editor/editor-layout-v2.tsx +29 -0
- package/src/components/editor/first-person/build-collider-world.ts +365 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +795 -0
- package/src/components/editor/first-person-controls.tsx +496 -143
- package/src/components/editor/floating-action-menu.tsx +281 -83
- package/src/components/editor/floating-building-action-menu.tsx +4 -3
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +10442 -3275
- package/src/components/editor/index.tsx +270 -20
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/selection-manager.tsx +766 -12
- package/src/components/editor/site-edge-labels.tsx +9 -3
- package/src/components/editor/thumbnail-generator.tsx +350 -157
- package/src/components/editor/use-floorplan-background-placement.ts +257 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +171 -0
- package/src/components/editor/use-floorplan-scene-data.ts +189 -0
- package/src/components/editor/wall-measurement-label.tsx +377 -58
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +95 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +160 -0
- package/src/components/editor-2d/floorplan-hotkey-handlers.tsx +92 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +119 -0
- package/src/components/editor-2d/renderers/floorplan-marquee-layer.tsx +58 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +197 -0
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +113 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +474 -0
- package/src/components/editor-2d/svg-paths.ts +119 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +272 -0
- package/src/components/systems/roof/roof-edit-system.tsx +5 -5
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +1 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +2 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +5 -5
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +257 -0
- package/src/components/tools/column/column-tool.tsx +97 -0
- package/src/components/tools/column/move-column-tool.tsx +105 -0
- package/src/components/tools/door/door-tool.tsx +19 -0
- package/src/components/tools/door/move-door-tool.tsx +38 -8
- package/src/components/tools/fence/curve-fence-tool.tsx +179 -0
- package/src/components/tools/fence/fence-drafting.ts +27 -8
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +438 -0
- package/src/components/tools/fence/move-fence-tool.tsx +102 -27
- package/src/components/tools/item/move-tool.tsx +19 -1
- package/src/components/tools/item/placement-math.ts +44 -7
- package/src/components/tools/item/placement-strategies.ts +111 -33
- package/src/components/tools/item/placement-types.ts +7 -0
- package/src/components/tools/item/use-draft-node.ts +2 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +701 -61
- package/src/components/tools/roof/move-roof-tool.tsx +111 -43
- package/src/components/tools/shared/polygon-editor.tsx +244 -29
- package/src/components/tools/shared/segment-angle.ts +156 -0
- package/src/components/tools/slab/move-slab-tool.tsx +182 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +1 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +2 -0
- package/src/components/tools/spawn/move-spawn-tool.tsx +101 -0
- package/src/components/tools/spawn/spawn-tool.tsx +130 -0
- package/src/components/tools/stair/stair-tool.tsx +11 -3
- package/src/components/tools/tool-manager.tsx +30 -3
- package/src/components/tools/wall/curve-wall-tool.tsx +176 -0
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +423 -0
- package/src/components/tools/wall/move-wall-tool.tsx +356 -0
- package/src/components/tools/wall/wall-drafting.ts +348 -17
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +28 -0
- package/src/components/tools/window/window-tool.tsx +17 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +37 -5
- package/src/components/ui/action-menu/index.tsx +91 -1
- package/src/components/ui/action-menu/structure-tools.tsx +2 -0
- package/src/components/ui/action-menu/view-toggles.tsx +424 -35
- package/src/components/ui/command-palette/editor-commands.tsx +27 -5
- package/src/components/ui/command-palette/index.tsx +0 -1
- package/src/components/ui/controls/material-picker.tsx +189 -169
- package/src/components/ui/controls/slider-control.tsx +88 -26
- package/src/components/ui/floating-level-selector.tsx +286 -55
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1121 -1219
- package/src/components/ui/item-catalog/item-catalog.tsx +42 -175
- package/src/components/ui/level-duplicate-dialog.tsx +115 -0
- package/src/components/ui/panels/ceiling-panel.tsx +47 -27
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +986 -294
- package/src/components/ui/panels/fence-panel.tsx +55 -12
- package/src/components/ui/panels/item-panel.tsx +5 -5
- package/src/components/ui/panels/mobile-panel-sheet.tsx +108 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +100 -0
- package/src/components/ui/panels/node-display.ts +39 -0
- package/src/components/ui/panels/paint-panel.tsx +138 -0
- package/src/components/ui/panels/panel-manager.tsx +241 -30
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +243 -9
- package/src/components/ui/panels/roof-panel.tsx +30 -62
- package/src/components/ui/panels/roof-segment-panel.tsx +8 -23
- package/src/components/ui/panels/slab-panel.tsx +46 -24
- package/src/components/ui/panels/spawn-panel.tsx +155 -0
- package/src/components/ui/panels/stair-panel.tsx +117 -69
- package/src/components/ui/panels/stair-segment-panel.tsx +13 -27
- package/src/components/ui/panels/wall-panel.tsx +71 -17
- package/src/components/ui/panels/window-panel.tsx +665 -146
- package/src/components/ui/sidebar/mobile-tab-bar.tsx +46 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +138 -56
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +12 -6
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +96 -2
- package/src/components/viewer-overlay.tsx +25 -19
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-contextual-tools.ts +14 -13
- package/src/hooks/use-keyboard.ts +67 -9
- package/src/hooks/use-mobile.ts +12 -12
- package/src/index.tsx +2 -1
- package/src/lib/door-interaction.ts +88 -0
- package/src/lib/floorplan/geometry.ts +263 -0
- package/src/lib/floorplan/index.ts +38 -0
- package/src/lib/floorplan/items.ts +179 -0
- package/src/lib/floorplan/selection-tool.ts +231 -0
- package/src/lib/floorplan/stairs.ts +478 -0
- package/src/lib/floorplan/types.ts +57 -0
- package/src/lib/floorplan/walls.ts +23 -0
- package/src/lib/guide-events.ts +10 -0
- package/src/lib/history.ts +20 -0
- package/src/lib/level-duplication.test.ts +72 -0
- package/src/lib/level-duplication.ts +153 -0
- package/src/lib/local-guide-image.ts +42 -0
- package/src/lib/material-paint.ts +284 -0
- package/src/lib/roof-duplication.ts +214 -0
- package/src/lib/scene-bounds.test.ts +183 -0
- package/src/lib/scene-bounds.ts +169 -0
- package/src/lib/sfx-player.ts +96 -13
- package/src/lib/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +279 -15
|
@@ -4,54 +4,165 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
emitter,
|
|
7
|
-
|
|
7
|
+
useInteractive,
|
|
8
8
|
useScene,
|
|
9
9
|
WindowNode,
|
|
10
10
|
} from '@pascal-app/core'
|
|
11
11
|
import { useViewer } from '@pascal-app/viewer'
|
|
12
12
|
import { BookMarked, Copy, FlipHorizontal2, Move, Trash2 } from 'lucide-react'
|
|
13
|
-
import { useCallback } from 'react'
|
|
13
|
+
import { useCallback, useRef } from 'react'
|
|
14
14
|
import { usePresetsAdapter } from '../../../contexts/presets-context'
|
|
15
|
+
import { cn } from '../../../lib/utils'
|
|
15
16
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
16
17
|
import useEditor from '../../../store/use-editor'
|
|
17
18
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
18
|
-
import { MaterialPicker } from '../controls/material-picker'
|
|
19
19
|
import { MetricControl } from '../controls/metric-control'
|
|
20
20
|
import { PanelSection } from '../controls/panel-section'
|
|
21
|
+
import { SegmentedControl } from '../controls/segmented-control'
|
|
21
22
|
import { SliderControl } from '../controls/slider-control'
|
|
22
23
|
import { ToggleControl } from '../controls/toggle-control'
|
|
23
24
|
import { PanelWrapper } from './panel-wrapper'
|
|
24
25
|
import { PresetsPopover } from './presets/presets-popover'
|
|
25
26
|
|
|
27
|
+
function isSameWindowValue(current: unknown, next: unknown): boolean {
|
|
28
|
+
if (typeof current === 'number' && typeof next === 'number') {
|
|
29
|
+
return Math.abs(current - next) < 1e-6
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(current) && Array.isArray(next)) {
|
|
33
|
+
return (
|
|
34
|
+
current.length === next.length &&
|
|
35
|
+
current.every((value, index) => isSameWindowValue(value, next[index]))
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return Object.is(current, next)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getMaxSharedWindowRadius(width: number, height: number) {
|
|
43
|
+
return Math.max(0, Math.min(width / 2, height / 2))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeWindowCornerRadii(
|
|
47
|
+
radii: [number, number, number, number],
|
|
48
|
+
width: number,
|
|
49
|
+
height: number,
|
|
50
|
+
): [number, number, number, number] {
|
|
51
|
+
const next = radii.map((radius) => Math.max(radius, 0)) as [number, number, number, number]
|
|
52
|
+
const scale = Math.min(
|
|
53
|
+
1,
|
|
54
|
+
Math.max(width, 0) / Math.max(next[0] + next[1], 1e-6),
|
|
55
|
+
Math.max(width, 0) / Math.max(next[3] + next[2], 1e-6),
|
|
56
|
+
Math.max(height, 0) / Math.max(next[0] + next[3], 1e-6),
|
|
57
|
+
Math.max(height, 0) / Math.max(next[1] + next[2], 1e-6),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if (scale >= 1) return next
|
|
61
|
+
|
|
62
|
+
return next.map((radius) => radius * scale) as [number, number, number, number]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isSameRadiusTuple(
|
|
66
|
+
current: [number, number, number, number],
|
|
67
|
+
next: [number, number, number, number],
|
|
68
|
+
) {
|
|
69
|
+
return current.every((value, index) => Math.abs(value - (next[index] ?? 0)) < 1e-6)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const windowTypeOptions: Array<{ label: string; value: WindowNode['windowType'] }> = [
|
|
73
|
+
{ label: 'Fixed', value: 'fixed' },
|
|
74
|
+
{ label: 'Sliding', value: 'sliding' },
|
|
75
|
+
{ label: 'Casement', value: 'casement' },
|
|
76
|
+
{ label: 'Awning', value: 'awning' },
|
|
77
|
+
{ label: 'Single Hung', value: 'single-hung' },
|
|
78
|
+
{ label: 'Double Hung', value: 'double-hung' },
|
|
79
|
+
{ label: 'Bay', value: 'bay' },
|
|
80
|
+
{ label: 'Bow', value: 'bow' },
|
|
81
|
+
{ label: 'Louvered', value: 'louvered' },
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
const rectangleOnlyWindowTypes = new Set<WindowNode['windowType']>([
|
|
85
|
+
'sliding',
|
|
86
|
+
'single-hung',
|
|
87
|
+
'double-hung',
|
|
88
|
+
'bay',
|
|
89
|
+
'bow',
|
|
90
|
+
])
|
|
91
|
+
|
|
26
92
|
export function WindowPanel() {
|
|
27
|
-
const
|
|
93
|
+
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
|
28
94
|
const setSelection = useViewer((s) => s.setSelection)
|
|
29
|
-
const nodes = useScene((s) => s.nodes)
|
|
30
95
|
const updateNode = useScene((s) => s.updateNode)
|
|
31
96
|
const deleteNode = useScene((s) => s.deleteNode)
|
|
32
97
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
98
|
+
const previewRef = useRef<{
|
|
99
|
+
id: AnyNodeId
|
|
100
|
+
key: keyof WindowNode
|
|
101
|
+
value: unknown
|
|
102
|
+
} | null>(null)
|
|
33
103
|
|
|
34
104
|
const adapter = usePresetsAdapter()
|
|
35
105
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
: undefined
|
|
106
|
+
const node = useScene((s) =>
|
|
107
|
+
selectedId ? (s.nodes[selectedId as AnyNode['id']] as WindowNode | undefined) : undefined,
|
|
108
|
+
)
|
|
40
109
|
|
|
41
110
|
const handleUpdate = useCallback(
|
|
42
111
|
(updates: Partial<WindowNode>) => {
|
|
43
|
-
if (!selectedId) return
|
|
112
|
+
if (!(selectedId && node)) return
|
|
113
|
+
const hasChange = Object.entries(updates).some(([key, value]) => {
|
|
114
|
+
const currentValue = node[key as keyof WindowNode]
|
|
115
|
+
return !isSameWindowValue(currentValue, value)
|
|
116
|
+
})
|
|
117
|
+
if (!hasChange) return
|
|
118
|
+
|
|
44
119
|
updateNode(selectedId as AnyNode['id'], updates)
|
|
45
120
|
useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
|
|
46
121
|
},
|
|
47
|
-
[selectedId, updateNode],
|
|
122
|
+
[selectedId, node, updateNode],
|
|
48
123
|
)
|
|
49
124
|
|
|
50
|
-
const
|
|
51
|
-
(
|
|
52
|
-
|
|
125
|
+
const previewWindowUpdate = useCallback(
|
|
126
|
+
<K extends keyof WindowNode>(key: K, value: WindowNode[K]) => {
|
|
127
|
+
if (!selectedId) return
|
|
128
|
+
const liveNode = useScene.getState().nodes[selectedId as AnyNodeId]
|
|
129
|
+
if (liveNode?.type !== 'window') return
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
!(previewRef.current && previewRef.current.id === selectedId && previewRef.current.key === key)
|
|
133
|
+
) {
|
|
134
|
+
previewRef.current = {
|
|
135
|
+
id: selectedId as AnyNodeId,
|
|
136
|
+
key,
|
|
137
|
+
value: liveNode[key],
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (isSameWindowValue(liveNode[key], value)) return
|
|
142
|
+
|
|
143
|
+
;(liveNode as WindowNode)[key] = value
|
|
144
|
+
useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
|
|
53
145
|
},
|
|
54
|
-
[
|
|
146
|
+
[selectedId],
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
const commitWindowPreview = useCallback(
|
|
150
|
+
<K extends keyof WindowNode>(key: K, value: WindowNode[K]) => {
|
|
151
|
+
if (!selectedId) return
|
|
152
|
+
|
|
153
|
+
const scene = useScene.getState()
|
|
154
|
+
const liveNode = scene.nodes[selectedId as AnyNodeId]
|
|
155
|
+
const preview = previewRef.current
|
|
156
|
+
if (liveNode?.type === 'window' && preview?.id === selectedId && preview.key === key) {
|
|
157
|
+
;(liveNode as WindowNode)[key] = preview.value as WindowNode[K]
|
|
158
|
+
scene.dirtyNodes.add(selectedId as AnyNodeId)
|
|
159
|
+
}
|
|
160
|
+
previewRef.current = null
|
|
161
|
+
|
|
162
|
+
updateNode(selectedId as AnyNode['id'], { [key]: value } as Partial<WindowNode>)
|
|
163
|
+
scene.dirtyNodes.add(selectedId as AnyNodeId)
|
|
164
|
+
},
|
|
165
|
+
[selectedId, updateNode],
|
|
55
166
|
)
|
|
56
167
|
|
|
57
168
|
const handleClose = useCallback(() => {
|
|
@@ -93,8 +204,20 @@ export function WindowPanel() {
|
|
|
93
204
|
parentId: node.parentId,
|
|
94
205
|
width: node.width,
|
|
95
206
|
height: node.height,
|
|
207
|
+
windowType: node.windowType,
|
|
208
|
+
operationState: node.operationState,
|
|
209
|
+
awningDirection: node.awningDirection,
|
|
210
|
+
casementStyle: node.casementStyle,
|
|
211
|
+
hingesSide: node.hingesSide,
|
|
96
212
|
frameThickness: node.frameThickness,
|
|
97
213
|
frameDepth: node.frameDepth,
|
|
214
|
+
openingKind: node.openingKind,
|
|
215
|
+
openingShape: node.openingShape,
|
|
216
|
+
openingRadiusMode: node.openingRadiusMode ?? 'all',
|
|
217
|
+
openingCornerRadii: [...(node.openingCornerRadii ?? [0.15, 0.15, 0.15, 0.15])],
|
|
218
|
+
cornerRadius: node.cornerRadius,
|
|
219
|
+
archHeight: node.archHeight,
|
|
220
|
+
openingRevealRadius: node.openingRevealRadius,
|
|
98
221
|
columnRatios: [...node.columnRatios],
|
|
99
222
|
rowRatios: [...node.rowRatios],
|
|
100
223
|
columnDividerThickness: node.columnDividerThickness,
|
|
@@ -114,8 +237,20 @@ export function WindowPanel() {
|
|
|
114
237
|
return {
|
|
115
238
|
width: node.width,
|
|
116
239
|
height: node.height,
|
|
240
|
+
windowType: node.windowType,
|
|
241
|
+
operationState: node.operationState,
|
|
242
|
+
awningDirection: node.awningDirection,
|
|
243
|
+
casementStyle: node.casementStyle,
|
|
244
|
+
hingesSide: node.hingesSide,
|
|
117
245
|
frameThickness: node.frameThickness,
|
|
118
246
|
frameDepth: node.frameDepth,
|
|
247
|
+
openingKind: node.openingKind,
|
|
248
|
+
openingShape: node.openingShape,
|
|
249
|
+
openingRadiusMode: node.openingRadiusMode ?? 'all',
|
|
250
|
+
openingCornerRadii: node.openingCornerRadii ?? [0.15, 0.15, 0.15, 0.15],
|
|
251
|
+
cornerRadius: node.cornerRadius,
|
|
252
|
+
archHeight: node.archHeight,
|
|
253
|
+
openingRevealRadius: node.openingRevealRadius,
|
|
119
254
|
columnRatios: node.columnRatios,
|
|
120
255
|
rowRatios: node.rowRatios,
|
|
121
256
|
columnDividerThickness: node.columnDividerThickness,
|
|
@@ -153,7 +288,7 @@ export function WindowPanel() {
|
|
|
153
288
|
[handleUpdate],
|
|
154
289
|
)
|
|
155
290
|
|
|
156
|
-
if (!node
|
|
291
|
+
if (!(node && node.type === 'window' && selectedId)) return null
|
|
157
292
|
|
|
158
293
|
const numCols = node.columnRatios.length
|
|
159
294
|
const numRows = node.rowRatios.length
|
|
@@ -162,6 +297,75 @@ export function WindowPanel() {
|
|
|
162
297
|
const rowSum = node.rowRatios.reduce((a, b) => a + b, 0)
|
|
163
298
|
const normCols = node.columnRatios.map((r) => r / colSum)
|
|
164
299
|
const normRows = node.rowRatios.map((r) => r / rowSum)
|
|
300
|
+
const isOpening = node.openingKind === 'opening'
|
|
301
|
+
const openingShape = node.openingShape ?? 'rectangle'
|
|
302
|
+
const windowShape = openingShape === 'arch' || openingShape === 'rounded' ? openingShape : 'rectangle'
|
|
303
|
+
const openingRadiusMode = node.openingRadiusMode ?? 'all'
|
|
304
|
+
const openingCornerRadii = node.openingCornerRadii ?? [0.15, 0.15, 0.15, 0.15]
|
|
305
|
+
const cornerRadius = node.cornerRadius ?? 0.15
|
|
306
|
+
const archHeight = node.archHeight ?? 0.35
|
|
307
|
+
const openingRevealRadius = node.openingRevealRadius ?? 0.025
|
|
308
|
+
const maxRoundedRadius = Math.max(0.01, getMaxSharedWindowRadius(node.width, node.height))
|
|
309
|
+
const displayedWindowType = node.windowType === 'hopper' ? 'awning' : (node.windowType ?? 'fixed')
|
|
310
|
+
const awningDirection = node.windowType === 'hopper' ? 'down' : (node.awningDirection ?? 'up')
|
|
311
|
+
const isOperableWindow =
|
|
312
|
+
node.windowType === 'sliding' ||
|
|
313
|
+
node.windowType === 'casement' ||
|
|
314
|
+
node.windowType === 'awning' ||
|
|
315
|
+
node.windowType === 'hopper' ||
|
|
316
|
+
node.windowType === 'single-hung' ||
|
|
317
|
+
node.windowType === 'double-hung' ||
|
|
318
|
+
node.windowType === 'louvered'
|
|
319
|
+
|
|
320
|
+
const setOperationState = (value: number) => {
|
|
321
|
+
useInteractive.getState().cancelWindowAnimation(node.id)
|
|
322
|
+
useInteractive.getState().removeWindowOpenState(node.id)
|
|
323
|
+
handleUpdate({ operationState: Math.max(0, Math.min(1, value)) })
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const getDimensionUpdates = (updates: Partial<Pick<WindowNode, 'width' | 'height'>>) => {
|
|
327
|
+
const nextWidth = updates.width ?? node.width
|
|
328
|
+
const nextHeight = updates.height ?? node.height
|
|
329
|
+
const nextUpdates: Partial<WindowNode> = { ...updates }
|
|
330
|
+
|
|
331
|
+
if (openingShape === 'rounded') {
|
|
332
|
+
if (openingRadiusMode === 'individual') {
|
|
333
|
+
const currentRadii = openingCornerRadii as [number, number, number, number]
|
|
334
|
+
const nextRadii = normalizeWindowCornerRadii(
|
|
335
|
+
openingCornerRadii as [number, number, number, number],
|
|
336
|
+
nextWidth,
|
|
337
|
+
nextHeight,
|
|
338
|
+
)
|
|
339
|
+
if (!isSameRadiusTuple(currentRadii, nextRadii)) {
|
|
340
|
+
nextUpdates.openingCornerRadii = nextRadii
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
const nextRadius = Math.min(Math.max(cornerRadius, 0), getMaxSharedWindowRadius(nextWidth, nextHeight))
|
|
344
|
+
if (Math.abs(nextRadius - cornerRadius) > 1e-6) {
|
|
345
|
+
nextUpdates.cornerRadius = nextRadius
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (openingShape === 'arch') {
|
|
351
|
+
const nextArchHeight = Math.min(Math.max(archHeight, 0.05), Math.max(nextHeight, 0.05))
|
|
352
|
+
if (Math.abs(nextArchHeight - archHeight) > 1e-6) {
|
|
353
|
+
nextUpdates.archHeight = nextArchHeight
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return nextUpdates
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const setOpeningCornerRadius = (index: number, value: number, commit = false) => {
|
|
361
|
+
const next = [...openingCornerRadii] as [number, number, number, number]
|
|
362
|
+
next[index] = value
|
|
363
|
+
if (commit) {
|
|
364
|
+
commitWindowPreview('openingCornerRadii', next)
|
|
365
|
+
} else {
|
|
366
|
+
previewWindowUpdate('openingCornerRadii', next)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
165
369
|
|
|
166
370
|
const setColumnRatio = (index: number, newVal: number) => {
|
|
167
371
|
const clamped = Math.max(0.05, Math.min(0.95, newVal))
|
|
@@ -217,6 +421,122 @@ export function WindowPanel() {
|
|
|
217
421
|
</PresetsPopover>
|
|
218
422
|
</div>
|
|
219
423
|
|
|
424
|
+
<PanelSection title="Type">
|
|
425
|
+
<SegmentedControl
|
|
426
|
+
onChange={(value) =>
|
|
427
|
+
handleUpdate({
|
|
428
|
+
openingKind: value as WindowNode['openingKind'],
|
|
429
|
+
...(value === 'opening'
|
|
430
|
+
? {
|
|
431
|
+
openingShape,
|
|
432
|
+
openingRadiusMode,
|
|
433
|
+
openingCornerRadii,
|
|
434
|
+
cornerRadius,
|
|
435
|
+
archHeight,
|
|
436
|
+
openingRevealRadius,
|
|
437
|
+
}
|
|
438
|
+
: {}),
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
options={[
|
|
442
|
+
{ value: 'window', label: 'Window' },
|
|
443
|
+
{ value: 'opening', label: 'Opening' },
|
|
444
|
+
]}
|
|
445
|
+
value={node.openingKind ?? 'window'}
|
|
446
|
+
/>
|
|
447
|
+
</PanelSection>
|
|
448
|
+
|
|
449
|
+
{!isOpening && (
|
|
450
|
+
<PanelSection title="Window Type">
|
|
451
|
+
<div className="grid grid-cols-2 gap-1.5 px-1 pt-1">
|
|
452
|
+
{windowTypeOptions.map((option) => {
|
|
453
|
+
const isSelected = displayedWindowType === option.value
|
|
454
|
+
return (
|
|
455
|
+
<button
|
|
456
|
+
className={cn(
|
|
457
|
+
'flex min-h-12 items-center rounded-lg border px-2.5 text-left text-xs transition-colors',
|
|
458
|
+
isSelected
|
|
459
|
+
? 'border-orange-400/60 bg-orange-400/10 text-foreground'
|
|
460
|
+
: 'border-border/50 bg-[#2C2C2E] text-muted-foreground hover:bg-[#3e3e3e] hover:text-foreground',
|
|
461
|
+
)}
|
|
462
|
+
key={option.value}
|
|
463
|
+
onClick={() =>
|
|
464
|
+
handleUpdate({
|
|
465
|
+
windowType: option.value,
|
|
466
|
+
...(option.value === 'awning' ? { awningDirection } : {}),
|
|
467
|
+
...(rectangleOnlyWindowTypes.has(option.value)
|
|
468
|
+
? { openingShape: 'rectangle' }
|
|
469
|
+
: {}),
|
|
470
|
+
...(option.value === 'bay' || option.value === 'bow' ? { sill: false } : {}),
|
|
471
|
+
})
|
|
472
|
+
}
|
|
473
|
+
type="button"
|
|
474
|
+
>
|
|
475
|
+
<span className="truncate font-medium">{option.label}</span>
|
|
476
|
+
</button>
|
|
477
|
+
)
|
|
478
|
+
})}
|
|
479
|
+
</div>
|
|
480
|
+
{displayedWindowType === 'awning' && (
|
|
481
|
+
<div className="mt-2">
|
|
482
|
+
<SegmentedControl
|
|
483
|
+
onChange={(value) =>
|
|
484
|
+
handleUpdate({
|
|
485
|
+
windowType: 'awning',
|
|
486
|
+
awningDirection: value as WindowNode['awningDirection'],
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
options={[
|
|
490
|
+
{ value: 'up', label: 'Up' },
|
|
491
|
+
{ value: 'down', label: 'Down' },
|
|
492
|
+
]}
|
|
493
|
+
value={awningDirection}
|
|
494
|
+
/>
|
|
495
|
+
</div>
|
|
496
|
+
)}
|
|
497
|
+
{node.windowType === 'casement' && (
|
|
498
|
+
<div className="mt-2 space-y-2">
|
|
499
|
+
<SegmentedControl
|
|
500
|
+
onChange={(value) =>
|
|
501
|
+
handleUpdate({ casementStyle: value as WindowNode['casementStyle'] })
|
|
502
|
+
}
|
|
503
|
+
options={[
|
|
504
|
+
{ value: 'single', label: 'Single' },
|
|
505
|
+
{ value: 'french', label: 'French' },
|
|
506
|
+
]}
|
|
507
|
+
value={node.casementStyle ?? 'single'}
|
|
508
|
+
/>
|
|
509
|
+
{(node.casementStyle ?? 'single') === 'single' && (
|
|
510
|
+
<SegmentedControl
|
|
511
|
+
onChange={(value) =>
|
|
512
|
+
handleUpdate({ hingesSide: value as WindowNode['hingesSide'] })
|
|
513
|
+
}
|
|
514
|
+
options={[
|
|
515
|
+
{ value: 'left', label: 'Left' },
|
|
516
|
+
{ value: 'right', label: 'Right' },
|
|
517
|
+
]}
|
|
518
|
+
value={node.hingesSide ?? 'left'}
|
|
519
|
+
/>
|
|
520
|
+
)}
|
|
521
|
+
</div>
|
|
522
|
+
)}
|
|
523
|
+
{isOperableWindow && (
|
|
524
|
+
<div className="mt-2">
|
|
525
|
+
<SliderControl
|
|
526
|
+
label="Open"
|
|
527
|
+
max={1}
|
|
528
|
+
min={0}
|
|
529
|
+
onChange={setOperationState}
|
|
530
|
+
precision={2}
|
|
531
|
+
restoreOnCommit={false}
|
|
532
|
+
step={0.05}
|
|
533
|
+
value={Math.round((node.operationState ?? 0) * 100) / 100}
|
|
534
|
+
/>
|
|
535
|
+
</div>
|
|
536
|
+
)}
|
|
537
|
+
</PanelSection>
|
|
538
|
+
)}
|
|
539
|
+
|
|
220
540
|
<PanelSection title="Position">
|
|
221
541
|
<SliderControl
|
|
222
542
|
label={
|
|
@@ -242,22 +562,25 @@ export function WindowPanel() {
|
|
|
242
562
|
unit="m"
|
|
243
563
|
value={Math.round(node.position[1] * 100) / 100}
|
|
244
564
|
/>
|
|
245
|
-
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
565
|
+
{!isOpening && (
|
|
566
|
+
<div className="px-1 pt-2 pb-1">
|
|
567
|
+
<ActionButton
|
|
568
|
+
className="w-full"
|
|
569
|
+
icon={<FlipHorizontal2 className="h-4 w-4" />}
|
|
570
|
+
label="Flip Side"
|
|
571
|
+
onClick={handleFlip}
|
|
572
|
+
/>
|
|
573
|
+
</div>
|
|
574
|
+
)}
|
|
253
575
|
</PanelSection>
|
|
254
576
|
|
|
255
577
|
<PanelSection title="Dimensions">
|
|
256
578
|
<SliderControl
|
|
257
579
|
label="Width"
|
|
258
580
|
min={0}
|
|
259
|
-
onChange={(v) => handleUpdate({ width: v })}
|
|
581
|
+
onChange={(v) => handleUpdate(getDimensionUpdates({ width: v }))}
|
|
260
582
|
precision={2}
|
|
583
|
+
restoreOnCommit={false}
|
|
261
584
|
step={0.1}
|
|
262
585
|
unit="m"
|
|
263
586
|
value={Math.round(node.width * 100) / 100}
|
|
@@ -265,157 +588,356 @@ export function WindowPanel() {
|
|
|
265
588
|
<SliderControl
|
|
266
589
|
label="Height"
|
|
267
590
|
min={0}
|
|
268
|
-
onChange={(v) => handleUpdate({ height: v })}
|
|
591
|
+
onChange={(v) => handleUpdate(getDimensionUpdates({ height: v }))}
|
|
269
592
|
precision={2}
|
|
593
|
+
restoreOnCommit={false}
|
|
270
594
|
step={0.1}
|
|
271
595
|
unit="m"
|
|
272
596
|
value={Math.round(node.height * 100) / 100}
|
|
273
597
|
/>
|
|
274
598
|
</PanelSection>
|
|
275
599
|
|
|
276
|
-
|
|
277
|
-
<
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
max={8}
|
|
313
|
-
min={1}
|
|
314
|
-
onChange={(v) => {
|
|
315
|
-
const n = Math.max(1, Math.min(8, Math.round(v)))
|
|
316
|
-
handleUpdate({ rowRatios: Array(n).fill(1 / n) })
|
|
317
|
-
}}
|
|
318
|
-
precision={0}
|
|
319
|
-
step={1}
|
|
320
|
-
value={numRows}
|
|
321
|
-
/>
|
|
322
|
-
|
|
323
|
-
{numCols > 1 && (
|
|
324
|
-
<div className="mt-2 flex flex-col gap-1">
|
|
325
|
-
<div className="mb-1 px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
|
|
326
|
-
Col Widths
|
|
327
|
-
</div>
|
|
328
|
-
{normCols.map((ratio, i) => (
|
|
329
|
-
<SliderControl
|
|
330
|
-
key={`c-${i}`}
|
|
331
|
-
label={`C${i + 1}`}
|
|
332
|
-
max={95}
|
|
333
|
-
min={5}
|
|
334
|
-
onChange={(v) => setColumnRatio(i, v / 100)}
|
|
335
|
-
precision={1}
|
|
336
|
-
step={1}
|
|
337
|
-
unit="%"
|
|
338
|
-
value={Math.round(ratio * 100 * 10) / 10}
|
|
600
|
+
{!isOpening && !rectangleOnlyWindowTypes.has(node.windowType) && (
|
|
601
|
+
<PanelSection title="Corner Shape">
|
|
602
|
+
<SegmentedControl
|
|
603
|
+
onChange={(value) =>
|
|
604
|
+
handleUpdate({
|
|
605
|
+
openingShape: value as WindowNode['openingShape'],
|
|
606
|
+
...(value === 'rounded'
|
|
607
|
+
? {
|
|
608
|
+
openingRadiusMode,
|
|
609
|
+
openingCornerRadii,
|
|
610
|
+
cornerRadius: Math.min(cornerRadius, maxRoundedRadius),
|
|
611
|
+
openingRevealRadius,
|
|
612
|
+
sill: false,
|
|
613
|
+
}
|
|
614
|
+
: {}),
|
|
615
|
+
...(value === 'arch' ? { archHeight } : {}),
|
|
616
|
+
})
|
|
617
|
+
}
|
|
618
|
+
options={[
|
|
619
|
+
{ value: 'rectangle', label: 'Rect' },
|
|
620
|
+
{ value: 'rounded', label: 'Rounded' },
|
|
621
|
+
{ value: 'arch', label: 'Arch' },
|
|
622
|
+
]}
|
|
623
|
+
value={windowShape}
|
|
624
|
+
/>
|
|
625
|
+
{windowShape === 'rounded' && (
|
|
626
|
+
<div className="mt-2 flex flex-col gap-1">
|
|
627
|
+
<SegmentedControl
|
|
628
|
+
onChange={(value) =>
|
|
629
|
+
handleUpdate({ openingRadiusMode: value as WindowNode['openingRadiusMode'] })
|
|
630
|
+
}
|
|
631
|
+
options={[
|
|
632
|
+
{ value: 'all', label: 'All' },
|
|
633
|
+
{ value: 'individual', label: 'Individual' },
|
|
634
|
+
]}
|
|
635
|
+
value={openingRadiusMode}
|
|
339
636
|
/>
|
|
340
|
-
|
|
341
|
-
|
|
637
|
+
{openingRadiusMode === 'all' ? (
|
|
638
|
+
<SliderControl
|
|
639
|
+
label="Corner Radius"
|
|
640
|
+
max={maxRoundedRadius}
|
|
641
|
+
min={0}
|
|
642
|
+
onChange={(value) => previewWindowUpdate('cornerRadius', value)}
|
|
643
|
+
onCommit={(value) => commitWindowPreview('cornerRadius', value)}
|
|
644
|
+
precision={2}
|
|
645
|
+
step={0.05}
|
|
646
|
+
unit="m"
|
|
647
|
+
value={Math.round(cornerRadius * 100) / 100}
|
|
648
|
+
/>
|
|
649
|
+
) : (
|
|
650
|
+
<>
|
|
651
|
+
{[
|
|
652
|
+
['Top Left', 0],
|
|
653
|
+
['Top Right', 1],
|
|
654
|
+
['Bottom Right', 2],
|
|
655
|
+
['Bottom Left', 3],
|
|
656
|
+
].map(([label, index]) => (
|
|
657
|
+
<SliderControl
|
|
658
|
+
key={label}
|
|
659
|
+
label={label}
|
|
660
|
+
max={maxRoundedRadius}
|
|
661
|
+
min={0}
|
|
662
|
+
onChange={(value) => setOpeningCornerRadius(index as number, value)}
|
|
663
|
+
onCommit={(value) => setOpeningCornerRadius(index as number, value, true)}
|
|
664
|
+
precision={2}
|
|
665
|
+
step={0.05}
|
|
666
|
+
unit="m"
|
|
667
|
+
value={Math.round((openingCornerRadii[index as number] ?? 0) * 100) / 100}
|
|
668
|
+
/>
|
|
669
|
+
))}
|
|
670
|
+
</>
|
|
671
|
+
)}
|
|
342
672
|
<SliderControl
|
|
343
|
-
label="
|
|
344
|
-
max={0.
|
|
345
|
-
min={0
|
|
346
|
-
onChange={(
|
|
673
|
+
label="Reveal Radius"
|
|
674
|
+
max={0.08}
|
|
675
|
+
min={0}
|
|
676
|
+
onChange={(value) => previewWindowUpdate('openingRevealRadius', value)}
|
|
677
|
+
onCommit={(value) => commitWindowPreview('openingRevealRadius', value)}
|
|
347
678
|
precision={3}
|
|
348
|
-
step={0.
|
|
679
|
+
step={0.005}
|
|
349
680
|
unit="m"
|
|
350
|
-
value={Math.round(
|
|
681
|
+
value={Math.round(openingRevealRadius * 1000) / 1000}
|
|
351
682
|
/>
|
|
352
683
|
</div>
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
{numRows > 1 && (
|
|
357
|
-
<div className="mt-2 flex flex-col gap-1">
|
|
358
|
-
<div className="mb-1 px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
|
|
359
|
-
Row Heights
|
|
360
|
-
</div>
|
|
361
|
-
{normRows.map((ratio, i) => (
|
|
684
|
+
)}
|
|
685
|
+
{windowShape === 'arch' && (
|
|
686
|
+
<div className="mt-2 flex flex-col gap-1">
|
|
362
687
|
<SliderControl
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
step={
|
|
370
|
-
unit="
|
|
371
|
-
value={Math.round(
|
|
688
|
+
label="Arch Height"
|
|
689
|
+
max={Math.max(0.05, node.height)}
|
|
690
|
+
min={0.05}
|
|
691
|
+
onChange={(value) => handleUpdate({ archHeight: value })}
|
|
692
|
+
precision={2}
|
|
693
|
+
restoreOnCommit={false}
|
|
694
|
+
step={0.05}
|
|
695
|
+
unit="m"
|
|
696
|
+
value={Math.round(archHeight * 100) / 100}
|
|
372
697
|
/>
|
|
373
|
-
|
|
374
|
-
|
|
698
|
+
</div>
|
|
699
|
+
)}
|
|
700
|
+
</PanelSection>
|
|
701
|
+
)}
|
|
702
|
+
|
|
703
|
+
{isOpening && (
|
|
704
|
+
<PanelSection title="Opening Shape">
|
|
705
|
+
<SegmentedControl
|
|
706
|
+
onChange={(value) =>
|
|
707
|
+
handleUpdate({ openingShape: value as WindowNode['openingShape'] })
|
|
708
|
+
}
|
|
709
|
+
options={[
|
|
710
|
+
{ value: 'rectangle', label: 'Rect' },
|
|
711
|
+
{ value: 'rounded', label: 'Rounded' },
|
|
712
|
+
{ value: 'arch', label: 'Arch' },
|
|
713
|
+
]}
|
|
714
|
+
value={openingShape}
|
|
715
|
+
/>
|
|
716
|
+
{openingShape === 'rounded' && (
|
|
717
|
+
<div className="mt-2 flex flex-col gap-1">
|
|
718
|
+
<SegmentedControl
|
|
719
|
+
onChange={(value) =>
|
|
720
|
+
handleUpdate({ openingRadiusMode: value as WindowNode['openingRadiusMode'] })
|
|
721
|
+
}
|
|
722
|
+
options={[
|
|
723
|
+
{ value: 'all', label: 'All' },
|
|
724
|
+
{ value: 'individual', label: 'Individual' },
|
|
725
|
+
]}
|
|
726
|
+
value={openingRadiusMode}
|
|
727
|
+
/>
|
|
728
|
+
{openingRadiusMode === 'all' ? (
|
|
729
|
+
<SliderControl
|
|
730
|
+
label="Corner Radius"
|
|
731
|
+
max={maxRoundedRadius}
|
|
732
|
+
min={0}
|
|
733
|
+
onChange={(value) => previewWindowUpdate('cornerRadius', value)}
|
|
734
|
+
onCommit={(value) => commitWindowPreview('cornerRadius', value)}
|
|
735
|
+
precision={2}
|
|
736
|
+
step={0.05}
|
|
737
|
+
unit="m"
|
|
738
|
+
value={Math.round(cornerRadius * 100) / 100}
|
|
739
|
+
/>
|
|
740
|
+
) : (
|
|
741
|
+
<>
|
|
742
|
+
{[
|
|
743
|
+
['Top Left', 0],
|
|
744
|
+
['Top Right', 1],
|
|
745
|
+
['Bottom Right', 2],
|
|
746
|
+
['Bottom Left', 3],
|
|
747
|
+
].map(([label, index]) => (
|
|
748
|
+
<SliderControl
|
|
749
|
+
key={label}
|
|
750
|
+
label={label}
|
|
751
|
+
max={maxRoundedRadius}
|
|
752
|
+
min={0}
|
|
753
|
+
onChange={(value) => setOpeningCornerRadius(index as number, value)}
|
|
754
|
+
onCommit={(value) => setOpeningCornerRadius(index as number, value, true)}
|
|
755
|
+
precision={2}
|
|
756
|
+
step={0.05}
|
|
757
|
+
unit="m"
|
|
758
|
+
value={Math.round((openingCornerRadii[index as number] ?? 0) * 100) / 100}
|
|
759
|
+
/>
|
|
760
|
+
))}
|
|
761
|
+
</>
|
|
762
|
+
)}
|
|
375
763
|
<SliderControl
|
|
376
|
-
label="
|
|
377
|
-
max={0.
|
|
378
|
-
min={0
|
|
379
|
-
onChange={(
|
|
764
|
+
label="Reveal Radius"
|
|
765
|
+
max={0.08}
|
|
766
|
+
min={0}
|
|
767
|
+
onChange={(value) => previewWindowUpdate('openingRevealRadius', value)}
|
|
768
|
+
onCommit={(value) => commitWindowPreview('openingRevealRadius', value)}
|
|
380
769
|
precision={3}
|
|
381
|
-
step={0.
|
|
770
|
+
step={0.005}
|
|
382
771
|
unit="m"
|
|
383
|
-
value={Math.round(
|
|
772
|
+
value={Math.round(openingRevealRadius * 1000) / 1000}
|
|
384
773
|
/>
|
|
385
774
|
</div>
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
775
|
+
)}
|
|
776
|
+
{openingShape === 'arch' && (
|
|
777
|
+
<div className="mt-2 flex flex-col gap-1">
|
|
778
|
+
<SliderControl
|
|
779
|
+
label="Arch Height"
|
|
780
|
+
max={Math.max(0.05, node.height)}
|
|
781
|
+
min={0.05}
|
|
782
|
+
onChange={(value) => handleUpdate({ archHeight: value })}
|
|
783
|
+
precision={2}
|
|
784
|
+
restoreOnCommit={false}
|
|
785
|
+
step={0.05}
|
|
786
|
+
unit="m"
|
|
787
|
+
value={Math.round(archHeight * 100) / 100}
|
|
788
|
+
/>
|
|
789
|
+
</div>
|
|
790
|
+
)}
|
|
791
|
+
</PanelSection>
|
|
792
|
+
)}
|
|
389
793
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
label="Enable Sill"
|
|
394
|
-
onChange={(checked) => handleUpdate({ sill: checked })}
|
|
395
|
-
/>
|
|
396
|
-
{node.sill && (
|
|
397
|
-
<div className="mt-1 flex flex-col gap-1">
|
|
794
|
+
{!isOpening && (
|
|
795
|
+
<>
|
|
796
|
+
<PanelSection title="Frame">
|
|
398
797
|
<SliderControl
|
|
399
|
-
label="
|
|
798
|
+
label="Thickness"
|
|
400
799
|
min={0}
|
|
401
|
-
onChange={(v) => handleUpdate({
|
|
800
|
+
onChange={(v) => handleUpdate({ frameThickness: v })}
|
|
402
801
|
precision={3}
|
|
403
802
|
step={0.01}
|
|
404
803
|
unit="m"
|
|
405
|
-
value={Math.round(node.
|
|
804
|
+
value={Math.round(node.frameThickness * 1000) / 1000}
|
|
406
805
|
/>
|
|
407
806
|
<SliderControl
|
|
408
|
-
label="
|
|
807
|
+
label="Depth"
|
|
409
808
|
min={0}
|
|
410
|
-
onChange={(v) => handleUpdate({
|
|
809
|
+
onChange={(v) => handleUpdate({ frameDepth: v })}
|
|
411
810
|
precision={3}
|
|
412
811
|
step={0.01}
|
|
413
812
|
unit="m"
|
|
414
|
-
value={Math.round(node.
|
|
813
|
+
value={Math.round(node.frameDepth * 1000) / 1000}
|
|
415
814
|
/>
|
|
416
|
-
</
|
|
417
|
-
|
|
418
|
-
|
|
815
|
+
</PanelSection>
|
|
816
|
+
|
|
817
|
+
<PanelSection title="Grid">
|
|
818
|
+
<SliderControl
|
|
819
|
+
label="Columns"
|
|
820
|
+
max={8}
|
|
821
|
+
min={1}
|
|
822
|
+
onChange={(v) => {
|
|
823
|
+
const n = Math.max(1, Math.min(8, Math.round(v)))
|
|
824
|
+
handleUpdate({ columnRatios: Array(n).fill(1 / n) })
|
|
825
|
+
}}
|
|
826
|
+
precision={0}
|
|
827
|
+
step={1}
|
|
828
|
+
value={numCols}
|
|
829
|
+
/>
|
|
830
|
+
<SliderControl
|
|
831
|
+
label="Rows"
|
|
832
|
+
max={8}
|
|
833
|
+
min={1}
|
|
834
|
+
onChange={(v) => {
|
|
835
|
+
const n = Math.max(1, Math.min(8, Math.round(v)))
|
|
836
|
+
handleUpdate({ rowRatios: Array(n).fill(1 / n) })
|
|
837
|
+
}}
|
|
838
|
+
precision={0}
|
|
839
|
+
step={1}
|
|
840
|
+
value={numRows}
|
|
841
|
+
/>
|
|
842
|
+
|
|
843
|
+
{numCols > 1 && (
|
|
844
|
+
<div className="mt-2 flex flex-col gap-1">
|
|
845
|
+
<div className="mb-1 px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
|
|
846
|
+
Col Widths
|
|
847
|
+
</div>
|
|
848
|
+
{normCols.map((ratio, i) => (
|
|
849
|
+
<SliderControl
|
|
850
|
+
key={`c-${i}`}
|
|
851
|
+
label={`C${i + 1}`}
|
|
852
|
+
max={95}
|
|
853
|
+
min={5}
|
|
854
|
+
onChange={(v) => setColumnRatio(i, v / 100)}
|
|
855
|
+
precision={1}
|
|
856
|
+
step={1}
|
|
857
|
+
unit="%"
|
|
858
|
+
value={Math.round(ratio * 100 * 10) / 10}
|
|
859
|
+
/>
|
|
860
|
+
))}
|
|
861
|
+
<div className="mt-1 border-border/50 border-t pt-1">
|
|
862
|
+
<SliderControl
|
|
863
|
+
label="Divider"
|
|
864
|
+
max={0.1}
|
|
865
|
+
min={0.005}
|
|
866
|
+
onChange={(v) => handleUpdate({ columnDividerThickness: v })}
|
|
867
|
+
precision={3}
|
|
868
|
+
step={0.01}
|
|
869
|
+
unit="m"
|
|
870
|
+
value={Math.round((node.columnDividerThickness ?? 0.03) * 1000) / 1000}
|
|
871
|
+
/>
|
|
872
|
+
</div>
|
|
873
|
+
</div>
|
|
874
|
+
)}
|
|
875
|
+
|
|
876
|
+
{numRows > 1 && (
|
|
877
|
+
<div className="mt-2 flex flex-col gap-1">
|
|
878
|
+
<div className="mb-1 px-1 font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
|
|
879
|
+
Row Heights
|
|
880
|
+
</div>
|
|
881
|
+
{normRows.map((ratio, i) => (
|
|
882
|
+
<SliderControl
|
|
883
|
+
key={`r-${i}`}
|
|
884
|
+
label={`R${i + 1}`}
|
|
885
|
+
max={95}
|
|
886
|
+
min={5}
|
|
887
|
+
onChange={(v) => setRowRatio(i, v / 100)}
|
|
888
|
+
precision={1}
|
|
889
|
+
step={1}
|
|
890
|
+
unit="%"
|
|
891
|
+
value={Math.round(ratio * 100 * 10) / 10}
|
|
892
|
+
/>
|
|
893
|
+
))}
|
|
894
|
+
<div className="mt-1 border-border/50 border-t pt-1">
|
|
895
|
+
<SliderControl
|
|
896
|
+
label="Divider"
|
|
897
|
+
max={0.1}
|
|
898
|
+
min={0.005}
|
|
899
|
+
onChange={(v) => handleUpdate({ rowDividerThickness: v })}
|
|
900
|
+
precision={3}
|
|
901
|
+
step={0.01}
|
|
902
|
+
unit="m"
|
|
903
|
+
value={Math.round((node.rowDividerThickness ?? 0.03) * 1000) / 1000}
|
|
904
|
+
/>
|
|
905
|
+
</div>
|
|
906
|
+
</div>
|
|
907
|
+
)}
|
|
908
|
+
</PanelSection>
|
|
909
|
+
|
|
910
|
+
<PanelSection title="Sill">
|
|
911
|
+
<ToggleControl
|
|
912
|
+
checked={node.sill}
|
|
913
|
+
label="Enable Sill"
|
|
914
|
+
onChange={(checked) => handleUpdate({ sill: checked })}
|
|
915
|
+
/>
|
|
916
|
+
{node.sill && (
|
|
917
|
+
<div className="mt-1 flex flex-col gap-1">
|
|
918
|
+
<SliderControl
|
|
919
|
+
label="Depth"
|
|
920
|
+
min={0}
|
|
921
|
+
onChange={(v) => handleUpdate({ sillDepth: v })}
|
|
922
|
+
precision={3}
|
|
923
|
+
step={0.01}
|
|
924
|
+
unit="m"
|
|
925
|
+
value={Math.round(node.sillDepth * 1000) / 1000}
|
|
926
|
+
/>
|
|
927
|
+
<SliderControl
|
|
928
|
+
label="Thickness"
|
|
929
|
+
min={0}
|
|
930
|
+
onChange={(v) => handleUpdate({ sillThickness: v })}
|
|
931
|
+
precision={3}
|
|
932
|
+
step={0.01}
|
|
933
|
+
unit="m"
|
|
934
|
+
value={Math.round(node.sillThickness * 1000) / 1000}
|
|
935
|
+
/>
|
|
936
|
+
</div>
|
|
937
|
+
)}
|
|
938
|
+
</PanelSection>
|
|
939
|
+
</>
|
|
940
|
+
)}
|
|
419
941
|
|
|
420
942
|
<PanelSection title="Actions">
|
|
421
943
|
<ActionGroup>
|
|
@@ -433,9 +955,6 @@ export function WindowPanel() {
|
|
|
433
955
|
/>
|
|
434
956
|
</ActionGroup>
|
|
435
957
|
</PanelSection>
|
|
436
|
-
<PanelSection title="Material">
|
|
437
|
-
<MaterialPicker onChange={handleMaterialChange} value={node.material} />
|
|
438
|
-
</PanelSection>
|
|
439
958
|
</PanelWrapper>
|
|
440
959
|
)
|
|
441
960
|
}
|