@pascal-app/editor 1.0.3 → 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 (440) hide show
  1. package/package.json +11 -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 +202 -57
  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 +36 -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 +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,143 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNodeId,
5
+ adjacentLevelId,
6
+ area,
7
+ floorOpeningHints,
8
+ getLevelDisplayName,
9
+ useScene,
10
+ } from '@pascal-app/core'
11
+ import { useViewer } from '@pascal-app/viewer'
12
+ import {
13
+ deleteFloorOpening,
14
+ finishFloorOpening,
15
+ openingRoom,
16
+ } from '../../../lib/floor-edit-session'
17
+ import { formatAreaLabel } from '../../../lib/measurements'
18
+ import { Button } from '../primitives/button'
19
+ import { Switch } from '../primitives/switch'
20
+ import { PanelWrapper } from './panel-wrapper'
21
+
22
+ /**
23
+ * A floor (or ceiling) opening: where it is, what it cuts, and the switch for
24
+ * the adjacent surface — "Also cut the ceiling below" for an opening drawn on
25
+ * a floor, "Also cut the floor above" for one drawn on a ceiling. A hatch in a
26
+ * mezzanine cuts only the mezzanine. Its shape is edited with the corner
27
+ * handles in the scene. Done and Delete return to the room.
28
+ */
29
+ export function FloorOpeningPanel({ openingId }: { openingId: string }) {
30
+ const nodes = useScene((state) => state.nodes)
31
+ const unit = useViewer((s) => s.unit)
32
+ const readOnly = useScene((state) => state.readOnly)
33
+ const opening = nodes[openingId as AnyNodeId]
34
+ if (opening?.type !== 'floor-opening') return null
35
+ const onCeiling = opening.drawnOn === 'ceiling'
36
+ const zoneId = openingRoom(nodes, opening)
37
+ const zone = zoneId ? nodes[zoneId as AnyNodeId] : undefined
38
+ const roomName = zone?.type === 'zone' ? zone.name?.trim() || 'Room' : null
39
+ const level = opening.parentId ? nodes[opening.parentId as AnyNodeId] : undefined
40
+ const adjacentId = opening.parentId
41
+ ? adjacentLevelId(nodes, opening.parentId, onCeiling ? 1 : -1)
42
+ : undefined
43
+ const adjacent = adjacentId ? nodes[adjacentId as AnyNodeId] : undefined
44
+ const hosted = !!opening.hostZoneId
45
+ const owned =
46
+ (opening.source === 'stair' || opening.source === 'elevator') &&
47
+ !!opening.ownerId &&
48
+ nodes[opening.ownerId as AnyNodeId]?.type === opening.source
49
+ const hints = floorOpeningHints(nodes, opening)
50
+ const title = opening.source === 'stair'
51
+ ? 'Stair opening'
52
+ : opening.source === 'elevator'
53
+ ? 'Elevator opening'
54
+ : onCeiling ? 'Ceiling opening' : 'Floor opening'
55
+ const kicker = [
56
+ 'Opening',
57
+ roomName ? `in ${roomName}` : null,
58
+ level?.type === 'level' ? getLevelDisplayName(level) : null,
59
+ ]
60
+ .filter(Boolean)
61
+ .join(' · ')
62
+ const switchLabel = onCeiling ? 'Also cut the floor above' : 'Also cut the ceiling below'
63
+ const setAdjacent = (next: boolean) =>
64
+ useScene.getState().updateNode(opening.id, { cutsAdjacent: next })
65
+
66
+ return (
67
+ <PanelWrapper
68
+ icon="/icons/floor.webp"
69
+ kicker={kicker}
70
+ onClose={() => finishFloorOpening(opening.id)}
71
+ title={title}
72
+ >
73
+ <div className="flex flex-col gap-3 px-4 pt-3 pb-4 text-sm" data-floor-opening-panel={opening.id}>
74
+ <p className="text-muted-foreground text-xs">
75
+ {formatAreaLabel(area([{ outer: opening.polygon, holes: [] }]), unit, 2)}
76
+ {owned ? ' · Shape follows its owner.' : ' · Drag its corners to change the shape.'}
77
+ </p>
78
+ {owned && (
79
+ <button
80
+ className="text-left text-xs text-primary underline"
81
+ data-opening-owner={opening.ownerId}
82
+ onClick={() => useViewer.getState().setSelection({ selectedIds: [opening.ownerId!] })}
83
+ type="button"
84
+ >
85
+ Owned by {opening.source === 'stair' ? 'Staircase' : 'Elevator'}
86
+ </button>
87
+ )}
88
+ {hosted ? (
89
+ <p className="text-muted-foreground text-xs" data-opening-hosted>
90
+ A hatch in the mezzanine: it cuts only the mezzanine’s floor.
91
+ </p>
92
+ ) : owned ? null : (
93
+ <label className="flex items-start gap-3" data-opening-adjacent>
94
+ <span className="flex flex-1 flex-col gap-0.5">
95
+ <span className="font-medium text-[13px] text-foreground">{switchLabel}</span>
96
+ <span className="text-muted-foreground text-xs">
97
+ {adjacent?.type === 'level'
98
+ ? `On ${getLevelDisplayName(adjacent)}, as one void with this opening.`
99
+ : onCeiling
100
+ ? 'There is no floor above this storey.'
101
+ : 'There is no storey below.'}
102
+ </span>
103
+ </span>
104
+ <Switch
105
+ aria-label={switchLabel}
106
+ checked={opening.cutsAdjacent && adjacent?.type === 'level'}
107
+ disabled={readOnly || adjacent?.type !== 'level'}
108
+ onCheckedChange={setAdjacent}
109
+ />
110
+ </label>
111
+ )}
112
+ {hints.map((hint) => (
113
+ <p className="text-muted-foreground text-xs" data-opening-hint={hint.code} key={hint.code}>
114
+ {hint.message}
115
+ </p>
116
+ ))}
117
+ <div className="flex items-center gap-2">
118
+ <Button
119
+ className="flex-1 rounded-full"
120
+ data-opening-done
121
+ onClick={() => finishFloorOpening(opening.id)}
122
+ size="sm"
123
+ type="button"
124
+ variant="outline"
125
+ >
126
+ Done
127
+ </Button>
128
+ {!owned && <Button
129
+ className="flex-1 rounded-full"
130
+ data-opening-delete
131
+ disabled={readOnly}
132
+ onClick={() => deleteFloorOpening(opening.id)}
133
+ size="sm"
134
+ type="button"
135
+ variant="outline"
136
+ >
137
+ Delete
138
+ </Button>}
139
+ </div>
140
+ </div>
141
+ </PanelWrapper>
142
+ )
143
+ }
@@ -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]
@@ -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
  }