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