@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,280 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNode,
5
- type AnyNodeId,
6
- getEffectiveRoofSurfaceMaterial,
7
- type MaterialSchema,
8
- type RoofNode,
9
- RoofNode as RoofNodeSchema,
10
- type RoofSegmentNode,
11
- RoofSegmentNode as RoofSegmentNodeSchema,
12
- type RoofSurfaceMaterialRole,
13
- useScene,
14
- } from '@pascal-app/core'
15
- import { useViewer } from '@pascal-app/viewer'
16
- import { Copy, Move, Plus, Trash2 } from 'lucide-react'
17
- import { useCallback } from 'react'
18
- import { useShallow } from 'zustand/react/shallow'
19
- import { buildRoofSurfaceMaterialPatch } from '../../../lib/material-paint'
20
- import { duplicateRoofSubtree } from '../../../lib/roof-duplication'
21
- import { sfxEmitter } from '../../../lib/sfx-bus'
22
- import useEditor from '../../../store/use-editor'
23
- import { ActionButton, ActionGroup } from '../controls/action-button'
24
- import { MaterialPicker } from '../controls/material-picker'
25
- import { PanelSection } from '../controls/panel-section'
26
- import { SliderControl } from '../controls/slider-control'
27
- import { PanelWrapper } from './panel-wrapper'
28
-
29
- export function RoofPanel() {
30
- const selectedId = useViewer((s) => s.selection.selectedIds[0])
31
- const setSelection = useViewer((s) => s.setSelection)
32
- const updateNode = useScene((s) => s.updateNode)
33
- const createNode = useScene((s) => s.createNode)
34
- const setMovingNode = useEditor((s) => s.setMovingNode)
35
- const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
36
-
37
- const node = useScene((s) =>
38
- selectedId ? (s.nodes[selectedId as AnyNode['id']] as RoofNode | undefined) : undefined,
39
- )
40
- // Shallow selector — only re-renders when the segment list content changes.
41
- const segments = useScene(
42
- useShallow((s) => {
43
- if (!node) return []
44
- return (node.children ?? [])
45
- .map((childId) => s.nodes[childId as AnyNodeId] as RoofSegmentNode | undefined)
46
- .filter((n): n is RoofSegmentNode => n?.type === 'roof-segment')
47
- }),
48
- )
49
-
50
- const handleUpdate = useCallback(
51
- (updates: Partial<RoofNode>) => {
52
- if (!selectedId) return
53
- updateNode(selectedId as AnyNode['id'], updates)
54
- },
55
- [selectedId, updateNode],
56
- )
57
-
58
- const materialTargetRole =
59
- selectedMaterialTarget &&
60
- selectedMaterialTarget.nodeId === node?.id &&
61
- (selectedMaterialTarget.role === 'top' ||
62
- selectedMaterialTarget.role === 'edge' ||
63
- selectedMaterialTarget.role === 'wall')
64
- ? selectedMaterialTarget.role
65
- : null
66
- const materialPickerValue =
67
- node && materialTargetRole ? getEffectiveRoofSurfaceMaterial(node, materialTargetRole) : {}
68
-
69
- const handleTargetedMaterialChange = useCallback(
70
- (material: MaterialSchema) => {
71
- if (!(node && materialTargetRole)) return
72
- handleUpdate(buildRoofSurfaceMaterialPatch(node, materialTargetRole, material, undefined))
73
- },
74
- [handleUpdate, materialTargetRole, node],
75
- )
76
-
77
- const handleTargetedMaterialPresetChange = useCallback(
78
- (materialPreset: string) => {
79
- if (!(node && materialTargetRole)) return
80
- handleUpdate(
81
- buildRoofSurfaceMaterialPatch(node, materialTargetRole, undefined, materialPreset),
82
- )
83
- },
84
- [handleUpdate, materialTargetRole, node],
85
- )
86
-
87
- const handleClose = useCallback(() => {
88
- setSelection({ selectedIds: [] })
89
- }, [setSelection])
90
-
91
- const handleAddSegment = useCallback(() => {
92
- if (!node) return
93
- const segment = RoofSegmentNodeSchema.parse({
94
- width: 6,
95
- depth: 6,
96
- wallHeight: 0.5,
97
- roofHeight: 2.5,
98
- roofType: 'gable',
99
- position: [2, 0, 2],
100
- })
101
- createNode(segment, node.id as AnyNodeId)
102
- }, [node, createNode])
103
-
104
- const handleSelectSegment = useCallback(
105
- (segmentId: string) => {
106
- setSelection({ selectedIds: [segmentId as AnyNode['id']] })
107
- },
108
- [setSelection],
109
- )
110
-
111
- const handleDuplicate = useCallback(() => {
112
- if (!node) return
113
- sfxEmitter.emit('sfx:item-pick')
114
-
115
- try {
116
- duplicateRoofSubtree(node.id as AnyNodeId, { mode: 'move' })
117
- } catch (e) {
118
- console.error('Failed to duplicate roof', e)
119
- }
120
- }, [node])
121
-
122
- const handleMove = useCallback(() => {
123
- if (node) {
124
- sfxEmitter.emit('sfx:item-pick')
125
- setMovingNode(node)
126
- setSelection({ selectedIds: [] })
127
- }
128
- }, [node, setMovingNode, setSelection])
129
-
130
- const handleDelete = useCallback(() => {
131
- if (!(selectedId && node)) return
132
- sfxEmitter.emit('sfx:item-delete')
133
- const parentId = node.parentId
134
- useScene.getState().deleteNode(selectedId as AnyNodeId)
135
- if (parentId) {
136
- useScene.getState().dirtyNodes.add(parentId as AnyNodeId)
137
- }
138
- setSelection({ selectedIds: [] })
139
- }, [selectedId, node, setSelection])
140
-
141
- if (!(node && node.type === 'roof' && selectedId)) return null
142
-
143
- return (
144
- <PanelWrapper
145
- icon="/icons/roof.png"
146
- onClose={handleClose}
147
- title={node.name || 'Roof'}
148
- width={300}
149
- >
150
- <PanelSection title="Segments">
151
- <div className="flex flex-col gap-1">
152
- {segments.map((seg, i) => (
153
- <button
154
- 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]"
155
- key={seg.id}
156
- onClick={() => handleSelectSegment(seg.id)}
157
- type="button"
158
- >
159
- <span className="truncate">{seg.name || `Segment ${i + 1}`}</span>
160
- <span className="text-muted-foreground text-xs capitalize">{seg.roofType}</span>
161
- </button>
162
- ))}
163
- </div>
164
- <ActionGroup>
165
- <ActionButton
166
- icon={<Plus className="h-3.5 w-3.5" />}
167
- label="Add Segment"
168
- onClick={handleAddSegment}
169
- />
170
- </ActionGroup>
171
- </PanelSection>
172
-
173
- <PanelSection title="Position">
174
- <SliderControl
175
- label="X"
176
- max={50}
177
- min={-50}
178
- onChange={(v) => {
179
- const pos = [...node.position] as [number, number, number]
180
- pos[0] = v
181
- handleUpdate({ position: pos })
182
- }}
183
- precision={2}
184
- step={0.05}
185
- unit="m"
186
- value={Math.round(node.position[0] * 100) / 100}
187
- />
188
- <SliderControl
189
- label="Y"
190
- max={50}
191
- min={-50}
192
- onChange={(v) => {
193
- const pos = [...node.position] as [number, number, number]
194
- pos[1] = v
195
- handleUpdate({ position: pos })
196
- }}
197
- precision={2}
198
- step={0.05}
199
- unit="m"
200
- value={Math.round(node.position[1] * 100) / 100}
201
- />
202
- <SliderControl
203
- label="Z"
204
- max={50}
205
- min={-50}
206
- onChange={(v) => {
207
- const pos = [...node.position] as [number, number, number]
208
- pos[2] = v
209
- handleUpdate({ position: pos })
210
- }}
211
- precision={2}
212
- step={0.05}
213
- unit="m"
214
- value={Math.round(node.position[2] * 100) / 100}
215
- />
216
- <SliderControl
217
- label="Rotation"
218
- max={180}
219
- min={-180}
220
- onChange={(degrees) => {
221
- handleUpdate({ rotation: (degrees * Math.PI) / 180 })
222
- }}
223
- precision={0}
224
- step={1}
225
- unit="°"
226
- value={Math.round((node.rotation * 180) / Math.PI)}
227
- />
228
- <div className="flex gap-1.5 px-1 pt-2 pb-1">
229
- <ActionButton
230
- label="-45°"
231
- onClick={() => {
232
- sfxEmitter.emit('sfx:item-rotate')
233
- handleUpdate({ rotation: node.rotation - Math.PI / 4 })
234
- }}
235
- />
236
- <ActionButton
237
- label="+45°"
238
- onClick={() => {
239
- sfxEmitter.emit('sfx:item-rotate')
240
- handleUpdate({ rotation: node.rotation + Math.PI / 4 })
241
- }}
242
- />
243
- </div>
244
- </PanelSection>
245
-
246
- <PanelSection title="Actions">
247
- <ActionGroup>
248
- <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
249
- <ActionButton
250
- icon={<Copy className="h-3.5 w-3.5" />}
251
- label="Duplicate"
252
- onClick={handleDuplicate}
253
- />
254
- <ActionButton
255
- className="hover:bg-red-500/20"
256
- icon={<Trash2 className="h-3.5 w-3.5 text-red-400" />}
257
- label="Delete"
258
- onClick={handleDelete}
259
- />
260
- </ActionGroup>
261
- </PanelSection>
262
- <PanelSection title="Material">
263
- {materialTargetRole ? null : (
264
- <div className="mb-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-[11px] text-muted-foreground">
265
- Click the roof surface you want to edit. Materials apply to one target at a time.
266
- </div>
267
- )}
268
- <MaterialPicker
269
- disabled={!materialTargetRole}
270
- hideSideControl
271
- nodeType="roof"
272
- onChange={handleTargetedMaterialChange}
273
- onSelectMaterialPreset={handleTargetedMaterialPresetChange}
274
- selectedMaterialPreset={materialPickerValue.materialPreset}
275
- value={materialPickerValue.material}
276
- />
277
- </PanelSection>
278
- </PanelWrapper>
279
- )
280
- }
@@ -1,311 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNode,
5
- type AnyNodeId,
6
- type RoofSegmentNode,
7
- RoofSegmentNode as RoofSegmentNodeSchema,
8
- type RoofType,
9
- useScene,
10
- } from '@pascal-app/core'
11
- import { useViewer } from '@pascal-app/viewer'
12
- import { Copy, Move, Trash2 } from 'lucide-react'
13
- import { useCallback } from 'react'
14
- import { sfxEmitter } from '../../../lib/sfx-bus'
15
- import useEditor from '../../../store/use-editor'
16
- import { ActionButton, ActionGroup } from '../controls/action-button'
17
- import { PanelSection } from '../controls/panel-section'
18
- import { SegmentedControl } from '../controls/segmented-control'
19
- import { SliderControl } from '../controls/slider-control'
20
- import { PanelWrapper } from './panel-wrapper'
21
-
22
- const ROOF_TYPE_OPTIONS: { label: string; value: RoofType }[] = [
23
- { label: 'Hip', value: 'hip' },
24
- { label: 'Gable', value: 'gable' },
25
- { label: 'Shed', value: 'shed' },
26
- { label: 'Flat', value: 'flat' },
27
- ]
28
-
29
- const ROOF_TYPE_OPTIONS_2: { label: string; value: RoofType }[] = [
30
- { label: 'Gambrel', value: 'gambrel' },
31
- { label: 'Dutch', value: 'dutch' },
32
- { label: 'Mansard', value: 'mansard' },
33
- ]
34
-
35
- export function RoofSegmentPanel() {
36
- const selectedId = useViewer((s) => s.selection.selectedIds[0])
37
- const setSelection = useViewer((s) => s.setSelection)
38
- const updateNode = useScene((s) => s.updateNode)
39
- const setMovingNode = useEditor((s) => s.setMovingNode)
40
-
41
- const node = useScene((s) =>
42
- selectedId ? (s.nodes[selectedId as AnyNode['id']] as RoofSegmentNode | undefined) : undefined,
43
- )
44
-
45
- const handleUpdate = useCallback(
46
- (updates: Partial<RoofSegmentNode>) => {
47
- if (!selectedId) return
48
- updateNode(selectedId as AnyNode['id'], updates)
49
- },
50
- [selectedId, updateNode],
51
- )
52
-
53
- const handleClose = useCallback(() => {
54
- setSelection({ selectedIds: [] })
55
- }, [setSelection])
56
-
57
- const handleBack = useCallback(() => {
58
- if (node?.parentId) {
59
- setSelection({ selectedIds: [node.parentId] })
60
- }
61
- }, [node?.parentId, setSelection])
62
-
63
- const handleDuplicate = useCallback(() => {
64
- if (!node?.parentId) return
65
- sfxEmitter.emit('sfx:item-pick')
66
-
67
- let duplicateInfo = structuredClone(node) as any
68
- delete duplicateInfo.id
69
- duplicateInfo.metadata = { ...duplicateInfo.metadata, isNew: true }
70
- // Offset slightly so it's visible
71
- duplicateInfo.position = [
72
- duplicateInfo.position[0] + 1,
73
- duplicateInfo.position[1],
74
- duplicateInfo.position[2] + 1,
75
- ]
76
-
77
- try {
78
- const duplicate = RoofSegmentNodeSchema.parse(duplicateInfo)
79
- useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
80
- setSelection({ selectedIds: [] })
81
- setMovingNode(duplicate)
82
- } catch (e) {
83
- console.error('Failed to duplicate roof segment', e)
84
- }
85
- }, [node, setSelection, setMovingNode])
86
-
87
- const handleMove = useCallback(() => {
88
- if (node) {
89
- sfxEmitter.emit('sfx:item-pick')
90
- setMovingNode(node)
91
- setSelection({ selectedIds: [] })
92
- }
93
- }, [node, setMovingNode, setSelection])
94
-
95
- const handleDelete = useCallback(() => {
96
- if (!(selectedId && node)) return
97
- sfxEmitter.emit('sfx:item-delete')
98
- const parentId = node.parentId
99
- useScene.getState().deleteNode(selectedId as AnyNodeId)
100
- if (parentId) {
101
- useScene.getState().dirtyNodes.add(parentId as AnyNodeId)
102
- setSelection({ selectedIds: [parentId] })
103
- } else {
104
- setSelection({ selectedIds: [] })
105
- }
106
- }, [selectedId, node, setSelection])
107
-
108
- if (!(node && node.type === 'roof-segment' && selectedId)) return null
109
-
110
- return (
111
- <PanelWrapper
112
- icon="/icons/roof.png"
113
- onBack={handleBack}
114
- onClose={handleClose}
115
- title={node.name || 'Roof Segment'}
116
- width={300}
117
- >
118
- <PanelSection title="Roof Type">
119
- <SegmentedControl
120
- onChange={(v) => handleUpdate({ roofType: v })}
121
- options={ROOF_TYPE_OPTIONS}
122
- value={node.roofType}
123
- />
124
- <SegmentedControl
125
- onChange={(v) => handleUpdate({ roofType: v })}
126
- options={ROOF_TYPE_OPTIONS_2}
127
- value={node.roofType}
128
- />
129
- </PanelSection>
130
-
131
- <PanelSection title="Footprint">
132
- <SliderControl
133
- label="Width"
134
- max={25}
135
- min={0.5}
136
- onChange={(v) => handleUpdate({ width: v })}
137
- precision={2}
138
- step={0.5}
139
- unit="m"
140
- value={Math.round(node.width * 100) / 100}
141
- />
142
- <SliderControl
143
- label="Depth"
144
- max={25}
145
- min={0.5}
146
- onChange={(v) => handleUpdate({ depth: v })}
147
- precision={2}
148
- step={0.5}
149
- unit="m"
150
- value={Math.round(node.depth * 100) / 100}
151
- />
152
- </PanelSection>
153
-
154
- <PanelSection title="Heights">
155
- <SliderControl
156
- label="Wall"
157
- max={5}
158
- min={0}
159
- onChange={(v) => handleUpdate({ wallHeight: v })}
160
- precision={2}
161
- step={0.1}
162
- unit="m"
163
- value={Math.round(node.wallHeight * 100) / 100}
164
- />
165
- <SliderControl
166
- label="Roof"
167
- max={15}
168
- min={0}
169
- onChange={(v) => handleUpdate({ roofHeight: v })}
170
- precision={2}
171
- step={0.1}
172
- unit="m"
173
- value={Math.round(node.roofHeight * 100) / 100}
174
- />
175
- </PanelSection>
176
-
177
- <PanelSection title="Structure">
178
- <SliderControl
179
- label="Wall Thick."
180
- max={1}
181
- min={0.05}
182
- onChange={(v) => handleUpdate({ wallThickness: v })}
183
- precision={2}
184
- step={0.05}
185
- unit="m"
186
- value={Math.round(node.wallThickness * 100) / 100}
187
- />
188
- <SliderControl
189
- label="Deck Thick."
190
- max={0.3}
191
- min={0.04}
192
- onChange={(v) => handleUpdate({ deckThickness: v })}
193
- precision={2}
194
- step={0.01}
195
- unit="m"
196
- value={Math.round(node.deckThickness * 100) / 100}
197
- />
198
- <SliderControl
199
- label="Overhang"
200
- max={1}
201
- min={0}
202
- onChange={(v) => handleUpdate({ overhang: v })}
203
- precision={2}
204
- step={0.05}
205
- unit="m"
206
- value={Math.round(node.overhang * 100) / 100}
207
- />
208
- <SliderControl
209
- label="Shingle Thick."
210
- max={0.3}
211
- min={0.02}
212
- onChange={(v) => handleUpdate({ shingleThickness: v })}
213
- precision={2}
214
- step={0.01}
215
- unit="m"
216
- value={Math.round(node.shingleThickness * 100) / 100}
217
- />
218
- </PanelSection>
219
-
220
- <PanelSection title="Position">
221
- <SliderControl
222
- label="X"
223
- max={50}
224
- min={-50}
225
- onChange={(v) => {
226
- const pos = [...node.position] as [number, number, number]
227
- pos[0] = v
228
- handleUpdate({ position: pos })
229
- }}
230
- precision={2}
231
- step={0.05}
232
- unit="m"
233
- value={Math.round(node.position[0] * 100) / 100}
234
- />
235
- <SliderControl
236
- label="Y"
237
- max={50}
238
- min={-50}
239
- onChange={(v) => {
240
- const pos = [...node.position] as [number, number, number]
241
- pos[1] = v
242
- handleUpdate({ position: pos })
243
- }}
244
- precision={2}
245
- step={0.05}
246
- unit="m"
247
- value={Math.round(node.position[1] * 100) / 100}
248
- />
249
- <SliderControl
250
- label="Z"
251
- max={50}
252
- min={-50}
253
- onChange={(v) => {
254
- const pos = [...node.position] as [number, number, number]
255
- pos[2] = v
256
- handleUpdate({ position: pos })
257
- }}
258
- precision={2}
259
- step={0.05}
260
- unit="m"
261
- value={Math.round(node.position[2] * 100) / 100}
262
- />
263
- <SliderControl
264
- label="Rotation"
265
- max={180}
266
- min={-180}
267
- onChange={(degrees) => {
268
- handleUpdate({ rotation: (degrees * Math.PI) / 180 })
269
- }}
270
- precision={0}
271
- step={1}
272
- unit="°"
273
- value={Math.round((node.rotation * 180) / Math.PI)}
274
- />
275
- <div className="flex gap-1.5 px-1 pt-2 pb-1">
276
- <ActionButton
277
- label="-45°"
278
- onClick={() => {
279
- sfxEmitter.emit('sfx:item-rotate')
280
- handleUpdate({ rotation: node.rotation - Math.PI / 4 })
281
- }}
282
- />
283
- <ActionButton
284
- label="+45°"
285
- onClick={() => {
286
- sfxEmitter.emit('sfx:item-rotate')
287
- handleUpdate({ rotation: node.rotation + Math.PI / 4 })
288
- }}
289
- />
290
- </div>
291
- </PanelSection>
292
-
293
- <PanelSection title="Actions">
294
- <ActionGroup>
295
- <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
296
- <ActionButton
297
- icon={<Copy className="h-3.5 w-3.5" />}
298
- label="Duplicate"
299
- onClick={handleDuplicate}
300
- />
301
- <ActionButton
302
- className="hover:bg-red-500/20"
303
- icon={<Trash2 className="h-3.5 w-3.5 text-red-400" />}
304
- label="Delete"
305
- onClick={handleDelete}
306
- />
307
- </ActionGroup>
308
- </PanelSection>
309
- </PanelWrapper>
310
- )
311
- }