@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,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
+ }