@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
@@ -1,414 +1,126 @@
1
- import { Howl, Howler } from 'howler'
2
- import useAudio from '../store/use-audio'
3
-
4
- // Per-sound variation config. Playback rate also shifts pitch (one semitone ≈ 1.0595×),
5
- // so a rate range of ~0.88–1.12 reads as a subtle ±2 semitones — enough to kill the
6
- // machine-gun feeling when the same SFX fires in rapid succession.
7
- type SFXConfig = {
8
- // One file, or several pre-rendered variations cycled round-robin per play.
9
- src: string | string[]
10
- // Random playback-rate range applied per play (1 = unchanged).
11
- rateRange?: [number, number]
12
- // Random volume multiplier range applied per play (1 = unchanged).
13
- volumeRange?: [number, number]
14
- // Minimum gap between two plays of this SFX. Triggers within this window
15
- // are silently dropped so bursty sequences don't phase-stack into noise.
16
- minIntervalMs?: number
17
- }
18
-
19
- type LoopSFXConfig = {
20
- src: string
21
- volumeMultiplier: number
22
- fadeInMs?: number
23
- fadeOutMs?: number
24
- }
25
-
26
- const DEFAULT_MIN_INTERVAL_MS = 30
27
- const SFX_FAILURE_BACKOFF_MS = 5_000
28
- const DEFAULT_LOOP_FADE_MS = 90
29
-
30
- // SFX sound definitions
31
- export const SFX: Record<string, SFXConfig> = {
1
+ import {
2
+ disposeSounds,
3
+ playSound,
4
+ preloadSounds,
5
+ SOUNDS,
6
+ type SoundDefinition,
7
+ type SoundPlaybackOptions,
8
+ startSoundLoop,
9
+ stopSoundLoop,
10
+ updateSoundVolumes,
11
+ } from '@pascal-app/viewer/sfx'
12
+
13
+ export const SFX = {
14
+ ...SOUNDS,
32
15
  gridSnap: {
33
- src: [
34
- '/audios/sfx/grid_snap_0.mp3',
35
- '/audios/sfx/grid_snap_1.mp3',
36
- '/audios/sfx/grid_snap_2.mp3',
37
- ],
16
+ src: ['grid_snap_0.mp3', 'grid_snap_1.mp3', 'grid_snap_2.mp3'],
38
17
  rateRange: [0.98, 1.02],
39
18
  volumeRange: [0.5, 0.6],
40
19
  minIntervalMs: 50,
41
20
  },
42
21
  itemDelete: {
43
- src: '/audios/sfx/item_delete.mp3',
22
+ src: 'item_delete.mp3',
44
23
  rateRange: [0.9, 1.1],
45
24
  volumeRange: [0.9, 1.0],
46
25
  },
47
26
  itemPick: {
48
- src: '/audios/sfx/item_pick.mp3',
27
+ src: 'item_pick.mp3',
49
28
  rateRange: [0.95, 1.05],
50
29
  volumeRange: [0.92, 1.0],
51
30
  },
52
31
  itemPlace: {
53
- src: '/audios/sfx/item_place.mp3',
32
+ src: 'item_place.mp3',
54
33
  rateRange: [0.98, 1.02],
55
34
  volumeRange: [0.9, 1.0],
56
35
  },
57
36
  itemRotate: {
58
- src: '/audios/sfx/item_rotate.mp3',
37
+ src: 'item_rotate.mp3',
59
38
  rateRange: [0.94, 1.06],
60
39
  volumeRange: [0.92, 1.0],
61
40
  },
62
- // Ticks as a resize handle is dragged across snap steps. Fires in rapid
63
- // succession, so it mirrors gridSnap: three variations cycled round-robin
64
- // with pitch jitter and a gap so the run reads as texture, not a tone.
65
41
  resize: {
66
- src: ['/audios/sfx/resize_0.mp3', '/audios/sfx/resize_1.mp3', '/audios/sfx/resize_2.mp3'],
42
+ src: ['resize_0.mp3', 'resize_1.mp3', 'resize_2.mp3'],
67
43
  rateRange: [0.98, 1.02],
68
44
  volumeRange: [0.26, 0.34],
69
45
  minIntervalMs: 80,
70
46
  },
71
- // Fired when a structure draft begins (first click of a wall/slab/etc).
72
47
  structureBuildStart: {
73
- src: '/audios/sfx/structure_build_start.mp3',
48
+ src: 'structure_build_start.mp3',
74
49
  rateRange: [0.95, 1.05],
75
50
  volumeRange: [0.88, 1.0],
76
51
  },
77
- // Fired when a structure is committed (segment placed / polygon closed).
78
52
  structureBuildEnd: {
79
- src: '/audios/sfx/structure_build_end.mp3',
53
+ src: 'structure_build_end.mp3',
80
54
  rateRange: [0.95, 1.05],
81
55
  volumeRange: [0.88, 1.0],
82
56
  },
83
57
  structureDelete: {
84
- src: '/audios/sfx/structure_delete.mp3',
58
+ src: 'structure_delete.mp3',
85
59
  rateRange: [0.9, 1.1],
86
60
  volumeRange: [0.9, 1.0],
87
61
  },
88
62
  snapshotCapture: {
89
63
  // Shutter should sound consistent — no variation.
90
- src: '/audios/sfx/snapshot_capture.mp3',
91
- },
92
- // Soft tick when hovering a main category in the Build / Items panels.
93
- // Kept quiet and rate-locked so sweeping across the grid reads as texture,
94
- // not a melody.
95
- menuHover: {
96
- src: '/audios/sfx/menu_hover.mp3',
97
- rateRange: [0.98, 1.02],
98
- volumeRange: [0.2, 0.3],
99
- minIntervalMs: 0,
100
- },
101
- // Fired when a main category in the Build / Items panels is clicked.
102
- menuClick: {
103
- src: '/audios/sfx/menu_click.mp3',
104
- rateRange: [0.98, 1.02],
105
- volumeRange: [0.5, 0.6],
64
+ src: 'snapshot_capture.mp3',
106
65
  },
107
- // Fired when a material is applied to a surface in paint mode. Painting can
108
- // fire in quick succession across faces, so keep variation + a small gap.
109
66
  paintApply: {
110
- src: '/audios/sfx/paint_apply.mp3',
67
+ src: 'paint_apply.mp3',
111
68
  rateRange: [0.95, 1.05],
112
69
  volumeRange: [0.85, 1.0],
113
70
  minIntervalMs: 60,
114
71
  },
115
- // A three-second jingle for finishing something, not a click cue. No pitch or
116
- // volume jitter — a fanfare that lands a semitone off reads as broken rather
117
- // than as varied — and a gap longer than the sound itself, so two milestones
118
- // that land together play once instead of phasing over each other.
119
72
  success: {
120
- src: '/audios/sfx/success.mp3',
73
+ src: 'success.mp3',
121
74
  minIntervalMs: 3_000,
122
75
  },
123
- // An agent's turn, heard from another tab or another app: it ended with its work done, it failed,
124
- // or it waits on the person. Two-note chimes (`scripts/synth-turn-cues.ts`), so no jitter, as for
125
- // `success`. The host plays each once per turn; no gap here could tell two turns from one.
126
- turnDone: { src: '/audios/sfx/turn_done.mp3' },
127
- turnFailed: { src: '/audios/sfx/turn_failed.mp3' },
128
- turnNeedsInput: { src: '/audios/sfx/turn_needs_input.mp3' },
129
- // Operating the building from a viewer's callout or a walkthrough: recorded
130
- // door and shutter movements; the switches and the viewer's hover, select and
131
- // deselect ticks are the marketing house's synth cues (`scripts/synth-viewer-cues.ts`),
132
- // already mixed against the doors, so they play at full volume without jitter.
133
- doorOpen: { src: '/audios/sfx/door_open.mp3', rateRange: [0.96, 1.04], volumeRange: [0.9, 1] },
134
- doorClose: { src: '/audios/sfx/door_close.mp3', rateRange: [0.96, 1.04], volumeRange: [0.9, 1] },
135
- // Air moving as a leaf swings shut, ahead of the latch (`scripts/synth-viewer-cues.ts`).
136
- doorSwing: { src: '/audios/sfx/door_swing.mp3', rateRange: [0.97, 1.03] },
137
- garageDoor: { src: '/audios/sfx/garage_door.mp3', volumeRange: [0.9, 1] },
138
- shuttersOpen: { src: '/audios/sfx/shutters_open.mp3', rateRange: [0.96, 1.04] },
139
- shuttersClose: { src: '/audios/sfx/shutters_close.mp3', rateRange: [0.96, 1.04] },
140
- switchOn: { src: '/audios/sfx/switch_on.mp3' },
141
- switchOff: { src: '/audios/sfx/switch_off.mp3' },
142
- viewerHover: { src: '/audios/sfx/viewer_hover.mp3', minIntervalMs: 95 },
143
- viewerSelect: { src: '/audios/sfx/viewer_select.mp3', minIntervalMs: 90 },
144
- viewerDeselect: { src: '/audios/sfx/viewer_deselect.mp3' },
145
- } as const
76
+ turnDone: { src: 'turn_done.mp3' },
77
+ turnFailed: { src: 'turn_failed.mp3' },
78
+ turnNeedsInput: { src: 'turn_needs_input.mp3' },
79
+ } satisfies Record<string, SoundDefinition>
146
80
 
147
81
  export type SFXName = keyof typeof SFX
148
82
 
149
83
  export const LOOP_SFX = {
150
84
  terrainRaise: {
151
- src: '/audios/sfx/terrain_raise.mp3',
85
+ src: 'terrain_raise.mp3',
86
+ loop: true,
152
87
  volumeMultiplier: 0.2,
153
88
  },
154
89
  terrainLower: {
155
- src: '/audios/sfx/terrain_lower.mp3',
90
+ src: 'terrain_lower.mp3',
91
+ loop: true,
156
92
  volumeMultiplier: 0.2,
157
93
  },
158
94
  terrainFlatten: {
159
- src: '/audios/sfx/terrain_flatten.mp3',
95
+ src: 'terrain_flatten.mp3',
96
+ loop: true,
160
97
  volumeMultiplier: 0.2,
161
98
  },
162
99
  terrainSmooth: {
163
- src: '/audios/sfx/terrain_smooth.mp3',
100
+ src: 'terrain_smooth.mp3',
101
+ loop: true,
164
102
  volumeMultiplier: 0.2,
165
103
  },
166
- } as const satisfies Record<string, LoopSFXConfig>
104
+ } as const satisfies Record<string, SoundDefinition>
167
105
 
168
106
  export type LoopSFXName = keyof typeof LOOP_SFX
169
- type CachedSFXName = SFXName | LoopSFXName
170
-
171
- function loopConfig(name: LoopSFXName): LoopSFXConfig {
172
- return LOOP_SFX[name]
173
- }
174
-
175
- export type SFXPlaybackOptions = {
176
- source?: 'local' | 'remote'
177
- stereo?: number
178
- volumeMultiplier?: number
179
- }
180
-
181
- function randomInRange([min, max]: [number, number]): number {
182
- return min + Math.random() * (max - min)
183
- }
184
-
185
- let sfxCache = new Map<CachedSFXName, Howl[]>()
186
- let sfxAudioContext: AudioContext | null = null
187
- let sfxRetryAfter = 0
188
- const lastPlayedAt = new Map<SFXName, number>()
189
- const lastVariation = new Map<SFXName, number>()
190
- let activeLoop: {
191
- id: number
192
- name: LoopSFXName
193
- sound: Howl
194
- volume: number
195
- } | null = null
196
- let pendingLoop: { name: LoopSFXName; sound: Howl } | null = null
197
- let requestedLoop: LoopSFXName | null = null
198
-
199
- function loopVolume(name: LoopSFXName): number {
200
- const { masterVolume, sfxVolume } = useAudio.getState()
201
- return (masterVolume / 100) * (sfxVolume / 100) * LOOP_SFX[name].volumeMultiplier
202
- }
203
-
204
- function stopActiveLoop(fadeMs: number) {
205
- const active = activeLoop
206
- if (!active) return
207
- activeLoop = null
208
107
 
209
- if (fadeMs <= 0) {
210
- active.sound.stop(active.id)
211
- return
212
- }
213
-
214
- active.sound.once('fade', () => active.sound.stop(active.id), active.id)
215
- active.sound.fade(active.volume, 0, fadeMs, active.id)
216
- }
217
-
218
- function unloadCachedSounds(resetPlaybackState: boolean) {
219
- requestedLoop = null
220
- pendingLoop = null
221
- stopActiveLoop(0)
222
- for (const sounds of sfxCache.values()) {
223
- for (const sound of sounds) {
224
- try {
225
- sound.unload()
226
- } catch {}
227
- }
228
- }
229
- sfxCache.clear()
230
- sfxAudioContext = null
231
- if (resetPlaybackState) {
232
- sfxRetryAfter = 0
233
- lastPlayedAt.clear()
234
- lastVariation.clear()
235
- }
236
- }
237
-
238
- function cacheNeedsRebuild(): boolean {
239
- if (sfxCache.size === 0) return true
240
- if (sfxAudioContext !== Howler.ctx) return true
241
- for (const sounds of sfxCache.values()) {
242
- if (sounds.some((sound) => sound.state() === 'unloaded')) return true
243
- }
244
- return false
245
- }
108
+ export type SFXPlaybackOptions = SoundPlaybackOptions
246
109
 
247
110
  export function preloadSFX() {
248
- if (!cacheNeedsRebuild()) return
249
- unloadCachedSounds(false)
250
-
251
- const definitions = { ...SFX, ...LOOP_SFX }
252
- for (const [name, config] of Object.entries(definitions)) {
253
- const sources = Array.isArray(config.src) ? config.src : [config.src]
254
- sfxCache.set(
255
- name as CachedSFXName,
256
- sources.map(
257
- (src) =>
258
- new Howl({
259
- loop: name in LOOP_SFX,
260
- src: [src],
261
- preload: true,
262
- volume: 0.5,
263
- }),
264
- ),
265
- )
266
- }
267
- sfxAudioContext = Howler.ctx ?? null
111
+ preloadSounds([...Object.values(SFX), ...Object.values(LOOP_SFX)])
268
112
  }
269
113
 
270
- export function disposeSFX() {
271
- unloadCachedSounds(true)
114
+ export function playSFX(name: SFXName, options?: SFXPlaybackOptions) {
115
+ preloadSFX()
116
+ playSound(SFX[name], options)
272
117
  }
273
118
 
274
119
  export function startLoopSFX(name: LoopSFXName) {
275
- const { muted } = useAudio.getState()
276
- if (muted) {
277
- stopLoopSFX()
278
- return
279
- }
280
-
281
- requestedLoop = name
282
- const current = activeLoop
283
- if (current?.name === name && current.sound.playing(current.id)) return
284
- if (current) stopActiveLoop(loopConfig(current.name).fadeOutMs ?? DEFAULT_LOOP_FADE_MS)
285
-
286
- const now = performance.now()
287
- if (now < sfxRetryAfter) return
288
-
289
- try {
290
- preloadSFX()
291
- // A fresh Howler context rebuilds the cache and clears stale playback
292
- // state. Restore this current press after that cleanup so a first-use loop
293
- // can still begin when its asset finishes loading.
294
- requestedLoop = name
295
- const sound = sfxCache.get(name)?.[0]
296
- if (!sound) return
297
- if (sound.state() !== 'loaded') {
298
- if (pendingLoop?.name === name && pendingLoop.sound === sound) return
299
- pendingLoop = { name, sound }
300
- sound.once('load', () => {
301
- if (pendingLoop?.sound === sound) pendingLoop = null
302
- if (requestedLoop === name) startLoopSFX(name)
303
- })
304
- return
305
- }
306
-
307
- const config = loopConfig(name)
308
- const volume = loopVolume(name)
309
- const id = sound.play()
310
- sound.volume(0, id)
311
- activeLoop = { id, name, sound, volume }
312
- sound.fade(0, volume, config.fadeInMs ?? DEFAULT_LOOP_FADE_MS, id)
313
- } catch {
314
- unloadCachedSounds(false)
315
- sfxRetryAfter = now + SFX_FAILURE_BACKOFF_MS
316
- }
120
+ preloadSFX()
121
+ startSoundLoop(LOOP_SFX[name])
317
122
  }
318
123
 
319
- export function stopLoopSFX() {
320
- requestedLoop = null
321
- pendingLoop = null
322
- const fadeMs = activeLoop ? (loopConfig(activeLoop.name).fadeOutMs ?? DEFAULT_LOOP_FADE_MS) : 0
323
- stopActiveLoop(fadeMs)
324
- }
325
-
326
- /**
327
- * Play a sound effect with volume based on audio settings
328
- */
329
- export function playSFX(name: SFXName, options: SFXPlaybackOptions = {}) {
330
- const config = SFX[name]!
331
- const { masterVolume, sfxVolume, muted } = useAudio.getState()
332
-
333
- if (muted) return
334
-
335
- // Drop rapid repeats — two plays of the same SFX within minIntervalMs just
336
- // smear into noise, they don't add useful information.
337
- const now = performance.now()
338
- if (now < sfxRetryAfter) return
339
- const minInterval = config.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS
340
- const source = options.source ?? 'local'
341
- const playbackKey = `${source}:${name}`
342
- const last = lastPlayedAt.get(playbackKey)
343
- if (last !== undefined && now - last < minInterval) return
344
- // Local feedback stays legible when a collaborator makes the same kind of
345
- // change at nearly the same time; the quieter remote cue yields instead.
346
- const lastLocal = lastPlayedAt.get(`local:${name}`)
347
- if (source === 'remote' && lastLocal !== undefined && now - lastLocal < 120) return
348
- lastPlayedAt.set(playbackKey, now)
349
-
350
- try {
351
- preloadSFX()
352
- const sounds = sfxCache.get(name)
353
- if (!sounds || sounds.length === 0) return
354
-
355
- // Pick a random variation, avoiding an immediate repeat of the last one so
356
- // consecutive plays don't land on the same file.
357
- let index = Math.floor(Math.random() * sounds.length)
358
- if (sounds.length > 1 && index === lastVariation.get(name)) {
359
- index = (index + 1) % sounds.length
360
- }
361
- lastVariation.set(name, index)
362
- const sound = sounds[index]!
363
- // Howler queues per-play mutations while a sound is loading. If its global
364
- // AudioContext is replaced before that queue drains, stereo setup can try
365
- // to connect nodes from different contexts and throw asynchronously.
366
- if (sound.state() !== 'loaded') return
367
- const baseVolume = (masterVolume / 100) * (sfxVolume / 100)
368
- const volumeJitter = config.volumeRange ? randomInRange(config.volumeRange) : 1
369
- const volumeMultiplier = Number.isFinite(options.volumeMultiplier)
370
- ? Math.max(0, Math.min(2, options.volumeMultiplier!))
371
- : 1
372
- const rate = config.rateRange ? randomInRange(config.rateRange) : 1
373
- const id = sound.play()
374
- sound.volume(baseVolume * volumeJitter * volumeMultiplier, id)
375
- if (Number.isFinite(options.stereo)) {
376
- sound.stereo(Math.max(-1, Math.min(1, options.stereo!)), id)
377
- }
378
- if (rate !== 1) sound.rate(rate, id)
379
- } catch {
380
- // Optional audio must never abort an editor input callback. Rebuild from
381
- // the current Howler context after a backoff instead of retrying every pointer cue.
382
- unloadCachedSounds(false)
383
- sfxRetryAfter = now + SFX_FAILURE_BACKOFF_MS
384
- }
385
- }
386
-
387
- /**
388
- * Update all cached SFX volumes (useful when settings change)
389
- */
390
- export function updateSFXVolumes() {
391
- const { masterVolume, muted, sfxVolume } = useAudio.getState()
392
- if (muted) {
393
- stopLoopSFX()
394
- return
395
- }
396
- const finalVolume = (masterVolume / 100) * (sfxVolume / 100)
397
-
398
- try {
399
- if (performance.now() < sfxRetryAfter) return
400
- preloadSFX()
401
- sfxCache.forEach((sounds) => {
402
- sounds.forEach((sound) => {
403
- sound.volume(finalVolume)
404
- })
405
- })
406
- if (activeLoop) {
407
- activeLoop.volume = loopVolume(activeLoop.name)
408
- activeLoop.sound.volume(activeLoop.volume, activeLoop.id)
409
- }
410
- } catch {
411
- unloadCachedSounds(false)
412
- sfxRetryAfter = performance.now() + SFX_FAILURE_BACKOFF_MS
413
- }
414
- }
124
+ export const stopLoopSFX = stopSoundLoop
125
+ export const updateSFXVolumes = updateSoundVolumes
126
+ export const disposeSFX = disposeSounds
@@ -106,6 +106,8 @@ describe('snapContextOf (profile-driven, node-declared)', () => {
106
106
  roof: 'structural',
107
107
  zone: 'structural',
108
108
  block: 'structural',
109
+ door: 'item',
110
+ window: 'item',
109
111
  }
110
112
  const profileOf = (t: string) => declared[t]
111
113
  const profileOfNode = (id: string) =>
@@ -173,7 +175,7 @@ describe('snapContextOf (profile-driven, node-declared)', () => {
173
175
  })
174
176
 
175
177
  it('an undeclared kind (no snapProfile) gets no snap context', () => {
176
- expect(ctx({ kind: 'moving', nodeType: 'door' })).toBeNull()
178
+ expect(ctx({ kind: 'moving', nodeType: 'unknown_plugin_kind' })).toBeNull()
177
179
  expect(ctx({ kind: 'idle' }, 'build', 'shelf')).toBeNull()
178
180
  })
179
181
 
@@ -205,4 +207,24 @@ describe('snapContextOf (profile-driven, node-declared)', () => {
205
207
  }),
206
208
  ).toBe('polygon')
207
209
  })
210
+ it('moving scope resolves active snap context when moving a window or door', () => {
211
+ expect(
212
+ snapContextOf({
213
+ scope: { kind: 'moving', nodeType: 'window' },
214
+ mode: 'select',
215
+ tool: null,
216
+ profileOf,
217
+ profileOfNode,
218
+ }),
219
+ ).toBe('item')
220
+ expect(
221
+ snapContextOf({
222
+ scope: { kind: 'moving', nodeType: 'door' },
223
+ mode: 'select',
224
+ tool: null,
225
+ profileOf,
226
+ profileOfNode,
227
+ }),
228
+ ).toBe('item')
229
+ })
208
230
  })
@@ -9,6 +9,8 @@ import {
9
9
  ItemNode,
10
10
  LevelNode,
11
11
  type NodeEvent,
12
+ RoofNode,
13
+ RoofSegmentNode,
12
14
  SlabNode,
13
15
  sceneRegistry,
14
16
  UnitNode,
@@ -18,25 +20,25 @@ import {
18
20
  } from '@pascal-app/core'
19
21
  import { migrateRoomZones } from '@pascal-app/core/scene-migrations'
20
22
  import { resolveGlbPick, useViewer } from '@pascal-app/viewer'
21
- import { CameraControlsImpl } from '@react-three/drei'
22
- import { createElement } from 'react'
23
- import * as THREE from 'three'
24
- import { Group, Vector3 } from 'three'
25
- import { viewerHoverLabel } from '../components/viewer/viewer-hover-label'
26
- import { ViewerSelectionManager } from '../components/viewer/viewer-selection-manager'
27
- import { publishCameraPose } from '../store/camera-pose-store'
28
- import useEditor, { takePreviewCameraRestore } from '../store/use-editor'
29
- import { withSelectionHarness } from '../test-utils/selection-harness'
30
23
  import {
31
24
  frameViewerCamera,
25
+ publishCameraPose,
32
26
  resolveViewerClick,
33
27
  resolveViewerHover,
34
28
  selectViewerRoom,
35
29
  type ViewerSelection,
30
+ ViewerSelectionManager,
36
31
  viewerBreadcrumb,
32
+ viewerHoverLabel,
37
33
  viewerPickFromNodeEvent,
38
34
  viewerSidebarSections,
39
- } from './viewer-selection'
35
+ } from '@pascal-app/viewer/ui'
36
+ import { CameraControlsImpl } from '@react-three/drei'
37
+ import { createElement } from 'react'
38
+ import * as THREE from 'three'
39
+ import { Group, Vector3 } from 'three'
40
+ import useEditor, { takePreviewCameraRestore } from '../store/use-editor'
41
+ import { withSelectionHarness } from '../test-utils/selection-harness'
40
42
 
41
43
  const english = {
42
44
  _: (descriptor: { id: string; message?: string }) => descriptor.message ?? descriptor.id,
@@ -164,6 +166,32 @@ describe('viewer selection rules', () => {
164
166
  }
165
167
  })
166
168
 
169
+ test("a roof's segment or its face of another kind selects the whole roof", () => {
170
+ const segment = RoofSegmentNode.parse({ id: 'rseg_main', parentId: 'roof_main' })
171
+ // A plugin's roof plane (`capabilities.roofs`): the click picks its roof as a segment's does.
172
+ const plane = {
173
+ object: 'node',
174
+ id: 'roofplane_south',
175
+ type: 'test:roof-plane',
176
+ parentId: 'roof_main',
177
+ visible: true,
178
+ metadata: {},
179
+ } as unknown as AnyNode
180
+ const roof = RoofNode.parse({
181
+ id: 'roof_main',
182
+ parentId: levelId,
183
+ children: [segment.id, plane.id],
184
+ })
185
+ const { nodes } = scene([roof])
186
+ nodes[segment.id] = segment
187
+ nodes[plane.id] = plane
188
+ for (const nodeId of [segment.id, plane.id])
189
+ expect(resolveViewerClick(nothing, { nodeId, point: [2, 2] }, nodes)).toMatchObject({
190
+ levelId,
191
+ selectedIds: [roof.id],
192
+ })
193
+ })
194
+
167
195
  test('empty space clears the room and the element and keeps the floor', () => {
168
196
  const { nodes, kitchen } = scene()
169
197
  const drilled: ViewerSelection = {
@@ -12,6 +12,7 @@ import {
12
12
  WallNode as WallSchema,
13
13
  wallAssemblyPatch,
14
14
  wallClosesRoom,
15
+ wallPullSeed,
15
16
  } from '@pascal-app/core'
16
17
  import { useViewer } from '@pascal-app/viewer'
17
18
  import { createWallOnCurrentLevel } from '../components/tools/wall/wall-drafting'
@@ -23,7 +24,6 @@ import {
23
24
  startWallPull,
24
25
  useWallPullRun,
25
26
  wallDraftIsRun,
26
- wallPullSeed,
27
27
  wallPullTypePatch,
28
28
  } from './wall-pull'
29
29
  import { isWallRunFree, snapWallRunDraftPoint, wallRunDirection } from './wall-run-snap'
@@ -19,13 +19,13 @@
19
19
 
20
20
  import {
21
21
  type AnyNodeId,
22
- getWallCurveFrameAt,
23
22
  getWallThickness,
24
23
  useScene,
25
24
  WALL_CONSTRUCTION_FIELDS,
26
25
  type WallNode,
27
26
  type WallPlanPoint,
28
27
  wallFollowsStorey,
28
+ wallPullSeed,
29
29
  } from '@pascal-app/core'
30
30
  import { useViewer } from '@pascal-app/viewer'
31
31
  import { create } from 'zustand'
@@ -34,15 +34,6 @@ import type { ContinuationMode } from './continuation'
34
34
  import { getWallDrawVariant, selectWallDrawVariant } from './wall-draw-variant'
35
35
  import { publishWallRunFeedback } from './wall-run-snap'
36
36
 
37
- /** The floor plan's affordance id for a wall end's pull grip (a `move-arrow`, variant `pull`). */
38
- export const WALL_PULL_AFFORDANCE = 'wall-pull'
39
-
40
- /** What a pull grip carries: the wall, and which of its ends it pulls from. */
41
- export type WallPullGripPayload = { wallId: string; endpoint: 'start' | 'end' }
42
-
43
- /** The grip's tooltip, in both views. */
44
- export const WALL_PULL_GRIP_TITLE = 'Drag to draw the next wall'
45
-
46
37
  export type WallPullRun = {
47
38
  /** Bumped per pull, so each view seeds its draft from it once. */
48
39
  seq: number
@@ -93,23 +84,6 @@ export function wallPullTypePatch(wall: WallNode, storeyHeight: number | null):
93
84
  return patch as Partial<WallNode>
94
85
  }
95
86
 
96
- /**
97
- * The pulled end of `wall` and the direction out of it: along the wall for a
98
- * straight one, along its tangent for a curved one.
99
- */
100
- export function wallPullSeed(
101
- wall: Pick<WallNode, 'start' | 'end' | 'curveOffset'>,
102
- endpoint: 'start' | 'end',
103
- ): { start: WallPlanPoint; reference: WallPlanPoint } | null {
104
- const start: WallPlanPoint =
105
- endpoint === 'start' ? [wall.start[0], wall.start[1]] : [wall.end[0], wall.end[1]]
106
- const tangent = getWallCurveFrameAt(wall as WallNode, endpoint === 'start' ? 0 : 1).tangent
107
- const sign = endpoint === 'start' ? -1 : 1
108
- const length = Math.hypot(tangent.x, tangent.y)
109
- if (!(length > 1e-9)) return null
110
- return { start, reference: [(tangent.x / length) * sign, (tangent.y / length) * sign] }
111
- }
112
-
113
87
  // What the pull changed for its run, put back when it ends.
114
88
  let restore: { shape: ContinuationMode; toolDefaults: Record<string, unknown> | null } | null = null
115
89
  let stopWatching: (() => void) | null = null
@@ -1,7 +1,10 @@
1
1
  import {
2
2
  type AnyNode,
3
3
  getCatalogMaterialById,
4
+ getLinearUnitLabel,
4
5
  getMaterialSwatch,
6
+ type LinearUnit,
7
+ metersToLinearUnit,
5
8
  parseMaterialRef,
6
9
  roomSideFaces,
7
10
  type SceneMaterial,
@@ -11,7 +14,6 @@ import {
11
14
  type WallFaceRegion,
12
15
  type WallNode,
13
16
  } from '@pascal-app/core'
14
- import { getLinearUnitLabel, type LinearUnit, metersToLinearUnit } from './measurements'
15
17
 
16
18
  // Rows for the wall panel's "Paint regions" list: finish swatch + name, the
17
19
  // face in room terms when the wall has a room on exactly one side, and the
@@ -1,11 +1,13 @@
1
1
  import { afterEach, expect, test } from 'bun:test'
2
- import { useInteractive, useScene, WindowNode } from '@pascal-app/core'
3
2
  import {
4
3
  getDisplayedWindowShutterValue,
5
4
  getDisplayedWindowValue,
6
5
  openWindowOpenState,
7
6
  setWindowShuttersOpenState,
8
- } from '../index'
7
+ useInteractive,
8
+ useScene,
9
+ WindowNode,
10
+ } from '@pascal-app/core'
9
11
 
10
12
  const originalScene = useScene.getState()
11
13
  const originalInteractive = useInteractive.getState()