@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,14 +1,7 @@
1
1
  import type { MessageDescriptor } from '@pascal-app/core'
2
+ import { type DescriptorTranslator, displayRoomName } from '@pascal-app/viewer/ui'
2
3
  import type { RoomNameSection } from './room-name-catalog'
3
4
 
4
- /**
5
- * The room-name catalog in the UI language. The catalog stays English (it is
6
- * matched, numbered and stored as data); the combobox and room labels show these.
7
- * `room-name-catalog.test.ts` fails when a catalog name has no entry here.
8
- */
9
- /** Anything that translates a descriptor: a `PascalI18n`, or a stand-in in tests. */
10
- export type DescriptorTranslator = { _(descriptor: MessageDescriptor): string }
11
-
12
5
  export const ROOM_NAME_SECTION_TITLES: Record<string, MessageDescriptor> = {
13
6
  living: /* i18n */ { id: 'editor.roomName.section.living', message: 'Living' },
14
7
  sleeping: /* i18n */ { id: 'editor.roomName.section.sleeping', message: 'Sleeping' },
@@ -20,72 +13,6 @@ export const ROOM_NAME_SECTION_TITLES: Record<string, MessageDescriptor> = {
20
13
  mezzanine: /* i18n */ { id: 'editor.roomName.section.mezzanine', message: 'Mezzanine' },
21
14
  }
22
15
 
23
- export const ROOM_NAME_MESSAGES: Record<string, MessageDescriptor> = {
24
- 'Living room': /* i18n */ { id: 'editor.roomName.livingRoom', message: 'Living room' },
25
- 'Family room': /* i18n */ { id: 'editor.roomName.familyRoom', message: 'Family room' },
26
- 'Dining room': /* i18n */ { id: 'editor.roomName.diningRoom', message: 'Dining room' },
27
- Kitchen: /* i18n */ { id: 'editor.roomName.kitchen', message: 'Kitchen' },
28
- Kitchenette: /* i18n */ { id: 'editor.roomName.kitchenette', message: 'Kitchenette' },
29
- Pantry: /* i18n */ { id: 'editor.roomName.pantry', message: 'Pantry' },
30
- Lounge: /* i18n */ { id: 'editor.roomName.lounge', message: 'Lounge' },
31
- Den: /* i18n */ { id: 'editor.roomName.den', message: 'Den' },
32
- Library: /* i18n */ { id: 'editor.roomName.library', message: 'Library' },
33
- Playroom: /* i18n */ { id: 'editor.roomName.playroom', message: 'Playroom' },
34
- 'Home cinema': /* i18n */ { id: 'editor.roomName.homeCinema', message: 'Home cinema' },
35
- 'Games room': /* i18n */ { id: 'editor.roomName.gamesRoom', message: 'Games room' },
36
- Sunroom: /* i18n */ { id: 'editor.roomName.sunroom', message: 'Sunroom' },
37
- Bedroom: /* i18n */ { id: 'editor.roomName.bedroom', message: 'Bedroom' },
38
- 'Primary bedroom': /* i18n */ {
39
- id: 'editor.roomName.primaryBedroom',
40
- message: 'Primary bedroom',
41
- },
42
- 'Guest room': /* i18n */ { id: 'editor.roomName.guestRoom', message: 'Guest room' },
43
- "Kids' room": /* i18n */ { id: 'editor.roomName.kidsRoom', message: "Kids' room" },
44
- Nursery: /* i18n */ { id: 'editor.roomName.nursery', message: 'Nursery' },
45
- 'Dressing room': /* i18n */ { id: 'editor.roomName.dressingRoom', message: 'Dressing room' },
46
- 'Walk-in closet': /* i18n */ { id: 'editor.roomName.walkInCloset', message: 'Walk-in closet' },
47
- Bathroom: /* i18n */ { id: 'editor.roomName.bathroom', message: 'Bathroom' },
48
- 'En-suite': /* i18n */ { id: 'editor.roomName.enSuite', message: 'En-suite' },
49
- 'Shower room': /* i18n */ { id: 'editor.roomName.showerRoom', message: 'Shower room' },
50
- Toilet: /* i18n */ { id: 'editor.roomName.toilet', message: 'Toilet' },
51
- 'Powder room': /* i18n */ { id: 'editor.roomName.powderRoom', message: 'Powder room' },
52
- Office: /* i18n */ { id: 'editor.roomName.office', message: 'Office' },
53
- Study: /* i18n */ { id: 'editor.roomName.study', message: 'Study' },
54
- Studio: /* i18n */ { id: 'editor.roomName.studio', message: 'Studio' },
55
- Workshop: /* i18n */ { id: 'editor.roomName.workshop', message: 'Workshop' },
56
- Gym: /* i18n */ { id: 'editor.roomName.gym', message: 'Gym' },
57
- Entrance: /* i18n */ { id: 'editor.roomName.entrance', message: 'Entrance' },
58
- Hallway: /* i18n */ { id: 'editor.roomName.hallway', message: 'Hallway' },
59
- Corridor: /* i18n */ { id: 'editor.roomName.corridor', message: 'Corridor' },
60
- Landing: /* i18n */ { id: 'editor.roomName.landing', message: 'Landing' },
61
- Stairwell: /* i18n */ { id: 'editor.roomName.stairwell', message: 'Stairwell' },
62
- Mudroom: /* i18n */ { id: 'editor.roomName.mudroom', message: 'Mudroom' },
63
- Laundry: /* i18n */ { id: 'editor.roomName.laundry', message: 'Laundry' },
64
- 'Utility room': /* i18n */ { id: 'editor.roomName.utilityRoom', message: 'Utility room' },
65
- Storage: /* i18n */ { id: 'editor.roomName.storage', message: 'Storage' },
66
- Closet: /* i18n */ { id: 'editor.roomName.closet', message: 'Closet' },
67
- 'Boiler room': /* i18n */ { id: 'editor.roomName.boilerRoom', message: 'Boiler room' },
68
- Cellar: /* i18n */ { id: 'editor.roomName.cellar', message: 'Cellar' },
69
- Basement: /* i18n */ { id: 'editor.roomName.basement', message: 'Basement' },
70
- Attic: /* i18n */ { id: 'editor.roomName.attic', message: 'Attic' },
71
- Garage: /* i18n */ { id: 'editor.roomName.garage', message: 'Garage' },
72
- Garden: /* i18n */ { id: 'editor.roomName.garden', message: 'Garden' },
73
- Patio: /* i18n */ { id: 'editor.roomName.patio', message: 'Patio' },
74
- Terrace: /* i18n */ { id: 'editor.roomName.terrace', message: 'Terrace' },
75
- Balcony: /* i18n */ { id: 'editor.roomName.balcony', message: 'Balcony' },
76
- Porch: /* i18n */ { id: 'editor.roomName.porch', message: 'Porch' },
77
- Veranda: /* i18n */ { id: 'editor.roomName.veranda', message: 'Veranda' },
78
- Deck: /* i18n */ { id: 'editor.roomName.deck', message: 'Deck' },
79
- Courtyard: /* i18n */ { id: 'editor.roomName.courtyard', message: 'Courtyard' },
80
- 'Pool area': /* i18n */ { id: 'editor.roomName.poolArea', message: 'Pool area' },
81
- Carport: /* i18n */ { id: 'editor.roomName.carport', message: 'Carport' },
82
- 'Garden shed': /* i18n */ { id: 'editor.roomName.gardenShed', message: 'Garden shed' },
83
- Mezzanine: /* i18n */ { id: 'editor.roomName.mezzanine', message: 'Mezzanine' },
84
- Loft: /* i18n */ { id: 'editor.roomName.loft', message: 'Loft' },
85
- 'Reading nook': /* i18n */ { id: 'editor.roomName.readingNook', message: 'Reading nook' },
86
- Gallery: /* i18n */ { id: 'editor.roomName.gallery', message: 'Gallery' },
87
- }
88
-
89
16
  export function localizedRoomNameSections(
90
17
  sections: readonly RoomNameSection[],
91
18
  i18n: DescriptorTranslator,
@@ -101,47 +28,3 @@ export function localizedRoomNameSections(
101
28
  }
102
29
  })
103
30
  }
104
-
105
- /**
106
- * A room name in the UI language. Catalog names stay English in the scene
107
- * (numbering, MCP and exports read them); a catalog name with the number the
108
- * picker adds shows translated with that number ("Bedroom 2" → "Chambre 2").
109
- * Anything else the user typed shows as written.
110
- */
111
- export function displayRoomName(name: string, i18n: DescriptorTranslator): string {
112
- const { lead, base, number, trail } = roomNameParts(name)
113
- const label = catalogLabel(base, i18n)
114
- return label === undefined ? name : `${lead}${label}${number}${trail}`
115
- }
116
-
117
- /**
118
- * What an edit of a room's name field stores. The field shows the room's own
119
- * catalog name translated ("Lounge 2" as "Salon 2"); when the edit only
120
- * changes, adds or removes that name's number ("Salon 3"), it stores the
121
- * English name with the new number ("Lounge 3"), as editing it in English
122
- * would. Any other text, an exact translation of a different name included, is
123
- * stored exactly as typed.
124
- */
125
- export function editedRoomName(stored: string, text: string, i18n: DescriptorTranslator): string {
126
- const own = roomNameParts(stored).base
127
- const label = catalogLabel(own, i18n)
128
- const edited = roomNameParts(text)
129
- if (label === undefined || edited.base !== label) return text
130
- return `${edited.lead}${own}${edited.number}${edited.trail}`
131
- }
132
-
133
- function roomNameParts(name: string) {
134
- const match = /^(\s*)(.*?)(\s+\d+)?(\s*)$/.exec(name)
135
- return {
136
- lead: match?.[1] ?? '',
137
- base: match?.[2] ?? '',
138
- number: match?.[3] ?? '',
139
- trail: match?.[4] ?? '',
140
- }
141
- }
142
-
143
- function catalogLabel(english: string, i18n: DescriptorTranslator): string | undefined {
144
- // Own keys only: a room named "constructor" or "__proto__" is free text.
145
- if (!(english && Object.hasOwn(ROOM_NAME_MESSAGES, english))) return undefined
146
- return i18n._(ROOM_NAME_MESSAGES[english]!)
147
- }
@@ -15,6 +15,7 @@ import {
15
15
  } from '@pascal-app/core'
16
16
  import { AGENT_OPERATIONS, applyAgentOutcome } from '@pascal-app/core/agent-operations'
17
17
  import { createAgentHost, liveSceneRuntime } from '@pascal-app/core/runtime'
18
+ import { displayRoomName } from '@pascal-app/viewer/ui'
18
19
  import {
19
20
  commitRoomNameEdit,
20
21
  commitRoomNamePick,
@@ -22,7 +23,7 @@ import {
22
23
  roomNameOptions,
23
24
  } from '../components/ui/panels/room-name-options'
24
25
  import { roomNameSections } from './room-name-catalog'
25
- import { displayRoomName, localizedRoomNameSections } from './room-name-messages'
26
+ import { localizedRoomNameSections } from './room-name-messages'
26
27
  import { applyRoomPlan, renameRoom } from './room-structure-commands'
27
28
  import { copySelectedNodesToEditorClipboard, pasteEditorClipboardToLevel } from './scene-clipboard'
28
29
 
@@ -2,11 +2,11 @@ import { describe, expect, test } from 'bun:test'
2
2
  import {
3
3
  type AnyNode,
4
4
  createRoomTopologyIndex,
5
+ formatLinearMeasurement,
5
6
  LevelNode,
6
7
  type Point,
7
8
  WallNode,
8
9
  } from '@pascal-app/core'
9
- import { formatLinearMeasurement } from './measurements'
10
10
  import {
11
11
  DIMENSION_FLOOR_LIFT,
12
12
  mezzanineEdgeDimensions,
@@ -1,9 +1,9 @@
1
1
  import { type AnyNodeId, type BuildingNode, type LevelNode, useScene } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
+ import type { SelectionModifierKeys } from '@pascal-app/viewer/ui'
4
+ import { type RoomKey, shouldSelectRoom } from '@pascal-app/viewer/ui'
3
5
  import useEditor from '../store/use-editor'
4
6
  import useSessionGroups from '../store/use-session-groups'
5
- import { type RoomKey, shouldSelectRoom } from './room-selection'
6
- import type { SelectionModifierKeys } from './selection-routing'
7
7
 
8
8
  export function selectRoom(room: RoomKey) {
9
9
  const level = useScene.getState().nodes[room.levelId as AnyNodeId]
@@ -24,6 +24,12 @@ import {
24
24
  } from '@pascal-app/core'
25
25
  import { migrateCeilingRoomLinks, migrateRoomZones } from '@pascal-app/core/scene-migrations'
26
26
  import { useViewer } from '@pascal-app/viewer'
27
+ import {
28
+ type RoomKey,
29
+ RoomSelectionIndex,
30
+ resolveRoomHit,
31
+ shouldSelectRoom,
32
+ } from '@pascal-app/viewer/ui'
27
33
  import { _roots, act } from '@react-three/fiber'
28
34
  import { createElement } from 'react'
29
35
  import { Group, type LineSegments } from 'three'
@@ -37,12 +43,6 @@ import useSessionGroups from '../store/use-session-groups'
37
43
  import { withSelectionHarness } from '../test-utils/selection-harness'
38
44
  import { startRoomDivide } from './room-divide-session'
39
45
  import { runRoomHandleDrag } from './room-handle-drag'
40
- import {
41
- type RoomKey,
42
- RoomSelectionIndex,
43
- resolveRoomHit,
44
- shouldSelectRoom,
45
- } from './room-selection'
46
46
  import { selectRoom, selectRoomFromHit, shouldInterceptRoom } from './room-selection-commands'
47
47
 
48
48
  const levelId = 'level_room_selection'
@@ -10,7 +10,7 @@ import {
10
10
  useScene,
11
11
  type ZoneTransformPlan,
12
12
  } from '@pascal-app/core'
13
- import { useViewer } from '@pascal-app/viewer'
13
+ import { isTypingTarget, useViewer } from '@pascal-app/viewer'
14
14
  import { Plane, Vector2, Vector3 } from 'three'
15
15
  import { create } from 'zustand'
16
16
  import { levelFrame } from '../components/editor/group-transform-shared'
@@ -23,7 +23,6 @@ import { beginGesture, type GestureHandle } from './gesture-lifecycle'
23
23
  import { isHistoryShortcut } from './history'
24
24
  import { mezzanineConflictMessage } from './mezzanine-messages'
25
25
  import { sfxEmitter } from './sfx-bus'
26
- import { isTypingTarget } from './typing-target'
27
26
 
28
27
  export const ROOM_MOVE_DRAG_LABEL = 'room-move'
29
28
 
@@ -1,11 +1,11 @@
1
1
  import { type AnyNode, type AnyNodeId, useScene, type ZoneNode } from '@pascal-app/core'
2
2
  import { useViewer } from '@pascal-app/viewer'
3
+ import { type RoomKey, type SelectionModifierKeys, sameRoom } from '@pascal-app/viewer/ui'
3
4
  import { getRoomSelectionIndex, hoverRoomFromHit } from '../hooks/use-selected-room'
4
5
  import useEditor from '../store/use-editor'
5
6
  import { expandSessionSelectionForNode } from '../store/use-session-groups'
6
- import { type RoomKey, sameRoom } from './room-selection'
7
7
  import { selectRoom } from './room-selection-commands'
8
- import { resolveSelectedIdsForNodeClick, type SelectionModifierKeys } from './selection-routing'
8
+ import { resolveSelectedIdsForNodeClick } from './selection-routing'
9
9
 
10
10
  // A room is one thing to the user whichever way they reach it: the canvas, the
11
11
  // Rooms list, the plan's fill or its label. Rooms the room index knows (closed,
@@ -6,6 +6,7 @@ import {
6
6
  generateSceneMaterialId,
7
7
  getArtifactStore,
8
8
  isDerivedNode,
9
+ isNodeDuplicable,
9
10
  isStoreyContent,
10
11
  type LevelNode,
11
12
  nodeRegistry,
@@ -156,7 +157,7 @@ function isClipboardRoot(
156
157
  }
157
158
 
158
159
  function isCopyableRootType(node: AnyNode) {
159
- if (isDerivedNode(node)) return false
160
+ if (isDerivedNode(node) || !isNodeDuplicable(node)) return false
160
161
  if (COPYABLE_ROOT_TYPES.has(node.type)) return true
161
162
  const definition = nodeRegistry.get(node.type)
162
163
  return !!definition && definition.capabilities?.duplicable !== false
@@ -18,7 +18,6 @@ import {
18
18
  resolveCanvasBuildingId,
19
19
  resolveCanvasSelectionNode,
20
20
  resolveSelectedIdsForNodeClick,
21
- selectionModifiersFromEvent,
22
21
  shouldPreserveSelectedRoofHostTarget,
23
22
  } from './selection-routing'
24
23
 
@@ -249,33 +248,6 @@ describe('deleteNodeFromCanvas', () => {
249
248
  })
250
249
  })
251
250
 
252
- describe('selectionModifiersFromEvent', () => {
253
- test('falls back to tracked modifier state when the click event omits keys', () => {
254
- expect(
255
- selectionModifiersFromEvent({}, { meta: false, ctrl: true, shift: false, alt: false }),
256
- ).toEqual({
257
- meta: false,
258
- ctrl: true,
259
- shift: false,
260
- alt: false,
261
- })
262
- })
263
-
264
- test('prefers explicit event key state over stale tracked modifiers', () => {
265
- expect(
266
- selectionModifiersFromEvent(
267
- { metaKey: false, ctrlKey: false, shiftKey: false, altKey: false },
268
- { meta: true, ctrl: true, shift: true, alt: true },
269
- ),
270
- ).toEqual({
271
- meta: false,
272
- ctrl: false,
273
- shift: false,
274
- alt: false,
275
- })
276
- })
277
- })
278
-
279
251
  describe('resolveCanvasSelectionNode', () => {
280
252
  // Mirrors the cabinet-module setup: modules proxy to their run for grouped
281
253
  // move / rotate, but declare `selectionProxy.bypassDirectPick` so a direct
@@ -8,6 +8,7 @@ import {
8
8
  useScene,
9
9
  } from '@pascal-app/core'
10
10
  import { useViewer } from '@pascal-app/viewer'
11
+ import type { SelectionModifierKeys } from '@pascal-app/viewer/ui'
11
12
  import useEditor from '../store/use-editor'
12
13
  import { emitDeleteSFX } from './sfx-bus'
13
14
 
@@ -43,14 +44,6 @@ export function enterBuildingFromPlanHit(hitId: string | null): boolean {
43
44
  return node ? enterBuildingFromCanvas(node) : false
44
45
  }
45
46
 
46
- export type SelectionModifierKeys = {
47
- meta: boolean
48
- ctrl: boolean
49
- shift: boolean
50
- /** Alt alone: select one session-group member without expanding. */
51
- alt: boolean
52
- }
53
-
54
47
  /** A Delete-mode click on a plan entry: the sledgehammer, not a selection. */
55
48
  /**
56
49
  * A canvas click (2D or 3D) selected `node`: publish it on
@@ -111,38 +104,6 @@ export function isSelectionModifierActive(keys: SelectionModifierKeys): boolean
111
104
  return keys.meta || keys.ctrl || keys.shift
112
105
  }
113
106
 
114
- export function selectionModifiersFromEvent(
115
- event?: {
116
- metaKey?: boolean
117
- ctrlKey?: boolean
118
- shiftKey?: boolean
119
- altKey?: boolean
120
- nativeEvent?: {
121
- metaKey?: boolean
122
- ctrlKey?: boolean
123
- shiftKey?: boolean
124
- altKey?: boolean
125
- }
126
- } | null,
127
- fallback?: Partial<SelectionModifierKeys>,
128
- ): SelectionModifierKeys {
129
- const fromEvent = (
130
- key: keyof SelectionModifierKeys,
131
- eventKey: 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey',
132
- ) => {
133
- if (typeof event?.[eventKey] === 'boolean') return event[eventKey]
134
- if (typeof event?.nativeEvent?.[eventKey] === 'boolean') return event.nativeEvent[eventKey]
135
- return Boolean(fallback?.[key])
136
- }
137
-
138
- return {
139
- meta: fromEvent('meta', 'metaKey'),
140
- ctrl: fromEvent('ctrl', 'ctrlKey'),
141
- shift: fromEvent('shift', 'shiftKey'),
142
- alt: fromEvent('alt', 'altKey'),
143
- }
144
- }
145
-
146
107
  export function resolveSelectedIdsForNodeClick({
147
108
  baseSelectedIds,
148
109
  currentSelectedIds,
@@ -1,11 +1,11 @@
1
- import { emitter, type InteractionSound, type TerrainVerb } from '@pascal-app/core'
1
+ import type { TerrainVerb } from '@pascal-app/core'
2
+ import { initInteractionSounds } from '@pascal-app/viewer/sfx'
2
3
  import mitt from 'mitt'
3
- import useAudio from '../store/use-audio'
4
4
  import {
5
5
  disposeSFX,
6
6
  type LoopSFXName,
7
7
  playSFX,
8
- type SFXName,
8
+ preloadSFX,
9
9
  startLoopSFX,
10
10
  stopLoopSFX,
11
11
  updateSFXVolumes,
@@ -73,30 +73,7 @@ const TERRAIN_LOOP_BY_VERB = {
73
73
  } as const satisfies Record<TerrainVerb, LoopSFXName>
74
74
  const handleTerrainSculptStart = (verb: TerrainVerb) => startLoopSFX(TERRAIN_LOOP_BY_VERB[verb])
75
75
  const handleTerrainSculptStop = () => stopLoopSFX()
76
- let unsubscribeAudio: (() => void) | null = null
77
-
78
- /** Windows ride the door recordings, softer: a sash weighs less than a leaf. */
79
- const INTERACTION_SFX: Record<InteractionSound, [SFXName, number]> = {
80
- 'door-open': ['doorOpen', 1],
81
- 'door-swing': ['doorSwing', 1],
82
- 'door-close': ['doorClose', 1],
83
- 'window-open': ['doorOpen', 0.6],
84
- 'window-swing': ['doorSwing', 0.6],
85
- 'window-close': ['doorClose', 0.6],
86
- garage: ['garageDoor', 1],
87
- 'shutters-open': ['shuttersOpen', 1],
88
- 'shutters-close': ['shuttersClose', 1],
89
- // Cabinet doors and drawers: the panel recordings, as the marketing house plays its cabinets.
90
- 'panel-open': ['shuttersOpen', 0.75],
91
- 'panel-swing': ['doorSwing', 0.45],
92
- 'panel-close': ['shuttersClose', 0.75],
93
- 'switch-on': ['switchOn', 1],
94
- 'switch-off': ['switchOff', 1],
95
- }
96
- const handleInteractionSound = (sound: InteractionSound) => {
97
- const [name, volumeMultiplier] = INTERACTION_SFX[sound]
98
- playSFX(name, { volumeMultiplier })
99
- }
76
+ let unsubscribeInteractions: (() => void) | null = null
100
77
 
101
78
  /**
102
79
  * Initialize SFX Bus - connects SFX events to actual sound playback.
@@ -124,8 +101,8 @@ export function initSFXBus() {
124
101
  sfxEmitter.on('sfx:turn-needs-input', handleTurnNeedsInput)
125
102
  sfxEmitter.on('sfx:terrain-sculpt-start', handleTerrainSculptStart)
126
103
  sfxEmitter.on('sfx:terrain-sculpt-stop', handleTerrainSculptStop)
127
- emitter.on('interaction:sound', handleInteractionSound)
128
- unsubscribeAudio = useAudio.subscribe(updateSFXVolumes)
104
+ unsubscribeInteractions = initInteractionSounds()
105
+ preloadSFX()
129
106
  updateSFXVolumes()
130
107
  }
131
108
 
@@ -150,9 +127,8 @@ export function disposeSFXBus() {
150
127
  sfxEmitter.off('sfx:turn-needs-input', handleTurnNeedsInput)
151
128
  sfxEmitter.off('sfx:terrain-sculpt-start', handleTerrainSculptStart)
152
129
  sfxEmitter.off('sfx:terrain-sculpt-stop', handleTerrainSculptStop)
153
- emitter.off('interaction:sound', handleInteractionSound)
154
- unsubscribeAudio?.()
155
- unsubscribeAudio = null
130
+ unsubscribeInteractions?.()
131
+ unsubscribeInteractions = null
156
132
  sfxBusInitialized = false
157
133
  }
158
134
  disposeSFX()
@@ -1,5 +1,6 @@
1
1
  import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test'
2
- import useAudio from '../store/use-audio'
2
+ import { emitter } from '@pascal-app/core'
3
+ import { configureViewer } from '@pascal-app/viewer'
3
4
 
4
5
  type FakeContext = { id: string }
5
6
 
@@ -93,12 +94,18 @@ const fakeHowler = {
93
94
 
94
95
  mock.module('howler', () => ({ Howl: FakeHowl, Howler: fakeHowler }))
95
96
 
97
+ const sharedSounds = await import('@pascal-app/viewer/sfx')
98
+ const howlsOnImport = instances.length
99
+
100
+ const { useAudioSettings: useAudio } = sharedSounds
101
+
96
102
  const { disposeSFX, playSFX, preloadSFX, startLoopSFX, stopLoopSFX, updateSFXVolumes } =
97
103
  await import('./sfx-player')
98
104
  const { disposeSFXBus, initSFXBus, sfxEmitter, triggerSFX } = await import('./sfx-bus')
99
105
 
100
106
  beforeEach(() => {
101
107
  disposeSFXBus()
108
+ configureViewer({ sfxBaseUrl: '/audios/sfx' })
102
109
  activeContext = { id: 'first' }
103
110
  initialState = 'loaded'
104
111
  useAudio.setState({ masterVolume: 100, muted: false, sfxVolume: 100 })
@@ -108,6 +115,7 @@ beforeEach(() => {
108
115
 
109
116
  afterAll(() => {
110
117
  disposeSFXBus()
118
+ configureViewer({})
111
119
  mock.restore()
112
120
  })
113
121
 
@@ -325,3 +333,66 @@ describe('SFX audio context lifecycle', () => {
325
333
  }
326
334
  })
327
335
  })
336
+
337
+ describe('public viewer SFX entry', () => {
338
+ test('importing the public entry creates no Howl instances', () => {
339
+ expect(howlsOnImport).toBe(0)
340
+ })
341
+
342
+ test('resolves the default and configured SFX origins at playback time', () => {
343
+ configureViewer({})
344
+ sharedSounds.playSound('menuClick')
345
+ expect(
346
+ instances.some((s) => s.src === 'https://editor.pascal.app/audios/sfx/menu_click.mp3'),
347
+ ).toBe(true)
348
+ const previous = [...instances]
349
+ configureViewer({ sfxBaseUrl: 'https://sounds.example/custom/' })
350
+ sharedSounds.playSound('switchOn')
351
+ expect(previous.every((s) => s.unloadCount === 1)).toBe(true)
352
+ expect(instances.find((s) => s.playCount === 1 && s.src?.endsWith('switch_on.mp3'))?.src).toBe(
353
+ 'https://sounds.example/custom/switch_on.mp3',
354
+ )
355
+ configureViewer({ sfxBaseUrl: '/audios/sfx' })
356
+ sharedSounds.playSound('switchOff')
357
+ expect(
358
+ instances.find((s) => s.playCount === 1 && s.src === '/audios/sfx/switch_off.mp3'),
359
+ ).toBeDefined()
360
+ })
361
+
362
+ test('interaction subscriptions share delivery and detach independently and idempotently', () => {
363
+ const first = sharedSounds.initInteractionSounds()
364
+ const second = sharedSounds.initInteractionSounds()
365
+ emitter.emit('interaction:sound', 'switch-on')
366
+ expect(instances.filter((s) => s.playCount === 1)).toHaveLength(1)
367
+ first()
368
+ first()
369
+ emitter.emit('interaction:sound', 'switch-off')
370
+ expect(instances.filter((s) => s.playCount === 1)).toHaveLength(2)
371
+ second()
372
+ emitter.emit('interaction:sound', 'garage')
373
+ expect(instances.filter((s) => s.playCount === 1)).toHaveLength(2)
374
+ })
375
+
376
+ test('interaction playback uses window and panel gains and the shared mute', () => {
377
+ const stop = sharedSounds.initInteractionSounds()
378
+ emitter.emit('interaction:sound', 'window-swing')
379
+ const swing = instances.find((s) => s.src === '/audios/sfx/door_swing.mp3')
380
+ expect(swing?.volumeCalls.find(([, id]) => id === 1)?.[0]).toBeCloseTo(0.6)
381
+ emitter.emit('interaction:sound', 'panel-open')
382
+ const panel = instances.find((s) => s.src === '/audios/sfx/shutters_open.mp3')
383
+ expect(panel?.volumeCalls.find(([, id]) => id === 1)?.[0]).toBeCloseTo(0.75)
384
+ useAudio.getState().setMuted(true)
385
+ emitter.emit('interaction:sound', 'switch-on')
386
+ expect(instances.find((s) => s.src === '/audios/sfx/switch_on.mp3')?.playCount).toBe(0)
387
+ stop()
388
+ })
389
+
390
+ test('viewer and editor UI cues reuse the same Howl cache and settings store', () => {
391
+ preloadSFX()
392
+ const count = instances.length
393
+ sharedSounds.playSound('menuHover')
394
+ playSFX('menuHover')
395
+ expect(instances).toHaveLength(count)
396
+ expect(instances.find((s) => s.src === '/audios/sfx/menu_hover.mp3')?.playCount).toBe(2)
397
+ })
398
+ })