@pascal-app/editor 0.9.1 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -1,10 +1,24 @@
1
1
  'use client'
2
2
 
3
- import { sceneRegistry } from '@pascal-app/core'
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ getWallCurveFrameAt,
7
+ getWallCurveLength,
8
+ getWallPlaneTop,
9
+ getWallThickness,
10
+ isCurvedWall,
11
+ resolveLevelId,
12
+ resolveWallTop,
13
+ sceneRegistry,
14
+ spatialGridManager,
15
+ useScene,
16
+ type WallNode,
17
+ } from '@pascal-app/core'
4
18
  import { useWallSnapIndicator, type WallSnapKind } from '@pascal-app/editor'
5
19
  import { useViewer } from '@pascal-app/viewer'
6
20
  import { useFrame } from '@react-three/fiber'
7
- import { memo, useRef } from 'react'
21
+ import { memo, useMemo, useRef } from 'react'
8
22
  import { BoxGeometry, CircleGeometry, CylinderGeometry, type Group } from 'three'
9
23
  import { MeshBasicNodeMaterial } from 'three/webgpu'
10
24
  import { EDITOR_LAYER } from '../../lib/constants'
@@ -20,8 +34,8 @@ import { EDITOR_LAYER } from '../../lib/constants'
20
34
  * intersection → ✕ cross wall body (edge) → circle
21
35
  *
22
36
  * Subscribes to the shared `useWallSnapIndicator` store (published by the wall
23
- * draft + endpoint-move tools). Shares the indigo accent of
24
- * `Alignment3DGuideLayer` for visual consistency.
37
+ * draft + endpoint-move tools). The vertical mouse pillar and the corner
38
+ * (endpoint) square are green; the other floor glyphs are indigo.
25
39
  *
26
40
  * The point carries only XZ (building-local plan coords); like the alignment
27
41
  * guides it's lifted to the active level's building-local Y each frame so it
@@ -30,10 +44,19 @@ import { EDITOR_LAYER } from '../../lib/constants'
30
44
  */
31
45
 
32
46
  const BEACON_COLOR = 0x81_8c_f8 // indigo-400 — matches the alignment guide accent
47
+ const MARKER_GREEN = 0x22_c5_5e // green-500 — vertical mouse marker + corner (endpoint) glyph
33
48
  const BEACON_HEIGHT = 2.5 // world-meter height of the pillar
34
49
  const BEACON_RADIUS = 0.018 // world-meter radius of the pillar
35
50
  const MARKER = 0.13 // world-meter base size of the floor glyph
36
51
  const FLOOR_LIFT = 0.012 // tiny lift so the marker reads above the floor grid
52
+ const WALL_TOP_HIGHLIGHT_LIFT = 0.035
53
+ const WALL_TOP_HIGHLIGHT_HEIGHT = 0.018
54
+ const WALL_TOP_HIGHLIGHT_OVERHANG = 0.14
55
+ const WALL_TOP_GLOW_HEIGHT = 0.026
56
+ const WALL_TOP_GLOW_OVERHANG = 0.36
57
+ const WALL_TOP_END_OVERHANG = 0.08
58
+ const CURVED_WALL_HIGHLIGHT_SEGMENT_LENGTH = 0.45
59
+ const NO_RAYCAST = () => null
37
60
 
38
61
  // Shared resources — one material + unit geometries, so snap churn during a
39
62
  // drag doesn't rebuild GPU buffers (mirrors the alignment guide layer).
@@ -45,17 +68,49 @@ const beaconMaterial = new MeshBasicNodeMaterial({
45
68
  transparent: true,
46
69
  opacity: 0.9,
47
70
  })
71
+ const greenMarkerMaterial = new MeshBasicNodeMaterial({
72
+ color: MARKER_GREEN,
73
+ depthTest: false,
74
+ depthWrite: false,
75
+ toneMapped: false,
76
+ transparent: true,
77
+ opacity: 0.9,
78
+ })
79
+ const wallTopHighlightMaterial = new MeshBasicNodeMaterial({
80
+ color: BEACON_COLOR,
81
+ depthTest: false,
82
+ depthWrite: false,
83
+ toneMapped: false,
84
+ transparent: true,
85
+ opacity: 0.88,
86
+ })
87
+ const wallTopHighlightGlowMaterial = new MeshBasicNodeMaterial({
88
+ color: BEACON_COLOR,
89
+ depthTest: false,
90
+ depthWrite: false,
91
+ toneMapped: false,
92
+ transparent: true,
93
+ opacity: 0.26,
94
+ })
48
95
  const PILLAR_GEOMETRY = new CylinderGeometry(BEACON_RADIUS, BEACON_RADIUS, BEACON_HEIGHT, 8)
49
96
  // Flat unit geometries scaled per marker. Boxes are 0.002 tall so they read as
50
97
  // a flat plate; circles/triangles lie flat via an X rotation at the mesh.
51
98
  const FLAT_BOX_GEOMETRY = new BoxGeometry(1, 0.002, 1)
99
+ const WALL_TOP_HIGHLIGHT_GEOMETRY = new BoxGeometry(1, 1, 1)
52
100
  const TRIANGLE_GEOMETRY = new CircleGeometry(1, 3)
53
101
  const CIRCLE_GEOMETRY = new CircleGeometry(1, 28)
54
102
 
55
103
  export const WallSnapBeaconLayer = memo(function WallSnapBeaconLayer() {
56
104
  const point = useWallSnapIndicator((s) => s.point)
57
105
  const levelId = useViewer((s) => s.selection.levelId)
106
+ const nodes = useScene((s) => s.nodes)
58
107
  const groupRef = useRef<Group>(null)
108
+ const highlightedWalls = useMemo(() => {
109
+ if (!point?.wallIds?.length) return []
110
+ return point.wallIds
111
+ .map((wallId) => nodes[wallId as AnyNodeId])
112
+ .filter((node): node is WallNode => node?.type === 'wall' && node.visible !== false)
113
+ }, [nodes, point?.wallIds])
59
114
 
60
115
  // Track the active level's building-local Y each frame so the beacon stands
61
116
  // on the floor being edited, not the building base — same source the
@@ -70,10 +125,13 @@ export const WallSnapBeaconLayer = memo(function WallSnapBeaconLayer() {
70
125
  if (!point) return null
71
126
  return (
72
127
  <group ref={groupRef}>
128
+ {highlightedWalls.map((wall) => (
129
+ <WallTopHighlight key={wall.id} nodes={nodes} wall={wall} />
130
+ ))}
73
131
  <mesh
74
132
  geometry={PILLAR_GEOMETRY}
75
133
  layers={EDITOR_LAYER}
76
- material={beaconMaterial}
134
+ material={greenMarkerMaterial}
77
135
  position={[point.x, BEACON_HEIGHT / 2, point.z]}
78
136
  renderOrder={1002}
79
137
  />
@@ -82,6 +140,104 @@ export const WallSnapBeaconLayer = memo(function WallSnapBeaconLayer() {
82
140
  )
83
141
  })
84
142
 
143
+ type WallTopHighlightSegment = {
144
+ angle: number
145
+ center: [number, number]
146
+ length: number
147
+ }
148
+
149
+ function getWallTopY(wall: WallNode, nodes: Readonly<Record<string, AnyNode>>) {
150
+ const levelId = resolveLevelId(wall, nodes as Record<string, AnyNode>)
151
+ const support = spatialGridManager.getSlabSupportForWall(
152
+ levelId,
153
+ wall.start,
154
+ wall.end,
155
+ wall.curveOffset ?? 0,
156
+ wall.thickness,
157
+ wall.supportSlabId,
158
+ )
159
+ const planeTop = getWallPlaneTop(wall, levelId, nodes as Record<string, AnyNode>)
160
+ return resolveWallTop(wall, planeTop, support.elevation) + WALL_TOP_HIGHLIGHT_LIFT
161
+ }
162
+
163
+ function buildHighlightSegment(start: [number, number], end: [number, number]) {
164
+ const dx = end[0] - start[0]
165
+ const dz = end[1] - start[1]
166
+ const length = Math.hypot(dx, dz)
167
+ if (length < 1e-6) return null
168
+
169
+ return {
170
+ angle: -Math.atan2(dz, dx),
171
+ center: [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2] as [number, number],
172
+ length,
173
+ }
174
+ }
175
+
176
+ function buildWallTopHighlightSegments(wall: WallNode): WallTopHighlightSegment[] {
177
+ if (!isCurvedWall(wall)) {
178
+ const segment = buildHighlightSegment(wall.start, wall.end)
179
+ return segment ? [segment] : []
180
+ }
181
+
182
+ const sampleCount = Math.max(
183
+ 8,
184
+ Math.ceil(getWallCurveLength(wall) / CURVED_WALL_HIGHLIGHT_SEGMENT_LENGTH),
185
+ )
186
+ const segments: WallTopHighlightSegment[] = []
187
+ let previous = getWallCurveFrameAt(wall, 0).point
188
+ for (let index = 1; index <= sampleCount; index += 1) {
189
+ const current = getWallCurveFrameAt(wall, index / sampleCount).point
190
+ const segment = buildHighlightSegment([previous.x, previous.y], [current.x, current.y])
191
+ if (segment) segments.push(segment)
192
+ previous = current
193
+ }
194
+ return segments
195
+ }
196
+
197
+ function WallTopHighlight({
198
+ nodes,
199
+ wall,
200
+ }: {
201
+ nodes: Readonly<Record<string, AnyNode>>
202
+ wall: WallNode
203
+ }) {
204
+ const segments = useMemo(() => buildWallTopHighlightSegments(wall), [wall])
205
+ const y = getWallTopY(wall, nodes)
206
+ const width = Math.max(getWallThickness(wall) + WALL_TOP_HIGHLIGHT_OVERHANG, 0.24)
207
+ const glowWidth = Math.max(getWallThickness(wall) + WALL_TOP_GLOW_OVERHANG, 0.42)
208
+
209
+ return (
210
+ <>
211
+ {segments.map((segment, index) => (
212
+ <group key={`${wall.id}:${index}`}>
213
+ <mesh
214
+ frustumCulled={false}
215
+ geometry={WALL_TOP_HIGHLIGHT_GEOMETRY}
216
+ layers={EDITOR_LAYER}
217
+ material={wallTopHighlightGlowMaterial}
218
+ position={[segment.center[0], y - 0.003, segment.center[1]]}
219
+ raycast={NO_RAYCAST}
220
+ renderOrder={1003}
221
+ rotation={[0, segment.angle, 0]}
222
+ scale={[segment.length + WALL_TOP_END_OVERHANG, WALL_TOP_GLOW_HEIGHT, glowWidth]}
223
+ />
224
+ <mesh
225
+ frustumCulled={false}
226
+ geometry={WALL_TOP_HIGHLIGHT_GEOMETRY}
227
+ layers={EDITOR_LAYER}
228
+ material={wallTopHighlightMaterial}
229
+ position={[segment.center[0], y + 0.002, segment.center[1]]}
230
+ raycast={NO_RAYCAST}
231
+ renderOrder={1004}
232
+ rotation={[0, segment.angle, 0]}
233
+ scale={[segment.length + WALL_TOP_END_OVERHANG, WALL_TOP_HIGHLIGHT_HEIGHT, width]}
234
+ />
235
+ </group>
236
+ ))}
237
+ </>
238
+ )
239
+ }
240
+
85
241
  /** Floor glyph whose shape encodes which kind of geometry the point snapped to. */
86
242
  function SnapMarker({ kind, x, z }: { kind: WallSnapKind; x: number; z: number }) {
87
243
  const y = FLOOR_LIFT
@@ -90,7 +246,7 @@ function SnapMarker({ kind, x, z }: { kind: WallSnapKind; x: number; z: number }
90
246
  <mesh
91
247
  geometry={FLAT_BOX_GEOMETRY}
92
248
  layers={EDITOR_LAYER}
93
- material={beaconMaterial}
249
+ material={greenMarkerMaterial}
94
250
  position={[x, y, z]}
95
251
  renderOrder={1001}
96
252
  scale={[MARKER * 2, 1, MARKER * 2]}
@@ -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 = {
@@ -48,12 +53,11 @@ export const FloorplanActionMenuLayer = memo(function FloorplanActionMenuLayer({
48
53
  offsetY = 10,
49
54
  }: FloorplanActionMenuLayerProps) {
50
55
  const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
51
- const movingNode = useEditor((state) => state.movingNode)
52
- const movingFenceEndpoint = useEditor((state) => state.movingFenceEndpoint)
53
- const curvingWall = useEditor((state) => state.curvingWall)
54
- const curvingFence = useEditor((state) => state.curvingFence)
56
+ const movingNode = useMovingNode()
57
+ const endpointReshape = useEndpointReshape()
58
+ const isCurveReshape = useIsCurveReshape()
55
59
 
56
- if (!isFloorplanHovered || movingNode || movingFenceEndpoint || curvingWall || curvingFence) {
60
+ if (!isFloorplanHovered || movingNode || endpointReshape || isCurveReshape) {
57
61
  return null
58
62
  }
59
63
 
@@ -28,6 +28,7 @@ import { useFloorplanRender } from './floorplan-render-context'
28
28
  export const FloorplanAlignmentGuideLayer = memo(function FloorplanAlignmentGuideLayer() {
29
29
  const guides = useAlignmentGuides((s) => s.guides)
30
30
  const unit = useViewer((s) => s.unit)
31
+ const metricNotation = useViewer((s) => s.metricNotation)
31
32
  const ctx = useFloorplanRender()
32
33
 
33
34
  if (guides.length === 0) return null
@@ -61,7 +62,7 @@ export const FloorplanAlignmentGuideLayer = memo(function FloorplanAlignmentGuid
61
62
  // offset along X.
62
63
  const pillX = axis === 'x' ? midX + pillOffset : midX
63
64
  const pillZ = axis === 'z' ? midZ + pillOffset : midZ
64
- const distLabel = formatMeasurement(distMeters, unit)
65
+ const distLabel = formatMeasurement(distMeters, unit, metricNotation)
65
66
  const charWidth = pillFontSize * 0.55
66
67
  const pillWidth = distLabel.length * charWidth + pillPadX * 2
67
68
  const pillHeight = pillFontSize + pillPadY * 2
@@ -1,15 +1,16 @@
1
1
  'use client'
2
2
 
3
3
  import { Icon } from '@iconify/react'
4
- import { memo, useMemo } from 'react'
4
+ import { memo, useLayoutEffect, useMemo, useRef, useState } from 'react'
5
5
  import useEditor, { type FloorplanSelectionTool } from '../../store/use-editor'
6
+ import { useFloorplanDraftPreview } from '../../store/use-floorplan-draft-preview'
6
7
  import { furnishTools } from '../ui/action-menu/furnish-tools'
7
8
  import { tools as structureTools } from '../ui/action-menu/structure-tools'
8
-
9
- type SvgPoint = {
10
- x: number
11
- y: number
12
- }
9
+ import {
10
+ type FloorplanCursorPoint,
11
+ projectFloorplanCursorPoint,
12
+ resolveFloorplanCursorIndicatorPosition,
13
+ } from './floorplan-cursor-indicator-position'
13
14
 
14
15
  type FloorplanCursorIndicator =
15
16
  | {
@@ -22,7 +23,7 @@ type FloorplanCursorIndicator =
22
23
  }
23
24
 
24
25
  type FloorplanCursorIndicatorOverlayProps = {
25
- cursorPosition: SvgPoint | null
26
+ cursorPosition: FloorplanCursorPoint | null
26
27
  floorplanSelectionTool: FloorplanSelectionTool
27
28
  movingOpeningType: 'door' | 'window' | null
28
29
  isPanning: boolean
@@ -46,6 +47,10 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
46
47
  const tool = useEditor((state) => state.tool)
47
48
  const structureLayer = useEditor((state) => state.structureLayer)
48
49
  const catalogCategory = useEditor((state) => state.catalogCategory)
50
+ const cursorPoint = useFloorplanDraftPreview((state) => state.cursorPoint)
51
+ const anchorRef = useRef<HTMLDivElement>(null)
52
+ const [projectedCursorPosition, setProjectedCursorPosition] =
53
+ useState<FloorplanCursorPoint | null>(null)
49
54
 
50
55
  const activeFloorplanToolConfig = useMemo(() => {
51
56
  if (movingOpeningType) {
@@ -77,13 +82,38 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
77
82
  }
78
83
 
79
84
  if (mode === 'material-paint') {
80
- return { kind: 'asset', iconSrc: '/icons/paint.png' }
85
+ return { kind: 'asset', iconSrc: '/icons/paint.webp' }
81
86
  }
82
87
 
83
88
  return null
84
89
  }, [activeFloorplanToolConfig, floorplanSelectionTool, mode, structureLayer])
85
90
 
86
- const position = cursorPosition
91
+ useLayoutEffect(() => {
92
+ const anchor = anchorRef.current
93
+ const overlayHost = anchor?.parentElement
94
+ const scene = overlayHost?.querySelector<SVGGElement>('[data-floorplan-scene]')
95
+ const sceneToViewport = scene?.getScreenCTM()
96
+
97
+ if (!(cursorPoint && cursorPosition && overlayHost && sceneToViewport)) {
98
+ setProjectedCursorPosition(null)
99
+ return
100
+ }
101
+
102
+ const overlayRect = overlayHost.getBoundingClientRect()
103
+ const nextPosition = projectFloorplanCursorPoint(cursorPoint, sceneToViewport, {
104
+ x: overlayRect.left,
105
+ y: overlayRect.top,
106
+ })
107
+ setProjectedCursorPosition((currentPosition) =>
108
+ currentPosition &&
109
+ currentPosition.x === nextPosition.x &&
110
+ currentPosition.y === nextPosition.y
111
+ ? currentPosition
112
+ : nextPosition,
113
+ )
114
+ }, [cursorPoint, cursorPosition])
115
+
116
+ const position = resolveFloorplanCursorIndicatorPosition(cursorPosition, projectedCursorPosition)
87
117
 
88
118
  if (!(indicator && position) || isPanning) {
89
119
  return null
@@ -93,6 +123,7 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
93
123
  <div
94
124
  aria-hidden="true"
95
125
  className="pointer-events-none absolute z-20"
126
+ ref={anchorRef}
96
127
  style={{ left: position.x, top: position.y }}
97
128
  >
98
129
  {mode === 'delete' ? (
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ projectFloorplanCursorPoint,
4
+ resolveFloorplanCursorIndicatorPosition,
5
+ } from './floorplan-cursor-indicator-position'
6
+
7
+ describe('projectFloorplanCursorPoint', () => {
8
+ test('projects a snapped plan point into overlay-local screen coordinates', () => {
9
+ expect(
10
+ projectFloorplanCursorPoint(
11
+ [3, 4],
12
+ {
13
+ a: 0,
14
+ b: 10,
15
+ c: -10,
16
+ d: 0,
17
+ e: 128,
18
+ f: 40,
19
+ },
20
+ { x: 30, y: 20 },
21
+ ),
22
+ ).toEqual({ x: 58, y: 50 })
23
+ })
24
+
25
+ test('anchors the placement pin to the projected snap point instead of the raw pointer', () => {
26
+ expect(resolveFloorplanCursorIndicatorPosition({ x: 88, y: 97 }, { x: 58, y: 70 })).toEqual({
27
+ x: 58,
28
+ y: 70,
29
+ })
30
+ })
31
+ })
@@ -0,0 +1,39 @@
1
+ export type FloorplanCursorPoint = {
2
+ x: number
3
+ y: number
4
+ }
5
+
6
+ type Matrix2D = {
7
+ a: number
8
+ b: number
9
+ c: number
10
+ d: number
11
+ e: number
12
+ f: number
13
+ }
14
+
15
+ export function projectFloorplanCursorPoint(
16
+ point: readonly [number, number],
17
+ sceneToViewport: Matrix2D,
18
+ overlayOrigin: FloorplanCursorPoint,
19
+ ): FloorplanCursorPoint {
20
+ return {
21
+ x:
22
+ sceneToViewport.a * point[0] +
23
+ sceneToViewport.c * point[1] +
24
+ sceneToViewport.e -
25
+ overlayOrigin.x,
26
+ y:
27
+ sceneToViewport.b * point[0] +
28
+ sceneToViewport.d * point[1] +
29
+ sceneToViewport.f -
30
+ overlayOrigin.y,
31
+ }
32
+ }
33
+
34
+ export function resolveFloorplanCursorIndicatorPosition(
35
+ cursorPosition: FloorplanCursorPoint | null,
36
+ projectedCursorPosition: FloorplanCursorPoint | null,
37
+ ): FloorplanCursorPoint | null {
38
+ return projectedCursorPosition ?? cursorPosition
39
+ }
@@ -0,0 +1,87 @@
1
+ 'use client'
2
+
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import { useEffect, useState } from 'react'
5
+ import { createPortal } from 'react-dom'
6
+ import { isActive } from '../../lib/interaction/scope'
7
+ import useEditor from '../../store/use-editor'
8
+ import useInteractionScope, { useMovingNode } from '../../store/use-interaction-scope'
9
+ import {
10
+ deleteSelection,
11
+ duplicateSelectionAndPickUp,
12
+ startGroupPickUp,
13
+ } from '../editor/group-actions'
14
+ import { NodeActionMenu } from '../editor/node-action-menu'
15
+
16
+ /**
17
+ * Floating Move / Duplicate / Delete pill for a MULTI-selection in the 2D
18
+ * floor plan — the group sibling of `FloorplanRegistryActionMenu` (which is
19
+ * sole-selection only). Anchored above the dashed group selection box; every
20
+ * action targets the whole selection: Move picks the group up (it rides the
21
+ * cursor until a click places it), Duplicate clones the selection and picks
22
+ * the clones up, Delete removes everything selected.
23
+ *
24
+ * Gated on floorplan hover so it never coexists with the 3D group menu in
25
+ * split view (that one hides while the floor plan is hovered), and hidden
26
+ * during any active interaction so it never competes with a live drag.
27
+ */
28
+ export function FloorplanGroupActionMenu() {
29
+ const isMultiSelect = useViewer((s) => s.selection.selectedIds.length > 1)
30
+ const movingNode = useMovingNode()
31
+ const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
32
+ const scopeActive = useInteractionScope((s) => isActive(s.scope))
33
+
34
+ const [position, setPosition] = useState<{ left: number; top: number } | null>(null)
35
+
36
+ const isVisible = isMultiSelect && !movingNode && isFloorplanHovered && !scopeActive
37
+
38
+ useEffect(() => {
39
+ if (!isVisible) {
40
+ setPosition(null)
41
+ return
42
+ }
43
+ let raf = 0
44
+ const tick = () => {
45
+ raf = requestAnimationFrame(tick)
46
+ // The dashed group box exists exactly while the multi-selection has
47
+ // transformable participants — anchor to its top edge. Only publish
48
+ // actual changes so the idle poll doesn't re-render every frame.
49
+ const box = document.querySelector('[data-group-selection-box]') as SVGGElement | null
50
+ if (!box) {
51
+ setPosition((prev) => (prev === null ? prev : null))
52
+ return
53
+ }
54
+ const rect = box.getBoundingClientRect()
55
+ const next = { left: rect.left + rect.width / 2, top: rect.top }
56
+ setPosition((prev) =>
57
+ prev && Math.abs(prev.left - next.left) < 0.5 && Math.abs(prev.top - next.top) < 0.5
58
+ ? prev
59
+ : next,
60
+ )
61
+ }
62
+ raf = requestAnimationFrame(tick)
63
+ return () => cancelAnimationFrame(raf)
64
+ }, [isVisible])
65
+
66
+ if (!(isVisible && position)) return null
67
+
68
+ return createPortal(
69
+ <div
70
+ className="pointer-events-none fixed z-30"
71
+ style={{
72
+ left: position.left,
73
+ top: position.top,
74
+ transform: 'translate(-50%, calc(-100% - 12px))',
75
+ }}
76
+ >
77
+ <NodeActionMenu
78
+ onDelete={() => deleteSelection()}
79
+ onDuplicate={() => duplicateSelectionAndPickUp()}
80
+ onMove={() => startGroupPickUp()}
81
+ onPointerDown={(event) => event.stopPropagation()}
82
+ onPointerUp={(event) => event.stopPropagation()}
83
+ />
84
+ </div>,
85
+ document.body,
86
+ )
87
+ }