@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,181 @@
1
+ 'use client'
2
+
3
+ import { type AnyNodeId, sceneRegistry, useLiveNodeOverrides, useScene } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
5
+ import { createPortal, useFrame, useThree } from '@react-three/fiber'
6
+ import { useEffect, useMemo, useRef } from 'react'
7
+ import { BoxGeometry, type BufferGeometry, EdgesGeometry, type Group, Vector3 } from 'three'
8
+ import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
9
+ import { EDITOR_LAYER } from '../../lib/constants'
10
+
11
+ // How far the outline sits outside the opening's own extents, so it reads as
12
+ // a frame *around* the opening rather than coinciding with its edges.
13
+ const PAD = 0.05
14
+
15
+ const ACCENT = 0x83_81_ed
16
+
17
+ const NO_RAYCAST = () => null
18
+ const scratchScale = new Vector3()
19
+
20
+ // Indigo accent matching the resize-arrow handles — deliberately distinct
21
+ // from the white selection outline so the highlight reads as "editable child
22
+ // here", not "this is selected". `depthTest: false` keeps both layers drawn
23
+ // on top of the wall so frameless openings (which have no visible geometry)
24
+ // are still located. Shared across every box; never disposed.
25
+ const outlineMaterial = new LineBasicNodeMaterial({
26
+ color: ACCENT,
27
+ depthTest: false,
28
+ depthWrite: false,
29
+ })
30
+
31
+ // Translucent block that fills the opening volume so the cutout reads as an
32
+ // occupied slot from every angle — the front face shows it head-on, the top
33
+ // face shows it from a top-down floorplan view (a single vertical pane would
34
+ // be edge-on, hence invisible, when looking straight down). Front-side culling
35
+ // keeps the translucency even instead of doubling up on overlapping back faces.
36
+ const fillMaterial = new MeshBasicNodeMaterial({
37
+ color: ACCENT,
38
+ transparent: true,
39
+ opacity: 0.5,
40
+ depthTest: false,
41
+ depthWrite: false,
42
+ })
43
+
44
+ function makeOutlineGeometry(width: number, height: number, depth: number): BufferGeometry {
45
+ const box = new BoxGeometry(width + PAD, height + PAD, depth + PAD)
46
+ const edges = new EdgesGeometry(box)
47
+ box.dispose()
48
+ return edges
49
+ }
50
+
51
+ /**
52
+ * When a wall is selected, draws a translucent indigo highlight (filled block
53
+ * + outline) over each door / window it hosts. Openings whose `openingKind`
54
+ * is `'opening'` have no visible geometry, so without this affordance the
55
+ * user can't tell an editable cutout lives there — the fill marks it (and
56
+ * stays out of the way of clicking the opening itself, which selects it).
57
+ *
58
+ * Highlights are portalled to the scene root so they sit outside the wall's
59
+ * selection-outline subtree and keep their own accent colour.
60
+ */
61
+ export function WallOpeningHighlights() {
62
+ const selectedIds = useViewer((state) => state.selection.selectedIds)
63
+ const { scene } = useThree()
64
+
65
+ if (selectedIds.length === 0) return null
66
+
67
+ return createPortal(
68
+ <>
69
+ {selectedIds.map((id) => (
70
+ <SelectionOpeningHighlights key={id} selectedId={id} />
71
+ ))}
72
+ </>,
73
+ scene,
74
+ )
75
+ }
76
+
77
+ // Resolves a selected node into the opening highlight(s) to draw:
78
+ // - a selected wall → a hint over each door / window it hosts ("editable
79
+ // child here").
80
+ // - a directly-selected frameless opening (a `door` whose `openingKind` is
81
+ // `'opening'`) → a fill over its own cutout, so the selection reads as
82
+ // occupied even though the opening renders no geometry of its own.
83
+ function SelectionOpeningHighlights({ selectedId }: { selectedId: string }) {
84
+ const node = useScene((state) => state.nodes[selectedId as AnyNodeId])
85
+
86
+ if (node?.type === 'wall') {
87
+ const depth = node.thickness ?? 0.1
88
+ return (
89
+ <>
90
+ {(node.children ?? []).map((childId) => (
91
+ <OpeningHighlight depth={depth} key={childId} openingId={childId} />
92
+ ))}
93
+ </>
94
+ )
95
+ }
96
+
97
+ if (node?.type === 'door' && node.openingKind === 'opening') {
98
+ return <SelectedOpeningHighlight openingId={selectedId} parentId={node.parentId} />
99
+ }
100
+
101
+ return null
102
+ }
103
+
104
+ // A frameless opening selected on its own. Pulls the cutout depth from its
105
+ // host wall's thickness so the fill block matches the wall it sits in.
106
+ function SelectedOpeningHighlight({
107
+ openingId,
108
+ parentId,
109
+ }: {
110
+ openingId: string
111
+ parentId: string | null
112
+ }) {
113
+ const parent = useScene((state) => (parentId ? state.nodes[parentId as AnyNodeId] : undefined))
114
+ const depth = parent?.type === 'wall' ? (parent.thickness ?? 0.1) : 0.1
115
+ return <OpeningHighlight depth={depth} openingId={openingId} />
116
+ }
117
+
118
+ function OpeningHighlight({ openingId, depth }: { openingId: string; depth: number }) {
119
+ const node = useScene((state) => state.nodes[openingId as AnyNodeId])
120
+ // Resize arrows publish width/height to the live-override store during the
121
+ // drag and only commit to the scene node on pointer-up, so read the
122
+ // override-merged dimensions to keep the highlight box tracking the resize.
123
+ const override = useLiveNodeOverrides((s) => s.overrides.get(openingId))
124
+ const groupRef = useRef<Group>(null)
125
+
126
+ const isOpening = node?.type === 'door' || node?.type === 'window'
127
+ const width = isOpening ? ((override?.width as number | undefined) ?? node.width) : 0
128
+ const height = isOpening ? ((override?.height as number | undefined) ?? node.height) : 0
129
+
130
+ const outlineGeometry = useMemo(
131
+ () => (isOpening ? makeOutlineGeometry(width, height, depth) : null),
132
+ [isOpening, width, height, depth],
133
+ )
134
+ const fillGeometry = useMemo(
135
+ () => (isOpening ? new BoxGeometry(width, height, depth) : null),
136
+ [isOpening, width, height, depth],
137
+ )
138
+ useEffect(() => () => outlineGeometry?.dispose(), [outlineGeometry])
139
+ useEffect(() => () => fillGeometry?.dispose(), [fillGeometry])
140
+
141
+ // The opening's mesh (registered by its renderer) already carries the wall
142
+ // transform + its own local pose baked into `matrixWorld`. Copy that
143
+ // straight onto the highlight each frame so it tracks moves, resizes, and
144
+ // wall rotation without any wall-local maths here.
145
+ useFrame(() => {
146
+ const group = groupRef.current
147
+ if (!group) return
148
+ const obj = sceneRegistry.nodes.get(openingId as AnyNodeId)
149
+ if (!obj) {
150
+ group.visible = false
151
+ return
152
+ }
153
+ group.visible = true
154
+ obj.matrixWorld.decompose(group.position, group.quaternion, scratchScale)
155
+ })
156
+
157
+ if (!isOpening || !outlineGeometry || !fillGeometry) return null
158
+
159
+ return (
160
+ <group ref={groupRef}>
161
+ <mesh
162
+ frustumCulled={false}
163
+ geometry={fillGeometry}
164
+ layers={EDITOR_LAYER}
165
+ material={fillMaterial}
166
+ raycast={NO_RAYCAST}
167
+ renderOrder={1004}
168
+ />
169
+ <lineSegments
170
+ frustumCulled={false}
171
+ geometry={outlineGeometry}
172
+ layers={EDITOR_LAYER}
173
+ material={outlineMaterial}
174
+ raycast={NO_RAYCAST}
175
+ renderOrder={1005}
176
+ />
177
+ </group>
178
+ )
179
+ }
180
+
181
+ export default WallOpeningHighlights
@@ -0,0 +1,304 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ DEFAULT_WALL_HEIGHT,
7
+ getWallCurveFrameAt,
8
+ getWallCurveLength,
9
+ getWallThickness,
10
+ isCurvedWall,
11
+ resolveLevelId,
12
+ sceneRegistry,
13
+ spatialGridManager,
14
+ useScene,
15
+ type WallNode,
16
+ } from '@pascal-app/core'
17
+ import { useWallSnapIndicator, type WallSnapKind } from '@pascal-app/editor'
18
+ import { useViewer } from '@pascal-app/viewer'
19
+ import { useFrame } from '@react-three/fiber'
20
+ import { memo, useMemo, useRef } from 'react'
21
+ import { BoxGeometry, CircleGeometry, CylinderGeometry, type Group } from 'three'
22
+ import { MeshBasicNodeMaterial } from 'three/webgpu'
23
+ import { EDITOR_LAYER } from '../../lib/constants'
24
+
25
+ /**
26
+ * "Magnetic" wall-snap beacon for the 3D editor — a vertical marker that
27
+ * stands at the draft / move endpoint while it's locked onto existing wall
28
+ * geometry. It's the spatial cue from the design reference: a standing pillar
29
+ * so you can see *where* the snap caught even at an angle, plus a floor marker
30
+ * whose shape tells you *what* it caught (CAD-style osnap glyphs):
31
+ *
32
+ * endpoint (corner) → square midpoint → triangle
33
+ * intersection → ✕ cross wall body (edge) → circle
34
+ *
35
+ * Subscribes to the shared `useWallSnapIndicator` store (published by the wall
36
+ * draft + endpoint-move tools). The vertical mouse pillar and the corner
37
+ * (endpoint) square are green; the other floor glyphs are indigo.
38
+ *
39
+ * The point carries only XZ (building-local plan coords); like the alignment
40
+ * guides it's lifted to the active level's building-local Y each frame so it
41
+ * stands on the floor being edited when floors are stacked. Mounted inside
42
+ * ToolManager's building-local group.
43
+ */
44
+
45
+ const BEACON_COLOR = 0x81_8c_f8 // indigo-400 — matches the alignment guide accent
46
+ const MARKER_GREEN = 0x22_c5_5e // green-500 — vertical mouse marker + corner (endpoint) glyph
47
+ const BEACON_HEIGHT = 2.5 // world-meter height of the pillar
48
+ const BEACON_RADIUS = 0.018 // world-meter radius of the pillar
49
+ const MARKER = 0.13 // world-meter base size of the floor glyph
50
+ const FLOOR_LIFT = 0.012 // tiny lift so the marker reads above the floor grid
51
+ const WALL_TOP_HIGHLIGHT_LIFT = 0.035
52
+ const WALL_TOP_HIGHLIGHT_HEIGHT = 0.018
53
+ const WALL_TOP_HIGHLIGHT_OVERHANG = 0.14
54
+ const WALL_TOP_GLOW_HEIGHT = 0.026
55
+ const WALL_TOP_GLOW_OVERHANG = 0.36
56
+ const WALL_TOP_END_OVERHANG = 0.08
57
+ const CURVED_WALL_HIGHLIGHT_SEGMENT_LENGTH = 0.45
58
+ const NO_RAYCAST = () => null
59
+
60
+ // Shared resources — one material + unit geometries, so snap churn during a
61
+ // drag doesn't rebuild GPU buffers (mirrors the alignment guide layer).
62
+ const beaconMaterial = new MeshBasicNodeMaterial({
63
+ color: BEACON_COLOR,
64
+ depthTest: false,
65
+ depthWrite: false,
66
+ toneMapped: false,
67
+ transparent: true,
68
+ opacity: 0.9,
69
+ })
70
+ const greenMarkerMaterial = new MeshBasicNodeMaterial({
71
+ color: MARKER_GREEN,
72
+ depthTest: false,
73
+ depthWrite: false,
74
+ toneMapped: false,
75
+ transparent: true,
76
+ opacity: 0.9,
77
+ })
78
+ const wallTopHighlightMaterial = new MeshBasicNodeMaterial({
79
+ color: BEACON_COLOR,
80
+ depthTest: false,
81
+ depthWrite: false,
82
+ toneMapped: false,
83
+ transparent: true,
84
+ opacity: 0.88,
85
+ })
86
+ const wallTopHighlightGlowMaterial = new MeshBasicNodeMaterial({
87
+ color: BEACON_COLOR,
88
+ depthTest: false,
89
+ depthWrite: false,
90
+ toneMapped: false,
91
+ transparent: true,
92
+ opacity: 0.26,
93
+ })
94
+ const PILLAR_GEOMETRY = new CylinderGeometry(BEACON_RADIUS, BEACON_RADIUS, BEACON_HEIGHT, 8)
95
+ // Flat unit geometries scaled per marker. Boxes are 0.002 tall so they read as
96
+ // a flat plate; circles/triangles lie flat via an X rotation at the mesh.
97
+ const FLAT_BOX_GEOMETRY = new BoxGeometry(1, 0.002, 1)
98
+ const WALL_TOP_HIGHLIGHT_GEOMETRY = new BoxGeometry(1, 1, 1)
99
+ const TRIANGLE_GEOMETRY = new CircleGeometry(1, 3)
100
+ const CIRCLE_GEOMETRY = new CircleGeometry(1, 28)
101
+
102
+ export const WallSnapBeaconLayer = memo(function WallSnapBeaconLayer() {
103
+ const point = useWallSnapIndicator((s) => s.point)
104
+ const levelId = useViewer((s) => s.selection.levelId)
105
+ const nodes = useScene((s) => s.nodes)
106
+ const groupRef = useRef<Group>(null)
107
+ const highlightedWalls = useMemo(() => {
108
+ if (!point?.wallIds?.length) return []
109
+ return point.wallIds
110
+ .map((wallId) => nodes[wallId as AnyNodeId])
111
+ .filter((node): node is WallNode => node?.type === 'wall' && node.visible !== false)
112
+ }, [nodes, point?.wallIds])
113
+
114
+ // Track the active level's building-local Y each frame so the beacon stands
115
+ // on the floor being edited, not the building base — same source the
116
+ // alignment guide layer and `grid.tsx` read.
117
+ useFrame(() => {
118
+ const group = groupRef.current
119
+ if (!group) return
120
+ const levelMesh = levelId ? sceneRegistry.nodes.get(levelId) : null
121
+ group.position.y = levelMesh ? levelMesh.position.y : 0
122
+ })
123
+
124
+ if (!point) return null
125
+ return (
126
+ <group ref={groupRef}>
127
+ {highlightedWalls.map((wall) => (
128
+ <WallTopHighlight key={wall.id} nodes={nodes} wall={wall} />
129
+ ))}
130
+ <mesh
131
+ geometry={PILLAR_GEOMETRY}
132
+ layers={EDITOR_LAYER}
133
+ material={greenMarkerMaterial}
134
+ position={[point.x, BEACON_HEIGHT / 2, point.z]}
135
+ renderOrder={1002}
136
+ />
137
+ <SnapMarker kind={point.kind} x={point.x} z={point.z} />
138
+ </group>
139
+ )
140
+ })
141
+
142
+ type WallTopHighlightSegment = {
143
+ angle: number
144
+ center: [number, number]
145
+ length: number
146
+ }
147
+
148
+ function getWallTopY(wall: WallNode, nodes: Readonly<Record<string, AnyNode>>) {
149
+ const levelId = resolveLevelId(wall, nodes as Record<string, AnyNode>)
150
+ const slabElevation = spatialGridManager.getSlabElevationForWall(
151
+ levelId,
152
+ wall.start,
153
+ wall.end,
154
+ wall.curveOffset ?? 0,
155
+ wall.thickness,
156
+ )
157
+ const wallHeight = wall.height ?? DEFAULT_WALL_HEIGHT
158
+ return (slabElevation > 0 ? slabElevation + wallHeight : wallHeight) + WALL_TOP_HIGHLIGHT_LIFT
159
+ }
160
+
161
+ function buildHighlightSegment(start: [number, number], end: [number, number]) {
162
+ const dx = end[0] - start[0]
163
+ const dz = end[1] - start[1]
164
+ const length = Math.hypot(dx, dz)
165
+ if (length < 1e-6) return null
166
+
167
+ return {
168
+ angle: -Math.atan2(dz, dx),
169
+ center: [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2] as [number, number],
170
+ length,
171
+ }
172
+ }
173
+
174
+ function buildWallTopHighlightSegments(wall: WallNode): WallTopHighlightSegment[] {
175
+ if (!isCurvedWall(wall)) {
176
+ const segment = buildHighlightSegment(wall.start, wall.end)
177
+ return segment ? [segment] : []
178
+ }
179
+
180
+ const sampleCount = Math.max(
181
+ 8,
182
+ Math.ceil(getWallCurveLength(wall) / CURVED_WALL_HIGHLIGHT_SEGMENT_LENGTH),
183
+ )
184
+ const segments: WallTopHighlightSegment[] = []
185
+ let previous = getWallCurveFrameAt(wall, 0).point
186
+ for (let index = 1; index <= sampleCount; index += 1) {
187
+ const current = getWallCurveFrameAt(wall, index / sampleCount).point
188
+ const segment = buildHighlightSegment([previous.x, previous.y], [current.x, current.y])
189
+ if (segment) segments.push(segment)
190
+ previous = current
191
+ }
192
+ return segments
193
+ }
194
+
195
+ function WallTopHighlight({
196
+ nodes,
197
+ wall,
198
+ }: {
199
+ nodes: Readonly<Record<string, AnyNode>>
200
+ wall: WallNode
201
+ }) {
202
+ const segments = useMemo(() => buildWallTopHighlightSegments(wall), [wall])
203
+ const y = getWallTopY(wall, nodes)
204
+ const width = Math.max(getWallThickness(wall) + WALL_TOP_HIGHLIGHT_OVERHANG, 0.24)
205
+ const glowWidth = Math.max(getWallThickness(wall) + WALL_TOP_GLOW_OVERHANG, 0.42)
206
+
207
+ return (
208
+ <>
209
+ {segments.map((segment, index) => (
210
+ <group key={`${wall.id}:${index}`}>
211
+ <mesh
212
+ frustumCulled={false}
213
+ geometry={WALL_TOP_HIGHLIGHT_GEOMETRY}
214
+ layers={EDITOR_LAYER}
215
+ material={wallTopHighlightGlowMaterial}
216
+ position={[segment.center[0], y - 0.003, segment.center[1]]}
217
+ raycast={NO_RAYCAST}
218
+ renderOrder={1003}
219
+ rotation={[0, segment.angle, 0]}
220
+ scale={[segment.length + WALL_TOP_END_OVERHANG, WALL_TOP_GLOW_HEIGHT, glowWidth]}
221
+ />
222
+ <mesh
223
+ frustumCulled={false}
224
+ geometry={WALL_TOP_HIGHLIGHT_GEOMETRY}
225
+ layers={EDITOR_LAYER}
226
+ material={wallTopHighlightMaterial}
227
+ position={[segment.center[0], y + 0.002, segment.center[1]]}
228
+ raycast={NO_RAYCAST}
229
+ renderOrder={1004}
230
+ rotation={[0, segment.angle, 0]}
231
+ scale={[segment.length + WALL_TOP_END_OVERHANG, WALL_TOP_HIGHLIGHT_HEIGHT, width]}
232
+ />
233
+ </group>
234
+ ))}
235
+ </>
236
+ )
237
+ }
238
+
239
+ /** Floor glyph whose shape encodes which kind of geometry the point snapped to. */
240
+ function SnapMarker({ kind, x, z }: { kind: WallSnapKind; x: number; z: number }) {
241
+ const y = FLOOR_LIFT
242
+ if (kind === 'endpoint') {
243
+ return (
244
+ <mesh
245
+ geometry={FLAT_BOX_GEOMETRY}
246
+ layers={EDITOR_LAYER}
247
+ material={greenMarkerMaterial}
248
+ position={[x, y, z]}
249
+ renderOrder={1001}
250
+ scale={[MARKER * 2, 1, MARKER * 2]}
251
+ />
252
+ )
253
+ }
254
+ if (kind === 'midpoint') {
255
+ return (
256
+ <mesh
257
+ geometry={TRIANGLE_GEOMETRY}
258
+ layers={EDITOR_LAYER}
259
+ material={beaconMaterial}
260
+ position={[x, y, z]}
261
+ renderOrder={1001}
262
+ rotation={[-Math.PI / 2, 0, 0]}
263
+ scale={[MARKER * 1.4, MARKER * 1.4, MARKER * 1.4]}
264
+ />
265
+ )
266
+ }
267
+ if (kind === 'intersection') {
268
+ // Two crossed bars → an ✕, the universal "crossing" glyph.
269
+ return (
270
+ <>
271
+ <mesh
272
+ geometry={FLAT_BOX_GEOMETRY}
273
+ layers={EDITOR_LAYER}
274
+ material={beaconMaterial}
275
+ position={[x, y, z]}
276
+ renderOrder={1001}
277
+ rotation={[0, Math.PI / 4, 0]}
278
+ scale={[MARKER * 2.6, 1, MARKER * 0.5]}
279
+ />
280
+ <mesh
281
+ geometry={FLAT_BOX_GEOMETRY}
282
+ layers={EDITOR_LAYER}
283
+ material={beaconMaterial}
284
+ position={[x, y, z]}
285
+ renderOrder={1001}
286
+ rotation={[0, -Math.PI / 4, 0]}
287
+ scale={[MARKER * 2.6, 1, MARKER * 0.5]}
288
+ />
289
+ </>
290
+ )
291
+ }
292
+ // 'wall' (edge / along-wall) → circle
293
+ return (
294
+ <mesh
295
+ geometry={CIRCLE_GEOMETRY}
296
+ layers={EDITOR_LAYER}
297
+ material={beaconMaterial}
298
+ position={[x, y, z]}
299
+ renderOrder={1001}
300
+ rotation={[-Math.PI / 2, 0, 0]}
301
+ scale={[MARKER, MARKER, MARKER]}
302
+ />
303
+ )
304
+ }
@@ -2,6 +2,11 @@
2
2
 
3
3
  import { memo, type MouseEvent as ReactMouseEvent } from 'react'
4
4
  import useEditor from '../../store/use-editor'
5
+ import {
6
+ useEndpointReshape,
7
+ useIsCurveReshape,
8
+ useMovingNode,
9
+ } from '../../store/use-interaction-scope'
5
10
  import { NodeActionMenu } from '../editor/node-action-menu'
6
11
 
7
12
  type SvgPoint = {
@@ -14,12 +19,14 @@ export type FloorplanActionMenuHandler = (event: ReactMouseEvent<HTMLButtonEleme
14
19
  export type FloorplanActionMenuEntry = {
15
20
  position: SvgPoint | null
16
21
  onDelete: FloorplanActionMenuHandler
17
- onMove: FloorplanActionMenuHandler
22
+ onMove?: FloorplanActionMenuHandler
18
23
  onAddHole?: FloorplanActionMenuHandler
24
+ onCurve?: FloorplanActionMenuHandler
19
25
  onDuplicate?: FloorplanActionMenuHandler
20
26
  }
21
27
 
22
28
  type FloorplanActionMenuLayerProps = {
29
+ elevator: FloorplanActionMenuEntry
23
30
  item: FloorplanActionMenuEntry
24
31
  wall: FloorplanActionMenuEntry
25
32
  fence: FloorplanActionMenuEntry
@@ -33,6 +40,7 @@ type FloorplanActionMenuLayerProps = {
33
40
  }
34
41
 
35
42
  export const FloorplanActionMenuLayer = memo(function FloorplanActionMenuLayer({
43
+ elevator,
36
44
  item,
37
45
  wall,
38
46
  fence,
@@ -45,16 +53,16 @@ export const FloorplanActionMenuLayer = memo(function FloorplanActionMenuLayer({
45
53
  offsetY = 10,
46
54
  }: FloorplanActionMenuLayerProps) {
47
55
  const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
48
- const movingNode = useEditor((state) => state.movingNode)
49
- const movingFenceEndpoint = useEditor((state) => state.movingFenceEndpoint)
50
- const curvingWall = useEditor((state) => state.curvingWall)
51
- const curvingFence = useEditor((state) => state.curvingFence)
56
+ const movingNode = useMovingNode()
57
+ const endpointReshape = useEndpointReshape()
58
+ const isCurveReshape = useIsCurveReshape()
52
59
 
53
- if (!isFloorplanHovered || movingNode || movingFenceEndpoint || curvingWall || curvingFence) {
60
+ if (!isFloorplanHovered || movingNode || endpointReshape || isCurveReshape) {
54
61
  return null
55
62
  }
56
63
 
57
64
  const entries: FloorplanActionMenuEntry[] = [
65
+ elevator,
58
66
  item,
59
67
  wall,
60
68
  fence,
@@ -81,6 +89,7 @@ export const FloorplanActionMenuLayer = memo(function FloorplanActionMenuLayer({
81
89
  >
82
90
  <NodeActionMenu
83
91
  onAddHole={entry.onAddHole}
92
+ onCurve={entry.onCurve}
84
93
  onDelete={entry.onDelete}
85
94
  onDuplicate={entry.onDuplicate}
86
95
  onMove={entry.onMove}
@@ -0,0 +1,142 @@
1
+ 'use client'
2
+
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import { memo } from 'react'
5
+ import useAlignmentGuides from '../../store/use-alignment-guides'
6
+ import { formatMeasurement } from '../editor/measurement-pill'
7
+ import { useFloorplanRender } from './floorplan-render-context'
8
+
9
+ /**
10
+ * Figma-style alignment guides for the 2D floor plan.
11
+ *
12
+ * Subscribes to the editor-local `useAlignmentGuides` store (separate
13
+ * from the core store the 3D layer reads). Guides come in
14
+ * building-local meters, so the layer is mounted INSIDE the rotated
15
+ * `<g data-floorplan-scene>` — the SVG transform that takes the rest
16
+ * of the floor-plan geometry from local → screen carries the guide
17
+ * lines too. Pill labels are counter-rotated by `sceneRotationDeg`
18
+ * (from `FloorplanRenderProvider`) so they stay upright even when the
19
+ * scene `<g>` is rotated by building rotation.
20
+ *
21
+ * Each guide renders as a red line between the moving and matched
22
+ * candidate anchors with small `×` end-caps. A distance pill is drawn
23
+ * at the midpoint when the perpendicular gap is non-zero.
24
+ *
25
+ * Stroke widths and handle radii are scaled by `unitsPerPixel` so they
26
+ * stay a constant size on screen no matter the zoom.
27
+ */
28
+ export const FloorplanAlignmentGuideLayer = memo(function FloorplanAlignmentGuideLayer() {
29
+ const guides = useAlignmentGuides((s) => s.guides)
30
+ const unit = useViewer((s) => s.unit)
31
+ const ctx = useFloorplanRender()
32
+
33
+ if (guides.length === 0) return null
34
+
35
+ const upp = ctx?.unitsPerPixel ?? 0.01
36
+ const sceneRot = ctx?.sceneRotationDeg ?? 0
37
+
38
+ // Pixel-budgeted sizes converted to world meters so visuals stay
39
+ // constant across zoom. Numbers picked to mirror Figma's snap chrome.
40
+ const stroke = 1 * upp
41
+ const xCapSize = 4 * upp
42
+ const pillFontSize = 11 * upp
43
+ const pillPadX = 5 * upp
44
+ const pillPadY = 3 * upp
45
+ const pillRadius = 3 * upp
46
+ const pillOffset = 8 * upp
47
+
48
+ const color = '#ef4444' // tailwind red-500 — matches Figma's snap red
49
+
50
+ return (
51
+ <g pointerEvents="none">
52
+ {guides.map((guide, i) => {
53
+ const { from, to, axis } = guide
54
+ const midX = (from.x + to.x) / 2
55
+ const midZ = (from.z + to.z) / 2
56
+ const distMeters = guide.distance
57
+
58
+ // Pill placed offset perpendicular to the guide's axis so it
59
+ // doesn't sit on top of the line itself. For an X-axis guide
60
+ // (vertical line) we offset along Z; for a Z-axis guide we
61
+ // offset along X.
62
+ const pillX = axis === 'x' ? midX + pillOffset : midX
63
+ const pillZ = axis === 'z' ? midZ + pillOffset : midZ
64
+ const distLabel = formatMeasurement(distMeters, unit)
65
+ const charWidth = pillFontSize * 0.55
66
+ const pillWidth = distLabel.length * charWidth + pillPadX * 2
67
+ const pillHeight = pillFontSize + pillPadY * 2
68
+
69
+ return (
70
+ <g key={i}>
71
+ <line stroke={color} strokeWidth={stroke} x1={from.x} x2={to.x} y1={from.z} y2={to.z} />
72
+ <XCap color={color} size={xCapSize} stroke={stroke} x={from.x} y={from.z} />
73
+ <XCap color={color} size={xCapSize} stroke={stroke} x={to.x} y={to.z} />
74
+ {distMeters > 1e-4 && (
75
+ // Counter-rotate the pill so it stays upright when the
76
+ // scene `<g>` is rotated by building rotation. SVG's
77
+ // `transform` runs in the local coord system, so the
78
+ // rotation pivots around the pill's center.
79
+ <g transform={`rotate(${-sceneRot} ${pillX} ${pillZ})`}>
80
+ <rect
81
+ fill={color}
82
+ height={pillHeight}
83
+ rx={pillRadius}
84
+ ry={pillRadius}
85
+ width={pillWidth}
86
+ x={pillX - pillWidth / 2}
87
+ y={pillZ - pillHeight / 2}
88
+ />
89
+ <text
90
+ fill="#ffffff"
91
+ fontFamily="-apple-system, system-ui, sans-serif"
92
+ fontSize={pillFontSize}
93
+ fontWeight={500}
94
+ textAnchor="middle"
95
+ x={pillX}
96
+ y={pillZ + pillFontSize * 0.35}
97
+ >
98
+ {distLabel}
99
+ </text>
100
+ </g>
101
+ )}
102
+ </g>
103
+ )
104
+ })}
105
+ </g>
106
+ )
107
+ })
108
+
109
+ function XCap({
110
+ color,
111
+ size,
112
+ stroke,
113
+ x,
114
+ y,
115
+ }: {
116
+ color: string
117
+ size: number
118
+ stroke: number
119
+ x: number
120
+ y: number
121
+ }) {
122
+ return (
123
+ <g>
124
+ <line
125
+ stroke={color}
126
+ strokeWidth={stroke}
127
+ x1={x - size}
128
+ x2={x + size}
129
+ y1={y - size}
130
+ y2={y + size}
131
+ />
132
+ <line
133
+ stroke={color}
134
+ strokeWidth={stroke}
135
+ x1={x - size}
136
+ x2={x + size}
137
+ y1={y + size}
138
+ y2={y - size}
139
+ />
140
+ </g>
141
+ )
142
+ }