@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
@@ -0,0 +1,237 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ normalizeCameraPose,
4
+ planCameraPoseApplication,
5
+ publishInitialCameraPose,
6
+ releaseCameraPoseEventSuppression,
7
+ stepCameraPoseInterpolation,
8
+ withCameraPoseDistance,
9
+ } from './camera-pose'
10
+
11
+ describe('camera pose', () => {
12
+ test('normalizes a finite pose without retaining input tuples', () => {
13
+ const position: [number, number, number] = [1, 2, 3]
14
+ const target: [number, number, number] = [4, 5, 6]
15
+ const pose = normalizeCameraPose({
16
+ position,
17
+ target,
18
+ projection: 'perspective',
19
+ fov: 50,
20
+ viewWidth: 12,
21
+ })
22
+
23
+ expect(pose).toEqual({
24
+ fov: 50,
25
+ position,
26
+ projection: 'perspective',
27
+ target,
28
+ viewWidth: 12,
29
+ })
30
+ expect(pose?.position).not.toBe(position)
31
+ expect(pose?.target).not.toBe(target)
32
+ })
33
+
34
+ test('rejects non-finite, malformed, and unknown camera poses', () => {
35
+ expect(normalizeCameraPose(null)).toBeNull()
36
+ expect(
37
+ normalizeCameraPose({
38
+ position: [1, 2],
39
+ target: [4, 5, 6],
40
+ projection: 'perspective',
41
+ }),
42
+ ).toBeNull()
43
+ expect(
44
+ normalizeCameraPose({
45
+ position: [1, Number.NaN, 3],
46
+ target: [4, 5, 6],
47
+ projection: 'perspective',
48
+ }),
49
+ ).toBeNull()
50
+ expect(
51
+ normalizeCameraPose({
52
+ position: [1, 2, 3],
53
+ target: [4, 5, Number.POSITIVE_INFINITY],
54
+ projection: 'perspective',
55
+ }),
56
+ ).toBeNull()
57
+ expect(
58
+ normalizeCameraPose({
59
+ position: [1, 2, 3],
60
+ target: [4, 5, 6],
61
+ projection: 'panoramic',
62
+ }),
63
+ ).toBeNull()
64
+ expect(
65
+ normalizeCameraPose({
66
+ position: [1, 2, 3],
67
+ target: [4, 5, 6],
68
+ projection: 'perspective',
69
+ fov: Number.NaN,
70
+ }),
71
+ ).toBeNull()
72
+ expect(
73
+ normalizeCameraPose({
74
+ position: [1, 2, 3],
75
+ target: [4, 5, 6],
76
+ projection: 'orthographic',
77
+ viewWidth: 0,
78
+ }),
79
+ ).toBeNull()
80
+ expect(
81
+ normalizeCameraPose({
82
+ position: [1, 2, 3],
83
+ target: [4, 5, 6],
84
+ projection: 'orthographic',
85
+ viewWidth: Number.POSITIVE_INFINITY,
86
+ }),
87
+ ).toBeNull()
88
+ })
89
+
90
+ test('plans only perspective fov application and clamps it to a safe range', () => {
91
+ expect(
92
+ planCameraPoseApplication({
93
+ position: [1, 2, 3],
94
+ target: [4, 5, 6],
95
+ projection: 'perspective',
96
+ fov: 0,
97
+ })?.perspectiveFov,
98
+ ).toBe(1)
99
+ expect(
100
+ planCameraPoseApplication({
101
+ position: [1, 2, 3],
102
+ target: [4, 5, 6],
103
+ projection: 'perspective',
104
+ fov: 200,
105
+ })?.perspectiveFov,
106
+ ).toBe(179)
107
+ expect(
108
+ planCameraPoseApplication({
109
+ position: [1, 2, 3],
110
+ target: [4, 5, 6],
111
+ projection: 'orthographic',
112
+ fov: 70,
113
+ })?.perspectiveFov,
114
+ ).toBeNull()
115
+ })
116
+
117
+ test('interpolates toward the latest pose and converges exactly', () => {
118
+ const destination = {
119
+ position: [12, 6, -4] as [number, number, number],
120
+ target: [2, 1, 3] as [number, number, number],
121
+ projection: 'perspective' as const,
122
+ }
123
+ const first = stepCameraPoseInterpolation([0, 0, 0], [0, 0, 0], destination, 0.016)
124
+ expect(first.settled).toBe(false)
125
+ expect(first.position[0]).toBeGreaterThan(0)
126
+ expect(first.position[0]).toBeLessThan(destination.position[0])
127
+
128
+ let position = first.position
129
+ let target = first.target
130
+ let settled = first.settled
131
+ for (let frame = 0; frame < 240 && !settled; frame += 1) {
132
+ const step = stepCameraPoseInterpolation(position, target, destination, 0.016)
133
+ position = step.position
134
+ target = step.target
135
+ settled = step.settled
136
+ }
137
+
138
+ expect(settled).toBe(true)
139
+ expect(position).toEqual(destination.position)
140
+ expect(target).toEqual(destination.target)
141
+ })
142
+
143
+ test('settles an already-current pose without advancing on an invalid delta', () => {
144
+ const destination = {
145
+ position: [1, 2, 3] as [number, number, number],
146
+ target: [4, 5, 6] as [number, number, number],
147
+ projection: 'orthographic' as const,
148
+ }
149
+ expect(
150
+ stepCameraPoseInterpolation(
151
+ destination.position,
152
+ destination.target,
153
+ destination,
154
+ Number.NaN,
155
+ ),
156
+ ).toEqual({
157
+ position: destination.position,
158
+ settled: true,
159
+ target: destination.target,
160
+ })
161
+ expect(stepCameraPoseInterpolation([0, 0, 0], [0, 0, 0], destination, Number.NaN)).toEqual({
162
+ position: [0, 0, 0],
163
+ settled: false,
164
+ target: [0, 0, 0],
165
+ })
166
+ })
167
+
168
+ test('publishes the initial pose before any camera control update event', () => {
169
+ let publishCount = 0
170
+
171
+ publishInitialCameraPose(() => {
172
+ publishCount += 1
173
+ })
174
+
175
+ expect(publishCount).toBe(1)
176
+ })
177
+
178
+ test('publishes the settled pose when programmatic interpolation releases suppression', () => {
179
+ const suppression = { current: true }
180
+ let publishCount = 0
181
+
182
+ releaseCameraPoseEventSuppression(suppression, () => {
183
+ publishCount += 1
184
+ })
185
+
186
+ expect(suppression.current).toBe(false)
187
+ expect(publishCount).toBe(1)
188
+ })
189
+
190
+ test('retargets the bounded interpolation owner to the latest pose', () => {
191
+ const first = stepCameraPoseInterpolation(
192
+ [0, 0, 0],
193
+ [0, 0, 0],
194
+ {
195
+ position: [10, 0, 0],
196
+ target: [5, 0, 0],
197
+ projection: 'perspective',
198
+ },
199
+ 0.016,
200
+ )
201
+ const retargeted = stepCameraPoseInterpolation(
202
+ first.position,
203
+ first.target,
204
+ {
205
+ position: [-10, 0, 0],
206
+ target: [-5, 0, 0],
207
+ projection: 'perspective',
208
+ },
209
+ 0.1,
210
+ )
211
+
212
+ expect(retargeted.position[0]).toBeLessThan(first.position[0])
213
+ expect(retargeted.target[0]).toBeLessThan(first.target[0])
214
+ })
215
+
216
+ test('preserves view direction while adapting distance to the local viewport', () => {
217
+ const pose = {
218
+ position: [4, 6, 12] as [number, number, number],
219
+ target: [1, 2, 3] as [number, number, number],
220
+ projection: 'perspective' as const,
221
+ viewWidth: 20,
222
+ }
223
+ const adapted = withCameraPoseDistance(pose, 5)
224
+ const sourceDirection = pose.position.map((value, index) => value - pose.target[index]!)
225
+ const adaptedDirection = adapted.position.map((value, index) => value - adapted.target[index]!)
226
+ const sourceLength = Math.hypot(...sourceDirection)
227
+
228
+ expect(Math.hypot(...adaptedDirection)).toBeCloseTo(5)
229
+ const expectedDirection = sourceDirection.map((component) => component * (5 / sourceLength))
230
+ expect(adaptedDirection[0]).toBeCloseTo(expectedDirection[0]!)
231
+ expect(adaptedDirection[1]).toBeCloseTo(expectedDirection[1]!)
232
+ expect(adaptedDirection[2]).toBeCloseTo(expectedDirection[2]!)
233
+ expect(adapted.target).toEqual(pose.target)
234
+ expect(adapted.target).not.toBe(pose.target)
235
+ expect(pose.position).toEqual([4, 6, 12])
236
+ })
237
+ })
@@ -0,0 +1,172 @@
1
+ import type { CameraPose } from '@pascal-app/core'
2
+
3
+ const MIN_PERSPECTIVE_FOV = 1
4
+ const MAX_PERSPECTIVE_FOV = 179
5
+ const INTERPOLATION_TIME_CONSTANT_SECONDS = 0.08
6
+ const MAX_INTERPOLATION_DELTA_SECONDS = 0.1
7
+ const INTERPOLATION_SETTLE_DISTANCE = 0.001
8
+
9
+ export type CameraPoseApplicationPlan = {
10
+ pose: CameraPose
11
+ perspectiveFov: number | null
12
+ }
13
+
14
+ export type CameraPoseInterpolationStep = {
15
+ position: [number, number, number]
16
+ settled: boolean
17
+ target: [number, number, number]
18
+ }
19
+
20
+ export function publishInitialCameraPose(publishCurrentPose: () => void): void {
21
+ publishCurrentPose()
22
+ }
23
+
24
+ export function releaseCameraPoseEventSuppression(
25
+ suppression: { current: boolean },
26
+ publishCurrentPose: () => void,
27
+ ): void {
28
+ suppression.current = false
29
+ publishCurrentPose()
30
+ }
31
+
32
+ function finiteVectorTuple(value: unknown): [number, number, number] | null {
33
+ if (
34
+ !(
35
+ Array.isArray(value) &&
36
+ value.length === 3 &&
37
+ value.every((component) => typeof component === 'number' && Number.isFinite(component))
38
+ )
39
+ ) {
40
+ return null
41
+ }
42
+
43
+ return [value[0], value[1], value[2]]
44
+ }
45
+
46
+ export function normalizeCameraPose(value: unknown): CameraPose | null {
47
+ if (!(typeof value === 'object' && value !== null)) {
48
+ return null
49
+ }
50
+
51
+ const candidate = value as Partial<CameraPose>
52
+ const position = finiteVectorTuple(candidate.position)
53
+ const target = finiteVectorTuple(candidate.target)
54
+ if (
55
+ !(
56
+ position &&
57
+ target &&
58
+ (candidate.projection === 'perspective' || candidate.projection === 'orthographic')
59
+ )
60
+ ) {
61
+ return null
62
+ }
63
+
64
+ if (
65
+ candidate.fov !== undefined &&
66
+ !(typeof candidate.fov === 'number' && Number.isFinite(candidate.fov))
67
+ ) {
68
+ return null
69
+ }
70
+
71
+ if (
72
+ candidate.viewWidth !== undefined &&
73
+ !(
74
+ typeof candidate.viewWidth === 'number' &&
75
+ Number.isFinite(candidate.viewWidth) &&
76
+ candidate.viewWidth > 0
77
+ )
78
+ ) {
79
+ return null
80
+ }
81
+
82
+ return {
83
+ ...(candidate.fov === undefined ? {} : { fov: candidate.fov }),
84
+ position,
85
+ projection: candidate.projection,
86
+ target,
87
+ ...(candidate.viewWidth === undefined ? {} : { viewWidth: candidate.viewWidth }),
88
+ }
89
+ }
90
+
91
+ export function planCameraPoseApplication(value: unknown): CameraPoseApplicationPlan | null {
92
+ const pose = normalizeCameraPose(value)
93
+ if (!pose) {
94
+ return null
95
+ }
96
+
97
+ return {
98
+ pose,
99
+ perspectiveFov:
100
+ pose.projection === 'perspective' && pose.fov !== undefined
101
+ ? Math.min(Math.max(pose.fov, MIN_PERSPECTIVE_FOV), MAX_PERSPECTIVE_FOV)
102
+ : null,
103
+ }
104
+ }
105
+
106
+ function interpolateVectorTuple(
107
+ current: [number, number, number],
108
+ destination: [number, number, number],
109
+ alpha: number,
110
+ ): { settled: boolean; value: [number, number, number] } {
111
+ const dx = destination[0] - current[0]
112
+ const dy = destination[1] - current[1]
113
+ const dz = destination[2] - current[2]
114
+ const settleDistanceSquared = INTERPOLATION_SETTLE_DISTANCE ** 2
115
+ if (dx * dx + dy * dy + dz * dz <= settleDistanceSquared) {
116
+ return { settled: true, value: [...destination] }
117
+ }
118
+
119
+ const value: [number, number, number] = [
120
+ current[0] + dx * alpha,
121
+ current[1] + dy * alpha,
122
+ current[2] + dz * alpha,
123
+ ]
124
+ const remainingX = destination[0] - value[0]
125
+ const remainingY = destination[1] - value[1]
126
+ const remainingZ = destination[2] - value[2]
127
+ const settled =
128
+ remainingX * remainingX + remainingY * remainingY + remainingZ * remainingZ <=
129
+ settleDistanceSquared
130
+
131
+ return { settled, value: settled ? [...destination] : value }
132
+ }
133
+
134
+ export function withCameraPoseDistance(pose: CameraPose, distance: number): CameraPose {
135
+ const dx = pose.position[0] - pose.target[0]
136
+ const dy = pose.position[1] - pose.target[1]
137
+ const dz = pose.position[2] - pose.target[2]
138
+ const currentDistance = Math.hypot(dx, dy, dz)
139
+ if (!(Number.isFinite(distance) && distance > 0 && currentDistance > 0)) {
140
+ return { ...pose, position: [...pose.position], target: [...pose.target] }
141
+ }
142
+
143
+ const scale = distance / currentDistance
144
+ return {
145
+ ...pose,
146
+ position: [
147
+ pose.target[0] + dx * scale,
148
+ pose.target[1] + dy * scale,
149
+ pose.target[2] + dz * scale,
150
+ ],
151
+ target: [...pose.target],
152
+ }
153
+ }
154
+
155
+ export function stepCameraPoseInterpolation(
156
+ currentPosition: [number, number, number],
157
+ currentTarget: [number, number, number],
158
+ destination: CameraPose,
159
+ deltaSeconds: number,
160
+ ): CameraPoseInterpolationStep {
161
+ const finiteDelta = Number.isFinite(deltaSeconds) ? deltaSeconds : 0
162
+ const elapsed = Math.min(Math.max(finiteDelta, 0), MAX_INTERPOLATION_DELTA_SECONDS)
163
+ const alpha = 1 - Math.exp(-elapsed / INTERPOLATION_TIME_CONSTANT_SECONDS)
164
+ const position = interpolateVectorTuple(currentPosition, destination.position, alpha)
165
+ const target = interpolateVectorTuple(currentTarget, destination.target, alpha)
166
+
167
+ return {
168
+ position: position.value,
169
+ settled: position.settled && target.settled,
170
+ target: target.value,
171
+ }
172
+ }
@@ -0,0 +1,24 @@
1
+ import {
2
+ clearSurfacePlanSnapFeedback,
3
+ resolveSurfacePlanPointSnap,
4
+ SURFACE_ALIGNMENT_THRESHOLD_M,
5
+ type SurfacePlanSnapInput,
6
+ type SurfacePlanSnapResult,
7
+ } from './surface-plan-snap'
8
+
9
+ const CEILING_SNAP_MOVING_ID = '__ceiling_snap__'
10
+
11
+ export const CEILING_ALIGNMENT_THRESHOLD_M = SURFACE_ALIGNMENT_THRESHOLD_M
12
+ export type CeilingPlanSnapInput = SurfacePlanSnapInput
13
+ export type CeilingPlanSnapResult = SurfacePlanSnapResult
14
+
15
+ export function clearCeilingSnapFeedback() {
16
+ clearSurfacePlanSnapFeedback()
17
+ }
18
+
19
+ export function resolveCeilingPlanPointSnap(input: CeilingPlanSnapInput): CeilingPlanSnapResult {
20
+ return resolveSurfacePlanPointSnap({
21
+ ...input,
22
+ movingId: input.movingId ?? CEILING_SNAP_MOVING_ID,
23
+ })
24
+ }
@@ -0,0 +1,112 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { resolveSelectModeHelpHints } from './contextual-help'
3
+
4
+ describe('resolveSelectModeHelpHints', () => {
5
+ test('stays hidden in idle select mode with no selection', () => {
6
+ expect(
7
+ resolveSelectModeHelpHints({
8
+ selectedCount: 0,
9
+ hasMovableSelection: false,
10
+ hasRotatableSelection: false,
11
+ commandPressed: false,
12
+ shiftPressed: false,
13
+ }),
14
+ ).toEqual([])
15
+ })
16
+
17
+ test('shows multi-select guidance when a modifier is held without selection', () => {
18
+ expect(
19
+ resolveSelectModeHelpHints({
20
+ selectedCount: 0,
21
+ hasMovableSelection: false,
22
+ hasRotatableSelection: false,
23
+ commandPressed: true,
24
+ shiftPressed: false,
25
+ }),
26
+ ).toEqual([
27
+ {
28
+ keys: [['Cmd/Ctrl', 'Shift'], 'Left click'],
29
+ label: 'Add or remove objects from the selection',
30
+ active: true,
31
+ },
32
+ ])
33
+ })
34
+
35
+ test('shows direct manipulation tips for selected movable and rotatable nodes', () => {
36
+ const hints = resolveSelectModeHelpHints({
37
+ selectedCount: 1,
38
+ hasMovableSelection: true,
39
+ hasRotatableSelection: true,
40
+ commandPressed: false,
41
+ shiftPressed: false,
42
+ })
43
+
44
+ expect(hints).toContainEqual({
45
+ keys: ['Left click'],
46
+ label: 'Drag selected movable object',
47
+ })
48
+ expect(hints).toContainEqual({
49
+ keys: ['Cmd/Ctrl', 'Right click'],
50
+ label: 'Drag left or right to rotate selected object',
51
+ })
52
+ // Cmd/Ctrl and Shift click both toggle selection membership (3D selection
53
+ // manager and 2D floorplan alike) — advertised as a single or-group row.
54
+ expect(hints).toContainEqual({
55
+ keys: [['Cmd/Ctrl', 'Shift'], 'Left click'],
56
+ label: 'Add or remove objects from the selection',
57
+ active: false,
58
+ })
59
+ })
60
+
61
+ test('multi-selection advertises the group move + rotate gestures', () => {
62
+ const hints = resolveSelectModeHelpHints({
63
+ selectedCount: 3,
64
+ hasMovableSelection: true,
65
+ hasRotatableSelection: true,
66
+ commandPressed: false,
67
+ shiftPressed: false,
68
+ })
69
+
70
+ expect(hints).toEqual([
71
+ {
72
+ keys: ['Left click'],
73
+ label: 'Click or drag the selection to move it as one',
74
+ },
75
+ { keys: ['R / T'], label: 'Rotate the selection ±45°' },
76
+ {
77
+ keys: [['Cmd/Ctrl', 'Shift'], 'Left click'],
78
+ label: 'Add or remove objects from the selection',
79
+ active: false,
80
+ },
81
+ { keys: ['Esc'], label: 'Clear the selection (or click outside)' },
82
+ ])
83
+ })
84
+
85
+ test('holding a modifier keeps the same rows and only lights the selection one', () => {
86
+ // Guides/snapping are governed by the snapping mode (Shift toggles it),
87
+ // so no modifier-specific "freely / with guides / bypass" variants exist.
88
+ const hints = resolveSelectModeHelpHints({
89
+ selectedCount: 1,
90
+ hasMovableSelection: true,
91
+ hasRotatableSelection: true,
92
+ commandPressed: true,
93
+ shiftPressed: true,
94
+ })
95
+
96
+ expect(hints).toEqual([
97
+ {
98
+ keys: ['Left click'],
99
+ label: 'Drag selected movable object',
100
+ },
101
+ {
102
+ keys: ['Cmd/Ctrl', 'Right click'],
103
+ label: 'Drag left or right to rotate selected object',
104
+ },
105
+ {
106
+ keys: [['Cmd/Ctrl', 'Shift'], 'Left click'],
107
+ label: 'Add or remove objects from the selection',
108
+ active: true,
109
+ },
110
+ ])
111
+ })
112
+ })
@@ -0,0 +1,144 @@
1
+ export type ContextualShortcutHint = {
2
+ // A combo of keys pressed together (rendered joined by "+"). An entry may
3
+ // itself be an array of alternatives (rendered joined by "/"), e.g.
4
+ // [['Cmd/Ctrl', 'Shift'], 'Left click'] → "⌘ / ⇧ + click".
5
+ keys: Array<string | string[]>
6
+ label: string
7
+ // Optional secondary line under the label for a terser qualifier
8
+ // (e.g. "disable 15° snap"). The HUD wraps both lines rather than truncating.
9
+ subtitle?: string
10
+ active?: boolean
11
+ }
12
+
13
+ // `activeHandleDrag.label` value a rotate gizmo sets while dragging, so the
14
+ // contextual HUD can surface the Shift = free-rotation toggle for the duration
15
+ // (mirrors how wall drafting advertises Shift). Distinct from resize handles,
16
+ // which route their own measurement label here.
17
+ export const ROTATE_HANDLE_DRAG_LABEL = 'rotate-handle'
18
+
19
+ // `activeHandleDrag.label` a plain resize / radial-resize arrow sets while
20
+ // dragging (when it carries no dimension `measureLabel`). It exists only so the
21
+ // interaction scope is non-idle during a resize, which keeps the idle
22
+ // select-mode hints off-screen — a resize is its own action, not a selection.
23
+ export const RESIZE_HANDLE_DRAG_LABEL = 'resize-handle'
24
+
25
+ // `activeHandleDrag.label`s for the multi-selection group drags: the body
26
+ // drag sessions (2D floorplan + 3D ground plane) and the rotate gizmo. Kept
27
+ // here (not in the session/gizmo modules) so `snapping-mode.ts` can resolve
28
+ // the group move to the 'item' snap context without importing component code.
29
+ export const GROUP_MOVE_DRAG_LABEL = 'group-move-handle'
30
+ export const GROUP_ROTATE_DRAG_LABEL = 'group-rotate-handle'
31
+
32
+ // Hints shown while a rotate gizmo is mid-drag: Shift bypasses the angle step
33
+ // (free rotation), the same toggle wall drafting exposes. `active` lights the
34
+ // pill while Shift is held.
35
+ export function resolveRotateHandleHelpHints(shiftPressed: boolean): ContextualShortcutHint[] {
36
+ return [
37
+ {
38
+ keys: [SHIFT_KEY],
39
+ label: shiftPressed ? 'Rotating freely (no angle step)' : 'Hold to rotate freely',
40
+ active: shiftPressed,
41
+ },
42
+ ]
43
+ }
44
+
45
+ export type SelectModeHelpContext = {
46
+ selectedCount: number
47
+ hasMovableSelection: boolean
48
+ hasRotatableSelection: boolean
49
+ commandPressed: boolean
50
+ shiftPressed: boolean
51
+ // When a single MEP node is selected its in-world handle rig (click a dot to
52
+ // reveal move arrows) is the real editing path, so the panel leads with the
53
+ // handle-specific hints instead of just the generic Cmd-drag tips.
54
+ mepSelection?: 'run' | 'fitting' | null
55
+ }
56
+
57
+ const COMMAND_KEY = 'Cmd/Ctrl'
58
+ const LEFT_CLICK = 'Left click'
59
+ const RIGHT_CLICK = 'Right click'
60
+ const SHIFT_KEY = 'Shift'
61
+ const CLICK = 'Click'
62
+ const ALT_KEY = 'Alt'
63
+ const ROTATE_KEYS = 'R / T'
64
+ const ESC_KEY = 'Esc'
65
+
66
+ export function resolveSelectModeHelpHints({
67
+ selectedCount,
68
+ hasMovableSelection,
69
+ hasRotatableSelection,
70
+ commandPressed,
71
+ shiftPressed,
72
+ mepSelection = null,
73
+ }: SelectModeHelpContext): ContextualShortcutHint[] {
74
+ const hints: ContextualShortcutHint[] = []
75
+
76
+ if (selectedCount === 0) {
77
+ if (!commandPressed && !shiftPressed) return hints
78
+
79
+ hints.push({
80
+ keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
81
+ label: 'Add or remove objects from the selection',
82
+ active: true,
83
+ })
84
+ return hints
85
+ }
86
+
87
+ // Multi-selection — advertise the group gestures: drag any member to slide
88
+ // the whole selection (2D floor plan body drag + the 3D move gizmo), R / T
89
+ // to step-rotate the group around its bounding-box center.
90
+ if (selectedCount > 1) {
91
+ hints.push({
92
+ keys: [LEFT_CLICK],
93
+ label: 'Click or drag the selection to move it as one',
94
+ })
95
+ hints.push({ keys: [ROTATE_KEYS], label: 'Rotate the selection ±45°' })
96
+ hints.push({
97
+ keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
98
+ label: 'Add or remove objects from the selection',
99
+ active: commandPressed || shiftPressed,
100
+ })
101
+ hints.push({ keys: [ESC_KEY], label: 'Clear the selection (or click outside)' })
102
+ return hints
103
+ }
104
+
105
+ // MEP handle workflow — duct/pipe runs and fittings are edited through the
106
+ // in-world arrow rig that a click on the handle dot reveals, so surface those
107
+ // hints first. A run endpoint's side / up-down arrows swing the run and Alt
108
+ // detaches the joint mid-drag; a fitting's cluster adds rotate arcs, with
109
+ // R / T (and Alt to switch axis) for keyboard rotation.
110
+ if (mepSelection === 'run') {
111
+ hints.push({ keys: [CLICK], label: 'Click a handle dot to show move arrows' })
112
+ hints.push({ keys: [ALT_KEY], label: 'Detach the joint while dragging an arrow' })
113
+ } else if (mepSelection === 'fitting') {
114
+ hints.push({ keys: [CLICK], label: 'Click the handle dot to show move + rotate handles' })
115
+ hints.push({ keys: [ROTATE_KEYS], label: 'Rotate ±45°' })
116
+ hints.push({ keys: [ALT_KEY], label: 'Switch the rotation axis (Y → X → Z)' })
117
+ }
118
+
119
+ // The rows are the same whatever modifier is held — guides/snapping are
120
+ // governed by the snapping mode (Shift toggles it), not by the modifier of
121
+ // this gesture, so there are no modifier-specific variants to advertise.
122
+ // Holding Cmd/Ctrl or Shift just lights the selection row.
123
+ if (hasMovableSelection) {
124
+ hints.push({
125
+ keys: [LEFT_CLICK],
126
+ label: 'Drag selected movable object',
127
+ })
128
+ }
129
+
130
+ if (hasRotatableSelection) {
131
+ hints.push({
132
+ keys: [COMMAND_KEY, RIGHT_CLICK],
133
+ label: 'Drag left or right to rotate selected object',
134
+ })
135
+ }
136
+
137
+ hints.push({
138
+ keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
139
+ label: 'Add or remove objects from the selection',
140
+ active: commandPressed || shiftPressed,
141
+ })
142
+
143
+ return hints
144
+ }