@pascal-app/editor 0.4.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.
Files changed (165) hide show
  1. package/package.json +62 -0
  2. package/src/components/editor/custom-camera-controls.tsx +387 -0
  3. package/src/components/editor/editor-layout-v2.tsx +220 -0
  4. package/src/components/editor/export-manager.tsx +78 -0
  5. package/src/components/editor/first-person-controls.tsx +249 -0
  6. package/src/components/editor/floating-action-menu.tsx +231 -0
  7. package/src/components/editor/floorplan-panel.tsx +9609 -0
  8. package/src/components/editor/grid.tsx +161 -0
  9. package/src/components/editor/index.tsx +928 -0
  10. package/src/components/editor/node-action-menu.tsx +66 -0
  11. package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
  12. package/src/components/editor/selection-manager.tsx +897 -0
  13. package/src/components/editor/site-edge-labels.tsx +90 -0
  14. package/src/components/editor/thumbnail-generator.tsx +166 -0
  15. package/src/components/editor/wall-measurement-label.tsx +258 -0
  16. package/src/components/feedback-dialog.tsx +265 -0
  17. package/src/components/pascal-radio.tsx +280 -0
  18. package/src/components/preview-button.tsx +16 -0
  19. package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
  20. package/src/components/systems/roof/roof-edit-system.tsx +69 -0
  21. package/src/components/systems/stair/stair-edit-system.tsx +69 -0
  22. package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
  23. package/src/components/systems/zone/zone-system.tsx +87 -0
  24. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
  25. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
  26. package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
  27. package/src/components/tools/door/door-math.ts +110 -0
  28. package/src/components/tools/door/door-tool.tsx +293 -0
  29. package/src/components/tools/door/move-door-tool.tsx +373 -0
  30. package/src/components/tools/item/item-tool.tsx +26 -0
  31. package/src/components/tools/item/move-tool.tsx +90 -0
  32. package/src/components/tools/item/placement-math.ts +85 -0
  33. package/src/components/tools/item/placement-strategies.ts +556 -0
  34. package/src/components/tools/item/placement-types.ts +117 -0
  35. package/src/components/tools/item/use-draft-node.ts +227 -0
  36. package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
  37. package/src/components/tools/roof/move-roof-tool.tsx +288 -0
  38. package/src/components/tools/roof/roof-tool.tsx +318 -0
  39. package/src/components/tools/select/box-select-tool.tsx +626 -0
  40. package/src/components/tools/shared/cursor-sphere.tsx +119 -0
  41. package/src/components/tools/shared/polygon-editor.tsx +361 -0
  42. package/src/components/tools/site/site-boundary-editor.tsx +42 -0
  43. package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
  44. package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
  45. package/src/components/tools/slab/slab-tool.tsx +322 -0
  46. package/src/components/tools/stair/stair-defaults.ts +7 -0
  47. package/src/components/tools/stair/stair-tool.tsx +194 -0
  48. package/src/components/tools/tool-manager.tsx +120 -0
  49. package/src/components/tools/wall/wall-drafting.ts +140 -0
  50. package/src/components/tools/wall/wall-tool.tsx +210 -0
  51. package/src/components/tools/window/move-window-tool.tsx +410 -0
  52. package/src/components/tools/window/window-math.ts +117 -0
  53. package/src/components/tools/window/window-tool.tsx +303 -0
  54. package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
  55. package/src/components/tools/zone/zone-tool.tsx +364 -0
  56. package/src/components/ui/action-menu/action-button.tsx +59 -0
  57. package/src/components/ui/action-menu/camera-actions.tsx +74 -0
  58. package/src/components/ui/action-menu/control-modes.tsx +240 -0
  59. package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
  60. package/src/components/ui/action-menu/index.tsx +152 -0
  61. package/src/components/ui/action-menu/structure-tools.tsx +100 -0
  62. package/src/components/ui/action-menu/view-toggles.tsx +397 -0
  63. package/src/components/ui/command-palette/editor-commands.tsx +396 -0
  64. package/src/components/ui/command-palette/index.tsx +730 -0
  65. package/src/components/ui/controls/action-button.tsx +33 -0
  66. package/src/components/ui/controls/material-picker.tsx +194 -0
  67. package/src/components/ui/controls/metric-control.tsx +262 -0
  68. package/src/components/ui/controls/panel-section.tsx +65 -0
  69. package/src/components/ui/controls/segmented-control.tsx +45 -0
  70. package/src/components/ui/controls/slider-control.tsx +245 -0
  71. package/src/components/ui/controls/toggle-control.tsx +38 -0
  72. package/src/components/ui/floating-level-selector.tsx +355 -0
  73. package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
  74. package/src/components/ui/helpers/helper-manager.tsx +33 -0
  75. package/src/components/ui/helpers/item-helper.tsx +40 -0
  76. package/src/components/ui/helpers/roof-helper.tsx +16 -0
  77. package/src/components/ui/helpers/slab-helper.tsx +20 -0
  78. package/src/components/ui/helpers/wall-helper.tsx +20 -0
  79. package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
  80. package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
  81. package/src/components/ui/panels/ceiling-panel.tsx +230 -0
  82. package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
  83. package/src/components/ui/panels/door-panel.tsx +600 -0
  84. package/src/components/ui/panels/item-panel.tsx +306 -0
  85. package/src/components/ui/panels/panel-manager.tsx +59 -0
  86. package/src/components/ui/panels/panel-wrapper.tsx +80 -0
  87. package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
  88. package/src/components/ui/panels/reference-panel.tsx +177 -0
  89. package/src/components/ui/panels/roof-panel.tsx +262 -0
  90. package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
  91. package/src/components/ui/panels/slab-panel.tsx +228 -0
  92. package/src/components/ui/panels/stair-panel.tsx +304 -0
  93. package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
  94. package/src/components/ui/panels/wall-panel.tsx +123 -0
  95. package/src/components/ui/panels/window-panel.tsx +441 -0
  96. package/src/components/ui/primitives/button.tsx +69 -0
  97. package/src/components/ui/primitives/card.tsx +75 -0
  98. package/src/components/ui/primitives/color-dot.tsx +61 -0
  99. package/src/components/ui/primitives/context-menu.tsx +227 -0
  100. package/src/components/ui/primitives/dialog.tsx +129 -0
  101. package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
  102. package/src/components/ui/primitives/error-boundary.tsx +52 -0
  103. package/src/components/ui/primitives/input.tsx +21 -0
  104. package/src/components/ui/primitives/number-input.tsx +187 -0
  105. package/src/components/ui/primitives/opacity-control.tsx +79 -0
  106. package/src/components/ui/primitives/popover.tsx +42 -0
  107. package/src/components/ui/primitives/separator.tsx +28 -0
  108. package/src/components/ui/primitives/sheet.tsx +130 -0
  109. package/src/components/ui/primitives/shortcut-token.tsx +64 -0
  110. package/src/components/ui/primitives/sidebar.tsx +855 -0
  111. package/src/components/ui/primitives/skeleton.tsx +13 -0
  112. package/src/components/ui/primitives/slider.tsx +58 -0
  113. package/src/components/ui/primitives/switch.tsx +29 -0
  114. package/src/components/ui/primitives/tooltip.tsx +57 -0
  115. package/src/components/ui/scene-loader.tsx +40 -0
  116. package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
  117. package/src/components/ui/sidebar/icon-rail.tsx +147 -0
  118. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
  119. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
  120. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
  121. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
  122. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
  123. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
  125. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
  126. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
  127. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
  128. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
  130. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
  131. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
  133. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
  134. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
  135. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
  136. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
  137. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
  138. package/src/components/ui/sidebar/tab-bar.tsx +39 -0
  139. package/src/components/ui/slider-demo.tsx +36 -0
  140. package/src/components/ui/slider.tsx +81 -0
  141. package/src/components/ui/viewer-toolbar.tsx +342 -0
  142. package/src/components/viewer-overlay.tsx +499 -0
  143. package/src/components/viewer-zone-system.tsx +48 -0
  144. package/src/contexts/presets-context.tsx +121 -0
  145. package/src/hooks/use-auto-save.ts +194 -0
  146. package/src/hooks/use-contextual-tools.ts +52 -0
  147. package/src/hooks/use-grid-events.ts +106 -0
  148. package/src/hooks/use-keyboard.ts +214 -0
  149. package/src/hooks/use-mobile.ts +19 -0
  150. package/src/hooks/use-reduced-motion.ts +20 -0
  151. package/src/index.tsx +33 -0
  152. package/src/lib/constants.ts +3 -0
  153. package/src/lib/level-selection.ts +31 -0
  154. package/src/lib/scene.ts +394 -0
  155. package/src/lib/sfx/index.ts +2 -0
  156. package/src/lib/sfx-bus.ts +49 -0
  157. package/src/lib/sfx-player.ts +60 -0
  158. package/src/lib/utils.ts +43 -0
  159. package/src/store/use-audio.tsx +45 -0
  160. package/src/store/use-command-registry.ts +36 -0
  161. package/src/store/use-editor.tsx +522 -0
  162. package/src/store/use-palette-view-registry.ts +45 -0
  163. package/src/store/use-upload.ts +90 -0
  164. package/src/three-types.ts +3 -0
  165. package/tsconfig.json +9 -0
@@ -0,0 +1,339 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ type AttachmentSide,
7
+ type MaterialSchema,
8
+ type StairSegmentNode,
9
+ StairSegmentNode as StairSegmentNodeSchema,
10
+ type StairSegmentType,
11
+ useScene,
12
+ } from '@pascal-app/core'
13
+ import { useViewer } from '@pascal-app/viewer'
14
+ import { Copy, Move, Trash2 } from 'lucide-react'
15
+ import { useCallback } from 'react'
16
+ import { sfxEmitter } from '../../../lib/sfx-bus'
17
+ import useEditor from '../../../store/use-editor'
18
+ import { ActionButton, ActionGroup } from '../controls/action-button'
19
+ import { MaterialPicker } from '../controls/material-picker'
20
+ import { MetricControl } from '../controls/metric-control'
21
+ import { PanelSection } from '../controls/panel-section'
22
+ import { SegmentedControl } from '../controls/segmented-control'
23
+ import { SliderControl } from '../controls/slider-control'
24
+ import { PanelWrapper } from './panel-wrapper'
25
+
26
+ const SEGMENT_TYPE_OPTIONS: { label: string; value: StairSegmentType }[] = [
27
+ { label: 'Flight', value: 'stair' },
28
+ { label: 'Landing', value: 'landing' },
29
+ ]
30
+
31
+ const ATTACHMENT_SIDE_OPTIONS: { label: string; value: AttachmentSide }[] = [
32
+ { label: 'Front', value: 'front' },
33
+ { label: 'Left', value: 'left' },
34
+ { label: 'Right', value: 'right' },
35
+ ]
36
+
37
+ export function StairSegmentPanel() {
38
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
39
+ const setSelection = useViewer((s) => s.setSelection)
40
+ const nodes = useScene((s) => s.nodes)
41
+ const updateNode = useScene((s) => s.updateNode)
42
+ const setMovingNode = useEditor((s) => s.setMovingNode)
43
+
44
+ const selectedId = selectedIds[0]
45
+ const node = selectedId
46
+ ? (nodes[selectedId as AnyNode['id']] as StairSegmentNode | undefined)
47
+ : undefined
48
+
49
+ // Check if this is the first segment in the parent stair
50
+ const isFirstSegment = (() => {
51
+ if (!node?.parentId) return true
52
+ const parent = nodes[node.parentId as AnyNodeId]
53
+ if (!parent || parent.type !== 'stair') return true
54
+ const children = (parent as any).children ?? []
55
+ return children[0] === node.id
56
+ })()
57
+
58
+ const handleUpdate = useCallback(
59
+ (updates: Partial<StairSegmentNode>) => {
60
+ if (!selectedId) return
61
+ updateNode(selectedId as AnyNode['id'], updates)
62
+ },
63
+ [selectedId, updateNode],
64
+ )
65
+
66
+ const handleMaterialChange = useCallback(
67
+ (material: MaterialSchema) => {
68
+ handleUpdate({ material })
69
+ },
70
+ [handleUpdate],
71
+ )
72
+
73
+ const handleClose = useCallback(() => {
74
+ setSelection({ selectedIds: [] })
75
+ }, [setSelection])
76
+
77
+ const handleBack = useCallback(() => {
78
+ if (node?.parentId) {
79
+ setSelection({ selectedIds: [node.parentId] })
80
+ }
81
+ }, [node?.parentId, setSelection])
82
+
83
+ const handleDuplicate = useCallback(() => {
84
+ if (!node?.parentId) return
85
+ sfxEmitter.emit('sfx:item-pick')
86
+
87
+ let duplicateInfo = structuredClone(node) as any
88
+ delete duplicateInfo.id
89
+ duplicateInfo.metadata = { ...duplicateInfo.metadata, isNew: true }
90
+ duplicateInfo.position = [
91
+ duplicateInfo.position[0] + 1,
92
+ duplicateInfo.position[1],
93
+ duplicateInfo.position[2] + 1,
94
+ ]
95
+
96
+ try {
97
+ const duplicate = StairSegmentNodeSchema.parse(duplicateInfo)
98
+ useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
99
+ setSelection({ selectedIds: [] })
100
+ setMovingNode(duplicate)
101
+ } catch (e) {
102
+ console.error('Failed to duplicate stair segment', e)
103
+ }
104
+ }, [node, setSelection, setMovingNode])
105
+
106
+ const handleMove = useCallback(() => {
107
+ if (node) {
108
+ sfxEmitter.emit('sfx:item-pick')
109
+ setMovingNode(node)
110
+ setSelection({ selectedIds: [] })
111
+ }
112
+ }, [node, setMovingNode, setSelection])
113
+
114
+ const handleDelete = useCallback(() => {
115
+ if (!(selectedId && node)) return
116
+ sfxEmitter.emit('sfx:item-delete')
117
+ const parentId = node.parentId
118
+ useScene.getState().deleteNode(selectedId as AnyNodeId)
119
+ if (parentId) {
120
+ useScene.getState().dirtyNodes.add(parentId as AnyNodeId)
121
+ setSelection({ selectedIds: [parentId] })
122
+ } else {
123
+ setSelection({ selectedIds: [] })
124
+ }
125
+ }, [selectedId, node, setSelection])
126
+
127
+ if (!node || node.type !== 'stair-segment' || selectedIds.length !== 1) return null
128
+
129
+ return (
130
+ <PanelWrapper
131
+ icon="/icons/stairs.png"
132
+ onBack={handleBack}
133
+ onClose={handleClose}
134
+ title={node.name || 'Stair Segment'}
135
+ width={300}
136
+ >
137
+ <PanelSection title="Type">
138
+ <SegmentedControl
139
+ onChange={(v) => {
140
+ const updates: Partial<StairSegmentNode> = { segmentType: v }
141
+ if (v === 'landing') {
142
+ updates.height = 0
143
+ updates.stepCount = 0
144
+ updates.length = 1.0
145
+ } else {
146
+ updates.height = 2.5
147
+ updates.stepCount = 10
148
+ updates.length = 3.0
149
+ }
150
+ handleUpdate(updates)
151
+ }}
152
+ options={SEGMENT_TYPE_OPTIONS}
153
+ value={node.segmentType}
154
+ />
155
+ </PanelSection>
156
+
157
+ {!isFirstSegment && (
158
+ <PanelSection title="Attachment">
159
+ <SegmentedControl
160
+ onChange={(v) => handleUpdate({ attachmentSide: v })}
161
+ options={ATTACHMENT_SIDE_OPTIONS}
162
+ value={node.attachmentSide}
163
+ />
164
+ </PanelSection>
165
+ )}
166
+
167
+ <PanelSection title="Dimensions">
168
+ <SliderControl
169
+ label="Width"
170
+ max={5}
171
+ min={0.5}
172
+ onChange={(v) => handleUpdate({ width: v })}
173
+ precision={2}
174
+ step={0.1}
175
+ unit="m"
176
+ value={Math.round(node.width * 100) / 100}
177
+ />
178
+ <SliderControl
179
+ label="Length"
180
+ max={10}
181
+ min={0.5}
182
+ onChange={(v) => handleUpdate({ length: v })}
183
+ precision={2}
184
+ step={0.1}
185
+ unit="m"
186
+ value={Math.round(node.length * 100) / 100}
187
+ />
188
+ {node.segmentType === 'stair' && (
189
+ <>
190
+ <SliderControl
191
+ label="Height"
192
+ max={10}
193
+ min={0.5}
194
+ onChange={(v) => handleUpdate({ height: v })}
195
+ precision={2}
196
+ step={0.1}
197
+ unit="m"
198
+ value={Math.round(node.height * 100) / 100}
199
+ />
200
+ <SliderControl
201
+ label="Steps"
202
+ max={30}
203
+ min={2}
204
+ onChange={(v) => handleUpdate({ stepCount: Math.round(v) })}
205
+ precision={0}
206
+ step={1}
207
+ unit=""
208
+ value={node.stepCount}
209
+ />
210
+ </>
211
+ )}
212
+ </PanelSection>
213
+
214
+ <PanelSection title="Structure">
215
+ <div className="flex items-center justify-between px-1 py-1">
216
+ <span className="text-muted-foreground text-xs">Fill to floor</span>
217
+ <button
218
+ className={`relative h-5 w-10 rounded-full transition-colors ${
219
+ node.fillToFloor ? 'bg-blue-500' : 'bg-[#3e3e3e]'
220
+ }`}
221
+ onClick={() => handleUpdate({ fillToFloor: !node.fillToFloor })}
222
+ type="button"
223
+ >
224
+ <div
225
+ className={`absolute top-1 h-3 w-3 rounded-full bg-white transition-transform ${
226
+ node.fillToFloor ? 'left-6' : 'left-1'
227
+ }`}
228
+ />
229
+ </button>
230
+ </div>
231
+ {!node.fillToFloor && (
232
+ <SliderControl
233
+ label="Thickness"
234
+ max={1}
235
+ min={0.05}
236
+ onChange={(v) => handleUpdate({ thickness: v })}
237
+ precision={2}
238
+ step={0.05}
239
+ unit="m"
240
+ value={Math.round((node.thickness ?? 0.25) * 100) / 100}
241
+ />
242
+ )}
243
+ </PanelSection>
244
+
245
+ <PanelSection title="Position">
246
+ <MetricControl
247
+ label="X"
248
+ max={50}
249
+ min={-50}
250
+ onChange={(v) => {
251
+ const pos = [...node.position] as [number, number, number]
252
+ pos[0] = v
253
+ handleUpdate({ position: pos })
254
+ }}
255
+ precision={2}
256
+ step={0.05}
257
+ unit="m"
258
+ value={Math.round(node.position[0] * 100) / 100}
259
+ />
260
+ <MetricControl
261
+ label="Y"
262
+ max={50}
263
+ min={-50}
264
+ onChange={(v) => {
265
+ const pos = [...node.position] as [number, number, number]
266
+ pos[1] = v
267
+ handleUpdate({ position: pos })
268
+ }}
269
+ precision={2}
270
+ step={0.05}
271
+ unit="m"
272
+ value={Math.round(node.position[1] * 100) / 100}
273
+ />
274
+ <MetricControl
275
+ label="Z"
276
+ max={50}
277
+ min={-50}
278
+ onChange={(v) => {
279
+ const pos = [...node.position] as [number, number, number]
280
+ pos[2] = v
281
+ handleUpdate({ position: pos })
282
+ }}
283
+ precision={2}
284
+ step={0.05}
285
+ unit="m"
286
+ value={Math.round(node.position[2] * 100) / 100}
287
+ />
288
+ <SliderControl
289
+ label="Rotation"
290
+ max={180}
291
+ min={-180}
292
+ onChange={(degrees) => {
293
+ handleUpdate({ rotation: (degrees * Math.PI) / 180 })
294
+ }}
295
+ precision={0}
296
+ step={1}
297
+ unit="°"
298
+ value={Math.round((node.rotation * 180) / Math.PI)}
299
+ />
300
+ <div className="flex gap-1.5 px-1 pt-2 pb-1">
301
+ <ActionButton
302
+ label="-45°"
303
+ onClick={() => {
304
+ sfxEmitter.emit('sfx:item-rotate')
305
+ handleUpdate({ rotation: node.rotation - Math.PI / 4 })
306
+ }}
307
+ />
308
+ <ActionButton
309
+ label="+45°"
310
+ onClick={() => {
311
+ sfxEmitter.emit('sfx:item-rotate')
312
+ handleUpdate({ rotation: node.rotation + Math.PI / 4 })
313
+ }}
314
+ />
315
+ </div>
316
+ </PanelSection>
317
+
318
+ <PanelSection title="Actions">
319
+ <ActionGroup>
320
+ <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
321
+ <ActionButton
322
+ icon={<Copy className="h-3.5 w-3.5" />}
323
+ label="Duplicate"
324
+ onClick={handleDuplicate}
325
+ />
326
+ <ActionButton
327
+ className="hover:bg-red-500/20"
328
+ icon={<Trash2 className="h-3.5 w-3.5 text-red-400" />}
329
+ label="Delete"
330
+ onClick={handleDelete}
331
+ />
332
+ </ActionGroup>
333
+ </PanelSection>
334
+ <PanelSection title="Material">
335
+ <MaterialPicker onChange={handleMaterialChange} value={node.material} />
336
+ </PanelSection>
337
+ </PanelWrapper>
338
+ )
339
+ }
@@ -0,0 +1,123 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ type MaterialSchema,
7
+ useScene,
8
+ type WallNode,
9
+ } from '@pascal-app/core'
10
+ import { useViewer } from '@pascal-app/viewer'
11
+ import { useCallback } from 'react'
12
+ import { MaterialPicker } from '../controls/material-picker'
13
+ import { PanelSection } from '../controls/panel-section'
14
+ import { SliderControl } from '../controls/slider-control'
15
+ import { PanelWrapper } from './panel-wrapper'
16
+
17
+ export function WallPanel() {
18
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
19
+ const setSelection = useViewer((s) => s.setSelection)
20
+ const nodes = useScene((s) => s.nodes)
21
+ const updateNode = useScene((s) => s.updateNode)
22
+
23
+ const selectedId = selectedIds[0]
24
+ const node = selectedId ? (nodes[selectedId as AnyNode['id']] as WallNode | undefined) : undefined
25
+
26
+ const handleUpdate = useCallback(
27
+ (updates: Partial<WallNode>) => {
28
+ if (!selectedId) return
29
+ updateNode(selectedId as AnyNode['id'], updates)
30
+ useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
31
+ },
32
+ [selectedId, updateNode],
33
+ )
34
+
35
+ const handleUpdateLength = useCallback(
36
+ (newLength: number) => {
37
+ if (!node || newLength <= 0) return
38
+
39
+ const dx = node.end[0] - node.start[0]
40
+ const dz = node.end[1] - node.start[1]
41
+ const currentLength = Math.sqrt(dx * dx + dz * dz)
42
+
43
+ if (currentLength === 0) return
44
+
45
+ const dirX = dx / currentLength
46
+ const dirZ = dz / currentLength
47
+
48
+ const newEnd: [number, number] = [
49
+ node.start[0] + dirX * newLength,
50
+ node.start[1] + dirZ * newLength,
51
+ ]
52
+
53
+ handleUpdate({ end: newEnd })
54
+ },
55
+ [node, handleUpdate],
56
+ )
57
+
58
+ const handleMaterialChange = useCallback(
59
+ (material: MaterialSchema) => {
60
+ handleUpdate({ material })
61
+ },
62
+ [handleUpdate],
63
+ )
64
+
65
+ const handleClose = useCallback(() => {
66
+ setSelection({ selectedIds: [] })
67
+ }, [setSelection])
68
+
69
+ if (!node || node.type !== 'wall' || selectedIds.length !== 1) return null
70
+
71
+ const dx = node.end[0] - node.start[0]
72
+ const dz = node.end[1] - node.start[1]
73
+ const length = Math.sqrt(dx * dx + dz * dz)
74
+
75
+ const height = node.height ?? 2.5
76
+ const thickness = node.thickness ?? 0.1
77
+
78
+ return (
79
+ <PanelWrapper
80
+ icon="/icons/wall.png"
81
+ onClose={handleClose}
82
+ title={node.name || 'Wall'}
83
+ width={280}
84
+ >
85
+ <PanelSection title="Dimensions">
86
+ <SliderControl
87
+ label="Length"
88
+ max={20}
89
+ min={0.1}
90
+ onChange={handleUpdateLength}
91
+ precision={2}
92
+ step={0.01}
93
+ unit="m"
94
+ value={length}
95
+ />
96
+ <SliderControl
97
+ label="Height"
98
+ max={6}
99
+ min={0.1}
100
+ onChange={(v) => handleUpdate({ height: Math.max(0.1, v) })}
101
+ precision={2}
102
+ step={0.1}
103
+ unit="m"
104
+ value={Math.round(height * 100) / 100}
105
+ />
106
+ <SliderControl
107
+ label="Thickness"
108
+ max={1}
109
+ min={0.05}
110
+ onChange={(v) => handleUpdate({ thickness: Math.max(0.05, v) })}
111
+ precision={3}
112
+ step={0.01}
113
+ unit="m"
114
+ value={Math.round(thickness * 1000) / 1000}
115
+ />
116
+ </PanelSection>
117
+
118
+ <PanelSection title="Material">
119
+ <MaterialPicker onChange={handleMaterialChange} value={node.material} />
120
+ </PanelSection>
121
+ </PanelWrapper>
122
+ )
123
+ }