@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,525 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNode,
5
- type AnyNodeId,
6
- type LevelNode,
7
- type StairNode,
8
- type StairRailingMode,
9
- type StairSlabOpeningMode,
10
- type StairTopLandingMode,
11
- type StairType,
12
- type StairSegmentNode,
13
- StairSegmentNode as StairSegmentNodeSchema,
14
- useScene,
15
- } from '@pascal-app/core'
16
- import { useViewer } from '@pascal-app/viewer'
17
- import { Copy, Move, Plus, Trash2 } from 'lucide-react'
18
- import { useCallback } from 'react'
19
- import { duplicateStairSubtree } from '../../../lib/stair-duplication'
20
- import { useShallow } from 'zustand/react/shallow'
21
- import { sfxEmitter } from '../../../lib/sfx-bus'
22
- import useEditor from '../../../store/use-editor'
23
- import { DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE } from '../../tools/stair/stair-defaults'
24
- import { ActionButton, ActionGroup } from '../controls/action-button'
25
- import { MetricControl } from '../controls/metric-control'
26
- import { PanelSection } from '../controls/panel-section'
27
- import { SegmentedControl } from '../controls/segmented-control'
28
- import { SliderControl } from '../controls/slider-control'
29
- import { ToggleControl } from '../controls/toggle-control'
30
- import { PanelWrapper } from './panel-wrapper'
31
-
32
- const RAILING_MODE_OPTIONS: { label: string; value: StairRailingMode }[] = [
33
- { label: 'None', value: 'none' },
34
- { label: 'Left', value: 'left' },
35
- { label: 'Right', value: 'right' },
36
- { label: 'Both', value: 'both' },
37
- ]
38
-
39
- const STAIR_TYPE_OPTIONS: { label: string; value: StairType }[] = [
40
- { label: 'Straight', value: 'straight' },
41
- { label: 'Curved', value: 'curved' },
42
- { label: 'Spiral', value: 'spiral' },
43
- ]
44
-
45
- const TOP_LANDING_MODE_OPTIONS: { label: string; value: StairTopLandingMode }[] = [
46
- { label: 'None', value: 'none' },
47
- { label: 'Integrated', value: 'integrated' },
48
- ]
49
-
50
- const STAIR_SLAB_OPENING_OPTIONS: { label: string; value: StairSlabOpeningMode }[] = [
51
- { label: 'None', value: 'none' },
52
- { label: 'Destination', value: 'destination' },
53
- ]
54
-
55
- export function StairPanel() {
56
- const selectedId = useViewer((s) => s.selection.selectedIds[0])
57
- const selectedCount = useViewer((s) => s.selection.selectedIds.length)
58
- const setSelection = useViewer((s) => s.setSelection)
59
- const updateNode = useScene((s) => s.updateNode)
60
- const createNode = useScene((s) => s.createNode)
61
- const setMovingNode = useEditor((s) => s.setMovingNode)
62
-
63
- const node = useScene((s) =>
64
- selectedId ? (s.nodes[selectedId as AnyNode['id']] as StairNode | undefined) : undefined,
65
- )
66
- const levels = useScene(
67
- useShallow((s) =>
68
- Object.values(s.nodes)
69
- .filter((entry): entry is LevelNode => entry.type === 'level')
70
- .sort((left, right) => left.level - right.level),
71
- ),
72
- )
73
- const segments = useScene(
74
- useShallow((s) => {
75
- if (!selectedId) return []
76
- const stairNode = s.nodes[selectedId as AnyNode['id']] as StairNode | undefined
77
- if (stairNode?.type !== 'stair') return []
78
- return (stairNode.children ?? [])
79
- .map((childId) => s.nodes[childId as AnyNodeId] as StairSegmentNode | undefined)
80
- .filter((entry): entry is StairSegmentNode => entry?.type === 'stair-segment')
81
- }),
82
- )
83
-
84
- const handleUpdate = useCallback(
85
- (updates: Partial<StairNode>) => {
86
- if (!selectedId) return
87
- updateNode(selectedId as AnyNode['id'], updates)
88
- },
89
- [selectedId, updateNode],
90
- )
91
-
92
- const handleClose = useCallback(() => {
93
- setSelection({ selectedIds: [] })
94
- }, [setSelection])
95
-
96
- const getLastSegmentFillDefaults = useCallback(() => {
97
- if (!node) return { fillToFloor: true }
98
- const children = node.children ?? []
99
- const lastChildId = children[children.length - 1]
100
- if (lastChildId) {
101
- const lastChild = useScene.getState().nodes[lastChildId as AnyNodeId] as
102
- | StairSegmentNode
103
- | undefined
104
- if (lastChild?.type === 'stair-segment') {
105
- return { fillToFloor: lastChild.fillToFloor }
106
- }
107
- }
108
- return { fillToFloor: true }
109
- }, [node])
110
-
111
- const handleAddFlight = useCallback(() => {
112
- if (!node) return
113
- const { fillToFloor } = getLastSegmentFillDefaults()
114
- const segment = StairSegmentNodeSchema.parse({
115
- segmentType: 'stair',
116
- width: 1.0,
117
- length: 3.0,
118
- height: 2.5,
119
- stepCount: 10,
120
- attachmentSide: 'front',
121
- fillToFloor,
122
- thickness: 0.25,
123
- position: [0, 0, 0],
124
- })
125
- createNode(segment, node.id as AnyNodeId)
126
- }, [node, createNode, getLastSegmentFillDefaults])
127
-
128
- const handleAddLanding = useCallback(() => {
129
- if (!node) return
130
- const { fillToFloor } = getLastSegmentFillDefaults()
131
- const segment = StairSegmentNodeSchema.parse({
132
- segmentType: 'landing',
133
- width: 1.0,
134
- length: 1.0,
135
- height: 0,
136
- stepCount: 0,
137
- attachmentSide: 'front',
138
- fillToFloor,
139
- thickness: 0.32,
140
- position: [0, 0, 0],
141
- })
142
- createNode(segment, node.id as AnyNodeId)
143
- }, [node, createNode, getLastSegmentFillDefaults])
144
-
145
- const handleSelectSegment = useCallback(
146
- (segmentId: string) => {
147
- setSelection({ selectedIds: [segmentId as AnyNode['id']] })
148
- },
149
- [setSelection],
150
- )
151
-
152
- const handleDuplicate = useCallback(() => {
153
- if (!node) return
154
- sfxEmitter.emit('sfx:item-pick')
155
-
156
- try {
157
- duplicateStairSubtree(node.id as AnyNodeId, { mode: 'move' })
158
- } catch (e) {
159
- console.error('Failed to duplicate stair', e)
160
- }
161
- }, [node])
162
-
163
- const handleMove = useCallback(() => {
164
- if (node) {
165
- sfxEmitter.emit('sfx:item-pick')
166
- setMovingNode(node)
167
- setSelection({ selectedIds: [] })
168
- }
169
- }, [node, setMovingNode, setSelection])
170
-
171
- const handleDelete = useCallback(() => {
172
- if (!(selectedId && node)) return
173
- sfxEmitter.emit('sfx:item-delete')
174
- const parentId = node.parentId
175
- useScene.getState().deleteNode(selectedId as AnyNodeId)
176
- if (parentId) {
177
- useScene.getState().dirtyNodes.add(parentId as AnyNodeId)
178
- }
179
- setSelection({ selectedIds: [] })
180
- }, [selectedId, node, setSelection])
181
-
182
- if (!(node && node.type === 'stair' && selectedId && selectedCount === 1)) return null
183
-
184
- const resolvedFromLevelId = node.fromLevelId ?? node.parentId ?? levels[0]?.id ?? null
185
- const resolvedToLevelId = node.toLevelId ?? resolvedFromLevelId
186
-
187
- return (
188
- <PanelWrapper
189
- icon="/icons/stairs.png"
190
- onClose={handleClose}
191
- title={node.name || 'Staircase'}
192
- width={300}
193
- >
194
- <PanelSection title="Type">
195
- <SegmentedControl
196
- onChange={(value) =>
197
- handleUpdate(
198
- value === 'spiral' && node.stairType !== 'spiral'
199
- ? {
200
- stairType: value,
201
- sweepAngle: DEFAULT_SPIRAL_STAIR_SWEEP_ANGLE,
202
- position: [node.position[0], 0, node.position[2]],
203
- }
204
- : { stairType: value },
205
- )
206
- }
207
- options={STAIR_TYPE_OPTIONS}
208
- value={node.stairType ?? 'straight'}
209
- />
210
- </PanelSection>
211
-
212
- <PanelSection title="Opening">
213
- <div className="space-y-3">
214
- <ToggleControl
215
- checked={(node.slabOpeningMode ?? 'none') === 'destination'}
216
- label="Auto Cutout"
217
- onChange={(checked) =>
218
- handleUpdate({
219
- slabOpeningMode: checked ? 'destination' : 'none',
220
- })
221
- }
222
- />
223
-
224
- <div className="space-y-1.5">
225
- <div className="px-1 text-[11px] uppercase tracking-[0.14em] text-muted-foreground">
226
- From Level
227
- </div>
228
- <select
229
- className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm text-foreground"
230
- onChange={(event) => handleUpdate({ fromLevelId: event.target.value })}
231
- value={resolvedFromLevelId ?? ''}
232
- >
233
- {levels.map((level) => (
234
- <option key={level.id} value={level.id}>
235
- {level.name || `Level ${level.level + 1}`}
236
- </option>
237
- ))}
238
- </select>
239
- </div>
240
-
241
- <div className="space-y-1.5">
242
- <div className="px-1 text-[11px] uppercase tracking-[0.14em] text-muted-foreground">
243
- To Level
244
- </div>
245
- <select
246
- className="h-9 w-full rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm text-foreground"
247
- onChange={(event) => handleUpdate({ toLevelId: event.target.value })}
248
- value={resolvedToLevelId ?? ''}
249
- >
250
- {levels.map((level) => (
251
- <option key={level.id} value={level.id}>
252
- {level.name || `Level ${level.level + 1}`}
253
- </option>
254
- ))}
255
- </select>
256
- </div>
257
-
258
- <SegmentedControl
259
- onChange={(value) => handleUpdate({ slabOpeningMode: value as StairSlabOpeningMode })}
260
- options={STAIR_SLAB_OPENING_OPTIONS}
261
- value={node.slabOpeningMode ?? 'none'}
262
- />
263
-
264
- {(node.slabOpeningMode ?? 'none') === 'destination' ? (
265
- <SliderControl
266
- label="Opening Offset"
267
- max={0.5}
268
- min={0}
269
- onChange={(value) => handleUpdate({ openingOffset: value })}
270
- precision={2}
271
- step={0.01}
272
- unit="m"
273
- value={Math.round((node.openingOffset ?? 0) * 100) / 100}
274
- />
275
- ) : null}
276
- </div>
277
- </PanelSection>
278
-
279
- {node.stairType === 'straight' && (
280
- <PanelSection title="Segments">
281
- <div className="flex flex-col gap-1">
282
- {segments.map((seg, i) => (
283
- <button
284
- 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]"
285
- key={seg.id}
286
- onClick={() => handleSelectSegment(seg.id)}
287
- type="button"
288
- >
289
- <span className="truncate">{seg.name || `Segment ${i + 1}`}</span>
290
- <span className="text-muted-foreground text-xs capitalize">{seg.segmentType}</span>
291
- </button>
292
- ))}
293
- </div>
294
- <div className="flex gap-1.5">
295
- <ActionButton
296
- icon={<Plus className="h-3.5 w-3.5" />}
297
- label="Add flight"
298
- onClick={handleAddFlight}
299
- />
300
- <ActionButton
301
- icon={<Plus className="h-3.5 w-3.5" />}
302
- label="Add landing"
303
- onClick={handleAddLanding}
304
- />
305
- </div>
306
- </PanelSection>
307
- )}
308
-
309
- {(node.stairType === 'curved' || node.stairType === 'spiral') && (
310
- <PanelSection title="Geometry">
311
- <SliderControl
312
- label="Width"
313
- max={10}
314
- min={0.4}
315
- onChange={(value) => handleUpdate({ width: value })}
316
- precision={2}
317
- step={0.05}
318
- unit="m"
319
- value={Math.round((node.width ?? 1) * 100) / 100}
320
- />
321
- <SliderControl
322
- label="Rise"
323
- max={10}
324
- min={0.2}
325
- onChange={(value) => handleUpdate({ totalRise: value })}
326
- precision={2}
327
- step={0.05}
328
- unit="m"
329
- value={Math.round((node.totalRise ?? 2.5) * 100) / 100}
330
- />
331
- <SliderControl
332
- label="Steps"
333
- max={32}
334
- min={2}
335
- onChange={(value) => handleUpdate({ stepCount: Math.max(2, Math.round(value)) })}
336
- precision={0}
337
- step={1}
338
- unit=""
339
- value={Math.max(2, Math.round(node.stepCount ?? 10))}
340
- />
341
- {node.stairType !== 'spiral' && (
342
- <ToggleControl
343
- checked={node.fillToFloor ?? true}
344
- label="Fit To Floor"
345
- onChange={(checked) => handleUpdate({ fillToFloor: checked })}
346
- />
347
- )}
348
- {(node.stairType === 'spiral' || !(node.fillToFloor ?? true)) && (
349
- <SliderControl
350
- label="Thickness"
351
- max={1}
352
- min={0.02}
353
- onChange={(value) => handleUpdate({ thickness: value })}
354
- precision={2}
355
- step={0.01}
356
- unit="m"
357
- value={Math.round((node.thickness ?? 0.25) * 100) / 100}
358
- />
359
- )}
360
- <SliderControl
361
- label="Inner Radius"
362
- max={10}
363
- min={node.stairType === 'spiral' ? 0.05 : 0.2}
364
- onChange={(value) => handleUpdate({ innerRadius: value })}
365
- precision={2}
366
- step={0.05}
367
- unit="m"
368
- value={Math.round((node.innerRadius ?? 0.9) * 100) / 100}
369
- />
370
- <SliderControl
371
- label="Sweep"
372
- max={node.stairType === 'spiral' ? 720 : 270}
373
- min={node.stairType === 'spiral' ? -720 : -270}
374
- onChange={(degrees) => handleUpdate({ sweepAngle: (degrees * Math.PI) / 180 })}
375
- precision={0}
376
- step={1}
377
- unit="°"
378
- value={Math.round(((node.sweepAngle ?? Math.PI / 2) * 180) / Math.PI)}
379
- />
380
- {node.stairType === 'spiral' && (
381
- <>
382
- <SegmentedControl
383
- onChange={(value) => handleUpdate({ topLandingMode: value })}
384
- options={TOP_LANDING_MODE_OPTIONS}
385
- value={node.topLandingMode ?? 'none'}
386
- />
387
- {(node.topLandingMode ?? 'none') === 'integrated' && (
388
- <SliderControl
389
- label="Top Landing"
390
- max={5}
391
- min={0.3}
392
- onChange={(value) => handleUpdate({ topLandingDepth: value })}
393
- precision={2}
394
- step={0.05}
395
- unit="m"
396
- value={Math.round((node.topLandingDepth ?? 0.9) * 100) / 100}
397
- />
398
- )}
399
- <ToggleControl
400
- checked={node.showCenterColumn ?? true}
401
- label="Center Column"
402
- onChange={(checked) => handleUpdate({ showCenterColumn: checked })}
403
- />
404
- <ToggleControl
405
- checked={node.showStepSupports ?? true}
406
- label="Step Supports"
407
- onChange={(checked) => handleUpdate({ showStepSupports: checked })}
408
- />
409
- </>
410
- )}
411
- </PanelSection>
412
- )}
413
-
414
- <PanelSection title="Position">
415
- <SliderControl
416
- label="X"
417
- max={50}
418
- min={-50}
419
- onChange={(v) => {
420
- const pos = [...node.position] as [number, number, number]
421
- pos[0] = v
422
- handleUpdate({ position: pos })
423
- }}
424
- precision={2}
425
- step={0.05}
426
- unit="m"
427
- value={Math.round(node.position[0] * 100) / 100}
428
- />
429
- <SliderControl
430
- label="Y"
431
- max={50}
432
- min={-50}
433
- onChange={(v) => {
434
- const pos = [...node.position] as [number, number, number]
435
- pos[1] = v
436
- handleUpdate({ position: pos })
437
- }}
438
- precision={2}
439
- step={0.05}
440
- unit="m"
441
- value={Math.round(node.position[1] * 100) / 100}
442
- />
443
- <SliderControl
444
- label="Z"
445
- max={50}
446
- min={-50}
447
- onChange={(v) => {
448
- const pos = [...node.position] as [number, number, number]
449
- pos[2] = v
450
- handleUpdate({ position: pos })
451
- }}
452
- precision={2}
453
- step={0.05}
454
- unit="m"
455
- value={Math.round(node.position[2] * 100) / 100}
456
- />
457
- <SliderControl
458
- label="Rotation"
459
- max={180}
460
- min={-180}
461
- onChange={(degrees) => {
462
- handleUpdate({ rotation: (degrees * Math.PI) / 180 })
463
- }}
464
- precision={0}
465
- step={1}
466
- unit="°"
467
- value={Math.round((node.rotation * 180) / Math.PI)}
468
- />
469
- <div className="flex gap-1.5 px-1 pt-2 pb-1">
470
- <ActionButton
471
- label="-45°"
472
- onClick={() => {
473
- sfxEmitter.emit('sfx:item-rotate')
474
- handleUpdate({ rotation: node.rotation - Math.PI / 4 })
475
- }}
476
- />
477
- <ActionButton
478
- label="+45°"
479
- onClick={() => {
480
- sfxEmitter.emit('sfx:item-rotate')
481
- handleUpdate({ rotation: node.rotation + Math.PI / 4 })
482
- }}
483
- />
484
- </div>
485
- </PanelSection>
486
-
487
- <PanelSection title="Railing">
488
- <SegmentedControl
489
- onChange={(value) => handleUpdate({ railingMode: value })}
490
- options={RAILING_MODE_OPTIONS}
491
- value={node.railingMode ?? 'none'}
492
- />
493
- {(node.railingMode ?? 'none') !== 'none' && (
494
- <SliderControl
495
- label="Height"
496
- max={1.4}
497
- min={0.7}
498
- onChange={(value) => handleUpdate({ railingHeight: value })}
499
- precision={2}
500
- step={0.02}
501
- unit="m"
502
- value={Math.round((node.railingHeight ?? 0.92) * 100) / 100}
503
- />
504
- )}
505
- </PanelSection>
506
-
507
- <PanelSection title="Actions">
508
- <ActionGroup>
509
- <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
510
- <ActionButton
511
- icon={<Copy className="h-3.5 w-3.5" />}
512
- label="Duplicate"
513
- onClick={handleDuplicate}
514
- />
515
- <ActionButton
516
- className="hover:bg-red-500/20"
517
- icon={<Trash2 className="h-3.5 w-3.5 text-red-400" />}
518
- label="Delete"
519
- onClick={handleDelete}
520
- />
521
- </ActionGroup>
522
- </PanelSection>
523
- </PanelWrapper>
524
- )
525
- }