@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,182 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNodeId,
5
- emitter,
6
- type FenceNode,
7
- type GridEvent,
8
- type LevelNode,
9
- type SlabNode,
10
- useScene,
11
- type WallNode,
12
- } from '@pascal-app/core'
13
- import { useViewer } from '@pascal-app/viewer'
14
- import { useCallback, useEffect, useRef, useState } from 'react'
15
- import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
16
- import { sfxEmitter } from '../../../lib/sfx-bus'
17
- import useEditor from '../../../store/use-editor'
18
- import { snapFenceDraftPoint } from '../fence/fence-drafting'
19
- import { CursorSphere } from '../shared/cursor-sphere'
20
-
21
- function translatePolygon(
22
- polygon: Array<[number, number]>,
23
- deltaX: number,
24
- deltaZ: number,
25
- ): Array<[number, number]> {
26
- return polygon.map(([x, z]) => [x + deltaX, z + deltaZ] as [number, number])
27
- }
28
-
29
- function getPolygonCenter(polygon: Array<[number, number]>): [number, number] {
30
- if (polygon.length === 0) return [0, 0]
31
- let sumX = 0
32
- let sumZ = 0
33
- for (const [x, z] of polygon) {
34
- sumX += x
35
- sumZ += z
36
- }
37
- return [sumX / polygon.length, sumZ / polygon.length]
38
- }
39
-
40
- export const MoveSlabTool: React.FC<{ node: SlabNode }> = ({ node }) => {
41
- const activatedAtRef = useRef<number>(Date.now())
42
- const originalPolygonRef = useRef(node.polygon.map(([x, z]) => [x, z] as [number, number]))
43
- const originalHolesRef = useRef(
44
- (node.holes ?? []).map((hole) => hole.map(([x, z]) => [x, z] as [number, number])),
45
- )
46
- const dragAnchorRef = useRef<[number, number] | null>(null)
47
- const previousGridPosRef = useRef<[number, number] | null>(null)
48
- const previewRef = useRef<{
49
- polygon: Array<[number, number]>
50
- holes: Array<Array<[number, number]>>
51
- } | null>(null)
52
-
53
- const [cursorLocalPos, setCursorLocalPos] = useState<[number, number, number]>(() => {
54
- const center = getPolygonCenter(node.polygon)
55
- return [center[0], 0, center[1]]
56
- })
57
-
58
- const exitMoveMode = useCallback(() => {
59
- useEditor.getState().setMovingNode(null)
60
- }, [])
61
-
62
- useEffect(() => {
63
- const originalPolygon = originalPolygonRef.current
64
- const originalHoles = originalHolesRef.current
65
- const levelNode =
66
- node.parentId && useScene.getState().nodes[node.parentId as AnyNodeId]?.type === 'level'
67
- ? (useScene.getState().nodes[node.parentId as AnyNodeId] as LevelNode)
68
- : null
69
- const levelChildren = levelNode?.children ?? []
70
- const levelWalls = levelChildren
71
- .map((childId) => useScene.getState().nodes[childId as AnyNodeId])
72
- .filter((child): child is WallNode => child?.type === 'wall')
73
- const levelFences = levelChildren
74
- .map((childId) => useScene.getState().nodes[childId as AnyNodeId])
75
- .filter((child): child is FenceNode => child?.type === 'fence')
76
-
77
- useScene.temporal.getState().pause()
78
- let wasCommitted = false
79
-
80
- const applyPreview = (
81
- polygon: Array<[number, number]>,
82
- holes: Array<Array<[number, number]>>,
83
- ) => {
84
- previewRef.current = { polygon, holes }
85
- const center = getPolygonCenter(polygon)
86
- setCursorLocalPos([center[0], 0, center[1]])
87
- useScene.getState().updateNode(node.id, { polygon, holes })
88
- useScene.getState().markDirty(node.id as AnyNodeId)
89
- }
90
-
91
- const restoreOriginal = () => {
92
- useScene.getState().updateNode(node.id, {
93
- holes: originalHoles,
94
- polygon: originalPolygon,
95
- })
96
- useScene.getState().markDirty(node.id as AnyNodeId)
97
- }
98
-
99
- const onGridMove = (event: GridEvent) => {
100
- const [localX, localZ] = snapFenceDraftPoint({
101
- point: [event.localPosition[0], event.localPosition[2]],
102
- walls: levelWalls,
103
- fences: levelFences,
104
- })
105
-
106
- if (
107
- previousGridPosRef.current &&
108
- (localX !== previousGridPosRef.current[0] || localZ !== previousGridPosRef.current[1])
109
- ) {
110
- sfxEmitter.emit('sfx:grid-snap')
111
- }
112
- previousGridPosRef.current = [localX, localZ]
113
-
114
- const anchor = dragAnchorRef.current ?? [localX, localZ]
115
- dragAnchorRef.current = anchor
116
-
117
- const deltaX = localX - anchor[0]
118
- const deltaZ = localZ - anchor[1]
119
-
120
- applyPreview(
121
- translatePolygon(originalPolygon, deltaX, deltaZ),
122
- originalHoles.map((hole) => translatePolygon(hole, deltaX, deltaZ)),
123
- )
124
- }
125
-
126
- const onGridClick = (event: GridEvent) => {
127
- if (Date.now() - activatedAtRef.current < 150) {
128
- event.nativeEvent?.stopPropagation?.()
129
- return
130
- }
131
-
132
- const preview = previewRef.current ?? { polygon: originalPolygon, holes: originalHoles }
133
-
134
- wasCommitted = true
135
-
136
- // Restore original baseline while paused so the next resume+update
137
- // registers as a single tracked change (undo reverts to original).
138
- useScene.getState().updateNode(node.id, {
139
- polygon: originalPolygon,
140
- holes: originalHoles,
141
- })
142
-
143
- useScene.temporal.getState().resume()
144
- useScene.getState().updateNode(node.id, preview)
145
- useScene.getState().markDirty(node.id as AnyNodeId)
146
- useScene.temporal.getState().pause()
147
-
148
- sfxEmitter.emit('sfx:item-place')
149
- useViewer.getState().setSelection({ selectedIds: [node.id] })
150
- exitMoveMode()
151
- event.nativeEvent?.stopPropagation?.()
152
- }
153
-
154
- const onCancel = () => {
155
- restoreOriginal()
156
- useViewer.getState().setSelection({ selectedIds: [node.id] })
157
- useScene.temporal.getState().resume()
158
- markToolCancelConsumed()
159
- exitMoveMode()
160
- }
161
-
162
- emitter.on('grid:move', onGridMove)
163
- emitter.on('grid:click', onGridClick)
164
- emitter.on('tool:cancel', onCancel)
165
-
166
- return () => {
167
- if (!wasCommitted) {
168
- restoreOriginal()
169
- }
170
- useScene.temporal.getState().resume()
171
- emitter.off('grid:move', onGridMove)
172
- emitter.off('grid:click', onGridClick)
173
- emitter.off('tool:cancel', onCancel)
174
- }
175
- }, [exitMoveMode, node.id])
176
-
177
- return (
178
- <group>
179
- <CursorSphere position={cursorLocalPos} showTooltip={false} />
180
- </group>
181
- )
182
- }
@@ -1,43 +0,0 @@
1
- import { resolveLevelId, type SlabNode, useScene } from '@pascal-app/core'
2
- import { useViewer } from '@pascal-app/viewer'
3
- import { useCallback } from 'react'
4
- import { PolygonEditor } from '../shared/polygon-editor'
5
-
6
- interface SlabBoundaryEditorProps {
7
- slabId: SlabNode['id']
8
- }
9
-
10
- /**
11
- * Slab boundary editor - allows editing slab polygon vertices for a specific slab
12
- * Uses the generic PolygonEditor component
13
- */
14
- export const SlabBoundaryEditor: React.FC<SlabBoundaryEditorProps> = ({ slabId }) => {
15
- const slabNode = useScene((state) => state.nodes[slabId])
16
- const updateNode = useScene((state) => state.updateNode)
17
- const setSelection = useViewer((state) => state.setSelection)
18
-
19
- const slab = slabNode?.type === 'slab' ? (slabNode as SlabNode) : null
20
-
21
- const handlePolygonChange = useCallback(
22
- (newPolygon: Array<[number, number]>) => {
23
- updateNode(slabId, { polygon: newPolygon })
24
- // Re-assert selection so the slab stays selected after the edit
25
- setSelection({ selectedIds: [slabId] })
26
- },
27
- [slabId, updateNode, setSelection],
28
- )
29
-
30
- if (!slab?.polygon || slab.polygon.length < 3) return null
31
-
32
- return (
33
- <PolygonEditor
34
- allowEdgeMove
35
- color="#a3a3a3"
36
- levelId={resolveLevelId(slab, useScene.getState().nodes)}
37
- minVertices={3}
38
- onPolygonChange={handlePolygonChange}
39
- polygon={slab.polygon}
40
- surfaceHeight={slab.elevation ?? 0.05}
41
- />
42
- )
43
- }
@@ -1,49 +0,0 @@
1
- import { resolveLevelId, type SlabNode, useScene } from '@pascal-app/core'
2
- import { useViewer } from '@pascal-app/viewer'
3
- import { useCallback } from 'react'
4
- import { PolygonEditor } from '../shared/polygon-editor'
5
-
6
- interface SlabHoleEditorProps {
7
- slabId: SlabNode['id']
8
- holeIndex: number
9
- }
10
-
11
- /**
12
- * Slab hole editor - allows editing a specific hole polygon within a slab
13
- * Uses the generic PolygonEditor component
14
- */
15
- export const SlabHoleEditor: React.FC<SlabHoleEditorProps> = ({ slabId, holeIndex }) => {
16
- const slabNode = useScene((state) => state.nodes[slabId])
17
- const updateNode = useScene((state) => state.updateNode)
18
- const setSelection = useViewer((state) => state.setSelection)
19
-
20
- const slab = slabNode?.type === 'slab' ? (slabNode as SlabNode) : null
21
- const holes = slab?.holes || []
22
- const hole = holes[holeIndex]
23
-
24
- const handlePolygonChange = useCallback(
25
- (newPolygon: Array<[number, number]>) => {
26
- const updatedHoles = [...holes]
27
- updatedHoles[holeIndex] = newPolygon
28
- updateNode(slabId, { holes: updatedHoles })
29
- // Re-assert selection so the slab stays selected after the edit
30
- setSelection({ selectedIds: [slabId] })
31
- },
32
- [slabId, holeIndex, holes, updateNode, setSelection],
33
- )
34
-
35
- if (!(slab && hole) || hole.length < 3) return null
36
-
37
- return (
38
- <PolygonEditor
39
- allowEdgeMove
40
- allowPolygonMove
41
- color="#ef4444"
42
- levelId={resolveLevelId(slab, useScene.getState().nodes)} // red for holes
43
- minVertices={3}
44
- onPolygonChange={handlePolygonChange}
45
- polygon={hole}
46
- surfaceHeight={slab.elevation ?? 0.05}
47
- />
48
- )
49
- }
@@ -1,322 +0,0 @@
1
- import { emitter, type GridEvent, type LevelNode, SlabNode, useScene } from '@pascal-app/core'
2
- import { useViewer } from '@pascal-app/viewer'
3
- import { useEffect, useMemo, useRef, useState } from 'react'
4
- import { BufferGeometry, DoubleSide, type Group, type Line, Shape, Vector3 } from 'three'
5
- import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
6
- import { EDITOR_LAYER } from '../../../lib/constants'
7
- import { sfxEmitter } from '../../../lib/sfx-bus'
8
- import { CursorSphere } from '../shared/cursor-sphere'
9
-
10
- const Y_OFFSET = 0.02
11
-
12
- /**
13
- * Snaps a point to the nearest axis-aligned or 45-degree diagonal from the last point
14
- */
15
- const calculateSnapPoint = (
16
- lastPoint: [number, number],
17
- currentPoint: [number, number],
18
- ): [number, number] => {
19
- const [x1, y1] = lastPoint
20
- const [x, y] = currentPoint
21
-
22
- const dx = x - x1
23
- const dy = y - y1
24
- const absDx = Math.abs(dx)
25
- const absDy = Math.abs(dy)
26
-
27
- // Calculate distances to horizontal, vertical, and diagonal lines
28
- const horizontalDist = absDy
29
- const verticalDist = absDx
30
- const diagonalDist = Math.abs(absDx - absDy)
31
-
32
- // Find the minimum distance to determine which axis to snap to
33
- const minDist = Math.min(horizontalDist, verticalDist, diagonalDist)
34
-
35
- if (minDist === diagonalDist) {
36
- // Snap to 45° diagonal
37
- const diagonalLength = Math.min(absDx, absDy)
38
- return [x1 + Math.sign(dx) * diagonalLength, y1 + Math.sign(dy) * diagonalLength]
39
- }
40
- if (minDist === horizontalDist) {
41
- // Snap to horizontal
42
- return [x, y1]
43
- }
44
- // Snap to vertical
45
- return [x1, y]
46
- }
47
-
48
- /**
49
- * Creates a slab with the given polygon points and returns its ID
50
- */
51
- const commitSlabDrawing = (levelId: LevelNode['id'], points: Array<[number, number]>): string => {
52
- const { createNode, nodes } = useScene.getState()
53
-
54
- // Count existing slabs for naming
55
- const slabCount = Object.values(nodes).filter((n) => n.type === 'slab').length
56
- const name = `Slab ${slabCount + 1}`
57
-
58
- const slab = SlabNode.parse({
59
- name,
60
- polygon: points,
61
- })
62
-
63
- createNode(slab, levelId)
64
- sfxEmitter.emit('sfx:structure-build')
65
- return slab.id
66
- }
67
-
68
- export const SlabTool: React.FC = () => {
69
- const cursorRef = useRef<Group>(null)
70
- const mainLineRef = useRef<Line>(null!)
71
- const closingLineRef = useRef<Line>(null!)
72
- const currentLevelId = useViewer((state) => state.selection.levelId)
73
- const setSelection = useViewer((state) => state.setSelection)
74
-
75
- const [points, setPoints] = useState<Array<[number, number]>>([])
76
- const [cursorPosition, setCursorPosition] = useState<[number, number]>([0, 0])
77
- const [snappedCursorPosition, setSnappedCursorPosition] = useState<[number, number]>([0, 0])
78
- const [levelY, setLevelY] = useState(0)
79
- const previousSnappedPointRef = useRef<[number, number] | null>(null)
80
- const shiftPressed = useRef(false)
81
-
82
- // Update cursor position and lines on grid move
83
- useEffect(() => {
84
- if (!currentLevelId) return
85
-
86
- const onGridMove = (event: GridEvent) => {
87
- if (!cursorRef.current) return
88
-
89
- const gridX = Math.round(event.localPosition[0] * 2) / 2
90
- const gridZ = Math.round(event.localPosition[2] * 2) / 2
91
- const gridPosition: [number, number] = [gridX, gridZ]
92
-
93
- setCursorPosition(gridPosition)
94
- setLevelY(event.localPosition[1])
95
-
96
- // Calculate snapped display position (bypass snap when Shift is held)
97
- const lastPoint = points[points.length - 1]
98
- const displayPoint =
99
- shiftPressed.current || !lastPoint
100
- ? gridPosition
101
- : calculateSnapPoint(lastPoint, gridPosition)
102
- setSnappedCursorPosition(displayPoint)
103
-
104
- // Play snap sound when the snapped position actually changes (only when drawing)
105
- if (
106
- points.length > 0 &&
107
- previousSnappedPointRef.current &&
108
- (displayPoint[0] !== previousSnappedPointRef.current[0] ||
109
- displayPoint[1] !== previousSnappedPointRef.current[1])
110
- ) {
111
- sfxEmitter.emit('sfx:grid-snap')
112
- }
113
-
114
- previousSnappedPointRef.current = displayPoint
115
- cursorRef.current.position.set(displayPoint[0], event.localPosition[1], displayPoint[1])
116
- }
117
-
118
- const onGridClick = (_event: GridEvent) => {
119
- if (!currentLevelId) return
120
-
121
- // Use the last displayed snapped position (respects Shift state from onGridMove)
122
- const clickPoint = previousSnappedPointRef.current ?? cursorPosition
123
-
124
- // Check if clicking on the first point to close the shape
125
- const firstPoint = points[0]
126
- if (
127
- points.length >= 3 &&
128
- firstPoint &&
129
- Math.abs(clickPoint[0] - firstPoint[0]) < 0.25 &&
130
- Math.abs(clickPoint[1] - firstPoint[1]) < 0.25
131
- ) {
132
- // Create the slab and select it
133
- const slabId = commitSlabDrawing(currentLevelId, points)
134
- setSelection({ selectedIds: [slabId] })
135
- setPoints([])
136
- } else {
137
- // Add point to polygon
138
- setPoints([...points, clickPoint])
139
- }
140
- }
141
-
142
- const onGridDoubleClick = (_event: GridEvent) => {
143
- if (!currentLevelId) return
144
-
145
- // Need at least 3 points to form a polygon
146
- if (points.length >= 3) {
147
- const slabId = commitSlabDrawing(currentLevelId, points)
148
- setSelection({ selectedIds: [slabId] })
149
- setPoints([])
150
- }
151
- }
152
-
153
- const onCancel = () => {
154
- if (points.length > 0) markToolCancelConsumed()
155
- setPoints([])
156
- }
157
-
158
- const onKeyDown = (e: KeyboardEvent) => {
159
- if (e.key === 'Shift') shiftPressed.current = true
160
- }
161
- const onKeyUp = (e: KeyboardEvent) => {
162
- if (e.key === 'Shift') shiftPressed.current = false
163
- }
164
- document.addEventListener('keydown', onKeyDown)
165
- document.addEventListener('keyup', onKeyUp)
166
-
167
- emitter.on('grid:move', onGridMove)
168
- emitter.on('grid:click', onGridClick)
169
- emitter.on('grid:double-click', onGridDoubleClick)
170
- emitter.on('tool:cancel', onCancel)
171
-
172
- return () => {
173
- document.removeEventListener('keydown', onKeyDown)
174
- document.removeEventListener('keyup', onKeyUp)
175
- emitter.off('grid:move', onGridMove)
176
- emitter.off('grid:click', onGridClick)
177
- emitter.off('grid:double-click', onGridDoubleClick)
178
- emitter.off('tool:cancel', onCancel)
179
- }
180
- }, [currentLevelId, points, cursorPosition, setSelection])
181
-
182
- // Update line geometries when points change
183
- useEffect(() => {
184
- if (!(mainLineRef.current && closingLineRef.current)) return
185
-
186
- if (points.length === 0) {
187
- mainLineRef.current.visible = false
188
- closingLineRef.current.visible = false
189
- return
190
- }
191
-
192
- const y = levelY + Y_OFFSET
193
- const snappedCursor = snappedCursorPosition
194
-
195
- // Build main line points
196
- const linePoints: Vector3[] = points.map(([x, z]) => new Vector3(x, y, z))
197
- linePoints.push(new Vector3(snappedCursor[0], y, snappedCursor[1]))
198
-
199
- // Update main line
200
- if (linePoints.length >= 2) {
201
- mainLineRef.current.geometry.dispose()
202
- mainLineRef.current.geometry = new BufferGeometry().setFromPoints(linePoints)
203
- mainLineRef.current.visible = true
204
- } else {
205
- mainLineRef.current.visible = false
206
- }
207
-
208
- // Update closing line (from cursor back to first point)
209
- const firstPoint = points[0]
210
- if (points.length >= 2 && firstPoint) {
211
- const closingPoints = [
212
- new Vector3(snappedCursor[0], y, snappedCursor[1]),
213
- new Vector3(firstPoint[0], y, firstPoint[1]),
214
- ]
215
- closingLineRef.current.geometry.dispose()
216
- closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints)
217
- closingLineRef.current.visible = true
218
- } else {
219
- closingLineRef.current.visible = false
220
- }
221
- }, [points, snappedCursorPosition, levelY])
222
-
223
- // Create preview shape when we have 3+ points
224
- const previewShape = useMemo(() => {
225
- if (points.length < 3) return null
226
-
227
- const snappedCursor = snappedCursorPosition
228
-
229
- const allPoints = [...points, snappedCursor]
230
-
231
- // THREE.Shape is in X-Y plane. After rotation of -PI/2 around X:
232
- // - Shape X -> World X
233
- // - Shape Y -> World -Z (so we negate Z to get correct orientation)
234
- const firstPt = allPoints[0]
235
- if (!firstPt) return null
236
-
237
- const shape = new Shape()
238
- shape.moveTo(firstPt[0], -firstPt[1])
239
-
240
- for (let i = 1; i < allPoints.length; i++) {
241
- const pt = allPoints[i]
242
- if (pt) {
243
- shape.lineTo(pt[0], -pt[1])
244
- }
245
- }
246
- shape.closePath()
247
-
248
- return shape
249
- }, [points, snappedCursorPosition])
250
-
251
- return (
252
- <group>
253
- {/* Cursor */}
254
- <CursorSphere ref={cursorRef} />
255
-
256
- {/* Preview fill */}
257
- {previewShape && (
258
- <mesh
259
- frustumCulled={false}
260
- layers={EDITOR_LAYER}
261
- position={[0, levelY + Y_OFFSET, 0]}
262
- rotation={[-Math.PI / 2, 0, 0]}
263
- >
264
- <shapeGeometry args={[previewShape]} />
265
- <meshBasicMaterial
266
- color="#818cf8"
267
- depthTest={false}
268
- opacity={0.15}
269
- side={DoubleSide}
270
- transparent
271
- />
272
- </mesh>
273
- )}
274
-
275
- {/* Main line */}
276
- {/* @ts-ignore */}
277
- <line
278
- frustumCulled={false}
279
- layers={EDITOR_LAYER}
280
- // @ts-expect-error
281
- ref={mainLineRef}
282
- renderOrder={1}
283
- visible={false}
284
- >
285
- <bufferGeometry />
286
- <lineBasicNodeMaterial color="#818cf8" depthTest={false} depthWrite={false} linewidth={3} />
287
- </line>
288
-
289
- {/* Closing line */}
290
- {/* @ts-ignore */}
291
- <line
292
- frustumCulled={false}
293
- layers={EDITOR_LAYER}
294
- // @ts-expect-error
295
- ref={closingLineRef}
296
- renderOrder={1}
297
- visible={false}
298
- >
299
- <bufferGeometry />
300
- <lineBasicNodeMaterial
301
- color="#818cf8"
302
- depthTest={false}
303
- depthWrite={false}
304
- linewidth={2}
305
- opacity={0.5}
306
- transparent
307
- />
308
- </line>
309
-
310
- {/* Point markers */}
311
- {points.map(([x, z], index) => (
312
- <CursorSphere
313
- color="#818cf8"
314
- height={0}
315
- key={index}
316
- position={[x, levelY + Y_OFFSET + 0.01, z]}
317
- showTooltip={false}
318
- />
319
- ))}
320
- </group>
321
- )
322
- }
@@ -1,101 +0,0 @@
1
- import '../../../three-types'
2
-
3
- import {
4
- emitter,
5
- type GridEvent,
6
- type SpawnNode,
7
- sceneRegistry,
8
- useLiveTransforms,
9
- useScene,
10
- } from '@pascal-app/core'
11
- import { useCallback, useEffect, useState } from 'react'
12
- import { Vector3 } from 'three'
13
- import { sfxEmitter } from '../../../lib/sfx-bus'
14
- import useEditor from '../../../store/use-editor'
15
- import { CursorSphere } from '../shared/cursor-sphere'
16
-
17
- const roundToHalf = (value: number) => Math.round(value * 2) / 2
18
- const worldVector = new Vector3()
19
-
20
- function getLevelLocalSpawnPosition(node: SpawnNode, event: GridEvent): [number, number, number] {
21
- const levelObject = node.parentId ? sceneRegistry.nodes.get(node.parentId) : null
22
- if (!levelObject) {
23
- return [
24
- roundToHalf(event.localPosition[0]),
25
- event.localPosition[1],
26
- roundToHalf(event.localPosition[2]),
27
- ]
28
- }
29
-
30
- worldVector.set(event.position[0], event.position[1], event.position[2])
31
- levelObject.updateWorldMatrix(true, false)
32
- levelObject.worldToLocal(worldVector)
33
-
34
- return [roundToHalf(worldVector.x), worldVector.y, roundToHalf(worldVector.z)]
35
- }
36
-
37
- export const MoveSpawnTool: React.FC<{
38
- node: SpawnNode
39
- onCommitted?: (nodeId: SpawnNode['id']) => void
40
- }> = ({ node, onCommitted }) => {
41
- const [previewPosition, setPreviewPosition] = useState<[number, number, number]>(node.position)
42
-
43
- const exitMoveMode = useCallback(() => {
44
- useEditor.getState().setMovingNode(null)
45
- }, [])
46
-
47
- useEffect(() => {
48
- useScene.temporal.getState().pause()
49
-
50
- let committed = false
51
-
52
- const onGridMove = (event: GridEvent) => {
53
- const nextPosition: [number, number, number] = [
54
- roundToHalf(event.localPosition[0]),
55
- event.localPosition[1],
56
- roundToHalf(event.localPosition[2]),
57
- ]
58
- setPreviewPosition(nextPosition)
59
- useLiveTransforms.getState().set(node.id, {
60
- position: [...nextPosition],
61
- rotation: node.rotation,
62
- })
63
- }
64
-
65
- const onGridClick = (event: GridEvent) => {
66
- const nextPosition = getLevelLocalSpawnPosition(node, event)
67
-
68
- committed = true
69
- useScene.temporal.getState().resume()
70
- useScene.getState().updateNode(node.id, { position: nextPosition })
71
- onCommitted?.(node.id)
72
- useLiveTransforms.getState().clear(node.id)
73
- sfxEmitter.emit('sfx:item-place')
74
- exitMoveMode()
75
- }
76
-
77
- const onCancel = () => {
78
- useLiveTransforms.getState().clear(node.id)
79
- useScene.temporal.getState().resume()
80
- exitMoveMode()
81
- }
82
-
83
- emitter.on('grid:move', onGridMove)
84
- emitter.on('grid:click', onGridClick)
85
- emitter.on('tool:cancel', onCancel)
86
-
87
- return () => {
88
- emitter.off('grid:move', onGridMove)
89
- emitter.off('grid:click', onGridClick)
90
- emitter.off('tool:cancel', onCancel)
91
- useLiveTransforms.getState().clear(node.id)
92
- if (!committed) {
93
- useScene.temporal.getState().resume()
94
- }
95
- }
96
- }, [exitMoveMode, node, onCommitted])
97
-
98
- return (
99
- <CursorSphere color="#60a5fa" height={2.2} position={previewPosition} showTooltip={false} />
100
- )
101
- }