@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,129 @@
1
+ import {
2
+ emitter,
3
+ type SnapshotCapturedEvent,
4
+ type SnapshotCaptureFailedEvent,
5
+ } from '@pascal-app/core'
6
+ import { useViewer } from '@pascal-app/viewer'
7
+ import { canvasRect } from './projector'
8
+ import type { Rect } from './types'
9
+
10
+ // The picture an ask carries as evidence (spec 2.4): the targets' screen box padded 25%, at least
11
+ // 512 px on its long side, rendered by the snapshot pipeline as an ephemeral frame so nothing is
12
+ // stored. Started at pointer-down, so it is ready by the time the person has typed.
13
+
14
+ const CROP_LONG_SIDE_MIN = 512
15
+ const CROP_LONG_SIDE_MAX = 768
16
+ const CAPTURE_TIMEOUT_MS = 12_000
17
+
18
+ export type CapturedCrop = { dataUrl: string; width: number; height: number }
19
+
20
+ /** The target's box padded by `pad` of its size on every side, and kept inside the canvas. */
21
+ export function paddedCropRect(
22
+ box: Rect,
23
+ canvas: { width: number; height: number },
24
+ pad = 0.25,
25
+ ): Rect {
26
+ const w = Math.max(48, box.x1 - box.x0)
27
+ const h = Math.max(48, box.y1 - box.y0)
28
+ const cx = (box.x0 + box.x1) / 2
29
+ const cy = (box.y0 + box.y1) / 2
30
+ const half = { x: (w * (1 + pad * 2)) / 2, y: (h * (1 + pad * 2)) / 2 }
31
+ return {
32
+ x0: Math.max(0, cx - half.x),
33
+ y0: Math.max(0, cy - half.y),
34
+ x1: Math.min(canvas.width, cx + half.x),
35
+ y1: Math.min(canvas.height, cy + half.y),
36
+ }
37
+ }
38
+
39
+ /** A rectangle as 0-1 fractions of the frame, for the snapshot pipeline's `cropRegion`. */
40
+ export function toCropRegion(rect: Rect, canvas: { width: number; height: number }) {
41
+ return {
42
+ x: rect.x0 / canvas.width,
43
+ y: rect.y0 / canvas.height,
44
+ width: (rect.x1 - rect.x0) / canvas.width,
45
+ height: (rect.y1 - rect.y0) / canvas.height,
46
+ }
47
+ }
48
+
49
+ async function blobToScaledDataUrl(blob: Blob): Promise<CapturedCrop> {
50
+ const bitmap = await createImageBitmap(blob)
51
+ const long = Math.max(bitmap.width, bitmap.height)
52
+ const scale =
53
+ long > CROP_LONG_SIDE_MAX ? CROP_LONG_SIDE_MAX / long : long < CROP_LONG_SIDE_MIN ? 1 : 1
54
+ const width = Math.round(bitmap.width * scale)
55
+ const height = Math.round(bitmap.height * scale)
56
+ const canvas = document.createElement('canvas')
57
+ canvas.width = width
58
+ canvas.height = height
59
+ const context = canvas.getContext('2d')
60
+ if (!context) throw new Error('No 2D canvas to scale the picture')
61
+ context.drawImage(bitmap, 0, 0, width, height)
62
+ bitmap.close()
63
+ return { dataUrl: canvas.toDataURL('image/jpeg', 0.84), width, height }
64
+ }
65
+
66
+ const BUSY = 'Another snapshot is being captured'
67
+ const BUSY_RETRY_MS = 1_500
68
+
69
+ /**
70
+ * One crop of the current view over `screenRect` (viewport pixels); once more after 1.5 s when
71
+ * another capture (the agent's own view) holds the renderer. Rejects when the 3D canvas cannot
72
+ * answer in time (a hidden tab, a busy renderer): the ask then goes without a picture.
73
+ */
74
+ export async function captureViewCrop(
75
+ screenRect: Rect,
76
+ options: { pad?: number } = {},
77
+ ): Promise<CapturedCrop> {
78
+ try {
79
+ return await captureOnce(screenRect, options)
80
+ } catch (error) {
81
+ if (!(error instanceof Error && error.message.startsWith(BUSY))) throw error
82
+ await new Promise((resolve) => setTimeout(resolve, BUSY_RETRY_MS))
83
+ return captureOnce(screenRect, options)
84
+ }
85
+ }
86
+
87
+ function captureOnce(screenRect: Rect, options: { pad?: number }): Promise<CapturedCrop> {
88
+ const rect = canvasRect()
89
+ const projectId = useViewer.getState().projectId
90
+ if (!rect || !projectId) return Promise.reject(new Error('The 3D view is not ready'))
91
+ const local: Rect = {
92
+ x0: screenRect.x0 - rect.left,
93
+ y0: screenRect.y0 - rect.top,
94
+ x1: screenRect.x1 - rect.left,
95
+ y1: screenRect.y1 - rect.top,
96
+ }
97
+ const cropRegion = toCropRegion(paddedCropRect(local, rect, options.pad ?? 0.25), rect)
98
+ const requestId = `point-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`
99
+ return new Promise((resolve, reject) => {
100
+ const stop = () => {
101
+ clearTimeout(timer)
102
+ emitter.off('snapshot:captured', onCaptured)
103
+ emitter.off('snapshot:capture-failed', onFailed)
104
+ }
105
+ const onCaptured = (event: SnapshotCapturedEvent) => {
106
+ if (event.requestId !== requestId) return
107
+ stop()
108
+ blobToScaledDataUrl(event.blob).then(resolve, reject)
109
+ }
110
+ const onFailed = (event: SnapshotCaptureFailedEvent) => {
111
+ if (event.requestId !== requestId) return
112
+ stop()
113
+ reject(new Error(event.error))
114
+ }
115
+ const timer = setTimeout(() => {
116
+ stop()
117
+ reject(new Error('No picture came back from the 3D view'))
118
+ }, CAPTURE_TIMEOUT_MS)
119
+ emitter.on('snapshot:captured', onCaptured)
120
+ emitter.on('snapshot:capture-failed', onFailed)
121
+ emitter.emit('camera-controls:generate-thumbnail', {
122
+ projectId,
123
+ requestId,
124
+ ephemeral: true,
125
+ captureMode: 'area',
126
+ cropRegion,
127
+ })
128
+ })
129
+ }
@@ -0,0 +1,69 @@
1
+ // The numbers Point and ask moves by, in one place: the direction's tokens
2
+ // (chat-juice-direction.md), the choreography section of point-and-ask-spec.md, and where the two
3
+ // differ, the prototype's (pascal-builds.html), which the owner played and called the bar.
4
+
5
+ export const EASE_OUT = 'cubic-bezier(0.23, 1, 0.32, 1)'
6
+ export const EASE_IN_OUT = 'cubic-bezier(0.77, 0, 0.175, 1)'
7
+
8
+ export type SpringTokens = { damping: number; response: number }
9
+
10
+ /** Critically damped: the default for anything a person can touch. */
11
+ export const SPRING: SpringTokens = { damping: 1, response: 0.35 }
12
+ /** A little bounce, only for things that carry momentum. */
13
+ export const MOMENTUM: SpringTokens = { damping: 0.8, response: 0.35 }
14
+ /** The outline's four corners travelling between targets. */
15
+ export const CORNER_SPRING: SpringTokens = { damping: 1, response: 0.16 }
16
+
17
+ // Durations (ms).
18
+ export const PRESS_MS = 120
19
+ export const ENTER_MS = 180
20
+ export const CARD_MS = 220
21
+ /** An exit is quicker than an entrance. */
22
+ export const BUBBLE_CLOSE_MS = 150
23
+ export const CARD_CLOSE_MS = 120
24
+ export const PIN_RETIRE_FADE_MS = 180
25
+ export const CHECK_STROKE_MS = 240
26
+ export const CHECK_PULSE_MS = 380
27
+ export const PIN_DROP_MS = 320
28
+ export const PIN_RING_MS = 560
29
+ export const PIN_RING_DELAY_MS = 160
30
+ // The prototype's flash, longer than the spec's 120 ms: a shutter has to be seen to be felt.
31
+ export const REGION_FLASH_MS = 280
32
+ export const SUGGESTION_STAGGER_MS = 30
33
+ export const SUGGESTION_DELAY_MS = 40
34
+ export const CHIP_FADE_MS = 120
35
+
36
+ // Thresholds and dwell (ms or px).
37
+ export const CLICK_SLOP_PX = 6
38
+ export const DWELL_MS = 120
39
+ export const SIDE_HYSTERESIS_MS = 150
40
+ export const TAP_LATCH_MS = 200
41
+ export const CARD_AUTO_MS = 4000
42
+ export const CARD_CLOSE_DELAY_MS = 400
43
+ // A card the camera carried from under a still pointer stays this long, for the pointer to reach it.
44
+ export const CARD_CARRIED_MS = 6000
45
+ export const PIN_RETIRE_MS = 8000
46
+ // A press on a done pin that stays down this long shows the scene as it was before the turn.
47
+ export const PIN_HOLD_MS = 220
48
+ // While held, the pointer may drift this far from where it pressed before the hold ends.
49
+ export const PIN_HOLD_SLOP_PX = 36
50
+ export const THIN_TARGET_PX = 6
51
+
52
+ // Layout (px).
53
+ export const BUBBLE_W = 320
54
+ export const BUBBLE_GAP = 16
55
+ export const VIEWPORT_INSET = 12
56
+ export const BIG_BOX_DISC = 48
57
+ export const MAX_TARGETS = 12
58
+ export const PIN_DISC = 24
59
+ export const PIN_HIT = 32
60
+ export const PIN_HIT_TOUCH = 44
61
+ export const CARD_W = 250
62
+ export const CHIP_OFFSET = 14
63
+
64
+ // Motion shapes.
65
+ /** The bubble grows from the click: 0.92 -> 1 with opacity and a 4 px blur (the prototype's, not the spec's 0.96). */
66
+ export const BUBBLE_OPEN_SCALE = 0.92
67
+ export const BUBBLE_OPEN_BLUR_PX = 4
68
+ export const PIN_DROP_PX = 8
69
+ export const CHIP_RISE_PX = 4
@@ -0,0 +1,145 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { createCompare } from './compare'
3
+
4
+ // Hold a control, see the scene as it was before the turn; let go, and it is back. The preview is
5
+ // the core's (it writes nothing: its own tests say so); what this decides is when one may start,
6
+ // and that it always ends. Written first: a hold that starts under a turn that is writing, or with
7
+ // no state to show; a second hold that leaves the first up; a release that is missed.
8
+
9
+ const snapshot = { nodes: {} } as never
10
+
11
+ function setup(options: { before?: unknown; working?: boolean; begin?: unknown } = {}) {
12
+ const ended: string[] = []
13
+ const changes: (string | null)[] = []
14
+ let began = 0
15
+ const compare = createCompare({
16
+ before: () => ('before' in options ? (options.before as never) : snapshot),
17
+ turnsWorking: () => options.working ?? false,
18
+ begin: () => {
19
+ began++
20
+ if (options.begin === null) return null
21
+ return { end: () => ended.push('end') }
22
+ },
23
+ onChange: (askId) => changes.push(askId),
24
+ })
25
+ return { compare, ended, changes, began: () => began }
26
+ }
27
+
28
+ describe('why a hold is refused', () => {
29
+ test('says a turn is writing', () => {
30
+ const { compare } = setup({ working: true })
31
+ compare.start('ask_1')
32
+ expect(compare.refusal()).toBe('turn-working')
33
+ })
34
+
35
+ test('says history cannot show the before', () => {
36
+ const { compare } = setup({ before: null })
37
+ compare.start('ask_1')
38
+ expect(compare.refusal()).toBe('no-before')
39
+ })
40
+
41
+ test('says the scene is held by something else', () => {
42
+ const { compare } = setup({ begin: null })
43
+ compare.start('ask_1')
44
+ expect(compare.refusal()).toBe('history-held')
45
+ })
46
+
47
+ test('says nothing after a hold that started', () => {
48
+ const { compare } = setup({ working: true })
49
+ compare.start('ask_1')
50
+ const next = setup()
51
+ next.compare.start('ask_1')
52
+ expect(next.compare.refusal()).toBeNull()
53
+ })
54
+ })
55
+
56
+ describe('start', () => {
57
+ test('shows the before of that ask and says whose', () => {
58
+ const { compare, changes } = setup()
59
+
60
+ expect(compare.start('ask_1')).toBe(true)
61
+
62
+ expect(compare.current()).toBe('ask_1')
63
+ expect(changes).toEqual(['ask_1'])
64
+ })
65
+
66
+ test('does not start while a turn is writing: it would be swallowed or land on the wrong state', () => {
67
+ const { compare, began } = setup({ working: true })
68
+
69
+ expect(compare.start('ask_1')).toBe(false)
70
+
71
+ expect(began()).toBe(0)
72
+ expect(compare.current()).toBeNull()
73
+ })
74
+
75
+ test('does not start when history cannot say what it was before', () => {
76
+ const { compare, began } = setup({ before: null })
77
+
78
+ expect(compare.start('ask_1')).toBe(false)
79
+
80
+ expect(began()).toBe(0)
81
+ })
82
+
83
+ test('does not start when the scene would not take a preview (a gesture holds it)', () => {
84
+ const { compare, changes } = setup({ begin: null })
85
+
86
+ expect(compare.start('ask_1')).toBe(false)
87
+
88
+ expect(compare.current()).toBeNull()
89
+ expect(changes).toEqual([])
90
+ })
91
+
92
+ test('a second hold ends the first before it starts', () => {
93
+ const { compare, ended, changes } = setup()
94
+ compare.start('ask_1')
95
+
96
+ compare.start('ask_2')
97
+
98
+ expect(ended).toEqual(['end'])
99
+ expect(compare.current()).toBe('ask_2')
100
+ expect(changes).toEqual(['ask_1', null, 'ask_2'])
101
+ })
102
+ })
103
+
104
+ describe('end', () => {
105
+ test('lets the scene go and clears the tag', () => {
106
+ const { compare, ended, changes } = setup()
107
+ compare.start('ask_1')
108
+
109
+ compare.end()
110
+
111
+ expect(ended).toEqual(['end'])
112
+ expect(compare.current()).toBeNull()
113
+ expect(changes).toEqual(['ask_1', null])
114
+ })
115
+
116
+ test('is idempotent: a pointer-up, a blur and a key can all arrive', () => {
117
+ const { compare, ended, changes } = setup()
118
+ compare.start('ask_1')
119
+
120
+ compare.end()
121
+ compare.end()
122
+ compare.end()
123
+
124
+ expect(ended).toEqual(['end'])
125
+ expect(changes).toEqual(['ask_1', null])
126
+ })
127
+
128
+ test('with nothing held is nothing', () => {
129
+ const { compare, changes } = setup()
130
+
131
+ compare.end()
132
+
133
+ expect(changes).toEqual([])
134
+ })
135
+
136
+ test('when the scene let go by itself (a write), the tag follows', () => {
137
+ const { compare, changes } = setup()
138
+ compare.start('ask_1')
139
+
140
+ compare.released()
141
+
142
+ expect(compare.current()).toBeNull()
143
+ expect(changes).toEqual(['ask_1', null])
144
+ })
145
+ })
@@ -0,0 +1,73 @@
1
+ import type { SceneSnapshot } from '@pascal-app/core'
2
+
3
+ // Hold a control on a pin or its card to see the scene as it was before that turn (the owner,
4
+ // 8 October: "I hold and I see the previous state in 3D"). The core's scene preview does the
5
+ // showing and writes nothing; this decides when a hold may start and keeps the person's view of it
6
+ // (whose, for the "Before" tag) in step with the preview, which ends on its own when a write is
7
+ // about to land.
8
+
9
+ export type CompareDeps = {
10
+ /** The scene as it was before the ask's turn, from the history Undo would restore; null when unknown. */
11
+ before: (askId: string) => SceneSnapshot | null
12
+ /** A turn is writing the scene: it would swallow the preview's writes or land on the wrong state. */
13
+ turnsWorking: () => boolean
14
+ /** The core's `beginScenePreview`: null when the scene will not take one. */
15
+ begin: (snapshot: SceneSnapshot) => { end: () => void } | null
16
+ /** Whose before is on screen, or null: the tag follows. */
17
+ onChange: (askId: string | null) => void
18
+ }
19
+
20
+ /** Why a hold did not start, so the person is told instead of left holding a button that does nothing. */
21
+ export type CompareRefusal = 'turn-working' | 'no-before' | 'history-held'
22
+
23
+ export type Compare = {
24
+ /** Starts showing that ask's before; false when it may not. Ends a hold already up. */
25
+ start: (askId: string) => boolean
26
+ /** Why the last `start` returned false; null after one that started. */
27
+ refusal: () => CompareRefusal | null
28
+ /** Lets go. Idempotent. */
29
+ end: () => void
30
+ /** The preview ended by itself (a write was about to land): clears the tag. */
31
+ released: () => void
32
+ current: () => string | null
33
+ }
34
+
35
+ export function createCompare(deps: CompareDeps): Compare {
36
+ let held: { askId: string; preview: { end: () => void } } | null = null
37
+ let refused: CompareRefusal | null = null
38
+
39
+ const set = (askId: string | null) => deps.onChange(askId)
40
+
41
+ return {
42
+ start(askId) {
43
+ const refuse = (reason: CompareRefusal) => {
44
+ refused = reason
45
+ return false
46
+ }
47
+ if (deps.turnsWorking()) return refuse('turn-working')
48
+ const snapshot = deps.before(askId)
49
+ if (!snapshot) return refuse('no-before')
50
+ if (held) this.end()
51
+ const preview = deps.begin(snapshot)
52
+ if (!preview) return refuse('history-held')
53
+ refused = null
54
+ held = { askId, preview }
55
+ set(askId)
56
+ return true
57
+ },
58
+ end() {
59
+ if (!held) return
60
+ const { preview } = held
61
+ held = null
62
+ preview.end()
63
+ set(null)
64
+ },
65
+ released() {
66
+ if (!held) return
67
+ held = null
68
+ set(null)
69
+ },
70
+ refusal: () => refused,
71
+ current: () => held?.askId ?? null,
72
+ }
73
+ }
@@ -0,0 +1,165 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import type { AnyNode } from '@pascal-app/core'
3
+ import { house } from './__fixtures__/house'
4
+ import { describeTarget } from './describe-target'
5
+
6
+ // Point and ask names what the finger is on, in the viewer's unit, for the label chip and the
7
+ // bubble's context row, and in metres for the agent (the context's `size`). Written first: a size
8
+ // in the wrong unit; a wall that does not say which room it bounds; a window's sill missing from the
9
+ // context; an element of a kind nobody described showing no name at all.
10
+
11
+ const nodes = house()
12
+ const metric = { unit: 'metric' } as const
13
+ const at = (id: string, unit: Parameters<typeof describeTarget>[2] = metric) =>
14
+ describeTarget(nodes[id] as AnyNode, nodes, unit)
15
+
16
+ describe('a room', () => {
17
+ test('is named by its zone, sized by its area, and is the kind "room"', () => {
18
+ const room = at('zone_kitchen')
19
+
20
+ expect(room).toMatchObject({
21
+ id: 'zone_kitchen',
22
+ type: 'zone',
23
+ kind: 'room',
24
+ name: 'Kitchen',
25
+ size: '12.0 m²',
26
+ levelId: 'level_0',
27
+ })
28
+ expect(room.sizes).toEqual({ area: 12, width: 4, depth: 3 })
29
+ expect(room.zoneId).toBeUndefined()
30
+ })
31
+
32
+ test('a room with no name is "Room"', () => {
33
+ const unnamed = { ...nodes.zone_kitchen, name: '' } as AnyNode
34
+
35
+ expect(describeTarget(unnamed, { ...nodes, zone_kitchen: unnamed }, metric).name).toBe('Room')
36
+ })
37
+ })
38
+
39
+ describe('a wall', () => {
40
+ test('says which room it bounds, its length by its height, and the room it is in', () => {
41
+ const wall = at('wall_north')
42
+
43
+ expect(wall).toMatchObject({ kind: 'wall', name: 'Wall · Kitchen', zoneId: 'zone_kitchen' })
44
+ expect(wall.sizes.length).toBeCloseTo(4, 6)
45
+ expect(wall.sizes.thickness).toBeGreaterThan(0)
46
+ expect(wall.size).toBe(`4.00 × ${wall.sizes.height!.toFixed(2)} m`)
47
+ })
48
+
49
+ test('a wall bounding no room is just "Wall"', () => {
50
+ const { zone_kitchen: _gone, ...without } = nodes
51
+ const wall = describeTarget(nodes.wall_north as AnyNode, without as typeof nodes, metric)
52
+
53
+ expect(wall.name).toBe('Wall')
54
+ expect(wall.zoneId).toBeUndefined()
55
+ })
56
+ })
57
+
58
+ describe('a window and a door', () => {
59
+ test('a window: width by height, its sill in the context, its wall as parent', () => {
60
+ const window = at('window_north')
61
+
62
+ expect(window).toMatchObject({
63
+ kind: 'window',
64
+ name: 'Window',
65
+ size: '1.20 × 1.40 m',
66
+ parentId: 'wall_north',
67
+ zoneId: 'zone_kitchen',
68
+ })
69
+ expect(window.sizes.width).toBeCloseTo(1.2, 6)
70
+ expect(window.sizes.height).toBeCloseTo(1.4, 6)
71
+ expect(window.sizes.sill).toBeCloseTo(0.7, 6)
72
+ })
73
+
74
+ test('a door stands on the floor: sill 0', () => {
75
+ const door = at('door_front')
76
+
77
+ expect(door).toMatchObject({ kind: 'door', name: 'Door', size: '0.90 × 2.10 m' })
78
+ expect(door.sizes.sill).toBeCloseTo(0, 6)
79
+ })
80
+ })
81
+
82
+ describe('an item', () => {
83
+ test('is named by the person’s name, sized by its width, and sits in the room it stands in', () => {
84
+ const sofa = at('item_sofa')
85
+
86
+ expect(sofa).toMatchObject({
87
+ kind: 'item',
88
+ name: 'Sofa',
89
+ size: '2.10 m wide',
90
+ zoneId: 'zone_kitchen',
91
+ })
92
+ expect(sofa.parentId).toBeUndefined()
93
+ expect(sofa.sizes).toEqual({ width: 2.1, depth: 0.95, height: 0.85 })
94
+ })
95
+
96
+ test('with no name it is the asset’s, and then just "Item"', () => {
97
+ const unnamed = { ...nodes.item_sofa, name: undefined } as AnyNode
98
+ expect(describeTarget(unnamed, nodes, metric).name).toBe('Oslo 3-seat')
99
+
100
+ const bare = {
101
+ ...nodes.item_sofa,
102
+ name: undefined,
103
+ asset: { ...(nodes.item_sofa as never as { asset: object }).asset, name: '' },
104
+ } as AnyNode
105
+ expect(describeTarget(bare, nodes, metric).name).toBe('Item')
106
+ })
107
+ })
108
+
109
+ describe('units', () => {
110
+ test('imperial reads in feet, inches and square feet', () => {
111
+ const imperial = { unit: 'imperial' } as const
112
+
113
+ expect(at('zone_kitchen', imperial).size).toBe('129.2 ft²')
114
+ expect(at('window_north', imperial).size).toBe(`3'11" × 4'7"`)
115
+ expect(at('item_sofa', imperial).size).toBe(`6'11" wide`)
116
+ // The context stays in metres whatever the viewer shows.
117
+ expect(at('window_north', imperial).sizes.width).toBeCloseTo(1.2, 6)
118
+ })
119
+
120
+ test('millimetres when the viewer shows them', () => {
121
+ const mm = { unit: 'metric', metricNotation: 'millimeters' } as const
122
+
123
+ expect(at('window_north', mm).size).toBe('1200 × 1400 mm')
124
+ expect(at('item_sofa', mm).size).toBe('2100 mm wide')
125
+ })
126
+ })
127
+
128
+ describe('everything else', () => {
129
+ test('a kind nobody described still has a name and a type', () => {
130
+ const odd = {
131
+ id: 'column_1',
132
+ type: 'column',
133
+ parentId: 'level_0',
134
+ name: 'Pillar',
135
+ } as unknown as AnyNode
136
+ const anonymous = { id: 'fence_1', type: 'fence', parentId: 'level_0' } as unknown as AnyNode
137
+
138
+ expect(describeTarget(odd, { ...nodes, column_1: odd }, metric)).toMatchObject({
139
+ kind: 'other',
140
+ name: 'Pillar',
141
+ type: 'column',
142
+ })
143
+ expect(describeTarget(anonymous, { ...nodes, fence_1: anonymous }, metric).name).toBe('Fence')
144
+ })
145
+
146
+ test('a roof segment is the kind "roof" and a stair the kind "stair"', () => {
147
+ const segment = {
148
+ id: 'roof_segment_1',
149
+ type: 'roof-segment',
150
+ parentId: 'roof_1',
151
+ width: 8,
152
+ depth: 6,
153
+ } as unknown as AnyNode
154
+ const stair = {
155
+ id: 'stair_1',
156
+ type: 'stair',
157
+ parentId: 'level_0',
158
+ width: 1,
159
+ totalRise: 2.7,
160
+ } as unknown as AnyNode
161
+
162
+ expect(describeTarget(segment, nodes, metric).kind).toBe('roof')
163
+ expect(describeTarget(stair, nodes, metric).kind).toBe('stair')
164
+ })
165
+ })