@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,302 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNodeId,
5
- emitter,
6
- type FenceNode,
7
- type GridEvent,
8
- type LevelNode,
9
- sceneRegistry,
10
- useLiveTransforms,
11
- useScene,
12
- type WallNode,
13
- } from '@pascal-app/core'
14
- import { useViewer } from '@pascal-app/viewer'
15
- import { useCallback, useEffect, useRef, useState } from 'react'
16
- import type * as THREE from 'three'
17
- import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
18
- import { sfxEmitter } from '../../../lib/sfx-bus'
19
- import useEditor from '../../../store/use-editor'
20
- import { CursorSphere } from '../shared/cursor-sphere'
21
- import { snapFenceDraftPoint } from './fence-drafting'
22
-
23
- function samePoint(a: [number, number], b: [number, number]) {
24
- return a[0] === b[0] && a[1] === b[1]
25
- }
26
-
27
- type LinkedFenceSnapshot = {
28
- id: FenceNode['id']
29
- start: [number, number]
30
- end: [number, number]
31
- }
32
-
33
- function getLinkedFenceSnapshots(args: {
34
- fenceId: FenceNode['id']
35
- fenceParentId: string | null
36
- originalStart: [number, number]
37
- originalEnd: [number, number]
38
- }) {
39
- const { fenceId, fenceParentId, originalStart, originalEnd } = args
40
- const { nodes } = useScene.getState()
41
- const snapshots: LinkedFenceSnapshot[] = []
42
-
43
- for (const node of Object.values(nodes)) {
44
- if (!(node?.type === 'fence' && node.id !== fenceId)) {
45
- continue
46
- }
47
-
48
- if ((node.parentId ?? null) !== fenceParentId) {
49
- continue
50
- }
51
-
52
- if (
53
- !(
54
- samePoint(node.start, originalStart) ||
55
- samePoint(node.start, originalEnd) ||
56
- samePoint(node.end, originalStart) ||
57
- samePoint(node.end, originalEnd)
58
- )
59
- ) {
60
- continue
61
- }
62
-
63
- snapshots.push({
64
- id: node.id,
65
- start: [...node.start] as [number, number],
66
- end: [...node.end] as [number, number],
67
- })
68
- }
69
-
70
- return snapshots
71
- }
72
-
73
- function getLinkedFenceUpdates(
74
- linkedFences: LinkedFenceSnapshot[],
75
- originalStart: [number, number],
76
- originalEnd: [number, number],
77
- nextStart: [number, number],
78
- nextEnd: [number, number],
79
- ) {
80
- return linkedFences.map((fence) => ({
81
- id: fence.id,
82
- start: samePoint(fence.start, originalStart)
83
- ? nextStart
84
- : samePoint(fence.start, originalEnd)
85
- ? nextEnd
86
- : fence.start,
87
- end: samePoint(fence.end, originalStart)
88
- ? nextStart
89
- : samePoint(fence.end, originalEnd)
90
- ? nextEnd
91
- : fence.end,
92
- }))
93
- }
94
-
95
- export const MoveFenceTool: React.FC<{ node: FenceNode }> = ({ node }) => {
96
- const activatedAtRef = useRef<number>(Date.now())
97
- const previousGridPosRef = useRef<[number, number] | null>(null)
98
- const originalStartRef = useRef<[number, number]>([...node.start] as [number, number])
99
- const originalEndRef = useRef<[number, number]>([...node.end] as [number, number])
100
- const linkedOriginalsRef = useRef(
101
- getLinkedFenceSnapshots({
102
- fenceId: node.id,
103
- fenceParentId: node.parentId ?? null,
104
- originalStart: node.start,
105
- originalEnd: node.end,
106
- }),
107
- )
108
- const dragAnchorRef = useRef<[number, number] | null>(null)
109
- const nodeIdRef = useRef(node.id)
110
- const previewRef = useRef<{ start: [number, number]; end: [number, number] } | null>(null)
111
-
112
- const [cursorLocalPos, setCursorLocalPos] = useState<[number, number, number]>(() => {
113
- const centerX = (node.start[0] + node.end[0]) / 2
114
- const centerZ = (node.start[1] + node.end[1]) / 2
115
- return [centerX, 0, centerZ]
116
- })
117
-
118
- const exitMoveMode = useCallback(() => {
119
- useEditor.getState().setMovingNode(null)
120
- }, [])
121
-
122
- useEffect(() => {
123
- const nodeId = nodeIdRef.current
124
- const originalStart = originalStartRef.current
125
- const originalEnd = originalEndRef.current
126
- const levelNode =
127
- node.parentId && useScene.getState().nodes[node.parentId as AnyNodeId]?.type === 'level'
128
- ? (useScene.getState().nodes[node.parentId as AnyNodeId] as LevelNode)
129
- : null
130
- const levelChildren = levelNode?.children ?? []
131
- const levelWalls = levelChildren
132
- .map((childId) => useScene.getState().nodes[childId as AnyNodeId])
133
- .filter((child): child is WallNode => child?.type === 'wall')
134
- const levelFences = levelChildren
135
- .map((childId) => useScene.getState().nodes[childId as AnyNodeId])
136
- .filter((child): child is FenceNode => child?.type === 'fence')
137
-
138
- useScene.temporal.getState().pause()
139
- let wasCommitted = false
140
-
141
- const setMeshOffset = (fenceId: FenceNode['id'], deltaX: number, deltaZ: number) => {
142
- const mesh = sceneRegistry.nodes.get(fenceId) as THREE.Object3D | undefined
143
- if (!mesh) {
144
- return
145
- }
146
-
147
- mesh.position.set(deltaX, 0, deltaZ)
148
- }
149
-
150
- const setFenceLiveTransform = (fence: FenceNode, deltaX: number, deltaZ: number) => {
151
- const originalCenterX = (fence.start[0] + fence.end[0]) / 2
152
- const originalCenterZ = (fence.start[1] + fence.end[1]) / 2
153
- useLiveTransforms.getState().set(fence.id, {
154
- position: [originalCenterX + deltaX, 0, originalCenterZ + deltaZ],
155
- rotation: 0,
156
- })
157
- }
158
-
159
- const clearPreviewState = () => {
160
- setMeshOffset(nodeId, 0, 0)
161
- useLiveTransforms.getState().clear(nodeId)
162
-
163
- for (const linkedFence of linkedOriginalsRef.current) {
164
- setMeshOffset(linkedFence.id, 0, 0)
165
- useLiveTransforms.getState().clear(linkedFence.id)
166
- }
167
- }
168
-
169
- const applyNodePreview = (
170
- updates: Array<{ id: FenceNode['id']; start: [number, number]; end: [number, number] }>,
171
- ) => {
172
- useScene.getState().updateNodes(
173
- updates.map((entry) => ({
174
- id: entry.id as AnyNodeId,
175
- data: { start: entry.start, end: entry.end },
176
- })),
177
- )
178
- for (const entry of updates) {
179
- useScene.getState().markDirty(entry.id as AnyNodeId)
180
- }
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
- const deltaX = nextStart[0] - originalStart[0]
189
- const deltaZ = nextStart[1] - originalStart[1]
190
- setMeshOffset(nodeId, deltaX, deltaZ)
191
- setFenceLiveTransform(node, deltaX, deltaZ)
192
-
193
- for (const linkedFence of linkedOriginalsRef.current) {
194
- setMeshOffset(linkedFence.id, deltaX, deltaZ)
195
- setFenceLiveTransform(
196
- {
197
- ...node,
198
- id: linkedFence.id,
199
- start: linkedFence.start,
200
- end: linkedFence.end,
201
- },
202
- deltaX,
203
- deltaZ,
204
- )
205
- }
206
- }
207
-
208
- const onGridMove = (event: GridEvent) => {
209
- const [localX, localZ] = snapFenceDraftPoint({
210
- point: [event.localPosition[0], event.localPosition[2]],
211
- walls: levelWalls,
212
- fences: levelFences,
213
- ignoreFenceIds: [nodeId],
214
- })
215
-
216
- if (
217
- previousGridPosRef.current &&
218
- (localX !== previousGridPosRef.current[0] || localZ !== previousGridPosRef.current[1])
219
- ) {
220
- sfxEmitter.emit('sfx:grid-snap')
221
- }
222
- previousGridPosRef.current = [localX, localZ]
223
-
224
- const anchor = dragAnchorRef.current ?? [localX, localZ]
225
- dragAnchorRef.current = anchor
226
-
227
- const deltaX = localX - anchor[0]
228
- const deltaZ = localZ - anchor[1]
229
-
230
- const nextStart: [number, number] = [originalStart[0] + deltaX, originalStart[1] + deltaZ]
231
- const nextEnd: [number, number] = [originalEnd[0] + deltaX, originalEnd[1] + deltaZ]
232
-
233
- applyPreview(nextStart, nextEnd)
234
- }
235
-
236
- const onGridClick = (event: GridEvent) => {
237
- if (Date.now() - activatedAtRef.current < 150) {
238
- event.nativeEvent?.stopPropagation?.()
239
- return
240
- }
241
-
242
- const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
243
-
244
- wasCommitted = true
245
-
246
- useScene.temporal.getState().resume()
247
- applyNodePreview([
248
- { id: nodeId, start: preview.start, end: preview.end },
249
- ...getLinkedFenceUpdates(
250
- linkedOriginalsRef.current,
251
- originalStart,
252
- originalEnd,
253
- preview.start,
254
- preview.end,
255
- ),
256
- ])
257
- useLiveTransforms.getState().clear(nodeId)
258
- for (const linkedFence of linkedOriginalsRef.current) {
259
- useLiveTransforms.getState().clear(linkedFence.id)
260
- }
261
- useScene.temporal.getState().pause()
262
-
263
- sfxEmitter.emit('sfx:item-place')
264
- useViewer.getState().setSelection({ selectedIds: [nodeId] })
265
- exitMoveMode()
266
- event.nativeEvent?.stopPropagation?.()
267
- }
268
-
269
- const onCancel = () => {
270
- clearPreviewState()
271
- useViewer.getState().setSelection({ selectedIds: [nodeId] })
272
- useScene.temporal.getState().resume()
273
- markToolCancelConsumed()
274
- exitMoveMode()
275
- }
276
-
277
- emitter.on('grid:move', onGridMove)
278
- emitter.on('grid:click', onGridClick)
279
- emitter.on('tool:cancel', onCancel)
280
-
281
- return () => {
282
- if (wasCommitted) {
283
- useLiveTransforms.getState().clear(nodeId)
284
- for (const linkedFence of linkedOriginalsRef.current) {
285
- useLiveTransforms.getState().clear(linkedFence.id)
286
- }
287
- } else {
288
- clearPreviewState()
289
- }
290
- useScene.temporal.getState().resume()
291
- emitter.off('grid:move', onGridMove)
292
- emitter.off('grid:click', onGridClick)
293
- emitter.off('tool:cancel', onCancel)
294
- }
295
- }, [exitMoveMode, node])
296
-
297
- return (
298
- <group>
299
- <CursorSphere position={cursorLocalPos} showTooltip={false} />
300
- </group>
301
- )
302
- }
@@ -1,26 +0,0 @@
1
- import { sfxEmitter } from '../../../lib/sfx-bus'
2
- import useEditor from '../../../store/use-editor'
3
- import { useDraftNode } from './use-draft-node'
4
- import { usePlacementCoordinator } from './use-placement-coordinator'
5
-
6
- export const ItemTool: React.FC = () => {
7
- const selectedItem = useEditor((state) => state.selectedItem)
8
- const draftNode = useDraftNode()
9
-
10
- const cursor = usePlacementCoordinator({
11
- asset: selectedItem,
12
- draftNode,
13
- initDraft: (gridPosition) => {
14
- if (selectedItem && !selectedItem.attachTo) {
15
- draftNode.create(gridPosition, selectedItem)
16
- }
17
- },
18
- onCommitted: () => {
19
- sfxEmitter.emit('sfx:item-place')
20
- return true
21
- },
22
- })
23
-
24
- if (!selectedItem) return null
25
- return <>{cursor}</>
26
- }
@@ -1,364 +0,0 @@
1
- import {
2
- type AnyNodeId,
3
- emitter,
4
- type FenceNode,
5
- type GridEvent,
6
- type LevelNode,
7
- type RoofNode,
8
- type RoofSegmentNode,
9
- type StairNode,
10
- type StairSegmentNode,
11
- sceneRegistry,
12
- useLiveTransforms,
13
- useScene,
14
- type WallNode,
15
- } from '@pascal-app/core'
16
- import { useViewer } from '@pascal-app/viewer'
17
- import { useCallback, useEffect, useRef, useState } from 'react'
18
- import * as THREE from 'three'
19
- import { clearRoofDuplicateMetadata } from '../../../lib/roof-duplication'
20
- import { sfxEmitter } from '../../../lib/sfx-bus'
21
- import useEditor from '../../../store/use-editor'
22
- import { snapFenceDraftPoint } from '../fence/fence-drafting'
23
- import { CursorSphere } from '../shared/cursor-sphere'
24
- import type { WallPlanPoint } from '../wall/wall-drafting'
25
-
26
- export const MoveRoofTool: React.FC<{
27
- node: RoofNode | RoofSegmentNode | StairNode | StairSegmentNode
28
- }> = ({ node: movingNode }) => {
29
- const exitMoveMode = useCallback(() => {
30
- useEditor.getState().setMovingNode(null)
31
- }, [])
32
-
33
- const previousGridPosRef = useRef<[number, number] | null>(null)
34
-
35
- const [cursorWorldPos, setCursorWorldPos] = useState<[number, number, number]>(() => {
36
- const obj = sceneRegistry.nodes.get(movingNode.id)
37
- if (obj) {
38
- const worldPos = obj.getWorldPosition(new THREE.Vector3())
39
- // Cursor renders inside the building-local ToolManager group, so convert
40
- // world → building-local to honor any building rotation.
41
- const buildingId = useViewer.getState().selection.buildingId
42
- const buildingObj = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
43
- if (buildingObj) buildingObj.worldToLocal(worldPos)
44
- return [worldPos.x, worldPos.y, worldPos.z]
45
- }
46
- // Fallback if not registered (e.g. newly created duplicate without mesh yet)
47
- if (
48
- (movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') &&
49
- movingNode.parentId
50
- ) {
51
- const parentNode = useScene.getState().nodes[movingNode.parentId as AnyNodeId]
52
- if (parentNode && 'position' in parentNode && 'rotation' in parentNode) {
53
- const parentAngle = parentNode.rotation as number
54
- const px = parentNode.position[0] as number
55
- const py = parentNode.position[1] as number
56
- const pz = parentNode.position[2] as number
57
- const lx = movingNode.position[0]
58
- const ly = movingNode.position[1]
59
- const lz = movingNode.position[2]
60
-
61
- const wx = lx * Math.cos(parentAngle) - lz * Math.sin(parentAngle) + px
62
- const wz = lx * Math.sin(parentAngle) + lz * Math.cos(parentAngle) + pz
63
- return [wx, py + ly, wz]
64
- }
65
- }
66
- return [movingNode.position[0], movingNode.position[1], movingNode.position[2]]
67
- })
68
-
69
- useEffect(() => {
70
- useScene.temporal.getState().pause()
71
-
72
- const meta =
73
- typeof movingNode.metadata === 'object' && movingNode.metadata !== null
74
- ? (movingNode.metadata as Record<string, unknown>)
75
- : {}
76
- const isNew = !!meta.isNew
77
- const committedMeta: RoofNode['metadata'] = (() => {
78
- if (
79
- typeof movingNode.metadata !== 'object' ||
80
- movingNode.metadata === null ||
81
- Array.isArray(movingNode.metadata)
82
- ) {
83
- return movingNode.metadata
84
- }
85
-
86
- const nextMeta = { ...movingNode.metadata } as Record<string, unknown>
87
- delete nextMeta.isNew
88
- delete nextMeta.isTransient
89
- return nextMeta as RoofNode['metadata']
90
- })()
91
-
92
- const original = {
93
- position: [...movingNode.position] as [number, number, number],
94
- rotation: movingNode.rotation,
95
- parentId: movingNode.parentId,
96
- metadata: movingNode.metadata,
97
- }
98
-
99
- // Track whether the move was committed so cleanup knows whether to revert.
100
- // We avoid setting isTransient on the store to prevent RoofSystem from
101
- // resetting the mesh position (it resets on dirty) and from triggering
102
- // expensive merged-mesh CSG rebuilds on every frame.
103
- let wasCommitted = false
104
- let wasCancelled = false
105
-
106
- // Track pending rotation — no store updates during drag
107
- let pendingRotation: number = movingNode.rotation as number
108
-
109
- // For roof-segment moves: the selection was cleared before entering move mode,
110
- // so isSelected=false on the parent roof, hiding individual segment meshes and
111
- // showing only the merged mesh. We directly flip Three.js visibility so the
112
- // user sees the individual segment tracking the cursor.
113
- let segmentWrapperGroup: THREE.Object3D | null = null
114
- let mergedRoofMesh: THREE.Object3D | null = null
115
- if (movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') {
116
- const segmentMesh = sceneRegistry.nodes.get(movingNode.id)
117
- if (segmentMesh?.parent) {
118
- // segmentMesh.parent = <group visible={isSelected}> wrapper in Roof/StairRenderer
119
- // segmentMesh.parent.parent = the registered roof/stair group
120
- segmentWrapperGroup = segmentMesh.parent
121
- const mergedName = movingNode.type === 'stair-segment' ? 'merged-stair' : 'merged-roof'
122
- mergedRoofMesh = segmentMesh.parent.parent?.getObjectByName(mergedName) ?? null
123
- segmentWrapperGroup.visible = true
124
- if (mergedRoofMesh) mergedRoofMesh.visible = false
125
- }
126
- }
127
-
128
- const resolveLevelId = () => {
129
- if (movingNode.type === 'roof' || movingNode.type === 'stair') {
130
- return movingNode.parentId ?? null
131
- }
132
-
133
- if (
134
- (movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') &&
135
- movingNode.parentId
136
- ) {
137
- const parentNode = useScene.getState().nodes[movingNode.parentId as AnyNodeId]
138
- return parentNode && 'parentId' in parentNode ? (parentNode.parentId ?? null) : null
139
- }
140
-
141
- return null
142
- }
143
-
144
- const levelId = resolveLevelId()
145
- const levelNode =
146
- levelId && useScene.getState().nodes[levelId as AnyNodeId]?.type === 'level'
147
- ? (useScene.getState().nodes[levelId as AnyNodeId] as LevelNode)
148
- : null
149
- const levelChildren = levelNode?.children ?? []
150
- const levelWalls = levelChildren
151
- .map((childId) => useScene.getState().nodes[childId as AnyNodeId])
152
- .filter((node): node is WallNode => node?.type === 'wall')
153
- const levelFences = levelChildren
154
- .map((childId) => useScene.getState().nodes[childId as AnyNodeId])
155
- .filter((node): node is FenceNode => node?.type === 'fence')
156
- const buildingId = useViewer.getState().selection.buildingId
157
- const buildingObj = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
158
-
159
- const localToWorldPoint = (localPoint: WallPlanPoint, y: number): [number, number, number] => {
160
- if (buildingObj) {
161
- const worldPoint = buildingObj.localToWorld(
162
- new THREE.Vector3(localPoint[0], y, localPoint[1]),
163
- )
164
- return [worldPoint.x, worldPoint.y, worldPoint.z]
165
- }
166
-
167
- return [localPoint[0], y, localPoint[1]]
168
- }
169
-
170
- const computeLocal = (
171
- gridX: number,
172
- gridZ: number,
173
- y: number,
174
- buildingLocalX: number,
175
- buildingLocalZ: number,
176
- ): [number, number] => {
177
- // Segments have a transformed parent (stair/roof). Convert world → parent-local
178
- // via Three.js hierarchy so the segment's stored position stays parent-relative.
179
- if (
180
- (movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') &&
181
- movingNode.parentId
182
- ) {
183
- const parentNode = useScene.getState().nodes[movingNode.parentId as AnyNodeId]
184
- if (parentNode && 'position' in parentNode && 'rotation' in parentNode) {
185
- const parentObj = sceneRegistry.nodes.get(movingNode.parentId)
186
- if (parentObj) {
187
- const worldVec = new THREE.Vector3(gridX, y, gridZ)
188
- parentObj.worldToLocal(worldVec)
189
- return [worldVec.x, worldVec.z]
190
- }
191
- const dx = gridX - (parentNode.position[0] as number)
192
- const dz = gridZ - (parentNode.position[2] as number)
193
- const angle = -(parentNode.rotation as number)
194
- return [
195
- dx * Math.cos(angle) - dz * Math.sin(angle),
196
- dx * Math.sin(angle) + dz * Math.cos(angle),
197
- ]
198
- }
199
- }
200
-
201
- // Stair/roof live directly in the level — their stored position is building-local.
202
- // event.localPosition is already building-local, so using it handles building rotation.
203
- return [buildingLocalX, buildingLocalZ]
204
- }
205
-
206
- const onGridMove = (event: GridEvent) => {
207
- const y = event.position[1]
208
-
209
- const snappedLocal = snapFenceDraftPoint({
210
- point: [event.localPosition[0], event.localPosition[2]],
211
- walls: levelWalls,
212
- fences: levelFences,
213
- })
214
- const [gridX, , gridZ] = localToWorldPoint(snappedLocal, y)
215
-
216
- if (
217
- previousGridPosRef.current &&
218
- (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
219
- ) {
220
- sfxEmitter.emit('sfx:grid-snap')
221
- }
222
-
223
- previousGridPosRef.current = [gridX, gridZ]
224
- const [lx, lz] = snappedLocal
225
- setCursorWorldPos([lx, event.localPosition[1], lz])
226
-
227
- const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
228
-
229
- // Directly update the Three.js mesh — no store update during drag
230
- const mesh = sceneRegistry.nodes.get(movingNode.id)
231
- if (mesh) {
232
- mesh.position.x = localX
233
- mesh.position.z = localZ
234
- }
235
-
236
- // Publish world-space position so the 2D floorplan can track the drag
237
- useLiveTransforms.getState().set(movingNode.id, {
238
- position: [gridX, y, gridZ],
239
- rotation: pendingRotation,
240
- })
241
- }
242
-
243
- const onGridClick = (event: GridEvent) => {
244
- const y = event.position[1]
245
- const snappedLocal = snapFenceDraftPoint({
246
- point: [event.localPosition[0], event.localPosition[2]],
247
- walls: levelWalls,
248
- fences: levelFences,
249
- })
250
- const [gridX, , gridZ] = localToWorldPoint(snappedLocal, y)
251
- const [lx, lz] = snappedLocal
252
-
253
- const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
254
-
255
- wasCommitted = true
256
-
257
- // The store still holds the original values (we didn't update during drag).
258
- // Resume temporal and apply the final state as a single undoable step.
259
- useScene.temporal.getState().resume()
260
-
261
- if (isNew && movingNode.type === 'roof') {
262
- clearRoofDuplicateMetadata(movingNode.id as AnyNodeId, {
263
- position: [localX, movingNode.position[1], localZ],
264
- rotation: pendingRotation,
265
- metadata: committedMeta,
266
- })
267
- } else {
268
- useScene.getState().updateNode(movingNode.id, {
269
- position: [localX, movingNode.position[1], localZ],
270
- rotation: pendingRotation,
271
- metadata: committedMeta,
272
- })
273
- }
274
-
275
- useScene.temporal.getState().pause()
276
-
277
- sfxEmitter.emit('sfx:item-place')
278
- useViewer.getState().setSelection({ selectedIds: [movingNode.id] })
279
- useLiveTransforms.getState().clear(movingNode.id)
280
- exitMoveMode()
281
- event.nativeEvent?.stopPropagation?.()
282
- }
283
-
284
- const onCancel = () => {
285
- wasCancelled = true
286
- useLiveTransforms.getState().clear(movingNode.id)
287
- if (isNew) {
288
- useScene.getState().deleteNode(movingNode.id)
289
- } else {
290
- useScene.getState().updateNode(movingNode.id, {
291
- position: original.position,
292
- rotation: original.rotation,
293
- metadata: original.metadata,
294
- })
295
- }
296
- useScene.temporal.getState().resume()
297
- exitMoveMode()
298
- }
299
-
300
- const onKeyDown = (event: KeyboardEvent) => {
301
- if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
302
- return
303
- }
304
-
305
- const ROTATION_STEP = Math.PI / 4
306
- let rotationDelta = 0
307
- if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
308
- else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
309
-
310
- if (rotationDelta !== 0) {
311
- event.preventDefault()
312
- sfxEmitter.emit('sfx:item-rotate')
313
-
314
- pendingRotation += rotationDelta
315
-
316
- // Directly update the Three.js mesh — no store update during drag
317
- const mesh = sceneRegistry.nodes.get(movingNode.id)
318
- if (mesh) mesh.rotation.y = pendingRotation
319
-
320
- // Update live transform rotation for 2D floorplan
321
- const currentLive = useLiveTransforms.getState().get(movingNode.id)
322
- if (currentLive) {
323
- useLiveTransforms.getState().set(movingNode.id, {
324
- ...currentLive,
325
- rotation: pendingRotation,
326
- })
327
- }
328
- }
329
- }
330
-
331
- emitter.on('grid:move', onGridMove)
332
- emitter.on('grid:click', onGridClick)
333
- emitter.on('tool:cancel', onCancel)
334
- window.addEventListener('keydown', onKeyDown)
335
-
336
- return () => {
337
- // Restore segment wrapper visibility (React will re-sync on next render)
338
- if (segmentWrapperGroup) segmentWrapperGroup.visible = false
339
- if (mergedRoofMesh) mergedRoofMesh.visible = true
340
-
341
- // Clear ephemeral live transform
342
- useLiveTransforms.getState().clear(movingNode.id)
343
-
344
- if (!(wasCommitted || wasCancelled || isNew)) {
345
- useScene.getState().updateNode(movingNode.id, {
346
- position: original.position,
347
- rotation: original.rotation,
348
- metadata: original.metadata,
349
- })
350
- }
351
- useScene.temporal.getState().resume()
352
- emitter.off('grid:move', onGridMove)
353
- emitter.off('grid:click', onGridClick)
354
- emitter.off('tool:cancel', onCancel)
355
- window.removeEventListener('keydown', onKeyDown)
356
- }
357
- }, [movingNode, exitMoveMode])
358
-
359
- return (
360
- <group>
361
- <CursorSphere position={cursorWorldPos} showTooltip={false} />
362
- </group>
363
- )
364
- }