@pascal-app/editor 0.7.0 → 0.9.1

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.
Files changed (244) hide show
  1. package/package.json +12 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +755 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +62 -54
  6. package/src/components/editor/export-manager.tsx +32 -3
  7. package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
  8. package/src/components/editor/first-person/build-collider-world.ts +203 -87
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
  10. package/src/components/editor/first-person-controls.tsx +836 -31
  11. package/src/components/editor/floating-action-menu.tsx +287 -196
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floorplan-panel.tsx +5837 -12006
  14. package/src/components/editor/grid.tsx +51 -17
  15. package/src/components/editor/group-move-handle.tsx +316 -0
  16. package/src/components/editor/group-rotate-handle.tsx +382 -0
  17. package/src/components/editor/group-transform-shared.test.ts +205 -0
  18. package/src/components/editor/group-transform-shared.ts +256 -0
  19. package/src/components/editor/handles/handle-arrow.tsx +523 -0
  20. package/src/components/editor/handles/use-handle-drag.ts +213 -0
  21. package/src/components/editor/index.tsx +234 -190
  22. package/src/components/editor/measurement-pill.tsx +75 -0
  23. package/src/components/editor/node-arrow-handles.tsx +1453 -0
  24. package/src/components/editor/selection-manager.tsx +435 -158
  25. package/src/components/editor/site-edge-labels.tsx +44 -20
  26. package/src/components/editor/slab-hole-highlights.tsx +466 -0
  27. package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
  28. package/src/components/editor/thumbnail-generator.tsx +27 -64
  29. package/src/components/editor/use-floorplan-background-placement.ts +122 -35
  30. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  31. package/src/components/editor/use-floorplan-scene-data.ts +14 -2
  32. package/src/components/editor/wall-measurement-label.tsx +11 -6
  33. package/src/components/editor/wall-move-side-handles.tsx +855 -0
  34. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  35. package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
  36. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
  37. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  38. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  39. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
  41. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  42. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
  43. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  44. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
  45. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  46. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
  47. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  49. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
  50. package/src/components/editor-2d/svg-paths.ts +10 -2
  51. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
  52. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  53. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  54. package/src/components/systems/stair/stair-edit-system.tsx +2 -2
  55. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  56. package/src/components/systems/zone/zone-system.tsx +0 -0
  57. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  58. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  59. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  60. package/src/components/tools/fence/fence-drafting.ts +64 -22
  61. package/src/components/tools/item/move-tool.tsx +40 -107
  62. package/src/components/tools/item/placement-math.test.ts +10 -0
  63. package/src/components/tools/item/placement-math.ts +33 -9
  64. package/src/components/tools/item/placement-strategies.ts +201 -25
  65. package/src/components/tools/item/placement-types.ts +10 -1
  66. package/src/components/tools/item/use-draft-node.ts +20 -2
  67. package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
  68. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  69. package/src/components/tools/roof/roof-tool.tsx +108 -12
  70. package/src/components/tools/select/box-select-state.ts +77 -0
  71. package/src/components/tools/select/box-select-tool.tsx +324 -543
  72. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  73. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  74. package/src/components/tools/select/select-candidates.test.ts +109 -0
  75. package/src/components/tools/select/select-candidates.ts +85 -0
  76. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  77. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  78. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  79. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  80. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  81. package/src/components/tools/shared/placement-box.tsx +123 -0
  82. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  83. package/src/components/tools/shared/segment-angle.ts +57 -1
  84. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  85. package/src/components/tools/site/site-flag-model.ts +18 -0
  86. package/src/components/tools/stair/stair-defaults.ts +1 -1
  87. package/src/components/tools/stair/stair-tool.tsx +269 -35
  88. package/src/components/tools/tool-manager.tsx +172 -56
  89. package/src/components/tools/wall/wall-drafting.ts +98 -98
  90. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  91. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  92. package/src/components/tools/zone/zone-tool.tsx +36 -11
  93. package/src/components/ui/action-menu/action-button.tsx +21 -1
  94. package/src/components/ui/action-menu/camera-actions.tsx +0 -0
  95. package/src/components/ui/action-menu/control-modes.tsx +5 -76
  96. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  97. package/src/components/ui/action-menu/index.tsx +33 -196
  98. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  99. package/src/components/ui/action-menu/view-toggles.tsx +262 -37
  100. package/src/components/ui/command-palette/editor-commands.tsx +24 -17
  101. package/src/components/ui/command-palette/index.tsx +6 -256
  102. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  103. package/src/components/ui/controls/material-picker.tsx +17 -35
  104. package/src/components/ui/controls/metric-control.tsx +33 -11
  105. package/src/components/ui/floating-level-selector.tsx +48 -11
  106. package/src/components/ui/helpers/helper-manager.tsx +21 -17
  107. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  108. package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
  109. package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
  110. package/src/components/ui/level-duplicate-dialog.tsx +5 -6
  111. package/src/components/ui/panels/node-display.ts +1 -0
  112. package/src/components/ui/panels/paint-panel.tsx +66 -41
  113. package/src/components/ui/panels/panel-manager.tsx +33 -74
  114. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  115. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  116. package/src/components/ui/panels/reference-panel.tsx +28 -13
  117. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  118. package/src/components/ui/primitives/number-input.tsx +1 -1
  119. package/src/components/ui/primitives/sidebar.tsx +0 -0
  120. package/src/components/ui/scene-loader.tsx +1 -3
  121. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  122. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  123. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  124. package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  127. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  128. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  129. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  132. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  133. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  134. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  135. package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
  136. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  137. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  138. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  139. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  140. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  141. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  142. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  143. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
  144. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
  145. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
  146. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  147. package/src/components/ui/slider.tsx +1 -1
  148. package/src/components/viewer-overlay.tsx +169 -52
  149. package/src/components/viewer-zone-system.tsx +1 -1
  150. package/src/hooks/use-auto-save.ts +24 -3
  151. package/src/hooks/use-ceiling-events.ts +175 -0
  152. package/src/hooks/use-drag-action.ts +124 -0
  153. package/src/hooks/use-keyboard.ts +125 -62
  154. package/src/hooks/use-selection.ts +64 -0
  155. package/src/index.tsx +243 -4
  156. package/src/lib/constants.ts +6 -2
  157. package/src/lib/editor-api.ts +62 -0
  158. package/src/lib/elevator-support.ts +96 -0
  159. package/src/lib/floorplan/apply-alignment.ts +111 -0
  160. package/src/lib/floorplan/index.ts +7 -0
  161. package/src/lib/floorplan/selection-tool.ts +40 -0
  162. package/src/lib/floorplan/stairs.ts +12 -1
  163. package/src/lib/fresh-planar-placement.test.ts +102 -0
  164. package/src/lib/fresh-planar-placement.ts +63 -0
  165. package/src/lib/history.ts +2 -1
  166. package/src/lib/level-duplication.test.ts +0 -2
  167. package/src/lib/level-duplication.ts +55 -43
  168. package/src/lib/level-name.ts +11 -0
  169. package/src/lib/material-paint.ts +212 -20
  170. package/src/lib/placement-metadata.ts +29 -0
  171. package/src/lib/planar-cursor-placement.test.ts +43 -0
  172. package/src/lib/planar-cursor-placement.ts +42 -0
  173. package/src/lib/roof-duplication.ts +2 -2
  174. package/src/lib/scene-bounds.ts +1 -1
  175. package/src/lib/scene-clipboard.ts +267 -0
  176. package/src/lib/scene.ts +13 -4
  177. package/src/lib/sfx-bus.ts +25 -1
  178. package/src/lib/sfx-player.ts +75 -27
  179. package/src/lib/site-boundary.ts +1 -0
  180. package/src/lib/stair-duplication.ts +2 -2
  181. package/src/lib/stair-levels.test.ts +146 -0
  182. package/src/lib/stair-levels.ts +177 -0
  183. package/src/lib/world-grid-snap.ts +199 -0
  184. package/src/lib/zone-content.ts +130 -0
  185. package/src/store/use-alignment-guides.ts +21 -0
  186. package/src/store/use-editor.tsx +342 -91
  187. package/src/store/use-placement-preview.ts +31 -0
  188. package/src/store/use-wall-move-ghosts.ts +36 -0
  189. package/src/store/use-wall-snap-indicator.ts +32 -0
  190. package/tsconfig.json +2 -1
  191. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  192. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  193. package/src/components/feedback-dialog.tsx +0 -265
  194. package/src/components/pascal-radio.tsx +0 -280
  195. package/src/components/preview-button.tsx +0 -16
  196. package/src/components/tools/building/move-building-tool.tsx +0 -157
  197. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  198. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  199. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  200. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
  201. package/src/components/tools/column/column-tool.tsx +0 -97
  202. package/src/components/tools/column/move-column-tool.tsx +0 -105
  203. package/src/components/tools/door/door-math.ts +0 -110
  204. package/src/components/tools/door/door-tool.tsx +0 -324
  205. package/src/components/tools/door/move-door-tool.tsx +0 -412
  206. package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
  207. package/src/components/tools/fence/fence-tool.tsx +0 -346
  208. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
  209. package/src/components/tools/fence/move-fence-tool.tsx +0 -298
  210. package/src/components/tools/item/item-tool.tsx +0 -26
  211. package/src/components/tools/roof/move-roof-tool.tsx +0 -359
  212. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  213. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  214. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  215. package/src/components/tools/slab/slab-tool.tsx +0 -322
  216. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  217. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  218. package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
  219. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
  220. package/src/components/tools/wall/move-wall-tool.tsx +0 -356
  221. package/src/components/tools/wall/wall-tool.tsx +0 -332
  222. package/src/components/tools/window/move-window-tool.tsx +0 -447
  223. package/src/components/tools/window/window-math.ts +0 -117
  224. package/src/components/tools/window/window-tool.tsx +0 -332
  225. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  226. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  227. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  228. package/src/components/ui/panels/ceiling-panel.tsx +0 -250
  229. package/src/components/ui/panels/column-panel.tsx +0 -715
  230. package/src/components/ui/panels/door-panel.tsx +0 -1292
  231. package/src/components/ui/panels/fence-panel.tsx +0 -227
  232. package/src/components/ui/panels/item-panel.tsx +0 -306
  233. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  234. package/src/components/ui/panels/roof-panel.tsx +0 -230
  235. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  236. package/src/components/ui/panels/slab-panel.tsx +0 -250
  237. package/src/components/ui/panels/spawn-panel.tsx +0 -155
  238. package/src/components/ui/panels/stair-panel.tsx +0 -525
  239. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  240. package/src/components/ui/panels/wall-panel.tsx +0 -177
  241. package/src/components/ui/panels/window-panel.tsx +0 -960
  242. package/src/components/ui/viewer-toolbar.tsx +0 -436
  243. package/src/contexts/presets-context.tsx +0 -121
  244. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -1,960 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNode,
5
- type AnyNodeId,
6
- emitter,
7
- useInteractive,
8
- useScene,
9
- WindowNode,
10
- } from '@pascal-app/core'
11
- import { useViewer } from '@pascal-app/viewer'
12
- import { BookMarked, Copy, FlipHorizontal2, Move, Trash2 } from 'lucide-react'
13
- import { useCallback, useRef } from 'react'
14
- import { usePresetsAdapter } from '../../../contexts/presets-context'
15
- import { cn } from '../../../lib/utils'
16
- import { sfxEmitter } from '../../../lib/sfx-bus'
17
- import useEditor from '../../../store/use-editor'
18
- import { ActionButton, ActionGroup } from '../controls/action-button'
19
- import { MetricControl } from '../controls/metric-control'
20
- import { PanelSection } from '../controls/panel-section'
21
- import { SegmentedControl } from '../controls/segmented-control'
22
- import { SliderControl } from '../controls/slider-control'
23
- import { ToggleControl } from '../controls/toggle-control'
24
- import { PanelWrapper } from './panel-wrapper'
25
- import { PresetsPopover } from './presets/presets-popover'
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
-
92
- export function WindowPanel() {
93
- const selectedId = useViewer((s) => s.selection.selectedIds[0])
94
- const setSelection = useViewer((s) => s.setSelection)
95
- const updateNode = useScene((s) => s.updateNode)
96
- const deleteNode = useScene((s) => s.deleteNode)
97
- const setMovingNode = useEditor((s) => s.setMovingNode)
98
- const previewRef = useRef<{
99
- id: AnyNodeId
100
- key: keyof WindowNode
101
- value: unknown
102
- } | null>(null)
103
-
104
- const adapter = usePresetsAdapter()
105
-
106
- const node = useScene((s) =>
107
- selectedId ? (s.nodes[selectedId as AnyNode['id']] as WindowNode | undefined) : undefined,
108
- )
109
-
110
- const handleUpdate = useCallback(
111
- (updates: Partial<WindowNode>) => {
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
-
119
- updateNode(selectedId as AnyNode['id'], updates)
120
- useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
121
- },
122
- [selectedId, node, updateNode],
123
- )
124
-
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)
145
- },
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],
166
- )
167
-
168
- const handleClose = useCallback(() => {
169
- setSelection({ selectedIds: [] })
170
- }, [setSelection])
171
-
172
- const handleFlip = useCallback(() => {
173
- if (!node) return
174
- handleUpdate({
175
- side: node.side === 'front' ? 'back' : 'front',
176
- rotation: [node.rotation[0], node.rotation[1] + Math.PI, node.rotation[2]],
177
- })
178
- }, [node, handleUpdate])
179
-
180
- const handleMove = useCallback(() => {
181
- if (!node) return
182
- sfxEmitter.emit('sfx:item-pick')
183
- setMovingNode(node)
184
- setSelection({ selectedIds: [] })
185
- }, [node, setMovingNode, setSelection])
186
-
187
- const handleDelete = useCallback(() => {
188
- if (!(selectedId && node)) return
189
- sfxEmitter.emit('sfx:item-delete')
190
- deleteNode(selectedId as AnyNode['id'])
191
- if (node.parentId) useScene.getState().dirtyNodes.add(node.parentId as AnyNodeId)
192
- setSelection({ selectedIds: [] })
193
- }, [selectedId, node, deleteNode, setSelection])
194
-
195
- const handleDuplicate = useCallback(() => {
196
- if (!node?.parentId) return
197
- sfxEmitter.emit('sfx:item-pick')
198
- useScene.temporal.getState().pause()
199
- const duplicate = WindowNode.parse({
200
- position: [...node.position] as [number, number, number],
201
- rotation: [...node.rotation] as [number, number, number],
202
- side: node.side,
203
- wallId: node.wallId,
204
- parentId: node.parentId,
205
- width: node.width,
206
- height: node.height,
207
- windowType: node.windowType,
208
- operationState: node.operationState,
209
- awningDirection: node.awningDirection,
210
- casementStyle: node.casementStyle,
211
- hingesSide: node.hingesSide,
212
- frameThickness: node.frameThickness,
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,
221
- columnRatios: [...node.columnRatios],
222
- rowRatios: [...node.rowRatios],
223
- columnDividerThickness: node.columnDividerThickness,
224
- rowDividerThickness: node.rowDividerThickness,
225
- sill: node.sill,
226
- sillDepth: node.sillDepth,
227
- sillThickness: node.sillThickness,
228
- metadata: { isNew: true },
229
- })
230
- useScene.getState().createNode(duplicate, node.parentId as AnyNodeId)
231
- setMovingNode(duplicate)
232
- setSelection({ selectedIds: [] })
233
- }, [node, setMovingNode, setSelection])
234
-
235
- const getWindowPresetData = useCallback(() => {
236
- if (!node) return null
237
- return {
238
- width: node.width,
239
- height: node.height,
240
- windowType: node.windowType,
241
- operationState: node.operationState,
242
- awningDirection: node.awningDirection,
243
- casementStyle: node.casementStyle,
244
- hingesSide: node.hingesSide,
245
- frameThickness: node.frameThickness,
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,
254
- columnRatios: node.columnRatios,
255
- rowRatios: node.rowRatios,
256
- columnDividerThickness: node.columnDividerThickness,
257
- rowDividerThickness: node.rowDividerThickness,
258
- sill: node.sill,
259
- sillDepth: node.sillDepth,
260
- sillThickness: node.sillThickness,
261
- }
262
- }, [node])
263
-
264
- const handleSavePreset = useCallback(
265
- async (name: string) => {
266
- const data = getWindowPresetData()
267
- if (!(data && selectedId)) return
268
- const presetId = await adapter.savePreset('window', name, data)
269
- if (presetId) emitter.emit('preset:generate-thumbnail', { presetId, nodeId: selectedId })
270
- },
271
- [getWindowPresetData, selectedId, adapter],
272
- )
273
-
274
- const handleOverwritePreset = useCallback(
275
- async (id: string) => {
276
- const data = getWindowPresetData()
277
- if (!(data && selectedId)) return
278
- await adapter.overwritePreset('window', id, data)
279
- emitter.emit('preset:generate-thumbnail', { presetId: id, nodeId: selectedId })
280
- },
281
- [getWindowPresetData, selectedId, adapter],
282
- )
283
-
284
- const handleApplyPreset = useCallback(
285
- (data: Record<string, unknown>) => {
286
- handleUpdate(data as Partial<WindowNode>)
287
- },
288
- [handleUpdate],
289
- )
290
-
291
- if (!(node && node.type === 'window' && selectedId)) return null
292
-
293
- const numCols = node.columnRatios.length
294
- const numRows = node.rowRatios.length
295
-
296
- const colSum = node.columnRatios.reduce((a, b) => a + b, 0)
297
- const rowSum = node.rowRatios.reduce((a, b) => a + b, 0)
298
- const normCols = node.columnRatios.map((r) => r / colSum)
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
- }
369
-
370
- const setColumnRatio = (index: number, newVal: number) => {
371
- const clamped = Math.max(0.05, Math.min(0.95, newVal))
372
- const neighborIdx = index < numCols - 1 ? index + 1 : index - 1
373
- const delta = clamped - normCols[index]!
374
- const neighborVal = Math.max(0.05, normCols[neighborIdx]! - delta)
375
- const newRatios = normCols.map((v, i) => {
376
- if (i === index) return clamped
377
- if (i === neighborIdx) return neighborVal
378
- return v
379
- })
380
- handleUpdate({ columnRatios: newRatios })
381
- }
382
-
383
- const setRowRatio = (index: number, newVal: number) => {
384
- const clamped = Math.max(0.05, Math.min(0.95, newVal))
385
- const neighborIdx = index < numRows - 1 ? index + 1 : index - 1
386
- const delta = clamped - normRows[index]!
387
- const neighborVal = Math.max(0.05, normRows[neighborIdx]! - delta)
388
- const newRatios = normRows.map((v, i) => {
389
- if (i === index) return clamped
390
- if (i === neighborIdx) return neighborVal
391
- return v
392
- })
393
- handleUpdate({ rowRatios: newRatios })
394
- }
395
-
396
- return (
397
- <PanelWrapper
398
- icon="/icons/window.png"
399
- onClose={handleClose}
400
- title={node.name || 'Window'}
401
- width={320}
402
- >
403
- {/* Presets strip */}
404
- <div className="border-border/30 border-b px-3 pt-2.5 pb-1.5">
405
- <PresetsPopover
406
- isAuthenticated={adapter.isAuthenticated}
407
- onApply={handleApplyPreset}
408
- onDelete={(id) => adapter.deletePreset(id)}
409
- onFetchPresets={(tab) => adapter.fetchPresets('window', tab)}
410
- onOverwrite={handleOverwritePreset}
411
- onRename={(id, name) => adapter.renamePreset(id, name)}
412
- onSave={handleSavePreset}
413
- onToggleCommunity={adapter.togglePresetCommunity}
414
- tabs={adapter.tabs}
415
- type="window"
416
- >
417
- <button className="flex w-full items-center gap-2 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 font-medium text-muted-foreground text-xs transition-colors hover:bg-[#3e3e3e] hover:text-foreground">
418
- <BookMarked className="h-3.5 w-3.5 shrink-0" />
419
- <span>Presets</span>
420
- </button>
421
- </PresetsPopover>
422
- </div>
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
-
540
- <PanelSection title="Position">
541
- <SliderControl
542
- label={
543
- <>
544
- X<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
545
- </>
546
- }
547
- onChange={(v) => handleUpdate({ position: [v, node.position[1], node.position[2]] })}
548
- precision={2}
549
- step={0.1}
550
- unit="m"
551
- value={Math.round(node.position[0] * 100) / 100}
552
- />
553
- <SliderControl
554
- label={
555
- <>
556
- Y<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
557
- </>
558
- }
559
- onChange={(v) => handleUpdate({ position: [node.position[0], v, node.position[2]] })}
560
- precision={2}
561
- step={0.1}
562
- unit="m"
563
- value={Math.round(node.position[1] * 100) / 100}
564
- />
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
- )}
575
- </PanelSection>
576
-
577
- <PanelSection title="Dimensions">
578
- <SliderControl
579
- label="Width"
580
- min={0}
581
- onChange={(v) => handleUpdate(getDimensionUpdates({ width: v }))}
582
- precision={2}
583
- restoreOnCommit={false}
584
- step={0.1}
585
- unit="m"
586
- value={Math.round(node.width * 100) / 100}
587
- />
588
- <SliderControl
589
- label="Height"
590
- min={0}
591
- onChange={(v) => handleUpdate(getDimensionUpdates({ height: v }))}
592
- precision={2}
593
- restoreOnCommit={false}
594
- step={0.1}
595
- unit="m"
596
- value={Math.round(node.height * 100) / 100}
597
- />
598
- </PanelSection>
599
-
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}
636
- />
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
- )}
672
- <SliderControl
673
- label="Reveal Radius"
674
- max={0.08}
675
- min={0}
676
- onChange={(value) => previewWindowUpdate('openingRevealRadius', value)}
677
- onCommit={(value) => commitWindowPreview('openingRevealRadius', value)}
678
- precision={3}
679
- step={0.005}
680
- unit="m"
681
- value={Math.round(openingRevealRadius * 1000) / 1000}
682
- />
683
- </div>
684
- )}
685
- {windowShape === 'arch' && (
686
- <div className="mt-2 flex flex-col gap-1">
687
- <SliderControl
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}
697
- />
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
- )}
763
- <SliderControl
764
- label="Reveal Radius"
765
- max={0.08}
766
- min={0}
767
- onChange={(value) => previewWindowUpdate('openingRevealRadius', value)}
768
- onCommit={(value) => commitWindowPreview('openingRevealRadius', value)}
769
- precision={3}
770
- step={0.005}
771
- unit="m"
772
- value={Math.round(openingRevealRadius * 1000) / 1000}
773
- />
774
- </div>
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
- )}
793
-
794
- {!isOpening && (
795
- <>
796
- <PanelSection title="Frame">
797
- <SliderControl
798
- label="Thickness"
799
- min={0}
800
- onChange={(v) => handleUpdate({ frameThickness: v })}
801
- precision={3}
802
- step={0.01}
803
- unit="m"
804
- value={Math.round(node.frameThickness * 1000) / 1000}
805
- />
806
- <SliderControl
807
- label="Depth"
808
- min={0}
809
- onChange={(v) => handleUpdate({ frameDepth: v })}
810
- precision={3}
811
- step={0.01}
812
- unit="m"
813
- value={Math.round(node.frameDepth * 1000) / 1000}
814
- />
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
- )}
941
-
942
- <PanelSection title="Actions">
943
- <ActionGroup>
944
- <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
945
- <ActionButton
946
- icon={<Copy className="h-3.5 w-3.5" />}
947
- label="Duplicate"
948
- onClick={handleDuplicate}
949
- />
950
- <ActionButton
951
- className="hover:bg-red-500/20"
952
- icon={<Trash2 className="h-3.5 w-3.5 text-red-400" />}
953
- label="Delete"
954
- onClick={handleDelete}
955
- />
956
- </ActionGroup>
957
- </PanelSection>
958
- </PanelWrapper>
959
- )
960
- }