@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,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,273 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNode,
5
- type AnyNodeId,
6
- getClampedWallCurveOffset,
7
- getEffectiveWallSurfaceMaterial,
8
- getMaxWallCurveOffset,
9
- getWallCurveLength,
10
- getWallSurfaceMaterialSignature,
11
- type MaterialSchema,
12
- normalizeWallCurveOffset,
13
- useScene,
14
- type WallNode,
15
- type WallSurfaceSide,
16
- } from '@pascal-app/core'
17
- import { useViewer } from '@pascal-app/viewer'
18
- import { Move, Spline } from 'lucide-react'
19
- import { useCallback, useMemo } from 'react'
20
- import { sfxEmitter } from '../../../lib/sfx-bus'
21
- import useEditor from '../../../store/use-editor'
22
- import { ActionButton, ActionGroup } from '../controls/action-button'
23
- import { MaterialPicker } from '../controls/material-picker'
24
- import { PanelSection } from '../controls/panel-section'
25
- import { SliderControl } from '../controls/slider-control'
26
- import { PanelWrapper } from './panel-wrapper'
27
-
28
- function buildWallSurfaceMaterialPatch(
29
- node: WallNode,
30
- targetSide: WallSurfaceSide | null,
31
- material: MaterialSchema | undefined,
32
- materialPreset: string | undefined,
33
- ): Partial<WallNode> {
34
- const nextSurfaceMaterial = { material, materialPreset }
35
- const nextInterior =
36
- targetSide === null || targetSide === 'interior'
37
- ? nextSurfaceMaterial
38
- : getEffectiveWallSurfaceMaterial(node, 'interior')
39
- const nextExterior =
40
- targetSide === null || targetSide === 'exterior'
41
- ? nextSurfaceMaterial
42
- : getEffectiveWallSurfaceMaterial(node, 'exterior')
43
-
44
- return {
45
- interiorMaterial: nextInterior.material,
46
- interiorMaterialPreset: nextInterior.materialPreset,
47
- exteriorMaterial: nextExterior.material,
48
- exteriorMaterialPreset: nextExterior.materialPreset,
49
- material: undefined,
50
- materialPreset: undefined,
51
- }
52
- }
53
-
54
- export function WallPanel() {
55
- const selectedId = useViewer((s) => s.selection.selectedIds[0])
56
- const setSelection = useViewer((s) => s.setSelection)
57
- const updateNode = useScene((s) => s.updateNode)
58
- const setMovingNode = useEditor((s) => s.setMovingNode)
59
- const setCurvingWall = useEditor((s) => s.setCurvingWall)
60
- const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
61
-
62
- const node = useScene((s) =>
63
- selectedId ? (s.nodes[selectedId as AnyNode['id']] as WallNode | undefined) : undefined,
64
- )
65
-
66
- // Boolean selector — re-renders only when this specific wall's child
67
- // composition crosses the "has a door/window/wall-item" threshold.
68
- const hasWallChildrenBlockingCurve = useScene((s) => {
69
- if (!node) return false
70
- return (node.children ?? []).some((childId) => {
71
- const child = s.nodes[childId as AnyNodeId]
72
- if (!child) return false
73
- if (child.type === 'door' || child.type === 'window') return true
74
- if (child.type === 'item') {
75
- const attachTo = child.asset?.attachTo
76
- return attachTo === 'wall' || attachTo === 'wall-side'
77
- }
78
- return false
79
- })
80
- })
81
-
82
- const handleUpdate = useCallback(
83
- (updates: Partial<WallNode>) => {
84
- if (!selectedId) return
85
- updateNode(selectedId as AnyNode['id'], updates)
86
- useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
87
- },
88
- [selectedId, updateNode],
89
- )
90
-
91
- const effectiveInteriorMaterial = useMemo(
92
- () => (node ? getEffectiveWallSurfaceMaterial(node, 'interior') : {}),
93
- [node],
94
- )
95
- const effectiveExteriorMaterial = useMemo(
96
- () => (node ? getEffectiveWallSurfaceMaterial(node, 'exterior') : {}),
97
- [node],
98
- )
99
- const surfaceMaterialsMatch = useMemo(
100
- () =>
101
- getWallSurfaceMaterialSignature(effectiveInteriorMaterial) ===
102
- getWallSurfaceMaterialSignature(effectiveExteriorMaterial),
103
- [effectiveExteriorMaterial, effectiveInteriorMaterial],
104
- )
105
- const materialTargetSide =
106
- selectedMaterialTarget &&
107
- selectedMaterialTarget.nodeId === node?.id &&
108
- (selectedMaterialTarget.role === 'interior' || selectedMaterialTarget.role === 'exterior')
109
- ? selectedMaterialTarget.role
110
- : null
111
- const materialPickerValue =
112
- materialTargetSide === 'interior'
113
- ? effectiveInteriorMaterial
114
- : materialTargetSide === 'exterior'
115
- ? effectiveExteriorMaterial
116
- : surfaceMaterialsMatch
117
- ? effectiveInteriorMaterial
118
- : {}
119
-
120
- const handleUpdateLength = useCallback(
121
- (newLength: number) => {
122
- if (!node || newLength <= 0) return
123
-
124
- const dx = node.end[0] - node.start[0]
125
- const dz = node.end[1] - node.start[1]
126
- const currentLength = Math.sqrt(dx * dx + dz * dz)
127
-
128
- if (currentLength === 0) return
129
-
130
- const dirX = dx / currentLength
131
- const dirZ = dz / currentLength
132
-
133
- const newEnd: [number, number] = [
134
- node.start[0] + dirX * newLength,
135
- node.start[1] + dirZ * newLength,
136
- ]
137
-
138
- handleUpdate({ end: newEnd })
139
- },
140
- [node, handleUpdate],
141
- )
142
-
143
- const handleMaterialPresetChange = useCallback(
144
- (materialPreset: string) => {
145
- if (!(node && materialTargetSide)) return
146
- handleUpdate(
147
- buildWallSurfaceMaterialPatch(node, materialTargetSide, undefined, materialPreset),
148
- )
149
- },
150
- [handleUpdate, materialTargetSide, node],
151
- )
152
-
153
- const handleCustomMaterialChange = useCallback(
154
- (material: MaterialSchema) => {
155
- if (!(node && materialTargetSide)) return
156
- handleUpdate(buildWallSurfaceMaterialPatch(node, materialTargetSide, material, undefined))
157
- },
158
- [handleUpdate, materialTargetSide, node],
159
- )
160
-
161
- const handleClose = useCallback(() => {
162
- setSelection({ selectedIds: [] })
163
- }, [setSelection])
164
-
165
- const handleMove = useCallback(() => {
166
- if (!node) return
167
- sfxEmitter.emit('sfx:item-pick')
168
- setMovingNode(node)
169
- setSelection({ selectedIds: [] })
170
- }, [node, setMovingNode, setSelection])
171
-
172
- const handleCurve = useCallback(() => {
173
- if (!node) return
174
- sfxEmitter.emit('sfx:item-pick')
175
- setCurvingWall(node)
176
- setSelection({ selectedIds: [] })
177
- }, [node, setCurvingWall, setSelection])
178
-
179
- if (!(node && node.type === 'wall' && selectedId)) return null
180
-
181
- const dx = node.end[0] - node.start[0]
182
- const dz = node.end[1] - node.start[1]
183
- const length = getWallCurveLength(node)
184
-
185
- const height = node.height ?? 2.5
186
- const thickness = node.thickness ?? 0.1
187
- const curveOffset = getClampedWallCurveOffset(node)
188
- const maxCurveOffset = getMaxWallCurveOffset(node)
189
-
190
- return (
191
- <PanelWrapper
192
- icon="/icons/wall.png"
193
- onClose={handleClose}
194
- title={node.name || 'Wall'}
195
- width={280}
196
- >
197
- <PanelSection title="Dimensions">
198
- <SliderControl
199
- label="Length"
200
- max={20}
201
- min={0.1}
202
- onChange={handleUpdateLength}
203
- precision={2}
204
- step={0.01}
205
- unit="m"
206
- value={length}
207
- />
208
- <SliderControl
209
- label="Height"
210
- max={6}
211
- min={0.1}
212
- onChange={(v) => handleUpdate({ height: Math.max(0.1, v) })}
213
- precision={2}
214
- step={0.1}
215
- unit="m"
216
- value={Math.round(height * 100) / 100}
217
- />
218
- <SliderControl
219
- label="Thickness"
220
- max={1}
221
- min={0.05}
222
- onChange={(v) => handleUpdate({ thickness: Math.max(0.05, v) })}
223
- precision={3}
224
- step={0.01}
225
- unit="m"
226
- value={Math.round(thickness * 1000) / 1000}
227
- />
228
- {!hasWallChildrenBlockingCurve && (
229
- <SliderControl
230
- label="Curve"
231
- max={Math.max(0.01, maxCurveOffset)}
232
- min={-Math.max(0.01, maxCurveOffset)}
233
- onChange={(v) => handleUpdate({ curveOffset: normalizeWallCurveOffset(node, v) })}
234
- precision={2}
235
- step={0.1}
236
- unit="m"
237
- value={Math.round(curveOffset * 100) / 100}
238
- />
239
- )}
240
- </PanelSection>
241
-
242
- <PanelSection title="Material">
243
- {materialTargetSide ? null : (
244
- <div className="mb-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-[11px] text-muted-foreground">
245
- Click the wall face you want to edit. Materials now apply to one side at a time.
246
- </div>
247
- )}
248
- <MaterialPicker
249
- disabled={!materialTargetSide}
250
- hideSideControl
251
- nodeType="wall"
252
- onChange={handleCustomMaterialChange}
253
- onSelectMaterialPreset={handleMaterialPresetChange}
254
- selectedMaterialPreset={materialPickerValue.materialPreset}
255
- value={materialPickerValue.material}
256
- />
257
- </PanelSection>
258
-
259
- <PanelSection title="Actions">
260
- <ActionGroup>
261
- <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
262
- {!hasWallChildrenBlockingCurve && (
263
- <ActionButton
264
- icon={<Spline className="h-3.5 w-3.5" />}
265
- label="Curve"
266
- onClick={handleCurve}
267
- />
268
- )}
269
- </ActionGroup>
270
- </PanelSection>
271
- </PanelWrapper>
272
- )
273
- }