@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
@@ -10,6 +10,7 @@ import {
10
10
  type ChimneyMaterialRole,
11
11
  DEFAULT_BRUSH_SETTINGS,
12
12
  type DormerSurfaceMaterialRole,
13
+ isMoveLocked,
13
14
  type LevelNode,
14
15
  nodeRegistry,
15
16
  type RoofSurfaceMaterialRole,
@@ -20,6 +21,15 @@ import {
20
21
  type WallSurfaceSide,
21
22
  } from '@pascal-app/core'
22
23
  import { useViewer } from '@pascal-app/viewer'
24
+ import {
25
+ cameraPoseStore,
26
+ type NavigationSyncPose,
27
+ type NavigationSyncPoseInput,
28
+ publishNavigationSyncPose,
29
+ type RoomKey,
30
+ sameRoom,
31
+ subscribeNavigationSyncPose,
32
+ } from '@pascal-app/viewer/ui'
23
33
  import { create } from 'zustand'
24
34
  import { persist } from 'zustand/middleware'
25
35
  import {
@@ -55,7 +65,6 @@ import {
55
65
  type PaintHoverInfo,
56
66
  type PaintScope,
57
67
  } from '../lib/paint-scope'
58
- import { type RoomKey, sameRoom } from '../lib/room-selection'
59
68
  import {
60
69
  cycleSnappingModeIn,
61
70
  defaultSnappingModeFor,
@@ -65,8 +74,6 @@ import {
65
74
  snapContextOf,
66
75
  snappingModesFor,
67
76
  } from '../lib/snapping-mode'
68
- import { cameraPoseStore } from './camera-pose-store'
69
- import { publishNavigationSyncPoseToStore } from './navigation-sync-pose-store'
70
77
  import useInteractionScope from './use-interaction-scope'
71
78
  import {
72
79
  clampViewPaneRatio,
@@ -232,18 +239,6 @@ export type CatalogCategory =
232
239
  export type FloorplanSelectionTool = 'click' | 'marquee'
233
240
  export type GridSnapStep = 0.5 | 0.25 | 0.1 | 0.05
234
241
 
235
- export type NavigationSyncSource = '2d' | '3d'
236
-
237
- export type NavigationSyncPose = {
238
- source: NavigationSyncSource
239
- revision: number
240
- target: [number, number, number]
241
- azimuth: number
242
- viewWidth: number
243
- }
244
-
245
- export type NavigationSyncPoseInput = Omit<NavigationSyncPose, 'revision'>
246
-
247
242
  // Combined tool type. Known literals keep autocomplete; the `(string & {})`
248
243
  // arm lets plugin-contributed tool ids (e.g. `'trees:tree'`) typecheck without
249
244
  // the host enumerating every plugin kind. The runtime dispatch is already
@@ -489,12 +484,13 @@ type EditorState = {
489
484
  setViewPinned: (viewId: string, pinned: boolean) => void
490
485
  isFloorplanHovered: boolean
491
486
  setFloorplanHovered: (hovered: boolean) => void
487
+ /** Mirror of viewer/ui's navigation sync store, kept for plugins that read it here. */
488
+ navigationSyncPose: NavigationSyncPose | null
489
+ publishNavigationSyncPose: (pose: NavigationSyncPoseInput) => void
492
490
  // Toggleable DWV riser-diagram (plumbing isometric) overlay.
493
491
  isRiserOpen: boolean
494
492
  setRiserOpen: (open: boolean) => void
495
493
  toggleRiserOpen: () => void
496
- navigationSyncPose: NavigationSyncPose | null
497
- publishNavigationSyncPose: (pose: NavigationSyncPoseInput) => void
498
494
  floorplanSelectionTool: FloorplanSelectionTool
499
495
  setFloorplanSelectionTool: (tool: FloorplanSelectionTool) => void
500
496
  gridSnapStep: GridSnapStep
@@ -1256,6 +1252,10 @@ const useEditor = create<EditorState>()(
1256
1252
  set({ placementDragMode: false })
1257
1253
  return
1258
1254
  }
1255
+ if (isMoveLocked(node)) {
1256
+ set({ placementDragMode: false })
1257
+ return
1258
+ }
1259
1259
  const targetNode = node
1260
1260
  const isNew = Boolean((targetNode as { metadata?: { isNew?: boolean } }).metadata?.isNew)
1261
1261
  if (isNew) {
@@ -1533,18 +1533,11 @@ const useEditor = create<EditorState>()(
1533
1533
  set((state) => ({ pinnedViews: { ...state.pinnedViews, [viewId]: pinned } })),
1534
1534
  isFloorplanHovered: false,
1535
1535
  setFloorplanHovered: (hovered) => set({ isFloorplanHovered: hovered }),
1536
+ navigationSyncPose: null,
1537
+ publishNavigationSyncPose,
1536
1538
  isRiserOpen: false,
1537
1539
  setRiserOpen: (open) => set({ isRiserOpen: open }),
1538
1540
  toggleRiserOpen: () => set((state) => ({ isRiserOpen: !state.isRiserOpen })),
1539
- navigationSyncPose: null,
1540
- publishNavigationSyncPose: (pose) => {
1541
- const navigationSyncPose = {
1542
- ...pose,
1543
- revision: (get().navigationSyncPose?.revision ?? 0) + 1,
1544
- }
1545
- publishNavigationSyncPoseToStore(navigationSyncPose)
1546
- set({ navigationSyncPose })
1547
- },
1548
1541
  floorplanSelectionTool: 'click' as FloorplanSelectionTool,
1549
1542
  setFloorplanSelectionTool: (tool) => set({ floorplanSelectionTool: tool }),
1550
1543
  gridSnapStep: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
@@ -1858,4 +1851,7 @@ usePaintRegionMode.subscribe((state) => {
1858
1851
  if (useEditor.getState().paintEraser !== paintEraser) useEditor.setState({ paintEraser })
1859
1852
  })
1860
1853
 
1854
+ // Published plugins read the 2D/3D navigation pose from the editor store; the shared store owns it.
1855
+ subscribeNavigationSyncPose((navigationSyncPose) => useEditor.setState({ navigationSyncPose }))
1856
+
1861
1857
  export default useEditor
@@ -1,5 +1,10 @@
1
1
  'use client'
2
2
 
3
+ import {
4
+ DEFAULT_FLOORPLAN_WALL_DIMENSION_FACES,
5
+ type FloorplanWallDimensionFaces,
6
+ normalizeFloorplanWallDimensionFaces,
7
+ } from '@pascal-app/core'
3
8
  import { create } from 'zustand'
4
9
  import { persist } from 'zustand/middleware'
5
10
  import {
@@ -8,11 +13,6 @@ import {
8
13
  type FloorplanAnnotationVisibility,
9
14
  normalizeFloorplanAnnotationVisibility,
10
15
  } from '../lib/floorplan/annotation-visibility'
11
- import {
12
- DEFAULT_FLOORPLAN_WALL_DIMENSION_FACES,
13
- type FloorplanWallDimensionFaces,
14
- normalizeFloorplanWallDimensionFaces,
15
- } from '../lib/floorplan/floorplan-extension'
16
16
 
17
17
  type PersistedFloorplanAnnotationState = {
18
18
  visibility: FloorplanAnnotationVisibility
@@ -0,0 +1,165 @@
1
+ import { beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ isPlacementTypingKey,
4
+ PLACEMENT_TYPING_DISTANCE_OFFSET,
5
+ PLACEMENT_TYPING_LENGTH_ANGLE,
6
+ routePlacementTypingKey,
7
+ usePlacementTyping,
8
+ } from './use-placement-typing'
9
+
10
+ const key = (
11
+ k: string,
12
+ mods: Partial<Pick<KeyboardEvent, 'metaKey' | 'ctrlKey' | 'altKey'>> = {},
13
+ ) => ({
14
+ key: k,
15
+ metaKey: false,
16
+ ctrlKey: false,
17
+ altKey: false,
18
+ ...mods,
19
+ })
20
+
21
+ const beginTwoFields = (activeField: 0 | 1) => {
22
+ usePlacementTyping.getState().begin({ labels: PLACEMENT_TYPING_DISTANCE_OFFSET, activeField })
23
+ return true
24
+ }
25
+
26
+ describe('usePlacementTyping', () => {
27
+ beforeEach(() => {
28
+ usePlacementTyping.getState().clear()
29
+ })
30
+
31
+ test('keeps two independent field buffers and toggles the active field', () => {
32
+ const typing = usePlacementTyping.getState()
33
+ typing.begin({ labels: PLACEMENT_TYPING_DISTANCE_OFFSET, fieldDefaults: ['1.2', '0'] })
34
+ typing.append('2')
35
+ typing.toggleField()
36
+ typing.append('-')
37
+ typing.append('4')
38
+
39
+ expect(usePlacementTyping.getState().fields).toEqual(['2', '-4'])
40
+ expect(usePlacementTyping.getState().fieldDefaults).toEqual(['1.2', '0'])
41
+ expect(usePlacementTyping.getState().activeField).toBe(1)
42
+ })
43
+
44
+ test('backspace only changes the active field', () => {
45
+ const typing = usePlacementTyping.getState()
46
+ typing.begin({ labels: PLACEMENT_TYPING_DISTANCE_OFFSET })
47
+ typing.append('25')
48
+ typing.toggleField()
49
+ typing.append('3')
50
+ typing.backspace()
51
+
52
+ expect(usePlacementTyping.getState().fields).toEqual(['25', ''])
53
+ })
54
+
55
+ test('publishes projection and an enter request without clearing input', () => {
56
+ const typing = usePlacementTyping.getState()
57
+ typing.begin({ labels: PLACEMENT_TYPING_DISTANCE_OFFSET })
58
+ typing.append('2')
59
+ typing.setProjectedPosition([2, 0, 1])
60
+ typing.requestCommit()
61
+
62
+ expect(usePlacementTyping.getState().projectedPosition).toEqual([2, 0, 1])
63
+ expect(usePlacementTyping.getState().submitRevision).toBe(1)
64
+ expect(usePlacementTyping.getState().isActive).toBe(true)
65
+ })
66
+
67
+ test('clear ends the session and forgets its labels', () => {
68
+ usePlacementTyping.getState().begin({ labels: PLACEMENT_TYPING_LENGTH_ANGLE })
69
+ usePlacementTyping.getState().append('3')
70
+ usePlacementTyping.getState().clear()
71
+ const state = usePlacementTyping.getState()
72
+ expect(state.isActive).toBe(false)
73
+ expect(state.fields).toEqual(['', ''])
74
+ expect(state.labels).toEqual([])
75
+ })
76
+ })
77
+
78
+ describe('routePlacementTypingKey', () => {
79
+ beforeEach(() => {
80
+ usePlacementTyping.getState().clear()
81
+ })
82
+
83
+ test('a digit or a leading dot opens a session on the first field', () => {
84
+ expect(routePlacementTypingKey(key('5'), { begin: beginTwoFields })).toBe('edit')
85
+ expect(usePlacementTyping.getState().fields).toEqual(['5', ''])
86
+ usePlacementTyping.getState().clear()
87
+ expect(routePlacementTypingKey(key('.'), { begin: beginTwoFields })).toBe('edit')
88
+ expect(usePlacementTyping.getState().fields).toEqual(['.', ''])
89
+ })
90
+
91
+ test('letters, signs and separators leave tool shortcuts alone until a session opens', () => {
92
+ for (const k of ['c', 'm', 't', 'f', '-', ' ', ',']) {
93
+ expect(routePlacementTypingKey(key(k), { begin: beginTwoFields })).toBeNull()
94
+ }
95
+ expect(usePlacementTyping.getState().isActive).toBe(false)
96
+ })
97
+
98
+ test('no session opens when the tool refuses or offers no begin', () => {
99
+ expect(routePlacementTypingKey(key('5'), { begin: () => false })).toBeNull()
100
+ expect(routePlacementTypingKey(key('5'))).toBeNull()
101
+ expect(usePlacementTyping.getState().isActive).toBe(false)
102
+ })
103
+
104
+ test('Tab opens a session on the second field only when the tool asks for it', () => {
105
+ expect(routePlacementTypingKey(key('Tab'), { begin: beginTwoFields })).toBeNull()
106
+ expect(routePlacementTypingKey(key('Tab'), { begin: beginTwoFields, tabBegins: true })).toBe(
107
+ 'edit',
108
+ )
109
+ expect(usePlacementTyping.getState().activeField).toBe(1)
110
+ })
111
+
112
+ test('inside a session: unit letters extend, Tab and comma switch, Enter commits', () => {
113
+ routePlacementTypingKey(key('1'), { begin: beginTwoFields })
114
+ for (const k of ['8', '0', 'c', 'm']) routePlacementTypingKey(key(k))
115
+ expect(routePlacementTypingKey(key('Tab'))).toBe('edit')
116
+ routePlacementTypingKey(key('-'))
117
+ routePlacementTypingKey(key('2'))
118
+ expect(routePlacementTypingKey(key(','))).toBe('edit')
119
+ expect(usePlacementTyping.getState().fields).toEqual(['180cm', '-2'])
120
+ expect(usePlacementTyping.getState().activeField).toBe(0)
121
+ expect(routePlacementTypingKey(key('Enter'))).toBe('commit')
122
+ expect(usePlacementTyping.getState().isActive).toBe(true)
123
+ })
124
+
125
+ test('Backspace edits, Delete empties the active field', () => {
126
+ routePlacementTypingKey(key('4'), { begin: beginTwoFields })
127
+ routePlacementTypingKey(key('2'))
128
+ routePlacementTypingKey(key('Backspace'))
129
+ expect(usePlacementTyping.getState().fields[0]).toBe('4')
130
+ routePlacementTypingKey(key('Delete'))
131
+ expect(usePlacementTyping.getState().fields[0]).toBe('')
132
+ expect(usePlacementTyping.getState().isActive).toBe(true)
133
+ })
134
+
135
+ test('the first Escape only ends the session', () => {
136
+ routePlacementTypingKey(key('3'), { begin: beginTwoFields })
137
+ expect(routePlacementTypingKey(key('Escape'))).toBe('clear')
138
+ expect(usePlacementTyping.getState().isActive).toBe(false)
139
+ expect(routePlacementTypingKey(key('Escape'))).toBeNull()
140
+ })
141
+
142
+ test('chords are never consumed', () => {
143
+ expect(
144
+ routePlacementTypingKey(key('5', { metaKey: true }), { begin: beginTwoFields }),
145
+ ).toBeNull()
146
+ routePlacementTypingKey(key('5'), { begin: beginTwoFields })
147
+ expect(routePlacementTypingKey(key('z', { ctrlKey: true }))).toBeNull()
148
+ expect(routePlacementTypingKey(key('Enter', { altKey: true }))).toBeNull()
149
+ expect(usePlacementTyping.getState().fields[0]).toBe('5')
150
+ })
151
+ })
152
+
153
+ describe('isPlacementTypingKey', () => {
154
+ test('accepts the measurement grammar', () => {
155
+ for (const k of ['0', '9', 'm', 'c', '.', "'", '"', '-', '+', ' ', '/']) {
156
+ expect(isPlacementTypingKey(k)).toBe(true)
157
+ }
158
+ })
159
+
160
+ test('rejects control keys and the field separator', () => {
161
+ for (const k of ['Enter', 'Escape', 'Tab', 'Shift', '', 'F1', ',']) {
162
+ expect(isPlacementTypingKey(k)).toBe(false)
163
+ }
164
+ })
165
+ })
@@ -0,0 +1,186 @@
1
+ import type { MessageDescriptor } from '@pascal-app/core'
2
+ import { create } from 'zustand'
3
+
4
+ /**
5
+ * Typed coordinate entry shared by the placement tools: a drafted wall's
6
+ * length/angle and a cabinet's distance/offset along its wall. One session at
7
+ * a time; the active tool owns what the two fields mean and reads them on
8
+ * every change, the HUD (`PlacementCoordinateInput`) only displays them.
9
+ */
10
+ export type PlacementTypingField = 0 | 1
11
+ export type PlacementTypingValues = [string, string]
12
+ export type PlacementTypingProjection = [number, number, number]
13
+
14
+ export const PLACEMENT_TYPING_DISTANCE_OFFSET: readonly MessageDescriptor[] = [
15
+ /* i18n */ { id: 'editor.placementTyping.distance', message: 'Distance' },
16
+ /* i18n */ { id: 'editor.placementTyping.offset', message: 'Offset' },
17
+ ]
18
+
19
+ export const PLACEMENT_TYPING_LENGTH_ANGLE: readonly MessageDescriptor[] = [
20
+ /* i18n */ { id: 'editor.placementTyping.length', message: 'Length' },
21
+ /* i18n */ { id: 'editor.placementTyping.angle', message: 'Angle' },
22
+ ]
23
+
24
+ export type PlacementTypingSession = {
25
+ /** What each field means, in field order; the HUD shows one box per label. */
26
+ labels: readonly MessageDescriptor[]
27
+ fieldDefaults?: PlacementTypingValues
28
+ activeField?: PlacementTypingField
29
+ }
30
+
31
+ export type PlacementTypingState = {
32
+ isActive: boolean
33
+ labels: readonly MessageDescriptor[]
34
+ fields: PlacementTypingValues
35
+ /** What an empty field stands for, shown as its placeholder. */
36
+ fieldDefaults: PlacementTypingValues
37
+ activeField: PlacementTypingField
38
+ projectedPosition: PlacementTypingProjection | null
39
+ submitRevision: number
40
+ begin: (session: PlacementTypingSession) => void
41
+ append: (value: string) => void
42
+ backspace: () => void
43
+ setField: (field: PlacementTypingField, value: string) => void
44
+ setActiveField: (field: PlacementTypingField) => void
45
+ toggleField: () => void
46
+ setProjectedPosition: (position: PlacementTypingProjection | null) => void
47
+ requestCommit: () => void
48
+ clear: () => void
49
+ }
50
+
51
+ const EMPTY_VALUES: PlacementTypingValues = ['', '']
52
+
53
+ export const usePlacementTyping = create<PlacementTypingState>((set) => ({
54
+ isActive: false,
55
+ labels: [],
56
+ fields: [...EMPTY_VALUES],
57
+ fieldDefaults: [...EMPTY_VALUES],
58
+ activeField: 0,
59
+ projectedPosition: null,
60
+ submitRevision: 0,
61
+ begin: ({ labels, fieldDefaults = EMPTY_VALUES, activeField = 0 }) =>
62
+ set({
63
+ isActive: true,
64
+ labels,
65
+ fields: [...EMPTY_VALUES],
66
+ fieldDefaults: [...fieldDefaults],
67
+ activeField,
68
+ projectedPosition: null,
69
+ }),
70
+ append: (value) =>
71
+ set((state) => {
72
+ const fields: PlacementTypingValues = [...state.fields]
73
+ fields[state.activeField] += value
74
+ return { isActive: true, fields }
75
+ }),
76
+ backspace: () =>
77
+ set((state) => {
78
+ const fields: PlacementTypingValues = [...state.fields]
79
+ fields[state.activeField] = fields[state.activeField].slice(0, -1)
80
+ return { fields }
81
+ }),
82
+ setField: (field, value) =>
83
+ set((state) => {
84
+ const fields: PlacementTypingValues = [...state.fields]
85
+ fields[field] = value
86
+ return { isActive: true, fields }
87
+ }),
88
+ setActiveField: (activeField) => set({ activeField }),
89
+ toggleField: () => set((state) => ({ activeField: state.activeField === 0 ? 1 : 0 })),
90
+ setProjectedPosition: (projectedPosition) => set({ projectedPosition }),
91
+ requestCommit: () =>
92
+ set((state: PlacementTypingState) => ({ submitRevision: state.submitRevision + 1 })),
93
+ clear: () =>
94
+ set({
95
+ isActive: false,
96
+ labels: [],
97
+ fields: [...EMPTY_VALUES],
98
+ fieldDefaults: [...EMPTY_VALUES],
99
+ activeField: 0,
100
+ projectedPosition: null,
101
+ }),
102
+ }))
103
+
104
+ /** Keys that extend a field: digits, unit letters, separators, signs, fractions. */
105
+ export function isPlacementTypingKey(key: string): boolean {
106
+ return key.length === 1 && /^[0-9a-zA-Z.'"+\-/ ]$/.test(key)
107
+ }
108
+
109
+ /**
110
+ * Keys that may open a session. Letters only ever continue a field, so a
111
+ * tool's single-letter shortcuts keep working until a number is typed.
112
+ */
113
+ export function isPlacementTypingStartKey(key: string): boolean {
114
+ return key.length === 1 && /^[0-9.]$/.test(key)
115
+ }
116
+
117
+ export type PlacementTypingKeyOutcome = 'edit' | 'commit' | 'clear'
118
+
119
+ export type PlacementTypingKeyOptions = {
120
+ /**
121
+ * Opens a session for a start key (or Tab, with `tabBegins`) when none is
122
+ * active. Returns false when the tool cannot take typed entry right now.
123
+ */
124
+ begin?: (activeField: PlacementTypingField) => boolean
125
+ /** Tab with no session opens one on the second field. */
126
+ tabBegins?: boolean
127
+ }
128
+
129
+ /**
130
+ * The one keyboard convention for typed entry. Tab or comma switches field,
131
+ * Enter asks the tool to commit, Escape clears the session (the tool's next
132
+ * Escape cancels its draft), Backspace edits and Delete empties the active
133
+ * field. Chords with Cmd/Ctrl/Alt are never consumed. Returns null when the
134
+ * key is not part of typed entry; the caller prevents default and stops
135
+ * propagation for every other outcome.
136
+ */
137
+ export function routePlacementTypingKey(
138
+ event: Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey' | 'altKey'>,
139
+ options: PlacementTypingKeyOptions = {},
140
+ ): PlacementTypingKeyOutcome | null {
141
+ if (event.metaKey || event.ctrlKey || event.altKey) return null
142
+ const typing = usePlacementTyping.getState()
143
+ const { key } = event
144
+ if (!typing.isActive) {
145
+ if (isPlacementTypingStartKey(key)) {
146
+ if (!options.begin?.(0)) return null
147
+ usePlacementTyping.getState().append(key)
148
+ return 'edit'
149
+ }
150
+ if (key === 'Tab' && options.tabBegins && options.begin?.(1)) return 'edit'
151
+ return null
152
+ }
153
+ if (key === 'Tab' || key === ',') {
154
+ typing.toggleField()
155
+ return 'edit'
156
+ }
157
+ if (key === 'Enter') return 'commit'
158
+ if (key === 'Escape') {
159
+ typing.clear()
160
+ return 'clear'
161
+ }
162
+ if (key === 'Backspace') {
163
+ typing.backspace()
164
+ return 'edit'
165
+ }
166
+ if (key === 'Delete') {
167
+ typing.setField(typing.activeField, '')
168
+ return 'edit'
169
+ }
170
+ if (isPlacementTypingKey(key)) {
171
+ typing.append(key)
172
+ return 'edit'
173
+ }
174
+ return null
175
+ }
176
+
177
+ /** Parse options for a typed length: a bare number is read in the display unit. */
178
+ export function placementTypingLengthOptions(
179
+ unit: 'metric' | 'imperial',
180
+ metricNotation: 'meters' | 'millimeters',
181
+ ) {
182
+ return {
183
+ bareUnit: unit === 'imperial' ? 'in' : metricNotation === 'millimeters' ? 'mm' : 'm',
184
+ system: unit === 'imperial' ? ('imperial' as const) : ('metric' as const),
185
+ }
186
+ }
@@ -1,10 +1,9 @@
1
1
  import { type AnyNodeId, useScene } from '@pascal-app/core'
2
- import { useViewer } from '@pascal-app/viewer'
2
+ import { isTypingTarget, useViewer } from '@pascal-app/viewer'
3
3
  import { create } from 'zustand'
4
4
  import { hasLiveGestures } from '../lib/gesture-lifecycle'
5
5
  import { removeWallRegion } from '../lib/paint-regions'
6
6
  import { sfxEmitter } from '../lib/sfx-bus'
7
- import { isTypingTarget } from '../lib/typing-target'
8
7
  import useDeleteConfirmation from './use-delete-confirmation'
9
8
  import useInteractionScope from './use-interaction-scope'
10
9
 
@@ -1,108 +0,0 @@
1
- import type { Polygon } from '@pascal-app/core'
2
- import {
3
- type PillCandidate,
4
- placePills,
5
- type RoomHit,
6
- type RoomLabelEntry,
7
- roomAt,
8
- } from '../room-labels'
9
-
10
- // One room-label placement pass and one pointer pick, for a 900 × 600 px house of 31, 60 and 120
11
- // rooms (every third an L), printed as JSON medians in ms. Run with `bun run <this file>`.
12
- let seed = 11
13
- const random = () => {
14
- seed = (seed * 1_103_515_245 + 12_345) % 2 ** 31
15
- return seed / 2 ** 31
16
- }
17
-
18
- function house(count: number) {
19
- const columns = Math.ceil(Math.sqrt(count * 1.5))
20
- const rows = Math.ceil(count / columns)
21
- const width = 900 / columns
22
- const depth = 600 / rows
23
- return Array.from({ length: count }, (_, i) => {
24
- const left = 270 + (i % columns) * width
25
- const top = 150 + Math.floor(i / columns) * depth
26
- const right = left + width
27
- const bottom = top + depth
28
- const outer: [number, number][] =
29
- i % 3 === 0
30
- ? [
31
- [left, top],
32
- [right, top],
33
- [right, top + depth / 2],
34
- [left + width / 2, top + depth / 2],
35
- [left + width / 2, bottom],
36
- [left, bottom],
37
- ]
38
- : [
39
- [left, top],
40
- [right, top],
41
- [right, bottom],
42
- [left, bottom],
43
- ]
44
- const outline: Polygon = { outer, holes: [] }
45
- const pill = 60 + random() * 110
46
- const candidate: PillCandidate = {
47
- id: `room_${i}`,
48
- x: left + width / 4,
49
- y: top + depth / 4,
50
- size: [pill, 24, pill * 0.6],
51
- area: width * depth * (i % 3 === 0 ? 0.75 : 1),
52
- outline,
53
- }
54
- const entry: RoomLabelEntry = {
55
- zoneId: candidate.id,
56
- polygon: outer,
57
- holes: [],
58
- area: candidate.area,
59
- }
60
- return { candidate, entry }
61
- })
62
- }
63
-
64
- const median = (samples: number[]) => samples.sort((a, b) => a - b)[samples.length >> 1]!
65
-
66
- const pane = { width: 1440, height: 900 }
67
- const results: Record<string, unknown> = {}
68
- for (const count of [31, 60, 120]) {
69
- const rooms = house(count)
70
- const candidates = rooms.map((room) => room.candidate)
71
- const entries = rooms.map((room) => room.entry)
72
- for (let i = 0; i < 20; i++) placePills(candidates, pane)
73
- const cold: number[] = []
74
- const warm: number[] = []
75
- let previous = placePills(candidates, pane)
76
- for (let i = 0; i < 100; i++) {
77
- let started = performance.now()
78
- placePills(candidates, pane)
79
- cold.push(performance.now() - started)
80
- started = performance.now()
81
- previous = placePills(candidates, pane, previous)
82
- warm.push(performance.now() - started)
83
- }
84
- const points = Array.from(
85
- { length: 1000 },
86
- (): RoomHit => [270 + random() * 900, 150 + random() * 600, 0],
87
- )
88
- for (const point of points) roomAt(entries, () => point)
89
- const picks: number[] = []
90
- for (let i = 0; i < 20; i++) {
91
- const started = performance.now()
92
- for (const point of points) roomAt(entries, () => point)
93
- picks.push((performance.now() - started) / points.length)
94
- }
95
- const tiers = Object.fromEntries(
96
- ['full', 'name', 'dot', 'hidden'].map((tier) => [
97
- tier,
98
- [...previous.values()].filter((placement) => placement.tier === tier).length,
99
- ]),
100
- )
101
- results[count] = {
102
- placeMs: median(cold),
103
- placeWithPreviousMs: median(warm),
104
- pickMs: median(picks),
105
- tiers,
106
- }
107
- }
108
- console.log(JSON.stringify(results, null, 2))