@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
@@ -0,0 +1,38 @@
1
+ 'use client'
2
+
3
+ import { type AnyNodeId, useScene } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
5
+ import { type ComponentType, Suspense, useMemo } from 'react'
6
+ import { getRegistryAffordanceTool } from '../tools/shared/affordance-dispatch'
7
+
8
+ /**
9
+ * Editor-mounted dispatcher for a kind's selection-time editing UI.
10
+ *
11
+ * Some kinds expose drag-to-edit affordances that should appear only
12
+ * while a single node of that kind is selected — duct / pipe / lineset
13
+ * path-point handles, fitting Alt-axis-cycling listeners. These read
14
+ * `useEditor` (grid snap step, rotation axis) and render the editor's
15
+ * `DimensionPill`, so they must NOT ride in `def.system` (which the
16
+ * viewer package mounts for the read-only route). The kind declares the
17
+ * component under `def.affordanceTools.selection` and this manager —
18
+ * mounted inside the editor only — loads it for the selected kind.
19
+ */
20
+ export function SelectionAffordanceManager() {
21
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
22
+ const selectedKind = useScene((s) => {
23
+ if (selectedIds.length !== 1) return null
24
+ return s.nodes[selectedIds[0] as AnyNodeId]?.type ?? null
25
+ })
26
+
27
+ const Component = useMemo<ComponentType | null>(() => {
28
+ if (!selectedKind) return null
29
+ return getRegistryAffordanceTool(selectedKind, 'selection')
30
+ }, [selectedKind])
31
+
32
+ if (!Component) return null
33
+ return (
34
+ <Suspense fallback={null}>
35
+ <Component />
36
+ </Suspense>
37
+ )
38
+ }
@@ -85,7 +85,7 @@ export const StairEditSystem = () => {
85
85
  }
86
86
 
87
87
  prevActiveStairIds.current = activeStairIds
88
- }, [selectedIds, selectedStairSignature])
88
+ }, [selectedIds])
89
89
 
90
90
  return null
91
91
  }
@@ -6,8 +6,10 @@ import { Check, Pencil } from 'lucide-react'
6
6
  import { useCallback, useEffect, useRef, useState } from 'react'
7
7
  import { createPortal } from 'react-dom'
8
8
  import { useShallow } from 'zustand/react/shallow'
9
+ import { resolveOverlayPolicy } from '../../../lib/interaction/overlay-policy'
9
10
  import { sfxEmitter } from '../../../lib/sfx-bus'
10
11
  import useEditor from '../../../store/use-editor'
12
+ import useInteractionScope from '../../../store/use-interaction-scope'
11
13
 
12
14
  // ─── Per-zone label editor ────────────────────────────────────────────────────
13
15
 
@@ -19,6 +21,10 @@ function ZoneLabelEditor({ zoneId }: { zoneId: ZoneNode['id'] }) {
19
21
  const selectedZoneId = useViewer((s) => s.selection.zoneId)
20
22
  const hoveredId = useViewer((s) => s.hoveredId)
21
23
  const mode = useEditor((s) => s.mode)
24
+ // During an active interaction the zone label is a context badge that steps
25
+ // back: faded + non-interactive so it can't be hovered/clicked mid-action.
26
+ const scope = useInteractionScope((s) => s.scope)
27
+ const labelStepBack = resolveOverlayPolicy(scope).contextBadges === 'faded'
22
28
  const isSelected = selectedZoneId === zoneId
23
29
  const isDeleteHovered = mode === 'delete' && hoveredId === zoneId
24
30
  const [editing, setEditing] = useState(false)
@@ -149,7 +155,8 @@ function ZoneLabelEditor({ zoneId }: { zoneId: ZoneNode['id'] }) {
149
155
  fontSize: 14,
150
156
  fontFamily: 'sans-serif',
151
157
  userSelect: 'none',
152
- pointerEvents: 'auto',
158
+ pointerEvents: labelStepBack ? 'none' : 'auto',
159
+ opacity: labelStepBack ? 0.4 : undefined,
153
160
  display: 'inline-flex',
154
161
  alignItems: 'center',
155
162
  gap: 4,
@@ -309,6 +316,12 @@ export function ZoneLabelEditorSystem() {
309
316
  .map((n) => n.id as ZoneNode['id']),
310
317
  ),
311
318
  )
319
+ // The zone renderer unmounts its <Html> label when zones are hidden —
320
+ // unmount the portals with it, or each editor would hold (and rAF-poll for)
321
+ // a detached label element.
322
+ const showZones = useViewer((s) => s.showZones)
323
+
324
+ if (!showZones) return null
312
325
 
313
326
  return (
314
327
  <>
@@ -1,21 +1,46 @@
1
1
  import { sceneRegistry, useScene, type ZoneNode } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
3
  import { useFrame } from '@react-three/fiber'
4
+ import { useEffect } from 'react'
4
5
  import { type Group, MathUtils, type Mesh } from 'three'
5
6
  import type { MeshBasicNodeMaterial } from 'three/webgpu'
7
+ import { resolveOverlayPolicy } from '../../../lib/interaction/overlay-policy'
6
8
  import useEditor from '../../../store/use-editor'
9
+ import useInteractionScope from '../../../store/use-interaction-scope'
7
10
 
8
11
  // Disable raycasting on zone geometry so clicks pass through to items underneath.
9
12
  // Zone selection in the editor is handled exclusively via the HTML label overlay.
10
13
  const noopRaycast = () => {}
11
14
 
12
15
  export const ZoneSystem = () => {
16
+ // Outside the zones layer (or during snapshot capture) zones unmount
17
+ // entirely — meshes AND drei <Html> labels, which cost per-frame matrix work
18
+ // + live DOM even at opacity 0. The renderer reads this viewer flag; the
19
+ // unmount cleanup restores the default so preview / first-person surfaces
20
+ // (which swap this system for ViewerZoneSystem) keep their labels.
21
+ const structureLayerState = useEditor((s) => s.structureLayer)
22
+ const isCaptureModeState = useEditor((s) => s.isCaptureMode)
23
+ useEffect(() => {
24
+ useViewer.getState().setShowZones(structureLayerState === 'zones' && !isCaptureModeState)
25
+ return () => useViewer.getState().setShowZones(true)
26
+ }, [structureLayerState, isCaptureModeState])
27
+
13
28
  useFrame((_, delta) => {
29
+ if (!useViewer.getState().showZones) return
30
+
14
31
  const structureLayer = useEditor.getState().structureLayer
15
32
  const editorMode = useEditor.getState().mode
16
33
  const selectedLevelId = useViewer.getState().selection.levelId
17
34
  const selectedZoneId = useViewer.getState().selection.zoneId
18
35
  const hoveredId = useViewer.getState().hoveredId
36
+ // Snapshot capture is a clean, camera-only surface — never show zone
37
+ // geometry or the HTML zone tags in the framed shot.
38
+ const isCaptureMode = useEditor.getState().isCaptureMode
39
+
40
+ // During any active interaction zone labels step back entirely — they are
41
+ // not a primary editing concern and would distract / invite misclicks.
42
+ const zoneLabelsHidden =
43
+ resolveOverlayPolicy(useInteractionScope.getState().scope).zoneLabels === 'hidden'
19
44
 
20
45
  const zoneGeometryVisible = structureLayer === 'zones'
21
46
  const zones = sceneRegistry.byType.zone || new Set()
@@ -35,12 +60,22 @@ export const ZoneSystem = () => {
35
60
  // Keep group visible (so <Html> labels stay active), hide/show meshes only.
36
61
  // Show meshes when: in zone mode, selected, or delete-hovered.
37
62
  if (!obj.visible) obj.visible = true
38
- const meshVisible = zoneGeometryVisible || isSelected || isDeleteHovered
39
- const targetOpacity = isSelected || isDeleteHovered ? 1 : zoneGeometryVisible ? 1 : 0
63
+ const meshVisible = !isCaptureMode && (zoneGeometryVisible || isSelected || isDeleteHovered)
64
+ const targetOpacity = isCaptureMode
65
+ ? 0
66
+ : isSelected || isDeleteHovered
67
+ ? 1
68
+ : zoneGeometryVisible
69
+ ? 1
70
+ : 0
40
71
 
72
+ // Raycast is re-disabled per frame (not once per group): the meshes
73
+ // remount whenever the zones layer toggles, so a one-shot flag on the
74
+ // persistent group would leave fresh meshes clickable.
41
75
  const walls = (obj as Group).getObjectByName('walls') as Mesh | undefined
42
76
  if (walls) {
43
77
  walls.visible = meshVisible
78
+ walls.raycast = noopRaycast
44
79
  const material = walls.material as MeshBasicNodeMaterial
45
80
  if (material?.userData?.uOpacity) {
46
81
  material.userData.uOpacity.value = MathUtils.lerp(
@@ -54,6 +89,7 @@ export const ZoneSystem = () => {
54
89
  const floor = (obj as Group).getObjectByName('floor') as Mesh | undefined
55
90
  if (floor) {
56
91
  floor.visible = meshVisible
92
+ floor.raycast = noopRaycast
57
93
  const material = floor.material as MeshBasicNodeMaterial
58
94
  if (material?.userData?.uOpacity) {
59
95
  material.userData.uOpacity.value = MathUtils.lerp(
@@ -64,17 +100,10 @@ export const ZoneSystem = () => {
64
100
  }
65
101
  }
66
102
 
67
- // Disable raycasting once per zone object so geometry never intercepts clicks
68
- if (!obj.userData.__raycastDisabled) {
69
- obj.raycast = noopRaycast
70
- obj.traverse((child) => {
71
- child.raycast = noopRaycast
72
- })
73
- obj.userData.__raycastDisabled = true
74
- }
75
-
76
- // Labels: always visible on the current level (regardless of mode)
77
- const showLabel = !!selectedLevelId && isOnSelectedLevel
103
+ // Labels: visible on the current level (regardless of mode), but never
104
+ // during snapshot capture.
105
+ const showLabel =
106
+ !isCaptureMode && !zoneLabelsHidden && !!selectedLevelId && isOnSelectedLevel
78
107
  const labelOpacity = showLabel ? '1' : '0'
79
108
  const labelEl = document.getElementById(`${zoneId}-label`)
80
109
  if (labelEl && labelEl.style.opacity !== labelOpacity) {
@@ -0,0 +1,8 @@
1
+ export const DEFAULT_ELEVATOR_WIDTH = 1.84
2
+ export const DEFAULT_ELEVATOR_DEPTH = 1.84
3
+ export const DEFAULT_ELEVATOR_CAB_HEIGHT = 2.35
4
+ export const DEFAULT_ELEVATOR_DOOR_WIDTH = 0.95
5
+ export const DEFAULT_ELEVATOR_DOOR_HEIGHT = 2.1
6
+ export const DEFAULT_ELEVATOR_SPEED = 2.2
7
+ export const DEFAULT_ELEVATOR_DOOR_DURATION_MS = 900
8
+ export const DEFAULT_ELEVATOR_DWELL_MS = 1400
@@ -0,0 +1,375 @@
1
+ import {
2
+ type AnyNodeId,
3
+ type BuildingNode,
4
+ collectAlignmentAnchors,
5
+ ElevatorNode,
6
+ emitter,
7
+ type GridEvent,
8
+ type LevelNode,
9
+ resolveAlignment,
10
+ useScene,
11
+ } from '@pascal-app/core'
12
+ import { useEffect, useMemo, useRef } from 'react'
13
+ import * as THREE from 'three'
14
+ import { resolveCurrentBuildingId, resolveElevatorSupportY } from '../../../lib/elevator-support'
15
+ import { sfxEmitter } from '../../../lib/sfx-bus'
16
+
17
+ import useAlignmentGuides from '../../../store/use-alignment-guides'
18
+ import useEditor, {
19
+ isAlignmentGuideActive,
20
+ isGridSnapActive,
21
+ isMagneticSnapActive,
22
+ } from '../../../store/use-editor'
23
+
24
+ import usePlacementPreview from '../../../store/use-placement-preview'
25
+ import { CursorSphere } from '../shared/cursor-sphere'
26
+ import {
27
+ DEFAULT_ELEVATOR_CAB_HEIGHT,
28
+ DEFAULT_ELEVATOR_DEPTH,
29
+ DEFAULT_ELEVATOR_DOOR_DURATION_MS,
30
+ DEFAULT_ELEVATOR_DOOR_HEIGHT,
31
+ DEFAULT_ELEVATOR_DOOR_WIDTH,
32
+ DEFAULT_ELEVATOR_DWELL_MS,
33
+ DEFAULT_ELEVATOR_SPEED,
34
+ DEFAULT_ELEVATOR_WIDTH,
35
+ } from './elevator-defaults'
36
+
37
+ const GRID_OFFSET = 0.02
38
+ /** Figma-style alignment-snap threshold (meters), matching the move tools. */
39
+ const ALIGNMENT_THRESHOLD_M = 0.08
40
+
41
+ type ElevatorToolProps = {
42
+ buildingId: BuildingNode['id'] | null
43
+ levelId: LevelNode['id'] | null
44
+ onPlaced?: (elevatorId: AnyNodeId, buildingId: BuildingNode['id']) => void
45
+ }
46
+
47
+ function resolveDefaultServiceRange(
48
+ buildingId: BuildingNode['id'],
49
+ selectedLevelId: LevelNode['id'] | null,
50
+ ): {
51
+ defaultLevelId: LevelNode['id'] | null
52
+ fromLevelId: LevelNode['id'] | null
53
+ toLevelId: LevelNode['id'] | null
54
+ } {
55
+ const nodes = useScene.getState().nodes
56
+ const building = nodes[buildingId as AnyNodeId]
57
+ if (building?.type !== 'building') {
58
+ return { defaultLevelId: null, fromLevelId: null, toLevelId: null }
59
+ }
60
+
61
+ const levels = building.children
62
+ .map((childId) => nodes[childId as AnyNodeId])
63
+ .filter((node): node is LevelNode => node?.type === 'level')
64
+ .sort((left, right) => left.level - right.level)
65
+ const selectedLevelIndex = levels.findIndex((level) => level.id === selectedLevelId)
66
+ const fromIndex = selectedLevelIndex >= 0 ? selectedLevelIndex : 0
67
+ const fromLevel = levels[fromIndex]
68
+ const toLevel = levels[Math.min(fromIndex + 1, levels.length - 1)] ?? fromLevel
69
+
70
+ return {
71
+ defaultLevelId: fromLevel?.id ?? null,
72
+ fromLevelId: fromLevel?.id ?? null,
73
+ toLevelId: toLevel?.id ?? fromLevel?.id ?? null,
74
+ }
75
+ }
76
+
77
+ function createElevatorPreviewGeometry(): THREE.BufferGeometry {
78
+ return new THREE.BoxGeometry(
79
+ DEFAULT_ELEVATOR_WIDTH,
80
+ DEFAULT_ELEVATOR_CAB_HEIGHT,
81
+ DEFAULT_ELEVATOR_DEPTH,
82
+ )
83
+ }
84
+
85
+ function createElevatorPreviewEdgeGeometry(): THREE.BufferGeometry {
86
+ return new THREE.EdgesGeometry(createElevatorPreviewGeometry())
87
+ }
88
+
89
+ function commitElevatorPlacement(
90
+ buildingId: BuildingNode['id'],
91
+ selectedLevelId: LevelNode['id'] | null,
92
+ x: number,
93
+ z: number,
94
+ rotation: number,
95
+ onPlaced: ElevatorToolProps['onPlaced'],
96
+ ): void {
97
+ const { createNode, nodes } = useScene.getState()
98
+ const elevatorCount = Object.values(nodes).filter((node) => node.type === 'elevator').length
99
+ const serviceRange = resolveDefaultServiceRange(buildingId, selectedLevelId)
100
+ const supportY = resolveElevatorSupportY({
101
+ buildingId,
102
+ preferredLevelId: serviceRange.fromLevelId ?? serviceRange.defaultLevelId,
103
+ x,
104
+ z,
105
+ })
106
+ const elevator = ElevatorNode.parse({
107
+ name: `Elevator ${elevatorCount + 1}`,
108
+ parentId: buildingId,
109
+ position: [x, supportY, z],
110
+ rotation,
111
+ width: DEFAULT_ELEVATOR_WIDTH,
112
+ depth: DEFAULT_ELEVATOR_DEPTH,
113
+ cabHeight: DEFAULT_ELEVATOR_CAB_HEIGHT,
114
+ doorWidth: DEFAULT_ELEVATOR_DOOR_WIDTH,
115
+ doorHeight: DEFAULT_ELEVATOR_DOOR_HEIGHT,
116
+ ...serviceRange,
117
+ speed: DEFAULT_ELEVATOR_SPEED,
118
+ doorDurationMs: DEFAULT_ELEVATOR_DOOR_DURATION_MS,
119
+ dwellMs: DEFAULT_ELEVATOR_DWELL_MS,
120
+ })
121
+
122
+ createNode(elevator, buildingId)
123
+ onPlaced?.(elevator.id as AnyNodeId, buildingId)
124
+ sfxEmitter.emit('sfx:structure-build')
125
+ }
126
+
127
+ export const ElevatorTool: React.FC<ElevatorToolProps> = ({ buildingId, levelId, onPlaced }) => {
128
+ const cursorRef = useRef<THREE.Group>(null)
129
+ const previewRef = useRef<THREE.Group>(null)
130
+ const rotationRef = useRef(0)
131
+ const previousGridPosRef = useRef<[number, number] | null>(null)
132
+ const previewGeometry = useMemo(() => createElevatorPreviewGeometry(), [])
133
+ const previewEdgeGeometry = useMemo(() => createElevatorPreviewEdgeGeometry(), [])
134
+
135
+ // Default-shaped elevator for the 2D floor-plan placement ghost. The 3D
136
+ // preview meshes below are hidden in 2D (canvas `display:none`), so this
137
+ // feeds `usePlacementPreview` → `FloorplanPlacementPreviewLayer`, which
138
+ // renders the elevator's footprint following the cursor.
139
+ const floorplanPreviewNode = useMemo(
140
+ () =>
141
+ ElevatorNode.parse({
142
+ name: 'Elevator',
143
+ position: [0, 0, 0],
144
+ rotation: 0,
145
+ width: DEFAULT_ELEVATOR_WIDTH,
146
+ depth: DEFAULT_ELEVATOR_DEPTH,
147
+ cabHeight: DEFAULT_ELEVATOR_CAB_HEIGHT,
148
+ doorWidth: DEFAULT_ELEVATOR_DOOR_WIDTH,
149
+ doorHeight: DEFAULT_ELEVATOR_DOOR_HEIGHT,
150
+ }),
151
+ [],
152
+ )
153
+
154
+ useEffect(() => {
155
+ const currentBuildingId = resolveCurrentBuildingId({
156
+ buildingId,
157
+ levelId,
158
+ nodes: useScene.getState().nodes,
159
+ })
160
+ if (!currentBuildingId) return
161
+
162
+ rotationRef.current = 0
163
+ if (previewRef.current) previewRef.current.rotation.y = 0
164
+
165
+ // Alignment candidates — anchors of every alignable object; refreshed
166
+ // after each placement. The elevator aligns by its ORIGIN point.
167
+ let alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', levelId)
168
+ // Snap the elevator origin onto another object's nearest real anchor and
169
+ // publish the guide. The probe is the RAW cursor, NOT the 0.5m-grid-snapped
170
+ // point: resolving against the grid point would only ever catch anchors
171
+ // that happen to sit on a grid line, so off-grid items (furniture, angled
172
+ // walls) would never surface a guide. The matched axis locks exactly to the
173
+ // candidate's coordinate; the other axis keeps its grid snap. Guides are
174
+ // published in every snapping mode (including Off); the magnetic pull toward
175
+ // them (applySnap) applies only in 'lines' mode.
176
+ const alignPoint = (
177
+ gridX: number,
178
+ gridZ: number,
179
+ rawX: number,
180
+ rawZ: number,
181
+ bypass: boolean,
182
+ applySnap: boolean,
183
+ ): [number, number] => {
184
+ if (bypass || alignmentCandidates.length === 0) {
185
+ useAlignmentGuides.getState().clear()
186
+ return [gridX, gridZ]
187
+ }
188
+ const ar = resolveAlignment({
189
+ moving: [{ nodeId: '__elevator-draft__', kind: 'corner', x: rawX, z: rawZ }],
190
+ candidates: alignmentCandidates,
191
+ threshold: ALIGNMENT_THRESHOLD_M,
192
+ })
193
+ if (ar.guides.length === 0) {
194
+ useAlignmentGuides.getState().clear()
195
+ return [gridX, gridZ]
196
+ }
197
+ useAlignmentGuides.getState().set(ar.guides)
198
+ if (!applySnap) return [gridX, gridZ]
199
+ let x = gridX
200
+ let z = gridZ
201
+ for (const guide of ar.guides) {
202
+ if (guide.axis === 'x') x = guide.coord
203
+ else z = guide.coord
204
+ }
205
+ return [x, z]
206
+ }
207
+
208
+ const onGridMove = (event: GridEvent) => {
209
+ // Grid snap follows the global mode (live step so the HUD chip is
210
+ // honest); Off keeps the raw cursor. Shift cycles the mode centrally.
211
+ const step = useEditor.getState().gridSnapStep
212
+ const [gridX, gridZ] = alignPoint(
213
+ isGridSnapActive()
214
+ ? Math.round(event.localPosition[0] / step) * step
215
+ : event.localPosition[0],
216
+ isGridSnapActive()
217
+ ? Math.round(event.localPosition[2] / step) * step
218
+ : event.localPosition[2],
219
+ event.localPosition[0],
220
+ event.localPosition[2],
221
+ !isAlignmentGuideActive(),
222
+ isMagneticSnapActive(),
223
+ )
224
+ const supportY = resolveElevatorSupportY({
225
+ buildingId: currentBuildingId,
226
+ preferredLevelId: levelId as LevelNode['id'] | null,
227
+ x: gridX,
228
+ z: gridZ,
229
+ })
230
+
231
+ cursorRef.current?.position.set(gridX, supportY + GRID_OFFSET, gridZ)
232
+ previewRef.current?.position.set(gridX, supportY + DEFAULT_ELEVATOR_CAB_HEIGHT / 2, gridZ)
233
+
234
+ // Publish the 2D floor-plan ghost at the snapped/aligned cursor.
235
+ usePlacementPreview.getState().set({
236
+ ...floorplanPreviewNode,
237
+ position: [gridX, supportY, gridZ],
238
+ rotation: rotationRef.current,
239
+ })
240
+
241
+ if (
242
+ (isGridSnapActive() || isMagneticSnapActive()) &&
243
+ previousGridPosRef.current &&
244
+ (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
245
+ ) {
246
+ sfxEmitter.emit('sfx:grid-snap')
247
+ }
248
+
249
+ previousGridPosRef.current = [gridX, gridZ]
250
+ }
251
+
252
+ const onGridClick = (event: GridEvent) => {
253
+ const latestBuildingId = resolveCurrentBuildingId({
254
+ buildingId,
255
+ levelId,
256
+ nodes: useScene.getState().nodes,
257
+ })
258
+ if (!latestBuildingId) return
259
+
260
+ const step = useEditor.getState().gridSnapStep
261
+ const [gridX, gridZ] = alignPoint(
262
+ isGridSnapActive()
263
+ ? Math.round(event.localPosition[0] / step) * step
264
+ : event.localPosition[0],
265
+ isGridSnapActive()
266
+ ? Math.round(event.localPosition[2] / step) * step
267
+ : event.localPosition[2],
268
+ event.localPosition[0],
269
+ event.localPosition[2],
270
+ !isAlignmentGuideActive(),
271
+ isMagneticSnapActive(),
272
+ )
273
+ commitElevatorPlacement(
274
+ latestBuildingId,
275
+ levelId,
276
+ gridX,
277
+ gridZ,
278
+ rotationRef.current,
279
+ onPlaced,
280
+ )
281
+ alignmentCandidates = collectAlignmentAnchors(useScene.getState().nodes, '', levelId)
282
+ useAlignmentGuides.getState().clear()
283
+ // The placed elevator's footprint now renders for real; drop the ghost
284
+ // (the next grid:move re-publishes it for the following placement).
285
+ usePlacementPreview.getState().clear()
286
+ }
287
+
288
+ const onKeyDown = (event: KeyboardEvent) => {
289
+ if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
290
+ return
291
+ }
292
+
293
+ const ROTATION_STEP = Math.PI / 4
294
+ let rotationDelta = 0
295
+ if (event.key === 'r' || event.key === 'R') rotationDelta = ROTATION_STEP
296
+ else if (event.key === 't' || event.key === 'T') rotationDelta = -ROTATION_STEP
297
+
298
+ if (rotationDelta !== 0) {
299
+ event.preventDefault()
300
+ sfxEmitter.emit('sfx:item-rotate')
301
+ rotationRef.current += rotationDelta
302
+ if (previewRef.current) previewRef.current.rotation.y = rotationRef.current
303
+ // Reflect the rotation in the 2D ghost immediately (no pointer move
304
+ // needed) by republishing at the last snapped cursor position.
305
+ const last = previousGridPosRef.current
306
+ if (last) {
307
+ usePlacementPreview.getState().set({
308
+ ...floorplanPreviewNode,
309
+ position: [last[0], 0, last[1]],
310
+ rotation: rotationRef.current,
311
+ })
312
+ }
313
+ }
314
+ }
315
+
316
+ emitter.on('grid:move', onGridMove)
317
+ emitter.on('grid:click', onGridClick)
318
+ window.addEventListener('keydown', onKeyDown)
319
+
320
+ return () => {
321
+ emitter.off('grid:move', onGridMove)
322
+ emitter.off('grid:click', onGridClick)
323
+ window.removeEventListener('keydown', onKeyDown)
324
+ useAlignmentGuides.getState().clear()
325
+ usePlacementPreview.getState().clear()
326
+ }
327
+ }, [buildingId, levelId, onPlaced, floorplanPreviewNode])
328
+
329
+ return (
330
+ <group>
331
+ <CursorSphere ref={cursorRef} />
332
+ <group ref={previewRef}>
333
+ <mesh geometry={previewGeometry}>
334
+ <meshStandardMaterial color="#38bdf8" depthWrite={false} opacity={0.12} transparent />
335
+ </mesh>
336
+ <lineSegments geometry={previewEdgeGeometry}>
337
+ <lineBasicMaterial color="#38bdf8" opacity={0.9} transparent />
338
+ </lineSegments>
339
+ <mesh
340
+ position={[0, -DEFAULT_ELEVATOR_CAB_HEIGHT / 2 + 0.015, 0]}
341
+ rotation={[-Math.PI / 2, 0, 0]}
342
+ >
343
+ <planeGeometry args={[DEFAULT_ELEVATOR_WIDTH, DEFAULT_ELEVATOR_DEPTH]} />
344
+ <meshBasicMaterial color="#38bdf8" opacity={0.16} transparent />
345
+ </mesh>
346
+ <mesh position={[0, 0, -DEFAULT_ELEVATOR_DEPTH / 2 - 0.01]}>
347
+ <planeGeometry args={[DEFAULT_ELEVATOR_WIDTH, DEFAULT_ELEVATOR_CAB_HEIGHT]} />
348
+ <meshBasicMaterial color="#e0f2fe" opacity={0.18} transparent />
349
+ </mesh>
350
+ <mesh
351
+ position={[
352
+ 0,
353
+ -DEFAULT_ELEVATOR_CAB_HEIGHT / 2 + DEFAULT_ELEVATOR_DOOR_HEIGHT / 2,
354
+ -DEFAULT_ELEVATOR_DEPTH / 2 - 0.03,
355
+ ]}
356
+ >
357
+ <boxGeometry args={[DEFAULT_ELEVATOR_DOOR_WIDTH, DEFAULT_ELEVATOR_DOOR_HEIGHT, 0.035]} />
358
+ <meshStandardMaterial color="#e5e7eb" depthWrite={false} opacity={0.45} transparent />
359
+ </mesh>
360
+ <mesh
361
+ position={[
362
+ 0,
363
+ -DEFAULT_ELEVATOR_CAB_HEIGHT / 2 + DEFAULT_ELEVATOR_DOOR_HEIGHT / 2,
364
+ -DEFAULT_ELEVATOR_DEPTH / 2 - 0.049,
365
+ ]}
366
+ >
367
+ <boxGeometry
368
+ args={[DEFAULT_ELEVATOR_DOOR_WIDTH + 0.14, DEFAULT_ELEVATOR_DOOR_HEIGHT + 0.14, 0.01]}
369
+ />
370
+ <meshBasicMaterial color="#38bdf8" opacity={0.5} transparent />
371
+ </mesh>
372
+ </group>
373
+ </group>
374
+ )
375
+ }