@pascal-app/editor 0.6.0 → 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 +9 -5
- 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 +20 -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 +32 -55
- package/src/components/editor/floorplan-background-selection.ts +113 -0
- package/src/components/editor/floorplan-panel.tsx +9855 -3298
- package/src/components/editor/index.tsx +269 -21
- package/src/components/editor/selection-manager.tsx +575 -13
- package/src/components/editor/thumbnail-generator.tsx +38 -7
- 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 +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/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/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/fence-drafting.ts +10 -3
- package/src/components/tools/fence/fence-tool.tsx +159 -3
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +129 -18
- package/src/components/tools/fence/move-fence-tool.tsx +101 -34
- package/src/components/tools/item/move-tool.tsx +10 -1
- package/src/components/tools/item/placement-math.ts +30 -1
- package/src/components/tools/item/placement-strategies.ts +109 -31
- 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 +660 -52
- package/src/components/tools/roof/move-roof-tool.tsx +22 -15
- 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 +18 -3
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +121 -20
- package/src/components/tools/wall/wall-drafting.ts +18 -9
- package/src/components/tools/wall/wall-tool.tsx +134 -2
- package/src/components/tools/window/move-window-tool.tsx +18 -0
- package/src/components/tools/window/window-tool.tsx +5 -0
- package/src/components/ui/action-menu/camera-actions.tsx +37 -33
- package/src/components/ui/action-menu/control-modes.tsx +28 -1
- 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 +18 -1
- package/src/components/ui/controls/material-picker.tsx +152 -165
- 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 +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1116 -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 +1 -25
- package/src/components/ui/panels/column-panel.tsx +715 -0
- package/src/components/ui/panels/door-panel.tsx +981 -289
- package/src/components/ui/panels/fence-panel.tsx +3 -45
- 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 +210 -1
- package/src/components/ui/panels/panel-wrapper.tsx +48 -39
- package/src/components/ui/panels/reference-panel.tsx +238 -5
- package/src/components/ui/panels/roof-panel.tsx +4 -105
- 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 +155 -0
- package/src/components/ui/panels/stair-panel.tsx +11 -117
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -25
- package/src/components/ui/panels/wall-panel.tsx +1 -95
- package/src/components/ui/panels/window-panel.tsx +660 -139
- 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 +2 -2
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +77 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +109 -24
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +82 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +9 -3
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/tab-bar.tsx +3 -0
- package/src/components/ui/viewer-toolbar.tsx +42 -1
- package/src/hooks/use-auto-frame.ts +45 -0
- package/src/hooks/use-keyboard.ts +64 -7
- package/src/hooks/use-mobile.ts +12 -12
- 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 +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/stair-duplication.ts +126 -0
- package/src/lib/window-interaction.ts +86 -0
- package/src/store/use-editor.tsx +164 -8
|
@@ -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
|
+
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
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,60 @@ 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
|
+
!(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)
|
|
51
145
|
},
|
|
52
|
-
[
|
|
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],
|
|
53
166
|
)
|
|
54
167
|
|
|
55
168
|
const handleClose = useCallback(() => {
|
|
@@ -91,8 +204,20 @@ export function WindowPanel() {
|
|
|
91
204
|
parentId: node.parentId,
|
|
92
205
|
width: node.width,
|
|
93
206
|
height: node.height,
|
|
207
|
+
windowType: node.windowType,
|
|
208
|
+
operationState: node.operationState,
|
|
209
|
+
awningDirection: node.awningDirection,
|
|
210
|
+
casementStyle: node.casementStyle,
|
|
211
|
+
hingesSide: node.hingesSide,
|
|
94
212
|
frameThickness: node.frameThickness,
|
|
95
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,
|
|
96
221
|
columnRatios: [...node.columnRatios],
|
|
97
222
|
rowRatios: [...node.rowRatios],
|
|
98
223
|
columnDividerThickness: node.columnDividerThickness,
|
|
@@ -112,8 +237,20 @@ export function WindowPanel() {
|
|
|
112
237
|
return {
|
|
113
238
|
width: node.width,
|
|
114
239
|
height: node.height,
|
|
240
|
+
windowType: node.windowType,
|
|
241
|
+
operationState: node.operationState,
|
|
242
|
+
awningDirection: node.awningDirection,
|
|
243
|
+
casementStyle: node.casementStyle,
|
|
244
|
+
hingesSide: node.hingesSide,
|
|
115
245
|
frameThickness: node.frameThickness,
|
|
116
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,
|
|
117
254
|
columnRatios: node.columnRatios,
|
|
118
255
|
rowRatios: node.rowRatios,
|
|
119
256
|
columnDividerThickness: node.columnDividerThickness,
|
|
@@ -160,6 +297,75 @@ export function WindowPanel() {
|
|
|
160
297
|
const rowSum = node.rowRatios.reduce((a, b) => a + b, 0)
|
|
161
298
|
const normCols = node.columnRatios.map((r) => r / colSum)
|
|
162
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
|
+
}
|
|
163
369
|
|
|
164
370
|
const setColumnRatio = (index: number, newVal: number) => {
|
|
165
371
|
const clamped = Math.max(0.05, Math.min(0.95, newVal))
|
|
@@ -215,6 +421,122 @@ export function WindowPanel() {
|
|
|
215
421
|
</PresetsPopover>
|
|
216
422
|
</div>
|
|
217
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
|
+
|
|
218
540
|
<PanelSection title="Position">
|
|
219
541
|
<SliderControl
|
|
220
542
|
label={
|
|
@@ -240,22 +562,25 @@ export function WindowPanel() {
|
|
|
240
562
|
unit="m"
|
|
241
563
|
value={Math.round(node.position[1] * 100) / 100}
|
|
242
564
|
/>
|
|
243
|
-
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
+
)}
|
|
251
575
|
</PanelSection>
|
|
252
576
|
|
|
253
577
|
<PanelSection title="Dimensions">
|
|
254
578
|
<SliderControl
|
|
255
579
|
label="Width"
|
|
256
580
|
min={0}
|
|
257
|
-
onChange={(v) => handleUpdate({ width: v })}
|
|
581
|
+
onChange={(v) => handleUpdate(getDimensionUpdates({ width: v }))}
|
|
258
582
|
precision={2}
|
|
583
|
+
restoreOnCommit={false}
|
|
259
584
|
step={0.1}
|
|
260
585
|
unit="m"
|
|
261
586
|
value={Math.round(node.width * 100) / 100}
|
|
@@ -263,157 +588,356 @@ export function WindowPanel() {
|
|
|
263
588
|
<SliderControl
|
|
264
589
|
label="Height"
|
|
265
590
|
min={0}
|
|
266
|
-
onChange={(v) => handleUpdate({ height: v })}
|
|
591
|
+
onChange={(v) => handleUpdate(getDimensionUpdates({ height: v }))}
|
|
267
592
|
precision={2}
|
|
593
|
+
restoreOnCommit={false}
|
|
268
594
|
step={0.1}
|
|
269
595
|
unit="m"
|
|
270
596
|
value={Math.round(node.height * 100) / 100}
|
|
271
597
|
/>
|
|
272
598
|
</PanelSection>
|
|
273
599
|
|
|
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}
|
|
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}
|
|
337
636
|
/>
|
|
338
|
-
|
|
339
|
-
|
|
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
|
+
)}
|
|
340
672
|
<SliderControl
|
|
341
|
-
label="
|
|
342
|
-
max={0.
|
|
343
|
-
min={0
|
|
344
|
-
onChange={(
|
|
673
|
+
label="Reveal Radius"
|
|
674
|
+
max={0.08}
|
|
675
|
+
min={0}
|
|
676
|
+
onChange={(value) => previewWindowUpdate('openingRevealRadius', value)}
|
|
677
|
+
onCommit={(value) => commitWindowPreview('openingRevealRadius', value)}
|
|
345
678
|
precision={3}
|
|
346
|
-
step={0.
|
|
679
|
+
step={0.005}
|
|
347
680
|
unit="m"
|
|
348
|
-
value={Math.round(
|
|
681
|
+
value={Math.round(openingRevealRadius * 1000) / 1000}
|
|
349
682
|
/>
|
|
350
683
|
</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) => (
|
|
684
|
+
)}
|
|
685
|
+
{windowShape === 'arch' && (
|
|
686
|
+
<div className="mt-2 flex flex-col gap-1">
|
|
360
687
|
<SliderControl
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
step={
|
|
368
|
-
unit="
|
|
369
|
-
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}
|
|
370
697
|
/>
|
|
371
|
-
|
|
372
|
-
|
|
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
|
+
)}
|
|
373
763
|
<SliderControl
|
|
374
|
-
label="
|
|
375
|
-
max={0.
|
|
376
|
-
min={0
|
|
377
|
-
onChange={(
|
|
764
|
+
label="Reveal Radius"
|
|
765
|
+
max={0.08}
|
|
766
|
+
min={0}
|
|
767
|
+
onChange={(value) => previewWindowUpdate('openingRevealRadius', value)}
|
|
768
|
+
onCommit={(value) => commitWindowPreview('openingRevealRadius', value)}
|
|
378
769
|
precision={3}
|
|
379
|
-
step={0.
|
|
770
|
+
step={0.005}
|
|
380
771
|
unit="m"
|
|
381
|
-
value={Math.round(
|
|
772
|
+
value={Math.round(openingRevealRadius * 1000) / 1000}
|
|
382
773
|
/>
|
|
383
774
|
</div>
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
+
)}
|
|
387
793
|
|
|
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">
|
|
794
|
+
{!isOpening && (
|
|
795
|
+
<>
|
|
796
|
+
<PanelSection title="Frame">
|
|
396
797
|
<SliderControl
|
|
397
|
-
label="
|
|
798
|
+
label="Thickness"
|
|
398
799
|
min={0}
|
|
399
|
-
onChange={(v) => handleUpdate({
|
|
800
|
+
onChange={(v) => handleUpdate({ frameThickness: v })}
|
|
400
801
|
precision={3}
|
|
401
802
|
step={0.01}
|
|
402
803
|
unit="m"
|
|
403
|
-
value={Math.round(node.
|
|
804
|
+
value={Math.round(node.frameThickness * 1000) / 1000}
|
|
404
805
|
/>
|
|
405
806
|
<SliderControl
|
|
406
|
-
label="
|
|
807
|
+
label="Depth"
|
|
407
808
|
min={0}
|
|
408
|
-
onChange={(v) => handleUpdate({
|
|
809
|
+
onChange={(v) => handleUpdate({ frameDepth: v })}
|
|
409
810
|
precision={3}
|
|
410
811
|
step={0.01}
|
|
411
812
|
unit="m"
|
|
412
|
-
value={Math.round(node.
|
|
813
|
+
value={Math.round(node.frameDepth * 1000) / 1000}
|
|
413
814
|
/>
|
|
414
|
-
</
|
|
415
|
-
|
|
416
|
-
|
|
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
|
+
)}
|
|
417
941
|
|
|
418
942
|
<PanelSection title="Actions">
|
|
419
943
|
<ActionGroup>
|
|
@@ -431,9 +955,6 @@ export function WindowPanel() {
|
|
|
431
955
|
/>
|
|
432
956
|
</ActionGroup>
|
|
433
957
|
</PanelSection>
|
|
434
|
-
<PanelSection title="Material">
|
|
435
|
-
<MaterialPicker onChange={handleMaterialChange} value={node.material} />
|
|
436
|
-
</PanelSection>
|
|
437
958
|
</PanelWrapper>
|
|
438
959
|
)
|
|
439
960
|
}
|