@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,265 +0,0 @@
1
- import {
2
- type AnyNode,
3
- type AnyNodeId,
4
- area,
5
- getWallArcData,
6
- getWallBaseElevationForNodes,
7
- getWallCurveFrameAt,
8
- getWallFaceOffsets,
9
- getWallTop,
10
- resolveCeilingHeight,
11
- roomDrawnFloor,
12
- type WallNode,
13
- } from '@pascal-app/core'
14
- import { ShapeUtils, Vector2 } from 'three'
15
- import type { RoomSelectionGeometry, RoomSelectionRecord } from './room-selection'
16
-
17
- type Point = readonly [number, number]
18
- type Ring = readonly Point[]
19
-
20
- /** Everything the assembly depends on besides the room's topology geometry. */
21
- export type RoomAssemblyHeights = {
22
- /** Level-local Y of the room's plate top (the level plane without a plate). */
23
- floorY: number
24
- /** Level-local Y of each boundary wall's top. */
25
- wallTops: ReadonlyMap<string, number>
26
- ceiling: { polygon: Ring; holes: readonly Ring[]; y: number } | null
27
- }
28
-
29
- export type RoomWallFacePortion = { wallId: string; face: 'a' | 'b'; from: Point; to: Point }
30
-
31
- /** Flat level-local position buffers; the renderer only wraps them. */
32
- export type RoomAssembly = {
33
- floor: Float32Array
34
- surfaces: Float32Array
35
- outline: Float32Array
36
- faces: RoomWallFacePortion[]
37
- }
38
-
39
- const ON_FACE = 1e-3
40
- const FLOOR_LIFT = 0.005
41
- const CEILING_DROP = 0.02
42
- const CURVE_SAMPLES = 24
43
- const MIN_PIECE_AREA = 0.01
44
-
45
- /**
46
- * Curved boundaries leave hairline slivers between the sampled room face and
47
- * the sampled wall footprint; they would add stray face runs.
48
- */
49
- function clearPieces(geometry: RoomSelectionGeometry) {
50
- return geometry.clearPolygon.filter((piece) => area([piece]) >= MIN_PIECE_AREA)
51
- }
52
-
53
- export function resolveRoomAssemblyHeights(
54
- room: RoomSelectionRecord,
55
- nodes: Readonly<Record<string, AnyNode>>,
56
- ): RoomAssemblyHeights {
57
- const sceneNodes = nodes as Record<AnyNodeId, AnyNode>
58
- const { levelId } = room.key
59
- const zone = nodes[room.zoneId]
60
- let floorY = 0
61
- // A drawn slab floors the room even with its floor switched off.
62
- if (zone?.type === 'zone' && (zone.hasFloor !== false || roomDrawnFloor(nodes, zone.id))) {
63
- const level = nodes[levelId]
64
- const plate =
65
- level?.type === 'level'
66
- ? level.children
67
- .map((id) => nodes[id])
68
- .find((node) => node?.type === 'slab' && node.zoneIds?.includes(room.zoneId))
69
- : undefined
70
- const drawn = plate ? null : roomDrawnFloor(nodes, room.zoneId)
71
- const linked =
72
- plate ?? (drawn ? nodes[drawn.slabId] : room.slabId ? nodes[room.slabId] : undefined)
73
- floorY = (linked?.type === 'slab' ? linked.elevation : zone.floor?.elevation) ?? 0
74
- }
75
- const wallTops = new Map<string, number>()
76
- for (const id of room.boundaryWallIds) {
77
- const wall = nodes[id]
78
- if (wall?.type !== 'wall') continue
79
- const base = getWallBaseElevationForNodes(wall, sceneNodes)
80
- wallTops.set(id, getWallTop(wall, levelId, sceneNodes, base))
81
- }
82
- const ceilingNode = room.ceilingId ? nodes[room.ceilingId] : undefined
83
- const ceiling =
84
- ceilingNode?.type === 'ceiling'
85
- ? {
86
- polygon: ceilingNode.polygon,
87
- holes: ceilingNode.holes,
88
- y: resolveCeilingHeight(ceilingNode, sceneNodes),
89
- }
90
- : null
91
- return { floorY, wallTops, ceiling }
92
- }
93
-
94
- function heightsKey({ floorY, wallTops, ceiling }: RoomAssemblyHeights) {
95
- const rings = ceiling ? [ceiling.polygon, ...ceiling.holes].map((ring) => ring.join(' ')) : []
96
- return [floorY, ...wallTops, ceiling?.y, ...rings].join('|')
97
- }
98
-
99
- const cache = new WeakMap<RoomSelectionGeometry, { key: string; assembly: RoomAssembly }>()
100
-
101
- /**
102
- * The room geometry object is reused for as long as the level topology
103
- * revision stands, so it keys the cache; heights and the ceiling outline are
104
- * compared by value. Hover changes and pointer moves only ever hit.
105
- */
106
- export function getRoomAssembly(
107
- geometry: RoomSelectionGeometry,
108
- heights: RoomAssemblyHeights,
109
- ): RoomAssembly {
110
- const key = heightsKey(heights)
111
- const cached = cache.get(geometry)
112
- if (cached?.key === key) return cached.assembly
113
- const assembly = buildRoomAssembly(geometry, heights)
114
- cache.set(geometry, { key, assembly })
115
- return assembly
116
- }
117
-
118
- function faceTest(wall: WallNode, face: 'a' | 'b') {
119
- const offset = getWallFaceOffsets(wall)[face]
120
- const arc = getWallArcData(wall)
121
- if (arc) {
122
- const radius = arc.radius - arc.direction * offset
123
- return ([x, z]: Point) =>
124
- Math.abs(Math.hypot(x - arc.center.x, z - arc.center.y) - radius) < ON_FACE
125
- }
126
- const dx = wall.end[0] - wall.start[0]
127
- const dz = wall.end[1] - wall.start[1]
128
- const length = Math.hypot(dx, dz)
129
- if (length < 1e-9) return () => false
130
- return ([x, z]: Point) =>
131
- Math.abs(((x - wall.start[0]) * -dz + (z - wall.start[1]) * dx) / length - offset) < ON_FACE
132
- }
133
-
134
- function* ringEdges(ring: Ring) {
135
- for (let i = 0; i < ring.length; i++) yield [ring[i]!, ring[(i + 1) % ring.length]!] as const
136
- }
137
-
138
- function overlap(p: Point, q: Point, a: Point, b: Point): [Point, Point] | null {
139
- const ex = b[0] - a[0]
140
- const ez = b[1] - a[1]
141
- const lengthSq = ex * ex + ez * ez
142
- if (lengthSq < 1e-12) return null
143
- const length = Math.sqrt(lengthSq)
144
- const offLine = ([x, z]: Point) => Math.abs((x - a[0]) * ez - (z - a[1]) * ex) / length
145
- if (offLine(p) > ON_FACE || offLine(q) > ON_FACE) return null
146
- const up = ((p[0] - a[0]) * ex + (p[1] - a[1]) * ez) / lengthSq
147
- const uq = ((q[0] - a[0]) * ex + (q[1] - a[1]) * ez) / lengthSq
148
- const lo = Math.max(0, Math.min(up, uq))
149
- const hi = Math.min(1, Math.max(up, uq))
150
- if ((hi - lo) * length < 1e-4) return null
151
- return [
152
- [a[0] + ex * lo, a[1] + ez * lo],
153
- [a[0] + ex * hi, a[1] + ez * hi],
154
- ]
155
- }
156
-
157
- /**
158
- * The room-facing part of each boundary wall face. The clear polygon's
159
- * boundary is exactly the mitered faces the room sees, clipped by T-stems
160
- * and separators, so its edges are matched against each wall's footprint
161
- * face edges; that also keeps collinear walls apart.
162
- */
163
- export function roomWallFaces(geometry: RoomSelectionGeometry): RoomWallFacePortion[] {
164
- const rings = clearPieces(geometry).flatMap(({ outer, holes }) => [outer, ...holes])
165
- const faces: RoomWallFacePortion[] = []
166
- const seen = new Set<string>()
167
- for (const span of geometry.spans) {
168
- if (span.kind !== 'wall') continue
169
- const wallId = span.boundaryId
170
- const wall = geometry.context.walls.get(wallId)
171
- if (!wall) continue
172
- if (!rings.length) {
173
- faces.push(...sampledFace(wall, span.face, span.t0, span.t1))
174
- continue
175
- }
176
- const key = `${wallId}:${span.face}`
177
- const footprint = geometry.context.wallFootprints.get(wallId)
178
- if (seen.has(key) || !footprint) continue
179
- seen.add(key)
180
- const onFace = faceTest(wall, span.face)
181
- const faceEdges = [...ringEdges(footprint)].filter(([a, b]) => onFace(a) && onFace(b))
182
- for (const ring of rings) {
183
- for (const [p, q] of ringEdges(ring)) {
184
- if (!(onFace(p) && onFace(q))) continue
185
- for (const [a, b] of faceEdges) {
186
- const portion = overlap(p, q, a, b)
187
- if (portion) faces.push({ wallId, face: span.face, from: portion[0], to: portion[1] })
188
- }
189
- }
190
- }
191
- }
192
- return faces
193
- }
194
-
195
- // Only used when the clear polygon could not be computed for the room.
196
- function sampledFace(wall: WallNode, face: 'a' | 'b', t0: number, t1: number) {
197
- const offset = getWallFaceOffsets(wall)[face]
198
- const steps = getWallArcData(wall) ? CURVE_SAMPLES : 1
199
- const points = Array.from({ length: steps + 1 }, (_, i): Point => {
200
- const { point, normal } = getWallCurveFrameAt(wall, t0 + ((t1 - t0) * i) / steps)
201
- return [point.x + normal.x * offset, point.y + normal.y * offset]
202
- })
203
- return points
204
- .slice(1)
205
- .map((to, i): RoomWallFacePortion => ({ wallId: wall.id, face, from: points[i]!, to }))
206
- }
207
-
208
- function pushTriangles(target: number[], outer: Ring, holes: readonly Ring[], y: number) {
209
- const contour = outer.map(([x, z]) => new Vector2(x, z))
210
- const holePoints = holes.map((ring) => ring.map(([x, z]) => new Vector2(x, z)))
211
- const points = [...contour, ...holePoints.flat()]
212
- for (const triangle of ShapeUtils.triangulateShape(contour, holePoints)) {
213
- for (const index of triangle) target.push(points[index]!.x, y, points[index]!.y)
214
- }
215
- }
216
-
217
- function pushRing(target: number[], ring: Ring, y: number) {
218
- for (const [[ax, az], [bx, bz]] of ringEdges(ring)) target.push(ax, y, az, bx, y, bz)
219
- }
220
-
221
- export function buildRoomAssembly(
222
- geometry: RoomSelectionGeometry,
223
- heights: RoomAssemblyHeights,
224
- ): RoomAssembly {
225
- const floor: number[] = []
226
- const surfaces: number[] = []
227
- const outline: number[] = []
228
- const floorY = heights.floorY + FLOOR_LIFT
229
- for (const { outer, holes } of clearPieces(geometry)) {
230
- pushTriangles(floor, outer, holes, floorY)
231
- for (const ring of [outer, ...holes]) pushRing(outline, ring, floorY)
232
- }
233
- const faces = roomWallFaces(geometry)
234
- const ends = new Map<string, { point: Point; top: number; count: number }>()
235
- for (const { wallId, from, to } of faces) {
236
- const top = heights.wallTops.get(wallId)
237
- if (top === undefined || top <= floorY) continue
238
- const [ax, az] = from
239
- const [bx, bz] = to
240
- surfaces.push(ax, floorY, az, bx, floorY, bz, bx, top, bz)
241
- surfaces.push(ax, floorY, az, bx, top, bz, ax, top, az)
242
- outline.push(ax, top, az, bx, top, bz)
243
- for (const point of [from, to]) {
244
- const key = `${wallId}:${point[0].toFixed(4)},${point[1].toFixed(4)}`
245
- const end = ends.get(key)
246
- if (end) end.count++
247
- else ends.set(key, { point, top, count: 1 })
248
- }
249
- }
250
- // Verticals only where a face run starts or stops, not between curve chords.
251
- for (const { point, top, count } of ends.values()) {
252
- if (count === 1) outline.push(point[0], floorY, point[1], point[0], top, point[1])
253
- }
254
- if (heights.ceiling) {
255
- const { polygon, holes, y } = heights.ceiling
256
- pushTriangles(surfaces, polygon, holes, y - CEILING_DROP)
257
- for (const ring of [polygon, ...holes]) pushRing(outline, ring, y - CEILING_DROP)
258
- }
259
- return {
260
- floor: new Float32Array(floor),
261
- surfaces: new Float32Array(surfaces),
262
- outline: new Float32Array(outline),
263
- faces,
264
- }
265
- }
@@ -1,417 +0,0 @@
1
- import {
2
- type AnyNode,
3
- area,
4
- type BoundaryNode,
5
- type BoundarySpan,
6
- containsPoint,
7
- createRoomTopologyIndex,
8
- getWallArcData,
9
- getWallFaceOffsets,
10
- type MultiPolygon,
11
- roomClearPolygon,
12
- type TopologyRoom,
13
- type WallNode,
14
- type ZoneNode,
15
- } from '@pascal-app/core'
16
- import type { SelectionModifierKeys } from './selection-routing'
17
- import { polygonMatchesZoneFootprint } from './zone-content'
18
-
19
- export type RoomKey = { levelId: string; zoneId: string }
20
-
21
- export function sameRoom(a: RoomKey | null, b: RoomKey | null): boolean {
22
- return a === b || (!!a && !!b && a.levelId === b.levelId && a.zoneId === b.zoneId)
23
- }
24
-
25
- export function shouldSelectRoom(
26
- selected: RoomKey | null,
27
- hit: RoomKey | null,
28
- modifiers: SelectionModifierKeys,
29
- ): boolean {
30
- return (
31
- !!hit &&
32
- !sameRoom(selected, hit) &&
33
- !(modifiers.alt || modifiers.shift || modifiers.ctrl || modifiers.meta)
34
- )
35
- }
36
-
37
- export type RoomSelectionGeometry = TopologyRoom & {
38
- key: RoomKey
39
- boundaryWallIds: string[]
40
- clearPolygon: ReturnType<typeof roomClearPolygon>
41
- area: number
42
- hitPolygon: MultiPolygon
43
- /** A stacked open-below room (`floor.support: 'open'`) inside its host room. */
44
- mezzanine: { hostZoneId: string } | null
45
- }
46
-
47
- export type RoomSelectionRecord = RoomSelectionGeometry & {
48
- geometry: RoomSelectionGeometry
49
- name: string
50
- zoneId: string
51
- slabId: string | null
52
- slabName: string | null
53
- ceilingId: string | null
54
- ceilingName: string | null
55
- }
56
-
57
- type Nodes = Record<string, AnyNode>
58
-
59
- function describeRoomGeometry(
60
- levelId: string,
61
- room: TopologyRoom,
62
- zone: ZoneNode,
63
- ): RoomSelectionGeometry {
64
- const clearPolygon = roomClearPolygon(room)
65
- return {
66
- ...room,
67
- key: { levelId, zoneId: zone.id },
68
- boundaryWallIds: [
69
- ...new Set(room.spans.filter((span) => span.kind === 'wall').map((span) => span.boundaryId)),
70
- ],
71
- clearPolygon,
72
- area: area([{ outer: zone.polygon, holes: zone.holes }]),
73
- hitPolygon: [{ outer: room.polygon, holes: room.holes }],
74
- mezzanine: null,
75
- }
76
- }
77
-
78
- export function isMezzanineZone(node: AnyNode | undefined): node is ZoneNode {
79
- return node?.type === 'zone' && node.floor?.support === 'open'
80
- }
81
-
82
- /**
83
- * A mezzanine as a selectable room. It has no walls of its own: its outline is
84
- * the zone's polygon, its floor the zone's own plate, and it bounds nothing.
85
- */
86
- function describeMezzanineGeometry(
87
- levelId: string,
88
- zone: ZoneNode,
89
- context: TopologyRoom['context'],
90
- ): RoomSelectionGeometry {
91
- const polygon = zone.polygon.map(([x, z]) => [x, z] as [number, number])
92
- const holes = (zone.holes ?? []).map((ring) => ring.map(([x, z]) => [x, z] as [number, number]))
93
- const outline = [{ outer: polygon, holes }]
94
- return {
95
- id: `mezzanine:${zone.id}`,
96
- polygon,
97
- holes,
98
- spans: [],
99
- context,
100
- key: { levelId, zoneId: zone.id },
101
- boundaryWallIds: [],
102
- clearPolygon: outline,
103
- area: area(outline),
104
- hitPolygon: outline,
105
- mezzanine: { hostZoneId: zone.hostZoneId ?? '' },
106
- }
107
- }
108
-
109
- const NO_WALLS: TopologyRoom['context'] = {
110
- revision: -1,
111
- walls: new Map(),
112
- wallFootprints: new Map(),
113
- }
114
-
115
- /**
116
- * A zone that bounds no room (a drawn area), outlined the way a room is: its
117
- * polygon as the floor, no walls of its own.
118
- */
119
- export function describeZoneOutline(levelId: string, zone: ZoneNode): RoomSelectionRecord {
120
- const geometry: RoomSelectionGeometry = {
121
- ...describeMezzanineGeometry(levelId, zone, NO_WALLS),
122
- id: `zone:${zone.id}`,
123
- mezzanine: null,
124
- }
125
- return {
126
- ...geometry,
127
- geometry,
128
- name: zone.name || 'Zone',
129
- zoneId: zone.id,
130
- slabId: null,
131
- slabName: null,
132
- ceilingId: null,
133
- ceilingName: null,
134
- }
135
- }
136
-
137
- function describeRoom(
138
- geometry: RoomSelectionGeometry,
139
- candidates: readonly AnyNode[],
140
- ): RoomSelectionRecord {
141
- const { clearPolygon } = geometry
142
- const matches = (node: AnyNode) =>
143
- (node.type === 'zone' || node.type === 'slab' || node.type === 'ceiling') &&
144
- (polygonMatchesZoneFootprint(node.polygon, geometry.polygon) ||
145
- clearPolygon.some(({ outer }) => polygonMatchesZoneFootprint(node.polygon, outer)))
146
- const slab = geometry.mezzanine
147
- ? candidates.find(
148
- (node) =>
149
- node.type === 'slab' &&
150
- node.support === 'open' &&
151
- !!node.zoneIds?.includes(geometry.key.zoneId),
152
- )
153
- : candidates.find(
154
- (node) =>
155
- node.type === 'slab' && node.support !== 'open' && node.autoFromWalls && matches(node),
156
- )
157
- const ceiling = candidates.find(
158
- (node) => node.type === 'ceiling' && node.zoneId === geometry.key.zoneId,
159
- )
160
- const zone = candidates.find(
161
- (node): node is ZoneNode => node.type === 'zone' && node.id === geometry.key.zoneId,
162
- )!
163
- return {
164
- ...geometry,
165
- geometry,
166
- name: zone.name || 'Room',
167
- area: area([{ outer: zone.polygon, holes: zone.holes }]),
168
- zoneId: zone.id,
169
- slabId: slab?.id ?? null,
170
- slabName: slab ? slab.name || 'Slab' : null,
171
- ceilingId: ceiling?.id ?? null,
172
- ceilingName: ceiling ? ceiling.name || 'Ceiling' : null,
173
- }
174
- }
175
-
176
- type RoomHitIndex = Pick<
177
- ReturnType<typeof createRoomTopologyIndex>,
178
- 'roomAtPoint' | 'roomForWallHit' | 'spansForWall'
179
- > & {
180
- /** The selectable room a zone is (a mezzanine's own plate or ceiling points at it). */
181
- roomForZone?: (levelId: string, zoneId: string) => TopologyRoom | null
182
- }
183
- const wallFrames = new WeakMap<WallNode, ReturnType<typeof wallHitFrame>>()
184
-
185
- function wallHitFrame(wall: WallNode) {
186
- const dx = wall.end[0] - wall.start[0]
187
- const dz = wall.end[1] - wall.start[1]
188
- const length = Math.hypot(dx, dz)
189
- const offsets = getWallFaceOffsets(wall)
190
- return {
191
- dx,
192
- dz,
193
- lengthSq: length * length,
194
- nx: -dz / length,
195
- nz: dx / length,
196
- arc: getWallArcData(wall),
197
- center: (offsets.a + offsets.b) / 2,
198
- }
199
- }
200
-
201
- export function resolveRoomHit<T extends RoomHitIndex>(
202
- index: T,
203
- levelId: string,
204
- node: AnyNode | null,
205
- point: [number, number],
206
- view?: '2d' | '3d',
207
- ): ReturnType<T['roomAtPoint']>
208
- export function resolveRoomHit(
209
- index: RoomHitIndex,
210
- levelId: string,
211
- node: AnyNode | null,
212
- point: [number, number],
213
- view: '2d' | '3d' = '2d',
214
- ): TopologyRoom | null {
215
- // A mezzanine's plate (railing included) and ceiling name their own room; the
216
- // host floor under it and every other surface resolve by the point, which
217
- // never lands in a mezzanine.
218
- if (node?.type === 'slab' && node.support === 'open')
219
- return index.roomForZone?.(levelId, node.zoneIds?.[0] ?? '') ?? null
220
- if (node?.type === 'ceiling' && node.zoneId) {
221
- const own = index.roomForZone?.(levelId, node.zoneId)
222
- if (own) return own
223
- }
224
- if (!node || node.type === 'slab' || node.type === 'ceiling')
225
- return index.roomAtPoint(levelId, point)
226
- if (node.type !== 'wall') return null
227
- let frame = wallFrames.get(node)
228
- if (!frame) {
229
- frame = wallHitFrame(node)
230
- wallFrames.set(node, frame)
231
- }
232
- const { dx, dz, lengthSq, arc } = frame
233
- if (!lengthSq) return null
234
- const station = Math.max(
235
- 0,
236
- Math.min(1, ((point[0] - node.start[0]) * dx + (point[1] - node.start[1]) * dz) / lengthSq),
237
- )
238
- let x = node.start[0] + station * dx
239
- let z = node.start[1] + station * dz
240
- let nx = frame.nx
241
- let nz = frame.nz
242
- if (arc) {
243
- const angle = Math.atan2(point[1] - arc.center.y, point[0] - arc.center.x)
244
- const delta = Math.atan2(Math.sin(angle - arc.startAngle), Math.cos(angle - arc.startAngle))
245
- const t = Math.max(0, Math.min(1, delta / arc.delta))
246
- const theta = arc.startAngle + arc.delta * t
247
- const cos = Math.cos(theta)
248
- const sin = Math.sin(theta)
249
- x = arc.center.x + cos * arc.radius
250
- z = arc.center.y + sin * arc.radius
251
- nx = -cos * arc.direction
252
- nz = -sin * arc.direction
253
- }
254
- const localZ = (point[0] - x) * nx + (point[1] - z) * nz
255
- // Justified bodies can lie entirely on one side of the reference line.
256
- const center = view === '3d' ? frame.center : 0
257
- const face = localZ >= center ? 'a' : 'b'
258
- // Room first: a face with no room behind it (the exterior) still resolves
259
- // to the room on the other face, or to the wall's only room; only walls
260
- // bounding no room reach the wall itself.
261
- return (
262
- index.roomForWallHit(levelId, node.id, face, station) ??
263
- index.roomForWallHit(levelId, node.id, face === 'a' ? 'b' : 'a', station) ??
264
- soleWallRoom(index, levelId, node.id)
265
- )
266
- }
267
-
268
- function soleWallRoom(index: RoomHitIndex, levelId: string, wallId: string) {
269
- let sole: BoundarySpan | null = null
270
- for (const span of index.spansForWall(levelId, wallId)) {
271
- if (sole && sole.roomId !== span.roomId) return null
272
- sole ??= span
273
- }
274
- return sole && index.roomForWallHit(levelId, wallId, sole.face, (sole.t0 + sole.t1) / 2)
275
- }
276
-
277
- function isBoundary(node: AnyNode): node is BoundaryNode {
278
- return node.type === 'wall' || node.type === 'separator'
279
- }
280
-
281
- function sameBoundary(a: BoundaryNode, b: AnyNode | undefined) {
282
- return (
283
- !!b &&
284
- isBoundary(b) &&
285
- a.type === b.type &&
286
- a.parentId === b.parentId &&
287
- a.start[0] === b.start[0] &&
288
- a.start[1] === b.start[1] &&
289
- a.end[0] === b.end[0] &&
290
- a.end[1] === b.end[1] &&
291
- (a.type === 'separator' ||
292
- (b.type === 'wall' &&
293
- a.curveOffset === b.curveOffset &&
294
- a.thickness === b.thickness &&
295
- a.justification === b.justification))
296
- )
297
- }
298
-
299
- export class RoomSelectionIndex {
300
- readonly topology = createRoomTopologyIndex()
301
- private nodes: Nodes | null = null
302
- private records: RoomSelectionRecord[] = []
303
- private geometry: RoomSelectionGeometry[] = []
304
- private candidates: AnyNode[] = []
305
- private revision = -1
306
- private readonly roomsById = new Map<string, RoomSelectionGeometry>()
307
-
308
- roomAtPoint(_levelId: string, point: [number, number]): RoomSelectionGeometry | null {
309
- for (const room of this.geometry)
310
- if (!room.mezzanine && containsPoint(room.hitPolygon, point)) return room
311
- return null
312
- }
313
-
314
- roomForZone(_levelId: string, zoneId: string): RoomSelectionGeometry | null {
315
- return this.geometry.find((room) => room.key.zoneId === zoneId) ?? null
316
- }
317
-
318
- roomForWallHit(
319
- levelId: string,
320
- wallId: string,
321
- face: 'a' | 'b',
322
- t: number,
323
- ): RoomSelectionGeometry | null {
324
- const spans = this.topology.getLevelTopology(levelId)?.spansByBoundary.get(wallId)
325
- if (!spans) return null
326
- for (const span of spans) {
327
- if (span.face === face && t >= span.t0 && (t < span.t1 || (t === 1 && span.t1 === 1)))
328
- return this.roomsById.get(span.roomId) ?? null
329
- }
330
- return null
331
- }
332
-
333
- spansForWall(levelId: string, wallId: string) {
334
- return this.topology.spansForWall(levelId, wallId)
335
- }
336
-
337
- constructor(readonly levelId: string) {}
338
-
339
- update(nodes: Nodes): RoomSelectionRecord[] {
340
- if (nodes === this.nodes) return this.records
341
- if (!this.nodes) this.topology.rebuildLevel(this.levelId, nodes)
342
- else {
343
- const changed = new Set<string>()
344
- for (const snapshot of [this.nodes, nodes]) {
345
- for (const node of Object.values(snapshot)) {
346
- if (
347
- isBoundary(node) &&
348
- node.parentId === this.levelId &&
349
- !sameBoundary(node, snapshot === nodes ? this.nodes[node.id] : nodes[node.id])
350
- )
351
- changed.add(node.id)
352
- }
353
- }
354
- if (changed.size) this.topology.applyWallDelta(this.levelId, changed, this.nodes, nodes)
355
- }
356
- this.nodes = nodes
357
- const topology = this.topology.getLevelTopology(this.levelId)
358
- const geometryChanged = topology?.revision !== this.revision
359
- const candidates = Object.values(nodes).filter(
360
- (node) =>
361
- node.parentId === this.levelId &&
362
- (node.type === 'slab' || node.type === 'ceiling' || node.type === 'zone'),
363
- )
364
- const candidatesChanged =
365
- candidates.length !== this.candidates.length ||
366
- candidates.some((node, i) => node !== this.candidates[i])
367
- if (!geometryChanged && !candidatesChanged) return this.records
368
- if (geometryChanged || candidatesChanged) {
369
- this.revision = topology?.revision ?? -1
370
- const previous = new Map(this.geometry.map((room) => [room.key.zoneId, room]))
371
- const matched = new Set<string>()
372
- const zones = candidates
373
- .filter(
374
- (node): node is ZoneNode =>
375
- node.type === 'zone' &&
376
- node.spaceRole === 'room' &&
377
- node.enclosureStatus !== 'open' &&
378
- !isMezzanineZone(node),
379
- )
380
- .sort((a, b) => a.id.localeCompare(b.id))
381
- this.geometry = (topology?.rooms ?? []).flatMap((room) => {
382
- const boundaryIds = [...new Set(room.spans.map((span) => span.boundaryId))].sort().join('|')
383
- const available = zones.filter((zone) => !matched.has(zone.id))
384
- const zone =
385
- available.find(
386
- (node) =>
387
- [...node.boundaryWallIds, ...node.boundarySeparatorIds].sort().join('|') ===
388
- boundaryIds,
389
- ) ??
390
- available.find(
391
- (node) =>
392
- !!node.seed && containsPoint([{ outer: room.polygon, holes: room.holes }], node.seed),
393
- )
394
- if (!zone) return []
395
- matched.add(zone.id)
396
- return [
397
- (!geometryChanged && previous.get(zone.id)) ||
398
- describeRoomGeometry(this.levelId, room, zone),
399
- ]
400
- })
401
- const context = topology?.rooms[0]?.context
402
- if (context)
403
- for (const zone of candidates) {
404
- if (!isMezzanineZone(zone) || zone.spaceRole !== 'room') continue
405
- const hosted =
406
- zone.hostZoneId && this.geometry.some((room) => room.key.zoneId === zone.hostZoneId)
407
- if (!hosted) continue
408
- this.geometry.push(describeMezzanineGeometry(this.levelId, zone, context))
409
- }
410
- this.roomsById.clear()
411
- for (const room of this.geometry) this.roomsById.set(room.id, room)
412
- }
413
- this.candidates = candidates
414
- this.records = this.geometry.map((room) => describeRoom(room, candidates))
415
- return this.records
416
- }
417
- }