@pascal-app/editor 0.7.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 (244) hide show
  1. package/package.json +12 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +755 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +62 -54
  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 +203 -87
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
  10. package/src/components/editor/first-person-controls.tsx +836 -31
  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 +5837 -12006
  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 +234 -190
  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 +435 -158
  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 +539 -0
  28. package/src/components/editor/thumbnail-generator.tsx +27 -64
  29. package/src/components/editor/use-floorplan-background-placement.ts +122 -35
  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-draft-layer.tsx +6 -1
  45. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  46. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
  47. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  49. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
  50. package/src/components/editor-2d/svg-paths.ts +10 -2
  51. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
  52. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  53. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  54. package/src/components/systems/stair/stair-edit-system.tsx +2 -2
  55. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  56. package/src/components/systems/zone/zone-system.tsx +0 -0
  57. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  58. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  59. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  60. package/src/components/tools/fence/fence-drafting.ts +64 -22
  61. package/src/components/tools/item/move-tool.tsx +40 -107
  62. package/src/components/tools/item/placement-math.test.ts +10 -0
  63. package/src/components/tools/item/placement-math.ts +33 -9
  64. package/src/components/tools/item/placement-strategies.ts +201 -25
  65. package/src/components/tools/item/placement-types.ts +10 -1
  66. package/src/components/tools/item/use-draft-node.ts +20 -2
  67. package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
  68. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  69. package/src/components/tools/roof/roof-tool.tsx +108 -12
  70. package/src/components/tools/select/box-select-state.ts +77 -0
  71. package/src/components/tools/select/box-select-tool.tsx +324 -543
  72. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  73. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  74. package/src/components/tools/select/select-candidates.test.ts +109 -0
  75. package/src/components/tools/select/select-candidates.ts +85 -0
  76. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  77. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  78. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  79. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  80. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  81. package/src/components/tools/shared/placement-box.tsx +123 -0
  82. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  83. package/src/components/tools/shared/segment-angle.ts +57 -1
  84. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  85. package/src/components/tools/site/site-flag-model.ts +18 -0
  86. package/src/components/tools/stair/stair-defaults.ts +1 -1
  87. package/src/components/tools/stair/stair-tool.tsx +269 -35
  88. package/src/components/tools/tool-manager.tsx +172 -56
  89. package/src/components/tools/wall/wall-drafting.ts +98 -98
  90. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  91. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  92. package/src/components/tools/zone/zone-tool.tsx +36 -11
  93. package/src/components/ui/action-menu/action-button.tsx +21 -1
  94. package/src/components/ui/action-menu/camera-actions.tsx +0 -0
  95. package/src/components/ui/action-menu/control-modes.tsx +5 -76
  96. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  97. package/src/components/ui/action-menu/index.tsx +33 -196
  98. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  99. package/src/components/ui/action-menu/view-toggles.tsx +262 -37
  100. package/src/components/ui/command-palette/editor-commands.tsx +24 -17
  101. package/src/components/ui/command-palette/index.tsx +6 -256
  102. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  103. package/src/components/ui/controls/material-picker.tsx +17 -35
  104. package/src/components/ui/controls/metric-control.tsx +33 -11
  105. package/src/components/ui/floating-level-selector.tsx +48 -11
  106. package/src/components/ui/helpers/helper-manager.tsx +21 -17
  107. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  108. package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
  109. package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
  110. package/src/components/ui/level-duplicate-dialog.tsx +5 -6
  111. package/src/components/ui/panels/node-display.ts +1 -0
  112. package/src/components/ui/panels/paint-panel.tsx +66 -41
  113. package/src/components/ui/panels/panel-manager.tsx +33 -74
  114. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  115. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  116. package/src/components/ui/panels/reference-panel.tsx +28 -13
  117. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  118. package/src/components/ui/primitives/number-input.tsx +1 -1
  119. package/src/components/ui/primitives/sidebar.tsx +0 -0
  120. package/src/components/ui/scene-loader.tsx +1 -3
  121. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  122. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  123. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  124. package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  127. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  128. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  129. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  132. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  133. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  134. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  135. package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
  136. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  137. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  138. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  139. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  140. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  141. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  142. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  143. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
  144. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
  145. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
  146. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  147. package/src/components/ui/slider.tsx +1 -1
  148. package/src/components/viewer-overlay.tsx +169 -52
  149. package/src/components/viewer-zone-system.tsx +1 -1
  150. package/src/hooks/use-auto-save.ts +24 -3
  151. package/src/hooks/use-ceiling-events.ts +175 -0
  152. package/src/hooks/use-drag-action.ts +124 -0
  153. package/src/hooks/use-keyboard.ts +125 -62
  154. package/src/hooks/use-selection.ts +64 -0
  155. package/src/index.tsx +243 -4
  156. package/src/lib/constants.ts +6 -2
  157. package/src/lib/editor-api.ts +62 -0
  158. package/src/lib/elevator-support.ts +96 -0
  159. package/src/lib/floorplan/apply-alignment.ts +111 -0
  160. package/src/lib/floorplan/index.ts +7 -0
  161. package/src/lib/floorplan/selection-tool.ts +40 -0
  162. package/src/lib/floorplan/stairs.ts +12 -1
  163. package/src/lib/fresh-planar-placement.test.ts +102 -0
  164. package/src/lib/fresh-planar-placement.ts +63 -0
  165. package/src/lib/history.ts +2 -1
  166. package/src/lib/level-duplication.test.ts +0 -2
  167. package/src/lib/level-duplication.ts +55 -43
  168. package/src/lib/level-name.ts +11 -0
  169. package/src/lib/material-paint.ts +212 -20
  170. package/src/lib/placement-metadata.ts +29 -0
  171. package/src/lib/planar-cursor-placement.test.ts +43 -0
  172. package/src/lib/planar-cursor-placement.ts +42 -0
  173. package/src/lib/roof-duplication.ts +2 -2
  174. package/src/lib/scene-bounds.ts +1 -1
  175. package/src/lib/scene-clipboard.ts +267 -0
  176. package/src/lib/scene.ts +13 -4
  177. package/src/lib/sfx-bus.ts +25 -1
  178. package/src/lib/sfx-player.ts +75 -27
  179. package/src/lib/site-boundary.ts +1 -0
  180. package/src/lib/stair-duplication.ts +2 -2
  181. package/src/lib/stair-levels.test.ts +146 -0
  182. package/src/lib/stair-levels.ts +177 -0
  183. package/src/lib/world-grid-snap.ts +199 -0
  184. package/src/lib/zone-content.ts +130 -0
  185. package/src/store/use-alignment-guides.ts +21 -0
  186. package/src/store/use-editor.tsx +342 -91
  187. package/src/store/use-placement-preview.ts +31 -0
  188. package/src/store/use-wall-move-ghosts.ts +36 -0
  189. package/src/store/use-wall-snap-indicator.ts +32 -0
  190. package/tsconfig.json +2 -1
  191. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  192. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  193. package/src/components/feedback-dialog.tsx +0 -265
  194. package/src/components/pascal-radio.tsx +0 -280
  195. package/src/components/preview-button.tsx +0 -16
  196. package/src/components/tools/building/move-building-tool.tsx +0 -157
  197. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  198. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  199. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  200. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
  201. package/src/components/tools/column/column-tool.tsx +0 -97
  202. package/src/components/tools/column/move-column-tool.tsx +0 -105
  203. package/src/components/tools/door/door-math.ts +0 -110
  204. package/src/components/tools/door/door-tool.tsx +0 -324
  205. package/src/components/tools/door/move-door-tool.tsx +0 -412
  206. package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
  207. package/src/components/tools/fence/fence-tool.tsx +0 -346
  208. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
  209. package/src/components/tools/fence/move-fence-tool.tsx +0 -298
  210. package/src/components/tools/item/item-tool.tsx +0 -26
  211. package/src/components/tools/roof/move-roof-tool.tsx +0 -359
  212. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  213. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  214. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  215. package/src/components/tools/slab/slab-tool.tsx +0 -322
  216. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  217. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  218. package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
  219. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
  220. package/src/components/tools/wall/move-wall-tool.tsx +0 -356
  221. package/src/components/tools/wall/wall-tool.tsx +0 -332
  222. package/src/components/tools/window/move-window-tool.tsx +0 -447
  223. package/src/components/tools/window/window-math.ts +0 -117
  224. package/src/components/tools/window/window-tool.tsx +0 -332
  225. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  226. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  227. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  228. package/src/components/ui/panels/ceiling-panel.tsx +0 -250
  229. package/src/components/ui/panels/column-panel.tsx +0 -715
  230. package/src/components/ui/panels/door-panel.tsx +0 -1292
  231. package/src/components/ui/panels/fence-panel.tsx +0 -227
  232. package/src/components/ui/panels/item-panel.tsx +0 -306
  233. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  234. package/src/components/ui/panels/roof-panel.tsx +0 -230
  235. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  236. package/src/components/ui/panels/slab-panel.tsx +0 -250
  237. package/src/components/ui/panels/spawn-panel.tsx +0 -155
  238. package/src/components/ui/panels/stair-panel.tsx +0 -525
  239. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  240. package/src/components/ui/panels/wall-panel.tsx +0 -177
  241. package/src/components/ui/panels/window-panel.tsx +0 -960
  242. package/src/components/ui/viewer-toolbar.tsx +0 -436
  243. package/src/contexts/presets-context.tsx +0 -121
  244. 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
- }