@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,14 +1,17 @@
1
1
  import {
2
2
  type AnyNode,
3
3
  type AnyNodeId,
4
+ collectAlignmentAnchors,
4
5
  emitter,
5
6
  type GridEvent,
6
7
  type LevelNode,
7
8
  RoofNode,
8
9
  RoofSegmentNode,
9
10
  sceneRegistry,
11
+ snapScalar,
10
12
  useScene,
11
13
  } from '@pascal-app/core'
14
+ import { useAlignmentGuides } from '@pascal-app/editor'
12
15
  import { useViewer } from '@pascal-app/viewer'
13
16
  import { useEffect, useMemo, useRef, useState } from 'react'
14
17
  import * as THREE from 'three'
@@ -16,12 +19,22 @@ import { BufferGeometry, DoubleSide, type Group, type Line, Vector3 } from 'thre
16
19
  import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
17
20
  import { EDITOR_LAYER } from '../../../lib/constants'
18
21
  import { sfxEmitter } from '../../../lib/sfx-bus'
22
+ import {
23
+ resolveAlignmentForActiveBuilding,
24
+ snapWorldXZForActiveBuilding,
25
+ } from '../../../lib/world-grid-snap'
19
26
  import useEditor from '../../../store/use-editor'
20
27
  import { CursorSphere } from '../shared/cursor-sphere'
21
28
 
22
29
  const DEFAULT_WALL_HEIGHT = 0.5
23
- const DEFAULT_ROOF_HEIGHT = 2.5
30
+ const DEFAULT_PITCH_DEG = 40
24
31
  const GRID_OFFSET = 0.02
32
+ /** Figma-style alignment-snap threshold (meters), matching the move tools. */
33
+ const ALIGNMENT_THRESHOLD_M = 0.08
34
+
35
+ function snapToActiveGrid(value: number): number {
36
+ return snapScalar(value, useEditor.getState().gridSnapStep)
37
+ }
25
38
 
26
39
  /**
27
40
  * Creates a roof group with one default gable segment
@@ -34,6 +47,13 @@ const commitRoofPlacement = (
34
47
  ): AnyNode['id'] => {
35
48
  const { createNode, createNodes, nodes } = useScene.getState()
36
49
 
50
+ // A placed roof preset seeds `toolDefaults.roof` with the flattened
51
+ // subtree params (roofType, pitch, wallHeight, overhang, materials, …)
52
+ // before the tool activates. The footprint (width/depth) and placement
53
+ // come from the drawn rectangle and always win; the segment carries the
54
+ // shape/material params, the roof container picks up the materials.
55
+ const defaults = useEditor.getState().toolDefaults.roof ?? {}
56
+
37
57
  const centerX = (corner1[0] + corner2[0]) / 2
38
58
  const centerZ = (corner1[2] + corner2[2]) / 2
39
59
 
@@ -74,11 +94,12 @@ const commitRoofPlacement = (
74
94
  }
75
95
 
76
96
  const segment = RoofSegmentNode.parse({
77
- width,
78
- depth,
79
97
  wallHeight: DEFAULT_WALL_HEIGHT,
80
- roofHeight: DEFAULT_ROOF_HEIGHT,
98
+ pitch: DEFAULT_PITCH_DEG,
81
99
  roofType: 'gable',
100
+ ...defaults,
101
+ width,
102
+ depth,
82
103
  position: [localX, 0, localZ],
83
104
  })
84
105
 
@@ -93,16 +114,19 @@ const commitRoofPlacement = (
93
114
 
94
115
  // Create the segment first (centered in its new parent)
95
116
  const segment = RoofSegmentNode.parse({
96
- width,
97
- depth,
98
117
  wallHeight: DEFAULT_WALL_HEIGHT,
99
- roofHeight: DEFAULT_ROOF_HEIGHT,
118
+ pitch: DEFAULT_PITCH_DEG,
100
119
  roofType: 'gable',
120
+ ...defaults,
121
+ width,
122
+ depth,
101
123
  position: [0, 0, 0],
102
124
  })
103
125
 
104
- // Create the roof container
126
+ // Create the roof container. Segment-shaped params (roofType, pitch, …) are
127
+ // dropped by the RoofNode schema; surface materials in `defaults` carry over.
105
128
  const roof = RoofNode.parse({
129
+ ...defaults,
106
130
  name,
107
131
  position: [centerX, 0, centerZ],
108
132
  children: [segment.id],
@@ -138,6 +162,10 @@ export const RoofTool: React.FC = () => {
138
162
  selectedIdsRef.current = selectedIds
139
163
  }, [selectedIds])
140
164
 
165
+ // Clear preset-seeded defaults on deactivation so a later manual roof draw
166
+ // isn't built with a stale preset's parameters. Unmount-only.
167
+ useEffect(() => () => useEditor.getState().setToolDefaults('roof', null), [])
168
+
141
169
  const corner1Ref = useRef<[number, number, number] | null>(null)
142
170
  const previousGridPosRef = useRef<[number, number] | null>(null)
143
171
  const [preview, setPreview] = useState<PreviewState>({
@@ -151,6 +179,45 @@ export const RoofTool: React.FC = () => {
151
179
 
152
180
  outlineRef.current.geometry = new BufferGeometry()
153
181
 
182
+ // Alignment candidates — anchors of every alignable object; refreshed
183
+ // after each roof commits. Both corners of the rectangle align.
184
+ let alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
185
+ // Snap the drafted corner onto another object's nearest real anchor and
186
+ // publish the guide. The probe is the RAW cursor, NOT the 0.5m-grid-snapped
187
+ // point: resolving against the grid point would only ever catch anchors
188
+ // that happen to sit on a grid line, so off-grid items (furniture, angled
189
+ // walls) would never surface a guide. The matched axis locks exactly to the
190
+ // candidate's coordinate; the other axis keeps its grid snap. Alt bypasses.
191
+ const alignPoint = (
192
+ gridX: number,
193
+ gridZ: number,
194
+ rawX: number,
195
+ rawZ: number,
196
+ bypass: boolean,
197
+ ): [number, number] => {
198
+ if (bypass || alignmentCandidates.length === 0) {
199
+ useAlignmentGuides.getState().clear()
200
+ return [gridX, gridZ]
201
+ }
202
+ const ar = resolveAlignmentForActiveBuilding({
203
+ moving: [{ nodeId: '__roof-draft__', kind: 'corner', x: rawX, z: rawZ }],
204
+ candidates: alignmentCandidates,
205
+ threshold: ALIGNMENT_THRESHOLD_M,
206
+ })
207
+ if (ar.guides.length === 0) {
208
+ useAlignmentGuides.getState().clear()
209
+ return [gridX, gridZ]
210
+ }
211
+ useAlignmentGuides.getState().set(ar.guides)
212
+ let x = gridX
213
+ let z = gridZ
214
+ for (const guide of ar.guides) {
215
+ if (guide.axis === 'x') x = guide.coord
216
+ else z = guide.coord
217
+ }
218
+ return [x, z]
219
+ }
220
+
154
221
  const updateOutline = (
155
222
  corner1: [number, number, number],
156
223
  corner2: [number, number, number],
@@ -173,8 +240,20 @@ export const RoofTool: React.FC = () => {
173
240
  const onGridMove = (event: GridEvent) => {
174
241
  if (!cursorRef.current) return
175
242
 
176
- const gridX = Math.round(event.localPosition[0] * 2) / 2
177
- const gridZ = Math.round(event.localPosition[2] * 2) / 2
243
+ // World-grid snap projected into building-local; rotated buildings
244
+ // used to drag every roof corner off the visible grid.
245
+ const snapped = snapWorldXZForActiveBuilding(
246
+ event.position[0],
247
+ event.position[2],
248
+ useEditor.getState().gridSnapStep,
249
+ ).local
250
+ const [gridX, gridZ] = alignPoint(
251
+ snapped[0],
252
+ snapped[1],
253
+ event.localPosition[0],
254
+ event.localPosition[2],
255
+ event.nativeEvent?.altKey === true,
256
+ )
178
257
  const y = event.localPosition[1]
179
258
 
180
259
  const cursorPosition: [number, number, number] = [gridX, y, gridZ]
@@ -206,8 +285,20 @@ export const RoofTool: React.FC = () => {
206
285
  const onGridClick = (event: GridEvent) => {
207
286
  if (!currentLevelId) return
208
287
 
209
- const gridX = Math.round(event.localPosition[0] * 2) / 2
210
- const gridZ = Math.round(event.localPosition[2] * 2) / 2
288
+ // World-grid snap projected into building-local; rotated buildings
289
+ // used to drag every roof corner off the visible grid.
290
+ const snapped = snapWorldXZForActiveBuilding(
291
+ event.position[0],
292
+ event.position[2],
293
+ useEditor.getState().gridSnapStep,
294
+ ).local
295
+ const [gridX, gridZ] = alignPoint(
296
+ snapped[0],
297
+ snapped[1],
298
+ event.localPosition[0],
299
+ event.localPosition[2],
300
+ event.nativeEvent?.altKey === true,
301
+ )
211
302
  const y = event.localPosition[1]
212
303
 
213
304
  if (corner1Ref.current) {
@@ -222,8 +313,11 @@ export const RoofTool: React.FC = () => {
222
313
 
223
314
  corner1Ref.current = null
224
315
  outlineRef.current.visible = false
316
+ alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', currentLevelId)
317
+ useAlignmentGuides.getState().clear()
225
318
  } else {
226
319
  corner1Ref.current = [gridX, y, gridZ]
320
+ sfxEmitter.emit('sfx:structure-build-start')
227
321
  setPreview((prev) => ({
228
322
  ...prev,
229
323
  corner1: corner1Ref.current,
@@ -238,6 +332,7 @@ export const RoofTool: React.FC = () => {
238
332
  outlineRef.current.visible = false
239
333
  setPreview((prev) => ({ ...prev, corner1: null }))
240
334
  }
335
+ useAlignmentGuides.getState().clear()
241
336
  }
242
337
 
243
338
  emitter.on('grid:move', onGridMove)
@@ -248,6 +343,7 @@ export const RoofTool: React.FC = () => {
248
343
  emitter.off('grid:move', onGridMove)
249
344
  emitter.off('grid:click', onGridClick)
250
345
  emitter.off('tool:cancel', onCancel)
346
+ useAlignmentGuides.getState().clear()
251
347
 
252
348
  corner1Ref.current = null
253
349
  }
@@ -0,0 +1,77 @@
1
+ export let boxSelectHandled = false
2
+
3
+ let resetTimeout: ReturnType<typeof setTimeout> | null = null
4
+ const suppressedPointerIds = new Set<number>()
5
+ const suppressionCleanups = new Map<number, () => void>()
6
+
7
+ type PointerEventLike = {
8
+ pointerId?: number
9
+ nativeEvent?: PointerEvent | PointerEventLike
10
+ }
11
+
12
+ function pointerIdFor(event: PointerEvent | PointerEventLike): number | null {
13
+ if ('pointerId' in event && typeof event.pointerId === 'number') {
14
+ return event.pointerId
15
+ }
16
+ const nativeEvent = 'nativeEvent' in event ? event.nativeEvent : undefined
17
+ return nativeEvent ? pointerIdFor(nativeEvent) : null
18
+ }
19
+
20
+ export function markBoxSelectHandled() {
21
+ boxSelectHandled = true
22
+ if (resetTimeout) {
23
+ clearTimeout(resetTimeout)
24
+ }
25
+ resetTimeout = setTimeout(() => {
26
+ boxSelectHandled = false
27
+ resetTimeout = null
28
+ }, 50)
29
+ }
30
+
31
+ export function suppressBoxSelectForPointer(event: PointerEvent | PointerEventLike) {
32
+ markBoxSelectHandled()
33
+
34
+ const pointerId = pointerIdFor(event)
35
+ if (pointerId === null || suppressedPointerIds.has(pointerId)) return
36
+
37
+ suppressedPointerIds.add(pointerId)
38
+
39
+ const clear = (releaseEvent?: PointerEvent) => {
40
+ if (releaseEvent && releaseEvent.pointerId !== pointerId) return
41
+ markBoxSelectHandled()
42
+ suppressedPointerIds.delete(pointerId)
43
+ const cleanup = suppressionCleanups.get(pointerId)
44
+ suppressionCleanups.delete(pointerId)
45
+ cleanup?.()
46
+ }
47
+
48
+ const onPointerUp = (releaseEvent: PointerEvent) => clear(releaseEvent)
49
+ const onPointerCancel = (releaseEvent: PointerEvent) => clear(releaseEvent)
50
+ const onBlur = () => clear()
51
+ const cleanup = () => {
52
+ window.removeEventListener('pointerup', onPointerUp)
53
+ window.removeEventListener('pointercancel', onPointerCancel)
54
+ window.removeEventListener('blur', onBlur)
55
+ }
56
+
57
+ suppressionCleanups.set(pointerId, cleanup)
58
+ window.addEventListener('pointerup', onPointerUp)
59
+ window.addEventListener('pointercancel', onPointerCancel)
60
+ window.addEventListener('blur', onBlur)
61
+ }
62
+
63
+ export function isBoxSelectPointerSuppressed(event: PointerEvent | PointerEventLike) {
64
+ const pointerId = pointerIdFor(event)
65
+ return pointerId !== null && suppressedPointerIds.has(pointerId)
66
+ }
67
+
68
+ export function clearBoxSelectHandled() {
69
+ if (resetTimeout) {
70
+ clearTimeout(resetTimeout)
71
+ resetTimeout = null
72
+ }
73
+ boxSelectHandled = false
74
+ for (const cleanup of suppressionCleanups.values()) cleanup()
75
+ suppressionCleanups.clear()
76
+ suppressedPointerIds.clear()
77
+ }