@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,325 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNode,
5
- type AnyNodeId,
6
- type AttachmentSide,
7
- type StairSegmentNode,
8
- StairSegmentNode as StairSegmentNodeSchema,
9
- type StairSegmentType,
10
- useScene,
11
- } from '@pascal-app/core'
12
- import { useViewer } from '@pascal-app/viewer'
13
- import { Copy, Move, Trash2 } from 'lucide-react'
14
- import { useCallback } from 'react'
15
- import { sfxEmitter } from '../../../lib/sfx-bus'
16
- import useEditor from '../../../store/use-editor'
17
- import { ActionButton, ActionGroup } from '../controls/action-button'
18
- import { PanelSection } from '../controls/panel-section'
19
- import { SegmentedControl } from '../controls/segmented-control'
20
- import { SliderControl } from '../controls/slider-control'
21
- import { PanelWrapper } from './panel-wrapper'
22
-
23
- const SEGMENT_TYPE_OPTIONS: { label: string; value: StairSegmentType }[] = [
24
- { label: 'Flight', value: 'stair' },
25
- { label: 'Landing', value: 'landing' },
26
- ]
27
-
28
- const ATTACHMENT_SIDE_OPTIONS: { label: string; value: AttachmentSide }[] = [
29
- { label: 'Front', value: 'front' },
30
- { label: 'Left', value: 'left' },
31
- { label: 'Right', value: 'right' },
32
- ]
33
-
34
- export function StairSegmentPanel() {
35
- const selectedId = useViewer((s) => s.selection.selectedIds[0])
36
- const setSelection = useViewer((s) => s.setSelection)
37
- const updateNode = useScene((s) => s.updateNode)
38
- const setMovingNode = useEditor((s) => s.setMovingNode)
39
-
40
- const node = useScene((s) =>
41
- selectedId ? (s.nodes[selectedId as AnyNode['id']] as StairSegmentNode | undefined) : undefined,
42
- )
43
-
44
- // Boolean selector — re-renders only when this segment's position among the
45
- // parent stair's children flips to/from "first".
46
- const isFirstSegment = useScene((s) => {
47
- if (!node?.parentId) return true
48
- const parent = s.nodes[node.parentId as AnyNodeId]
49
- if (!parent || parent.type !== 'stair') return true
50
- const children = (parent as any).children ?? []
51
- return children[0] === node.id
52
- })
53
-
54
- const handleUpdate = useCallback(
55
- (updates: Partial<StairSegmentNode>) => {
56
- if (!selectedId) return
57
- updateNode(selectedId as AnyNode['id'], updates)
58
- },
59
- [selectedId, updateNode],
60
- )
61
-
62
- const handleClose = useCallback(() => {
63
- setSelection({ selectedIds: [] })
64
- }, [setSelection])
65
-
66
- const handleBack = useCallback(() => {
67
- if (node?.parentId) {
68
- setSelection({ selectedIds: [node.parentId] })
69
- }
70
- }, [node?.parentId, setSelection])
71
-
72
- const handleDuplicate = useCallback(() => {
73
- if (!node?.parentId) return
74
- sfxEmitter.emit('sfx:item-pick')
75
-
76
- let duplicateInfo = structuredClone(node) as any
77
- delete duplicateInfo.id
78
- duplicateInfo.metadata = { ...duplicateInfo.metadata, isNew: true }
79
- duplicateInfo.position = [
80
- duplicateInfo.position[0] + 1,
81
- duplicateInfo.position[1],
82
- duplicateInfo.position[2] + 1,
83
- ]
84
-
85
- try {
86
- const duplicate = StairSegmentNodeSchema.parse(duplicateInfo)
87
- useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
88
- setSelection({ selectedIds: [] })
89
- setMovingNode(duplicate)
90
- } catch (e) {
91
- console.error('Failed to duplicate stair segment', e)
92
- }
93
- }, [node, setSelection, setMovingNode])
94
-
95
- const handleMove = useCallback(() => {
96
- if (node) {
97
- sfxEmitter.emit('sfx:item-pick')
98
- setMovingNode(node)
99
- setSelection({ selectedIds: [] })
100
- }
101
- }, [node, setMovingNode, setSelection])
102
-
103
- const handleDelete = useCallback(() => {
104
- if (!(selectedId && node)) return
105
- sfxEmitter.emit('sfx:item-delete')
106
- const parentId = node.parentId
107
- useScene.getState().deleteNode(selectedId as AnyNodeId)
108
- if (parentId) {
109
- useScene.getState().dirtyNodes.add(parentId as AnyNodeId)
110
- setSelection({ selectedIds: [parentId] })
111
- } else {
112
- setSelection({ selectedIds: [] })
113
- }
114
- }, [selectedId, node, setSelection])
115
-
116
- if (!(node && node.type === 'stair-segment' && selectedId)) return null
117
-
118
- return (
119
- <PanelWrapper
120
- icon="/icons/stairs.png"
121
- onBack={handleBack}
122
- onClose={handleClose}
123
- title={node.name || 'Stair Segment'}
124
- width={300}
125
- >
126
- <PanelSection title="Type">
127
- <SegmentedControl
128
- onChange={(v) => {
129
- const updates: Partial<StairSegmentNode> = { segmentType: v }
130
- if (v === 'landing') {
131
- updates.height = 0
132
- updates.stepCount = 0
133
- updates.length = 1.0
134
- } else {
135
- updates.height = 2.5
136
- updates.stepCount = 10
137
- updates.length = 3.0
138
- }
139
- handleUpdate(updates)
140
- }}
141
- options={SEGMENT_TYPE_OPTIONS}
142
- value={node.segmentType}
143
- />
144
- </PanelSection>
145
-
146
- {!isFirstSegment && (
147
- <PanelSection title="Attachment">
148
- <SegmentedControl
149
- onChange={(v) => handleUpdate({ attachmentSide: v })}
150
- options={ATTACHMENT_SIDE_OPTIONS}
151
- value={node.attachmentSide}
152
- />
153
- </PanelSection>
154
- )}
155
-
156
- <PanelSection title="Dimensions">
157
- <SliderControl
158
- label="Width"
159
- max={5}
160
- min={0.5}
161
- onChange={(v) => handleUpdate({ width: v })}
162
- precision={2}
163
- step={0.1}
164
- unit="m"
165
- value={Math.round(node.width * 100) / 100}
166
- />
167
- <SliderControl
168
- label="Length"
169
- max={10}
170
- min={0.5}
171
- onChange={(v) => handleUpdate({ length: v })}
172
- precision={2}
173
- step={0.1}
174
- unit="m"
175
- value={Math.round(node.length * 100) / 100}
176
- />
177
- {node.segmentType === 'stair' && (
178
- <>
179
- <SliderControl
180
- label="Height"
181
- max={10}
182
- min={0.5}
183
- onChange={(v) => handleUpdate({ height: v })}
184
- precision={2}
185
- step={0.1}
186
- unit="m"
187
- value={Math.round(node.height * 100) / 100}
188
- />
189
- <SliderControl
190
- label="Steps"
191
- max={30}
192
- min={2}
193
- onChange={(v) => handleUpdate({ stepCount: Math.round(v) })}
194
- precision={0}
195
- step={1}
196
- unit=""
197
- value={node.stepCount}
198
- />
199
- </>
200
- )}
201
- </PanelSection>
202
-
203
- <PanelSection title="Structure">
204
- <div className="flex items-center justify-between px-1 py-1">
205
- <span className="text-muted-foreground text-xs">Fill to floor</span>
206
- <button
207
- className={`relative h-5 w-10 rounded-full transition-colors ${
208
- node.fillToFloor ? 'bg-blue-500' : 'bg-[#3e3e3e]'
209
- }`}
210
- onClick={() => handleUpdate({ fillToFloor: !node.fillToFloor })}
211
- type="button"
212
- >
213
- <div
214
- className={`absolute top-1 h-3 w-3 rounded-full bg-white transition-transform ${
215
- node.fillToFloor ? 'left-6' : 'left-1'
216
- }`}
217
- />
218
- </button>
219
- </div>
220
- {!node.fillToFloor && (
221
- <SliderControl
222
- label="Thickness"
223
- max={1}
224
- min={0.05}
225
- onChange={(v) => handleUpdate({ thickness: v })}
226
- precision={2}
227
- step={0.05}
228
- unit="m"
229
- value={Math.round((node.thickness ?? 0.25) * 100) / 100}
230
- />
231
- )}
232
- </PanelSection>
233
-
234
- <PanelSection title="Position">
235
- <SliderControl
236
- label="X"
237
- max={50}
238
- min={-50}
239
- onChange={(v) => {
240
- const pos = [...node.position] as [number, number, number]
241
- pos[0] = v
242
- handleUpdate({ position: pos })
243
- }}
244
- precision={2}
245
- step={0.05}
246
- unit="m"
247
- value={Math.round(node.position[0] * 100) / 100}
248
- />
249
- <SliderControl
250
- label="Y"
251
- max={50}
252
- min={-50}
253
- onChange={(v) => {
254
- const pos = [...node.position] as [number, number, number]
255
- pos[1] = v
256
- handleUpdate({ position: pos })
257
- }}
258
- precision={2}
259
- step={0.05}
260
- unit="m"
261
- value={Math.round(node.position[1] * 100) / 100}
262
- />
263
- <SliderControl
264
- label="Z"
265
- max={50}
266
- min={-50}
267
- onChange={(v) => {
268
- const pos = [...node.position] as [number, number, number]
269
- pos[2] = v
270
- handleUpdate({ position: pos })
271
- }}
272
- precision={2}
273
- step={0.05}
274
- unit="m"
275
- value={Math.round(node.position[2] * 100) / 100}
276
- />
277
- <SliderControl
278
- label="Rotation"
279
- max={180}
280
- min={-180}
281
- onChange={(degrees) => {
282
- handleUpdate({ rotation: (degrees * Math.PI) / 180 })
283
- }}
284
- precision={0}
285
- step={1}
286
- unit="°"
287
- value={Math.round((node.rotation * 180) / Math.PI)}
288
- />
289
- <div className="flex gap-1.5 px-1 pt-2 pb-1">
290
- <ActionButton
291
- label="-45°"
292
- onClick={() => {
293
- sfxEmitter.emit('sfx:item-rotate')
294
- handleUpdate({ rotation: node.rotation - Math.PI / 4 })
295
- }}
296
- />
297
- <ActionButton
298
- label="+45°"
299
- onClick={() => {
300
- sfxEmitter.emit('sfx:item-rotate')
301
- handleUpdate({ rotation: node.rotation + Math.PI / 4 })
302
- }}
303
- />
304
- </div>
305
- </PanelSection>
306
-
307
- <PanelSection title="Actions">
308
- <ActionGroup>
309
- <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
310
- <ActionButton
311
- icon={<Copy className="h-3.5 w-3.5" />}
312
- label="Duplicate"
313
- onClick={handleDuplicate}
314
- />
315
- <ActionButton
316
- className="hover:bg-red-500/20"
317
- icon={<Trash2 className="h-3.5 w-3.5 text-red-400" />}
318
- label="Delete"
319
- onClick={handleDelete}
320
- />
321
- </ActionGroup>
322
- </PanelSection>
323
- </PanelWrapper>
324
- )
325
- }
@@ -1,177 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNode,
5
- type AnyNodeId,
6
- getClampedWallCurveOffset,
7
- getMaxWallCurveOffset,
8
- getWallCurveLength,
9
- normalizeWallCurveOffset,
10
- useScene,
11
- type WallNode,
12
- } from '@pascal-app/core'
13
- import { useViewer } from '@pascal-app/viewer'
14
- import { Move, Spline } 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 { PanelSection } from '../controls/panel-section'
20
- import { SliderControl } from '../controls/slider-control'
21
- import { PanelWrapper } from './panel-wrapper'
22
-
23
- export function WallPanel() {
24
- const selectedId = useViewer((s) => s.selection.selectedIds[0])
25
- const setSelection = useViewer((s) => s.setSelection)
26
- const updateNode = useScene((s) => s.updateNode)
27
- const setMovingNode = useEditor((s) => s.setMovingNode)
28
- const setCurvingWall = useEditor((s) => s.setCurvingWall)
29
-
30
- const node = useScene((s) =>
31
- selectedId ? (s.nodes[selectedId as AnyNode['id']] as WallNode | undefined) : undefined,
32
- )
33
-
34
- // Boolean selector — re-renders only when this specific wall's child
35
- // composition crosses the "has a door/window/wall-item" threshold.
36
- const hasWallChildrenBlockingCurve = useScene((s) => {
37
- if (!node) return false
38
- return (node.children ?? []).some((childId) => {
39
- const child = s.nodes[childId as AnyNodeId]
40
- if (!child) return false
41
- if (child.type === 'door' || child.type === 'window') return true
42
- if (child.type === 'item') {
43
- const attachTo = child.asset?.attachTo
44
- return attachTo === 'wall' || attachTo === 'wall-side'
45
- }
46
- return false
47
- })
48
- })
49
-
50
- const handleUpdate = useCallback(
51
- (updates: Partial<WallNode>) => {
52
- if (!selectedId) return
53
- updateNode(selectedId as AnyNode['id'], updates)
54
- useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
55
- },
56
- [selectedId, updateNode],
57
- )
58
-
59
- const handleUpdateLength = useCallback(
60
- (newLength: number) => {
61
- if (!node || newLength <= 0) return
62
-
63
- const dx = node.end[0] - node.start[0]
64
- const dz = node.end[1] - node.start[1]
65
- const currentLength = Math.sqrt(dx * dx + dz * dz)
66
-
67
- if (currentLength === 0) return
68
-
69
- const dirX = dx / currentLength
70
- const dirZ = dz / currentLength
71
-
72
- const newEnd: [number, number] = [
73
- node.start[0] + dirX * newLength,
74
- node.start[1] + dirZ * newLength,
75
- ]
76
-
77
- handleUpdate({ end: newEnd })
78
- },
79
- [node, handleUpdate],
80
- )
81
-
82
- const handleClose = useCallback(() => {
83
- setSelection({ selectedIds: [] })
84
- }, [setSelection])
85
-
86
- const handleMove = useCallback(() => {
87
- if (!node) return
88
- sfxEmitter.emit('sfx:item-pick')
89
- setMovingNode(node)
90
- setSelection({ selectedIds: [] })
91
- }, [node, setMovingNode, setSelection])
92
-
93
- const handleCurve = useCallback(() => {
94
- if (!node) return
95
- sfxEmitter.emit('sfx:item-pick')
96
- setCurvingWall(node)
97
- setSelection({ selectedIds: [] })
98
- }, [node, setCurvingWall, setSelection])
99
-
100
- if (!(node && node.type === 'wall' && selectedId)) return null
101
-
102
- const dx = node.end[0] - node.start[0]
103
- const dz = node.end[1] - node.start[1]
104
- const length = getWallCurveLength(node)
105
-
106
- const height = node.height ?? 2.5
107
- const thickness = node.thickness ?? 0.1
108
- const curveOffset = getClampedWallCurveOffset(node)
109
- const maxCurveOffset = getMaxWallCurveOffset(node)
110
-
111
- return (
112
- <PanelWrapper
113
- icon="/icons/wall.png"
114
- onClose={handleClose}
115
- title={node.name || 'Wall'}
116
- width={280}
117
- >
118
- <PanelSection title="Dimensions">
119
- <SliderControl
120
- label="Length"
121
- max={20}
122
- min={0.1}
123
- onChange={handleUpdateLength}
124
- precision={2}
125
- step={0.01}
126
- unit="m"
127
- value={length}
128
- />
129
- <SliderControl
130
- label="Height"
131
- max={6}
132
- min={0.1}
133
- onChange={(v) => handleUpdate({ height: Math.max(0.1, v) })}
134
- precision={2}
135
- step={0.1}
136
- unit="m"
137
- value={Math.round(height * 100) / 100}
138
- />
139
- <SliderControl
140
- label="Thickness"
141
- max={1}
142
- min={0.05}
143
- onChange={(v) => handleUpdate({ thickness: Math.max(0.05, v) })}
144
- precision={3}
145
- step={0.01}
146
- unit="m"
147
- value={Math.round(thickness * 1000) / 1000}
148
- />
149
- {!hasWallChildrenBlockingCurve && (
150
- <SliderControl
151
- label="Curve"
152
- max={Math.max(0.01, maxCurveOffset)}
153
- min={-Math.max(0.01, maxCurveOffset)}
154
- onChange={(v) => handleUpdate({ curveOffset: normalizeWallCurveOffset(node, v) })}
155
- precision={2}
156
- step={0.1}
157
- unit="m"
158
- value={Math.round(curveOffset * 100) / 100}
159
- />
160
- )}
161
- </PanelSection>
162
-
163
- <PanelSection title="Actions">
164
- <ActionGroup>
165
- <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
166
- {!hasWallChildrenBlockingCurve && (
167
- <ActionButton
168
- icon={<Spline className="h-3.5 w-3.5" />}
169
- label="Curve"
170
- onClick={handleCurve}
171
- />
172
- )}
173
- </ActionGroup>
174
- </PanelSection>
175
- </PanelWrapper>
176
- )
177
- }