@pascal-app/editor 1.0.1 → 1.0.4

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 (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,139 @@
1
+ import { type AnyNodeId, useScene } from '@pascal-app/core'
2
+ import { useViewer } from '@pascal-app/viewer'
3
+ import { create } from 'zustand'
4
+ import { hasLiveGestures } from '../lib/gesture-lifecycle'
5
+ import { removeWallRegion } from '../lib/paint-regions'
6
+ import { sfxEmitter } from '../lib/sfx-bus'
7
+ import { isTypingTarget } from '../lib/typing-target'
8
+ import useDeleteConfirmation from './use-delete-confirmation'
9
+ import useInteractionScope from './use-interaction-scope'
10
+
11
+ // The paint region the user last picked on the selected wall (a handle, an
12
+ // outline or a panel row). While it is set, Delete removes that region instead
13
+ // of the wall. Cleared whenever the wall selection changes.
14
+ //
15
+ // `stale` remembers an active region that vanished under the user (undo, another
16
+ // edit) while its wall stayed selected: the next Delete is swallowed rather than
17
+ // deleting the wall the user was not aiming at.
18
+
19
+ export type ActiveWallRegion = { wallId: string; regionId: string }
20
+
21
+ type WallRegionSelectionState = {
22
+ active: ActiveWallRegion | null
23
+ stale: ActiveWallRegion | null
24
+ setActive: (active: ActiveWallRegion | null) => void
25
+ setStale: (stale: ActiveWallRegion | null) => void
26
+ }
27
+
28
+ export const useWallRegionSelection = create<WallRegionSelectionState>((set) => ({
29
+ active: null,
30
+ stale: null,
31
+ setActive: (active) =>
32
+ set((state) =>
33
+ state.active?.wallId === active?.wallId && state.active?.regionId === active?.regionId
34
+ ? state
35
+ : active
36
+ ? { active, stale: null }
37
+ : { active },
38
+ ),
39
+ setStale: (stale) => set((state) => (state.stale === stale ? state : { stale })),
40
+ }))
41
+
42
+ function isSoleSelection(wallId: string, selectedIds: readonly string[]) {
43
+ return selectedIds.length === 1 && selectedIds[0] === wallId
44
+ }
45
+
46
+ function regionExists(wallId: string, regionId: string) {
47
+ const wall = useScene.getState().nodes[wallId as AnyNodeId]
48
+ if (wall?.type !== 'wall') return false
49
+ return wall.faceRegions?.some((region) => region.id === regionId) ?? false
50
+ }
51
+
52
+ /** The active region, only while its wall is the sole selection and the region still exists. */
53
+ export function resolveActiveWallRegion(): ActiveWallRegion | null {
54
+ const active = useWallRegionSelection.getState().active
55
+ if (!active) return null
56
+ if (!isSoleSelection(active.wallId, useViewer.getState().selection.selectedIds)) return null
57
+ return regionExists(active.wallId, active.regionId) ? active : null
58
+ }
59
+
60
+ function resolveStaleWallRegion(): ActiveWallRegion | null {
61
+ const stale = useWallRegionSelection.getState().stale
62
+ if (!stale) return null
63
+ return isSoleSelection(stale.wallId, useViewer.getState().selection.selectedIds) ? stale : null
64
+ }
65
+
66
+ /** Clears the active (and stale) region unless the selection is still exactly its wall. */
67
+ export function syncWallRegionSelection(selectedIds: readonly string[]) {
68
+ const { active, stale, setActive, setStale } = useWallRegionSelection.getState()
69
+ if (active && !isSoleSelection(active.wallId, selectedIds)) setActive(null)
70
+ if (stale && !isSoleSelection(stale.wallId, selectedIds)) setStale(null)
71
+ }
72
+
73
+ /** Drops the active region once it no longer exists in the scene. */
74
+ export function syncWallRegionWithScene() {
75
+ const { active, setActive, setStale } = useWallRegionSelection.getState()
76
+ if (!active || regionExists(active.wallId, active.regionId)) return
77
+ setActive(null)
78
+ const wallStillThere = useScene.getState().nodes[active.wallId as AnyNodeId]?.type === 'wall'
79
+ const selectedIds = useViewer.getState().selection.selectedIds
80
+ if (wallStillThere && isSoleSelection(active.wallId, selectedIds)) setStale(active)
81
+ }
82
+
83
+ /** Keeps the active region in step with scene edits (undo, remote edits). Returns the unsubscribe. */
84
+ export function installWallRegionSceneSync() {
85
+ return useScene.subscribe((state, previous) => {
86
+ if (state.nodes !== previous.nodes) syncWallRegionWithScene()
87
+ })
88
+ }
89
+
90
+ // Delete belongs to the region only when focus is on the scene or the wall's
91
+ // region list; a focused toolbar button or panel control keeps its own key.
92
+ function isSceneOrRegionListTarget(target: EventTarget | null) {
93
+ if (!target) return true
94
+ if (
95
+ typeof document !== 'undefined' &&
96
+ (target === document || target === document.body || target === document.documentElement)
97
+ ) {
98
+ return true
99
+ }
100
+ const element = target as { tagName?: string; closest?: (selector: string) => unknown }
101
+ if (element.tagName?.toUpperCase() === 'CANVAS') return true
102
+ return !!element.closest?.('[data-wall-region-list]')
103
+ }
104
+
105
+ function consume(event: KeyEventLike) {
106
+ event.preventDefault()
107
+ event.stopPropagation()
108
+ }
109
+
110
+ type KeyEventLike = Pick<KeyboardEvent, 'key' | 'target' | 'preventDefault' | 'stopPropagation'>
111
+
112
+ /**
113
+ * Delete / Backspace with an active region removes that region (one undo step)
114
+ * and consumes the key so the global delete never sees it. Mid-gesture, or
115
+ * right after the active region vanished, the key is swallowed so it cannot
116
+ * fall through to deleting the wall. Returns whether the key was consumed.
117
+ */
118
+ export function handleWallRegionDeleteKey(event: KeyEventLike): boolean {
119
+ if (event.key !== 'Delete' && event.key !== 'Backspace') return false
120
+ if (isTypingTarget(event.target)) return false
121
+ if (!isSceneOrRegionListTarget(event.target)) return false
122
+ if (useDeleteConfirmation.getState().request) return false
123
+ const active = resolveActiveWallRegion()
124
+ const stale = active ? null : resolveStaleWallRegion()
125
+ if (!(active || stale)) return false
126
+
127
+ consume(event)
128
+ const busy = useInteractionScope.getState().scope.kind !== 'idle' || hasLiveGestures()
129
+ if (busy) return true
130
+ if (stale) {
131
+ useWallRegionSelection.getState().setStale(null)
132
+ return true
133
+ }
134
+ if (!active) return true
135
+ // Cleared first so the scene sync does not mistake this removal for a vanished region.
136
+ useWallRegionSelection.getState().setActive(null)
137
+ if (removeWallRegion(active.wallId, active.regionId)) sfxEmitter.emit('sfx:structure-delete')
138
+ return true
139
+ }
@@ -0,0 +1,175 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { availableViews, editorHostViewRegistry, registerEditorHostView } from '../lib/editor-views'
3
+ import useEditor, { normalizePersistedEditorUiState } from './use-editor'
4
+ import {
5
+ activeViewLayout,
6
+ DEFAULT_VIEW_LAYOUT,
7
+ isViewVisible,
8
+ sceneLayout,
9
+ VIEW_2D,
10
+ VIEW_3D,
11
+ visibleScene,
12
+ } from './view-layout'
13
+
14
+ function layout() {
15
+ return activeViewLayout(useEditor.getState())
16
+ }
17
+
18
+ afterEach(() => {
19
+ useEditor.getState().setCaptureMode(false)
20
+ useEditor.getState().setFirstPersonMode(false)
21
+ useEditor.getState().setWorkspaceMode('edit')
22
+ useEditor.setState({ viewLayouts: {} })
23
+ editorHostViewRegistry.reset()
24
+ })
25
+
26
+ describe('the stage layout', () => {
27
+ test('a view sits in one pane: picking the other pane’s view swaps them', () => {
28
+ useEditor.getState().setViewLayout(sceneLayout('split'))
29
+ useEditor.getState().setPaneView(0, VIEW_3D)
30
+ expect(layout().panes).toEqual([VIEW_3D, VIEW_2D])
31
+ expect(layout().split).toBe(true)
32
+ })
33
+
34
+ test('a view opened beside the scene splits next to the focused pane', () => {
35
+ useEditor.getState().showView('gallery', { beside: true })
36
+ expect(layout()).toMatchObject({ split: true, panes: [VIEW_3D, 'gallery'], focus: 1 })
37
+ expect(visibleScene(useEditor.getState())).toBe('3d')
38
+ })
39
+
40
+ test('opening a view already on screen only focuses it', () => {
41
+ useEditor.getState().setViewLayout(sceneLayout('split'))
42
+ useEditor.getState().showView(VIEW_3D)
43
+ expect(layout()).toMatchObject({ panes: [VIEW_2D, VIEW_3D], focus: 1, split: true })
44
+ })
45
+
46
+ test('splitting from 3D brings the plan in on the left, as the split always did', () => {
47
+ useEditor.getState().toggleSplit()
48
+ expect(layout()).toMatchObject({ split: true, panes: [VIEW_2D, VIEW_3D], focus: 1 })
49
+ })
50
+
51
+ test('closing the split keeps the focused pane', () => {
52
+ useEditor.getState().setViewLayout(sceneLayout('split'))
53
+ useEditor.getState().focusViewPane(1)
54
+ useEditor.getState().toggleSplit()
55
+ expect(layout()).toMatchObject({ split: false, panes: [VIEW_3D, VIEW_2D] })
56
+ })
57
+
58
+ test('closing a view collapses its split, or brings a lone pane back to 3D', () => {
59
+ useEditor.getState().showView('gallery', { beside: true })
60
+ useEditor.getState().closeView('gallery')
61
+ expect(layout()).toMatchObject({ split: false, panes: [VIEW_3D, VIEW_2D] })
62
+ useEditor.getState().setViewLayout(sceneLayout('2d'))
63
+ useEditor.getState().showView('gallery')
64
+ useEditor.getState().closeView('gallery')
65
+ expect(visibleScene(useEditor.getState())).toBe('3d')
66
+ })
67
+
68
+ test('a plan beside another view owns the scene like a plan alone', () => {
69
+ useEditor.getState().setViewLayout(sceneLayout('2d'))
70
+ useEditor.getState().showView('gallery', { beside: true })
71
+ expect(visibleScene(useEditor.getState())).toBe('2d')
72
+ })
73
+
74
+ test('each workspace keeps its own layout', () => {
75
+ useEditor.getState().setViewLayout(sceneLayout('split'))
76
+ useEditor.getState().setWorkspaceMode('studio')
77
+ expect(layout()).toEqual(DEFAULT_VIEW_LAYOUT)
78
+ useEditor.getState().showView('gallery')
79
+ useEditor.getState().setWorkspaceMode('edit')
80
+ expect(visibleScene(useEditor.getState())).toBe('split')
81
+ })
82
+
83
+ test('capture frames in 3D alone and gives the layout back untouched', () => {
84
+ useEditor.getState().setViewLayout(sceneLayout('2d'))
85
+ useEditor.getState().setCaptureMode(true)
86
+ expect(visibleScene(useEditor.getState())).toBe('3d')
87
+ useEditor.getState().setCaptureMode(false)
88
+ expect(visibleScene(useEditor.getState())).toBe('2d')
89
+ })
90
+ })
91
+
92
+ describe('the deprecated viewMode for plugins written before views', () => {
93
+ test('mirrors the scene on screen and sets it', () => {
94
+ useEditor.getState().setViewMode('split')
95
+ expect(visibleScene(useEditor.getState())).toBe('split')
96
+ expect(useEditor.getState().viewMode).toBe('split')
97
+ useEditor.getState().showView('gallery')
98
+ expect(useEditor.getState().viewMode).toBe('3d')
99
+ useEditor.getState().setCaptureMode(true)
100
+ expect(useEditor.getState().viewMode).toBe('3d')
101
+ })
102
+ })
103
+
104
+ describe('layouts saved before views', () => {
105
+ test.each([
106
+ ['3d', '3d'],
107
+ ['2d', '2d'],
108
+ ['split', 'split'],
109
+ ] as const)('a stored %s view opens as %s, the plan on the left', (viewMode, scene) => {
110
+ const state = normalizePersistedEditorUiState({ viewMode, floorplanPaneRatio: 0.4 } as never)
111
+ const restored = state.viewLayouts.edit
112
+ expect(restored).toBeDefined()
113
+ if (!restored) return
114
+ expect(
115
+ visibleScene({
116
+ viewLayouts: state.viewLayouts,
117
+ workspaceMode: 'edit',
118
+ isCaptureMode: false,
119
+ isFirstPersonMode: false,
120
+ }),
121
+ ).toBe(scene)
122
+ if (scene !== '3d') expect(restored).toMatchObject({ panes: [VIEW_2D, VIEW_3D], ratio: 0.4 })
123
+ })
124
+
125
+ test('an open floorplan from before view modes opens split', () => {
126
+ const state = normalizePersistedEditorUiState({ isFloorplanOpen: true } as never)
127
+ expect(state.viewLayouts.edit).toMatchObject({ split: true })
128
+ })
129
+ })
130
+
131
+ describe('registered views', () => {
132
+ test('a plugin view is offered only where its plugin is installed and its workspace matches', () => {
133
+ registerEditorHostView({
134
+ id: 'acme:pets',
135
+ label: 'Pets',
136
+ icon: { kind: 'iconify', name: 'lucide:cat' },
137
+ component: async () => ({ default: () => null }),
138
+ pluginId: 'acme',
139
+ })
140
+ registerEditorHostView({
141
+ id: 'acme:habitat',
142
+ label: 'Habitat',
143
+ icon: { kind: 'iconify', name: 'lucide:house' },
144
+ component: async () => ({ default: () => null }),
145
+ pluginId: 'acme',
146
+ workspaces: ['studio'],
147
+ })
148
+ const ids = (workspaceMode: string, installedPlugins: string[]) =>
149
+ availableViews({
150
+ registered: editorHostViewRegistry.getSnapshot(),
151
+ workspaceMode,
152
+ installedPlugins,
153
+ }).map((view) => view.id)
154
+ expect(ids('edit', [])).toEqual([VIEW_3D, VIEW_2D])
155
+ expect(ids('edit', ['acme'])).toEqual([VIEW_3D, VIEW_2D, 'acme:pets'])
156
+ expect(ids('studio', ['acme'])).toEqual([VIEW_3D, VIEW_2D, 'acme:habitat'])
157
+ })
158
+
159
+ test('the scene views cannot be replaced', () => {
160
+ expect(() =>
161
+ registerEditorHostView({
162
+ id: VIEW_3D,
163
+ label: '3D',
164
+ icon: { kind: 'iconify', name: 'lucide:box' },
165
+ component: async () => ({ default: () => null }),
166
+ }),
167
+ ).toThrow()
168
+ })
169
+
170
+ test('a layout naming a view that is gone keeps it, so the choice returns with the view', () => {
171
+ useEditor.getState().showView('acme:pets')
172
+ expect(isViewVisible(useEditor.getState(), 'acme:pets')).toBe(true)
173
+ expect(isViewVisible(useEditor.getState(), VIEW_3D)).toBe(false)
174
+ })
175
+ })
@@ -0,0 +1,196 @@
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) return layout
117
+ const kept = layout.split ? layout.panes[pane === 0 ? 1 : 0] : VIEW_3D
118
+ return { ...layout, split: false, panes: [kept, kept === VIEW_3D ? VIEW_2D : VIEW_3D], focus: 0 }
119
+ }
120
+
121
+ export function swapped(layout: ViewLayout): ViewLayout {
122
+ return {
123
+ ...layout,
124
+ panes: [layout.panes[1], layout.panes[0]],
125
+ ratio: 1 - layout.ratio,
126
+ focus: layout.focus === 0 ? 1 : 0,
127
+ }
128
+ }
129
+
130
+ type ViewLayoutState = {
131
+ viewLayouts: Readonly<Record<string, ViewLayout>>
132
+ workspaceMode: string
133
+ isCaptureMode: boolean
134
+ isFirstPersonMode: boolean
135
+ }
136
+
137
+ export function storedViewLayout(state: Pick<ViewLayoutState, 'viewLayouts' | 'workspaceMode'>) {
138
+ return state.viewLayouts[state.workspaceMode] ?? DEFAULT_VIEW_LAYOUT
139
+ }
140
+
141
+ /** The layout on screen: the workspace's own, unless capture or first person needs 3D alone. */
142
+ export function activeViewLayout(state: ViewLayoutState): ViewLayout {
143
+ if (state.isCaptureMode || state.isFirstPersonMode) return SINGLE_3D_LAYOUT
144
+ return storedViewLayout(state)
145
+ }
146
+
147
+ export function isViewVisible(state: ViewLayoutState, viewId: string): boolean {
148
+ return paneOfView(activeViewLayout(state), viewId) !== null
149
+ }
150
+
151
+ export type VisibleScene = '3d' | '2d' | 'split'
152
+
153
+ /** A layout showing just the scene: 3D, the plan, or the plan beside 3D (the split as it always was). */
154
+ export function sceneLayout(scene: VisibleScene, ratio = DEFAULT_VIEW_LAYOUT.ratio): ViewLayout {
155
+ if (scene === '3d') return DEFAULT_VIEW_LAYOUT
156
+ return {
157
+ split: scene === 'split',
158
+ panes: [VIEW_2D, VIEW_3D],
159
+ ratio: clampViewPaneRatio(ratio),
160
+ focus: 0,
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Which projections of the scene are on screen — 3D alone, the plan alone, or
166
+ * both side by side — or `null` while the panes show other views only. The
167
+ * code that hands input between the canvas and the plan reads this: a plan
168
+ * beside a gallery owns input exactly as a plan alone does.
169
+ */
170
+ export function visibleScene(state: ViewLayoutState): VisibleScene | null {
171
+ const layout = activeViewLayout(state)
172
+ const has3d = paneOfView(layout, VIEW_3D) !== null
173
+ const has2d = paneOfView(layout, VIEW_2D) !== null
174
+ if (has3d && has2d) return 'split'
175
+ if (has3d) return '3d'
176
+ if (has2d) return '2d'
177
+ return null
178
+ }
179
+
180
+ /**
181
+ * Layouts saved before views existed stored one `viewMode` (`'3d' | '2d' |
182
+ * 'split'`, the plan always on the left) and the plan's width.
183
+ */
184
+ export function viewLayoutFromLegacy(legacy: {
185
+ viewMode?: unknown
186
+ isFloorplanOpen?: unknown
187
+ floorplanPaneRatio?: unknown
188
+ }): ViewLayout {
189
+ const viewMode =
190
+ legacy.viewMode === '2d' || legacy.viewMode === '3d' || legacy.viewMode === 'split'
191
+ ? legacy.viewMode
192
+ : legacy.isFloorplanOpen === true
193
+ ? 'split'
194
+ : '3d'
195
+ return sceneLayout(viewMode, clampViewPaneRatio(legacy.floorplanPaneRatio))
196
+ }
@@ -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
+ }