@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,264 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNodeId,
5
- type CeilingNode,
6
- emitter,
7
- type GridEvent,
8
- useScene,
9
- } from '@pascal-app/core'
10
- import { useViewer } from '@pascal-app/viewer'
11
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
12
- import { BufferGeometry, DoubleSide, Path, Shape, ShapeGeometry, Vector3 } from 'three'
13
- import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
14
- import { sfxEmitter } from '../../../lib/sfx-bus'
15
- import useEditor from '../../../store/use-editor'
16
- import { CursorSphere } from '../shared/cursor-sphere'
17
-
18
- function snap(value: number) {
19
- return Math.round(value * 2) / 2
20
- }
21
-
22
- function translatePolygon(
23
- polygon: Array<[number, number]>,
24
- deltaX: number,
25
- deltaZ: number,
26
- ): Array<[number, number]> {
27
- return polygon.map(([x, z]) => [x + deltaX, z + deltaZ] as [number, number])
28
- }
29
-
30
- function getPolygonCenter(polygon: Array<[number, number]>): [number, number] {
31
- if (polygon.length === 0) return [0, 0]
32
- let sumX = 0
33
- let sumZ = 0
34
- for (const [x, z] of polygon) {
35
- sumX += x
36
- sumZ += z
37
- }
38
- return [sumX / polygon.length, sumZ / polygon.length]
39
- }
40
-
41
- export const MoveCeilingTool: React.FC<{ node: CeilingNode }> = ({ node }) => {
42
- const activatedAtRef = useRef<number>(Date.now())
43
- const originalPolygonRef = useRef(node.polygon.map(([x, z]) => [x, z] as [number, number]))
44
- const originalHolesRef = useRef(
45
- (node.holes ?? []).map((hole) => hole.map(([x, z]) => [x, z] as [number, number])),
46
- )
47
- const dragAnchorRef = useRef<[number, number] | null>(null)
48
- const previousGridPosRef = useRef<[number, number] | null>(null)
49
- const previousCursorPosRef = useRef<[number, number, number] | null>(null)
50
- const previousDeltaRef = useRef<[number, number] | null>(null)
51
- const previewRef = useRef<{
52
- polygon: Array<[number, number]>
53
- holes: Array<Array<[number, number]>>
54
- } | null>(null)
55
-
56
- const [cursorLocalPos, setCursorLocalPos] = useState<[number, number, number]>(() => {
57
- const center = getPolygonCenter(node.polygon)
58
- return [center[0], node.height ?? 2.5, center[1]]
59
- })
60
- const [previewPolygon, setPreviewPolygon] = useState<Array<[number, number]>>(node.polygon)
61
- const [previewHoles, setPreviewHoles] = useState<Array<Array<[number, number]>>>(node.holes ?? [])
62
-
63
- const exitMoveMode = useCallback(() => {
64
- useEditor.getState().setMovingNode(null)
65
- }, [])
66
-
67
- useEffect(() => {
68
- const originalPolygon = originalPolygonRef.current
69
- const originalHoles = originalHolesRef.current
70
-
71
- useScene.temporal.getState().pause()
72
- let wasCommitted = false
73
-
74
- const applyPreview = (
75
- polygon: Array<[number, number]>,
76
- holes: Array<Array<[number, number]>>,
77
- ) => {
78
- previewRef.current = { polygon, holes }
79
- setPreviewPolygon(polygon)
80
- setPreviewHoles(holes)
81
- const center = getPolygonCenter(polygon)
82
- const nextCursorPos: [number, number, number] = [center[0], node.height ?? 2.5, center[1]]
83
- if (
84
- !previousCursorPosRef.current ||
85
- previousCursorPosRef.current[0] !== nextCursorPos[0] ||
86
- previousCursorPosRef.current[1] !== nextCursorPos[1] ||
87
- previousCursorPosRef.current[2] !== nextCursorPos[2]
88
- ) {
89
- previousCursorPosRef.current = nextCursorPos
90
- setCursorLocalPos(nextCursorPos)
91
- }
92
- useScene.getState().updateNode(node.id, { polygon, holes })
93
- useScene.getState().markDirty(node.id as AnyNodeId)
94
- }
95
-
96
- const restoreOriginal = () => {
97
- setPreviewPolygon(originalPolygon)
98
- setPreviewHoles(originalHoles)
99
- useScene.getState().updateNode(node.id, {
100
- holes: originalHoles,
101
- polygon: originalPolygon,
102
- })
103
- useScene.getState().markDirty(node.id as AnyNodeId)
104
- }
105
-
106
- const onGridMove = (event: GridEvent) => {
107
- const localX = snap(event.localPosition[0])
108
- const localZ = snap(event.localPosition[2])
109
-
110
- if (
111
- previousGridPosRef.current &&
112
- (localX !== previousGridPosRef.current[0] || localZ !== previousGridPosRef.current[1])
113
- ) {
114
- sfxEmitter.emit('sfx:grid-snap')
115
- }
116
- previousGridPosRef.current = [localX, localZ]
117
-
118
- const anchor = dragAnchorRef.current ?? [localX, localZ]
119
- dragAnchorRef.current = anchor
120
-
121
- const deltaX = localX - anchor[0]
122
- const deltaZ = localZ - anchor[1]
123
-
124
- if (
125
- previousDeltaRef.current &&
126
- previousDeltaRef.current[0] === deltaX &&
127
- previousDeltaRef.current[1] === deltaZ
128
- ) {
129
- return
130
- }
131
- previousDeltaRef.current = [deltaX, deltaZ]
132
-
133
- applyPreview(
134
- translatePolygon(originalPolygon, deltaX, deltaZ),
135
- originalHoles.map((hole) => translatePolygon(hole, deltaX, deltaZ)),
136
- )
137
- }
138
-
139
- const onGridClick = (event: GridEvent) => {
140
- if (Date.now() - activatedAtRef.current < 150) {
141
- event.nativeEvent?.stopPropagation?.()
142
- return
143
- }
144
-
145
- const preview = previewRef.current ?? { polygon: originalPolygon, holes: originalHoles }
146
-
147
- wasCommitted = true
148
-
149
- // Restore original baseline while paused so the next resume+update
150
- // registers as a single tracked change (undo reverts to original).
151
- useScene.getState().updateNode(node.id, {
152
- polygon: originalPolygon,
153
- holes: originalHoles,
154
- })
155
-
156
- useScene.temporal.getState().resume()
157
- useScene.getState().updateNode(node.id, preview)
158
- useScene.getState().markDirty(node.id as AnyNodeId)
159
- useScene.temporal.getState().pause()
160
-
161
- sfxEmitter.emit('sfx:item-place')
162
- useViewer.getState().setSelection({ selectedIds: [node.id] })
163
- exitMoveMode()
164
- event.nativeEvent?.stopPropagation?.()
165
- }
166
-
167
- const onCancel = () => {
168
- restoreOriginal()
169
- useViewer.getState().setSelection({ selectedIds: [node.id] })
170
- useScene.temporal.getState().resume()
171
- markToolCancelConsumed()
172
- exitMoveMode()
173
- }
174
-
175
- emitter.on('grid:move', onGridMove)
176
- emitter.on('grid:click', onGridClick)
177
- emitter.on('tool:cancel', onCancel)
178
-
179
- return () => {
180
- if (!wasCommitted) {
181
- restoreOriginal()
182
- }
183
- useScene.temporal.getState().resume()
184
- emitter.off('grid:move', onGridMove)
185
- emitter.off('grid:click', onGridClick)
186
- emitter.off('tool:cancel', onCancel)
187
- }
188
- }, [exitMoveMode, node.height, node.id])
189
-
190
- const previewFillGeometry = useMemo(
191
- () => createCeilingPreviewGeometry(previewPolygon, previewHoles),
192
- [previewHoles, previewPolygon],
193
- )
194
-
195
- const previewOutlineGeometry = useMemo(
196
- () => createCeilingOutlineGeometry(previewPolygon),
197
- [previewPolygon],
198
- )
199
-
200
- return (
201
- <group>
202
- <mesh geometry={previewFillGeometry} position={[0, (node.height ?? 2.5) + 0.012, 0]}>
203
- <meshBasicMaterial
204
- color="#f5f5f4"
205
- depthWrite={false}
206
- opacity={0.3}
207
- side={DoubleSide}
208
- transparent
209
- />
210
- </mesh>
211
- {/* @ts-ignore */}
212
- <line geometry={previewOutlineGeometry} position={[0, (node.height ?? 2.5) + 0.02, 0]}>
213
- <lineBasicMaterial color="#ffffff" depthWrite={false} opacity={0.95} transparent />
214
- </line>
215
- <CursorSphere position={cursorLocalPos} showTooltip={false} />
216
- </group>
217
- )
218
- }
219
-
220
- function createCeilingPreviewGeometry(
221
- polygon: Array<[number, number]>,
222
- holes: Array<Array<[number, number]>>,
223
- ): BufferGeometry {
224
- if (polygon.length < 3) return new BufferGeometry()
225
-
226
- const shape = new Shape()
227
- const [firstX, firstZ] = polygon[0]!
228
- shape.moveTo(firstX, -firstZ)
229
-
230
- for (let i = 1; i < polygon.length; i++) {
231
- const [x, z] = polygon[i]!
232
- shape.lineTo(x, -z)
233
- }
234
- shape.closePath()
235
-
236
- for (const holePolygon of holes) {
237
- if (holePolygon.length < 3) continue
238
- const hole = new Path()
239
- const [hx, hz] = holePolygon[0]!
240
- hole.moveTo(hx, -hz)
241
- for (let i = 1; i < holePolygon.length; i++) {
242
- const [x, z] = holePolygon[i]!
243
- hole.lineTo(x, -z)
244
- }
245
- hole.closePath()
246
- shape.holes.push(hole)
247
- }
248
-
249
- const geometry = new ShapeGeometry(shape)
250
- geometry.rotateX(-Math.PI / 2)
251
- geometry.computeVertexNormals()
252
- return geometry
253
- }
254
-
255
- function createCeilingOutlineGeometry(polygon: Array<[number, number]>): BufferGeometry {
256
- const geometry = new BufferGeometry()
257
- if (polygon.length < 2) return geometry
258
-
259
- const points = polygon.map(([x, z]) => new Vector3(x, 0, z))
260
- const [firstX, firstZ] = polygon[0]!
261
- points.push(new Vector3(firstX, 0, firstZ))
262
- geometry.setFromPoints(points)
263
- return geometry
264
- }
@@ -1,97 +0,0 @@
1
- import '../../../three-types'
2
-
3
- import {
4
- COLUMN_PRESETS,
5
- ColumnNode,
6
- type ColumnNode as ColumnNodeType,
7
- type ColumnPresetId,
8
- emitter,
9
- type GridEvent,
10
- type LevelNode,
11
- useScene,
12
- } from '@pascal-app/core'
13
- import { useEffect, useRef, useState } from 'react'
14
- import type { Group } from 'three'
15
- import { sfxEmitter } from '../../../lib/sfx-bus'
16
- import useEditor from '../../../store/use-editor'
17
- import { CursorSphere } from '../shared/cursor-sphere'
18
-
19
- const COLUMN_ICON = (
20
- // eslint-disable-next-line @next/next/no-img-element
21
- <img
22
- alt="Column"
23
- src="/icons/column.png"
24
- style={{ width: '100%', height: '100%', objectFit: 'contain' }}
25
- />
26
- )
27
-
28
- const roundToHalf = (value: number) => Math.round(value * 2) / 2
29
- const DEFAULT_COLUMN_PRESET_ID = 'basicPillar' satisfies ColumnPresetId
30
-
31
- function createColumnFromPreset(presetId: ColumnPresetId, position: [number, number, number]) {
32
- const { label, ...preset } = COLUMN_PRESETS[presetId]
33
- return ColumnNode.parse({
34
- name: label,
35
- position,
36
- rotation: 0,
37
- ...preset,
38
- })
39
- }
40
-
41
- type ColumnToolProps = {
42
- currentLevelId: LevelNode['id'] | null
43
- onPlaced?: (nodeId: ColumnNodeType['id']) => void
44
- }
45
-
46
- export const ColumnTool: React.FC<ColumnToolProps> = ({ currentLevelId, onPlaced }) => {
47
- const [, setCursorPosition] = useState<[number, number, number] | null>(null)
48
- const cursorRef = useRef<Group>(null)
49
-
50
- useEffect(() => {
51
- if (!currentLevelId) return
52
-
53
- const onGridMove = (event: GridEvent) => {
54
- const nextPosition: [number, number, number] = [
55
- roundToHalf(event.localPosition[0]),
56
- 0,
57
- roundToHalf(event.localPosition[2]),
58
- ]
59
- setCursorPosition(nextPosition)
60
- cursorRef.current?.position.set(nextPosition[0], event.localPosition[1], nextPosition[2])
61
- }
62
-
63
- const onGridClick = (event: GridEvent) => {
64
- const position: [number, number, number] = [
65
- roundToHalf(event.localPosition[0]),
66
- 0,
67
- roundToHalf(event.localPosition[2]),
68
- ]
69
- const column = createColumnFromPreset(DEFAULT_COLUMN_PRESET_ID, position)
70
- useScene.getState().createNode(column, currentLevelId)
71
- onPlaced?.(column.id)
72
- sfxEmitter.emit('sfx:structure-build')
73
- useEditor.getState().setTool(null)
74
- useEditor.getState().setMode('select')
75
- }
76
-
77
- emitter.on('grid:move', onGridMove)
78
- emitter.on('grid:click', onGridClick)
79
-
80
- return () => {
81
- emitter.off('grid:move', onGridMove)
82
- emitter.off('grid:click', onGridClick)
83
- }
84
- }, [currentLevelId, onPlaced])
85
-
86
- if (!currentLevelId) return null
87
-
88
- return (
89
- <CursorSphere
90
- color="#a78bfa"
91
- height={2.8}
92
- ref={cursorRef}
93
- showTooltip
94
- tooltipContent={COLUMN_ICON}
95
- />
96
- )
97
- }
@@ -1,105 +0,0 @@
1
- import '../../../three-types'
2
-
3
- import {
4
- type AnyNodeId,
5
- ColumnNode,
6
- type ColumnNode as ColumnNodeType,
7
- emitter,
8
- type GridEvent,
9
- sceneRegistry,
10
- useLiveTransforms,
11
- useScene,
12
- } from '@pascal-app/core'
13
- import { useCallback, useEffect, useState } from 'react'
14
- import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
15
- import { sfxEmitter } from '../../../lib/sfx-bus'
16
- import useEditor from '../../../store/use-editor'
17
- import { CursorSphere } from '../shared/cursor-sphere'
18
-
19
- const roundToHalf = (value: number) => Math.round(value * 2) / 2
20
-
21
- export function MoveColumnTool({ node }: { node: ColumnNodeType }) {
22
- const [previewPosition, setPreviewPosition] = useState<[number, number, number]>(node.position)
23
-
24
- const exitMoveMode = useCallback(() => {
25
- useEditor.getState().setMovingNode(null)
26
- }, [])
27
-
28
- useEffect(() => {
29
- useScene.temporal.getState().pause()
30
- let committed = false
31
-
32
- const applyPreview = (position: [number, number, number]) => {
33
- setPreviewPosition(position)
34
- useLiveTransforms.getState().set(node.id, {
35
- position,
36
- rotation: node.rotation,
37
- })
38
- sceneRegistry.nodes.get(node.id)?.position.set(position[0], position[1], position[2])
39
- }
40
-
41
- const onGridMove = (event: GridEvent) => {
42
- applyPreview([roundToHalf(event.localPosition[0]), 0, roundToHalf(event.localPosition[2])])
43
- }
44
-
45
- const onGridClick = (event: GridEvent) => {
46
- const position: [number, number, number] = [
47
- roundToHalf(event.localPosition[0]),
48
- 0,
49
- roundToHalf(event.localPosition[2]),
50
- ]
51
- const nodeId = (node as { id?: ColumnNodeType['id'] }).id
52
-
53
- if (nodeId && useScene.getState().nodes[nodeId]) {
54
- committed = true
55
- useLiveTransforms.getState().clear(nodeId)
56
- useScene.temporal.getState().resume()
57
- useScene.getState().updateNode(nodeId, { position })
58
- } else if (node.parentId) {
59
- const column = ColumnNode.parse({
60
- ...node,
61
- id: undefined,
62
- metadata: {},
63
- position,
64
- })
65
- committed = true
66
- useScene.temporal.getState().resume()
67
- useScene.getState().createNode(column, node.parentId as AnyNodeId)
68
- }
69
-
70
- useLiveTransforms.getState().clear(node.id)
71
- sfxEmitter.emit('sfx:item-place')
72
- exitMoveMode()
73
- event.nativeEvent?.stopPropagation?.()
74
- }
75
-
76
- const onCancel = () => {
77
- useLiveTransforms.getState().clear(node.id)
78
- sceneRegistry.nodes
79
- .get(node.id)
80
- ?.position.set(node.position[0], node.position[1], node.position[2])
81
- useScene.temporal.getState().resume()
82
- markToolCancelConsumed()
83
- exitMoveMode()
84
- }
85
-
86
- emitter.on('grid:move', onGridMove)
87
- emitter.on('grid:click', onGridClick)
88
- emitter.on('tool:cancel', onCancel)
89
-
90
- return () => {
91
- emitter.off('grid:move', onGridMove)
92
- emitter.off('grid:click', onGridClick)
93
- emitter.off('tool:cancel', onCancel)
94
- useLiveTransforms.getState().clear(node.id)
95
- if (!committed) {
96
- sceneRegistry.nodes
97
- .get(node.id)
98
- ?.position.set(node.position[0], node.position[1], node.position[2])
99
- useScene.temporal.getState().resume()
100
- }
101
- }
102
- }, [exitMoveMode, node])
103
-
104
- return <CursorSphere color="#a78bfa" height={node.height} position={previewPosition} />
105
- }
@@ -1,110 +0,0 @@
1
- import {
2
- type AnyNodeId,
3
- type DoorNode,
4
- getScaledDimensions,
5
- type ItemNode,
6
- useScene,
7
- type WallNode,
8
- type WindowNode,
9
- } from '@pascal-app/core'
10
-
11
- /**
12
- * Converts wall-local (X along wall, Y = height above wall base) to world XYZ.
13
- */
14
- export function wallLocalToWorld(
15
- wallNode: WallNode,
16
- localX: number,
17
- localY: number,
18
- levelYOffset = 0,
19
- slabElevation = 0,
20
- ): [number, number, number] {
21
- const wallAngle = Math.atan2(
22
- wallNode.end[1] - wallNode.start[1],
23
- wallNode.end[0] - wallNode.start[0],
24
- )
25
- return [
26
- wallNode.start[0] + localX * Math.cos(wallAngle),
27
- slabElevation + localY + levelYOffset,
28
- wallNode.start[1] + localX * Math.sin(wallAngle),
29
- ]
30
- }
31
-
32
- /**
33
- * Clamps door center X so it stays fully within wall bounds.
34
- * Y is always height/2 — doors sit at floor level.
35
- */
36
- export function clampToWall(
37
- wallNode: WallNode,
38
- localX: number,
39
- width: number,
40
- height: number,
41
- ): { clampedX: number; clampedY: number } {
42
- const dx = wallNode.end[0] - wallNode.start[0]
43
- const dz = wallNode.end[1] - wallNode.start[1]
44
- const wallLength = Math.sqrt(dx * dx + dz * dz)
45
-
46
- const clampedX = Math.max(width / 2, Math.min(wallLength - width / 2, localX))
47
- const clampedY = height / 2 // Doors always sit at floor level
48
- return { clampedX, clampedY }
49
- }
50
-
51
- /**
52
- * Checks if a proposed door position overlaps any existing wall children.
53
- * Handles item, window, and door types.
54
- */
55
- export function hasWallChildOverlap(
56
- wallId: string,
57
- clampedX: number,
58
- clampedY: number,
59
- width: number,
60
- height: number,
61
- ignoreId?: string,
62
- ): boolean {
63
- const nodes = useScene.getState().nodes
64
- const wallNode = nodes[wallId as AnyNodeId] as WallNode | undefined
65
- if (!wallNode) return true
66
- const halfW = width / 2
67
- const halfH = height / 2
68
- const newBottom = clampedY - halfH
69
- const newTop = clampedY + halfH
70
- const newLeft = clampedX - halfW
71
- const newRight = clampedX + halfW
72
-
73
- for (const childId of Array.isArray(wallNode.children) ? wallNode.children : []) {
74
- if (childId === ignoreId) continue
75
- const child = nodes[childId as AnyNodeId]
76
- if (!child) continue
77
-
78
- let childLeft: number, childRight: number, childBottom: number, childTop: number
79
-
80
- if (child.type === 'item') {
81
- const item = child as ItemNode
82
- if (item.asset.attachTo !== 'wall' && item.asset.attachTo !== 'wall-side') continue
83
- const [w, h] = getScaledDimensions(item)
84
- childLeft = item.position[0] - w / 2
85
- childRight = item.position[0] + w / 2
86
- childBottom = item.position[1]
87
- childTop = item.position[1] + h
88
- } else if (child.type === 'window') {
89
- const win = child as WindowNode
90
- childLeft = win.position[0] - win.width / 2
91
- childRight = win.position[0] + win.width / 2
92
- childBottom = win.position[1] - win.height / 2
93
- childTop = win.position[1] + win.height / 2
94
- } else if (child.type === 'door') {
95
- const door = child as DoorNode
96
- childLeft = door.position[0] - door.width / 2
97
- childRight = door.position[0] + door.width / 2
98
- childBottom = door.position[1] - door.height / 2
99
- childTop = door.position[1] + door.height / 2
100
- } else {
101
- continue
102
- }
103
-
104
- const xOverlap = newLeft < childRight && newRight > childLeft
105
- const yOverlap = newBottom < childTop && newTop > childBottom
106
- if (xOverlap && yOverlap) return true
107
- }
108
-
109
- return false
110
- }