@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
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { X } from 'lucide-react'
4
4
  import { AnimatePresence, motion } from 'motion/react'
5
- import Image from 'next/image'
6
5
  import { type ReactNode, useEffect, useState } from 'react'
7
6
  import { createPortal } from 'react-dom'
8
7
  import useEditor from '../../../store/use-editor'
8
+ import { NodeIconImage } from '../node-icon-image'
9
9
 
10
10
  interface MobilePanelSheetProps {
11
11
  open: boolean
@@ -75,7 +75,7 @@ export function MobilePanelSheet({ open, onClose, icon, title, children }: Mobil
75
75
  <div className="flex shrink-0 items-center justify-between border-border/50 border-b px-3 pt-1 pb-3">
76
76
  <div className="flex min-w-0 items-center gap-2">
77
77
  {icon && (
78
- <Image
78
+ <NodeIconImage
79
79
  alt=""
80
80
  className="shrink-0 object-contain"
81
81
  height={18}
@@ -2,9 +2,9 @@
2
2
 
3
3
  import type { AnyNode } from '@pascal-app/core'
4
4
  import { Copy, Move, SlidersHorizontal, Trash2 } from 'lucide-react'
5
- import Image from 'next/image'
6
5
  import type { MouseEventHandler } from 'react'
7
6
  import { cn } from '../../../lib/utils'
7
+ import { NodeIconImage } from '../node-icon-image'
8
8
  import { getNodeDisplay } from './node-display'
9
9
 
10
10
  interface MobileSelectionBarProps {
@@ -45,7 +45,7 @@ export function MobileSelectionBar({
45
45
  onClick={onEdit}
46
46
  type="button"
47
47
  >
48
- <Image
48
+ <NodeIconImage
49
49
  alt=""
50
50
  className="shrink-0 rounded object-contain"
51
51
  height={20}
@@ -5,6 +5,9 @@ import {
5
5
  BuildingNode,
6
6
  clearSceneHistory,
7
7
  LevelNode,
8
+ planStairFlightHeightEdit,
9
+ StairNode,
10
+ StairSegmentNode,
8
11
  useLiveNodeOverrides,
9
12
  useScene,
10
13
  WallNode,
@@ -124,9 +127,7 @@ describe('buildMultiNodePatches', () => {
124
127
  nodes,
125
128
  {
126
129
  derive: (next) => ({ thickness: (next as { height: number }).height / 10 }),
127
- reconcile: (prev) => [
128
- { id: `${prev.id}_follow` as AnyNodeId, data: { height: 4 } },
129
- ],
130
+ reconcile: (prev) => [{ id: `${prev.id}_follow` as AnyNodeId, data: { height: 4 } }],
130
131
  },
131
132
  )
132
133
  expect(patches).toEqual([
@@ -207,3 +208,25 @@ describe('commitMultiNodeFields', () => {
207
208
  expect((useScene.getState().nodes[WALL_C] as { height?: number }).height).toBe(2.7)
208
209
  })
209
210
  })
211
+
212
+ test('multi-flight edits reconcile their parent from the complete proposed scene', () => {
213
+ const first = StairSegmentNode.parse({ height: 1 })
214
+ const second = StairSegmentNode.parse({ height: 3 })
215
+ const stair = StairNode.parse({ totalRise: 4, children: [first.id, second.id] })
216
+ first.parentId = stair.id
217
+ second.parentId = stair.id
218
+ const nodes: Record<string, AnyNode> = Object.fromEntries(
219
+ [stair, first, second].map((node) => [node.id, node]),
220
+ )
221
+ const patches = buildMultiNodePatches([first.id, second.id], () => ({ height: 2 }), nodes, {
222
+ reconcile: (_prev, next, proposed) =>
223
+ next.type === 'stair-segment'
224
+ ? planStairFlightHeightEdit(next, next.height, proposed!).slice(1)
225
+ : [],
226
+ })
227
+ const result = { ...nodes }
228
+ for (const patch of patches) result[patch.id] = { ...result[patch.id], ...patch.data } as AnyNode
229
+ expect((result[first.id] as StairSegmentNode).height).toBe(2)
230
+ expect((result[second.id] as StairSegmentNode).height).toBe(2)
231
+ expect((result[stair.id] as StairNode).totalRise).toBe(4)
232
+ })
@@ -125,9 +125,12 @@ export function buildMultiNodePatches(
125
125
  patch = { ...patch, ...parametrics.derive(next, patch, node) } as Partial<AnyNode>
126
126
  }
127
127
  updates.push({ id, data: patch })
128
- if (parametrics?.reconcile) {
129
- const next = { ...node, ...patch } as AnyNode
130
- followUps.push(...parametrics.reconcile(node, next))
128
+ }
129
+ if (parametrics?.reconcile) {
130
+ const proposed = { ...nodes }
131
+ for (const { id, data } of updates) proposed[id] = { ...nodes[id], ...data } as AnyNode
132
+ for (const { id } of updates) {
133
+ followUps.push(...parametrics.reconcile(nodes[id]!, proposed[id]!, proposed))
131
134
  }
132
135
  }
133
136
  return [...updates, ...followUps]
@@ -58,6 +58,32 @@ function ceilingCustomHeight(node: CeilingNode, nodes: Record<string, AnyNode>):
58
58
  return Math.min(resolved, max)
59
59
  }
60
60
 
61
+ export function applyMultiHeightMode(
62
+ nodeIds: AnyNodeId[],
63
+ next: 'storey' | 'custom',
64
+ parametrics: ParametricDescriptor<AnyNode>,
65
+ ) {
66
+ const nodes = useScene.getState().nodes as Record<string, AnyNode>
67
+ commitMultiNodeFields(
68
+ nodeIds,
69
+ (node) => {
70
+ const isCustom = (node as { height?: number }).height != null
71
+ if (next === 'custom') {
72
+ if (isCustom) return {}
73
+ if (node.type === 'ceiling') return { height: ceilingCustomHeight(node, nodes) }
74
+ if (node.type === 'wall') {
75
+ return { height: Math.max(0.1, getWallEffectiveHeightForNodes(node, nodes)) }
76
+ }
77
+ return {}
78
+ }
79
+ if (!isCustom) return {}
80
+ if (node.type === 'wall') return wallFollowsLevelPatch(node, nodes)
81
+ return { height: undefined }
82
+ },
83
+ parametrics,
84
+ )
85
+ }
86
+
61
87
  export function MultiHeightModeField({
62
88
  nodeIds,
63
89
  nodeType,
@@ -97,27 +123,7 @@ export function MultiHeightModeField({
97
123
 
98
124
  const applyMode = useCallback(
99
125
  (next: 'storey' | 'custom') => {
100
- const nodes = useScene.getState().nodes as Record<string, AnyNode>
101
- commitMultiNodeFields(
102
- nodeIds,
103
- (node) => {
104
- const isCustom = (node as { height?: number }).height != null
105
- if (next === 'custom') {
106
- if (isCustom) return {}
107
- if (node.type === 'ceiling') {
108
- return { height: ceilingCustomHeight(node, nodes) }
109
- }
110
- if (node.type === 'wall') {
111
- return { height: Math.max(0.1, getWallEffectiveHeightForNodes(node, nodes)) }
112
- }
113
- return {}
114
- }
115
- if (!isCustom) return {}
116
- if (node.type === 'wall') return wallFollowsLevelPatch(node, nodes)
117
- return { height: undefined }
118
- },
119
- parametrics,
120
- )
126
+ applyMultiHeightMode(nodeIds, next, parametrics)
121
127
  },
122
128
  [nodeIds, parametrics],
123
129
  )
@@ -11,9 +11,11 @@ import {
11
11
  import { useViewer } from '@pascal-app/viewer'
12
12
  import { useCallback, useMemo } from 'react'
13
13
  import { useShallow } from 'zustand/react/shallow'
14
+ import { getNodePanelModel } from '../../../lib/panel-rows'
14
15
  import { selectionMatchesSessionGroup } from '../../../lib/session-groups'
15
16
  import useSessionGroups from '../../../store/use-session-groups'
16
17
  import { PanelSection } from '../controls/panel-section'
18
+ import { SegmentedControl } from '../controls/segmented-control'
17
19
  import { SliderControl } from '../controls/slider-control'
18
20
  import { resolveUniqueSelectionIds } from './homogeneous-selection'
19
21
  import {
@@ -52,6 +54,7 @@ export function MultiParametricInspector({ footer }: { footer?: React.ReactNode
52
54
 
53
55
  const def = nodeType ? nodeRegistry.get(nodeType) : undefined
54
56
  const parametrics = def?.parametrics as ParametricDescriptor<AnyNode> | undefined
57
+ const hasKindControls = !!getNodePanelModel(def)?.multiControls
55
58
 
56
59
  const handleClose = useCallback(() => {
57
60
  setSelection({ selectedIds: [] })
@@ -79,7 +82,11 @@ export function MultiParametricInspector({ footer }: { footer?: React.ReactNode
79
82
  nodeType={nodeType}
80
83
  parametrics={parametrics}
81
84
  title={group.label}
82
- />
85
+ >
86
+ {hasKindControls ? (
87
+ <MultiKindControls nodeIds={nodeIds} nodeType={nodeType} section={group.label} />
88
+ ) : null}
89
+ </MultiGroupFields>
83
90
  ))}
84
91
  <div className="border-border/50 border-t p-3">
85
92
  <MultiSelectionActions />
@@ -88,6 +95,46 @@ export function MultiParametricInspector({ footer }: { footer?: React.ReactNode
88
95
  )
89
96
  }
90
97
 
98
+ /**
99
+ * The kind's own multi-selection controls for one section (the wall's
100
+ * Reference), from its panel model — the same segmented control the single
101
+ * panel shows, mixed when the selection disagrees.
102
+ */
103
+ function MultiKindControls({
104
+ nodeIds,
105
+ nodeType,
106
+ section,
107
+ }: {
108
+ nodeIds: AnyNodeId[]
109
+ nodeType: AnyNode['type']
110
+ section: string
111
+ }) {
112
+ const nodes = useScene((s) => s.nodes)
113
+ const controls = useMemo(() => {
114
+ const model = getNodePanelModel(nodeRegistry.get(nodeType))
115
+ if (!model?.multiControls) return []
116
+ const selection = nodeIds.flatMap((id) => (nodes[id] ? [nodes[id]!] : []))
117
+ return model.multiControls({ selection, nodes }).filter((control) => control.section === section)
118
+ }, [nodeIds, nodeType, nodes, section])
119
+ return (
120
+ <>
121
+ {controls.map((control) => (
122
+ <div className="flex flex-col gap-1" key={control.id}>
123
+ <div className="px-1 font-medium text-[10px] text-muted-foreground/80">
124
+ {control.label}
125
+ </div>
126
+ <SegmentedControl
127
+ mixed={control.value === null}
128
+ onChange={control.onChange}
129
+ options={control.options.map((option) => ({ ...option }))}
130
+ value={control.value ?? ''}
131
+ />
132
+ </div>
133
+ ))}
134
+ </>
135
+ )
136
+ }
137
+
91
138
  function MultiGroupFields({
92
139
  defaultExpanded,
93
140
  title,
@@ -95,7 +142,9 @@ function MultiGroupFields({
95
142
  nodeIds,
96
143
  nodeType,
97
144
  parametrics,
145
+ children,
98
146
  }: {
147
+ children?: React.ReactNode
99
148
  defaultExpanded?: boolean
100
149
  title: string
101
150
  fields: ParamField<AnyNode>[]
@@ -109,7 +158,7 @@ function MultiGroupFields({
109
158
  fieldVisibleForAll(nodeIds, (field as { visibleIf?: (n: AnyNode) => boolean }).visibleIf, s.nodes),
110
159
  ),
111
160
  )
112
- if (genericFields.length === 0 || !anyVisible) return null
161
+ if ((genericFields.length === 0 || !anyVisible) && !children) return null
113
162
  return (
114
163
  <PanelSection defaultExpanded={defaultExpanded} title={title}>
115
164
  {genericFields.map((field, fi) => {
@@ -139,6 +188,7 @@ function MultiGroupFields({
139
188
  />
140
189
  )
141
190
  })}
191
+ {children}
142
192
  </PanelSection>
143
193
  )
144
194
  }
@@ -1,10 +1,16 @@
1
1
  'use client'
2
2
 
3
- import { type AnyNodeId, useScene } from '@pascal-app/core'
3
+ import {
4
+ type AnyNodeId,
5
+ type BuildingNode,
6
+ resolveBuildingForLevel,
7
+ useScene,
8
+ } from '@pascal-app/core'
4
9
  import { useViewer } from '@pascal-app/viewer'
5
- import { Copy, Group, Trash2, Ungroup } from 'lucide-react'
10
+ import { Building2, Copy, Group, Trash2, Ungroup } from 'lucide-react'
6
11
  import { useMemo } from 'react'
7
12
  import { deleteSelection, duplicateSelectionAndPickUp } from '../../editor/group-actions'
13
+ import { collectSelectableCandidateIds } from '../../tools/select/select-candidates'
8
14
  import {
9
15
  canCreateSessionGroup,
10
16
  selectionIntersectsSessionGroup,
@@ -18,6 +24,25 @@ import { ActionButton, ActionGroup } from '../controls/action-button'
18
24
  import { PanelWrapper } from './panel-wrapper'
19
25
  import { formatSelectionBreakdown } from './selection-breakdown'
20
26
 
27
+ /** The building holding the level, when the selection is everything selectable on it. */
28
+ function useWholeLevelBuildingId(selectedIds: readonly string[]) {
29
+ const levelId = useViewer((s) => s.selection.levelId)
30
+ const nodes = useScene((s) => s.nodes)
31
+ return useMemo(() => {
32
+ if (!levelId) return null
33
+ const candidates = collectSelectableCandidateIds()
34
+ if (candidates.length === 0) return null
35
+ const selected = new Set(selectedIds)
36
+ if (!candidates.every((id) => selected.has(id))) return null
37
+ return resolveBuildingForLevel(levelId as AnyNodeId, nodes) ?? null
38
+ }, [levelId, nodes, selectedIds])
39
+ }
40
+
41
+ /** The building's own selection (no level): its floating pill offers the whole-building Move. */
42
+ function selectBuilding(buildingId: AnyNodeId) {
43
+ useViewer.getState().setSelection({ buildingId: buildingId as BuildingNode['id'] })
44
+ }
45
+
21
46
  export function MultiSelectionActions() {
22
47
  const selectedIds = useViewer((s) => s.selection.selectedIds)
23
48
  const sessionGroups = useSessionGroups((s) => s.groups)
@@ -32,10 +57,22 @@ export function MultiSelectionActions() {
32
57
  [sessionGroups, selectedIds, liveIds],
33
58
  )
34
59
 
60
+ const buildingId = useWholeLevelBuildingId(selectedIds)
61
+
35
62
  return (
36
- <ActionGroup>
63
+ <ActionGroup className="flex-wrap">
64
+ {buildingId && (
65
+ <ActionButton
66
+ className="basis-full whitespace-nowrap"
67
+ icon={<Building2 className="h-4 w-4" />}
68
+ label="Select building"
69
+ onClick={() => selectBuilding(buildingId)}
70
+ title="Select the whole building to move all its levels"
71
+ />
72
+ )}
37
73
  {showGroup && (
38
74
  <ActionButton
75
+ className="min-w-20 px-2"
39
76
  icon={<Group className="h-4 w-4" />}
40
77
  label="Group"
41
78
  onClick={() => groupCurrentSelection()}
@@ -44,6 +81,7 @@ export function MultiSelectionActions() {
44
81
  )}
45
82
  {showUngroup && (
46
83
  <ActionButton
84
+ className="min-w-20 px-2"
47
85
  icon={<Ungroup className="h-4 w-4" />}
48
86
  label="Ungroup"
49
87
  onClick={() => ungroupCurrentSelection()}
@@ -51,12 +89,13 @@ export function MultiSelectionActions() {
51
89
  />
52
90
  )}
53
91
  <ActionButton
92
+ className="min-w-20 px-2"
54
93
  icon={<Copy className="h-4 w-4" />}
55
94
  label="Duplicate"
56
95
  onClick={() => duplicateSelectionAndPickUp()}
57
96
  />
58
97
  <ActionButton
59
- className="border-red-500/40 text-red-200 hover:bg-red-500/15"
98
+ className="min-w-20 px-2 border-red-500/40 text-red-200 hover:bg-red-500/15"
60
99
  icon={<Trash2 className="h-4 w-4 text-red-400" />}
61
100
  label="Delete"
62
101
  onClick={() => deleteSelection()}
@@ -1,4 +1,4 @@
1
- import type { AnyNode } from '@pascal-app/core'
1
+ import { type AnyNode, scriptImages } from '@pascal-app/core'
2
2
 
3
3
  export type NodeDisplay = {
4
4
  icon: string
@@ -21,6 +21,7 @@ const TYPE_DEFAULTS: Record<string, NodeDisplay> = {
21
21
  'stair-segment': { icon: '/icons/stairs.webp', label: 'Stair segment' },
22
22
  scan: { icon: '/icons/mesh.webp', label: '3D Scan' },
23
23
  guide: { icon: '/icons/floorplan.webp', label: 'Guide image' },
24
+ zone: { icon: '/icons/zone.webp', label: 'Zone' },
24
25
  }
25
26
 
26
27
  export function getTypeDisplay(type: string): NodeDisplay {
@@ -30,15 +31,19 @@ export function getTypeDisplay(type: string): NodeDisplay {
30
31
  export function getNodeDisplay(node: AnyNode | null | undefined): NodeDisplay {
31
32
  if (!node) return { icon: '/icons/select.webp', label: 'Selection' }
32
33
  const fallback = TYPE_DEFAULTS[node.type] ?? { icon: '/icons/select.webp', label: node.type }
34
+ const scripted = scriptImages(node)?.thumbnail
33
35
  // Item nodes carry an asset with its own thumbnail/name
34
36
  if (node.type === 'item') {
35
37
  return {
36
- icon: node.asset?.thumbnail || fallback.icon,
38
+ icon: scripted ?? (node.asset?.thumbnail || fallback.icon),
37
39
  label: node.name || node.asset?.name || fallback.label,
38
40
  }
39
41
  }
42
+ if (node.type === 'zone' && node.spaceRole === 'room') {
43
+ return { icon: '/icons/kitchen.webp', label: node.name || 'Room' }
44
+ }
40
45
  return {
41
- icon: fallback.icon,
46
+ icon: scripted ?? fallback.icon,
42
47
  label: node.name || fallback.label,
43
48
  }
44
49
  }
@@ -22,11 +22,16 @@ import {
22
22
  type WindowNode,
23
23
  } from '@pascal-app/core'
24
24
  import { useViewer } from '@pascal-app/viewer'
25
- import { useCallback, useEffect, useState } from 'react'
25
+ import { useCallback, useEffect, useMemo, useState } from 'react'
26
+ import { useSelectedRoom, useZoneRoom } from '../../../hooks/use-selected-room'
27
+ import { FloorOpeningPanel } from './floor-opening-panel'
28
+ import { OpenRoomPanel, RoomPanel } from './room-panel'
26
29
  import { useIsMobile } from '../../../hooks/use-mobile'
27
30
  import { shouldShowEditingControls } from '../../../lib/interaction/overlay-policy'
31
+ import { captureElementActionOrigin, completeElementAction } from '../../../lib/room-zone-routing'
28
32
  import { sfxEmitter } from '../../../lib/sfx-bus'
29
33
  import useEditor from '../../../store/use-editor'
34
+ import { duplicateNodeAndPickUp } from '../../editor/duplicate-node'
30
35
  import { deleteSelection, duplicateSelectionAndPickUp, startGroupPickUp } from '../../editor/group-actions'
31
36
  import { resolveHomogeneousSelection } from './homogeneous-selection'
32
37
  import { MobilePanelSheet } from './mobile-panel-sheet'
@@ -34,7 +39,7 @@ import { MobileSelectionBar } from './mobile-selection-bar'
34
39
  import { MultiParametricInspector } from './multi-parametric-inspector'
35
40
  import { MultiSelectionPanel } from './multi-selection-panel'
36
41
  import { getNodeDisplay, getTypeDisplay } from './node-display'
37
- import { resetDesktopInspectorCollapsed } from './panel-wrapper'
42
+ import { InspectedNodesContext } from './panel-wrapper'
38
43
  import { ParametricInspector } from './parametric-inspector'
39
44
  import { ReferencePanel } from './reference-panel'
40
45
  import { formatSelectionBreakdown } from './selection-breakdown'
@@ -127,24 +132,16 @@ function MobilePanelLayer({
127
132
  }, [node, setMovingNode, clearSelection])
128
133
 
129
134
  const handleDuplicate = useCallback(() => {
130
- if (!isMovableNode(node)) return
131
- sfxEmitter.emit('sfx:item-pick')
132
- const cloned = structuredClone(node) as MovableNode & { id?: AnyNodeId }
133
- delete (cloned as { id?: AnyNodeId }).id
134
- const prevMeta =
135
- cloned.metadata && typeof cloned.metadata === 'object' && !Array.isArray(cloned.metadata)
136
- ? (cloned.metadata as Record<string, unknown>)
137
- : {}
138
- cloned.metadata = { ...prevMeta, isNew: true }
139
- setMovingNode(cloned as MovableNode)
140
- clearSelection()
141
- }, [node, setMovingNode, clearSelection])
135
+ if (isMovableNode(node)) duplicateNodeAndPickUp(node)
136
+ }, [node])
142
137
 
143
138
  const handleDelete = useCallback(() => {
144
139
  if (!node) return
140
+ const origin = captureElementActionOrigin([node.id])
145
141
  sfxEmitter.emit('sfx:item-delete')
146
142
  deleteNode(node.id)
147
143
  clearSelection()
144
+ completeElementAction(origin)
148
145
  }, [node, deleteNode, clearSelection])
149
146
 
150
147
  if (!(node || isReference)) return null
@@ -222,6 +219,7 @@ export function PanelManager({
222
219
  multiSelectionFooter?: React.ReactNode
223
220
  }) {
224
221
  const isMobile = useIsMobile()
222
+ const room = useSelectedRoom()
225
223
  const selectedIds = useViewer((s) => s.selection.selectedIds)
226
224
  const selectedZoneId = useViewer((s) => s.selection.zoneId)
227
225
  const setSelection = useViewer((s) => s.setSelection)
@@ -248,6 +246,16 @@ export function PanelManager({
248
246
  : '',
249
247
  )
250
248
 
249
+ // A room reached as a zone (a unit's member, a zone kept by a route that is not
250
+ // room-first) still gets the room panel.
251
+ const panelZoneId =
252
+ selectedIds.length === 0
253
+ ? selectedZoneId
254
+ : selectedNodeType === 'zone'
255
+ ? (selectedIds[0] ?? null)
256
+ : null
257
+ const zoneRoom = useZoneRoom(panelZoneId)
258
+
251
259
  // Node and reference selection are mutually exclusive: selecting a guide
252
260
  // clears the node selection (handleGuideSelect), but node selection never
253
261
  // cleared a lingering reference — so clicking a wall with a floorplan
@@ -255,74 +263,101 @@ export function PanelManager({
255
263
  // moment a scene selection appears.
256
264
  const setSelectedReferenceId = useEditor((s) => s.setSelectedReferenceId)
257
265
  useEffect(() => {
258
- if (selectedIds.length > 0 || selectedZoneId) {
266
+ if (selectedIds.length > 0 || selectedZoneId || room) {
259
267
  setSelectedReferenceId(null)
260
268
  }
261
- }, [selectedIds, selectedZoneId, setSelectedReferenceId])
269
+ }, [selectedIds, selectedZoneId, room, setSelectedReferenceId])
262
270
 
263
- // The inspector's expanded state is shared across panel swaps, but a fresh
264
- // selection after everything was deselected should open collapsed again.
265
- const hasAnySelection = selectedIds.length > 0 || Boolean(selectedZoneId) || Boolean(selectedReferenceId)
266
- useEffect(() => {
267
- if (!hasAnySelection) {
268
- resetDesktopInspectorCollapsed()
269
- }
270
- }, [hasAnySelection])
271
+ // What the card shows, for its Collections section: a room is its zone.
272
+ const inspectedKey =
273
+ room && selectedIds.length === 0
274
+ ? room.zoneId
275
+ : panelZoneId && zoneRoom
276
+ ? panelZoneId
277
+ : selectedReferenceId
278
+ ? ''
279
+ : selectedIds.length === 0
280
+ ? (selectedZoneId ?? '')
281
+ : selectedIds.join(',')
282
+ const inspectedIds = useMemo(
283
+ () => (inspectedKey ? (inspectedKey.split(',') as AnyNodeId[]) : []),
284
+ [inspectedKey],
285
+ )
271
286
 
272
287
  if (!shouldShowEditingControls(readOnly)) return null
273
288
 
274
- if (isMobile) {
275
- if (selectedReferenceId) {
276
- return <MobilePanelLayer isReference={true} node={null} panel={<ReferencePanel />} />
289
+ return (
290
+ <InspectedNodesContext.Provider value={inspectedIds}>
291
+ {renderPanel()}
292
+ </InspectedNodesContext.Provider>
293
+ )
294
+
295
+ function renderPanel() {
296
+ if (room && selectedIds.length === 0) return <RoomPanel room={room} />
297
+ if (selectedNodeType === 'floor-opening' && selectedIds[0])
298
+ return <FloorOpeningPanel key={selectedIds[0]} openingId={selectedIds[0]} />
299
+ if (zoneRoom && panelZoneId) {
300
+ const closeZone = () => setSelection({ selectedIds: [], zoneId: null })
301
+ return zoneRoom.record ? (
302
+ <RoomPanel onClose={closeZone} room={zoneRoom.record} />
303
+ ) : (
304
+ <OpenRoomPanel key={panelZoneId} onClose={closeZone} zoneId={panelZoneId} />
305
+ )
277
306
  }
278
- if (selectedIds.length > 1) {
307
+
308
+ if (isMobile) {
309
+ if (selectedReferenceId) {
310
+ return <MobilePanelLayer isReference={true} node={null} panel={<ReferencePanel />} />
311
+ }
312
+ if (selectedIds.length > 1) {
313
+ return (
314
+ <MobileMultiPanelLayer
315
+ breakdown={multiBreakdown}
316
+ panel={
317
+ homogeneousType ? (
318
+ <MultiParametricInspector footer={multiSelectionFooter} />
319
+ ) : (
320
+ <MultiSelectionPanel footer={multiSelectionFooter} />
321
+ )
322
+ }
323
+ type={homogeneousType}
324
+ />
325
+ )
326
+ }
279
327
  return (
280
- <MobileMultiPanelLayer
281
- breakdown={multiBreakdown}
282
- panel={
283
- homogeneousType ? (
284
- <MultiParametricInspector footer={multiSelectionFooter} />
285
- ) : (
286
- <MultiSelectionPanel footer={multiSelectionFooter} />
287
- )
288
- }
289
- type={homogeneousType}
328
+ <MobilePanelLayer
329
+ isReference={false}
330
+ node={selectedNode}
331
+ panel={panelForType(selectedNodeType)}
290
332
  />
291
333
  )
292
334
  }
293
- return (
294
- <MobilePanelLayer
295
- isReference={false}
296
- node={selectedNode}
297
- panel={panelForType(selectedNodeType)}
298
- />
299
- )
300
- }
301
335
 
302
- // Show reference panel if a reference is selected
303
- if (selectedReferenceId) {
304
- return <ReferencePanel />
305
- }
336
+ // Show reference panel if a reference is selected
337
+ if (selectedReferenceId) {
338
+ return <ReferencePanel />
339
+ }
306
340
 
307
- if (selectedZoneId && selectedIds.length === 0) {
308
- return (
309
- <ParametricInspector
310
- footer={inspectorFooter}
311
- key={selectedZoneId}
312
- nodeId={selectedZoneId as AnyNodeId}
313
- onClose={() => setSelection({ zoneId: null })}
314
- />
315
- )
316
- }
341
+ if (selectedZoneId && selectedIds.length === 0) {
342
+ return (
343
+ <ParametricInspector
344
+ footer={inspectorFooter}
345
+ key={selectedZoneId}
346
+ nodeId={selectedZoneId as AnyNodeId}
347
+ onClose={() => setSelection({ zoneId: null })}
348
+ />
349
+ )
350
+ }
317
351
 
318
- // Multi-selection: parametric inspector when every resolved id shares a type,
319
- // otherwise the actions-only panel. Mobile uses the same panels in a sheet.
320
- if (selectedIds.length > 1) {
321
- if (homogeneousType) {
322
- return <MultiParametricInspector footer={multiSelectionFooter} />
352
+ // Multi-selection: parametric inspector when every resolved id shares a type,
353
+ // otherwise the actions-only panel. Mobile uses the same panels in a sheet.
354
+ if (selectedIds.length > 1) {
355
+ if (homogeneousType) {
356
+ return <MultiParametricInspector footer={multiSelectionFooter} />
357
+ }
358
+ return <MultiSelectionPanel footer={multiSelectionFooter} />
323
359
  }
324
- return <MultiSelectionPanel footer={multiSelectionFooter} />
325
- }
326
360
 
327
- return panelForType(selectedNodeType, inspectorFooter)
361
+ return panelForType(selectedNodeType, inspectorFooter)
362
+ }
328
363
  }