@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,248 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNode,
5
- buildUnitReport,
6
- polygonInteriorPoint,
7
- sceneRegistry,
8
- useLocale,
9
- usePascalI18n,
10
- useScene,
11
- type ZoneNode,
12
- } from '@pascal-app/core'
13
- import { SelectionCallout, useViewer } from '@pascal-app/viewer'
14
- import { useCallback, useEffect, useMemo } from 'react'
15
- import { Vector3 } from 'three'
16
- import { useShallow } from 'zustand/react/shallow'
17
- import { resolveRoomAssemblyHeights } from '../../lib/room-assembly-overlay'
18
- import { roomFloorElevation } from '../../lib/room-handle-drag'
19
- import { describeZoneOutline, type RoomSelectionRecord } from '../../lib/room-selection'
20
- import {
21
- selectViewerRoom,
22
- type ViewerSource,
23
- viewerRooms,
24
- zoneLabel,
25
- } from '../../lib/viewer-selection'
26
- import { RoomAssemblyMesh } from '../editor/room-highlight'
27
- import {
28
- formatRoomArea,
29
- RoomLabelAnchor3D,
30
- type RoomLabelEntry,
31
- RoomPillChip,
32
- RoomPillDetail,
33
- RoomPillsLayer3D,
34
- roomLabelEntries,
35
- roomLabelsVisible,
36
- useFadePresence,
37
- } from '../editor/room-labels'
38
-
39
- /**
40
- * Rooms on every viewer surface, as the editor shows them: the editor's pills
41
- * on the current floor while nothing but the floor is selected, and the
42
- * editor's room highlight on the selected or hovered room (or a focused
43
- * unit's rooms). Zones draw no tinted volume here, and their meshes are not
44
- * mounted at all, so they never catch a click.
45
- */
46
- export function ViewerRooms({ source }: { source: ViewerSource }) {
47
- const i18n = usePascalI18n()
48
- useEffect(() => {
49
- const before = useViewer.getState().showZones
50
- useViewer.getState().setShowZones(false)
51
- return () => useViewer.getState().setShowZones(before)
52
- }, [])
53
- const { levelId, zoneId, selectedIds } = useViewer(
54
- useShallow((s) => ({
55
- levelId: s.selection.levelId,
56
- zoneId: s.selection.zoneId,
57
- selectedIds: s.selection.selectedIds,
58
- })),
59
- )
60
- const focusedUnitId = useViewer((s) => s.focusedUnitId)
61
- const hoveredId = useViewer((s) => s.hoveredId)
62
- const walkthrough = useViewer((s) => s.walkthroughMode)
63
- const { nodes } = source
64
-
65
- const pillsShown =
66
- !!levelId &&
67
- !walkthrough &&
68
- roomLabelsVisible({
69
- phase: 'building',
70
- mode: 'select',
71
- scopeIdle: true,
72
- room: null,
73
- zoneId,
74
- focusedUnitId,
75
- isCaptureMode: false,
76
- isThumbnailCapture: false,
77
- selectedTypes: selectedIds.map((id) => nodes[id]?.type),
78
- })
79
-
80
- // The layer stays mounted through its fade-out, like the editor's.
81
- const pillsPresent = useFadePresence(pillsShown)
82
-
83
- const rooms = useMemo(() => (levelId ? viewerRooms(levelId, nodes) : []), [levelId, nodes])
84
- const entries = useMemo(() => {
85
- if (!levelId) return []
86
- const zones = Object.values(nodes).filter(
87
- (node): node is ZoneNode =>
88
- node.type === 'zone' && node.parentId === levelId && node.visible !== false,
89
- )
90
- return roomLabelEntries(
91
- rooms.filter((room) => nodes[room.zoneId]?.visible !== false),
92
- zones,
93
- )
94
- }, [levelId, nodes, rooms])
95
-
96
- const highlighted = useMemo(() => {
97
- if (walkthrough) return []
98
- const ids = new Set<string>()
99
- if (zoneId) ids.add(zoneId)
100
- if (hoveredId && nodes[hoveredId]?.type === 'zone') ids.add(hoveredId)
101
- const unit = focusedUnitId ? nodes[focusedUnitId] : undefined
102
- if (unit?.type === 'unit') {
103
- for (const member of buildUnitReport(unit, nodes).members)
104
- if (member.levelId === levelId) ids.add(member.zoneId)
105
- }
106
- return [...ids].flatMap((id): RoomSelectionRecord[] => {
107
- const zone = nodes[id]
108
- if (zone?.type !== 'zone' || !zone.parentId) return []
109
- const detected = viewerRooms(zone.parentId, nodes).find((room) => room.zoneId === id)
110
- return [detected ?? describeZoneOutline(zone.parentId, zone)]
111
- })
112
- }, [focusedUnitId, hoveredId, levelId, nodes, walkthrough, zoneId])
113
-
114
- return (
115
- <>
116
- {pillsPresent && levelId ? (
117
- <RoomPillsLayer3D
118
- entries={entries}
119
- // A level of its own: nothing open or placed on one floor carries to the next.
120
- key={levelId}
121
- levelObject={() => source.objectFor(levelId)}
122
- renderDetail={(entry) => {
123
- const zone = nodes[entry.zoneId]
124
- if (zone?.type !== 'zone') return null
125
- return (
126
- <RoomPillDetail
127
- areaM2={entry.area}
128
- color={zone.color}
129
- name={zoneLabel(zone, i18n)}
130
- view="3d"
131
- zoneId={zone.id}
132
- />
133
- )
134
- }}
135
- renderLabel={(entry, anchors, elements) => {
136
- const zone = nodes[entry.zoneId]
137
- if (zone?.type !== 'zone') return null
138
- return (
139
- <RoomLabelAnchor3D
140
- anchors={anchors}
141
- entry={entry}
142
- floorY={roomFloorElevation(nodes as Record<string, AnyNode>, entry.zoneId)}
143
- >
144
- <RoomPillChip
145
- areaM2={entry.area}
146
- color={zone.color}
147
- elements={elements}
148
- name={zoneLabel(zone, i18n)}
149
- onClick={() => selectViewerRoom(zone.id, source)}
150
- onHover={() => useViewer.getState().setHoveredId(zone.id)}
151
- onLeave={() => {
152
- if (useViewer.getState().hoveredId === zone.id)
153
- useViewer.getState().setHoveredId(null)
154
- }}
155
- view="3d"
156
- zoneId={zone.id}
157
- />
158
- </RoomLabelAnchor3D>
159
- )
160
- }}
161
- shown={pillsShown}
162
- />
163
- ) : null}
164
- {highlighted.map((room) => (
165
- <ViewerRoomHighlight key={room.zoneId} room={room} source={source} />
166
- ))}
167
- {zoneId && levelId && !walkthrough && selectedIds.length === 0
168
- ? (() => {
169
- const entry = entries.find((candidate) => candidate.zoneId === zoneId)
170
- const zone = nodes[zoneId]
171
- const level = source.objectFor(levelId)
172
- return entry && zone?.type === 'zone' && level ? (
173
- <ViewerRoomCallout
174
- entry={entry}
175
- floorY={roomFloorElevation(nodes as Record<string, AnyNode>, zoneId)}
176
- key={zoneId}
177
- level={level}
178
- name={zoneLabel(zone, i18n)}
179
- />
180
- ) : null
181
- })()
182
- : null}
183
- </>
184
- )
185
- }
186
-
187
- /** The selected room's callout: its name and area at a point inside it, and the close button. */
188
- function ViewerRoomCallout({
189
- entry,
190
- floorY,
191
- level,
192
- name,
193
- }: {
194
- entry: RoomLabelEntry
195
- floorY: number
196
- level: NonNullable<ReturnType<ViewerSource['objectFor']>>
197
- name: string
198
- }) {
199
- const unit = useViewer((s) => s.unit)
200
- const locale = useLocale()
201
- const anchor = useMemo(() => {
202
- const [x, z] = polygonInteriorPoint({ polygon: entry.polygon, holes: entry.holes })
203
- return new Vector3(x, floorY + 0.05, z)
204
- }, [entry.polygon, entry.holes, floorY])
205
- const dismiss = useCallback(() => {
206
- const { selection, setSelection } = useViewer.getState()
207
- if (selection.zoneId === entry.zoneId) setSelection({ zoneId: null })
208
- }, [entry.zoneId])
209
- return (
210
- <SelectionCallout
211
- actions={[]}
212
- anchor={anchor}
213
- object={level}
214
- onDismiss={dismiss}
215
- subtitle={formatRoomArea(entry.area, unit, locale)}
216
- title={name}
217
- />
218
- )
219
- }
220
-
221
- const parametricObject = (id: string) => sceneRegistry.nodes.get(id)
222
-
223
- /** `ViewerRooms` over the parametric scene store. */
224
- export function ParametricViewerRooms() {
225
- const nodes = useScene((s) => s.nodes)
226
- const source = useMemo<ViewerSource>(() => ({ nodes, objectFor: parametricObject }), [nodes])
227
- return <ViewerRooms source={source} />
228
- }
229
-
230
- function ViewerRoomHighlight({
231
- room,
232
- source,
233
- }: {
234
- room: RoomSelectionRecord
235
- source: ViewerSource
236
- }) {
237
- const heights = useMemo(
238
- () => resolveRoomAssemblyHeights(room, source.nodes as Record<string, AnyNode>),
239
- [room, source.nodes],
240
- )
241
- return (
242
- <RoomAssemblyMesh
243
- heights={heights}
244
- levelObject={() => source.objectFor(room.key.levelId)}
245
- room={room}
246
- />
247
- )
248
- }
@@ -1,286 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type LevelNode,
5
- type PascalI18n,
6
- sceneRegistry,
7
- useLocale,
8
- usePascalI18n,
9
- useScene,
10
- } from '@pascal-app/core'
11
- import { markPerfAction, useViewer } from '@pascal-app/viewer'
12
- import { ArrowLeft, ChevronRight, Layers } from 'lucide-react'
13
- import Link from 'next/link'
14
- import { type ReactNode, useMemo } from 'react'
15
- import { localizedLevelName } from '../../i18n/level-name'
16
- import { cn } from '../../lib/utils'
17
- import {
18
- applyViewerClick,
19
- frameViewerNode,
20
- roomOfObject,
21
- selectViewerBuilding,
22
- selectViewerLevel,
23
- selectViewerRoom,
24
- selectViewerUnit,
25
- type ViewerCrumb,
26
- type ViewerSidebarSection,
27
- type ViewerSource,
28
- viewerBreadcrumb,
29
- viewerSidebarSections,
30
- } from '../../lib/viewer-selection'
31
- import { formatRoomArea } from '../editor/room-labels'
32
-
33
- export type ViewerSceneHeaderProps = {
34
- projectName?: string | null
35
- owner?: { username?: string | null } | null
36
- onBack?: () => void
37
- /** Fallback destination when no `onBack` handler is supplied. Must already be
38
- * sanitized by the caller. `null` shows no back arrow (an embedded viewer with
39
- * nowhere to go back to). */
40
- backHref?: string | null
41
- /** Extra row under the project info (e.g. like and duplicate actions). */
42
- stats?: ReactNode
43
- /** The scene the header navigates; the parametric scene store by default. */
44
- source?: ViewerSource
45
- }
46
-
47
- function sectionTitle(kind: ViewerSidebarSection['kind'], i18n: PascalI18n): string {
48
- if (kind === 'units') return i18n._({ id: 'viewer.header.units', message: 'Units' })
49
- if (kind === 'rooms') return i18n._({ id: 'viewer.header.rooms', message: 'Rooms' })
50
- return i18n._({ id: 'viewer.header.zones', message: 'Zones' })
51
- }
52
-
53
- const parametricObject = (id: string) => sceneRegistry.nodes.get(id)
54
-
55
- /**
56
- * The navigation of every viewer surface: the project card, the bar (building
57
- * › floor › room › element) and the floor list with, under the current floor,
58
- * its units, rooms and zones. A row or a crumb selects and frames; the canvas
59
- * selects in place (`lib/viewer-selection.ts`).
60
- */
61
- export const ViewerSceneHeader = ({
62
- projectName,
63
- owner,
64
- onBack,
65
- backHref = '/',
66
- stats,
67
- source: sourceProp,
68
- }: ViewerSceneHeaderProps) => {
69
- const i18n = usePascalI18n()
70
- const catalogVersion = i18n.getVersion()
71
- const locale = useLocale()
72
- const sceneNodes = useScene((s) => s.nodes)
73
- const source = useMemo<ViewerSource>(
74
- () => sourceProp ?? { nodes: sceneNodes, objectFor: parametricObject },
75
- [sourceProp, sceneNodes],
76
- )
77
- const { nodes } = source
78
- const selection = useViewer((s) => s.selection)
79
- const focusedUnitId = useViewer((s) => s.focusedUnitId)
80
- const unit = useViewer((s) => s.unit)
81
-
82
- const crumbs = useMemo(
83
- () =>
84
- viewerBreadcrumb(selection, focusedUnitId, nodes, i18n, (nodeId, levelId) =>
85
- roomOfObject(nodeId, levelId, source),
86
- ),
87
- [selection, focusedUnitId, nodes, source, i18n, locale, catalogVersion],
88
- )
89
- const buildingId = crumbs.find((crumb) => crumb.kind === 'building')?.id ?? null
90
- // Highest first so the list reads top-down like a building section.
91
- const levels = useMemo(() => {
92
- const building = buildingId ? nodes[buildingId] : undefined
93
- if (building?.type !== 'building') return []
94
- return building.children
95
- .map((id) => nodes[id])
96
- .filter((node): node is LevelNode => node?.type === 'level')
97
- .sort((a, b) => b.level - a.level)
98
- }, [buildingId, nodes])
99
- const sections = useMemo(
100
- () => (selection.levelId ? viewerSidebarSections(selection.levelId, nodes, i18n) : []),
101
- [selection.levelId, nodes, i18n, locale, catalogVersion],
102
- )
103
-
104
- const handleCrumb = (crumb: ViewerCrumb) => {
105
- switch (crumb.kind) {
106
- case 'building':
107
- return selectViewerBuilding(source)
108
- case 'level':
109
- return selectViewerLevel(crumb.id, source)
110
- case 'unit':
111
- return selectViewerUnit(crumb.id, source)
112
- case 'room':
113
- return selectViewerRoom(crumb.id, source)
114
- case 'node':
115
- applyViewerClick({ nodeId: crumb.id, point: null }, nodes)
116
- return frameViewerNode(crumb.id, source)
117
- }
118
- }
119
-
120
- return (
121
- <div className="dark absolute top-4 left-4 z-20 flex max-h-[calc(100%-7rem)] flex-col gap-3 text-foreground">
122
- <div className="corner-smooth pointer-events-auto flex min-w-[200px] max-w-[min(28rem,calc(100vw-2rem))] flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 shadow-elevation-4 backdrop-blur-xl transition-colors duration-200 ease-out">
123
- {/* Project info + back */}
124
- <div className="flex items-center gap-3 px-3 py-2.5">
125
- {onBack ? (
126
- <button
127
- aria-label={i18n._({ id: 'viewer.header.back', message: 'Back' })}
128
- className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-white/10"
129
- onClick={onBack}
130
- type="button"
131
- >
132
- <ArrowLeft className="h-4 w-4 text-muted-foreground" />
133
- </button>
134
- ) : backHref === null ? null : (
135
- <Link
136
- aria-label={i18n._({ id: 'viewer.header.back', message: 'Back' })}
137
- className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-white/10"
138
- href={backHref}
139
- prefetch={false}
140
- >
141
- <ArrowLeft className="h-4 w-4 text-muted-foreground" />
142
- </Link>
143
- )}
144
- <div className="min-w-0 flex-1">
145
- <div className="truncate font-medium text-foreground text-sm">
146
- {projectName || i18n._({ id: 'viewer.header.untitled', message: 'Untitled' })}
147
- </div>
148
- {owner?.username && (
149
- <Link
150
- className="text-muted-foreground text-xs transition-colors hover:text-foreground"
151
- href={`/u/${owner.username}`}
152
- >
153
- @{owner.username}
154
- </Link>
155
- )}
156
- </div>
157
- </div>
158
-
159
- {stats && (
160
- <div className="flex items-center gap-1 border-border/40 border-t px-3 py-2">{stats}</div>
161
- )}
162
-
163
- {crumbs.length > 0 && (
164
- <nav
165
- aria-label={i18n._({ id: 'viewer.header.selection', message: 'Selection' })}
166
- className="flex flex-wrap items-center gap-x-1.5 gap-y-1 border-border/40 border-t px-3 py-2 text-xs"
167
- data-testid="viewer-breadcrumb"
168
- >
169
- {crumbs.map((crumb, index) => {
170
- const isLast = index === crumbs.length - 1
171
- return (
172
- <span
173
- className="flex min-w-0 items-center gap-1.5"
174
- key={`${crumb.kind}:${crumb.id}`}
175
- >
176
- {index > 0 && <ChevronRight className="h-3 w-3 text-muted-foreground/50" />}
177
- <button
178
- className={cn(
179
- 'truncate transition-colors',
180
- isLast
181
- ? 'font-medium text-foreground'
182
- : 'text-muted-foreground hover:text-foreground',
183
- )}
184
- data-crumb={crumb.kind}
185
- onClick={() => handleCrumb(crumb)}
186
- type="button"
187
- >
188
- {crumb.label}
189
- </button>
190
- </span>
191
- )
192
- })}
193
- </nav>
194
- )}
195
- </div>
196
-
197
- {levels.length > 0 && (
198
- <div className="corner-smooth pointer-events-auto flex min-h-0 w-56 flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 py-1 shadow-elevation-4 backdrop-blur-xl transition-colors duration-200 ease-out">
199
- <span className="px-3 py-2 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
200
- {i18n._({ id: 'viewer.header.levels', message: 'Levels' })}
201
- </span>
202
- <div className="flex min-h-0 flex-col overflow-y-auto">
203
- {levels.map((lvl) => {
204
- const isSelected = lvl.id === selection.levelId
205
- return (
206
- <div key={lvl.id}>
207
- <button
208
- className={cn(
209
- 'group/row relative flex h-8 w-full cursor-pointer select-none items-center border-border/50 border-r border-r-transparent border-b px-3 text-sm transition-all duration-200',
210
- isSelected
211
- ? 'border-r-3 border-r-white bg-accent/50 text-foreground'
212
- : 'text-muted-foreground hover:bg-accent/30 hover:text-foreground',
213
- )}
214
- onClick={() => {
215
- if (!isSelected) markPerfAction('level-switch', lvl.id)
216
- selectViewerLevel(lvl.id, source)
217
- }}
218
- type="button"
219
- >
220
- <div className="flex min-w-0 flex-1 items-center gap-2">
221
- <span
222
- className={cn(
223
- 'flex h-4 w-4 shrink-0 items-center justify-center transition-all duration-200',
224
- !isSelected && 'opacity-60 grayscale',
225
- )}
226
- >
227
- <Layers className="h-3.5 w-3.5" />
228
- </span>
229
- <div className="min-w-0 flex-1 truncate text-left">
230
- {localizedLevelName(lvl, i18n, locale)}
231
- </div>
232
- </div>
233
- </button>
234
- {isSelected &&
235
- sections.map((section) => (
236
- <div
237
- className="border-border/50 border-b pb-1"
238
- data-viewer-section={section.kind}
239
- key={section.kind}
240
- >
241
- <span className="block px-3 pt-2 pb-1 pl-9 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
242
- {sectionTitle(section.kind, i18n)}
243
- </span>
244
- {section.rows.map((row) => {
245
- const active =
246
- section.kind === 'units'
247
- ? row.id === focusedUnitId
248
- : row.id === selection.zoneId
249
- return (
250
- <button
251
- className={cn(
252
- 'flex h-7 w-full cursor-pointer select-none items-center gap-2 pr-3 pl-9 text-left text-sm transition-colors',
253
- active
254
- ? 'bg-accent/50 text-foreground'
255
- : 'text-muted-foreground hover:bg-accent/30 hover:text-foreground',
256
- )}
257
- key={row.id}
258
- onClick={() =>
259
- section.kind === 'units'
260
- ? selectViewerUnit(row.id, source)
261
- : selectViewerRoom(row.id, source)
262
- }
263
- type="button"
264
- >
265
- <span
266
- className="h-2 w-2 shrink-0 rounded-full"
267
- style={{ backgroundColor: row.color }}
268
- />
269
- <span className="min-w-0 flex-1 truncate">{row.label}</span>
270
- <span className="shrink-0 font-mono text-[11px] text-muted-foreground tabular-nums">
271
- {formatRoomArea(row.areaM2, unit, locale)}
272
- </span>
273
- </button>
274
- )
275
- })}
276
- </div>
277
- ))}
278
- </div>
279
- )
280
- })}
281
- </div>
282
- </div>
283
- )}
284
- </div>
285
- )
286
- }
@@ -1,124 +0,0 @@
1
- 'use client'
2
-
3
- import { type AnyNodeId, emitter, type NodeEvent, sceneRegistry, useScene } from '@pascal-app/core'
4
- import { useViewer } from '@pascal-app/viewer'
5
- import { useThree } from '@react-three/fiber'
6
- import { useEffect } from 'react'
7
- import { selectionModifiersFromEvent } from '../../lib/selection-routing'
8
- import {
9
- applyViewerClick,
10
- applyViewerHover,
11
- viewerPickFromNodeEvent,
12
- } from '../../lib/viewer-selection'
13
-
14
- /**
15
- * Selection for a parametric viewer surface (the published viewer's fallback,
16
- * its embed, the editor's Preview): free-form and room-first, through the same
17
- * rules as the baked viewer. Mount it with `<Viewer selectionManager="custom">`.
18
- */
19
- export function ViewerSelectionManager() {
20
- const gl = useThree((state) => state.gl)
21
-
22
- useEffect(() => {
23
- let clickHandled = false
24
- let hoveredFrom: string | null = null
25
- const pendingClicks = new Set<number>()
26
-
27
- const onMove = (event: NodeEvent) => {
28
- if (useViewer.getState().walkthroughMode) return
29
- const pick = viewerPickFromNodeEvent(event, useScene.getState().nodes)
30
- if (!pick) return
31
- event.stopPropagation()
32
- hoveredFrom = event.node.id
33
- applyViewerHover(pick, useScene.getState().nodes)
34
- }
35
- const onLeave = (event: NodeEvent) => {
36
- if (event.node.id !== hoveredFrom) return
37
- hoveredFrom = null
38
- useViewer.getState().setHoveredId(null)
39
- }
40
- const onClick = (event: NodeEvent) => {
41
- if (useViewer.getState().walkthroughMode) return
42
- const pick = viewerPickFromNodeEvent(event, useScene.getState().nodes)
43
- if (!pick) return
44
- event.stopPropagation()
45
- clickHandled = true
46
- applyViewerClick(
47
- pick,
48
- useScene.getState().nodes,
49
- selectionModifiersFromEvent(event.nativeEvent),
50
- )
51
- }
52
- // A click on nothing selectable clears the room and the element.
53
- const onCanvasClick = (event: MouseEvent) => {
54
- const viewer = useViewer.getState()
55
- if (event.button !== 0 || viewer.walkthroughMode) return
56
- if (viewer.cameraDragging || viewer.inputDragging) return
57
- const frame = requestAnimationFrame(() => {
58
- pendingClicks.delete(frame)
59
- if (useViewer.getState().walkthroughMode) return
60
- if (clickHandled) {
61
- clickHandled = false
62
- return
63
- }
64
- applyViewerClick(null, useScene.getState().nodes)
65
- })
66
- pendingClicks.add(frame)
67
- }
68
-
69
- emitter.on('node:move', onMove)
70
- emitter.on('node:enter', onMove)
71
- emitter.on('node:leave', onLeave)
72
- emitter.on('node:click', onClick)
73
- const canvas = gl.domElement
74
- canvas.addEventListener('click', onCanvasClick)
75
- return () => {
76
- emitter.off('node:move', onMove)
77
- emitter.off('node:enter', onMove)
78
- emitter.off('node:leave', onLeave)
79
- emitter.off('node:click', onClick)
80
- canvas.removeEventListener('click', onCanvasClick)
81
- for (const frame of pendingClicks) cancelAnimationFrame(frame)
82
- useViewer.getState().setHoveredId(null)
83
- }
84
- }, [gl])
85
-
86
- return <ViewerOutlineSync />
87
- }
88
-
89
- /** The selected elements and the hovered one, outlined (rooms highlight instead). */
90
- function ViewerOutlineSync() {
91
- const selectedIds = useViewer((s) => s.selection.selectedIds)
92
- const externalSelectedIds = useViewer((s) => s.externalSelectedIds)
93
- const hoveredId = useViewer((s) => s.hoveredId)
94
- const outliner = useViewer((s) => s.outliner)
95
- const geometryRevision = useViewer((s) => s.geometryRevision)
96
- const walkthrough = useViewer((s) => s.walkthroughMode)
97
- const nodes = useScene((s) => s.nodes)
98
-
99
- useEffect(() => {
100
- void geometryRevision
101
- outliner.selectedObjects.length = 0
102
- outliner.hoveredObjects.length = 0
103
- if (walkthrough) return
104
- for (const id of new Set([...selectedIds, ...externalSelectedIds])) {
105
- const object = sceneRegistry.nodes.get(id)
106
- if (object) outliner.selectedObjects.push(object)
107
- }
108
- const hovered = hoveredId ? nodes[hoveredId as AnyNodeId] : undefined
109
- if (hovered && hovered.type !== 'zone') {
110
- const object = sceneRegistry.nodes.get(hovered.id)
111
- if (object) outliner.hoveredObjects.push(object)
112
- }
113
- }, [selectedIds, externalSelectedIds, hoveredId, outliner, nodes, geometryRevision, walkthrough])
114
-
115
- useEffect(
116
- () => () => {
117
- outliner.selectedObjects.length = 0
118
- outliner.hoveredObjects.length = 0
119
- },
120
- [outliner],
121
- )
122
-
123
- return null
124
- }
@@ -1,38 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import {
3
- normalizeViewerStageModes,
4
- resolveMobileViewerStageMode,
5
- resolveViewerStageMode,
6
- viewerStageIncludes3D,
7
- } from './viewer-stage-modes'
8
-
9
- describe('viewer stage modes', () => {
10
- test('supports every ordered combination without duplicates', () => {
11
- expect(normalizeViewerStageModes(['split', '3d', 'split'])).toEqual(['3d', 'split'])
12
- expect(normalizeViewerStageModes(['2d'])).toEqual(['2d'])
13
- expect(normalizeViewerStageModes([])).toEqual(['3d'])
14
- })
15
-
16
- test('reuses normalized combinations across inline prop arrays', () => {
17
- expect(normalizeViewerStageModes(['split', '3d'])).toBe(
18
- normalizeViewerStageModes(['3d', 'split']),
19
- )
20
- })
21
-
22
- test('falls back to the first enabled mode', () => {
23
- expect(resolveViewerStageMode('split', ['3d', '2d'])).toBe('3d')
24
- expect(resolveViewerStageMode(undefined, ['2d', 'split'])).toBe('2d')
25
- })
26
-
27
- test('uses an enabled single-pane mode on mobile but preserves split-only embeds', () => {
28
- expect(resolveMobileViewerStageMode('split', ['3d', 'split'])).toBe('3d')
29
- expect(resolveMobileViewerStageMode('split', ['2d', 'split'])).toBe('2d')
30
- expect(resolveMobileViewerStageMode('split', ['split'])).toBe('split')
31
- })
32
-
33
- test('does not require a GPU canvas for a 2D-only embed', () => {
34
- expect(viewerStageIncludes3D(['2d'])).toBe(false)
35
- expect(viewerStageIncludes3D(['split'])).toBe(true)
36
- expect(viewerStageIncludes3D(['3d', '2d'])).toBe(true)
37
- })
38
- })