@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,199 @@
1
+ /**
2
+ * The stage is one or two panes, each showing one view: the 3D scene, the 2D
3
+ * plan, or a view a host or plugin registered (`registerEditorHostView`). A view
4
+ * sits in at most one pane — the 3D canvas owns a single WebGL context and the
5
+ * plan a single interaction session — so every write keeps the panes distinct.
6
+ *
7
+ * Each workspace keeps its own layout, so Studio can sit on 3D | Gallery while
8
+ * Edit stays on 2D | 3D.
9
+ */
10
+
11
+ export const VIEW_3D = '3d'
12
+ export const VIEW_2D = '2d'
13
+
14
+ export type ViewPaneIndex = 0 | 1
15
+
16
+ export type ViewLayout = {
17
+ split: boolean
18
+ /** `panes[1]` is what split opens beside `panes[0]`, also while unsplit. */
19
+ panes: readonly [string, string]
20
+ /** Width of pane 0 as a fraction of the stage. */
21
+ ratio: number
22
+ focus: ViewPaneIndex
23
+ }
24
+
25
+ export const MIN_VIEW_PANE_RATIO = 0.15
26
+ export const MAX_VIEW_PANE_RATIO = 0.85
27
+
28
+ export const DEFAULT_VIEW_LAYOUT: ViewLayout = {
29
+ split: false,
30
+ panes: [VIEW_3D, VIEW_2D],
31
+ ratio: 0.5,
32
+ focus: 0,
33
+ }
34
+
35
+ /** Capture and first person frame through the 3D camera, whatever the layout says. */
36
+ const SINGLE_3D_LAYOUT: ViewLayout = DEFAULT_VIEW_LAYOUT
37
+
38
+ export function clampViewPaneRatio(value: unknown): number {
39
+ if (!(typeof value === 'number' && Number.isFinite(value))) return DEFAULT_VIEW_LAYOUT.ratio
40
+ return Math.min(MAX_VIEW_PANE_RATIO, Math.max(MIN_VIEW_PANE_RATIO, value))
41
+ }
42
+
43
+ export function normalizeViewLayout(value: unknown): ViewLayout {
44
+ const candidate = value as Partial<ViewLayout> | null | undefined
45
+ const panes = candidate?.panes
46
+ const first = typeof panes?.[0] === 'string' && panes[0] ? panes[0] : VIEW_3D
47
+ let second = typeof panes?.[1] === 'string' && panes[1] ? panes[1] : VIEW_2D
48
+ if (second === first) second = first === VIEW_3D ? VIEW_2D : VIEW_3D
49
+ const split = candidate?.split === true
50
+ return {
51
+ split,
52
+ panes: [first, second],
53
+ ratio: clampViewPaneRatio(candidate?.ratio),
54
+ focus: split && candidate?.focus === 1 ? 1 : 0,
55
+ }
56
+ }
57
+
58
+ export function visibleViews(layout: ViewLayout): readonly string[] {
59
+ return layout.split ? layout.panes : [layout.panes[0]]
60
+ }
61
+
62
+ export function paneOfView(layout: ViewLayout, viewId: string): ViewPaneIndex | null {
63
+ if (layout.panes[0] === viewId) return 0
64
+ if (layout.split && layout.panes[1] === viewId) return 1
65
+ return null
66
+ }
67
+
68
+ /** Put `viewId` in `pane`; if the other pane holds it, the two trade places. */
69
+ export function withPaneView(layout: ViewLayout, pane: ViewPaneIndex, viewId: string): ViewLayout {
70
+ const target: ViewPaneIndex = layout.split ? pane : 0
71
+ const other: ViewPaneIndex = target === 0 ? 1 : 0
72
+ const panes: [string, string] = [layout.panes[0], layout.panes[1]]
73
+ if (panes[target] === viewId) return { ...layout, focus: target }
74
+ if (panes[other] === viewId) panes[other] = panes[target]
75
+ panes[target] = viewId
76
+ return { ...layout, panes, focus: target }
77
+ }
78
+
79
+ /**
80
+ * Show `viewId`. A view already on screen just takes focus; otherwise it
81
+ * replaces the focused pane, or with `beside` opens next to it.
82
+ */
83
+ export function withView(
84
+ layout: ViewLayout,
85
+ viewId: string,
86
+ options: { beside?: boolean } = {},
87
+ ): ViewLayout {
88
+ const pane = paneOfView(layout, viewId)
89
+ if (pane !== null) return { ...layout, focus: pane }
90
+ if (!options.beside) return withPaneView(layout, layout.focus, viewId)
91
+ if (!layout.split) return withPaneView({ ...layout, split: true }, 1, viewId)
92
+ return withPaneView(layout, layout.focus === 0 ? 1 : 0, viewId)
93
+ }
94
+
95
+ /**
96
+ * Opening the split from 3D brings the plan in on the left, where the split has
97
+ * always put it, with 3D keeping focus. Closing a split keeps the focused pane.
98
+ */
99
+ export function withSplitToggled(layout: ViewLayout): ViewLayout {
100
+ if (!layout.split) {
101
+ if (layout.panes[0] === VIEW_3D && layout.panes[1] === VIEW_2D) {
102
+ return { ...layout, split: true, panes: [VIEW_2D, VIEW_3D], focus: 1 }
103
+ }
104
+ return { ...layout, split: true }
105
+ }
106
+ const kept = layout.focus === 1 ? swapped(layout) : layout
107
+ return { ...kept, split: false, focus: 0 }
108
+ }
109
+
110
+ /**
111
+ * Close a view: a split collapses to the other pane; a single pane goes back to
112
+ * the scene.
113
+ */
114
+ export function withoutView(layout: ViewLayout, viewId: string): ViewLayout {
115
+ const pane = paneOfView(layout, viewId)
116
+ if (pane === null) {
117
+ if (layout.panes[1] !== viewId) return layout
118
+ return { ...layout, panes: [layout.panes[0], layout.panes[0] === VIEW_3D ? VIEW_2D : VIEW_3D] }
119
+ }
120
+ const kept = layout.split ? layout.panes[pane === 0 ? 1 : 0] : VIEW_3D
121
+ return { ...layout, split: false, panes: [kept, kept === VIEW_3D ? VIEW_2D : VIEW_3D], focus: 0 }
122
+ }
123
+
124
+ export function swapped(layout: ViewLayout): ViewLayout {
125
+ return {
126
+ ...layout,
127
+ panes: [layout.panes[1], layout.panes[0]],
128
+ ratio: 1 - layout.ratio,
129
+ focus: layout.focus === 0 ? 1 : 0,
130
+ }
131
+ }
132
+
133
+ type ViewLayoutState = {
134
+ viewLayouts: Readonly<Record<string, ViewLayout>>
135
+ workspaceMode: string
136
+ isCaptureMode: boolean
137
+ isFirstPersonMode: boolean
138
+ }
139
+
140
+ export function storedViewLayout(state: Pick<ViewLayoutState, 'viewLayouts' | 'workspaceMode'>) {
141
+ return state.viewLayouts[state.workspaceMode] ?? DEFAULT_VIEW_LAYOUT
142
+ }
143
+
144
+ /** The layout on screen: the workspace's own, unless capture or first person needs 3D alone. */
145
+ export function activeViewLayout(state: ViewLayoutState): ViewLayout {
146
+ if (state.isCaptureMode || state.isFirstPersonMode) return SINGLE_3D_LAYOUT
147
+ return storedViewLayout(state)
148
+ }
149
+
150
+ export function isViewVisible(state: ViewLayoutState, viewId: string): boolean {
151
+ return paneOfView(activeViewLayout(state), viewId) !== null
152
+ }
153
+
154
+ export type VisibleScene = '3d' | '2d' | 'split'
155
+
156
+ /** A layout showing just the scene: 3D, the plan, or the plan beside 3D (the split as it always was). */
157
+ export function sceneLayout(scene: VisibleScene, ratio = DEFAULT_VIEW_LAYOUT.ratio): ViewLayout {
158
+ if (scene === '3d') return DEFAULT_VIEW_LAYOUT
159
+ return {
160
+ split: scene === 'split',
161
+ panes: [VIEW_2D, VIEW_3D],
162
+ ratio: clampViewPaneRatio(ratio),
163
+ focus: 0,
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Which projections of the scene are on screen — 3D alone, the plan alone, or
169
+ * both side by side — or `null` while the panes show other views only. The
170
+ * code that hands input between the canvas and the plan reads this: a plan
171
+ * beside a gallery owns input exactly as a plan alone does.
172
+ */
173
+ export function visibleScene(state: ViewLayoutState): VisibleScene | null {
174
+ const layout = activeViewLayout(state)
175
+ const has3d = paneOfView(layout, VIEW_3D) !== null
176
+ const has2d = paneOfView(layout, VIEW_2D) !== null
177
+ if (has3d && has2d) return 'split'
178
+ if (has3d) return '3d'
179
+ if (has2d) return '2d'
180
+ return null
181
+ }
182
+
183
+ /**
184
+ * Layouts saved before views existed stored one `viewMode` (`'3d' | '2d' |
185
+ * 'split'`, the plan always on the left) and the plan's width.
186
+ */
187
+ export function viewLayoutFromLegacy(legacy: {
188
+ viewMode?: unknown
189
+ isFloorplanOpen?: unknown
190
+ floorplanPaneRatio?: unknown
191
+ }): ViewLayout {
192
+ const viewMode =
193
+ legacy.viewMode === '2d' || legacy.viewMode === '3d' || legacy.viewMode === 'split'
194
+ ? legacy.viewMode
195
+ : legacy.isFloorplanOpen === true
196
+ ? 'split'
197
+ : '3d'
198
+ return sceneLayout(viewMode, clampViewPaneRatio(legacy.floorplanPaneRatio))
199
+ }
@@ -0,0 +1,16 @@
1
+ export function installImmediateAnimationFrames(): () => void {
2
+ const names = ['requestAnimationFrame', 'cancelAnimationFrame'] as const
3
+ const previous = names.map((name) => Object.getOwnPropertyDescriptor(globalThis, name))
4
+ globalThis.requestAnimationFrame = (callback) => {
5
+ callback(0)
6
+ return 0
7
+ }
8
+ globalThis.cancelAnimationFrame = () => {}
9
+ return () => {
10
+ for (const [index, name] of names.entries()) {
11
+ const descriptor = previous[index]
12
+ if (descriptor) Object.defineProperty(globalThis, name, descriptor)
13
+ else Reflect.deleteProperty(globalThis, name)
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,224 @@
1
+ import { expect } from 'bun:test'
2
+ import {
3
+ type AnyNodeId,
4
+ acquireSceneHistoryPause,
5
+ BuildingNode,
6
+ createZone,
7
+ DoorNode,
8
+ generateId,
9
+ getSceneHistoryPauseDepth,
10
+ initSpaceDetectionSync,
11
+ LevelNode,
12
+ useLiveNodeOverrides,
13
+ useScene,
14
+ type WallNode,
15
+ } from '@pascal-app/core'
16
+ import { commitRoomElevation } from '../lib/room-handle-drag'
17
+ import { applyRoomPlan } from '../lib/room-structure-commands'
18
+
19
+ /**
20
+ * The harness scene, reconciled live: a 6 × 4 m room (`zoneId`, its east wall
21
+ * at x = 6 the one gestures move) and, west of it through a door, a room raised
22
+ * 0.45 m (`raisedZoneId`) whose floor steps down through the doorway. Returns
23
+ * the reconciler's stop.
24
+ */
25
+ export function seedRoundTripScene(buildingId: string, levelId: string) {
26
+ const building = BuildingNode.parse({ id: buildingId, children: [levelId] })
27
+ const level = LevelNode.parse({ id: levelId, parentId: building.id })
28
+ useScene.setState({
29
+ nodes: { [building.id]: building, [level.id]: level },
30
+ rootNodeIds: [building.id],
31
+ dirtyNodes: new Set(),
32
+ materials: {},
33
+ collections: {},
34
+ readOnly: false,
35
+ })
36
+ const stop = initSpaceDetectionSync(useScene, {
37
+ getState: () => ({ spaces: {}, setSpaces: () => {} }),
38
+ })
39
+ const room = (polygon: [number, number][]) => {
40
+ const plan = createZone(useScene.getState().nodes, {
41
+ levelId,
42
+ polygon,
43
+ enclose: true,
44
+ mintId: generateId,
45
+ })
46
+ applyRoomPlan(plan)
47
+ return plan.zoneId
48
+ }
49
+ const zoneId = room([
50
+ [0, 0],
51
+ [6, 0],
52
+ [6, 4],
53
+ [0, 4],
54
+ ])
55
+ const raisedZoneId = room([
56
+ [-4, 0],
57
+ [0, 0],
58
+ [0, 4],
59
+ [-4, 4],
60
+ ])
61
+ const shared = Object.values(useScene.getState().nodes).find(
62
+ (n): n is WallNode => n.type === 'wall' && n.start[0] === 0 && n.end[0] === 0,
63
+ )!
64
+ const door = DoorNode.parse({ parentId: shared.id, wallId: shared.id, position: [2, 0, 0] })
65
+ useScene.getState().applyNodeChanges({ create: [{ node: door, parentId: shared.id }] })
66
+ commitRoomElevation(raisedZoneId, 0.45)
67
+ return { stop, zoneId, raisedZoneId }
68
+ }
69
+
70
+ /**
71
+ * A live gesture as the harness drives it: `start` begins it and moves it far
72
+ * enough that its preview reshapes the building (derived plates, ceilings,
73
+ * zones included); then either `cancel` or `commit` ends it.
74
+ */
75
+ export type LiveGesture = {
76
+ start: () => unknown
77
+ cancel: () => unknown
78
+ commit: () => unknown
79
+ /** Whether the preview ran (an override, a ghost…), checked after `start`. */
80
+ previewed?: () => boolean
81
+ }
82
+
83
+ /** Every node, as plain data (JSON also folds -0 into 0, as the transport does). */
84
+ export const sceneData = (): Record<string, unknown> =>
85
+ JSON.parse(JSON.stringify(useScene.getState().nodes))
86
+
87
+ function expectReleased() {
88
+ expect([...useLiveNodeOverrides.getState().overrides.keys()]).toEqual([])
89
+ expect(getSceneHistoryPauseDepth()).toBe(0)
90
+ expect(useScene.temporal.getState().isTracking).toBe(true)
91
+ }
92
+
93
+ /**
94
+ * start → move → cancel leaves every node deep-equal to before (derived ones
95
+ * included), no history entry, history tracking again, no override left, and
96
+ * every node the preview drew from an override marked to rebuild — else its
97
+ * mesh stays at the preview.
98
+ */
99
+ export async function expectCancelRestores(gesture: LiveGesture) {
100
+ const before = sceneData()
101
+ const past = useScene.temporal.getState().pastStates.length
102
+ await gesture.start()
103
+ if (gesture.previewed) expect(gesture.previewed()).toBe(true)
104
+ const drawnFromPreview = [...useLiveNodeOverrides.getState().overrides.keys()] as AnyNodeId[]
105
+ useScene.getState().dirtyNodes.clear()
106
+ await gesture.cancel()
107
+ expect(sceneData()).toEqual(before)
108
+ expect(useScene.temporal.getState().pastStates.length).toBe(past)
109
+ expectReleased()
110
+ const { nodes, dirtyNodes, markDirty } = useScene.getState()
111
+ const stale = drawnFromPreview.filter((id) => {
112
+ if (!nodes[id] || dirtyNodes.has(id)) return false
113
+ // A kind that opts out of dirty tracking redraws from the override store itself.
114
+ markDirty(id)
115
+ return dirtyNodes.has(id)
116
+ })
117
+ expect(stale).toEqual([])
118
+ }
119
+
120
+ /**
121
+ * start → move → commit writes one undo step that reshapes derived nodes of
122
+ * `derivedTypes` too, releases everything, and one undo brings every node back.
123
+ */
124
+ export async function expectCommitIsOneUndoStep(
125
+ gesture: LiveGesture,
126
+ derivedTypes: readonly string[] = ['slab', 'zone'],
127
+ ) {
128
+ const before = sceneData()
129
+ const past = useScene.temporal.getState().pastStates.length
130
+ await gesture.start()
131
+ await gesture.commit()
132
+ const after = sceneData()
133
+ expect(useScene.temporal.getState().pastStates.length).toBe(past + 1)
134
+ expectReleased()
135
+ const changedTypes = new Set(
136
+ Object.keys({ ...before, ...after })
137
+ .filter((id) => JSON.stringify(before[id]) !== JSON.stringify(after[id]))
138
+ .map((id) => ((after[id] ?? before[id]) as { type: string }).type),
139
+ )
140
+ for (const type of derivedTypes) expect([...changedTypes]).toContain(type)
141
+ useScene.temporal.getState().undo()
142
+ expect(sceneData()).toEqual(before)
143
+ }
144
+
145
+ /**
146
+ * During the drag only the surfaces (plates, ceilings, zones) the release
147
+ * reshapes are drawn from a preview, and each is drawn exactly as the release
148
+ * leaves it — the floors it never touches keep their stored shape.
149
+ */
150
+ export async function expectPreviewMatchesCommit(
151
+ gesture: LiveGesture,
152
+ surfaceTypes: readonly string[] = ['slab', 'ceiling', 'zone'],
153
+ ) {
154
+ const before = sceneData() as Record<string, Record<string, unknown>>
155
+ await gesture.start()
156
+ const previews = new Map(
157
+ [...useLiveNodeOverrides.getState().overrides].filter(([id]) =>
158
+ surfaceTypes.includes(before[id]?.type as string),
159
+ ),
160
+ )
161
+ await gesture.commit()
162
+ const after = sceneData() as Record<string, Record<string, unknown>>
163
+ const json = (value: unknown) => JSON.stringify(value ?? null)
164
+ const untouched = [...previews.keys()].filter(
165
+ (id) => after[id] && json(before[id]) === json(after[id]),
166
+ )
167
+ expect(untouched).toEqual([])
168
+ const drawn: Record<string, Record<string, string>> = {}
169
+ const committed: Record<string, Record<string, string>> = {}
170
+ for (const [id, values] of previews) {
171
+ if (!after[id]) continue
172
+ drawn[id] = {}
173
+ committed[id] = {}
174
+ for (const key of Object.keys(values)) {
175
+ drawn[id][key] = json(values[key])
176
+ committed[id][key] = json(after[id][key])
177
+ }
178
+ }
179
+ expect(drawn).toEqual(committed)
180
+ const reshapedUnseen = Object.keys(after).filter(
181
+ (id) =>
182
+ before[id] &&
183
+ surfaceTypes.includes(after[id]!.type as string) &&
184
+ (json(before[id].polygon) !== json(after[id]!.polygon) ||
185
+ json(before[id].holes) !== json(after[id]!.holes)) &&
186
+ !previews.get(id)?.polygon,
187
+ )
188
+ expect(reshapedUnseen).toEqual([])
189
+ }
190
+
191
+ /**
192
+ * A collaborator's edit landing mid-gesture (here: a field the gesture never
193
+ * writes) survives both the cancel and the commit. `target` names the node,
194
+ * read once the gesture has started.
195
+ */
196
+ export async function expectRemoteEditSurvives(
197
+ gesture: () => LiveGesture,
198
+ target: () => AnyNodeId,
199
+ patch: Record<string, unknown> = { name: 'Edited by a collaborator' },
200
+ ) {
201
+ const remote = (id: AnyNodeId) => {
202
+ const nodes = useScene.getState().nodes
203
+ const release = acquireSceneHistoryPause(useScene)
204
+ try {
205
+ useScene.setState({ nodes: { ...nodes, [id]: { ...nodes[id]!, ...patch } } })
206
+ } finally {
207
+ release()
208
+ }
209
+ }
210
+ for (const end of ['cancel', 'commit'] as const) {
211
+ const run = gesture()
212
+ const before = sceneData() as Record<string, Record<string, unknown>>
213
+ await run.start()
214
+ const id = target()
215
+ remote(id)
216
+ await run[end]()
217
+ const after = sceneData() as Record<string, Record<string, unknown>>
218
+ expect({ end, ...pick(after[id], patch) }).toEqual({ end, ...patch })
219
+ if (end === 'cancel') expect(after).toEqual({ ...before, [id]: { ...before[id], ...patch } })
220
+ }
221
+ }
222
+
223
+ const pick = (node: Record<string, unknown> | undefined, patch: Record<string, unknown>) =>
224
+ Object.fromEntries(Object.keys(patch).map((key) => [key, node?.[key]]))
@@ -0,0 +1,16 @@
1
+ import { spyOn } from 'bun:test'
2
+ import React, { type ReactElement } from 'react'
3
+ import { renderToStaticMarkup } from 'react-dom/server'
4
+
5
+ /** Server-renders `element` from the stores' current state, not the startup state a server render reads. */
6
+ export function renderCurrentMarkup(element: ReactElement): string {
7
+ const useSyncExternalStore = React.useSyncExternalStore
8
+ const serverSnapshot = spyOn(React, 'useSyncExternalStore').mockImplementation(
9
+ (subscribe, getSnapshot) => useSyncExternalStore(subscribe, getSnapshot, getSnapshot),
10
+ )
11
+ try {
12
+ return renderToStaticMarkup(element)
13
+ } finally {
14
+ serverSnapshot.mockRestore()
15
+ }
16
+ }
@@ -0,0 +1,82 @@
1
+ import { useScene } from '@pascal-app/core'
2
+ import { useViewer } from '@pascal-app/viewer'
3
+ import { _roots, act, createRoot, extend } from '@react-three/fiber'
4
+ import type { ReactNode } from 'react'
5
+ import * as THREE from 'three'
6
+ import useEditor from '../store/use-editor'
7
+ import useInteractionScope from '../store/use-interaction-scope'
8
+ import useSessionGroups from '../store/use-session-groups'
9
+
10
+ extend({
11
+ Group: THREE.Group,
12
+ Mesh: THREE.Mesh,
13
+ LineSegments: THREE.LineSegments,
14
+ CircleGeometry: THREE.CircleGeometry,
15
+ CylinderGeometry: THREE.CylinderGeometry,
16
+ PlaneGeometry: THREE.PlaneGeometry,
17
+ MeshBasicMaterial: THREE.MeshBasicMaterial,
18
+ LineBasicMaterial: THREE.LineBasicMaterial,
19
+ })
20
+
21
+ export async function withSelectionHarness(
22
+ run: (harness: {
23
+ render: (node: ReactNode) => Promise<void>
24
+ canvas: HTMLCanvasElement
25
+ }) => Promise<void>,
26
+ ) {
27
+ const scene = useScene.getState()
28
+ const viewer = useViewer.getState()
29
+ const editor = useEditor.getState()
30
+ const scope = useInteractionScope.getState()
31
+ const groups = useSessionGroups.getState()
32
+ const previousWindow = globalThis.window
33
+ const previousRaf = globalThis.requestAnimationFrame
34
+ const actGlobal = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
35
+ const previousAct = actGlobal.IS_REACT_ACT_ENVIRONMENT
36
+ actGlobal.IS_REACT_ACT_ENVIRONMENT = true
37
+ globalThis.window = new EventTarget() as Window & typeof globalThis
38
+ globalThis.requestAnimationFrame = () => 0
39
+ const canvas = Object.assign(new EventTarget(), {
40
+ style: { cursor: '' },
41
+ getBoundingClientRect: () => ({ left: 0, top: 0, width: 1000, height: 1000 }),
42
+ }) as unknown as HTMLCanvasElement
43
+ const root = createRoot(canvas)
44
+ try {
45
+ const camera = Object.assign(new THREE.OrthographicCamera(-1, 9, 9, -1, 0.1, 100), {
46
+ manual: true,
47
+ })
48
+ camera.position.set(0, 10, 0)
49
+ camera.up.set(0, 0, -1)
50
+ camera.lookAt(0, 0, 0)
51
+ camera.updateMatrixWorld(true)
52
+ await root.configure({
53
+ gl: {
54
+ domElement: canvas,
55
+ render() {},
56
+ setSize() {},
57
+ setPixelRatio() {},
58
+ } as unknown as THREE.WebGLRenderer,
59
+ camera,
60
+ frameloop: 'never',
61
+ dpr: 1,
62
+ size: { width: 1000, height: 1000, top: 0, left: 0 },
63
+ })
64
+ await run({
65
+ render: async (node) => {
66
+ await act(async () => root.render(node))
67
+ },
68
+ canvas,
69
+ })
70
+ } finally {
71
+ await act(async () => root.render(null))
72
+ _roots.delete(canvas)
73
+ useScene.setState(scene)
74
+ useViewer.setState(viewer)
75
+ useEditor.setState(editor)
76
+ useInteractionScope.setState(scope)
77
+ useSessionGroups.setState(groups)
78
+ globalThis.window = previousWindow
79
+ globalThis.requestAnimationFrame = previousRaf
80
+ actGlobal.IS_REACT_ACT_ENVIRONMENT = previousAct
81
+ }
82
+ }