@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
@@ -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
  }
@@ -12,7 +12,6 @@ import {
12
12
  } from '@pascal-app/core'
13
13
  import { useViewer } from '@pascal-app/viewer'
14
14
  import { ChevronDown, ChevronLeft, GripHorizontal, RotateCcw, X } from 'lucide-react'
15
- import Image from 'next/image'
16
15
  import {
17
16
  type ComponentType,
18
17
  createContext,
@@ -27,27 +26,25 @@ import {
27
26
  useState,
28
27
  } from 'react'
29
28
  import { useIsMobile } from '../../../hooks/use-mobile'
29
+ import { IconRefImage } from '../icon-ref'
30
+ import { NodeIconImage } from '../node-icon-image'
30
31
  import {
31
32
  resolveActiveExtension,
32
33
  toggleCard,
33
34
  toggleExtension,
34
35
  } from '../../../lib/inspector-card-mode'
35
36
  import { cn } from '../../../lib/utils'
37
+ import { useInspectorExpanded } from '../../../lib/inspector-expanded'
38
+ import { ActionButton, ActionGroup } from '../controls/action-button'
36
39
  import { PanelSection } from '../controls/panel-section'
37
40
  import { ErrorBoundary } from '../primitives/error-boundary'
41
+ import { useInRightStack } from '../right-stack'
42
+ import { CollectionsPopover } from './collections/collections-popover'
38
43
 
39
44
  const DRAG_MARGIN = 8
40
45
  // Pointer travel (px) below which a header press is treated as a click
41
46
  // (toggles collapse) rather than a drag.
42
47
  const CLICK_SLOP = 4
43
- let desktopInspectorCollapsed = true
44
-
45
- /** Forget the shared expanded state. Called when the last selection clears so
46
- * a fresh selection opens the inspector collapsed — the sharing is only meant
47
- * to survive swaps between panels (roof ↔ segment), not a close/reopen. */
48
- export function resetDesktopInspectorCollapsed() {
49
- desktopInspectorCollapsed = true
50
- }
51
48
 
52
49
  function clamp(value: number, min: number, max: number): number {
53
50
  return Math.min(Math.max(value, min), Math.max(min, max))
@@ -81,8 +78,19 @@ function getDragBounds(el: HTMLElement | null): {
81
78
  */
82
79
  export const InspectorFooterContext = createContext<React.ReactNode>(null)
83
80
 
81
+ /**
82
+ * The scene elements the inspector card shows (the selection, or the zone behind a room).
83
+ * `PanelManager` provides it so every card gets the Collections section; a card mounted
84
+ * elsewhere (the site panel in the sidebar) is not about the selection and gets none.
85
+ */
86
+ export const InspectedNodesContext = createContext<AnyNodeId[]>([])
87
+
84
88
  interface PanelWrapperProps {
85
89
  title: string
90
+ /** A short line above the title ("Room · Ground floor"). */
91
+ kicker?: string
92
+ /** Replaces the plain title, e.g. with the name edited in place. */
93
+ titleContent?: React.ReactNode
86
94
  /** Either a URL path (legacy panels pass `/icons/floor.webp` etc.,
87
95
  * rendered via next/image) OR a React node (registry-driven
88
96
  * inspector renders `<Icon icon="lucide:fence" />` from
@@ -100,6 +108,8 @@ interface PanelWrapperProps {
100
108
 
101
109
  export function PanelWrapper({
102
110
  title,
111
+ kicker,
112
+ titleContent,
103
113
  icon,
104
114
  onClose,
105
115
  onReset,
@@ -110,7 +120,9 @@ export function PanelWrapper({
110
120
  width = 320, // default width
111
121
  }: PanelWrapperProps) {
112
122
  const isMobile = useIsMobile()
123
+ const inStack = useInRightStack()
113
124
  const contextFooter = useContext(InspectorFooterContext)
125
+ const inspectedIds = useContext(InspectedNodesContext)
114
126
  const resolvedFooter = footer ?? contextFooter
115
127
 
116
128
  const panelRef = useRef<HTMLDivElement>(null)
@@ -145,22 +157,13 @@ export function PanelWrapper({
145
157
  // Stale ids (kind changed, plugin gated off) fall back to regular mode.
146
158
  const activeExtension = resolveActiveExtension(activeExtensionId, extensions)
147
159
 
148
- // The whole panel is collapsed to just its header by default; the chevron
149
- // expands it to reveal the inspector body. Keep the desktop value shared
150
- // across inspector swaps (roof ↔ segment, etc.) so navigating between
151
- // related panels preserves whether the user left the inspector open.
152
- const [collapsed, setCollapsedState] = useState(desktopInspectorCollapsed)
153
-
154
- const setCollapsed = useCallback(
155
- (next: boolean | ((previous: boolean) => boolean)) => {
156
- setCollapsedState((previous) => {
157
- const resolved = typeof next === 'function' ? next(previous) : next
158
- desktopInspectorCollapsed = resolved
159
- return resolved
160
- })
161
- },
162
- [],
163
- )
160
+ // The panel is collapsed to just its header until the user expands it. The
161
+ // choice is one editor preference for every inspector (room, wall, item…),
162
+ // persisted, so every panel opens the way the user last left one.
163
+ const collapsed = !useInspectorExpanded((state) => state.expanded)
164
+ const setCollapsed = useCallback((next: boolean) => {
165
+ useInspectorExpanded.getState().setExpanded(!next)
166
+ }, [])
164
167
 
165
168
  const applyMode = useCallback(
166
169
  (next: { collapsed: boolean; activeExtensionId: string | null }) => {
@@ -205,8 +208,8 @@ export function PanelWrapper({
205
208
 
206
209
  const handleHeaderPointerDown = useCallback(
207
210
  (e: React.PointerEvent<HTMLDivElement>) => {
208
- // Buttons (close / reset / collapse) handle their own clicks.
209
- if ((e.target as HTMLElement).closest('button')) return
211
+ // Buttons (close / reset / collapse) and a title edited in place handle their own clicks.
212
+ if ((e.target as HTMLElement).closest('button, input, label')) return
210
213
  const rect = panelRef.current?.getBoundingClientRect()
211
214
  if (!rect) return
212
215
  const bounds = getDragBounds(panelRef.current)
@@ -282,6 +285,10 @@ export function PanelWrapper({
282
285
  className={cn(
283
286
  isMobile
284
287
  ? 'flex h-full w-full flex-col overflow-hidden bg-transparent dark:text-foreground'
288
+ : inStack
289
+ ? // In the shared right column: it grows down and scrolls on itself,
290
+ // leaving the shortcuts card below it its room (`right-stack.tsx`).
291
+ 'pointer-events-auto flex max-h-full min-h-0 flex-[0_1_auto] flex-col overflow-hidden rounded-xl border border-border/50 bg-sidebar/95 shadow-2xl backdrop-blur-xl dark:text-foreground'
285
292
  // Cap height at `100dvh - 154px` so a tall panel's bottom edge
286
293
  // aligns flush with the top of the floating bottom action bar.
287
294
  // Combined with `top-20` (80px), the panel's bottom sits at
@@ -292,6 +299,7 @@ export function PanelWrapper({
292
299
  : 'pointer-events-auto fixed top-20 right-4 z-50 flex max-h-[calc(100dvh-154px)] flex-col overflow-hidden rounded-xl border border-border/50 bg-sidebar/95 shadow-2xl backdrop-blur-xl dark:text-foreground',
293
300
  className,
294
301
  )}
302
+ data-panel-wrapper
295
303
  ref={panelRef}
296
304
  style={
297
305
  isMobile
@@ -311,11 +319,12 @@ export function PanelWrapper({
311
319
  !collapsed && 'border-border/50 border-b',
312
320
  isDragging ? 'cursor-grabbing' : 'cursor-grab',
313
321
  )}
322
+ data-panel-header
314
323
  onPointerDown={handleHeaderPointerDown}
315
324
  onPointerMove={handleHeaderPointerMove}
316
325
  onPointerUp={handleHeaderPointerUp}
317
326
  >
318
- <div className="flex min-w-0 items-center gap-2">
327
+ <div className={cn('flex min-w-0 items-center gap-2', titleContent && 'flex-1 pr-2')}>
319
328
  {onBack && (
320
329
  <button
321
330
  className="mr-1 flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-[#3e3e3e] hover:text-foreground"
@@ -327,7 +336,7 @@ export function PanelWrapper({
327
336
  )}
328
337
  {icon &&
329
338
  (typeof icon === 'string' ? (
330
- <Image
339
+ <NodeIconImage
331
340
  alt=""
332
341
  className="shrink-0 object-contain"
333
342
  height={16}
@@ -337,13 +346,30 @@ export function PanelWrapper({
337
346
  ) : (
338
347
  <span className="flex shrink-0 items-center justify-center">{icon}</span>
339
348
  ))}
340
- <h2 className="truncate font-semibold text-foreground text-sm tracking-tight">
341
- {title}
342
- </h2>
349
+ {kicker || titleContent ? (
350
+ <div className="flex min-w-0 flex-1 flex-col">
351
+ {kicker && (
352
+ <span className="truncate text-[11px] text-muted-foreground leading-tight" data-panel-kicker>
353
+ {kicker}
354
+ </span>
355
+ )}
356
+ {titleContent ?? (
357
+ <h2 className="truncate font-semibold text-foreground text-sm tracking-tight">
358
+ {title}
359
+ </h2>
360
+ )}
361
+ </div>
362
+ ) : (
363
+ <h2 className="truncate font-semibold text-foreground text-sm tracking-tight">
364
+ {title}
365
+ </h2>
366
+ )}
343
367
  </div>
344
368
 
345
- {/* Centered grip — purely a visual drag affordance. */}
346
- <GripHorizontal className="-translate-x-1/2 pointer-events-none absolute left-1/2 h-4 w-4 text-muted-foreground/40" />
369
+ {/* Centered grip — purely a visual drag affordance (a title edited in place takes its room). */}
370
+ {!titleContent && (
371
+ <GripHorizontal className="-translate-x-1/2 pointer-events-none absolute left-1/2 h-4 w-4 text-muted-foreground/40" />
372
+ )}
347
373
 
348
374
  <div className="flex items-center gap-1">
349
375
  {onReset && (
@@ -413,7 +439,7 @@ export function PanelWrapper({
413
439
  controls (`children`). A stale extension id falls back to regular
414
440
  via `resolveActiveExtension`. */}
415
441
  {!(collapsed && !isMobile) && (
416
- <div className="no-scrollbar flex min-h-0 flex-1 flex-col overflow-y-auto">
442
+ <div className="no-scrollbar flex min-h-0 flex-1 flex-col overflow-y-auto" data-panel-scroll>
417
443
  {!isMobile && selectedId && activeExtension ? (
418
444
  <InspectorExtensionSection
419
445
  extension={activeExtension}
@@ -422,7 +448,23 @@ export function PanelWrapper({
422
448
  />
423
449
  ) : (
424
450
  <>
451
+ {/* The mobile sheet has its own header: a title edited in place moves into the body. */}
452
+ {isMobile && titleContent && (
453
+ <div className="flex flex-col px-4 pt-3">
454
+ {kicker && <span className="text-[11px] text-muted-foreground">{kicker}</span>}
455
+ {titleContent}
456
+ </div>
457
+ )}
425
458
  {children}
459
+ {inspectedIds.length > 0 && (
460
+ <PanelSection title="Collections">
461
+ <ActionGroup>
462
+ <CollectionsPopover nodeIds={inspectedIds}>
463
+ <ActionButton label="Manage collections…" />
464
+ </CollectionsPopover>
465
+ </ActionGroup>
466
+ </PanelSection>
467
+ )}
426
468
  {/* Mobile sheet has no header icons to swap modes — keep the
427
469
  plugin sections appended after the kind's controls there. */}
428
470
  {isMobile &&
@@ -453,7 +495,7 @@ export function PanelWrapper({
453
495
  * inspector's `renderIcon` (plain <img> so no next/image server deps). */
454
496
  function renderExtensionIcon(ref: IconRef): React.ReactNode {
455
497
  if (ref.kind === 'url') {
456
- return <img alt="" className="h-4 w-4 shrink-0 object-contain" src={ref.src} />
498
+ return <IconRefImage className="h-4 w-4 shrink-0" src={ref.src} />
457
499
  }
458
500
  if (ref.kind === 'iconify') {
459
501
  return <Icon height={16} icon={ref.name} width={16} />
@@ -46,7 +46,7 @@ export function ParametricFieldControl({
46
46
  }
47
47
 
48
48
  case 'boolean': {
49
- const checked = !mixed && value === true
49
+ const checked = !mixed && (value ?? field.default) === true
50
50
  return (
51
51
  <ToggleControl
52
52
  checked={checked}
@@ -19,6 +19,7 @@ import { commitParametricNodeFields } from '../../../lib/parametric-node-update'
19
19
  import { sfxEmitter } from '../../../lib/sfx-bus'
20
20
  import { collectZoneContentIds } from '../../../lib/zone-content'
21
21
  import useEditor from '../../../store/use-editor'
22
+ import { IconRefImage } from '../icon-ref'
22
23
  import { ActionButton, ActionGroup } from '../controls/action-button'
23
24
  import { PanelSection } from '../controls/panel-section'
24
25
  import { ParametricFieldControl } from './parametric-field-control'
@@ -236,10 +237,9 @@ function ParamActionButton({ action, nodeId }: { action: ParamAction<AnyNode>; n
236
237
  function renderIcon(ref: IconRef | undefined): React.ReactNode | undefined {
237
238
  if (!ref) return undefined
238
239
  if (ref.kind === 'url') {
239
- // Plain <img> here so the inspector doesn't pull in next/image's
240
- // server-only requirements (the file is `'use client'`). Same
241
- // 16x16 box the legacy panels use.
242
- return <img alt="" className="h-4 w-4 shrink-0 object-contain" src={ref.src} />
240
+ // No next/image here: the inspector is `'use client'` and next/image
241
+ // drags in server-only requirements. Same 16x16 box the legacy panels use.
242
+ return <IconRefImage className="h-4 w-4 shrink-0" src={ref.src} />
243
243
  }
244
244
  if (ref.kind === 'iconify') {
245
245
  return <Icon height={16} icon={ref.name} width={16} />
@@ -25,6 +25,7 @@ import { guideEmitter } from '../../../lib/guide-events'
25
25
  import { getGuideImageName } from '../../../lib/local-guide-image'
26
26
  import { cn } from '../../../lib/utils'
27
27
  import useEditor from '../../../store/use-editor'
28
+ import { isViewVisible, VIEW_2D } from '../../../store/view-layout'
28
29
  import { ActionButton, ActionGroup } from '../controls/action-button'
29
30
  import { PanelSection } from '../controls/panel-section'
30
31
  import { SliderControl } from '../controls/slider-control'
@@ -134,9 +135,7 @@ export function ReferencePanel() {
134
135
  // The scale line is drawn on the 2D plan — starting from a 3D-only view
135
136
  // would arm the flow invisibly inside the hidden floorplan panel.
136
137
  const editor = useEditor.getState()
137
- if (editor.viewMode === '3d') {
138
- editor.setViewMode('2d')
139
- }
138
+ if (!isViewVisible(editor, VIEW_2D)) editor.showView(VIEW_2D)
140
139
 
141
140
  guideEmitter.emit('guide:set-reference-scale', { guideId: node.id })
142
141
  }, [node])