@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,380 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ BuildingNode,
4
+ clearSceneHistory,
5
+ emitter,
6
+ endScenePreview,
7
+ isScenePreviewing,
8
+ LevelNode,
9
+ registerPointAskHandler,
10
+ useScene,
11
+ WallNode,
12
+ } from '@pascal-app/core'
13
+ import { asksWait, type BubbleTarget, usePointAsk } from '../../store/use-point-ask'
14
+ import { setPointAskBehindQueue, setPointAskBusy } from './host-api'
15
+ import { endCompare, sendBubble, startCompare, startPointAskSession } from './session'
16
+
17
+ // The whole ask path with the real stores (hold to see before; an ask while Pascal is working): the session listening to the chat's
18
+ // statuses, a turn writing the scene through the store's own actions (each a history step), the pin
19
+ // reaching "done", and the hold. Only the chat's words (statuses) and the DOM are stood in for.
20
+
21
+ const building = BuildingNode.parse({ id: 'building_flow' })
22
+ const level = LevelNode.parse({ id: 'level_flow', parentId: building.id })
23
+ const wall = WallNode.parse({
24
+ id: 'wall_flow',
25
+ parentId: level.id,
26
+ start: [0, 0],
27
+ end: [4, 0],
28
+ height: 2.6,
29
+ })
30
+ building.children = [level.id]
31
+ level.children = [wall.id]
32
+
33
+ const STOCK_HEIGHT = 2.6
34
+ const original = useScene.getState()
35
+ const originalWindow = globalThis.window
36
+ const originalDocument = globalThis.document
37
+ const originalRaf = globalThis.requestAnimationFrame
38
+ const originalCancelRaf = globalThis.cancelAnimationFrame
39
+ let stopSession = () => {}
40
+ let unregisterHandler = () => {}
41
+
42
+ const target = {
43
+ id: wall.id,
44
+ type: 'wall',
45
+ name: 'Wall',
46
+ levelId: level.id,
47
+ size: { length: 4, height: STOCK_HEIGHT, thickness: 0.2 },
48
+ box: { min: [0, 0, -0.1], max: [4, STOCK_HEIGHT, 0.1] },
49
+ } as unknown as BubbleTarget
50
+
51
+ function resetScene() {
52
+ useScene.setState({
53
+ nodes: { [building.id]: building, [level.id]: level, [wall.id]: wall },
54
+ rootNodeIds: [building.id],
55
+ dirtyNodes: new Set(),
56
+ readOnly: false,
57
+ } as never)
58
+ clearSceneHistory()
59
+ }
60
+
61
+ beforeEach(() => {
62
+ const stub = { addEventListener() {}, removeEventListener() {} }
63
+ globalThis.window = { ...stub, innerWidth: 1280, innerHeight: 800 } as never
64
+ globalThis.document = { ...stub, hidden: false } as never
65
+ globalThis.requestAnimationFrame = (callback) => {
66
+ callback(0)
67
+ return 0
68
+ }
69
+ globalThis.cancelAnimationFrame = () => {}
70
+ resetScene()
71
+ usePointAsk.setState({ pins: {}, pinOrder: [], bubble: null, comparing: null, busy: false })
72
+ unregisterHandler = registerPointAskHandler(async () => ({ ok: true }))
73
+ stopSession = startPointAskSession()
74
+ })
75
+
76
+ afterEach(() => {
77
+ endCompare()
78
+ endScenePreview()
79
+ stopSession()
80
+ unregisterHandler()
81
+ useScene.setState(original, true)
82
+ clearSceneHistory()
83
+ globalThis.window = originalWindow
84
+ globalThis.document = originalDocument
85
+ if (originalRaf) globalThis.requestAnimationFrame = originalRaf
86
+ else Reflect.deleteProperty(globalThis, 'requestAnimationFrame')
87
+ if (originalCancelRaf) globalThis.cancelAnimationFrame = originalCancelRaf
88
+ else Reflect.deleteProperty(globalThis, 'cancelAnimationFrame')
89
+ })
90
+
91
+ /** The person points at the wall and sends; returns the ask's id. */
92
+ async function sendAsk() {
93
+ usePointAsk.getState().openBubble({
94
+ targets: [target],
95
+ anchor: [2, 1, 0],
96
+ hit: { point: [2, 1, 0], normal: [0, 0, 1] },
97
+ gesture: 'click',
98
+ capturedAt: new Date().toISOString(),
99
+ camera: null,
100
+ crop: { state: 'failed' },
101
+ draft: '',
102
+ error: null,
103
+ })
104
+ await sendBubble('make this wall taller', { x: 0, y: 0, width: 10, height: 10 })
105
+ const askId = usePointAsk.getState().pinOrder.at(-1)
106
+ if (!askId) throw new Error('the ask did not plant a pin')
107
+ return askId
108
+ }
109
+
110
+ const status = (askId: string, rest: Record<string, unknown>) =>
111
+ emitter.emit('point-ask:status', { askId, ...rest } as never)
112
+
113
+ /** A scene action of the agent: history tracks it, and is left as it was found (the hosted executor's contract). */
114
+ function agentWrite(write: () => void) {
115
+ const wasTracking = useScene.temporal.getState().isTracking
116
+ useScene.temporal.getState().resume()
117
+ try {
118
+ write()
119
+ } finally {
120
+ if (!wasTracking) useScene.temporal.getState().pause()
121
+ }
122
+ }
123
+
124
+ /** What the hosted executor did before it was fixed: always paused after, whatever it found. */
125
+ function leftoverPause(write: () => void) {
126
+ useScene.temporal.getState().resume()
127
+ try {
128
+ write()
129
+ } finally {
130
+ useScene.temporal.getState().pause()
131
+ }
132
+ }
133
+
134
+ const wallHeight = () => (useScene.getState().nodes[wall.id] as { height: number }).height
135
+
136
+ describe('a history left untracked after the turn', () => {
137
+ test('refuses the hold, and the card is told why (it was silent: the owner, 12 October)', async () => {
138
+ const askId = await sendAsk()
139
+ status(askId, { status: 'working', sentence: 'Raising the wall' })
140
+ leftoverPause(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
141
+ status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
142
+ expect(startCompare(askId)).toBe(false)
143
+ expect(isScenePreviewing()).toBe(false)
144
+ expect(usePointAsk.getState().compareRefusal).toEqual({ askId, reason: 'history-held' })
145
+ })
146
+ })
147
+
148
+ describe('hold to see before, end to end', () => {
149
+ test('a turn that wrote the scene can be held to show the scene as it was', async () => {
150
+ const askId = await sendAsk()
151
+ status(askId, { status: 'working', sentence: 'Raising the wall' })
152
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
153
+ status(askId, {
154
+ status: 'ended',
155
+ outcome: 'completed',
156
+ asked: false,
157
+ wroteScene: true,
158
+ })
159
+ expect(usePointAsk.getState().pins[askId]?.model.state).toBe('done')
160
+ expect(wallHeight()).toBe(3.4)
161
+
162
+ expect(startCompare(askId)).toBe(true)
163
+ expect(isScenePreviewing()).toBe(true)
164
+ expect(wallHeight()).toBe(STOCK_HEIGHT)
165
+ expect(usePointAsk.getState().comparing).toBe(askId)
166
+
167
+ endCompare()
168
+ expect(isScenePreviewing()).toBe(false)
169
+ expect(wallHeight()).toBe(3.4)
170
+ expect(usePointAsk.getState().comparing).toBeNull()
171
+ })
172
+
173
+ test('a turn of several writes shows the state from before the first of them', async () => {
174
+ const askId = await sendAsk()
175
+ status(askId, { status: 'working', sentence: 'Building' })
176
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3 }))
177
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.2 }))
178
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
179
+ status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
180
+ expect(startCompare(askId)).toBe(true)
181
+ expect(wallHeight()).toBe(STOCK_HEIGHT)
182
+ })
183
+
184
+ test('a write that lands before the chat reports "working" is not part of the before', async () => {
185
+ const askId = await sendAsk()
186
+ // The chat accepted the ask; its first write comes before its first status.
187
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
188
+ status(askId, { status: 'working', sentence: 'Reading your request' })
189
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.8 }))
190
+ status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
191
+ expect(startCompare(askId)).toBe(true)
192
+ expect(wallHeight()).toBe(STOCK_HEIGHT)
193
+ })
194
+
195
+ test('a turn that is not writing does not stop the hold, even when a pin never heard its end', async () => {
196
+ const first = await sendAsk()
197
+ status(first, { status: 'working', sentence: 'Raising the wall' })
198
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
199
+ status(first, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
200
+ // Another ask whose end the pin never heard: it says "working", but the chat is idle.
201
+ const stuck = usePointAsk.getState().pins[first]
202
+ if (!stuck) throw new Error('the ask has no pin')
203
+ usePointAsk.getState().addPin({
204
+ ...stuck,
205
+ model: { ...stuck.model, askId: 'ask_stuck', state: 'working' },
206
+ })
207
+ usePointAsk.setState({ busy: false })
208
+ expect(startCompare(first)).toBe(true)
209
+ endCompare()
210
+ usePointAsk.setState({ busy: true })
211
+ expect(startCompare(first)).toBe(false)
212
+ expect(usePointAsk.getState().compareRefusal).toEqual({
213
+ askId: first,
214
+ reason: 'turn-working',
215
+ })
216
+ })
217
+ })
218
+
219
+ function openBubbleOnWall(draft = '') {
220
+ usePointAsk.getState().openBubble({
221
+ targets: [target],
222
+ anchor: [2, 1, 0],
223
+ hit: { point: [2, 1, 0], normal: [0, 0, 1] },
224
+ gesture: 'click',
225
+ capturedAt: new Date().toISOString(),
226
+ camera: null,
227
+ crop: { state: 'failed' },
228
+ draft,
229
+ error: null,
230
+ })
231
+ }
232
+
233
+ describe('an ask while Pascal is working', () => {
234
+ test('is handed to the chat, which queues it as its own turn: the pin says queued', async () => {
235
+ const handed: string[] = []
236
+ unregisterHandler()
237
+ unregisterHandler = registerPointAskHandler(async (submit) => {
238
+ handed.push(submit.askId)
239
+ emitter.emit('point-ask:status', { askId: submit.askId, status: 'queued' } as never)
240
+ return { ok: true }
241
+ })
242
+ usePointAsk.setState({ busy: true })
243
+ openBubbleOnWall()
244
+ await sendBubble('make this wall taller', { x: 0, y: 0, width: 10, height: 10 })
245
+ const state = usePointAsk.getState()
246
+ expect(handed).toHaveLength(1)
247
+ expect(state.bubble).toBeNull()
248
+ expect(state.pinOrder).toHaveLength(1)
249
+ expect(state.pins[handed[0]!]?.model.state).toBe('queued')
250
+ })
251
+
252
+ test("takes none of the running turn's changes, and its before is the scene when its own turn starts", async () => {
253
+ unregisterHandler()
254
+ unregisterHandler = registerPointAskHandler(async (submit) => {
255
+ emitter.emit('point-ask:status', { askId: submit.askId, status: 'queued' } as never)
256
+ return { ok: true }
257
+ })
258
+ // An earlier ask is running and writes.
259
+ const running = await sendAsk()
260
+ status(running, { status: 'working', sentence: 'Raising the wall' })
261
+ usePointAsk.setState({ busy: true })
262
+ openBubbleOnWall()
263
+ await sendBubble('paint it', { x: 0, y: 0, width: 10, height: 10 })
264
+ const queued = usePointAsk.getState().pinOrder.at(-1)!
265
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3 }))
266
+ status(running, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
267
+ expect(usePointAsk.getState().pins[queued]?.model.committed).toBe(false)
268
+ // Its own turn starts and writes.
269
+ status(queued, { status: 'working', sentence: 'Painting' })
270
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
271
+ status(queued, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
272
+ const pin = usePointAsk.getState().pins[queued]?.model
273
+ expect(pin?.state).toBe('done')
274
+ expect(pin?.changedTargetIds).toEqual([wall.id])
275
+ expect(startCompare(queued)).toBe(true)
276
+ // The state it started from: the running turn's change is in it.
277
+ expect(wallHeight()).toBe(3)
278
+ })
279
+
280
+ test('a host that does not queue answers busy, and the bubble comes back with its reason', async () => {
281
+ unregisterHandler()
282
+ unregisterHandler = registerPointAskHandler(async () => ({
283
+ ok: false,
284
+ code: 'busy',
285
+ message: 'Pascal is working. Ask again when it is done.',
286
+ }))
287
+ openBubbleOnWall()
288
+ await sendBubble('make this wall taller', { x: 0, y: 0, width: 10, height: 10 })
289
+ const state = usePointAsk.getState()
290
+ expect(state.pinOrder).toHaveLength(0)
291
+ expect(state.bubble?.error).toBe('Pascal is working. Ask again when it is done.')
292
+ })
293
+ })
294
+
295
+ describe("which pin takes the scene's changes", () => {
296
+ test("a stale pin never absorbs the running ask's changes: they go to its pin alone", async () => {
297
+ // Two earlier asks said "working" and the pin never heard their end (a dropped turn, a reload of
298
+ // the chat), and they point at the same wall.
299
+ const staleWorking = await sendAsk()
300
+ status(staleWorking, { status: 'working', sentence: 'Raising the wall' })
301
+ const staleAsking = await sendAsk()
302
+ status(staleAsking, { status: 'working', sentence: 'Painting' })
303
+ status(staleAsking, { status: 'asking', question: 'Which colour?' })
304
+ // The ask that runs now.
305
+ const running = await sendAsk()
306
+ status(running, { status: 'working', sentence: 'Reading your request' })
307
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
308
+ status(running, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
309
+
310
+ const { pins } = usePointAsk.getState()
311
+ expect(pins[running]?.model.state).toBe('done')
312
+ expect(pins[running]?.model.changedTargetIds).toEqual([wall.id])
313
+ for (const stale of [staleWorking, staleAsking]) {
314
+ expect(pins[stale]?.model.committed).toBe(false)
315
+ expect(pins[stale]?.model.changedTargetIds).toEqual([])
316
+ expect(pins[stale]?.model.alsoChangedIds).toEqual([])
317
+ expect(pins[stale]?.model.state).not.toBe('done')
318
+ }
319
+ })
320
+
321
+ test('changes made before any ask is working go to no pin', async () => {
322
+ const waiting = await sendAsk()
323
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
324
+ status(waiting, { status: 'ended', outcome: 'completed', asked: false, wroteScene: false })
325
+ expect(usePointAsk.getState().pins[waiting]?.model.committed).toBe(false)
326
+ })
327
+
328
+ test("a stale pin is settled when another ask starts working: it is not left 'working'", async () => {
329
+ const stale = await sendAsk()
330
+ status(stale, { status: 'working', sentence: 'Raising the wall' })
331
+ const running = await sendAsk()
332
+ status(running, { status: 'working', sentence: 'Reading your request' })
333
+ expect(usePointAsk.getState().pins[stale]?.model.state).not.toBe('working')
334
+ expect(usePointAsk.getState().pins[running]?.model.state).toBe('working')
335
+ })
336
+
337
+ test("a plain message's changes after an ask whose end was never heard are no ask's", async () => {
338
+ const stale = await sendAsk()
339
+ setPointAskBusy(true)
340
+ status(stale, { status: 'working', sentence: 'Raising the wall' })
341
+ // The chat goes idle without the pin having heard the end, then a plain message runs.
342
+ setPointAskBusy(false)
343
+ setPointAskBusy(true)
344
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
345
+ setPointAskBusy(false)
346
+ const pin = usePointAsk.getState().pins[stale]?.model
347
+ expect(pin?.committed).toBe(false)
348
+ expect(pin?.state).not.toBe('working')
349
+ })
350
+ })
351
+
352
+ describe('messages already queued or held', () => {
353
+ // The chat queues an ask behind messages that wait (a queue held after an error needs Resume),
354
+ // with no turn running: the bubble must say Queue, but nothing is working.
355
+ test('a new ask waits behind them: the bubble says Queue, and no turn is said to run', () => {
356
+ expect(asksWait(usePointAsk.getState())).toBe(false)
357
+ setPointAskBehindQueue(true)
358
+ const state = usePointAsk.getState()
359
+ expect(asksWait(state)).toBe(true)
360
+ expect(state.busy).toBe(false)
361
+ setPointAskBehindQueue(false)
362
+ expect(asksWait(usePointAsk.getState())).toBe(false)
363
+ })
364
+
365
+ test('a running turn makes asks wait too, whether or not anything is queued', () => {
366
+ setPointAskBusy(true)
367
+ expect(asksWait(usePointAsk.getState())).toBe(true)
368
+ setPointAskBusy(false)
369
+ expect(asksWait(usePointAsk.getState())).toBe(false)
370
+ })
371
+
372
+ test('they do not refuse a hold, nor settle a pin the way an idle chat does', async () => {
373
+ const askId = await sendAsk()
374
+ status(askId, { status: 'working', sentence: 'Raising the wall' })
375
+ agentWrite(() => useScene.getState().updateNode(wall.id, { height: 3.4 }))
376
+ status(askId, { status: 'ended', outcome: 'completed', asked: false, wroteScene: true })
377
+ setPointAskBehindQueue(true)
378
+ expect(startCompare(askId)).toBe(true)
379
+ })
380
+ })
@@ -0,0 +1,173 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import type { AnyNode, PointContext } from '@pascal-app/core'
3
+ import { house, LEVEL_ID } from './__fixtures__/house'
4
+ import { type BuildPointContextInput, buildPointContext } from './build-context'
5
+
6
+ // What the agent receives when the person points (spec section 5): ids first, in metres, the camera
7
+ // the person saw. Written first: an id the agent cannot read back; a size in the viewer's unit
8
+ // instead of metres; a target dropped past a preview limit; an orthographic camera with no width, a
9
+ // perspective one with a stray one; fields present as `undefined` where there is nothing to say.
10
+
11
+ const CAMERA = {
12
+ position: [6.2, 4.1, 7.5] as [number, number, number],
13
+ target: [2, 1, 1.5] as [number, number, number],
14
+ fov: 50,
15
+ aspect: 1.6,
16
+ projection: 'perspective' as const,
17
+ }
18
+
19
+ const nodes = house()
20
+ const when = new Date('2026-10-08T01:02:03.000Z')
21
+
22
+ const build = (over: Partial<BuildPointContextInput> = {}): PointContext =>
23
+ buildPointContext({
24
+ askId: 'ask_1',
25
+ capturedAt: when,
26
+ gesture: 'click',
27
+ nodes,
28
+ levelId: LEVEL_ID,
29
+ camera: CAMERA,
30
+ targets: [{ id: 'zone_kitchen' }],
31
+ ...over,
32
+ })
33
+
34
+ describe('the context of a click on a room', () => {
35
+ const context = build()
36
+
37
+ test('names itself, its ask, its moment and its surface', () => {
38
+ expect(context).toMatchObject({
39
+ kind: 'scene-point',
40
+ version: 1,
41
+ askId: 'ask_1',
42
+ capturedAt: '2026-10-08T01:02:03.000Z',
43
+ source: 'editor',
44
+ gesture: 'click',
45
+ level: { id: 'level_0', name: 'Level 0' },
46
+ })
47
+ })
48
+
49
+ test('carries the room by id, with its name, level, size in metres and box', () => {
50
+ const [kitchen] = context.targets
51
+
52
+ expect(kitchen).toMatchObject({
53
+ id: 'zone_kitchen',
54
+ type: 'zone',
55
+ name: 'Kitchen',
56
+ levelId: 'level_0',
57
+ size: { area: 12, width: 4, depth: 3 },
58
+ })
59
+ expect(kitchen!.box.min[0]).toBeCloseTo(0, 6)
60
+ expect(kitchen!.box.max[0]).toBeCloseTo(4, 6)
61
+ expect(kitchen!.box.min[2]).toBeCloseTo(0, 6)
62
+ expect(kitchen!.box.max[2]).toBeCloseTo(3, 6)
63
+ expect(kitchen!.box.max[1]).toBeGreaterThan(kitchen!.box.min[1])
64
+ })
65
+
66
+ test('the camera is the one the person saw, as view_scene takes it', () => {
67
+ expect(context.camera).toEqual(CAMERA)
68
+ expect('viewWidth' in context.camera).toBe(false)
69
+ })
70
+
71
+ test('says nothing it has nothing to say about', () => {
72
+ for (const key of ['inRegion', 'region', 'image', 'suggestion'])
73
+ expect(key in context, key).toBe(false)
74
+ expect('hit' in context.targets[0]!).toBe(false)
75
+ })
76
+ })
77
+
78
+ describe('targets', () => {
79
+ test('a window: its wall as parent, its room, the finger’s hit, its sill', () => {
80
+ const hit = {
81
+ point: [2, 1.4, 0] as [number, number, number],
82
+ normal: [0, 0, -1] as [number, number, number],
83
+ face: 'exterior' as const,
84
+ }
85
+ const [window] = build({ targets: [{ id: 'window_north', hit }] }).targets
86
+
87
+ expect(window).toMatchObject({
88
+ id: 'window_north',
89
+ type: 'window',
90
+ name: 'Window',
91
+ parentId: 'wall_north',
92
+ zoneId: 'zone_kitchen',
93
+ hit,
94
+ })
95
+ expect(window!.size.sill).toBeCloseTo(0.7, 6)
96
+ })
97
+
98
+ test('a wall and an item', () => {
99
+ const [wall, sofa] = build({ targets: [{ id: 'wall_north' }, { id: 'item_sofa' }] }).targets
100
+
101
+ expect(wall).toMatchObject({ type: 'wall', zoneId: 'zone_kitchen' })
102
+ expect(wall!.size.length).toBeCloseTo(4, 6)
103
+ expect(sofa).toMatchObject({ type: 'item', name: 'Sofa' })
104
+ expect(sofa!.size).toEqual({ width: 2.1, depth: 0.95, height: 0.85 })
105
+ })
106
+
107
+ test('the viewer’s unit never reaches the context: sizes are metres', () => {
108
+ const [window] = build({ targets: [{ id: 'window_north' }] }).targets
109
+
110
+ expect(window!.size.width).toBeCloseTo(1.2, 6)
111
+ })
112
+
113
+ test('a dozen and more all arrive, each by its own id (no preview limit)', () => {
114
+ const extra: AnyNode[] = Array.from(
115
+ { length: 14 },
116
+ (_, i) =>
117
+ ({
118
+ ...(nodes.item_sofa as object),
119
+ id: `item_chair_${i}`,
120
+ name: 'Chair',
121
+ }) as unknown as AnyNode,
122
+ )
123
+ const scene = house(extra)
124
+ const context = build({
125
+ nodes: scene,
126
+ gesture: 'multi',
127
+ targets: extra.map((node) => ({ id: node.id })),
128
+ })
129
+
130
+ expect(context.targets).toHaveLength(14)
131
+ expect(new Set(context.targets.map((t) => t.id)).size).toBe(14)
132
+ expect(context.targets.every((t) => t.name === 'Chair')).toBe(true)
133
+ })
134
+
135
+ test('an element that is gone is left out, never guessed', () => {
136
+ const context = build({ targets: [{ id: 'zone_kitchen' }, { id: 'wall_gone' }] })
137
+
138
+ expect(context.targets.map((t) => t.id)).toEqual(['zone_kitchen'])
139
+ })
140
+ })
141
+
142
+ describe('the camera and the picture', () => {
143
+ test('an orthographic camera carries its view width, a perspective one does not', () => {
144
+ const ortho = build({ camera: { ...CAMERA, projection: 'orthographic', viewWidth: 14 } })
145
+
146
+ expect(ortho.camera).toMatchObject({ projection: 'orthographic', viewWidth: 14 })
147
+ expect('viewWidth' in build({ camera: { ...CAMERA, viewWidth: 14 } }).camera).toBe(false)
148
+ })
149
+
150
+ test('a region is a view, not a selection: its ids ride beside empty targets', () => {
151
+ const context = build({
152
+ gesture: 'region',
153
+ targets: [],
154
+ region: [0.2, 0.3, 0.6, 0.8],
155
+ inRegion: { ids: ['wall_north', 'window_north'], more: 3 },
156
+ })
157
+
158
+ expect(context).toMatchObject({
159
+ gesture: 'region',
160
+ targets: [],
161
+ region: [0.2, 0.3, 0.6, 0.8],
162
+ inRegion: { ids: ['wall_north', 'window_north'], more: 3 },
163
+ })
164
+ })
165
+
166
+ test('the crop and the suggestion pass through as given', () => {
167
+ const image = { dataUrl: 'data:image/png;base64,AAAA', width: 640, height: 480, marked: true }
168
+ const context = build({ image, suggestion: 'make-it-wider' })
169
+
170
+ expect(context.image).toEqual(image)
171
+ expect(context.suggestion).toBe('make-it-wider')
172
+ })
173
+ })
@@ -0,0 +1,94 @@
1
+ import type {
2
+ AnyNode,
3
+ LevelNode,
4
+ PointCamera,
5
+ PointContext,
6
+ PointTarget as PointContextTarget,
7
+ } from '@pascal-app/core'
8
+ import { sceneViewBounds } from '@pascal-app/core/agent-operations'
9
+ import { describeTarget } from './describe-target'
10
+
11
+ // What the agent receives when the person points (point-and-ask-spec.md section 5): ids first, in
12
+ // metres, from the camera the person saw. The words stay the person's; this is metadata.
13
+
14
+ export type BuildPointContextInput = {
15
+ /** The pin, the chat chip and the turn share it. */
16
+ askId: string
17
+ /** The moment of pointing (pointer-down); now when absent. */
18
+ capturedAt?: Date | string
19
+ gesture: PointContext['gesture']
20
+ nodes: Readonly<Record<string, AnyNode>>
21
+ /** The level in view. */
22
+ levelId: string
23
+ /** What the finger is on, each by id, with where it landed. */
24
+ targets: { id: string; hit?: PointContextTarget['hit'] }[]
25
+ camera: PointCamera
26
+ inRegion?: PointContext['inRegion']
27
+ region?: PointContext['region']
28
+ image?: PointContext['image']
29
+ suggestion?: string
30
+ }
31
+
32
+ type Box = PointContextTarget['box']
33
+
34
+ /** The element's box as view_scene frames it; a point at its position when that cannot say. */
35
+ function boxOf(nodes: Readonly<Record<string, AnyNode>>, node: AnyNode): Box {
36
+ try {
37
+ return sceneViewBounds(nodes, node.id)
38
+ } catch {
39
+ const at = (node as { position?: number[] }).position ?? [0, 0, 0]
40
+ const point: [number, number, number] = [at[0] ?? 0, at[1] ?? 0, at[2] ?? 0]
41
+ return { min: point, max: point }
42
+ }
43
+ }
44
+
45
+ export function buildPointContext(input: BuildPointContextInput): PointContext {
46
+ const { nodes } = input
47
+ const level = nodes[input.levelId] as LevelNode | undefined
48
+ const camera: PointCamera = {
49
+ position: input.camera.position,
50
+ target: input.camera.target,
51
+ fov: input.camera.fov,
52
+ aspect: input.camera.aspect,
53
+ projection: input.camera.projection,
54
+ ...(input.camera.projection === 'orthographic' && input.camera.viewWidth !== undefined
55
+ ? { viewWidth: input.camera.viewWidth }
56
+ : {}),
57
+ }
58
+
59
+ const targets: PointContextTarget[] = []
60
+ for (const { id, hit } of input.targets) {
61
+ const node = nodes[id]
62
+ // An element deleted between the pointing and the send is not a target any more.
63
+ if (!node) continue
64
+ const described = describeTarget(node, nodes, { unit: 'metric' })
65
+ targets.push({
66
+ id: node.id,
67
+ type: node.type,
68
+ name: described.name,
69
+ levelId: described.levelId,
70
+ ...(described.zoneId ? { zoneId: described.zoneId } : {}),
71
+ ...(described.parentId ? { parentId: described.parentId } : {}),
72
+ ...(hit ? { hit } : {}),
73
+ box: boxOf(nodes, node),
74
+ size: described.sizes,
75
+ })
76
+ }
77
+
78
+ const capturedAt = input.capturedAt ?? new Date()
79
+ return {
80
+ kind: 'scene-point',
81
+ version: 1,
82
+ askId: input.askId,
83
+ capturedAt: typeof capturedAt === 'string' ? capturedAt : capturedAt.toISOString(),
84
+ source: 'editor',
85
+ gesture: input.gesture,
86
+ level: { id: input.levelId, name: level?.name?.trim() || `Level ${level?.level ?? 0}` },
87
+ targets,
88
+ camera,
89
+ ...(input.inRegion ? { inRegion: input.inRegion } : {}),
90
+ ...(input.region ? { region: input.region } : {}),
91
+ ...(input.image ? { image: input.image } : {}),
92
+ ...(input.suggestion ? { suggestion: input.suggestion } : {}),
93
+ }
94
+ }