@pascal-app/editor 0.8.0 → 0.9.2

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 (350) hide show
  1. package/package.json +15 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +795 -28
  5. package/src/components/editor/editor-layout-mobile.tsx +8 -3
  6. package/src/components/editor/editor-layout-v2.tsx +76 -51
  7. package/src/components/editor/export-manager.tsx +69 -32
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
  10. package/src/components/editor/first-person/build-collider-world.ts +228 -83
  11. package/src/components/editor/first-person-controls.tsx +943 -27
  12. package/src/components/editor/floating-action-menu.tsx +676 -208
  13. package/src/components/editor/floating-building-action-menu.tsx +2 -2
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +6916 -12137
  17. package/src/components/editor/grid.tsx +197 -36
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +420 -0
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +396 -0
  24. package/src/components/editor/group-transform-shared.ts +489 -0
  25. package/src/components/editor/handles/handle-arrow.tsx +581 -0
  26. package/src/components/editor/handles/use-handle-drag.ts +246 -0
  27. package/src/components/editor/index.tsx +412 -162
  28. package/src/components/editor/measurement-pill.tsx +115 -0
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +1646 -0
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +1106 -483
  36. package/src/components/editor/site-edge-labels.tsx +45 -21
  37. package/src/components/editor/slab-hole-highlights.tsx +474 -0
  38. package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +140 -41
  41. package/src/components/editor/use-floorplan-background-placement.ts +190 -35
  42. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  43. package/src/components/editor/use-floorplan-scene-data.ts +19 -7
  44. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  45. package/src/components/editor/wall-measurement-label.tsx +14 -19
  46. package/src/components/editor/wall-move-side-handles.tsx +870 -0
  47. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  48. package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
  49. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
  50. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  51. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  52. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  53. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  54. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  55. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  56. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  57. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
  58. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
  59. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  60. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
  61. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  62. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  63. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  64. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  65. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
  66. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
  69. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
  70. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  71. package/src/components/editor-2d/svg-paths.ts +10 -2
  72. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
  73. package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
  74. package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
  75. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  76. package/src/components/systems/stair/stair-edit-system.tsx +1 -1
  77. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  78. package/src/components/systems/zone/zone-system.tsx +42 -13
  79. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  80. package/src/components/tools/elevator/elevator-tool.tsx +375 -0
  81. package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
  82. package/src/components/tools/fence/fence-drafting.ts +141 -27
  83. package/src/components/tools/item/move-tool.tsx +45 -105
  84. package/src/components/tools/item/placement-math.test.ts +22 -0
  85. package/src/components/tools/item/placement-math.ts +59 -7
  86. package/src/components/tools/item/placement-strategies.ts +432 -16
  87. package/src/components/tools/item/placement-types.ts +22 -1
  88. package/src/components/tools/item/use-draft-node.ts +51 -2
  89. package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
  90. package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
  91. package/src/components/tools/roof/roof-tool.tsx +350 -28
  92. package/src/components/tools/select/box-select-state.ts +88 -0
  93. package/src/components/tools/select/box-select-tool.tsx +442 -520
  94. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  95. package/src/components/tools/select/marquee-geometry.ts +155 -0
  96. package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
  97. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  98. package/src/components/tools/select/select-candidates.test.ts +109 -0
  99. package/src/components/tools/select/select-candidates.ts +85 -0
  100. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  101. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  102. package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
  103. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  104. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  105. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  106. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  107. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  108. package/src/components/tools/shared/placement-box.tsx +305 -0
  109. package/src/components/tools/shared/polygon-editor.tsx +866 -175
  110. package/src/components/tools/shared/segment-angle.ts +56 -0
  111. package/src/components/tools/site/site-boundary-editor.tsx +520 -16
  112. package/src/components/tools/site/site-flag-model.ts +18 -0
  113. package/src/components/tools/stair/stair-defaults.ts +2 -1
  114. package/src/components/tools/stair/stair-tool.tsx +342 -37
  115. package/src/components/tools/tool-manager.tsx +239 -62
  116. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  117. package/src/components/tools/wall/wall-drafting.ts +248 -171
  118. package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
  119. package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
  120. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  121. package/src/components/tools/zone/zone-tool.tsx +86 -82
  122. package/src/components/ui/action-menu/action-button.tsx +21 -1
  123. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  124. package/src/components/ui/action-menu/control-modes.tsx +40 -170
  125. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  126. package/src/components/ui/action-menu/index.tsx +3 -117
  127. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  128. package/src/components/ui/action-menu/structure-tools.tsx +27 -93
  129. package/src/components/ui/action-menu/view-toggles.tsx +279 -90
  130. package/src/components/ui/command-palette/editor-commands.tsx +20 -15
  131. package/src/components/ui/command-palette/index.tsx +5 -3
  132. package/src/components/ui/controls/material-paint-panel.tsx +152 -0
  133. package/src/components/ui/controls/material-picker.tsx +87 -177
  134. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  135. package/src/components/ui/controls/metric-control.tsx +142 -46
  136. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  137. package/src/components/ui/controls/slider-control.tsx +88 -38
  138. package/src/components/ui/floating-level-selector.tsx +47 -10
  139. package/src/components/ui/helpers/building-helper.tsx +10 -23
  140. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  141. package/src/components/ui/helpers/helper-manager.tsx +235 -23
  142. package/src/components/ui/helpers/item-helper.tsx +28 -32
  143. package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
  144. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  145. package/src/components/ui/icon-ref.tsx +47 -0
  146. package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
  147. package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
  148. package/src/components/ui/level-duplicate-dialog.tsx +2 -1
  149. package/src/components/ui/panels/node-display.ts +17 -16
  150. package/src/components/ui/panels/panel-manager.tsx +55 -58
  151. package/src/components/ui/panels/panel-wrapper.tsx +238 -12
  152. package/src/components/ui/panels/parametric-inspector.tsx +482 -0
  153. package/src/components/ui/panels/reference-panel.tsx +54 -19
  154. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  155. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  156. package/src/components/ui/primitives/sidebar.tsx +1 -0
  157. package/src/components/ui/scene-loader.tsx +1 -3
  158. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  159. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  160. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  161. package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
  162. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  163. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
  164. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
  165. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
  166. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  167. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
  168. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
  169. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  170. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  171. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
  172. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  173. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
  174. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
  175. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
  176. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  177. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  178. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
  179. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
  180. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
  181. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
  182. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
  183. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
  184. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  185. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
  186. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  187. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  188. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  189. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  190. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
  191. package/src/components/ui/sidebar/tab-bar.tsx +75 -1
  192. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  193. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  194. package/src/components/ui/snap-target-badge.tsx +88 -0
  195. package/src/components/viewer-overlay.tsx +253 -61
  196. package/src/components/viewer-zone-system.tsx +7 -2
  197. package/src/hooks/use-auto-save.test.ts +14 -0
  198. package/src/hooks/use-auto-save.ts +64 -10
  199. package/src/hooks/use-ceiling-events.ts +176 -0
  200. package/src/hooks/use-drag-action.ts +127 -0
  201. package/src/hooks/use-keyboard.ts +493 -95
  202. package/src/hooks/use-selection.ts +64 -0
  203. package/src/index.tsx +451 -6
  204. package/src/lib/active-placement-surface.ts +35 -0
  205. package/src/lib/ceiling-plan-snap.ts +24 -0
  206. package/src/lib/constants.ts +6 -2
  207. package/src/lib/contextual-help.test.ts +112 -0
  208. package/src/lib/contextual-help.ts +144 -0
  209. package/src/lib/continuation.ts +64 -0
  210. package/src/lib/direct-manipulation.test.ts +192 -0
  211. package/src/lib/direct-manipulation.ts +109 -0
  212. package/src/lib/editor-api.ts +50 -0
  213. package/src/lib/elevator-support.ts +96 -0
  214. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  215. package/src/lib/floorplan/apply-alignment.ts +117 -0
  216. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  217. package/src/lib/floorplan/geometry.ts +53 -0
  218. package/src/lib/floorplan/index.ts +10 -0
  219. package/src/lib/floorplan/items.ts +5 -2
  220. package/src/lib/floorplan/plan-coords.ts +21 -0
  221. package/src/lib/floorplan/selection-tool.ts +40 -0
  222. package/src/lib/floorplan/stairs.ts +12 -1
  223. package/src/lib/fresh-planar-placement.test.ts +339 -0
  224. package/src/lib/fresh-planar-placement.ts +130 -0
  225. package/src/lib/glb-export.test.ts +441 -0
  226. package/src/lib/glb-export.ts +874 -0
  227. package/src/lib/history.ts +11 -1
  228. package/src/lib/interaction/hot-set.test.ts +133 -0
  229. package/src/lib/interaction/hot-set.ts +67 -0
  230. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  231. package/src/lib/interaction/overlay-policy.ts +59 -0
  232. package/src/lib/interaction/scope.ts +211 -0
  233. package/src/lib/level-duplication.ts +54 -42
  234. package/src/lib/level-selection.ts +2 -2
  235. package/src/lib/material-paint.ts +227 -48
  236. package/src/lib/measurement-kind.test.ts +30 -0
  237. package/src/lib/measurement-kind.ts +19 -0
  238. package/src/lib/measurement-label.test.ts +47 -0
  239. package/src/lib/measurement-label.ts +68 -0
  240. package/src/lib/measurement-parser.ts +116 -0
  241. package/src/lib/measurements.test.ts +183 -0
  242. package/src/lib/measurements.ts +199 -0
  243. package/src/lib/paint-scope.test.ts +454 -0
  244. package/src/lib/paint-scope.ts +461 -0
  245. package/src/lib/placement-drag-release.ts +23 -0
  246. package/src/lib/placement-metadata.ts +29 -0
  247. package/src/lib/planar-cursor-placement.test.ts +100 -0
  248. package/src/lib/planar-cursor-placement.ts +42 -0
  249. package/src/lib/plugin-panels.test.ts +19 -0
  250. package/src/lib/plugin-panels.ts +91 -0
  251. package/src/lib/quick-measurement.test.ts +77 -0
  252. package/src/lib/quick-measurement.ts +109 -0
  253. package/src/lib/roof-duplication.ts +7 -7
  254. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  255. package/src/lib/roof-wall-hit.ts +164 -0
  256. package/src/lib/scene-clipboard.test.ts +196 -0
  257. package/src/lib/scene-clipboard.ts +332 -0
  258. package/src/lib/scene.ts +40 -8
  259. package/src/lib/selection-routing.test.ts +298 -0
  260. package/src/lib/selection-routing.ts +175 -0
  261. package/src/lib/sfx/index.ts +1 -0
  262. package/src/lib/sfx/movement-tick.test.ts +65 -0
  263. package/src/lib/sfx/movement-tick.ts +18 -0
  264. package/src/lib/sfx-bus.ts +69 -10
  265. package/src/lib/sfx-player.test.ts +124 -0
  266. package/src/lib/sfx-player.ts +148 -55
  267. package/src/lib/site-boundary.ts +1 -0
  268. package/src/lib/slab-plan-snap.test.ts +93 -0
  269. package/src/lib/slab-plan-snap.ts +108 -0
  270. package/src/lib/snapping-mode.test.ts +138 -0
  271. package/src/lib/snapping-mode.ts +179 -0
  272. package/src/lib/stair-duplication.ts +4 -4
  273. package/src/lib/stair-levels.test.ts +146 -0
  274. package/src/lib/stair-levels.ts +177 -0
  275. package/src/lib/surface-plan-snap.test.ts +71 -0
  276. package/src/lib/surface-plan-snap.ts +256 -0
  277. package/src/lib/use-linear-display.ts +40 -0
  278. package/src/lib/world-grid-snap.ts +231 -0
  279. package/src/lib/zone-content.ts +130 -0
  280. package/src/store/use-alignment-guides.ts +21 -0
  281. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  282. package/src/store/use-editor.tsx +767 -114
  283. package/src/store/use-facing-pose.ts +44 -0
  284. package/src/store/use-fence-curve-draft.ts +21 -0
  285. package/src/store/use-floorplan-draft-preview.ts +101 -0
  286. package/src/store/use-floorplan-marquee.ts +50 -0
  287. package/src/store/use-interaction-scope.test.ts +186 -0
  288. package/src/store/use-interaction-scope.ts +132 -0
  289. package/src/store/use-measurement-draft.test.ts +530 -0
  290. package/src/store/use-measurement-draft.ts +543 -0
  291. package/src/store/use-opening-guides.ts +41 -0
  292. package/src/store/use-placement-preview.ts +39 -0
  293. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  294. package/src/store/use-quick-measurement-hud.ts +77 -0
  295. package/src/store/use-segment-draft-chain.ts +28 -0
  296. package/src/store/use-stair-build-preview.ts +43 -0
  297. package/src/store/use-wall-move-ghosts.ts +36 -0
  298. package/src/store/use-wall-snap-indicator.ts +34 -0
  299. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  300. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  301. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  302. package/src/components/tools/building/move-building-tool.tsx +0 -157
  303. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  304. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  305. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  306. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
  307. package/src/components/tools/column/column-tool.tsx +0 -97
  308. package/src/components/tools/column/move-column-tool.tsx +0 -105
  309. package/src/components/tools/door/door-math.ts +0 -110
  310. package/src/components/tools/door/door-tool.tsx +0 -324
  311. package/src/components/tools/door/move-door-tool.tsx +0 -412
  312. package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
  313. package/src/components/tools/fence/fence-tool.tsx +0 -346
  314. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
  315. package/src/components/tools/fence/move-fence-tool.tsx +0 -302
  316. package/src/components/tools/item/item-tool.tsx +0 -26
  317. package/src/components/tools/roof/move-roof-tool.tsx +0 -364
  318. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  319. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  320. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  321. package/src/components/tools/slab/slab-tool.tsx +0 -322
  322. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  323. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  324. package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
  325. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
  326. package/src/components/tools/wall/move-wall-tool.tsx +0 -358
  327. package/src/components/tools/wall/wall-tool.tsx +0 -332
  328. package/src/components/tools/window/move-window-tool.tsx +0 -447
  329. package/src/components/tools/window/window-math.ts +0 -117
  330. package/src/components/tools/window/window-tool.tsx +0 -332
  331. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  332. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  333. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  334. package/src/components/ui/panels/ceiling-panel.tsx +0 -249
  335. package/src/components/ui/panels/column-panel.tsx +0 -759
  336. package/src/components/ui/panels/door-panel.tsx +0 -1299
  337. package/src/components/ui/panels/fence-panel.tsx +0 -222
  338. package/src/components/ui/panels/item-panel.tsx +0 -306
  339. package/src/components/ui/panels/paint-panel.tsx +0 -163
  340. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  341. package/src/components/ui/panels/roof-panel.tsx +0 -280
  342. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  343. package/src/components/ui/panels/slab-panel.tsx +0 -250
  344. package/src/components/ui/panels/spawn-panel.tsx +0 -161
  345. package/src/components/ui/panels/stair-panel.tsx +0 -577
  346. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  347. package/src/components/ui/panels/wall-panel.tsx +0 -273
  348. package/src/components/ui/panels/window-panel.tsx +0 -968
  349. package/src/contexts/presets-context.tsx +0 -121
  350. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -1,358 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNodeId,
5
- emitter,
6
- type GridEvent,
7
- pauseSceneHistory,
8
- resumeSceneHistory,
9
- useScene,
10
- type WallNode,
11
- } from '@pascal-app/core'
12
- import { useViewer } from '@pascal-app/viewer'
13
- import { useCallback, useEffect, useRef, useState } from 'react'
14
- import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
15
- import { sfxEmitter } from '../../../lib/sfx-bus'
16
- import useEditor from '../../../store/use-editor'
17
- import { CursorSphere } from '../shared/cursor-sphere'
18
- import { getWallGridStep, snapScalarToGrid } from './wall-drafting'
19
-
20
- function rotateVector([x, z]: [number, number], angle: number): [number, number] {
21
- const cos = Math.cos(angle)
22
- const sin = Math.sin(angle)
23
- return [x * cos - z * sin, x * sin + z * cos]
24
- }
25
-
26
- function samePoint(a: [number, number], b: [number, number]) {
27
- return a[0] === b[0] && a[1] === b[1]
28
- }
29
-
30
- function stripWallIsNewMetadata(meta: WallNode['metadata']): WallNode['metadata'] {
31
- if (!meta || typeof meta !== 'object' || Array.isArray(meta)) {
32
- return meta
33
- }
34
-
35
- const nextMeta = { ...(meta as Record<string, unknown>) } as Record<string, unknown>
36
- delete nextMeta.isNew
37
- return nextMeta as WallNode['metadata']
38
- }
39
-
40
- type LinkedWallSnapshot = {
41
- id: WallNode['id']
42
- start: [number, number]
43
- end: [number, number]
44
- }
45
-
46
- function getLinkedWallSnapshots(args: {
47
- wallId: WallNode['id']
48
- wallParentId: string | null
49
- originalStart: [number, number]
50
- originalEnd: [number, number]
51
- }) {
52
- const { wallId, wallParentId, originalStart, originalEnd } = args
53
- const { nodes } = useScene.getState()
54
- const snapshots: LinkedWallSnapshot[] = []
55
-
56
- for (const node of Object.values(nodes)) {
57
- if (!(node?.type === 'wall' && node.id !== wallId)) {
58
- continue
59
- }
60
-
61
- if ((node.parentId ?? null) !== wallParentId) {
62
- continue
63
- }
64
-
65
- if (
66
- !(
67
- samePoint(node.start, originalStart) ||
68
- samePoint(node.start, originalEnd) ||
69
- samePoint(node.end, originalStart) ||
70
- samePoint(node.end, originalEnd)
71
- )
72
- ) {
73
- continue
74
- }
75
-
76
- snapshots.push({
77
- id: node.id,
78
- start: [...node.start] as [number, number],
79
- end: [...node.end] as [number, number],
80
- })
81
- }
82
-
83
- return snapshots
84
- }
85
-
86
- function getLinkedWallUpdates(
87
- linkedWalls: LinkedWallSnapshot[],
88
- originalStart: [number, number],
89
- originalEnd: [number, number],
90
- nextStart: [number, number],
91
- nextEnd: [number, number],
92
- ) {
93
- return linkedWalls.map((wall) => ({
94
- id: wall.id,
95
- start: samePoint(wall.start, originalStart)
96
- ? nextStart
97
- : samePoint(wall.start, originalEnd)
98
- ? nextEnd
99
- : wall.start,
100
- end: samePoint(wall.end, originalStart)
101
- ? nextStart
102
- : samePoint(wall.end, originalEnd)
103
- ? nextEnd
104
- : wall.end,
105
- }))
106
- }
107
-
108
- export const MoveWallTool: React.FC<{ node: WallNode }> = ({ node }) => {
109
- const meta =
110
- typeof node.metadata === 'object' && node.metadata !== null && !Array.isArray(node.metadata)
111
- ? (node.metadata as Record<string, unknown>)
112
- : {}
113
- const isNew = !!meta.isNew
114
- const activatedAtRef = useRef<number>(Date.now())
115
- const previousGridPosRef = useRef<[number, number] | null>(null)
116
- const originalStartRef = useRef<[number, number]>([...node.start] as [number, number])
117
- const originalEndRef = useRef<[number, number]>([...node.end] as [number, number])
118
- const originalCenterRef = useRef<[number, number]>([
119
- (node.start[0] + node.end[0]) / 2,
120
- (node.start[1] + node.end[1]) / 2,
121
- ])
122
- const originalHalfVectorRef = useRef<[number, number]>([
123
- (node.end[0] - node.start[0]) / 2,
124
- (node.end[1] - node.start[1]) / 2,
125
- ])
126
- const linkedOriginalsRef = useRef(
127
- isNew
128
- ? []
129
- : getLinkedWallSnapshots({
130
- wallId: node.id,
131
- wallParentId: node.parentId ?? null,
132
- originalStart: node.start,
133
- originalEnd: node.end,
134
- }),
135
- )
136
- const dragAnchorRef = useRef<[number, number] | null>(null)
137
- const nodeIdRef = useRef(node.id)
138
- const previewRef = useRef<{ start: [number, number]; end: [number, number] } | null>(null)
139
- const pendingRotationRef = useRef(0)
140
- const shiftPressedRef = useRef(false)
141
-
142
- const [cursorLocalPos, setCursorLocalPos] = useState<[number, number, number]>(() => {
143
- const centerX = (node.start[0] + node.end[0]) / 2
144
- const centerZ = (node.start[1] + node.end[1]) / 2
145
- return [centerX, 0, centerZ]
146
- })
147
-
148
- const exitMoveMode = useCallback(() => {
149
- useEditor.getState().setMovingNode(null)
150
- }, [])
151
-
152
- useEffect(() => {
153
- const nodeId = nodeIdRef.current
154
- const originalStart = originalStartRef.current
155
- const originalEnd = originalEndRef.current
156
- const originalCenter = originalCenterRef.current
157
- const originalHalfVector = originalHalfVectorRef.current
158
-
159
- pauseSceneHistory(useScene)
160
- let wasCommitted = false
161
-
162
- const applyNodePreview = (
163
- updates: Array<{ id: WallNode['id']; start: [number, number]; end: [number, number] }>,
164
- ) => {
165
- useScene.getState().updateNodes(
166
- updates.map((entry) => ({
167
- id: entry.id as AnyNodeId,
168
- data: { start: entry.start, end: entry.end },
169
- })),
170
- )
171
- for (const entry of updates) {
172
- useScene.getState().markDirty(entry.id as AnyNodeId)
173
- }
174
- }
175
-
176
- const buildWallFromCenter = (center: [number, number]) => {
177
- const rotatedHalf = rotateVector(originalHalfVector, pendingRotationRef.current)
178
- const nextStart: [number, number] = [center[0] - rotatedHalf[0], center[1] - rotatedHalf[1]]
179
- const nextEnd: [number, number] = [center[0] + rotatedHalf[0], center[1] + rotatedHalf[1]]
180
- return { start: nextStart, end: nextEnd }
181
- }
182
-
183
- const applyPreview = (nextStart: [number, number], nextEnd: [number, number]) => {
184
- previewRef.current = { start: nextStart, end: nextEnd }
185
- const centerX = (nextStart[0] + nextEnd[0]) / 2
186
- const centerZ = (nextStart[1] + nextEnd[1]) / 2
187
- setCursorLocalPos([centerX, 0, centerZ])
188
- applyNodePreview([
189
- { id: nodeId, start: nextStart, end: nextEnd },
190
- ...getLinkedWallUpdates(
191
- linkedOriginalsRef.current,
192
- originalStart,
193
- originalEnd,
194
- nextStart,
195
- nextEnd,
196
- ),
197
- ])
198
- }
199
-
200
- const restoreOriginal = () => {
201
- applyNodePreview([
202
- { id: nodeId, start: originalStart, end: originalEnd },
203
- ...linkedOriginalsRef.current,
204
- ])
205
- }
206
-
207
- const onGridMove = (event: GridEvent) => {
208
- const rawX = event.localPosition[0]
209
- const rawZ = event.localPosition[2]
210
- const snapStep = getWallGridStep()
211
- const localX = shiftPressedRef.current ? rawX : snapScalarToGrid(rawX, snapStep)
212
- const localZ = shiftPressedRef.current ? rawZ : snapScalarToGrid(rawZ, snapStep)
213
-
214
- if (
215
- previousGridPosRef.current &&
216
- (localX !== previousGridPosRef.current[0] || localZ !== previousGridPosRef.current[1])
217
- ) {
218
- sfxEmitter.emit('sfx:grid-snap')
219
- }
220
- previousGridPosRef.current = [localX, localZ]
221
-
222
- const anchor = dragAnchorRef.current ?? [localX, localZ]
223
- dragAnchorRef.current = anchor
224
-
225
- const deltaX = localX - anchor[0]
226
- const deltaZ = localZ - anchor[1]
227
-
228
- const nextCenter: [number, number] = [originalCenter[0] + deltaX, originalCenter[1] + deltaZ]
229
- const nextWall = buildWallFromCenter(nextCenter)
230
- applyPreview(nextWall.start, nextWall.end)
231
- }
232
-
233
- const onGridClick = (event: GridEvent) => {
234
- if (Date.now() - activatedAtRef.current < 150) {
235
- event.nativeEvent?.stopPropagation?.()
236
- return
237
- }
238
-
239
- const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
240
-
241
- wasCommitted = true
242
-
243
- // Restore original baseline while paused so the next resume+update
244
- // registers as a single tracked change (undo reverts to original).
245
- applyNodePreview([
246
- { id: nodeId, start: originalStart, end: originalEnd },
247
- ...linkedOriginalsRef.current,
248
- ])
249
-
250
- resumeSceneHistory(useScene)
251
-
252
- const commitUpdates = [
253
- {
254
- id: nodeId as AnyNodeId,
255
- data: isNew
256
- ? {
257
- start: preview.start,
258
- end: preview.end,
259
- metadata: stripWallIsNewMetadata(node.metadata),
260
- }
261
- : { start: preview.start, end: preview.end },
262
- },
263
- ...getLinkedWallUpdates(
264
- linkedOriginalsRef.current,
265
- originalStart,
266
- originalEnd,
267
- preview.start,
268
- preview.end,
269
- ).map((entry) => ({
270
- id: entry.id as AnyNodeId,
271
- data: { start: entry.start, end: entry.end },
272
- })),
273
- ]
274
- useScene.getState().updateNodes(commitUpdates)
275
- for (const { id } of commitUpdates) {
276
- useScene.getState().markDirty(id)
277
- }
278
-
279
- pauseSceneHistory(useScene)
280
-
281
- sfxEmitter.emit('sfx:item-place')
282
- useViewer.getState().setSelection({ selectedIds: [nodeId] })
283
- exitMoveMode()
284
- event.nativeEvent?.stopPropagation?.()
285
- }
286
-
287
- const onKeyDown = (event: KeyboardEvent) => {
288
- if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
289
- return
290
- }
291
-
292
- if (event.key === 'Shift') {
293
- shiftPressedRef.current = true
294
- return
295
- }
296
-
297
- const ROTATION_STEP = Math.PI / 4
298
- let rotationDelta = 0
299
- if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
300
- else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
301
-
302
- if (rotationDelta === 0) {
303
- return
304
- }
305
-
306
- event.preventDefault()
307
- pendingRotationRef.current += rotationDelta
308
- sfxEmitter.emit('sfx:item-rotate')
309
-
310
- const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
311
- const currentCenter: [number, number] = [
312
- (preview.start[0] + preview.end[0]) / 2,
313
- (preview.start[1] + preview.end[1]) / 2,
314
- ]
315
- const nextWall = buildWallFromCenter(currentCenter)
316
- applyPreview(nextWall.start, nextWall.end)
317
- }
318
-
319
- const onKeyUp = (event: KeyboardEvent) => {
320
- if (event.key === 'Shift') {
321
- shiftPressedRef.current = false
322
- }
323
- }
324
-
325
- const onCancel = () => {
326
- restoreOriginal()
327
- useViewer.getState().setSelection({ selectedIds: [nodeId] })
328
- resumeSceneHistory(useScene)
329
- markToolCancelConsumed()
330
- exitMoveMode()
331
- }
332
-
333
- emitter.on('grid:move', onGridMove)
334
- emitter.on('grid:click', onGridClick)
335
- emitter.on('tool:cancel', onCancel)
336
- window.addEventListener('keydown', onKeyDown)
337
- window.addEventListener('keyup', onKeyUp)
338
-
339
- return () => {
340
- if (!wasCommitted) {
341
- restoreOriginal()
342
- }
343
- shiftPressedRef.current = false
344
- resumeSceneHistory(useScene)
345
- emitter.off('grid:move', onGridMove)
346
- emitter.off('grid:click', onGridClick)
347
- emitter.off('tool:cancel', onCancel)
348
- window.removeEventListener('keydown', onKeyDown)
349
- window.removeEventListener('keyup', onKeyUp)
350
- }
351
- }, [exitMoveMode, isNew, node.metadata])
352
-
353
- return (
354
- <group>
355
- <CursorSphere position={cursorLocalPos} showTooltip={false} />
356
- </group>
357
- )
358
- }
@@ -1,332 +0,0 @@
1
- import { emitter, type GridEvent, type LevelNode, useScene, type WallNode } from '@pascal-app/core'
2
- import { useViewer } from '@pascal-app/viewer'
3
- import { Html } from '@react-three/drei'
4
- import { useEffect, useRef, useState } from 'react'
5
- import { DoubleSide, type Group, type Mesh, Shape, ShapeGeometry, Vector3 } from 'three'
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
- import {
11
- formatAngleRadians,
12
- getAngleToSegmentReference,
13
- getSegmentAngleReferenceAtPoint,
14
- } from '../shared/segment-angle'
15
- import { createWallOnCurrentLevel, snapWallDraftPoint, type WallPlanPoint } from './wall-drafting'
16
-
17
- const WALL_HEIGHT = 2.5
18
- const DRAFT_LABEL_Y = WALL_HEIGHT + 0.22
19
- const DRAFT_ANGLE_LABEL_Y = 0.28
20
-
21
- type DraftAngleLabel = {
22
- id: string
23
- label: string
24
- position: [number, number, number]
25
- }
26
-
27
- type DraftMeasurementState = {
28
- lengthLabel: string
29
- lengthPosition: [number, number, number]
30
- angleLabels: DraftAngleLabel[]
31
- } | null
32
-
33
- function formatMeasurement(value: number, unit: 'metric' | 'imperial') {
34
- if (unit === 'imperial') {
35
- const feet = value * 3.280_84
36
- const wholeFeet = Math.floor(feet)
37
- const inches = Math.round((feet - wholeFeet) * 12)
38
- if (inches === 12) return `${wholeFeet + 1}'0"`
39
- return `${wholeFeet}'${inches}"`
40
- }
41
-
42
- return `${Number.parseFloat(value.toFixed(2))}m`
43
- }
44
-
45
- function getDraftAngleLabels(
46
- start: WallPlanPoint,
47
- end: WallPlanPoint,
48
- walls: WallNode[],
49
- ): DraftAngleLabel[] {
50
- const draftFromStart: WallPlanPoint = [end[0] - start[0], end[1] - start[1]]
51
- const draftFromEnd: WallPlanPoint = [start[0] - end[0], start[1] - end[1]]
52
- const endpoints = [
53
- { id: 'start', point: start, draftVector: draftFromStart },
54
- { id: 'end', point: end, draftVector: draftFromEnd },
55
- ]
56
- const labels: DraftAngleLabel[] = []
57
-
58
- for (const endpoint of endpoints) {
59
- const connectedWall = walls.find((wall) =>
60
- Boolean(getSegmentAngleReferenceAtPoint(endpoint.point, wall)),
61
- )
62
- if (!connectedWall) continue
63
-
64
- const connectedReference = getSegmentAngleReferenceAtPoint(endpoint.point, connectedWall)
65
- if (!connectedReference) continue
66
-
67
- const angle = getAngleToSegmentReference(endpoint.draftVector, connectedReference)
68
- if (angle === null) continue
69
-
70
- labels.push({
71
- id: endpoint.id,
72
- label: formatAngleRadians(angle),
73
- position: [endpoint.point[0], DRAFT_ANGLE_LABEL_Y, endpoint.point[1]],
74
- })
75
- }
76
-
77
- return labels
78
- }
79
-
80
- function getDraftMeasurementState(
81
- start: WallPlanPoint,
82
- end: WallPlanPoint,
83
- walls: WallNode[],
84
- unit: 'metric' | 'imperial',
85
- ): DraftMeasurementState {
86
- const dx = end[0] - start[0]
87
- const dz = end[1] - start[1]
88
- const length = Math.hypot(dx, dz)
89
-
90
- if (length < 0.01) return null
91
-
92
- return {
93
- lengthLabel: formatMeasurement(length, unit),
94
- lengthPosition: [(start[0] + end[0]) / 2, DRAFT_LABEL_Y, (start[1] + end[1]) / 2],
95
- angleLabels: getDraftAngleLabels(start, end, walls),
96
- }
97
- }
98
-
99
- /**
100
- * Update wall preview mesh geometry to create a vertical plane between two points
101
- */
102
- const updateWallPreview = (mesh: Mesh, start: Vector3, end: Vector3) => {
103
- // Calculate direction and perpendicular for wall thickness
104
- const direction = new Vector3(end.x - start.x, 0, end.z - start.z)
105
- const length = direction.length()
106
-
107
- if (length < 0.01) {
108
- mesh.visible = false
109
- return
110
- }
111
-
112
- mesh.visible = true
113
- direction.normalize()
114
-
115
- // Create wall shape (vertical rectangle in XY plane)
116
- const shape = new Shape()
117
- shape.moveTo(0, 0)
118
- shape.lineTo(length, 0)
119
- shape.lineTo(length, WALL_HEIGHT)
120
- shape.lineTo(0, WALL_HEIGHT)
121
- shape.closePath()
122
-
123
- // Create geometry
124
- const geometry = new ShapeGeometry(shape)
125
-
126
- // Calculate rotation angle
127
- // Negate the angle to fix the opposite direction issue
128
- const angle = -Math.atan2(direction.z, direction.x)
129
-
130
- // Position at start point and rotate
131
- mesh.position.set(start.x, start.y, start.z)
132
- mesh.rotation.y = angle
133
-
134
- // Dispose old geometry and assign new one
135
- if (mesh.geometry) {
136
- mesh.geometry.dispose()
137
- }
138
- mesh.geometry = geometry
139
- }
140
-
141
- const getCurrentLevelWalls = (): WallNode[] => {
142
- const currentLevelId = useViewer.getState().selection.levelId
143
- const { nodes } = useScene.getState()
144
-
145
- if (!currentLevelId) return []
146
-
147
- const levelNode = nodes[currentLevelId]
148
- if (!levelNode || levelNode.type !== 'level') return []
149
-
150
- return (levelNode as LevelNode).children
151
- .map((childId) => nodes[childId])
152
- .filter((node): node is WallNode => node?.type === 'wall')
153
- }
154
-
155
- export const WallTool: React.FC = () => {
156
- const unit = useViewer((state) => state.unit)
157
- const cursorRef = useRef<Group>(null)
158
- const wallPreviewRef = useRef<Mesh>(null!)
159
- // All positions are building-local: this tool is inside the ToolManager building group,
160
- // so local coords are used for both data and visual positioning.
161
- const startingPoint = useRef(new Vector3(0, 0, 0))
162
- const endingPoint = useRef(new Vector3(0, 0, 0))
163
- const buildingState = useRef(0)
164
- const shiftPressed = useRef(false)
165
- const [draftMeasurement, setDraftMeasurement] = useState<DraftMeasurementState>(null)
166
-
167
- useEffect(() => {
168
- let gridPosition: WallPlanPoint = [0, 0]
169
- let previousWallEnd: [number, number] | null = null
170
-
171
- const onGridMove = (event: GridEvent) => {
172
- if (!(cursorRef.current && wallPreviewRef.current)) return
173
-
174
- const walls = getCurrentLevelWalls()
175
- // event.localPosition is building-local — consistent with stored wall start/end
176
- const localPoint: WallPlanPoint = [event.localPosition[0], event.localPosition[2]]
177
- gridPosition = snapWallDraftPoint({ point: localPoint, walls })
178
-
179
- if (buildingState.current === 1) {
180
- const snappedLocal = snapWallDraftPoint({
181
- point: localPoint,
182
- walls,
183
- start: [startingPoint.current.x, startingPoint.current.z],
184
- angleSnap: !shiftPressed.current,
185
- })
186
- endingPoint.current.set(snappedLocal[0], event.localPosition[1], snappedLocal[1])
187
- cursorRef.current.position.copy(endingPoint.current)
188
-
189
- // Play snap sound only when the actual wall end position changes
190
- const currentWallEnd: [number, number] = [snappedLocal[0], snappedLocal[1]]
191
- if (
192
- previousWallEnd &&
193
- (currentWallEnd[0] !== previousWallEnd[0] || currentWallEnd[1] !== previousWallEnd[1])
194
- ) {
195
- sfxEmitter.emit('sfx:grid-snap')
196
- }
197
- previousWallEnd = currentWallEnd
198
-
199
- updateWallPreview(wallPreviewRef.current, startingPoint.current, endingPoint.current)
200
- setDraftMeasurement(
201
- getDraftMeasurementState(
202
- [startingPoint.current.x, startingPoint.current.z],
203
- snappedLocal,
204
- walls,
205
- unit,
206
- ),
207
- )
208
- } else {
209
- // Not drawing a wall yet, show the snapped anchor point.
210
- cursorRef.current.position.set(gridPosition[0], event.localPosition[1], gridPosition[1])
211
- setDraftMeasurement(null)
212
- }
213
- }
214
-
215
- const onGridClick = (event: GridEvent) => {
216
- const walls = getCurrentLevelWalls()
217
- const localClick: WallPlanPoint = [event.localPosition[0], event.localPosition[2]]
218
-
219
- if (buildingState.current === 0) {
220
- const snappedStart = snapWallDraftPoint({ point: localClick, walls })
221
- gridPosition = snappedStart
222
- startingPoint.current.set(snappedStart[0], event.localPosition[1], snappedStart[1])
223
- endingPoint.current.copy(startingPoint.current)
224
- buildingState.current = 1
225
- wallPreviewRef.current.visible = true
226
- setDraftMeasurement(null)
227
- } else if (buildingState.current === 1) {
228
- const snappedEnd = snapWallDraftPoint({
229
- point: localClick,
230
- walls,
231
- start: [startingPoint.current.x, startingPoint.current.z],
232
- angleSnap: !shiftPressed.current,
233
- })
234
- const dx = snappedEnd[0] - startingPoint.current.x
235
- const dz = snappedEnd[1] - startingPoint.current.z
236
- if (dx * dx + dz * dz < 0.01 * 0.01) return
237
- // Both start and end are building-local ✓
238
- createWallOnCurrentLevel([startingPoint.current.x, startingPoint.current.z], snappedEnd)
239
- wallPreviewRef.current.visible = false
240
- buildingState.current = 0
241
- setDraftMeasurement(null)
242
- }
243
- }
244
-
245
- const onKeyDown = (e: KeyboardEvent) => {
246
- if (e.key === 'Shift') {
247
- shiftPressed.current = true
248
- }
249
- }
250
-
251
- const onKeyUp = (e: KeyboardEvent) => {
252
- if (e.key === 'Shift') {
253
- shiftPressed.current = false
254
- }
255
- }
256
-
257
- const onCancel = () => {
258
- if (buildingState.current === 1) {
259
- markToolCancelConsumed()
260
- buildingState.current = 0
261
- wallPreviewRef.current.visible = false
262
- setDraftMeasurement(null)
263
- }
264
- }
265
-
266
- emitter.on('grid:move', onGridMove)
267
- emitter.on('grid:click', onGridClick)
268
- emitter.on('tool:cancel', onCancel)
269
- window.addEventListener('keydown', onKeyDown)
270
- window.addEventListener('keyup', onKeyUp)
271
-
272
- return () => {
273
- emitter.off('grid:move', onGridMove)
274
- emitter.off('grid:click', onGridClick)
275
- emitter.off('tool:cancel', onCancel)
276
- window.removeEventListener('keydown', onKeyDown)
277
- window.removeEventListener('keyup', onKeyUp)
278
- }
279
- }, [unit])
280
-
281
- return (
282
- <group>
283
- {/* Cursor indicator */}
284
- <CursorSphere ref={cursorRef} />
285
-
286
- {/* Wall preview */}
287
- <mesh layers={EDITOR_LAYER} ref={wallPreviewRef} renderOrder={1} visible={false}>
288
- <shapeGeometry />
289
- <meshBasicMaterial
290
- color="#818cf8"
291
- depthTest={false}
292
- depthWrite={false}
293
- opacity={0.5}
294
- side={DoubleSide}
295
- transparent
296
- />
297
- </mesh>
298
-
299
- {draftMeasurement && (
300
- <>
301
- <DraftMeasurementLabel
302
- label={draftMeasurement.lengthLabel}
303
- position={draftMeasurement.lengthPosition}
304
- />
305
- {draftMeasurement.angleLabels.map((angleLabel) => (
306
- <DraftMeasurementLabel
307
- key={angleLabel.id}
308
- label={angleLabel.label}
309
- position={angleLabel.position}
310
- />
311
- ))}
312
- </>
313
- )}
314
- </group>
315
- )
316
- }
317
-
318
- function DraftMeasurementLabel({
319
- label,
320
- position,
321
- }: {
322
- label: string
323
- position: [number, number, number]
324
- }) {
325
- return (
326
- <Html center position={position} style={{ pointerEvents: 'none' }} zIndexRange={[100, 0]}>
327
- <div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono font-semibold text-[11px] text-foreground shadow-lg backdrop-blur-md">
328
- {label}
329
- </div>
330
- </Html>
331
- )
332
- }