@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,465 +0,0 @@
1
- import { CeilingNode, emitter, type GridEvent, type LevelNode, 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 { mix, positionLocal } from 'three/tsl'
6
- import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
7
- import { EDITOR_LAYER } from '../../../lib/constants'
8
- import { sfxEmitter } from '../../../lib/sfx-bus'
9
- import { CursorSphere } from '../shared/cursor-sphere'
10
-
11
- const CEILING_HEIGHT = 2.52
12
- const GRID_OFFSET = 0.02
13
-
14
- /**
15
- * Snaps a point to the nearest axis-aligned or 45-degree diagonal from the last point
16
- */
17
- const calculateSnapPoint = (
18
- lastPoint: [number, number],
19
- currentPoint: [number, number],
20
- ): [number, number] => {
21
- const [x1, y1] = lastPoint
22
- const [x, y] = currentPoint
23
-
24
- const dx = x - x1
25
- const dy = y - y1
26
- const absDx = Math.abs(dx)
27
- const absDy = Math.abs(dy)
28
-
29
- // Calculate distances to horizontal, vertical, and diagonal lines
30
- const horizontalDist = absDy
31
- const verticalDist = absDx
32
- const diagonalDist = Math.abs(absDx - absDy)
33
-
34
- // Find the minimum distance to determine which axis to snap to
35
- const minDist = Math.min(horizontalDist, verticalDist, diagonalDist)
36
-
37
- if (minDist === diagonalDist) {
38
- // Snap to 45° diagonal
39
- const diagonalLength = Math.min(absDx, absDy)
40
- return [x1 + Math.sign(dx) * diagonalLength, y1 + Math.sign(dy) * diagonalLength]
41
- }
42
- if (minDist === horizontalDist) {
43
- // Snap to horizontal
44
- return [x, y1]
45
- }
46
- // Snap to vertical
47
- return [x1, y]
48
- }
49
-
50
- /**
51
- * Creates a ceiling with the given polygon points and returns its ID
52
- */
53
- const commitCeilingDrawing = (
54
- levelId: LevelNode['id'],
55
- points: Array<[number, number]>,
56
- ): string => {
57
- const { createNode, nodes } = useScene.getState()
58
-
59
- // Count existing ceilings for naming
60
- const ceilingCount = Object.values(nodes).filter((n) => n.type === 'ceiling').length
61
- const name = `Ceiling ${ceilingCount + 1}`
62
-
63
- const ceiling = CeilingNode.parse({
64
- name,
65
- polygon: points,
66
- })
67
-
68
- createNode(ceiling, levelId)
69
- sfxEmitter.emit('sfx:structure-build')
70
- return ceiling.id
71
- }
72
-
73
- export const CeilingTool: React.FC = () => {
74
- const cursorRef = useRef<Group>(null)
75
- const gridCursorRef = useRef<Group>(null)
76
- const mainLineRef = useRef<Line>(null!)
77
- const closingLineRef = useRef<Line>(null!)
78
- const groundMainLineRef = useRef<Line>(null!)
79
- const groundClosingLineRef = useRef<Line>(null!)
80
- const verticalLineRef = useRef<Line>(null!)
81
- const currentLevelId = useViewer((state) => state.selection.levelId)
82
- const setSelection = useViewer((state) => state.setSelection)
83
-
84
- const [points, setPoints] = useState<Array<[number, number]>>([])
85
- const [cursorPosition, setCursorPosition] = useState<[number, number]>([0, 0])
86
- const [snappedCursorPosition, setSnappedCursorPosition] = useState<[number, number]>([0, 0])
87
- const [levelY, setLevelY] = useState(0)
88
- const previousSnappedPointRef = useRef<[number, number] | null>(null)
89
- const shiftPressed = useRef(false)
90
-
91
- // Static geometry: local y goes 0 (grid) → H (ceiling), mesh is positioned at gridY
92
- const verticalGeo = useMemo(
93
- () =>
94
- new BufferGeometry().setFromPoints([
95
- new Vector3(0, 0, 0),
96
- new Vector3(0, CEILING_HEIGHT - GRID_OFFSET, 0),
97
- ]),
98
- [],
99
- )
100
-
101
- // opacityNode: positionLocal.y is 0 at grid, H at ceiling → fade from 0.6 to 0
102
- const gradientOpacityNode = useMemo(
103
- () => mix(0.6, 0.0, positionLocal.y.div(CEILING_HEIGHT - GRID_OFFSET).clamp()),
104
- [],
105
- )
106
-
107
- // Update cursor position and lines on grid move
108
- useEffect(() => {
109
- if (!currentLevelId) return
110
-
111
- const onGridMove = (event: GridEvent) => {
112
- if (!(cursorRef.current && gridCursorRef.current)) return
113
-
114
- const gridX = Math.round(event.localPosition[0] * 2) / 2
115
- const gridZ = Math.round(event.localPosition[2] * 2) / 2
116
- const gridPosition: [number, number] = [gridX, gridZ]
117
-
118
- setCursorPosition(gridPosition)
119
- setLevelY(event.localPosition[1])
120
-
121
- const ceilingY = event.localPosition[1] + CEILING_HEIGHT
122
- const gridY = event.localPosition[1] + GRID_OFFSET
123
-
124
- // Calculate snapped display position (bypass snap when Shift is held)
125
- const lastPoint = points[points.length - 1]
126
- const displayPoint =
127
- shiftPressed.current || !lastPoint
128
- ? gridPosition
129
- : calculateSnapPoint(lastPoint, gridPosition)
130
- setSnappedCursorPosition(displayPoint)
131
-
132
- // Play snap sound when the snapped position actually changes (only when drawing)
133
- if (
134
- points.length > 0 &&
135
- previousSnappedPointRef.current &&
136
- (displayPoint[0] !== previousSnappedPointRef.current[0] ||
137
- displayPoint[1] !== previousSnappedPointRef.current[1])
138
- ) {
139
- sfxEmitter.emit('sfx:grid-snap')
140
- }
141
-
142
- previousSnappedPointRef.current = displayPoint
143
- cursorRef.current.position.set(displayPoint[0], ceilingY, displayPoint[1])
144
- gridCursorRef.current.position.set(displayPoint[0], gridY, displayPoint[1])
145
-
146
- if (verticalLineRef.current) {
147
- verticalLineRef.current.position.set(displayPoint[0], gridY, displayPoint[1])
148
- }
149
- }
150
-
151
- const onGridClick = (_event: GridEvent) => {
152
- if (!currentLevelId) return
153
-
154
- // Use the last displayed snapped position (respects Shift state from onGridMove)
155
- const clickPoint = previousSnappedPointRef.current ?? cursorPosition
156
-
157
- // Check if clicking on the first point to close the shape
158
- const firstPoint = points[0]
159
- if (
160
- points.length >= 3 &&
161
- firstPoint &&
162
- Math.abs(clickPoint[0] - firstPoint[0]) < 0.25 &&
163
- Math.abs(clickPoint[1] - firstPoint[1]) < 0.25
164
- ) {
165
- // Create the ceiling and select it
166
- const ceilingId = commitCeilingDrawing(currentLevelId, points)
167
- setSelection({ selectedIds: [ceilingId] })
168
- setPoints([])
169
- } else {
170
- // Add point to polygon
171
- setPoints([...points, clickPoint])
172
- }
173
- }
174
-
175
- const onGridDoubleClick = (_event: GridEvent) => {
176
- if (!currentLevelId) return
177
-
178
- // Need at least 3 points to form a polygon
179
- if (points.length >= 3) {
180
- const ceilingId = commitCeilingDrawing(currentLevelId, points)
181
- setSelection({ selectedIds: [ceilingId] })
182
- setPoints([])
183
- }
184
- }
185
-
186
- const onCancel = () => {
187
- if (points.length > 0) markToolCancelConsumed()
188
- setPoints([])
189
- }
190
-
191
- const onKeyDown = (e: KeyboardEvent) => {
192
- if (e.key === 'Shift') shiftPressed.current = true
193
- }
194
- const onKeyUp = (e: KeyboardEvent) => {
195
- if (e.key === 'Shift') shiftPressed.current = false
196
- }
197
- document.addEventListener('keydown', onKeyDown)
198
- document.addEventListener('keyup', onKeyUp)
199
-
200
- emitter.on('grid:move', onGridMove)
201
- emitter.on('grid:click', onGridClick)
202
- emitter.on('grid:double-click', onGridDoubleClick)
203
- emitter.on('tool:cancel', onCancel)
204
-
205
- return () => {
206
- document.removeEventListener('keydown', onKeyDown)
207
- document.removeEventListener('keyup', onKeyUp)
208
- emitter.off('grid:move', onGridMove)
209
- emitter.off('grid:click', onGridClick)
210
- emitter.off('grid:double-click', onGridDoubleClick)
211
- emitter.off('tool:cancel', onCancel)
212
- }
213
- }, [currentLevelId, points, cursorPosition, setSelection])
214
-
215
- // Update line geometries when points change
216
- useEffect(() => {
217
- if (!(mainLineRef.current && closingLineRef.current)) return
218
-
219
- if (points.length === 0) {
220
- mainLineRef.current.visible = false
221
- closingLineRef.current.visible = false
222
- return
223
- }
224
-
225
- const ceilingY = levelY + CEILING_HEIGHT
226
- const snappedCursor = snappedCursorPosition
227
-
228
- // Build main line points
229
- const linePoints: Vector3[] = points.map(([x, z]) => new Vector3(x, ceilingY, z))
230
- linePoints.push(new Vector3(snappedCursor[0], ceilingY, snappedCursor[1]))
231
-
232
- const gridY = levelY + GRID_OFFSET
233
- const groundLinePoints: Vector3[] = points.map(([x, z]) => new Vector3(x, gridY, z))
234
- groundLinePoints.push(new Vector3(snappedCursor[0], gridY, snappedCursor[1]))
235
-
236
- // Update main line
237
- if (linePoints.length >= 2) {
238
- mainLineRef.current.geometry.dispose()
239
- mainLineRef.current.geometry = new BufferGeometry().setFromPoints(linePoints)
240
- mainLineRef.current.visible = true
241
-
242
- groundMainLineRef.current.geometry.dispose()
243
- groundMainLineRef.current.geometry = new BufferGeometry().setFromPoints(groundLinePoints)
244
- groundMainLineRef.current.visible = true
245
- } else {
246
- mainLineRef.current.visible = false
247
- groundMainLineRef.current.visible = false
248
- }
249
-
250
- // Update closing line (from cursor back to first point)
251
- const firstPoint = points[0]
252
- if (points.length >= 2 && firstPoint) {
253
- const closingPoints = [
254
- new Vector3(snappedCursor[0], ceilingY, snappedCursor[1]),
255
- new Vector3(firstPoint[0], ceilingY, firstPoint[1]),
256
- ]
257
- closingLineRef.current.geometry.dispose()
258
- closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints)
259
- closingLineRef.current.visible = true
260
-
261
- const groundClosingPoints = [
262
- new Vector3(snappedCursor[0], gridY, snappedCursor[1]),
263
- new Vector3(firstPoint[0], gridY, firstPoint[1]),
264
- ]
265
- groundClosingLineRef.current.geometry.dispose()
266
- groundClosingLineRef.current.geometry = new BufferGeometry().setFromPoints(
267
- groundClosingPoints,
268
- )
269
- groundClosingLineRef.current.visible = true
270
- } else {
271
- closingLineRef.current.visible = false
272
- groundClosingLineRef.current.visible = false
273
- }
274
- }, [points, snappedCursorPosition, levelY])
275
-
276
- // Create preview shape when we have 3+ points
277
- const previewShape = useMemo(() => {
278
- if (points.length < 3) return null
279
-
280
- const snappedCursor = snappedCursorPosition
281
-
282
- const allPoints = [...points, snappedCursor]
283
-
284
- // THREE.Shape is in X-Y plane. After rotation of -PI/2 around X:
285
- // - Shape X -> World X
286
- // - Shape Y -> World -Z (so we negate Z to get correct orientation)
287
- const firstPt = allPoints[0]
288
- if (!firstPt) return null
289
-
290
- const shape = new Shape()
291
- shape.moveTo(firstPt[0], -firstPt[1])
292
-
293
- for (let i = 1; i < allPoints.length; i++) {
294
- const pt = allPoints[i]
295
- if (pt) {
296
- shape.lineTo(pt[0], -pt[1])
297
- }
298
- }
299
- shape.closePath()
300
-
301
- return shape
302
- }, [points, snappedCursorPosition])
303
-
304
- return (
305
- <group>
306
- {/* Cursor at ceiling height */}
307
- <CursorSphere ref={cursorRef} />
308
-
309
- {/* Grid-level cursor indicator */}
310
- <mesh
311
- layers={EDITOR_LAYER}
312
- ref={gridCursorRef}
313
- renderOrder={2}
314
- rotation={[-Math.PI / 2, 0, 0]}
315
- >
316
- <ringGeometry args={[0.15, 0.2, 32]} />
317
- <meshBasicMaterial
318
- color="#818cf8"
319
- depthTest={false}
320
- depthWrite={true}
321
- opacity={0.5}
322
- side={DoubleSide}
323
- transparent
324
- />
325
- </mesh>
326
-
327
- {/* Vertical connector: local y=0 at grid, y=H at ceiling; position.y set to gridY on move */}
328
- {/* @ts-ignore */}
329
- <line geometry={verticalGeo} layers={EDITOR_LAYER} ref={verticalLineRef} renderOrder={1}>
330
- <lineBasicNodeMaterial
331
- color="#818cf8"
332
- depthTest={false}
333
- depthWrite={false}
334
- opacityNode={gradientOpacityNode}
335
- transparent
336
- />
337
- </line>
338
-
339
- {/* Preview fill (Top) */}
340
- {previewShape && (
341
- <mesh
342
- frustumCulled={false}
343
- layers={EDITOR_LAYER}
344
- position={[0, levelY + CEILING_HEIGHT, 0]}
345
- rotation={[-Math.PI / 2, 0, 0]}
346
- >
347
- <shapeGeometry args={[previewShape]} />
348
- <meshBasicMaterial
349
- color="#818cf8"
350
- depthTest={false}
351
- opacity={0.15}
352
- side={DoubleSide}
353
- transparent
354
- />
355
- </mesh>
356
- )}
357
-
358
- {/* Preview fill (Ground) */}
359
- {previewShape && (
360
- <mesh
361
- frustumCulled={false}
362
- layers={EDITOR_LAYER}
363
- position={[0, levelY + GRID_OFFSET, 0]}
364
- rotation={[-Math.PI / 2, 0, 0]}
365
- >
366
- <shapeGeometry args={[previewShape]} />
367
- <meshBasicMaterial
368
- color="#818cf8"
369
- depthTest={false}
370
- opacity={0.1}
371
- side={DoubleSide}
372
- transparent
373
- />
374
- </mesh>
375
- )}
376
-
377
- {/* Main line */}
378
- {/* @ts-ignore */}
379
- <line
380
- frustumCulled={false}
381
- layers={EDITOR_LAYER}
382
- // @ts-expect-error
383
- ref={mainLineRef}
384
- renderOrder={1}
385
- visible={false}
386
- >
387
- <bufferGeometry />
388
- <lineBasicNodeMaterial color="#818cf8" depthTest={false} depthWrite={false} linewidth={3} />
389
- </line>
390
-
391
- {/* Closing line */}
392
- {/* @ts-ignore */}
393
- <line
394
- frustumCulled={false}
395
- layers={EDITOR_LAYER}
396
- // @ts-expect-error
397
- ref={closingLineRef}
398
- renderOrder={1}
399
- visible={false}
400
- >
401
- <bufferGeometry />
402
- <lineBasicNodeMaterial
403
- color="#818cf8"
404
- depthTest={false}
405
- depthWrite={false}
406
- linewidth={2}
407
- opacity={0.5}
408
- transparent
409
- />
410
- </line>
411
-
412
- {/* Ground main line */}
413
- {/* @ts-ignore */}
414
- <line
415
- frustumCulled={false}
416
- layers={EDITOR_LAYER}
417
- // @ts-expect-error
418
- ref={groundMainLineRef}
419
- renderOrder={1}
420
- visible={false}
421
- >
422
- <bufferGeometry />
423
- <lineBasicNodeMaterial
424
- color="#818cf8"
425
- depthTest={false}
426
- depthWrite={false}
427
- linewidth={3}
428
- opacity={0.3}
429
- transparent
430
- />
431
- </line>
432
-
433
- {/* Ground closing line */}
434
- {/* @ts-ignore */}
435
- <line
436
- frustumCulled={false}
437
- layers={EDITOR_LAYER}
438
- // @ts-expect-error
439
- ref={groundClosingLineRef}
440
- renderOrder={1}
441
- visible={false}
442
- >
443
- <bufferGeometry />
444
- <lineBasicNodeMaterial
445
- color="#818cf8"
446
- depthTest={false}
447
- depthWrite={false}
448
- linewidth={2}
449
- opacity={0.15}
450
- transparent
451
- />
452
- </line>
453
-
454
- {/* Point markers */}
455
- {points.map(([x, z], index) => (
456
- <CursorSphere
457
- color="#818cf8"
458
- key={index}
459
- position={[x, levelY + CEILING_HEIGHT + 0.01, z]}
460
- showTooltip={false}
461
- />
462
- ))}
463
- </group>
464
- )
465
- }