@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
package/src/lib/scene.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  'use client'
2
2
 
3
3
  import {
4
+ type AnyNodeId,
4
5
  clearSceneHistory,
6
+ getSceneDocument,
7
+ materializeRegisteredNodeDefaults,
5
8
  nodeRegistry,
6
9
  resolveLevelId,
7
10
  sceneRegistry,
@@ -9,10 +12,11 @@ import {
9
12
  } from '@pascal-app/core'
10
13
  import { useViewer } from '@pascal-app/viewer'
11
14
  import useEditor, {
15
+ editorUiStateOnOpen,
12
16
  hasCustomPersistedEditorUiState,
13
- normalizePersistedEditorUiState,
14
17
  type PersistedEditorUiState,
15
18
  } from '../store/use-editor'
19
+ import useInteractionScope from '../store/use-interaction-scope'
16
20
  import { editorHostPanelRegistry } from './plugin-panels'
17
21
 
18
22
  export type SceneGraph = {
@@ -25,6 +29,18 @@ export type SceneGraph = {
25
29
  installedPlugins?: string[]
26
30
  }
27
31
 
32
+ /**
33
+ * The scene as saves and exports write it. A clone in hand — a preset or duplicate the user is
34
+ * placing — is in the store from the tile click so tools can render and host it, but it only
35
+ * becomes part of the document when its placement commits.
36
+ */
37
+ export function getSavedSceneDocument() {
38
+ const { scope } = useInteractionScope.getState()
39
+ const placing =
40
+ scope.kind === 'placing' && scope.driver === 'move-tool' ? [scope.nodeId as AnyNodeId] : []
41
+ return getSceneDocument(placing)
42
+ }
43
+
28
44
  type PersistedSelectionPath = {
29
45
  buildingId: string | null
30
46
  levelId: string | null
@@ -150,7 +166,6 @@ export function writePersistedSelection(selection: {
150
166
  }
151
167
 
152
168
  function getEditorUiStateForRestoredSelection(
153
- sceneNodes: Record<string, any>,
154
169
  selection: PersistedSelectionPath,
155
170
  fallbackUiState: PersistedEditorUiState,
156
171
  ): PersistedEditorUiState {
@@ -167,43 +182,16 @@ function getEditorUiStateForRestoredSelection(
167
182
  : { mode: 'select' },
168
183
  mode,
169
184
  tool: mode === 'build' ? 'property-line' : null,
170
- structureLayer: 'elements',
171
185
  catalogCategory: null,
172
186
  }
173
187
  }
174
188
 
175
- if (selection.zoneId) {
176
- return {
177
- ...fallbackUiState,
178
- phase: 'structure',
179
- toolMode: { mode: 'select' },
180
- mode: 'select',
181
- tool: null,
182
- structureLayer: 'zones',
183
- catalogCategory: null,
184
- }
185
- }
186
-
187
- const selectedNodes = selection.selectedIds
188
- .map((id) => sceneNodes[id])
189
- .filter((node): node is Record<string, any> => Boolean(node))
190
-
191
- const shouldRestoreFurnishPhase =
192
- selectedNodes.length > 0 &&
193
- selectedNodes.every(
194
- (node) =>
195
- node.type === 'item' &&
196
- node.asset?.category !== 'door' &&
197
- node.asset?.category !== 'window',
198
- )
199
-
200
189
  return {
201
190
  ...fallbackUiState,
202
- phase: shouldRestoreFurnishPhase ? 'furnish' : 'structure',
191
+ phase: 'building',
203
192
  toolMode: { mode: 'select' },
204
193
  mode: 'select',
205
194
  tool: null,
206
- structureLayer: 'elements',
207
195
  catalogCategory: null,
208
196
  }
209
197
  }
@@ -276,15 +264,22 @@ function getRestoredSelectionForScene(
276
264
  export function syncEditorSelectionFromCurrentScene() {
277
265
  const sceneNodes = useScene.getState().nodes as Record<string, any>
278
266
  const sceneRootIds = useScene.getState().rootNodeIds
279
- const siteNode = sceneRootIds[0] ? sceneNodes[sceneRootIds[0]] : null
280
267
  const resolve = (child: any) => (typeof child === 'string' ? sceneNodes[child] : child)
281
- const firstBuilding = siteNode?.children?.map(resolve).find((n: any) => n?.type === 'building')
268
+ const rootNodes = sceneRootIds.map((id) => sceneNodes[id]).filter(Boolean)
269
+ const firstBuilding =
270
+ rootNodes.find((node) => node.type === 'building') ??
271
+ rootNodes
272
+ .flatMap((node) => (Array.isArray(node.children) ? node.children.map(resolve) : []))
273
+ .find((node) => node?.type === 'building')
282
274
  const firstLevel = firstBuilding?.children?.map(resolve).find((n: any) => n?.type === 'level')
283
- const restoredEditorUiState = normalizePersistedEditorUiState(useEditor.getState())
275
+ // Every project open lands in select mode, a mid-session switch included: this
276
+ // runs on each scene load, so it reads the live state the last project left
277
+ // behind and would otherwise re-arm that project's tool over the new scene.
278
+ const restoredEditorUiState = editorUiStateOnOpen(useEditor.getState())
284
279
  const shouldRestoreEditorUiState = hasCustomPersistedEditorUiState(restoredEditorUiState)
285
280
  const restoredSelection = getRestoredSelectionForScene(sceneNodes)
286
281
  const selectionDrivenEditorUiState = restoredSelection
287
- ? getEditorUiStateForRestoredSelection(sceneNodes, restoredSelection, restoredEditorUiState)
282
+ ? getEditorUiStateForRestoredSelection(restoredSelection, restoredEditorUiState)
288
283
  : null
289
284
 
290
285
  if (firstBuilding && firstLevel) {
@@ -299,8 +294,7 @@ export function syncEditorSelectionFromCurrentScene() {
299
294
  selectedIds: [],
300
295
  zoneId: null,
301
296
  })
302
- useEditor.getState().setPhase('structure')
303
- useEditor.getState().setStructureLayer('elements')
297
+ useEditor.getState().setPhase('building')
304
298
  useEditor.getState().setMode('build')
305
299
  useEditor.getState().setTool('wall')
306
300
  return
@@ -347,8 +341,7 @@ export function syncEditorSelectionFromCurrentScene() {
347
341
  selectedIds: [],
348
342
  zoneId: null,
349
343
  })
350
- useEditor.getState().setPhase('structure')
351
- useEditor.getState().setStructureLayer('elements')
344
+ useEditor.getState().setPhase('building')
352
345
  } else {
353
346
  useEditor.getState().setPhase('site')
354
347
  useViewer.getState().setSelection({
@@ -377,7 +370,6 @@ function resetEditorInteractionState() {
377
370
  sceneRegistry.clear()
378
371
  useEditor.setState({
379
372
  phase: 'site',
380
- structureLayer: 'elements',
381
373
  catalogCategory: null,
382
374
  selectedItem: null,
383
375
  selectedReferenceId: null,
@@ -400,12 +392,14 @@ export function applySceneGraphToEditor(sceneGraph?: SceneGraph | null) {
400
392
  const defaultInstalledPlugins = editorHostPanelRegistry.getDefaultInstalledPluginIds()
401
393
  if (hasUsableSceneGraph(sceneGraph)) {
402
394
  const { nodes, rootNodeIds, collections, materials, installedPlugins } = sceneGraph
403
- useScene.getState().setScene(nodes as any, rootNodeIds as any, {
404
- collections: collections as any,
405
- materials: materials as any,
406
- installedPlugins: installedPlugins ?? defaultInstalledPlugins,
407
- hasExplicitPluginInstallState: installedPlugins !== undefined,
408
- })
395
+ useScene
396
+ .getState()
397
+ .setScene(materializeRegisteredNodeDefaults(nodes) as any, rootNodeIds as any, {
398
+ collections: collections as any,
399
+ materials: materials as any,
400
+ installedPlugins: installedPlugins ?? defaultInstalledPlugins,
401
+ hasExplicitPluginInstallState: installedPlugins !== undefined,
402
+ })
409
403
  } else {
410
404
  useScene.getState().clearScene()
411
405
  useScene.getState().setInstalledPlugins(defaultInstalledPlugins, { explicit: false })
@@ -11,14 +11,109 @@ import { useViewer } from '@pascal-app/viewer'
11
11
  import { z } from 'zod'
12
12
  import useEditor from '../store/use-editor'
13
13
  import {
14
+ deleteNodeFromCanvas,
14
15
  emitCanvasNodeSelection,
16
+ enterBuildingFromCanvas,
17
+ enterBuildingFromPlanHit,
18
+ resolveCanvasBuildingId,
15
19
  resolveCanvasSelectionNode,
16
- resolveNodeSelectionTarget,
17
20
  resolveSelectedIdsForNodeClick,
18
21
  selectionModifiersFromEvent,
19
22
  shouldPreserveSelectedRoofHostTarget,
20
23
  } from './selection-routing'
21
24
 
25
+ describe('building entry from either canvas', () => {
26
+ test('a Site hit enters the owning building before selecting an element', () => {
27
+ const previousScene = useScene.getState()
28
+ const previousSelection = useViewer.getState().selection
29
+ const previousEditor = useEditor.getState()
30
+ const building = { id: 'building_pick', type: 'building', children: ['level_pick'] }
31
+ const level = {
32
+ id: 'level_pick',
33
+ type: 'level',
34
+ level: 0,
35
+ parentId: building.id,
36
+ children: ['wall_pick'],
37
+ }
38
+ const wall = { id: 'wall_pick', type: 'wall', parentId: level.id }
39
+ const shelf = { id: 'shelf_pick', type: 'shelf', parentId: wall.id }
40
+ const elevator = { id: 'elevator_pick', type: 'elevator', parentId: building.id }
41
+ const nodes = Object.fromEntries(
42
+ [building, level, wall, shelf, elevator].map((node) => [node.id, node]),
43
+ ) as Record<string, AnyNode>
44
+ try {
45
+ useScene.setState({ nodes: nodes as never, rootNodeIds: [building.id as never] })
46
+ for (const hit of [wall, shelf, elevator]) {
47
+ useEditor.getState().setPhase('site')
48
+ useEditor.getState().armToolMode({ mode: 'select' })
49
+ const node = nodes[hit.id]!
50
+ expect(resolveCanvasBuildingId(node, nodes)).toBe(building.id as never)
51
+ expect(enterBuildingFromCanvas(node)).toBe(true)
52
+ expect(useEditor.getState().phase).toBe('building')
53
+ expect(useViewer.getState().selection).toMatchObject({
54
+ buildingId: building.id,
55
+ levelId: level.id,
56
+ selectedIds: [],
57
+ })
58
+ useViewer.getState().setSelection({ selectedIds: [wall.id as never] })
59
+ expect(enterBuildingFromCanvas(node)).toBe(false)
60
+ expect(useViewer.getState().selection.selectedIds).toEqual([wall.id])
61
+ }
62
+ useEditor.getState().setPhase('site')
63
+ useEditor.getState().armToolMode({ mode: 'build', tool: 'item' })
64
+ expect(enterBuildingFromCanvas(nodes[wall.id]!)).toBe(false)
65
+ expect(useEditor.getState().phase).toBe('site')
66
+ } finally {
67
+ useScene.setState(previousScene)
68
+ useViewer.setState({ selection: previousSelection })
69
+ useEditor.setState(previousEditor)
70
+ }
71
+ })
72
+
73
+ test('a 2D plan click from Site on a wall picks the whole building; bare ground stays in Site', () => {
74
+ const previousScene = useScene.getState()
75
+ const previousSelection = useViewer.getState().selection
76
+ const previousEditor = useEditor.getState()
77
+ const building = { id: 'building_plan', type: 'building', children: ['level_plan'] }
78
+ const level = { id: 'level_plan', type: 'level', level: 0, parentId: building.id, children: [] }
79
+ const wall = { id: 'wall_plan', type: 'wall', parentId: level.id }
80
+ const spawn = { id: 'spawn_plan', type: 'spawn', parentId: level.id }
81
+ const ground = { id: 'surface-material_plan', type: 'surface-material', parentId: 'site_plan' }
82
+ const siteFence = { id: 'fence_plan', type: 'fence', parentId: 'site_plan' }
83
+ const nodes = Object.fromEntries(
84
+ [building, level, wall, spawn, ground, siteFence].map((node) => [node.id, node]),
85
+ ) as Record<string, AnyNode>
86
+ try {
87
+ useScene.setState({ nodes: nodes as never, rootNodeIds: [building.id as never] })
88
+ useViewer.getState().setSelection({ buildingId: null })
89
+ useEditor.getState().setPhase('site')
90
+ useEditor.getState().armToolMode({ mode: 'select' })
91
+ for (const hit of [null, 'missing_plan', ground.id, siteFence.id]) {
92
+ expect(enterBuildingFromPlanHit(hit)).toBe(false)
93
+ expect(useEditor.getState().phase).toBe('site')
94
+ }
95
+ expect(enterBuildingFromPlanHit(wall.id)).toBe(true)
96
+ expect(useEditor.getState().phase).toBe('building')
97
+ expect(useViewer.getState().selection).toMatchObject({
98
+ buildingId: building.id,
99
+ selectedIds: [],
100
+ })
101
+ // Inside the building the same hit is an ordinary selection again.
102
+ expect(enterBuildingFromPlanHit(wall.id)).toBe(false)
103
+ useEditor.getState().setPhase('site')
104
+ expect(enterBuildingFromPlanHit(spawn.id)).toBe(true)
105
+ expect(useViewer.getState().selection).toMatchObject({
106
+ buildingId: building.id,
107
+ selectedIds: [],
108
+ })
109
+ } finally {
110
+ useScene.setState(previousScene)
111
+ useViewer.setState({ selection: previousSelection })
112
+ useEditor.setState(previousEditor)
113
+ }
114
+ })
115
+ })
116
+
22
117
  function registerTestDefinition(kind: string, overrides: Record<string, unknown> = {}) {
23
118
  if (nodeRegistry.has(kind)) return
24
119
  registerNode({
@@ -69,30 +164,50 @@ describe('resolveSelectedIdsForNodeClick', () => {
69
164
  })
70
165
 
71
166
  describe('emitCanvasNodeSelection', () => {
72
- test('publishes the accepted canvas node once', () => {
167
+ // Plugins (the WebXR plugin) listen for canvas selections on this event.
168
+ test('a canvas node click publishes the node once', () => {
73
169
  const node = { id: 'wall_1', type: 'wall' } as unknown as AnyNode
74
170
  const received: AnyNode[] = []
75
171
  const onSelection = (selectedNode: AnyNode) => received.push(selectedNode)
76
172
  emitter.on('selection:canvas-node-click', onSelection)
77
-
78
- emitCanvasNodeSelection(node)
79
-
80
- emitter.off('selection:canvas-node-click', onSelection)
173
+ try {
174
+ emitCanvasNodeSelection(node)
175
+ } finally {
176
+ emitter.off('selection:canvas-node-click', onSelection)
177
+ }
81
178
  expect(received).toEqual([node])
82
179
  })
83
180
 
84
- test('deletes an accepted floorplan node when Delete mode is active', () => {
85
- const node = BlockNode.parse({ id: 'block_floorplan-delete-target' })
181
+ test('in Delete mode the click deletes the node and publishes nothing', () => {
182
+ const node = BlockNode.parse({ id: 'block_canvas-delete-target' })
86
183
  const previousToolMode = useEditor.getState().toolMode
87
184
  const previousScene = useScene.getState()
88
185
  const previousSelection = useViewer.getState().selection
89
186
  const received: AnyNode[] = []
90
- const listener = (selectedNode: AnyNode) => received.push(selectedNode)
187
+ const onSelection = (selectedNode: AnyNode) => received.push(selectedNode)
188
+ emitter.on('selection:canvas-node-click', onSelection)
189
+ try {
190
+ useEditor.getState().armToolMode({ mode: 'delete' })
191
+ useScene.setState({ nodes: { [node.id]: node }, rootNodeIds: [node.id], readOnly: false })
192
+ emitCanvasNodeSelection(node)
193
+ expect(useScene.getState().nodes[node.id]).toBeUndefined()
194
+ expect(received).toEqual([])
195
+ } finally {
196
+ emitter.off('selection:canvas-node-click', onSelection)
197
+ useEditor.getState().armToolMode(previousToolMode)
198
+ useScene.setState(previousScene)
199
+ useViewer.setState({ selection: previousSelection })
200
+ }
201
+ })
202
+ })
91
203
 
92
- emitter.on('selection:canvas-node-click', listener)
204
+ describe('deleteNodeFromCanvas', () => {
205
+ test('deletes the clicked plan node and clears the selection', () => {
206
+ const node = BlockNode.parse({ id: 'block_floorplan-delete-target' })
207
+ const previousScene = useScene.getState()
208
+ const previousSelection = useViewer.getState().selection
93
209
 
94
210
  try {
95
- useEditor.getState().armToolMode({ mode: 'delete' })
96
211
  useScene.setState({
97
212
  nodes: { [node.id]: node },
98
213
  rootNodeIds: [node.id],
@@ -100,27 +215,22 @@ describe('emitCanvasNodeSelection', () => {
100
215
  })
101
216
  useViewer.getState().setSelection({ selectedIds: [node.id] })
102
217
 
103
- emitCanvasNodeSelection(node)
218
+ deleteNodeFromCanvas(node)
104
219
 
105
220
  expect(useScene.getState().nodes[node.id]).toBeUndefined()
106
221
  expect(useViewer.getState().selection.selectedIds).toEqual([])
107
- expect(received).toEqual([])
108
222
  } finally {
109
- emitter.off('selection:canvas-node-click', listener)
110
- useEditor.getState().armToolMode(previousToolMode)
111
223
  useScene.setState(previousScene)
112
224
  useViewer.setState({ selection: previousSelection })
113
225
  }
114
226
  })
115
227
 
116
- test('preserves a floorplan node and its selection when the scene is read-only', () => {
228
+ test('preserves a plan node and its selection when the scene is read-only', () => {
117
229
  const node = BlockNode.parse({ id: 'block_floorplan-read-only-target' })
118
- const previousToolMode = useEditor.getState().toolMode
119
230
  const previousScene = useScene.getState()
120
231
  const previousSelection = useViewer.getState().selection
121
232
 
122
233
  try {
123
- useEditor.getState().armToolMode({ mode: 'delete' })
124
234
  useScene.setState({
125
235
  nodes: { [node.id]: node },
126
236
  rootNodeIds: [node.id],
@@ -128,12 +238,11 @@ describe('emitCanvasNodeSelection', () => {
128
238
  })
129
239
  useViewer.getState().setSelection({ selectedIds: [node.id] })
130
240
 
131
- emitCanvasNodeSelection(node)
241
+ deleteNodeFromCanvas(node)
132
242
 
133
243
  expect(useScene.getState().nodes[node.id]).toEqual(node)
134
244
  expect(useViewer.getState().selection.selectedIds).toEqual([node.id])
135
245
  } finally {
136
- useEditor.getState().armToolMode(previousToolMode)
137
246
  useScene.setState(previousScene)
138
247
  useViewer.setState({ selection: previousSelection })
139
248
  }
@@ -167,31 +276,6 @@ describe('selectionModifiersFromEvent', () => {
167
276
  })
168
277
  })
169
278
 
170
- describe('resolveNodeSelectionTarget', () => {
171
- test('routes furniture items to furnish', () => {
172
- const node = {
173
- id: 'item_1',
174
- type: 'item',
175
- asset: { category: 'furniture' },
176
- } as unknown as AnyNode
177
-
178
- expect(resolveNodeSelectionTarget(node)).toEqual({ phase: 'furnish' })
179
- })
180
-
181
- test('routes door and window catalog items to structure', () => {
182
- const node = {
183
- id: 'item_1',
184
- type: 'item',
185
- asset: { category: 'door' },
186
- } as unknown as AnyNode
187
-
188
- expect(resolveNodeSelectionTarget(node)).toEqual({
189
- phase: 'structure',
190
- structureLayer: 'elements',
191
- })
192
- })
193
- })
194
-
195
279
  describe('resolveCanvasSelectionNode', () => {
196
280
  // Mirrors the cabinet-module setup: modules proxy to their run for grouped
197
281
  // move / rotate, but declare `selectionProxy.bypassDirectPick` so a direct
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  type AnyNode,
3
3
  type AnyNodeId,
4
+ type BuildingNode,
4
5
  emitter,
5
- type ItemNode,
6
6
  nodeRegistry,
7
7
  resolveSelectionProxyId,
8
8
  useScene,
@@ -11,6 +11,38 @@ import { useViewer } from '@pascal-app/viewer'
11
11
  import useEditor from '../store/use-editor'
12
12
  import { emitDeleteSFX } from './sfx-bus'
13
13
 
14
+ export function resolveCanvasBuildingId(
15
+ node: AnyNode,
16
+ nodes: Record<string, AnyNode>,
17
+ ): BuildingNode['id'] | null {
18
+ const visited = new Set<string>()
19
+ let current: AnyNode | undefined = node
20
+ while (current && !visited.has(current.id)) {
21
+ if (current.type === 'building') return current.id
22
+ visited.add(current.id)
23
+ current = current.parentId ? nodes[current.parentId] : undefined
24
+ }
25
+ return null
26
+ }
27
+
28
+ export function enterBuildingFromCanvas(node: AnyNode): boolean {
29
+ if (useEditor.getState().phase !== 'site' || useEditor.getState().mode !== 'select') return false
30
+ const buildingId = resolveCanvasBuildingId(node, useScene.getState().nodes)
31
+ if (!buildingId) return false
32
+ useViewer.getState().setSelection({ buildingId, selectedIds: [] })
33
+ useEditor.getState().setPhase('building')
34
+ return true
35
+ }
36
+
37
+ /**
38
+ * A floor-plan click from site, entry or background hit alike: a hit on any of
39
+ * a building's elements picks that building and goes inside, as in 3D.
40
+ */
41
+ export function enterBuildingFromPlanHit(hitId: string | null): boolean {
42
+ const node = hitId ? useScene.getState().nodes[hitId as AnyNodeId] : undefined
43
+ return node ? enterBuildingFromCanvas(node) : false
44
+ }
45
+
14
46
  export type SelectionModifierKeys = {
15
47
  meta: boolean
16
48
  ctrl: boolean
@@ -19,29 +51,33 @@ export type SelectionModifierKeys = {
19
51
  alt: boolean
20
52
  }
21
53
 
22
- export type NodeSelectionTarget = {
23
- phase: 'site' | 'structure' | 'furnish'
24
- structureLayer?: 'zones' | 'elements'
25
- }
26
-
54
+ /** A Delete-mode click on a plan entry: the sledgehammer, not a selection. */
55
+ /**
56
+ * A canvas click (2D or 3D) selected `node`: publish it on
57
+ * `selection:canvas-node-click`, which plugins listen for. In Delete mode the
58
+ * click deletes the node instead.
59
+ */
27
60
  export function emitCanvasNodeSelection(node: AnyNode): void {
28
61
  if (useEditor.getState().mode === 'delete') {
29
- const scene = useScene.getState()
30
- if (scene.readOnly) return
31
-
32
- emitDeleteSFX(node.type)
33
- scene.deleteNode(node.id as AnyNodeId)
34
- if (node.parentId) scene.dirtyNodes.add(node.parentId as AnyNodeId)
35
- useViewer.getState().setSelection({ selectedIds: [] })
36
- if (useViewer.getState().hoveredId === node.id) {
37
- useViewer.setState({ hoveredId: null })
38
- }
62
+ deleteNodeFromCanvas(node)
39
63
  return
40
64
  }
41
-
42
65
  emitter.emit('selection:canvas-node-click', node)
43
66
  }
44
67
 
68
+ export function deleteNodeFromCanvas(node: AnyNode): void {
69
+ const scene = useScene.getState()
70
+ if (scene.readOnly) return
71
+
72
+ emitDeleteSFX(node.type)
73
+ scene.deleteNode(node.id as AnyNodeId)
74
+ if (node.parentId) scene.dirtyNodes.add(node.parentId as AnyNodeId)
75
+ useViewer.getState().setSelection({ selectedIds: [] })
76
+ if (useViewer.getState().hoveredId === node.id) {
77
+ useViewer.setState({ hoveredId: null })
78
+ }
79
+ }
80
+
45
81
  function shouldBypassSelectionProxy(node: AnyNode, target: AnyNode): boolean {
46
82
  if (node.id === target.id) return false
47
83
  // Kind-declared bypass (`def.selectionProxy.bypassDirectPick`): the kind
@@ -154,60 +190,3 @@ export function shouldPreserveSelectedRoofHostTarget({
154
190
  selectedIds[0] === node.id
155
191
  )
156
192
  }
157
-
158
- export function resolveNodeSelectionTarget(node: AnyNode): NodeSelectionTarget | null {
159
- if (node.type === 'building') {
160
- return { phase: 'site' }
161
- }
162
-
163
- if (node.type === 'zone') {
164
- return {
165
- phase: 'structure',
166
- structureLayer: 'zones',
167
- }
168
- }
169
-
170
- if (node.type === 'item') {
171
- const item = node as ItemNode
172
- if (item.asset.category === 'door' || item.asset.category === 'window') {
173
- return {
174
- phase: 'structure',
175
- structureLayer: 'elements',
176
- }
177
- }
178
- return { phase: 'furnish' }
179
- }
180
-
181
- if (
182
- node.type === 'wall' ||
183
- node.type === 'fence' ||
184
- node.type === 'column' ||
185
- node.type === 'elevator' ||
186
- node.type === 'slab' ||
187
- node.type === 'ceiling' ||
188
- node.type === 'roof' ||
189
- node.type === 'roof-segment' ||
190
- node.type === 'stair' ||
191
- node.type === 'stair-segment' ||
192
- node.type === 'spawn' ||
193
- node.type === 'window' ||
194
- node.type === 'door'
195
- ) {
196
- return {
197
- phase: 'structure',
198
- structureLayer: 'elements',
199
- }
200
- }
201
-
202
- const def = nodeRegistry.get(node.type)
203
- if (!def) return null
204
-
205
- if (def.category === 'furnish') {
206
- return { phase: 'furnish' }
207
- }
208
-
209
- return {
210
- phase: 'structure',
211
- structureLayer: 'elements',
212
- }
213
- }