@pascal-app/editor 1.0.7 → 1.1.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 (336) hide show
  1. package/package.json +9 -15
  2. package/src/components/editor/action-menu-button.tsx +1 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
  4. package/src/components/editor/custom-camera-controls.tsx +101 -45
  5. package/src/components/editor/duplicate-node.ts +2 -1
  6. package/src/components/editor/editor-layout-v2.tsx +1 -1
  7. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  8. package/src/components/editor/first-person-controls.tsx +20 -1983
  9. package/src/components/editor/fit-scene-framing.test.ts +318 -0
  10. package/src/components/editor/fit-scene-framing.ts +142 -0
  11. package/src/components/editor/floating-action-menu.tsx +5 -3
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floor-openings-overlay.tsx +1 -1
  14. package/src/components/editor/floor-region-controls.tsx +8 -4
  15. package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
  16. package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
  17. package/src/components/editor/floorplan-panel.tsx +199 -30
  18. package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
  19. package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
  20. package/src/components/editor/follow-pascal.test.tsx +1 -2
  21. package/src/components/editor/follow-pascal.tsx +1 -1
  22. package/src/components/editor/footprint-height-handle.tsx +2 -2
  23. package/src/components/editor/group-actions.ts +1 -2
  24. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  25. package/src/components/editor/group-move-3d.ts +1 -2
  26. package/src/components/editor/group-rotate-handle.tsx +1 -2
  27. package/src/components/editor/group-transform-shared.ts +3 -1
  28. package/src/components/editor/handles/use-handle-drag.ts +1 -2
  29. package/src/components/editor/index.tsx +43 -13
  30. package/src/components/editor/inspector-extension-actions.tsx +1 -1
  31. package/src/components/editor/measurement-pill.tsx +1 -1
  32. package/src/components/editor/node-arrow-handles.tsx +2 -2
  33. package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
  34. package/src/components/editor/open-wall-ends-hint.tsx +1 -1
  35. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  36. package/src/components/editor/quick-measurement-card.tsx +1 -1
  37. package/src/components/editor/room-controls.tsx +10 -6
  38. package/src/components/editor/room-handles-2d.tsx +3 -3
  39. package/src/components/editor/room-handles.tsx +4 -5
  40. package/src/components/editor/room-highlight.tsx +6 -105
  41. package/src/components/editor/room-labels.test.ts +8 -822
  42. package/src/components/editor/room-labels.tsx +41 -1670
  43. package/src/components/editor/room-webgpu-materials.test.ts +1 -1
  44. package/src/components/editor/selection-manager.tsx +5 -3
  45. package/src/components/editor/site-edge-labels.tsx +7 -3
  46. package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
  47. package/src/components/editor/use-floorplan-background-placement.ts +2 -1
  48. package/src/components/editor/views/plugins-view.tsx +1 -1
  49. package/src/components/editor/views/view-bar.tsx +9 -9
  50. package/src/components/editor/wall-measurement-label.tsx +31 -15
  51. package/src/components/editor/wall-move-side-handles.tsx +11 -7
  52. package/src/components/editor/wall-pull-grip.tsx +2 -2
  53. package/src/components/editor/wall-region-handles.tsx +2 -2
  54. package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
  55. package/src/components/editor-2d/floorplan-group-move.tsx +3 -4
  56. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +13 -13
  57. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +2 -3
  58. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
  59. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +23 -6
  60. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
  61. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
  62. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
  63. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
  64. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +14 -3
  65. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
  66. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +30 -10
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +39 -35
  69. package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
  70. package/src/components/systems/roof/roof-edit-system.tsx +33 -21
  71. package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
  72. package/src/components/tools/distribution/distribution-run-tool.tsx +151 -33
  73. package/src/components/tools/item/move-tool.tsx +1 -6
  74. package/src/components/tools/item/placement-history.test.tsx +1 -1
  75. package/src/components/tools/item/use-placement-coordinator.tsx +3 -4
  76. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +7 -3
  77. package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
  78. package/src/components/tools/registry/move-registry-node-tool.tsx +39 -16
  79. package/src/components/tools/shared/cursor-sphere.tsx +1 -1
  80. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  81. package/src/components/tools/shared/placement-box.tsx +2 -6
  82. package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
  83. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  84. package/src/components/tools/shared/segment-angle.ts +0 -4
  85. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  86. package/src/components/tools/tool-manager.tsx +1 -24
  87. package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
  88. package/src/components/tools/wall/wall-typed-entry.ts +70 -0
  89. package/src/components/ui/action-menu/camera-actions.tsx +2 -2
  90. package/src/components/ui/action-menu/control-modes.tsx +1 -1
  91. package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
  92. package/src/components/ui/action-menu/index.tsx +3 -3
  93. package/src/components/ui/action-menu/measurement-control.tsx +3 -3
  94. package/src/components/ui/action-menu/placement.ts +0 -18
  95. package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
  96. package/src/components/ui/action-menu/view-toggles.tsx +5 -5
  97. package/src/components/ui/controls/finish-swatch.tsx +1 -1
  98. package/src/components/ui/controls/material-paint-panel.tsx +2 -2
  99. package/src/components/ui/controls/scene-material-list.tsx +2 -2
  100. package/src/components/ui/controls/select-control.tsx +1 -1
  101. package/src/components/ui/controls/slider-control.tsx +18 -6
  102. package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
  103. package/src/components/ui/controls/tool-options-panel.tsx +74 -44
  104. package/src/components/ui/floating-level-selector.tsx +3 -3
  105. package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
  106. package/src/components/ui/helpers/helper-manager.test.tsx +54 -1
  107. package/src/components/ui/helpers/helper-manager.tsx +55 -1
  108. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  109. package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
  110. package/src/components/ui/panels/floor-foundation-panel.tsx +3 -3
  111. package/src/components/ui/panels/floor-opening-panel.tsx +5 -5
  112. package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
  113. package/src/components/ui/panels/multi-height-mode.tsx +3 -3
  114. package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
  115. package/src/components/ui/panels/node-display.ts +1 -1
  116. package/src/components/ui/panels/panel-manager.tsx +5 -3
  117. package/src/components/ui/panels/panel-wrapper.tsx +42 -19
  118. package/src/components/ui/panels/parametric-inspector.tsx +1 -1
  119. package/src/components/ui/panels/room-construction-rows.tsx +7 -7
  120. package/src/components/ui/panels/room-name-options.ts +1 -5
  121. package/src/components/ui/panels/room-openings-field.tsx +1 -1
  122. package/src/components/ui/panels/room-panel.tsx +7 -9
  123. package/src/components/ui/primitives/color-dot.tsx +2 -2
  124. package/src/components/ui/primitives/opacity-control.tsx +2 -2
  125. package/src/components/ui/primitives/sidebar.tsx +3 -3
  126. package/src/components/ui/right-stack.test.ts +13 -6
  127. package/src/components/ui/right-stack.tsx +22 -11
  128. package/src/components/ui/sidebar/icon-rail.tsx +1 -1
  129. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  130. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  131. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
  132. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
  133. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
  134. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +2 -6
  135. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  136. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
  137. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
  138. package/src/components/ui/sidebar/panels/site-panel/index.tsx +3 -10
  139. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
  140. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  141. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
  142. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
  143. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
  144. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
  145. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
  146. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
  147. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
  148. package/src/components/ui/sidebar/tab-bar.tsx +1 -1
  149. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +2 -2
  150. package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
  151. package/src/components/viewer-overlay.tsx +1 -2
  152. package/src/hooks/use-auto-frame.ts +1 -2
  153. package/src/hooks/use-keyboard.ts +1 -2
  154. package/src/hooks/use-selected-room.ts +5 -7
  155. package/src/i18n/floor-footprint-name.ts +1 -1
  156. package/src/i18n/locales/en/messages.po +1036 -306
  157. package/src/i18n/locales/en/messages.ts +1 -1
  158. package/src/i18n/locales/en-XA/messages.ts +1 -1
  159. package/src/i18n/locales/es/messages.po +2267 -439
  160. package/src/i18n/locales/fr/messages.po +2267 -439
  161. package/src/i18n/locales/ja/messages.po +2267 -439
  162. package/src/i18n/locales/zh-CN/messages.po +2267 -439
  163. package/src/i18n/locales.test.ts +1 -1
  164. package/src/index.tsx +183 -178
  165. package/src/lib/below-grade.test.tsx +4 -4
  166. package/src/lib/camera-facing.test.ts +1 -1
  167. package/src/lib/camera-facing.ts +2 -3
  168. package/src/lib/contextual-help.ts +6 -0
  169. package/src/lib/editor-level-display.ts +1 -1
  170. package/src/lib/elevation-guides.ts +21 -6
  171. package/src/lib/elevator-support.ts +1 -72
  172. package/src/lib/floor-footprints.test.ts +3 -3
  173. package/src/lib/floor-footprints.ts +1 -9
  174. package/src/lib/floor-foundation-support.test.ts +8 -3
  175. package/src/lib/floor-opening-draft.ts +2 -2
  176. package/src/lib/floor-region-session.ts +1 -1
  177. package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
  178. package/src/lib/floorplan/annotation-visibility.ts +1 -1
  179. package/src/lib/floorplan/floorplan-export.test.ts +1 -1
  180. package/src/lib/floorplan/floorplan-export.tsx +8 -11
  181. package/src/lib/floorplan/floorplan-extension.ts +4 -157
  182. package/src/lib/floorplan/floorplan-mode.ts +2 -1
  183. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
  184. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
  185. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
  186. package/src/lib/floorplan/geometry.ts +2 -112
  187. package/src/lib/floorplan/index.ts +0 -9
  188. package/src/lib/floorplan/items.ts +2 -1
  189. package/src/lib/floorplan/open-wall-ends.ts +2 -1
  190. package/src/lib/floorplan/schedules.ts +6 -3
  191. package/src/lib/floorplan/sheet-export.ts +7 -3
  192. package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
  193. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
  194. package/src/lib/follow-pascal-presentation.ts +1 -1
  195. package/src/lib/gesture-lifecycle.ts +3 -1
  196. package/src/lib/glb-export.ts +1 -1
  197. package/src/lib/interaction/scope.ts +1 -0
  198. package/src/lib/layer-thickness.ts +1 -1
  199. package/src/lib/level-selection.test.ts +18 -2
  200. package/src/lib/level-selection.ts +31 -8
  201. package/src/lib/linear-display.ts +1 -1
  202. package/src/lib/live-gesture-round-trip.test.ts +1 -1
  203. package/src/lib/material-paint.ts +1 -1
  204. package/src/lib/measurement-label.test.ts +1 -1
  205. package/src/lib/measurements.test.ts +7 -5
  206. package/src/lib/mezzanine-selection.test.ts +1 -1
  207. package/src/lib/node-action-movement.test.tsx +115 -0
  208. package/src/lib/node-action-movement.ts +2 -0
  209. package/src/lib/paint-region-mode.ts +1 -1
  210. package/src/lib/placement-metadata.ts +4 -0
  211. package/src/lib/point-ask/describe-target.ts +6 -8
  212. package/src/lib/point-ask/region.ts +1 -1
  213. package/src/lib/point-ask/session.ts +1 -2
  214. package/src/lib/print-shell-compiler.ts +12 -1
  215. package/src/lib/print-wall-solids.test.ts +11 -0
  216. package/src/lib/print-wall-solids.ts +9 -0
  217. package/src/lib/roof-duplication.test.ts +100 -0
  218. package/src/lib/roof-duplication.ts +30 -5
  219. package/src/lib/room-built-on.test.ts +2 -5
  220. package/src/lib/room-built-on.ts +1 -15
  221. package/src/lib/room-construction-commands.ts +3 -2
  222. package/src/lib/room-controls-phase6.test.ts +1 -1
  223. package/src/lib/room-handle-drag.ts +8 -37
  224. package/src/lib/room-name-catalog.test.ts +2 -1
  225. package/src/lib/room-name-messages.test.ts +2 -1
  226. package/src/lib/room-name-messages.ts +1 -118
  227. package/src/lib/room-name-storage.test.ts +2 -1
  228. package/src/lib/room-push-dimensions.test.ts +1 -1
  229. package/src/lib/room-selection-commands.ts +2 -2
  230. package/src/lib/room-selection.test.ts +6 -6
  231. package/src/lib/room-transform-session.ts +1 -2
  232. package/src/lib/room-zone-routing.ts +2 -2
  233. package/src/lib/scene-clipboard.ts +2 -1
  234. package/src/lib/selection-routing.test.ts +0 -28
  235. package/src/lib/selection-routing.ts +1 -40
  236. package/src/lib/sfx-bus.ts +8 -32
  237. package/src/lib/sfx-player.test.ts +72 -1
  238. package/src/lib/sfx-player.ts +49 -337
  239. package/src/lib/snapping-mode.test.ts +23 -1
  240. package/src/lib/viewer-selection.test.ts +38 -10
  241. package/src/lib/wall-pull.test.ts +1 -1
  242. package/src/lib/wall-pull.ts +1 -27
  243. package/src/lib/wall-region-list.ts +3 -1
  244. package/src/lib/window-interaction.test.ts +4 -2
  245. package/src/store/use-editor.tsx +22 -26
  246. package/src/store/use-floorplan-annotation-visibility.ts +5 -5
  247. package/src/store/use-placement-typing.test.ts +165 -0
  248. package/src/store/use-placement-typing.ts +186 -0
  249. package/src/store/use-wall-region-handles-selection.ts +1 -2
  250. package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
  251. package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
  252. package/src/components/editor/first-person/build-collider-world.ts +0 -549
  253. package/src/components/editor/first-person/ground-holes.test.ts +0 -128
  254. package/src/components/editor/first-person/ground-holes.ts +0 -130
  255. package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
  256. package/src/components/editor/first-person/mechanism-targets.ts +0 -88
  257. package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
  258. package/src/components/editor/first-person/terrain-collider.ts +0 -212
  259. package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
  260. package/src/components/editor/floorplan-camera-sync.ts +0 -213
  261. package/src/components/editor/localized-html.tsx +0 -22
  262. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
  263. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
  264. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
  265. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
  266. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
  267. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
  268. package/src/components/editor-2d/svg-paths.ts +0 -124
  269. package/src/components/tools/elevator/elevator-defaults.ts +0 -8
  270. package/src/components/tools/elevator/elevator-tool.tsx +0 -372
  271. package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
  272. package/src/components/ui/action-menu/action-button.tsx +0 -89
  273. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
  274. package/src/components/ui/primitives/button.tsx +0 -69
  275. package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
  276. package/src/components/ui/primitives/popover.tsx +0 -42
  277. package/src/components/ui/primitives/switch.tsx +0 -29
  278. package/src/components/ui/primitives/tooltip.tsx +0 -57
  279. package/src/components/ui/scene-loader.tsx +0 -73
  280. package/src/components/viewer/floorplan-compass-button.tsx +0 -67
  281. package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
  282. package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
  283. package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
  284. package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
  285. package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
  286. package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
  287. package/src/components/viewer/floorplan-preview.tsx +0 -1065
  288. package/src/components/viewer/render-settings-menu.tsx +0 -294
  289. package/src/components/viewer/sky-control.tsx +0 -710
  290. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
  291. package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
  292. package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
  293. package/src/components/viewer/viewer-controls-bar.tsx +0 -496
  294. package/src/components/viewer/viewer-hover-label.tsx +0 -42
  295. package/src/components/viewer/viewer-rooms.tsx +0 -248
  296. package/src/components/viewer/viewer-scene-header.tsx +0 -286
  297. package/src/components/viewer/viewer-selection-manager.tsx +0 -124
  298. package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
  299. package/src/components/viewer/viewer-stage-modes.ts +0 -49
  300. package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
  301. package/src/components/viewer/viewer-stage.test.tsx +0 -47
  302. package/src/components/viewer/viewer-stage.tsx +0 -221
  303. package/src/components/walkthrough-hud.tsx +0 -193
  304. package/src/hooks/use-mobile.ts +0 -19
  305. package/src/i18n/level-name.ts +0 -39
  306. package/src/i18n/scene-theme-name.ts +0 -16
  307. package/src/lib/below-grade.ts +0 -133
  308. package/src/lib/camera-pose.test.ts +0 -237
  309. package/src/lib/camera-pose.ts +0 -172
  310. package/src/lib/door-interaction.ts +0 -102
  311. package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
  312. package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
  313. package/src/lib/floorplan/floorplan-readonly.ts +0 -95
  314. package/src/lib/floorplan/marks.test.ts +0 -213
  315. package/src/lib/floorplan/marks.ts +0 -238
  316. package/src/lib/measurement-label.ts +0 -68
  317. package/src/lib/measurements.ts +0 -225
  318. package/src/lib/node-display-label.ts +0 -37
  319. package/src/lib/room-assembly-overlay.test.ts +0 -275
  320. package/src/lib/room-assembly-overlay.ts +0 -265
  321. package/src/lib/room-selection.ts +0 -417
  322. package/src/lib/scene-bounds.test.ts +0 -183
  323. package/src/lib/scene-bounds.ts +0 -169
  324. package/src/lib/typing-target.ts +0 -9
  325. package/src/lib/viewer-selection.ts +0 -595
  326. package/src/lib/window-interaction.ts +0 -142
  327. package/src/lib/zone-content.ts +0 -133
  328. package/src/store/camera-pose-store.test.ts +0 -29
  329. package/src/store/camera-pose-store.ts +0 -27
  330. package/src/store/navigation-sync-pose-store.test.ts +0 -34
  331. package/src/store/navigation-sync-pose-store.ts +0 -27
  332. package/src/store/use-audio.test.ts +0 -146
  333. package/src/store/use-audio.tsx +0 -169
  334. package/src/store/use-first-person-hud.ts +0 -40
  335. package/src/store/use-follow-pascal.test.ts +0 -62
  336. package/src/store/use-follow-pascal.ts +0 -53
@@ -38,7 +38,7 @@ import {
38
38
  useLiveTransforms,
39
39
  useScene,
40
40
  } from '@pascal-app/core'
41
- import { useViewer } from '@pascal-app/viewer'
41
+ import { isTypingTarget, useViewer } from '@pascal-app/viewer'
42
42
  import { useFrame, useThree } from '@react-three/fiber'
43
43
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
44
  import { type Group, Vector3 } from 'three'
@@ -61,7 +61,6 @@ import {
61
61
  surfaceAttachmentUpdates,
62
62
  updateSurfaceNode,
63
63
  } from '../../../lib/surface-attachment'
64
- import { isTypingTarget } from '../../../lib/typing-target'
65
64
  import useAlignmentGuides from '../../../store/use-alignment-guides'
66
65
  import useEditor, {
67
66
  getActiveSnappingMode,
@@ -484,6 +483,18 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
484
483
  return Array.isArray(rotation) ? [rotation[0] ?? 0, y, rotation[2] ?? 0] : y
485
484
  }
486
485
 
486
+ const resolvePosition = (position: [number, number, number]) => {
487
+ const nodes = useScene.getState().nodes
488
+ const current = nodes[node.id] ?? node
489
+ return (
490
+ nodeRegistry.get(current.type)?.capabilities?.movable?.resolvePosition?.({
491
+ node: current,
492
+ position,
493
+ nodes,
494
+ }) ?? position
495
+ )
496
+ }
497
+
487
498
  const keepStartingElevation = (position: [number, number, number], yaw: number) => {
488
499
  if (!preserveLevelAndElevation) return position
489
500
  const rotation = toCommitRotation(yaw)
@@ -534,14 +545,19 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
534
545
  // them through `useLiveNodeOverrides` (transient — no history churn;
535
546
  // GeometrySystem merges overrides via getEffectiveNode). Each connected
536
547
  // node is re-dirtied so its geometry rebuilds against the new override.
537
- const previewConnectivity = (position: [number, number, number], rotationY: number) => {
548
+ // False, previewing nothing, when the connected joints cannot follow.
549
+ const previewConnectivity = (
550
+ position: [number, number, number],
551
+ rotationY: number,
552
+ ): boolean => {
538
553
  const connectivity = connectivityRef.current
539
- if (!connectivity) return
554
+ if (!connectivity) return true
540
555
  const updates = resolveConnectivityUpdates(
541
556
  connectivity,
542
557
  buildPreviewNode(position, rotationY),
543
558
  )
544
- if (updates.length === 0) return
559
+ if (!updates) return false
560
+ if (updates.length === 0) return true
545
561
  useLiveNodeOverrides
546
562
  .getState()
547
563
  .setMany(updates.map((u) => [u.id, u.data as Record<string, unknown>] as const))
@@ -549,6 +565,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
549
565
  for (const u of updates) {
550
566
  if (useScene.getState().nodes[u.id]) useScene.getState().markDirty(u.id)
551
567
  }
568
+ return true
552
569
  }
553
570
 
554
571
  const clearConnectivityOverrides = () => {
@@ -1026,7 +1043,11 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
1026
1043
  },
1027
1044
  ).position
1028
1045
  }
1029
- position = keepStartingElevation(position, rotationRef.current)
1046
+ position = resolvePosition(keepStartingElevation(position, rotationRef.current))
1047
+ // Carry connected ductwork along (preview only — committed on drop). A
1048
+ // position the connected joints cannot follow holds the last frame,
1049
+ // which is what the drop commits.
1050
+ if (!previewConnectivity(position, rotationRef.current)) return
1030
1051
  const visualPosition = getVisualPosition(position)
1031
1052
  hasMovedRef.current = true
1032
1053
  setCursorPosition(visualPosition)
@@ -1054,8 +1075,6 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
1054
1075
  })
1055
1076
  syncParentFramePreview(position)
1056
1077
  markMovedNodeDirty()
1057
- // Carry connected ductwork along (preview only — committed on drop).
1058
- previewConnectivity(position, rotationRef.current)
1059
1078
 
1060
1079
  const soundStep = {
1061
1080
  coords: [x, z],
@@ -1116,7 +1135,7 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
1116
1135
  // tool — unless Alt (free place) is held to force placement. Other kinds
1117
1136
  // carry no validity box (`validRef` stays true), so they're never blocked.
1118
1137
  if (!validRef.current && !altRef.current) return
1119
- const position: [number, number, number] = [...lastCursorRef.current]
1138
+ const position = resolvePosition([...lastCursorRef.current])
1120
1139
 
1121
1140
  const rotation = toCommitRotation(rotationRef.current)
1122
1141
  let committedId = node.id as AnyNodeId
@@ -1171,8 +1190,9 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
1171
1190
  ? resolveConnectivityUpdates(
1172
1191
  connectivityRef.current,
1173
1192
  buildPreviewNode(position, rotationRef.current),
1174
- ).filter((u) => useScene.getState().nodes[u.id])
1193
+ )?.filter((u) => useScene.getState().nodes[u.id])
1175
1194
  : []
1195
+ if (!connectivityUpdates) return
1176
1196
  const surfaceId = surfaceAttachmentId(effectiveNode)
1177
1197
  itemSurfaceMove?.restore()
1178
1198
  useScene.temporal.getState().resume()
@@ -1298,7 +1318,6 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
1298
1318
  attachmentRotationRef.current,
1299
1319
  )
1300
1320
  if (nextFreeRotation === null) return
1301
- sfxEmitter.emit('sfx:item-rotate')
1302
1321
  const hostedPose = itemSurfaceMove?.rotate(nextFreeRotation)
1303
1322
  let position = hostedPose?.position ?? lastCursorRef.current
1304
1323
  if (
@@ -1319,10 +1338,16 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
1319
1338
  )
1320
1339
  position = canonicalPositionFromPlan(planOrigin[0], position[1], planOrigin[2])
1321
1340
  }
1322
- position = keepStartingElevation(position, hostedPose?.rotationY ?? nextFreeRotation)
1341
+ const rotationY = hostedPose?.rotationY ?? nextFreeRotation
1342
+ position = keepStartingElevation(position, rotationY)
1343
+ // Rotating the fitting swings its collars — connected ducts follow. A
1344
+ // swing they cannot follow holds the last frame; a surface-hosted node
1345
+ // has already turned with its host, so its drop refuses instead.
1346
+ if (!previewConnectivity(position, rotationY) && !hostedPose) return
1347
+ sfxEmitter.emit('sfx:item-rotate')
1323
1348
  lastCursorRef.current = position
1324
- freeRotationRef.current = hostedPose?.rotationY ?? nextFreeRotation
1325
- rotationRef.current = freeRotationRef.current
1349
+ freeRotationRef.current = rotationY
1350
+ rotationRef.current = rotationY
1326
1351
  setCursorRotationY(previewRotationY(rotationRef.current))
1327
1352
  const visualPosition = getVisualPosition(position)
1328
1353
  setCursorPosition(visualPosition)
@@ -1335,8 +1360,6 @@ export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
1335
1360
  })
1336
1361
  syncParentFramePreview(position)
1337
1362
  markMovedNodeDirty()
1338
- // Rotating the fitting swings its collars — connected ducts follow.
1339
- previewConnectivity(position, rotationRef.current)
1340
1363
  // Rotation changes the footprint's collision span — re-check validity.
1341
1364
  if (!itemSurfaceMove?.hosted && itemSurfaceMove?.rejection && lastSurfaceEvent)
1342
1365
  onItemMove(lastSurfaceEvent)
@@ -1,4 +1,5 @@
1
1
  import { type MessageDescriptor, usePascalI18n } from '@pascal-app/core'
2
+ import { LocalizedHtml as Html } from '@pascal-app/viewer/ui'
2
3
  import type { ThreeElements } from '@react-three/fiber'
3
4
  import { forwardRef } from 'react'
4
5
  import { CircleGeometry, CylinderGeometry, type Group, SphereGeometry } from 'three'
@@ -8,7 +9,6 @@ import { EDITOR_LAYER } from '../../../lib/constants'
8
9
  import { wallCursorIcon } from '../../../lib/tool-cursor-icon'
9
10
  import useEditor from '../../../store/use-editor'
10
11
  import useWallSnapIndicator from '../../../store/use-wall-snap-indicator'
11
- import { Html } from '../../editor/localized-html'
12
12
 
13
13
  interface CursorSphereProps extends Omit<ThreeElements['group'], 'ref'> {
14
14
  color?: string
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { Html } from '../../editor/localized-html'
3
+ import { LocalizedHtml as Html } from '@pascal-app/viewer/ui'
4
4
 
5
5
  /** Lift of a draft's readout above the top of what it measures, in metres. */
6
6
  export const DRAFT_LABEL_Y_OFFSET = 0.22
@@ -2,17 +2,13 @@
2
2
 
3
3
  import '../../../three-types'
4
4
 
5
+ import { formatLinearMeasurement, type LinearUnit, type MetricNotation } from '@pascal-app/core'
6
+ import { LocalizedHtml as Html } from '@pascal-app/viewer/ui'
5
7
  import { useEffect, useMemo } from 'react'
6
8
  import { PlaneGeometry } from 'three'
7
9
  import { distance, smoothstep, uv, vec2 } from 'three/tsl'
8
10
  import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
9
11
  import { EDITOR_LAYER } from '../../../lib/constants'
10
- import {
11
- formatLinearMeasurement,
12
- type LinearUnit,
13
- type MetricNotation,
14
- } from '../../../lib/measurements'
15
- import { Html } from '../../editor/localized-html'
16
12
  import { createLineGeometry, getBoxEdgePoints } from './placement-box-geometry'
17
13
 
18
14
  const VALID_COLOR = 0x22_c5_5e // green-500
@@ -0,0 +1,94 @@
1
+ 'use client'
2
+
3
+ import { usePascalI18n } from '@pascal-app/core'
4
+ import { type PlacementTypingField, usePlacementTyping } from '../../../store/use-placement-typing'
5
+
6
+ export type PlacementCoordinateInputProps = {
7
+ /** What each empty field stands for; defaults to the session's `fieldDefaults`. */
8
+ placeholders?: readonly string[]
9
+ }
10
+
11
+ /**
12
+ * The typed-entry HUD, one box per field of the session. It only displays the
13
+ * session: keys reach the active tool's own handler (`routePlacementTypingKey`),
14
+ * so the same HUD can sit in the 3D view and the plan at once without
15
+ * competing for focus. Clicking a field makes it the active one.
16
+ */
17
+ export function PlacementCoordinateInput({ placeholders }: PlacementCoordinateInputProps) {
18
+ const i18n = usePascalI18n()
19
+ const isActive = usePlacementTyping((state) => state.isActive)
20
+ const labels = usePlacementTyping((state) => state.labels)
21
+ const fields = usePlacementTyping((state) => state.fields)
22
+ const fieldDefaults = usePlacementTyping((state) => state.fieldDefaults)
23
+ const activeField = usePlacementTyping((state) => state.activeField)
24
+
25
+ if (!isActive) return null
26
+
27
+ return (
28
+ <div
29
+ data-placement-coordinate-entry
30
+ onClick={(event) => event.stopPropagation()}
31
+ onPointerDown={(event) => event.stopPropagation()}
32
+ style={{
33
+ alignItems: 'center',
34
+ background: 'rgba(15, 23, 42, 0.94)',
35
+ border: '1px solid rgba(148, 163, 184, 0.55)',
36
+ borderRadius: 6,
37
+ boxShadow: '0 4px 14px rgba(15, 23, 42, 0.28)',
38
+ color: '#f8fafc',
39
+ display: 'flex',
40
+ gap: 6,
41
+ padding: '5px 7px',
42
+ pointerEvents: 'auto',
43
+ userSelect: 'none',
44
+ whiteSpace: 'nowrap',
45
+ }}
46
+ >
47
+ {labels.map((descriptor, index) => {
48
+ const field = index as PlacementTypingField
49
+ const label = i18n._(descriptor)
50
+ const active = field === activeField
51
+ const value = fields[field]
52
+ return (
53
+ <div
54
+ data-placement-coordinate-field={descriptor.id}
55
+ key={descriptor.id}
56
+ onPointerDown={(event) => {
57
+ event.stopPropagation()
58
+ usePlacementTyping.getState().setActiveField(field)
59
+ }}
60
+ style={{
61
+ alignItems: 'center',
62
+ display: 'flex',
63
+ flexDirection: 'column',
64
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
65
+ fontSize: 9,
66
+ gap: 2,
67
+ textTransform: 'uppercase',
68
+ }}
69
+ >
70
+ <span style={{ color: active ? '#a5b4fc' : '#94a3b8' }}>{label}</span>
71
+ <span
72
+ aria-label={label}
73
+ role="status"
74
+ style={{
75
+ background: active ? '#eef2ff' : '#e2e8f0',
76
+ borderRadius: 3,
77
+ color: value ? '#0f172a' : '#64748b',
78
+ fontSize: 12,
79
+ fontWeight: 600,
80
+ minWidth: 76,
81
+ outline: active ? '2px solid #818cf8' : 'none',
82
+ padding: '3px 5px',
83
+ textAlign: 'right',
84
+ textTransform: 'none',
85
+ }}
86
+ >
87
+ {value || placeholders?.[field] || fieldDefaults[field] || '\u00a0'}
88
+ </span>
89
+ </div>
90
+ )
91
+ })}
92
+ </div>
93
+ )
94
+ }
@@ -1,13 +1,13 @@
1
1
  'use client'
2
2
 
3
3
  import { useViewer } from '@pascal-app/viewer'
4
+ import { LocalizedHtml as Html } from '@pascal-app/viewer/ui'
4
5
  import { useLayoutEffect, useMemo } from 'react'
5
6
  import { BufferGeometry, Float32BufferAttribute, Line as ThreeLine } from 'three'
6
7
  import { LineBasicNodeMaterial } from 'three/webgpu'
7
8
  import { EDITOR_LAYER } from '../../../lib/constants'
8
9
  import type { PlacementPreviewDimension } from '../../../store/use-placement-preview'
9
10
  import usePlacementPreview from '../../../store/use-placement-preview'
10
- import { Html } from '../../editor/localized-html'
11
11
  import { formatMeasurement } from '../../editor/measurement-pill'
12
12
 
13
13
  const DIMENSION_COLOR = 0x63_66_f1
@@ -83,10 +83,6 @@ function getCurveTangentAtPoint(point: PlanPoint, segment: SegmentAngleLike): Pl
83
83
  return best.tangent
84
84
  }
85
85
 
86
- export function formatAngleRadians(angle: number) {
87
- return `${Math.round((angle * 180) / Math.PI)}°`
88
- }
89
-
90
86
  export function getAngleBetweenVectors(first: PlanPoint, second: PlanPoint): number | null {
91
87
  const firstLength = Math.hypot(first[0], first[1])
92
88
  const secondLength = Math.hypot(second[0], second[1])
@@ -10,6 +10,7 @@ import {
10
10
  useLiveTerrain,
11
11
  } from '@pascal-app/core'
12
12
  import { useViewer } from '@pascal-app/viewer'
13
+ import { LocalizedHtml as Html } from '@pascal-app/viewer/ui'
13
14
  import { useFrame, useThree } from '@react-three/fiber'
14
15
  import { type MutableRefObject, useEffect, useMemo, useRef } from 'react'
15
16
  import {
@@ -24,7 +25,6 @@ import { EDITOR_LAYER } from '../../../lib/constants'
24
25
  import { sculptFieldForSite, terrainPointInsideSite } from '../../../lib/terrain-sculpt'
25
26
  import { brushRingColor } from '../../../lib/terrain-verb-color'
26
27
  import useEditor from '../../../store/use-editor'
27
- import { Html } from '../../editor/localized-html'
28
28
  import { formatMeasurement } from '../../editor/measurement-pill'
29
29
 
30
30
  /** Segments in the cursor ring. 64 reads as a circle at any usable brush size. */
@@ -41,7 +41,6 @@ import { FloorRegionControls3D } from '../editor/floor-region-controls'
41
41
  import { OpenWallEnds3DLayer } from '../editor/open-wall-ends-3d-layer'
42
42
  import { OpeningGuides3DLayer } from '../editor/opening-guides-3d-layer'
43
43
  import { WallSnapBeaconLayer } from '../editor/wall-snap-beacon-layer'
44
- import { ElevatorTool } from './elevator/elevator-tool'
45
44
  import { MoveTool } from './item/move-tool'
46
45
  import { WallPaintRegionTool } from './paint-region/wall-paint-region-tool'
47
46
  import { RegistryToolProvider } from './registry-tool-context'
@@ -305,21 +304,6 @@ export const ToolManager: React.FC = () => {
305
304
  const handlePlacedNodeSelected = (nodeId: AnyNodeId) => {
306
305
  setSelection({ selectedIds: [nodeId] })
307
306
  }
308
- const handlePlacedElevatorSelected = (
309
- nodeId: AnyNodeId,
310
- elevatorBuildingId: BuildingNode['id'],
311
- ) => {
312
- // Preserve the active level. `setSelection`'s hierarchy guard nulls
313
- // `levelId` whenever `buildingId` is passed without an explicit
314
- // `levelId` — which deselected the current floor plan the moment an
315
- // elevator was placed. Pass the current level through so the floor
316
- // plan stays selected.
317
- setSelection({
318
- buildingId: elevatorBuildingId,
319
- levelId: activeLevelId ?? null,
320
- selectedIds: [nodeId],
321
- })
322
- }
323
307
 
324
308
  return (
325
309
  <RegistryToolProvider value={registryToolContext}>
@@ -463,14 +447,7 @@ export const ToolManager: React.FC = () => {
463
447
  <RegistryToolComponent />
464
448
  </Suspense>
465
449
  )}
466
- {!movingNode && !useRegistryTool && showBuildTool && tool === 'elevator' && (
467
- <ElevatorTool
468
- buildingId={buildingId as BuildingNode['id'] | null}
469
- levelId={activeLevelId ?? null}
470
- onPlaced={handlePlacedElevatorSelected}
471
- />
472
- )}
473
- {!movingNode && BuildToolComponent && tool !== 'elevator' ? <BuildToolComponent /> : null}
450
+ {!movingNode && BuildToolComponent ? <BuildToolComponent /> : null}
474
451
  {/* Figma-style alignment guides published by the move / placement
475
452
  tools above. Lives inside the building-local group so the
476
453
  building-local guide coords render at the right world position. */}
@@ -0,0 +1,99 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ formatWallDraftHeading,
4
+ resolveTypedWallEnd,
5
+ wallDraftHeadingDegrees,
6
+ } from './wall-typed-entry'
7
+
8
+ const metric = { bareUnit: 'm', system: 'metric' } as const
9
+
10
+ function expectPoint(actual: [number, number] | null, expected: [number, number]) {
11
+ expect(actual).not.toBeNull()
12
+ expect(actual![0]).toBeCloseTo(expected[0], 10)
13
+ expect(actual![1]).toBeCloseTo(expected[1], 10)
14
+ }
15
+
16
+ describe('resolveTypedWallEnd — length only', () => {
17
+ test('keeps the pointer direction and replaces the distance', () => {
18
+ expectPoint(resolveTypedWallEnd([0, 0], [3, 4], ['1', ''], metric), [0.6, 0.8])
19
+ expectPoint(resolveTypedWallEnd([0, 0], [-30, -40], ['5', ''], metric), [-3, -4])
20
+ })
21
+
22
+ test('reads the whole field, not the length the pointer last showed', () => {
23
+ expectPoint(resolveTypedWallEnd([0, 0], [1, 0], ['12', ''], metric), [12, 0])
24
+ })
25
+
26
+ test('honours unit suffixes and the bare unit', () => {
27
+ expectPoint(resolveTypedWallEnd([1, 1], [11, 1], ['250cm', ''], metric), [3.5, 1])
28
+ expectPoint(
29
+ resolveTypedWallEnd([0, 0], [0, 9], ['2500', ''], { bareUnit: 'mm', system: 'metric' }),
30
+ [0, 2.5],
31
+ )
32
+ expectPoint(
33
+ resolveTypedWallEnd([0, 0], [9, 0], ["10'", ''], { bareUnit: 'in', system: 'imperial' }),
34
+ [3.048, 0],
35
+ )
36
+ })
37
+
38
+ test('has no endpoint without a usable length or a direction', () => {
39
+ expect(resolveTypedWallEnd([0, 0], [1, 0], ['', ''], metric)).toBeNull()
40
+ expect(resolveTypedWallEnd([0, 0], [1, 0], ['abc', ''], metric)).toBeNull()
41
+ expect(resolveTypedWallEnd([0, 0], [1, 0], ['0', ''], metric)).toBeNull()
42
+ expect(resolveTypedWallEnd([2, 2], [2, 2], ['5', ''], metric)).toBeNull()
43
+ })
44
+ })
45
+
46
+ describe('resolveTypedWallEnd — angle', () => {
47
+ test('angle only locks the heading; the pointer projection sets the length', () => {
48
+ // 90° runs along +Z; a pointer at (1, 3) projects to 3 m along it.
49
+ expectPoint(resolveTypedWallEnd([0, 0], [1, 3], ['', '90'], metric), [0, 3])
50
+ // (3, 4) projects to 7/√2 along 45°, landing 3.5 out on each axis.
51
+ expectPoint(resolveTypedWallEnd([1, 1], [4, 5], ['', '45'], metric), [4.5, 4.5])
52
+ })
53
+
54
+ test('angle only quantises the projected length to the step', () => {
55
+ expectPoint(resolveTypedWallEnd([0, 0], [0.2, 2.37], ['', '90'], metric, 0.5), [0, 2.5])
56
+ })
57
+
58
+ test('angle only never flips the wall behind its start', () => {
59
+ expectPoint(resolveTypedWallEnd([0, 0], [-4, 0.1], ['', '0'], metric), [0, 0])
60
+ })
61
+
62
+ test('length and angle fix the endpoint whatever the pointer does', () => {
63
+ expectPoint(resolveTypedWallEnd([2, 1], [-50, 80], ['4', '180'], metric), [-2, 1])
64
+ expectPoint(resolveTypedWallEnd([0, 0], [0, 0], ['2', '270'], metric), [0, -2])
65
+ expectPoint(resolveTypedWallEnd([0, 0], [5, 5], ['1', '-90'], metric), [0, -1])
66
+ })
67
+
68
+ test('accepts degree and radian suffixes', () => {
69
+ expectPoint(resolveTypedWallEnd([0, 0], [9, 9], ['2', '90°'], metric), [0, 2])
70
+ expectPoint(resolveTypedWallEnd([0, 0], [9, 9], ['2', `${Math.PI}rad`], metric), [-2, 0])
71
+ })
72
+
73
+ test('an unreadable angle leaves the direction to the pointer', () => {
74
+ expectPoint(resolveTypedWallEnd([0, 0], [0, 4], ['2', 'x'], metric), [0, 2])
75
+ expect(resolveTypedWallEnd([0, 0], [0, 4], ['', 'x'], metric)).toBeNull()
76
+ })
77
+ })
78
+
79
+ describe('wallDraftHeadingDegrees', () => {
80
+ test('measures from +X towards +Z in [0, 360)', () => {
81
+ expect(wallDraftHeadingDegrees([0, 0], [1, 0])).toBeCloseTo(0, 10)
82
+ expect(wallDraftHeadingDegrees([0, 0], [0, 1])).toBeCloseTo(90, 10)
83
+ expect(wallDraftHeadingDegrees([0, 0], [-1, 0])).toBeCloseTo(180, 10)
84
+ expect(wallDraftHeadingDegrees([0, 0], [0, -1])).toBeCloseTo(270, 10)
85
+ expect(wallDraftHeadingDegrees([1, 1], [1, 1])).toBeNull()
86
+ })
87
+
88
+ test('round-trips through a typed angle', () => {
89
+ const heading = wallDraftHeadingDegrees([0, 0], [-3, -4])!
90
+ expectPoint(resolveTypedWallEnd([0, 0], [0, 0], ['5', String(heading)], metric), [-3, -4])
91
+ })
92
+
93
+ test('labels the live heading in the same convention, wrapping 360° to 0°', () => {
94
+ expect(formatWallDraftHeading([0, 0], [0, 1])).toBe('90°')
95
+ expect(formatWallDraftHeading([0, 0], [1, -1])).toBe('315°')
96
+ expect(formatWallDraftHeading([0, 0], [1, -0.001])).toBe('0°')
97
+ expect(formatWallDraftHeading([0, 0], [0, 0])).toBe('')
98
+ })
99
+ })
@@ -0,0 +1,70 @@
1
+ import { formatAngleRadians } from '@pascal-app/core'
2
+ import { type ParseMeasurementOptions, parseMeasurement } from '../../../lib/measurement-parser'
3
+ import type { PlacementTypingValues } from '../../../store/use-placement-typing'
4
+ import type { WallPlanPoint } from './wall-drafting'
5
+
6
+ /**
7
+ * Heading of a wall draft in degrees, the way the 15° angle snap measures it:
8
+ * from the building's +X axis towards +Z (`atan2(dz, dx)`), in [0, 360).
9
+ * On an unrotated plan +Z points down the screen, so 90 runs straight down.
10
+ */
11
+ export function wallDraftHeadingDegrees(start: WallPlanPoint, end: WallPlanPoint): number | null {
12
+ const dx = end[0] - start[0]
13
+ const dz = end[1] - start[1]
14
+ if (Math.hypot(dx, dz) <= 1e-6) return null
15
+ const degrees = (Math.atan2(dz, dx) * 180) / Math.PI
16
+ return ((degrees % 360) + 360) % 360
17
+ }
18
+
19
+ /** The draft's live heading as the angle field's placeholder, e.g. `90°`. */
20
+ export function formatWallDraftHeading(start: WallPlanPoint, end: WallPlanPoint): string {
21
+ const degrees = wallDraftHeadingDegrees(start, end)
22
+ if (degrees === null) return ''
23
+ return formatAngleRadians(((Math.round(degrees) % 360) * Math.PI) / 180)
24
+ }
25
+
26
+ /**
27
+ * The endpoint a typed wall draft commits to. Field 0 is the length, field 1
28
+ * the heading (see `wallDraftHeadingDegrees`); an empty field is left to the
29
+ * pointer:
30
+ * - length only: the pointer steers the direction, the length is exact;
31
+ * - angle only: the direction is locked, the pointer's projection onto it sets
32
+ * the length (quantised to `lengthStep` when one is given);
33
+ * - both: the endpoint is fully determined and the pointer is ignored.
34
+ *
35
+ * Returns null when no field holds a usable value or the pointer gives no
36
+ * direction to follow, so the caller keeps the pointer's own endpoint.
37
+ */
38
+ export function resolveTypedWallEnd(
39
+ start: WallPlanPoint,
40
+ pointer: WallPlanPoint,
41
+ fields: Readonly<PlacementTypingValues>,
42
+ lengthOptions: ParseMeasurementOptions,
43
+ lengthStep = 0,
44
+ ): WallPlanPoint | null {
45
+ const typedLength = fields[0].trim()
46
+ ? parseMeasurement(fields[0], { kind: 'length', unitId: 'm' }, lengthOptions)
47
+ : null
48
+ const length = typedLength !== null && typedLength > 0 ? typedLength : null
49
+ const typedAngle = fields[1].trim()
50
+ ? parseMeasurement(fields[1], { kind: 'angle', unitId: 'rad' }, { bareUnit: 'deg' })
51
+ : null
52
+
53
+ const dx = pointer[0] - start[0]
54
+ const dz = pointer[1] - start[1]
55
+ if (typedAngle === null) {
56
+ if (length === null) return null
57
+ const pointerLength = Math.hypot(dx, dz)
58
+ if (pointerLength <= 1e-6) return null
59
+ return [start[0] + (dx / pointerLength) * length, start[1] + (dz / pointerLength) * length]
60
+ }
61
+
62
+ const dirX = Math.cos(typedAngle)
63
+ const dirZ = Math.sin(typedAngle)
64
+ let distance = length
65
+ if (distance === null) {
66
+ const projected = Math.max(0, dx * dirX + dz * dirZ)
67
+ distance = lengthStep > 0 ? Math.round(projected / lengthStep) * lengthStep : projected
68
+ }
69
+ return [start[0] + dirX * distance, start[1] + dirZ * distance]
70
+ }
@@ -4,8 +4,8 @@ import { emitter, usePascalI18n } from '@pascal-app/core'
4
4
  import Image from 'next/image'
5
5
  import useEditor from '../../../store/use-editor'
6
6
  import { visibleScene } from '../../../store/view-layout'
7
- import { ActionButton } from './action-button'
8
- import { FollowPascalToggle } from './follow-pascal-toggle'
7
+ import { ActionButton } from '@pascal-app/viewer/ui'
8
+ import { FollowPascalToggle } from '@pascal-app/viewer/ui'
9
9
 
10
10
  export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) {
11
11
  const i18n = usePascalI18n()
@@ -9,7 +9,7 @@ import Image from 'next/image'
9
9
  import { Fragment } from 'react'
10
10
  import { cn } from './../../../lib/utils'
11
11
  import useEditor from './../../../store/use-editor'
12
- import { ActionButton } from './action-button'
12
+ import { ActionButton } from '@pascal-app/viewer/ui'
13
13
  import { MeasurementControl } from './measurement-control'
14
14
  import { PointAskButton } from './point-ask-button'
15
15
 
@@ -3,7 +3,7 @@
3
3
  import { usePascalI18n } from '@pascal-app/core'
4
4
  import { AnimatePresence, motion } from 'motion/react'
5
5
  import { useReducedMotion } from '../../../hooks/use-reduced-motion'
6
- import useFollowPascal from '../../../store/use-follow-pascal'
6
+ import { useFollowPascal } from '@pascal-app/viewer/ui'
7
7
 
8
8
  /**
9
9
  * Offered while a build plays and the person has the camera: one press and the camera goes back to
@@ -3,8 +3,8 @@
3
3
  import { useScene } from '@pascal-app/core'
4
4
  import { useViewer } from '@pascal-app/viewer'
5
5
  import { motion } from 'motion/react'
6
- import { TooltipProvider } from './../../../components/ui/primitives/tooltip'
7
- import { useIsMobile } from './../../../hooks/use-mobile'
6
+ import { TooltipProvider } from '@pascal-app/viewer/ui'
7
+ import { useIsMobile } from '@pascal-app/viewer/ui'
8
8
  import { useReducedMotion } from './../../../hooks/use-reduced-motion'
9
9
  import { shouldShowEditingControls } from './../../../lib/interaction/overlay-policy'
10
10
  import { cn } from './../../../lib/utils'
@@ -12,7 +12,7 @@ import useEditor from './../../../store/use-editor'
12
12
  import { CameraActions } from './camera-actions'
13
13
  import { ControlModes } from './control-modes'
14
14
  import { FollowPascalPill } from './follow-pascal-pill'
15
- import { type ActionMenuPlacement, ActionMenuPlacementProvider } from './placement'
15
+ import { type ActionMenuPlacement, ActionMenuPlacementProvider } from '@pascal-app/viewer/ui'
16
16
  import { SecondaryToggles } from './view-toggles'
17
17
 
18
18
  // Mobile bottom offset matches the viewer's overlap behind the sheet's
@@ -27,9 +27,9 @@ import { cn } from '../../../lib/utils'
27
27
  import useEditor from '../../../store/use-editor'
28
28
  import { VIEW_2D } from '../../../store/view-layout'
29
29
  import useFloorplanMode from '../../../store/use-floorplan-mode'
30
- import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
31
- import { ActionButton } from './action-button'
32
- import { useActionMenuPopupSide } from './placement'
30
+ import { Popover, PopoverContent, PopoverTrigger } from '@pascal-app/viewer/ui'
31
+ import { ActionButton } from '@pascal-app/viewer/ui'
32
+ import { useActionMenuPopupSide } from '@pascal-app/viewer/ui'
33
33
 
34
34
  const measurementOptions = [
35
35
  { kind: 'distance', label: /* i18n */ { id: 'editor.actionMenu.measurementControl.distance', message: 'Distance' }, icon: Ruler },
@@ -1,20 +1,2 @@
1
- import { createContext, useContext } from 'react'
2
-
3
- /** Viewport edge the action menu docks to. */
4
- export type ActionMenuPlacement = 'bottom' | 'top'
5
-
6
1
  /** Where chrome under a top-docked menu starts: below the host toolbar row and the menu itself. */
7
2
  export const TOP_MENU_CLEARANCE = '7.25rem'
8
-
9
- const ActionMenuPlacementContext = createContext<ActionMenuPlacement>('bottom')
10
-
11
- export const ActionMenuPlacementProvider = ActionMenuPlacementContext.Provider
12
-
13
- export function useActionMenuPlacement(): ActionMenuPlacement {
14
- return useContext(ActionMenuPlacementContext)
15
- }
16
-
17
- /** Side tooltips and popovers open toward, away from the docked edge. */
18
- export function useActionMenuPopupSide(): 'top' | 'bottom' {
19
- return useActionMenuPlacement() === 'top' ? 'bottom' : 'top'
20
- }
@@ -6,7 +6,7 @@ import { cn } from '../../../lib/utils'
6
6
  import useEditor from '../../../store/use-editor'
7
7
  import { usePointAsk } from '../../../store/use-point-ask'
8
8
  import { isViewVisible, VIEW_3D } from '../../../store/view-layout'
9
- import { ActionButton } from './action-button'
9
+ import { ActionButton } from '@pascal-app/viewer/ui'
10
10
 
11
11
  /**
12
12
  * Point and ask (key C): the toolbar's way into the mode where hovering names what Pascal would