@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,222 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNode,
5
- type AnyNodeId,
6
- type FenceNode,
7
- getClampedWallCurveOffset,
8
- getMaxWallCurveOffset,
9
- getWallCurveLength,
10
- type MaterialSchema,
11
- normalizeWallCurveOffset,
12
- useScene,
13
- } from '@pascal-app/core'
14
-
15
- import { useViewer } from '@pascal-app/viewer'
16
- import { Move, Spline } from 'lucide-react'
17
- import { useCallback } from 'react'
18
-
19
- import { sfxEmitter } from '../../../lib/sfx-bus'
20
- import useEditor from '../../../store/use-editor'
21
- import { ActionButton, ActionGroup } from '../controls/action-button'
22
- import { MaterialPicker } from '../controls/material-picker'
23
- import { PanelSection } from '../controls/panel-section'
24
- import { SegmentedControl } from '../controls/segmented-control'
25
- import { SliderControl } from '../controls/slider-control'
26
- import { PanelWrapper } from './panel-wrapper'
27
-
28
- type FenceStyleValue = 'slat' | 'rail' | 'privacy'
29
- type FenceBaseStyleValue = 'grounded' | 'floating'
30
-
31
- const FENCE_STYLE_OPTIONS: { label: string; value: FenceStyleValue }[] = [
32
- { label: 'Slat', value: 'slat' },
33
- { label: 'Rail', value: 'rail' },
34
- { label: 'Privacy', value: 'privacy' },
35
- ]
36
-
37
- const FENCE_BASE_STYLE_OPTIONS: { label: string; value: FenceBaseStyleValue }[] = [
38
- { label: 'Grounded', value: 'grounded' },
39
- { label: 'Floating', value: 'floating' },
40
- ]
41
-
42
- export function FencePanel() {
43
- const selectedId = useViewer((s) => s.selection.selectedIds[0])
44
- const selectedCount = useViewer((s) => s.selection.selectedIds.length)
45
- const setSelection = useViewer((s) => s.setSelection)
46
- const updateNode = useScene((s) => s.updateNode)
47
- const setMovingNode = useEditor((s) => s.setMovingNode)
48
- const setCurvingFence = useEditor((s) => s.setCurvingFence)
49
-
50
- const node = useScene((s) =>
51
- selectedId ? (s.nodes[selectedId as AnyNode['id']] as FenceNode | undefined) : undefined,
52
- )
53
-
54
- const handleUpdate = useCallback(
55
- (updates: Partial<FenceNode>) => {
56
- if (!selectedId) return
57
- updateNode(selectedId as AnyNode['id'], updates)
58
- useScene.getState().dirtyNodes.add(selectedId as AnyNodeId)
59
- },
60
- [selectedId, updateNode],
61
- )
62
-
63
- const handleUpdateLength = useCallback(
64
- (newLength: number) => {
65
- if (!node || newLength <= 0) return
66
-
67
- const dx = node.end[0] - node.start[0]
68
- const dz = node.end[1] - node.start[1]
69
- const currentLength = Math.sqrt(dx * dx + dz * dz)
70
- if (currentLength === 0) return
71
-
72
- const dirX = dx / currentLength
73
- const dirZ = dz / currentLength
74
- const newEnd: [number, number] = [
75
- node.start[0] + dirX * newLength,
76
- node.start[1] + dirZ * newLength,
77
- ]
78
-
79
- handleUpdate({ end: newEnd })
80
- },
81
- [node, handleUpdate],
82
- )
83
-
84
- const handleClose = useCallback(() => {
85
- setSelection({ selectedIds: [] })
86
- }, [setSelection])
87
-
88
- if (!(node && node.type === 'fence' && selectedId && selectedCount === 1)) return null
89
-
90
- const length = getWallCurveLength(node)
91
- const curveOffset = getClampedWallCurveOffset(node)
92
- const maxCurveOffset = getMaxWallCurveOffset(node)
93
-
94
- return (
95
- <PanelWrapper
96
- icon="/icons/build.png"
97
- onClose={handleClose}
98
- title={node.name || 'Fence'}
99
- width={300}
100
- >
101
- <PanelSection title="Style">
102
- <SegmentedControl
103
- onChange={(value) => handleUpdate({ style: value })}
104
- options={FENCE_STYLE_OPTIONS}
105
- value={node.style}
106
- />
107
- <SegmentedControl
108
- className="mt-2"
109
- onChange={(value) => handleUpdate({ baseStyle: value })}
110
- options={FENCE_BASE_STYLE_OPTIONS}
111
- value={node.baseStyle}
112
- />
113
- </PanelSection>
114
-
115
- <PanelSection title="Dimensions">
116
- <SliderControl
117
- label="Length"
118
- max={50}
119
- min={0.1}
120
- onChange={handleUpdateLength}
121
- precision={2}
122
- step={0.01}
123
- unit="m"
124
- value={length}
125
- />
126
- <SliderControl
127
- label="Curve"
128
- max={Math.max(0.01, maxCurveOffset)}
129
- min={-Math.max(0.01, maxCurveOffset)}
130
- onChange={(value) => handleUpdate({ curveOffset: normalizeWallCurveOffset(node, value) })}
131
- precision={2}
132
- step={0.1}
133
- unit="m"
134
- value={Math.round(curveOffset * 100) / 100}
135
- />
136
- <SliderControl
137
- label="Height"
138
- max={4}
139
- min={0.4}
140
- onChange={(value) => handleUpdate({ height: Math.max(0.4, value) })}
141
- precision={2}
142
- step={0.05}
143
- unit="m"
144
- value={node.height}
145
- />
146
- <SliderControl
147
- label="Thickness"
148
- max={0.5}
149
- min={0.03}
150
- onChange={(value) => handleUpdate({ thickness: Math.max(0.03, value) })}
151
- precision={3}
152
- step={0.005}
153
- unit="m"
154
- value={node.thickness}
155
- />
156
- </PanelSection>
157
-
158
- <PanelSection title="Structure">
159
- <SliderControl
160
- label="Base Height"
161
- max={1}
162
- min={0.04}
163
- onChange={(value) => handleUpdate({ baseHeight: Math.max(0.04, value) })}
164
- precision={3}
165
- step={0.01}
166
- unit="m"
167
- value={node.baseHeight}
168
- />
169
- <SliderControl
170
- label="Top Rail"
171
- max={0.25}
172
- min={0.01}
173
- onChange={(value) => handleUpdate({ topRailHeight: Math.max(0.01, value) })}
174
- precision={3}
175
- step={0.005}
176
- unit="m"
177
- value={node.topRailHeight}
178
- />
179
- <SliderControl
180
- label="Post Spacing"
181
- max={5}
182
- min={0.2}
183
- onChange={(value) => handleUpdate({ postSpacing: Math.max(0.2, value) })}
184
- precision={2}
185
- step={0.05}
186
- unit="m"
187
- value={node.postSpacing}
188
- />
189
- <SliderControl
190
- label="Post Size"
191
- max={0.4}
192
- min={0.01}
193
- onChange={(value) => handleUpdate({ postSize: Math.max(0.01, value) })}
194
- precision={3}
195
- step={0.005}
196
- unit="m"
197
- value={node.postSize}
198
- />
199
- <SliderControl
200
- label="Ground Clear"
201
- max={0.6}
202
- min={0}
203
- onChange={(value) => handleUpdate({ groundClearance: Math.max(0, value) })}
204
- precision={3}
205
- step={0.005}
206
- unit="m"
207
- value={node.groundClearance}
208
- />
209
- <SliderControl
210
- label="Edge Inset"
211
- max={0.25}
212
- min={0.005}
213
- onChange={(value) => handleUpdate({ edgeInset: Math.max(0.005, value) })}
214
- precision={3}
215
- step={0.005}
216
- unit="m"
217
- value={node.edgeInset}
218
- />
219
- </PanelSection>
220
- </PanelWrapper>
221
- )
222
- }
@@ -1,306 +0,0 @@
1
- 'use client'
2
-
3
- import { type AnyNode, getScaledDimensions, ItemNode, useScene } from '@pascal-app/core'
4
- import { useViewer } from '@pascal-app/viewer'
5
- import { Copy, Link, Link2Off, Move, Trash2 } from 'lucide-react'
6
- import { useCallback, useState } from 'react'
7
- import { sfxEmitter } from '../../../lib/sfx-bus'
8
- import { cn } from '../../../lib/utils'
9
- import useEditor from '../../../store/use-editor'
10
- import { ActionButton, ActionGroup } from '../controls/action-button'
11
- import { PanelSection } from '../controls/panel-section'
12
- import { SliderControl } from '../controls/slider-control'
13
- import { CollectionsPopover } from './collections/collections-popover'
14
- import { PanelWrapper } from './panel-wrapper'
15
-
16
- export function ItemPanel() {
17
- const selectedId = useViewer((s) => s.selection.selectedIds[0])
18
- const setSelection = useViewer((s) => s.setSelection)
19
- const updateNode = useScene((s) => s.updateNode)
20
- const deleteNode = useScene((s) => s.deleteNode)
21
- const setMovingNode = useEditor((s) => s.setMovingNode)
22
-
23
- const node = useScene((s) =>
24
- selectedId ? (s.nodes[selectedId as AnyNode['id']] as ItemNode | undefined) : undefined,
25
- )
26
-
27
- const [uniformScale, setUniformScale] = useState(true)
28
-
29
- const handleUpdate = useCallback(
30
- (updates: Partial<ItemNode>) => {
31
- if (!(selectedId && node)) return
32
- updateNode(selectedId as AnyNode['id'], updates)
33
-
34
- if (node.asset.attachTo === 'wall' && node.parentId) {
35
- requestAnimationFrame(() => {
36
- useScene.getState().dirtyNodes.add(node.parentId as AnyNode['id'])
37
- })
38
- }
39
- },
40
- [selectedId, node, updateNode],
41
- )
42
-
43
- const handleClose = useCallback(() => {
44
- setSelection({ selectedIds: [] })
45
- }, [setSelection])
46
-
47
- const handleMove = useCallback(() => {
48
- if (node) {
49
- sfxEmitter.emit('sfx:item-pick')
50
- setMovingNode(node)
51
- setSelection({ selectedIds: [] })
52
- }
53
- }, [node, setMovingNode, setSelection])
54
-
55
- const handleDuplicate = useCallback(() => {
56
- if (!node) return
57
- sfxEmitter.emit('sfx:item-pick')
58
- const proto = ItemNode.parse({
59
- position: [...node.position] as [number, number, number],
60
- rotation: [...node.rotation] as [number, number, number],
61
- name: node.name,
62
- asset: node.asset,
63
- parentId: node.parentId,
64
- side: node.side,
65
- metadata: { isNew: true },
66
- })
67
- setMovingNode(proto)
68
- setSelection({ selectedIds: [] })
69
- }, [node, setMovingNode, setSelection])
70
-
71
- const handleDelete = useCallback(() => {
72
- if (!selectedId) return
73
- sfxEmitter.emit('sfx:item-delete')
74
- deleteNode(selectedId as AnyNode['id'])
75
- setSelection({ selectedIds: [] })
76
- }, [selectedId, deleteNode, setSelection])
77
-
78
- if (!(node && node.type === 'item' && selectedId)) return null
79
-
80
- return (
81
- <PanelWrapper
82
- icon={node.asset.thumbnail || '/icons/furniture.png'}
83
- onClose={handleClose}
84
- title={node.name || node.asset.name}
85
- width={300}
86
- >
87
- <PanelSection title="Position">
88
- <SliderControl
89
- label={
90
- <>
91
- X<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
92
- </>
93
- }
94
- max={node.position[0] + 2}
95
- min={node.position[0] - 2}
96
- onChange={(value) =>
97
- handleUpdate({ position: [value, node.position[1], node.position[2]] })
98
- }
99
- precision={2}
100
- step={0.01}
101
- unit="m"
102
- value={Math.round(node.position[0] * 100) / 100}
103
- />
104
- <SliderControl
105
- label={
106
- <>
107
- Y<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
108
- </>
109
- }
110
- max={node.position[1] + 2}
111
- min={node.position[1] - 2}
112
- onChange={(value) =>
113
- handleUpdate({ position: [node.position[0], value, node.position[2]] })
114
- }
115
- precision={2}
116
- step={0.01}
117
- unit="m"
118
- value={Math.round(node.position[1] * 100) / 100}
119
- />
120
- <SliderControl
121
- label={
122
- <>
123
- Z<sub className="ml-[1px] text-[11px] opacity-70">pos</sub>
124
- </>
125
- }
126
- max={node.position[2] + 2}
127
- min={node.position[2] - 2}
128
- onChange={(value) =>
129
- handleUpdate({ position: [node.position[0], node.position[1], value] })
130
- }
131
- precision={2}
132
- step={0.01}
133
- unit="m"
134
- value={Math.round(node.position[2] * 100) / 100}
135
- />
136
- </PanelSection>
137
-
138
- <PanelSection title="Rotation">
139
- <SliderControl
140
- label={
141
- <>
142
- Y<sub className="ml-[1px] text-[11px] opacity-70">rot</sub>
143
- </>
144
- }
145
- max={Math.round((node.rotation[1] * 180) / Math.PI) + 45}
146
- min={Math.round((node.rotation[1] * 180) / Math.PI) - 45}
147
- onChange={(degrees) => {
148
- const radians = (degrees * Math.PI) / 180
149
- handleUpdate({ rotation: [node.rotation[0], radians, node.rotation[2]] })
150
- }}
151
- precision={0}
152
- step={1}
153
- unit="°"
154
- value={Math.round((node.rotation[1] * 180) / Math.PI)}
155
- />
156
- <div className="flex gap-1.5 px-1 pt-2 pb-1">
157
- <ActionButton
158
- label="-45°"
159
- onClick={() => {
160
- sfxEmitter.emit('sfx:item-rotate')
161
- const currentDegrees = (node.rotation[1] * 180) / Math.PI
162
- const radians = ((currentDegrees - 45) * Math.PI) / 180
163
- handleUpdate({ rotation: [node.rotation[0], radians, node.rotation[2]] })
164
- }}
165
- />
166
- <ActionButton
167
- label="+45°"
168
- onClick={() => {
169
- sfxEmitter.emit('sfx:item-rotate')
170
- const currentDegrees = (node.rotation[1] * 180) / Math.PI
171
- const radians = ((currentDegrees + 45) * Math.PI) / 180
172
- handleUpdate({ rotation: [node.rotation[0], radians, node.rotation[2]] })
173
- }}
174
- />
175
- </div>
176
- </PanelSection>
177
-
178
- <PanelSection title="Scale">
179
- <div className="flex items-center justify-between px-2 pb-2">
180
- <span className="font-medium text-[10px] text-muted-foreground/80 uppercase tracking-wider">
181
- Uniform Scale
182
- </span>
183
- <button
184
- className={cn(
185
- 'flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground',
186
- uniformScale ? 'bg-[#3e3e3e]' : 'bg-[#2C2C2E] hover:bg-[#3e3e3e]',
187
- )}
188
- onClick={() => setUniformScale((v) => !v)}
189
- type="button"
190
- >
191
- {uniformScale ? <Link className="h-3.5 w-3.5" /> : <Link2Off className="h-3.5 w-3.5" />}
192
- </button>
193
- </div>
194
-
195
- {uniformScale ? (
196
- <SliderControl
197
- label={
198
- <>
199
- XYZ<sub className="ml-[1px] text-[11px] opacity-70">scale</sub>
200
- </>
201
- }
202
- max={10}
203
- min={0.01}
204
- onChange={(value) => {
205
- const v = Math.max(0.01, value)
206
- handleUpdate({ scale: [v, v, v] })
207
- }}
208
- precision={2}
209
- step={0.1}
210
- value={Math.round(node.scale[0] * 100) / 100}
211
- />
212
- ) : (
213
- <>
214
- <SliderControl
215
- label={
216
- <>
217
- X<sub className="ml-[1px] text-[11px] opacity-70">scale</sub>
218
- </>
219
- }
220
- max={10}
221
- min={0.01}
222
- onChange={(value) =>
223
- handleUpdate({ scale: [Math.max(0.01, value), node.scale[1], node.scale[2]] })
224
- }
225
- precision={2}
226
- step={0.1}
227
- value={Math.round(node.scale[0] * 100) / 100}
228
- />
229
- <SliderControl
230
- label={
231
- <>
232
- Y<sub className="ml-[1px] text-[11px] opacity-70">scale</sub>
233
- </>
234
- }
235
- max={10}
236
- min={0.01}
237
- onChange={(value) =>
238
- handleUpdate({ scale: [node.scale[0], Math.max(0.01, value), node.scale[2]] })
239
- }
240
- precision={2}
241
- step={0.1}
242
- value={Math.round(node.scale[1] * 100) / 100}
243
- />
244
- <SliderControl
245
- label={
246
- <>
247
- Z<sub className="ml-[1px] text-[11px] opacity-70">scale</sub>
248
- </>
249
- }
250
- max={10}
251
- min={0.01}
252
- onChange={(value) =>
253
- handleUpdate({ scale: [node.scale[0], node.scale[1], Math.max(0.01, value)] })
254
- }
255
- precision={2}
256
- step={0.1}
257
- value={Math.round(node.scale[2] * 100) / 100}
258
- />
259
- </>
260
- )}
261
- </PanelSection>
262
-
263
- <PanelSection title="Info">
264
- <div className="flex items-center justify-between px-2 py-1 text-muted-foreground text-sm">
265
- <span>Dimensions</span>
266
- {(() => {
267
- const [w, h, d] = getScaledDimensions(node)
268
- return (
269
- <span className="font-mono text-white">
270
- {Math.round(w * 100) / 100}×{Math.round(h * 100) / 100}×{Math.round(d * 100) / 100}
271
- </span>
272
- )
273
- })()}
274
- </div>
275
- </PanelSection>
276
-
277
- <PanelSection title="Collections">
278
- <ActionGroup>
279
- <CollectionsPopover
280
- collectionIds={node.collectionIds}
281
- nodeId={selectedId as AnyNode['id']}
282
- >
283
- <ActionButton label="Manage collections…" />
284
- </CollectionsPopover>
285
- </ActionGroup>
286
- </PanelSection>
287
-
288
- <PanelSection title="Actions">
289
- <ActionGroup>
290
- <ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
291
- <ActionButton
292
- icon={<Copy className="h-3.5 w-3.5" />}
293
- label="Duplicate"
294
- onClick={handleDuplicate}
295
- />
296
- <ActionButton
297
- className="hover:bg-red-500/20"
298
- icon={<Trash2 className="h-3.5 w-3.5 text-red-400" />}
299
- label="Delete"
300
- onClick={handleDelete}
301
- />
302
- </ActionGroup>
303
- </PanelSection>
304
- </PanelWrapper>
305
- )
306
- }