@pascal-app/editor 0.8.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 (223) hide show
  1. package/package.json +8 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +754 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +58 -51
  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 +201 -83
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
  10. package/src/components/editor/first-person-controls.tsx +825 -20
  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 +5829 -12005
  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 +178 -149
  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 +433 -156
  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 +136 -62
  28. package/src/components/editor/thumbnail-generator.tsx +9 -3
  29. package/src/components/editor/use-floorplan-background-placement.ts +119 -32
  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-geometry-renderer.tsx +266 -0
  45. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  46. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  47. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  48. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  49. package/src/components/editor-2d/svg-paths.ts +10 -2
  50. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
  51. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  52. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  53. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  54. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  55. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  56. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  57. package/src/components/tools/fence/fence-drafting.ts +64 -22
  58. package/src/components/tools/item/move-tool.tsx +41 -105
  59. package/src/components/tools/item/placement-math.test.ts +10 -0
  60. package/src/components/tools/item/placement-math.ts +31 -5
  61. package/src/components/tools/item/placement-strategies.ts +190 -15
  62. package/src/components/tools/item/placement-types.ts +10 -1
  63. package/src/components/tools/item/use-draft-node.ts +20 -1
  64. package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
  65. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  66. package/src/components/tools/roof/roof-tool.tsx +108 -12
  67. package/src/components/tools/select/box-select-state.ts +77 -0
  68. package/src/components/tools/select/box-select-tool.tsx +324 -538
  69. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  70. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  71. package/src/components/tools/select/select-candidates.test.ts +109 -0
  72. package/src/components/tools/select/select-candidates.ts +85 -0
  73. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  74. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  75. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  76. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  77. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  78. package/src/components/tools/shared/placement-box.tsx +123 -0
  79. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  80. package/src/components/tools/shared/segment-angle.ts +56 -0
  81. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  82. package/src/components/tools/site/site-flag-model.ts +18 -0
  83. package/src/components/tools/stair/stair-defaults.ts +1 -1
  84. package/src/components/tools/stair/stair-tool.tsx +269 -35
  85. package/src/components/tools/tool-manager.tsx +172 -56
  86. package/src/components/tools/wall/wall-drafting.ts +98 -98
  87. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  88. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  89. package/src/components/tools/zone/zone-tool.tsx +16 -6
  90. package/src/components/ui/action-menu/action-button.tsx +21 -1
  91. package/src/components/ui/action-menu/control-modes.tsx +5 -82
  92. package/src/components/ui/action-menu/index.tsx +1 -113
  93. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  94. package/src/components/ui/action-menu/view-toggles.tsx +243 -6
  95. package/src/components/ui/command-palette/editor-commands.tsx +18 -13
  96. package/src/components/ui/command-palette/index.tsx +2 -1
  97. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  98. package/src/components/ui/controls/material-picker.tsx +11 -32
  99. package/src/components/ui/controls/metric-control.tsx +33 -11
  100. package/src/components/ui/floating-level-selector.tsx +47 -10
  101. package/src/components/ui/helpers/helper-manager.tsx +16 -17
  102. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  103. package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
  104. package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
  105. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  106. package/src/components/ui/panels/node-display.ts +1 -0
  107. package/src/components/ui/panels/panel-manager.tsx +35 -47
  108. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  109. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  110. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  111. package/src/components/ui/scene-loader.tsx +1 -3
  112. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  113. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  114. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  115. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  116. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  117. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  118. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  119. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  120. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  121. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  122. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  123. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
  125. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  126. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  127. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  128. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
  133. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  134. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  135. package/src/components/viewer-overlay.tsx +169 -52
  136. package/src/components/viewer-zone-system.tsx +1 -1
  137. package/src/hooks/use-auto-save.ts +10 -3
  138. package/src/hooks/use-ceiling-events.ts +175 -0
  139. package/src/hooks/use-drag-action.ts +124 -0
  140. package/src/hooks/use-keyboard.ts +115 -62
  141. package/src/hooks/use-selection.ts +64 -0
  142. package/src/index.tsx +237 -5
  143. package/src/lib/constants.ts +6 -2
  144. package/src/lib/editor-api.ts +62 -0
  145. package/src/lib/elevator-support.ts +96 -0
  146. package/src/lib/floorplan/apply-alignment.ts +111 -0
  147. package/src/lib/floorplan/index.ts +7 -0
  148. package/src/lib/floorplan/selection-tool.ts +40 -0
  149. package/src/lib/floorplan/stairs.ts +12 -1
  150. package/src/lib/fresh-planar-placement.test.ts +102 -0
  151. package/src/lib/fresh-planar-placement.ts +63 -0
  152. package/src/lib/history.ts +2 -1
  153. package/src/lib/level-duplication.ts +54 -42
  154. package/src/lib/level-name.ts +11 -0
  155. package/src/lib/material-paint.ts +211 -19
  156. package/src/lib/placement-metadata.ts +29 -0
  157. package/src/lib/planar-cursor-placement.test.ts +43 -0
  158. package/src/lib/planar-cursor-placement.ts +42 -0
  159. package/src/lib/roof-duplication.ts +1 -1
  160. package/src/lib/scene-clipboard.ts +267 -0
  161. package/src/lib/scene.ts +13 -4
  162. package/src/lib/sfx-bus.ts +23 -1
  163. package/src/lib/sfx-player.ts +70 -22
  164. package/src/lib/site-boundary.ts +1 -0
  165. package/src/lib/stair-levels.test.ts +146 -0
  166. package/src/lib/stair-levels.ts +177 -0
  167. package/src/lib/world-grid-snap.ts +199 -0
  168. package/src/lib/zone-content.ts +130 -0
  169. package/src/store/use-alignment-guides.ts +21 -0
  170. package/src/store/use-editor.tsx +320 -37
  171. package/src/store/use-placement-preview.ts +31 -0
  172. package/src/store/use-wall-move-ghosts.ts +36 -0
  173. package/src/store/use-wall-snap-indicator.ts +32 -0
  174. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  175. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  176. package/src/components/tools/building/move-building-tool.tsx +0 -157
  177. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  178. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  179. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  180. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  181. package/src/components/tools/column/column-tool.tsx +0 -97
  182. package/src/components/tools/column/move-column-tool.tsx +0 -105
  183. package/src/components/tools/door/door-math.ts +0 -110
  184. package/src/components/tools/door/door-tool.tsx +0 -324
  185. package/src/components/tools/door/move-door-tool.tsx +0 -412
  186. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  187. package/src/components/tools/fence/fence-tool.tsx +0 -346
  188. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  189. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  190. package/src/components/tools/item/item-tool.tsx +0 -26
  191. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  192. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  193. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  194. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  195. package/src/components/tools/slab/slab-tool.tsx +0 -322
  196. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  197. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  198. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  199. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  200. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  201. package/src/components/tools/wall/wall-tool.tsx +0 -332
  202. package/src/components/tools/window/move-window-tool.tsx +0 -447
  203. package/src/components/tools/window/window-math.ts +0 -117
  204. package/src/components/tools/window/window-tool.tsx +0 -332
  205. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  206. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  207. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  208. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  209. package/src/components/ui/panels/column-panel.tsx +0 -759
  210. package/src/components/ui/panels/door-panel.tsx +0 -1299
  211. package/src/components/ui/panels/fence-panel.tsx +0 -222
  212. package/src/components/ui/panels/item-panel.tsx +0 -306
  213. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  214. package/src/components/ui/panels/roof-panel.tsx +0 -280
  215. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  216. package/src/components/ui/panels/slab-panel.tsx +0 -250
  217. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  218. package/src/components/ui/panels/stair-panel.tsx +0 -577
  219. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  220. package/src/components/ui/panels/wall-panel.tsx +0 -273
  221. package/src/components/ui/panels/window-panel.tsx +0 -968
  222. package/src/contexts/presets-context.tsx +0 -121
  223. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -1,577 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNode,
5
- type AnyNodeId,
6
- getEffectiveStairSurfaceMaterial,
7
- type LevelNode,
8
- type MaterialSchema,
9
- type StairNode,
10
- StairNode as StairNodeSchema,
11
- type StairRailingMode,
12
- type StairSegmentNode,
13
- StairSegmentNode as StairSegmentNodeSchema,
14
- type StairSlabOpeningMode,
15
- type StairSurfaceMaterialRole,
16
- type StairTopLandingMode,
17
- type StairType,
18
- useScene,
19
- } from '@pascal-app/core'
20
- import { useViewer } from '@pascal-app/viewer'
21
- import { Copy, Move, Plus, Trash2 } from 'lucide-react'
22
- import { useCallback } from 'react'
23
- import { useShallow } from 'zustand/react/shallow'
24
- import { buildStairSurfaceMaterialPatch } from '../../../lib/material-paint'
25
- import { sfxEmitter } from '../../../lib/sfx-bus'
26
- import { duplicateStairSubtree } from '../../../lib/stair-duplication'
27
- import useEditor from '../../../store/use-editor'
28
- import { DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE } from '../../tools/stair/stair-defaults'
29
- import { ActionButton, ActionGroup } from '../controls/action-button'
30
- import { MaterialPicker } from '../controls/material-picker'
31
- import { MetricControl } from '../controls/metric-control'
32
- import { PanelSection } from '../controls/panel-section'
33
- import { SegmentedControl } from '../controls/segmented-control'
34
- import { SliderControl } from '../controls/slider-control'
35
- import { ToggleControl } from '../controls/toggle-control'
36
- import { PanelWrapper } from './panel-wrapper'
37
-
38
- const RAILING_MODE_OPTIONS: { label: string; value: StairRailingMode }[] = [
39
- { label: 'None', value: 'none' },
40
- { label: 'Left', value: 'left' },
41
- { label: 'Right', value: 'right' },
42
- { label: 'Both', value: 'both' },
43
- ]
44
-
45
- const STAIR_TYPE_OPTIONS: { label: string; value: StairType }[] = [
46
- { label: 'Straight', value: 'straight' },
47
- { label: 'Curved', value: 'curved' },
48
- { label: 'Spiral', value: 'spiral' },
49
- ]
50
-
51
- const TOP_LANDING_MODE_OPTIONS: { label: string; value: StairTopLandingMode }[] = [
52
- { label: 'None', value: 'none' },
53
- { label: 'Integrated', value: 'integrated' },
54
- ]
55
-
56
- const STAIR_SLAB_OPENING_OPTIONS: { label: string; value: StairSlabOpeningMode }[] = [
57
- { label: 'None', value: 'none' },
58
- { label: 'Destination', value: 'destination' },
59
- ]
60
-
61
- export function StairPanel() {
62
- const selectedId = useViewer((s) => s.selection.selectedIds[0])
63
- const selectedCount = useViewer((s) => s.selection.selectedIds.length)
64
- const setSelection = useViewer((s) => s.setSelection)
65
- const updateNode = useScene((s) => s.updateNode)
66
- const createNode = useScene((s) => s.createNode)
67
- const setMovingNode = useEditor((s) => s.setMovingNode)
68
- const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
69
-
70
- const node = useScene((s) =>
71
- selectedId ? (s.nodes[selectedId as AnyNode['id']] as StairNode | undefined) : undefined,
72
- )
73
- const levels = useScene(
74
- useShallow((s) =>
75
- Object.values(s.nodes)
76
- .filter((entry): entry is LevelNode => entry.type === 'level')
77
- .sort((left, right) => left.level - right.level),
78
- ),
79
- )
80
- const segments = useScene(
81
- useShallow((s) => {
82
- if (!selectedId) return []
83
- const stairNode = s.nodes[selectedId as AnyNode['id']] as StairNode | undefined
84
- if (stairNode?.type !== 'stair') return []
85
- return (stairNode.children ?? [])
86
- .map((childId) => s.nodes[childId as AnyNodeId] as StairSegmentNode | undefined)
87
- .filter((entry): entry is StairSegmentNode => entry?.type === 'stair-segment')
88
- }),
89
- )
90
-
91
- const handleUpdate = useCallback(
92
- (updates: Partial<StairNode>) => {
93
- if (!selectedId) return
94
- updateNode(selectedId as AnyNode['id'], updates)
95
- },
96
- [selectedId, updateNode],
97
- )
98
-
99
- const materialTargetRole =
100
- selectedMaterialTarget &&
101
- selectedMaterialTarget.nodeId === node?.id &&
102
- (selectedMaterialTarget.role === 'railing' ||
103
- selectedMaterialTarget.role === 'tread' ||
104
- selectedMaterialTarget.role === 'side')
105
- ? selectedMaterialTarget.role
106
- : null
107
- const materialPickerValue =
108
- node && materialTargetRole ? getEffectiveStairSurfaceMaterial(node, materialTargetRole) : {}
109
-
110
- const handleTargetedMaterialChange = useCallback(
111
- (material: MaterialSchema) => {
112
- if (!(node && materialTargetRole)) return
113
- handleUpdate(buildStairSurfaceMaterialPatch(node, materialTargetRole, material, undefined))
114
- },
115
- [handleUpdate, materialTargetRole, node],
116
- )
117
-
118
- const handleTargetedMaterialPresetChange = useCallback(
119
- (materialPreset: string) => {
120
- if (!(node && materialTargetRole)) return
121
- handleUpdate(
122
- buildStairSurfaceMaterialPatch(node, materialTargetRole, undefined, materialPreset),
123
- )
124
- },
125
- [handleUpdate, materialTargetRole, node],
126
- )
127
-
128
- const handleClose = useCallback(() => {
129
- setSelection({ selectedIds: [] })
130
- }, [setSelection])
131
-
132
- const getLastSegmentFillDefaults = useCallback(() => {
133
- if (!node) return { fillToFloor: true }
134
- const children = node.children ?? []
135
- const lastChildId = children[children.length - 1]
136
- if (lastChildId) {
137
- const lastChild = useScene.getState().nodes[lastChildId as AnyNodeId] as
138
- | StairSegmentNode
139
- | undefined
140
- if (lastChild?.type === 'stair-segment') {
141
- return { fillToFloor: lastChild.fillToFloor }
142
- }
143
- }
144
- return { fillToFloor: true }
145
- }, [node])
146
-
147
- const handleAddFlight = useCallback(() => {
148
- if (!node) return
149
- const { fillToFloor } = getLastSegmentFillDefaults()
150
- const segment = StairSegmentNodeSchema.parse({
151
- segmentType: 'stair',
152
- width: 1.0,
153
- length: 3.0,
154
- height: 2.5,
155
- stepCount: 10,
156
- attachmentSide: 'front',
157
- fillToFloor,
158
- thickness: 0.25,
159
- position: [0, 0, 0],
160
- })
161
- createNode(segment, node.id as AnyNodeId)
162
- }, [node, createNode, getLastSegmentFillDefaults])
163
-
164
- const handleAddLanding = useCallback(() => {
165
- if (!node) return
166
- const { fillToFloor } = getLastSegmentFillDefaults()
167
- const segment = StairSegmentNodeSchema.parse({
168
- segmentType: 'landing',
169
- width: 1.0,
170
- length: 1.0,
171
- height: 0,
172
- stepCount: 0,
173
- attachmentSide: 'front',
174
- fillToFloor,
175
- thickness: 0.32,
176
- position: [0, 0, 0],
177
- })
178
- createNode(segment, node.id as AnyNodeId)
179
- }, [node, createNode, getLastSegmentFillDefaults])
180
-
181
- const handleSelectSegment = useCallback(
182
- (segmentId: string) => {
183
- setSelection({ selectedIds: [segmentId as AnyNode['id']] })
184
- },
185
- [setSelection],
186
- )
187
-
188
- const handleDuplicate = useCallback(() => {
189
- if (!node) return
190
- sfxEmitter.emit('sfx:item-pick')
191
-
192
- try {
193
- duplicateStairSubtree(node.id as AnyNodeId, { mode: 'move' })
194
- } catch (e) {
195
- console.error('Failed to duplicate stair', e)
196
- }
197
- }, [node])
198
-
199
- const handleMove = useCallback(() => {
200
- if (node) {
201
- sfxEmitter.emit('sfx:item-pick')
202
- setMovingNode(node)
203
- setSelection({ selectedIds: [] })
204
- }
205
- }, [node, setMovingNode, setSelection])
206
-
207
- const handleDelete = useCallback(() => {
208
- if (!(selectedId && node)) return
209
- sfxEmitter.emit('sfx:item-delete')
210
- const parentId = node.parentId
211
- useScene.getState().deleteNode(selectedId as AnyNodeId)
212
- if (parentId) {
213
- useScene.getState().dirtyNodes.add(parentId as AnyNodeId)
214
- }
215
- setSelection({ selectedIds: [] })
216
- }, [selectedId, node, setSelection])
217
-
218
- if (!(node && node.type === 'stair' && selectedId && selectedCount === 1)) return null
219
-
220
- const resolvedFromLevelId = node.fromLevelId ?? node.parentId ?? levels[0]?.id ?? null
221
- const resolvedToLevelId = node.toLevelId ?? resolvedFromLevelId
222
-
223
- return (
224
- <PanelWrapper
225
- icon="/icons/stairs.png"
226
- onClose={handleClose}
227
- title={node.name || 'Staircase'}
228
- width={300}
229
- >
230
- <PanelSection title="Type">
231
- <SegmentedControl
232
- onChange={(value) =>
233
- handleUpdate(
234
- value === 'spiral' && node.stairType !== 'spiral'
235
- ? {
236
- stairType: value,
237
- sweepAngle: DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE,
238
- position: [node.position[0], 0, node.position[2]],
239
- }
240
- : { stairType: value },
241
- )
242
- }
243
- options={STAIR_TYPE_OPTIONS}
244
- value={node.stairType ?? 'straight'}
245
- />
246
- </PanelSection>
247
-
248
- <PanelSection title="Opening">
249
- <div className="space-y-3">
250
- <ToggleControl
251
- checked={(node.slabOpeningMode ?? 'none') === 'destination'}
252
- label="Auto Cutout"
253
- onChange={(checked) =>
254
- handleUpdate({
255
- slabOpeningMode: checked ? 'destination' : 'none',
256
- })
257
- }
258
- />
259
-
260
- <div className="space-y-1.5">
261
- <div className="px-1 text-[11px] text-muted-foreground uppercase tracking-[0.14em]">
262
- From Level
263
- </div>
264
- <select
265
- className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-foreground text-sm"
266
- onChange={(event) => handleUpdate({ fromLevelId: event.target.value })}
267
- value={resolvedFromLevelId ?? ''}
268
- >
269
- {levels.map((level) => (
270
- <option key={level.id} value={level.id}>
271
- {level.name || `Level ${level.level + 1}`}
272
- </option>
273
- ))}
274
- </select>
275
- </div>
276
-
277
- <div className="space-y-1.5">
278
- <div className="px-1 text-[11px] text-muted-foreground uppercase tracking-[0.14em]">
279
- To Level
280
- </div>
281
- <select
282
- className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-foreground text-sm"
283
- onChange={(event) => handleUpdate({ toLevelId: event.target.value })}
284
- value={resolvedToLevelId ?? ''}
285
- >
286
- {levels.map((level) => (
287
- <option key={level.id} value={level.id}>
288
- {level.name || `Level ${level.level + 1}`}
289
- </option>
290
- ))}
291
- </select>
292
- </div>
293
-
294
- <SegmentedControl
295
- onChange={(value) => handleUpdate({ slabOpeningMode: value as StairSlabOpeningMode })}
296
- options={STAIR_SLAB_OPENING_OPTIONS}
297
- value={node.slabOpeningMode ?? 'none'}
298
- />
299
-
300
- {(node.slabOpeningMode ?? 'none') === 'destination' ? (
301
- <MetricControl
302
- label="Opening Offset"
303
- max={0.5}
304
- min={0}
305
- onChange={(value) => handleUpdate({ openingOffset: value })}
306
- precision={2}
307
- step={0.01}
308
- unit="m"
309
- value={Math.round((node.openingOffset ?? 0) * 100) / 100}
310
- />
311
- ) : null}
312
- </div>
313
- </PanelSection>
314
-
315
- {node.stairType === 'straight' && (
316
- <PanelSection title="Segments">
317
- <div className="flex flex-col gap-1">
318
- {segments.map((seg, i) => (
319
- <button
320
- className="flex items-center justify-between rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-foreground text-sm transition-colors hover:bg-[#3e3e3e]"
321
- key={seg.id}
322
- onClick={() => handleSelectSegment(seg.id)}
323
- type="button"
324
- >
325
- <span className="truncate">{seg.name || `Segment ${i + 1}`}</span>
326
- <span className="text-muted-foreground text-xs capitalize">{seg.segmentType}</span>
327
- </button>
328
- ))}
329
- </div>
330
- <div className="flex gap-1.5">
331
- <ActionButton
332
- icon={<Plus className="h-3.5 w-3.5" />}
333
- label="Add flight"
334
- onClick={handleAddFlight}
335
- />
336
- <ActionButton
337
- icon={<Plus className="h-3.5 w-3.5" />}
338
- label="Add landing"
339
- onClick={handleAddLanding}
340
- />
341
- </div>
342
- </PanelSection>
343
- )}
344
-
345
- {(node.stairType === 'curved' || node.stairType === 'spiral') && (
346
- <PanelSection title="Geometry">
347
- <MetricControl
348
- label="Width"
349
- max={10}
350
- min={0.4}
351
- onChange={(value) => handleUpdate({ width: value })}
352
- precision={2}
353
- step={0.05}
354
- unit="m"
355
- value={Math.round((node.width ?? 1) * 100) / 100}
356
- />
357
- <MetricControl
358
- label="Rise"
359
- max={10}
360
- min={0.2}
361
- onChange={(value) => handleUpdate({ totalRise: value })}
362
- precision={2}
363
- step={0.05}
364
- unit="m"
365
- value={Math.round((node.totalRise ?? 2.5) * 100) / 100}
366
- />
367
- <MetricControl
368
- label="Steps"
369
- max={32}
370
- min={2}
371
- onChange={(value) => handleUpdate({ stepCount: Math.max(2, Math.round(value)) })}
372
- precision={0}
373
- step={1}
374
- unit=""
375
- value={Math.max(2, Math.round(node.stepCount ?? 10))}
376
- />
377
- {node.stairType !== 'spiral' && (
378
- <ToggleControl
379
- checked={node.fillToFloor ?? true}
380
- label="Fit To Floor"
381
- onChange={(checked) => handleUpdate({ fillToFloor: checked })}
382
- />
383
- )}
384
- {(node.stairType === 'spiral' || !(node.fillToFloor ?? true)) && (
385
- <MetricControl
386
- label="Thickness"
387
- max={1}
388
- min={0.02}
389
- onChange={(value) => handleUpdate({ thickness: value })}
390
- precision={2}
391
- step={0.01}
392
- unit="m"
393
- value={Math.round((node.thickness ?? 0.25) * 100) / 100}
394
- />
395
- )}
396
- <MetricControl
397
- label="Inner Radius"
398
- max={10}
399
- min={node.stairType === 'spiral' ? 0.05 : 0.2}
400
- onChange={(value) => handleUpdate({ innerRadius: value })}
401
- precision={2}
402
- step={0.05}
403
- unit="m"
404
- value={Math.round((node.innerRadius ?? 0.9) * 100) / 100}
405
- />
406
- <SliderControl
407
- label="Sweep"
408
- max={node.stairType === 'spiral' ? 720 : 270}
409
- min={node.stairType === 'spiral' ? -720 : -270}
410
- onChange={(degrees) => handleUpdate({ sweepAngle: (degrees * Math.PI) / 180 })}
411
- precision={0}
412
- step={1}
413
- unit="°"
414
- value={Math.round(((node.sweepAngle ?? Math.PI / 2) * 180) / Math.PI)}
415
- />
416
- {node.stairType === 'spiral' && (
417
- <>
418
- <SegmentedControl
419
- onChange={(value) => handleUpdate({ topLandingMode: value })}
420
- options={TOP_LANDING_MODE_OPTIONS}
421
- value={node.topLandingMode ?? 'none'}
422
- />
423
- {(node.topLandingMode ?? 'none') === 'integrated' && (
424
- <MetricControl
425
- label="Top Landing"
426
- max={5}
427
- min={0.3}
428
- onChange={(value) => handleUpdate({ topLandingDepth: value })}
429
- precision={2}
430
- step={0.05}
431
- unit="m"
432
- value={Math.round((node.topLandingDepth ?? 0.9) * 100) / 100}
433
- />
434
- )}
435
- <ToggleControl
436
- checked={node.showCenterColumn ?? true}
437
- label="Center Column"
438
- onChange={(checked) => handleUpdate({ showCenterColumn: checked })}
439
- />
440
- <ToggleControl
441
- checked={node.showStepSupports ?? true}
442
- label="Step Supports"
443
- onChange={(checked) => handleUpdate({ showStepSupports: checked })}
444
- />
445
- </>
446
- )}
447
- </PanelSection>
448
- )}
449
-
450
- <PanelSection title="Position">
451
- <SliderControl
452
- label="X"
453
- max={50}
454
- min={-50}
455
- onChange={(v) => {
456
- const pos = [...node.position] as [number, number, number]
457
- pos[0] = v
458
- handleUpdate({ position: pos })
459
- }}
460
- precision={2}
461
- step={0.05}
462
- unit="m"
463
- value={Math.round(node.position[0] * 100) / 100}
464
- />
465
- <SliderControl
466
- label="Y"
467
- max={50}
468
- min={-50}
469
- onChange={(v) => {
470
- const pos = [...node.position] as [number, number, number]
471
- pos[1] = v
472
- handleUpdate({ position: pos })
473
- }}
474
- precision={2}
475
- step={0.05}
476
- unit="m"
477
- value={Math.round(node.position[1] * 100) / 100}
478
- />
479
- <SliderControl
480
- label="Z"
481
- max={50}
482
- min={-50}
483
- onChange={(v) => {
484
- const pos = [...node.position] as [number, number, number]
485
- pos[2] = v
486
- handleUpdate({ position: pos })
487
- }}
488
- precision={2}
489
- step={0.05}
490
- unit="m"
491
- value={Math.round(node.position[2] * 100) / 100}
492
- />
493
- <SliderControl
494
- label="Rotation"
495
- max={180}
496
- min={-180}
497
- onChange={(degrees) => {
498
- handleUpdate({ rotation: (degrees * Math.PI) / 180 })
499
- }}
500
- precision={0}
501
- step={1}
502
- unit="°"
503
- value={Math.round((node.rotation * 180) / Math.PI)}
504
- />
505
- <div className="flex gap-1.5 px-1 pt-2 pb-1">
506
- <ActionButton
507
- label="-45°"
508
- onClick={() => {
509
- sfxEmitter.emit('sfx:item-rotate')
510
- handleUpdate({ rotation: node.rotation - Math.PI / 4 })
511
- }}
512
- />
513
- <ActionButton
514
- label="+45°"
515
- onClick={() => {
516
- sfxEmitter.emit('sfx:item-rotate')
517
- handleUpdate({ rotation: node.rotation + Math.PI / 4 })
518
- }}
519
- />
520
- </div>
521
- </PanelSection>
522
-
523
- <PanelSection title="Railing">
524
- <SegmentedControl
525
- onChange={(value) => handleUpdate({ railingMode: value })}
526
- options={RAILING_MODE_OPTIONS}
527
- value={node.railingMode ?? 'none'}
528
- />
529
- {(node.railingMode ?? 'none') !== 'none' && (
530
- <SliderControl
531
- label="Height"
532
- max={1.4}
533
- min={0.7}
534
- onChange={(value) => handleUpdate({ railingHeight: value })}
535
- precision={2}
536
- step={0.02}
537
- unit="m"
538
- value={Math.round((node.railingHeight ?? 0.92) * 100) / 100}
539
- />
540
- )}
541
- </PanelSection>
542
-
543
- <PanelSection title="Actions">
544
- <ActionGroup>
545
- <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
546
- <ActionButton
547
- icon={<Copy className="h-3.5 w-3.5" />}
548
- label="Duplicate"
549
- onClick={handleDuplicate}
550
- />
551
- <ActionButton
552
- className="hover:bg-red-500/20"
553
- icon={<Trash2 className="h-3.5 w-3.5 text-red-400" />}
554
- label="Delete"
555
- onClick={handleDelete}
556
- />
557
- </ActionGroup>
558
- </PanelSection>
559
- <PanelSection title="Material">
560
- {materialTargetRole ? null : (
561
- <div className="mb-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-[11px] text-muted-foreground">
562
- Click the stair surface you want to edit. Materials apply to one target at a time.
563
- </div>
564
- )}
565
- <MaterialPicker
566
- disabled={!materialTargetRole}
567
- hideSideControl
568
- nodeType="stair"
569
- onChange={handleTargetedMaterialChange}
570
- onSelectMaterialPreset={handleTargetedMaterialPresetChange}
571
- selectedMaterialPreset={materialPickerValue.materialPreset}
572
- value={materialPickerValue.material}
573
- />
574
- </PanelSection>
575
- </PanelWrapper>
576
- )
577
- }