@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
@@ -19,7 +19,9 @@ import {
19
19
  Copy,
20
20
  Eye,
21
21
  EyeOff,
22
+ Boxes,
22
23
  Group,
24
+ LayoutGrid,
23
25
  Loader2,
24
26
  MoreHorizontal,
25
27
  Pencil,
@@ -29,7 +31,18 @@ import {
29
31
  X,
30
32
  } from 'lucide-react'
31
33
  import { AnimatePresence, LayoutGroup, motion } from 'motion/react'
32
- import { memo, useEffect, useRef, useState, useSyncExternalStore } from 'react'
34
+ import { type ComponentType, lazy, memo, Suspense, useEffect, useRef, useState, useSyncExternalStore } from 'react'
35
+ import { nodeRegistry } from '@pascal-app/core'
36
+
37
+ /** The site kind's inspector (`parametrics.customPanel`), mounted under the Site header. */
38
+ const SiteKindPanel = lazy(async () => {
39
+ const def = nodeRegistry.get('site') as
40
+ | { parametrics?: { customPanel?: () => Promise<{ default: ComponentType }> } }
41
+ | undefined
42
+ const loader = def?.parametrics?.customPanel
43
+ if (!loader) return { default: () => null }
44
+ return loader()
45
+ })
33
46
  import { useShallow } from 'zustand/react/shallow'
34
47
  import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
35
48
  import {
@@ -40,8 +53,8 @@ import {
40
53
  import {
41
54
  buildLevelDuplicateCreateOps,
42
55
  type LevelDuplicatePreset,
43
- } from './../../../../../lib/level-duplication'
44
- import { getDefaultLevelName } from '@pascal-app/core'
56
+ } from '@pascal-app/core/building'
57
+ import { getDefaultLevelName, getLevelDisplayName } from '@pascal-app/core'
45
58
  import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
46
59
  import {
47
60
  formatAreaLabel,
@@ -53,12 +66,19 @@ import {
53
66
  } from './../../../../../lib/measurements'
54
67
  import { createLocalGuideImage, createLocalScan } from './../../../../../lib/local-guide-image'
55
68
  import { editorHostTreeChildrenRegistry } from './../../../../../lib/host-tree-children'
69
+ import { requestRoomDeletion } from './../../../../../lib/room-structure-commands'
70
+ import {
71
+ selectZoneOrRoom,
72
+ useZoneSelected,
73
+ zoneKindLabel,
74
+ } from './../../../../../lib/room-zone-routing'
56
75
  import { createUnitInBuilding, toggleZoneMembership } from './../../../../../lib/units'
57
76
  import { cn } from './../../../../../lib/utils'
58
77
  import useEditor from './../../../../../store/use-editor'
59
78
  import { useUploadStore } from '../../../../../store/use-upload'
60
79
  import { MetricControl } from '../../../controls/metric-control'
61
80
  import { LevelDuplicateDialog } from '../../../level-duplicate-dialog'
81
+ import { CollectionsSection } from './collection-tree-node'
62
82
  import { InlineRenameInput } from './inline-rename-input'
63
83
  import { ZoneMembershipCheckbox } from './zone-membership-checkbox'
64
84
  import { focusTreeNode, TreeNode, TreeNodeWrapper } from './tree-node'
@@ -719,8 +739,7 @@ const LevelItem = memo(function LevelItem({
719
739
  const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false)
720
740
  const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
721
741
  const [isEditing, setIsEditing] = useState(false)
722
- const createNodes = useScene((s) => s.createNodes)
723
- const updateNodes = useScene((s) => s.updateNodes)
742
+ const applyNodeChanges = useScene((s) => s.applyNodeChanges)
724
743
  const itemRef = useRef<HTMLDivElement>(null)
725
744
  const isSelected = selectedLevelId === level.id
726
745
  const canDeleteLevel = level.level !== 0
@@ -754,22 +773,14 @@ const LevelItem = memo(function LevelItem({
754
773
  }
755
774
 
756
775
  const handleDuplicateLevel = (preset: LevelDuplicatePreset = 'everything') => {
757
- const { createOps, newLevelId, shiftedLevels } = buildLevelDuplicateCreateOps({
776
+ const { createOps, newLevelId, updateOps } = buildLevelDuplicateCreateOps({
758
777
  nodes: useScene.getState().nodes,
759
778
  level,
760
779
  levels,
761
780
  preset,
762
781
  })
763
782
 
764
- if (shiftedLevels.length > 0) {
765
- updateNodes(
766
- shiftedLevels.map((shiftedLevel) => ({
767
- id: shiftedLevel.id as AnyNodeId,
768
- data: { level: shiftedLevel.level } as Partial<AnyNode>,
769
- })),
770
- )
771
- }
772
- createNodes(createOps)
783
+ applyNodeChanges({ create: createOps, update: updateOps })
773
784
  selectLevel(newLevelId as LevelNode['id'], false)
774
785
  setDuplicateDialogOpen(false)
775
786
  }
@@ -838,6 +849,7 @@ const LevelItem = memo(function LevelItem({
838
849
  />
839
850
  <InlineRenameInput
840
851
  defaultName={getDefaultLevelName(level.level)}
852
+ displayName={getLevelDisplayName(level)}
841
853
  isEditing={isEditing}
842
854
  nodeId={level.id}
843
855
  onStartEditing={() => setIsEditing(true)}
@@ -1149,137 +1161,15 @@ const UnitsSection = memo(function UnitsSection({
1149
1161
  )
1150
1162
  })
1151
1163
 
1152
- const LayerToggle = memo(function LayerToggle() {
1153
- const structureLayer = useEditor((state) => state.structureLayer)
1154
- const setStructureLayer = useEditor((state) => state.setStructureLayer)
1155
- const phase = useEditor((state) => state.phase)
1156
- const setPhase = useEditor((state) => state.setPhase)
1157
-
1158
- const activeTab =
1159
- phase === 'structure' && structureLayer === 'elements'
1160
- ? 'structure'
1161
- : phase === 'furnish'
1162
- ? 'furnish'
1163
- : phase === 'structure' && structureLayer === 'zones'
1164
- ? 'zones'
1165
- : 'none'
1166
-
1167
- return (
1168
- <div className="relative flex items-center gap-1 border-border/50 border-b bg-[#2C2C2E] p-1">
1169
- <button
1170
- className={cn(
1171
- 'relative flex flex-1 cursor-pointer flex-col items-center justify-center rounded-md py-2 font-medium text-[10px] transition-all duration-200',
1172
- activeTab === 'structure'
1173
- ? 'text-foreground'
1174
- : 'text-muted-foreground hover:bg-white/5 hover:text-foreground',
1175
- )}
1176
- onClick={() => {
1177
- setPhase('structure')
1178
- setStructureLayer('elements')
1179
- }}
1180
- >
1181
- {activeTab === 'structure' && (
1182
- <motion.div
1183
- className="absolute inset-0 rounded-md bg-[#3e3e3e] shadow-sm ring-1 ring-border/50"
1184
- layoutId="layerToggleActiveBg"
1185
- transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }}
1186
- />
1187
- )}
1188
- <div className="relative z-10 flex flex-col items-center">
1189
- <img
1190
- alt="Structure"
1191
- className={cn(
1192
- 'mb-1 h-6 w-6 transition-all',
1193
- activeTab !== 'structure' && 'opacity-50 grayscale',
1194
- )}
1195
- src="/icons/room.webp"
1196
- />
1197
- Structure
1198
- </div>
1199
- <div className="absolute right-1.5 bottom-1 z-10 rounded border border-border/40 bg-background/40 px-1 py-[2px] backdrop-blur-md">
1200
- <span className="block font-medium font-mono text-[9px] text-muted-foreground/70 leading-none">
1201
- B
1202
- </span>
1203
- </div>
1204
- </button>
1205
-
1206
- <button
1207
- className={cn(
1208
- 'relative flex flex-1 cursor-pointer flex-col items-center justify-center rounded-md py-2 font-medium text-[10px] transition-all duration-200',
1209
- activeTab === 'furnish'
1210
- ? 'text-foreground'
1211
- : 'text-muted-foreground hover:bg-white/5 hover:text-foreground',
1212
- )}
1213
- onClick={() => {
1214
- setPhase('furnish')
1215
- }}
1216
- >
1217
- {activeTab === 'furnish' && (
1218
- <motion.div
1219
- className="absolute inset-0 rounded-md bg-[#3e3e3e] shadow-sm ring-1 ring-border/50"
1220
- layoutId="layerToggleActiveBg"
1221
- transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }}
1222
- />
1223
- )}
1224
- <div className="relative z-10 flex flex-col items-center">
1225
- <img
1226
- alt="Furnish"
1227
- className={cn(
1228
- 'mb-1 h-6 w-6 transition-all',
1229
- activeTab !== 'furnish' && 'opacity-50 grayscale',
1230
- )}
1231
- src="/icons/couch.webp"
1232
- />
1233
- Furnish
1234
- </div>
1235
- <div className="absolute right-1.5 bottom-1 z-10 rounded border border-border/40 bg-background/40 px-1 py-[2px] backdrop-blur-md">
1236
- <span className="block font-medium font-mono text-[9px] text-muted-foreground/70 leading-none">
1237
- F
1238
- </span>
1239
- </div>
1240
- </button>
1241
-
1242
- <button
1243
- className={cn(
1244
- 'relative flex flex-1 cursor-pointer flex-col items-center justify-center rounded-md py-2 font-medium text-[10px] transition-all duration-200',
1245
- activeTab === 'zones'
1246
- ? 'text-foreground'
1247
- : 'text-muted-foreground hover:bg-white/5 hover:text-foreground',
1248
- )}
1249
- onClick={() => {
1250
- setPhase('structure')
1251
- setStructureLayer('zones')
1252
- }}
1253
- >
1254
- {activeTab === 'zones' && (
1255
- <motion.div
1256
- className="absolute inset-0 rounded-md bg-[#3e3e3e] shadow-sm ring-1 ring-border/50"
1257
- layoutId="layerToggleActiveBg"
1258
- transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }}
1259
- />
1260
- )}
1261
- <div className="relative z-10 flex flex-col items-center">
1262
- <img
1263
- alt="Zones"
1264
- className={cn(
1265
- 'mb-1 h-6 w-6 transition-all',
1266
- activeTab !== 'zones' && 'opacity-50 grayscale',
1267
- )}
1268
- src="/icons/kitchen.webp"
1269
- />
1270
- Zones
1271
- </div>
1272
- <div className="absolute right-1.5 bottom-1 z-10 rounded border border-border/40 bg-background/40 px-1 py-[2px] backdrop-blur-md">
1273
- <span className="block font-medium font-mono text-[9px] text-muted-foreground/70 leading-none">
1274
- Z
1275
- </span>
1276
- </div>
1277
- </button>
1278
- </div>
1279
- )
1280
- })
1281
-
1282
- const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLast?: boolean }) {
1164
+ const ZoneItem = memo(function ZoneItem({
1165
+ zone,
1166
+ depth,
1167
+ isLast,
1168
+ }: {
1169
+ zone: ZoneNode
1170
+ depth: number
1171
+ isLast?: boolean
1172
+ }) {
1283
1173
  const [isEditing, setIsEditing] = useState(false)
1284
1174
  const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false)
1285
1175
  const deleteNode = useScene((state) => state.deleteNode)
@@ -1289,15 +1179,14 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
1289
1179
  const unit = focusedUnitId ? s.nodes[focusedUnitId] : undefined
1290
1180
  return unit?.type === 'unit' ? unit : null
1291
1181
  })
1292
- const selectedZoneId = useViewer((state) => state.selection.zoneId)
1293
1182
  const hoveredId = useViewer((state) => state.hoveredId)
1294
1183
  const setSelection = useViewer((state) => state.setSelection)
1295
1184
  const setHoveredId = useViewer((state) => state.setHoveredId)
1296
- const setPhase = useEditor((state) => state.setPhase)
1297
1185
  const setMode = useEditor((state) => state.setMode)
1298
1186
  const unit = useViewer((state) => state.unit)
1299
1187
 
1300
- const isSelected = selectedZoneId === zone.id
1188
+ const isSelected = useZoneSelected(zone.id)
1189
+ const isRoom = zone.spaceRole === 'room'
1301
1190
  const isHovered = hoveredId === zone.id
1302
1191
 
1303
1192
  const itemRef = useRef<HTMLDivElement>(null)
@@ -1308,12 +1197,11 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
1308
1197
  }
1309
1198
  }, [isSelected])
1310
1199
 
1311
- const defaultName = `Zone (${formatAreaLabel(calculatePolygonArea(zone.polygon), unit)})`
1200
+ const defaultName = `${zoneKindLabel(zone)} (${formatAreaLabel(calculatePolygonArea(zone.polygon), unit)})`
1312
1201
 
1313
1202
  const handleClick = () => {
1314
- setSelection({ zoneId: zone.id })
1315
- setPhase('structure')
1316
1203
  setMode('select')
1204
+ selectZoneOrRoom(zone.id)
1317
1205
  }
1318
1206
 
1319
1207
  const handleDoubleClick = () => {
@@ -1322,6 +1210,8 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
1322
1210
 
1323
1211
  const handleDelete = (e: React.MouseEvent) => {
1324
1212
  e.stopPropagation()
1213
+ // A room goes the way the room panel deletes it (its walls, or merged back).
1214
+ if (isRoom) return requestRoomDeletion(zone.id)
1325
1215
  deleteNode(zone.id)
1326
1216
  if (isSelected) {
1327
1217
  setSelection({ zoneId: null })
@@ -1335,7 +1225,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
1335
1225
  return (
1336
1226
  <div
1337
1227
  className={cn(
1338
- 'group/row relative flex h-8 cursor-pointer select-none items-center border-border/50 border-b px-3 text-sm transition-all duration-200',
1228
+ 'group/row relative flex h-8 cursor-pointer select-none items-center border-border/50 border-b text-sm transition-all duration-200',
1339
1229
  isSelected
1340
1230
  ? 'bg-accent/50 text-foreground'
1341
1231
  : isHovered
@@ -1347,6 +1237,7 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
1347
1237
  onMouseEnter={() => setHoveredId(zone.id)}
1348
1238
  onMouseLeave={() => setHoveredId(null)}
1349
1239
  ref={itemRef}
1240
+ style={{ paddingLeft: depth * 12 + 28, paddingRight: 12 }}
1350
1241
  >
1351
1242
  {/* Vertical tree line */}
1352
1243
  <div
@@ -1354,12 +1245,12 @@ const ZoneItem = memo(function ZoneItem({ zone, isLast }: { zone: ZoneNode; isLa
1354
1245
  'pointer-events-none absolute w-px bg-border/50',
1355
1246
  isLast ? 'top-0 bottom-1/2' : 'top-0 bottom-0',
1356
1247
  )}
1357
- style={{ left: 8 }}
1248
+ style={{ left: (depth - 1) * 12 + 20 }}
1358
1249
  />
1359
1250
  {/* Horizontal branch line */}
1360
1251
  <div
1361
1252
  className="pointer-events-none absolute top-1/2 h-px bg-border/50"
1362
- style={{ left: 8, width: 4 }}
1253
+ style={{ left: (depth - 1) * 12 + 20, width: 4 }}
1363
1254
  />
1364
1255
 
1365
1256
  {focusedUnit && (
@@ -1476,17 +1367,9 @@ const MultiSelectionBadge = memo(function MultiSelectionBadge() {
1476
1367
  )
1477
1368
  })
1478
1369
 
1479
- const ContentSection = memo(function ContentSection() {
1370
+ const RoomsSection = memo(function RoomsSection() {
1480
1371
  const selectedLevelId = useViewer((state) => state.selection.levelId)
1481
- const structureLayer = useEditor((state) => state.structureLayer)
1482
- const phase = useEditor((state) => state.phase)
1483
- const setPhase = useEditor((state) => state.setPhase)
1484
- const setMode = useEditor((state) => state.setMode)
1485
- const setTool = useEditor((state) => state.setTool)
1486
-
1487
- const level = useScene((s) =>
1488
- selectedLevelId ? ((s.nodes[selectedLevelId] as LevelNode | undefined) ?? null) : null,
1489
- )
1372
+ const armToolMode = useEditor((state) => state.armToolMode)
1490
1373
  const levelZones = useScene(
1491
1374
  useShallow((s) => {
1492
1375
  if (!selectedLevelId) return []
@@ -1495,6 +1378,82 @@ const ContentSection = memo(function ContentSection() {
1495
1378
  )
1496
1379
  }),
1497
1380
  )
1381
+ const [expanded, setExpanded] = useState(true)
1382
+
1383
+ if (!selectedLevelId) return null
1384
+
1385
+ const handleDrawZone = (event: React.MouseEvent) => {
1386
+ event.stopPropagation()
1387
+ armToolMode({ mode: 'build', tool: 'zone' })
1388
+ }
1389
+
1390
+ const rooms = levelZones.filter((zone) => zone.spaceRole === 'room')
1391
+ const others = levelZones.filter((zone) => zone.spaceRole !== 'room')
1392
+
1393
+ return (
1394
+ <div className="subtle-scrollbar max-h-72 shrink-0 overflow-y-auto overflow-x-hidden">
1395
+ <TreeNodeWrapper
1396
+ actions={
1397
+ <button
1398
+ className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-black/5 hover:text-foreground dark:hover:bg-white/10"
1399
+ onClick={handleDrawZone}
1400
+ title="Draw a zone"
1401
+ type="button"
1402
+ >
1403
+ <Plus className="h-3 w-3" />
1404
+ </button>
1405
+ }
1406
+ depth={1}
1407
+ expanded={expanded}
1408
+ hasChildren
1409
+ icon={<LayoutGrid className="h-3.5 w-3.5" />}
1410
+ label={
1411
+ <span className="flex items-center gap-1.5">
1412
+ Rooms
1413
+ {rooms.length > 0 && (
1414
+ <span className="text-muted-foreground text-xs">{rooms.length}</span>
1415
+ )}
1416
+ </span>
1417
+ }
1418
+ onClick={() => setExpanded((value) => !value)}
1419
+ onToggle={() => setExpanded((value) => !value)}
1420
+ >
1421
+ {levelZones.length === 0 && (
1422
+ <div className="border-border/50 border-b py-2 pr-3 pl-[52px] text-muted-foreground text-xs">
1423
+ Close walls around a space to make a room.
1424
+ </div>
1425
+ )}
1426
+ {rooms.map((zone, index) => (
1427
+ <ZoneItem
1428
+ depth={2}
1429
+ isLast={others.length === 0 && index === rooms.length - 1}
1430
+ key={zone.id}
1431
+ zone={zone}
1432
+ />
1433
+ ))}
1434
+ {others.length > 0 && (
1435
+ <>
1436
+ <div
1437
+ className="pt-3 pb-1 pl-[52px] font-medium text-[11px] text-muted-foreground"
1438
+ data-other-zones
1439
+ >
1440
+ Other zones
1441
+ </div>
1442
+ {others.map((zone, index) => (
1443
+ <ZoneItem depth={2} isLast={index === others.length - 1} key={zone.id} zone={zone} />
1444
+ ))}
1445
+ </>
1446
+ )}
1447
+ </TreeNodeWrapper>
1448
+ </div>
1449
+ )
1450
+ })
1451
+
1452
+ const ContentSection = memo(function ContentSection() {
1453
+ const selectedLevelId = useViewer((state) => state.selection.levelId)
1454
+ const level = useScene((s) =>
1455
+ selectedLevelId ? ((s.nodes[selectedLevelId] as LevelNode | undefined) ?? null) : null,
1456
+ )
1498
1457
  const elementChildren = useScene(
1499
1458
  useShallow((s) => {
1500
1459
  if (!selectedLevelId) return []
@@ -1506,6 +1465,7 @@ const ContentSection = memo(function ContentSection() {
1506
1465
  })
1507
1466
  }),
1508
1467
  )
1468
+ const [expanded, setExpanded] = useState(true)
1509
1469
 
1510
1470
  if (!level) {
1511
1471
  return (
@@ -1513,49 +1473,40 @@ const ContentSection = memo(function ContentSection() {
1513
1473
  )
1514
1474
  }
1515
1475
 
1516
- if (structureLayer === 'zones') {
1517
- const handleAddZone = () => {
1518
- setPhase('structure')
1519
- setMode('build')
1520
- setTool('zone')
1521
- }
1522
-
1523
- if (levelZones.length === 0) {
1524
- return (
1525
- <div className="px-3 py-4 text-muted-foreground text-sm">
1526
- No zones on this level.{' '}
1527
- <button className="cursor-pointer text-primary hover:underline" onClick={handleAddZone}>
1528
- Add one
1529
- </button>
1530
- </div>
1531
- )
1532
- }
1533
-
1534
- return (
1535
- <div className="flex flex-col">
1536
- {levelZones.map((zone, index) => (
1537
- <ZoneItem isLast={index === levelZones.length - 1} key={zone.id} zone={zone} />
1538
- ))}
1539
- </div>
1540
- )
1541
- }
1542
-
1543
- if (elementChildren.length === 0) {
1544
- return <div className="px-3 py-4 text-muted-foreground text-sm">No elements on this level</div>
1545
- }
1546
1476
  return (
1547
- <TreeNodeDragProvider>
1548
- <div className="flex flex-col">
1549
- {elementChildren.map((childId, index) => (
1550
- <TreeNode
1551
- depth={0}
1552
- isLast={index === elementChildren.length - 1}
1553
- key={childId}
1554
- nodeId={childId}
1555
- />
1556
- ))}
1557
- </div>
1558
- </TreeNodeDragProvider>
1477
+ <TreeNodeWrapper
1478
+ depth={1}
1479
+ expanded={expanded}
1480
+ hasChildren
1481
+ icon={<Boxes className="h-3.5 w-3.5" />}
1482
+ label={
1483
+ <span className="flex items-center gap-1.5">
1484
+ Elements
1485
+ {elementChildren.length > 0 && (
1486
+ <span className="text-muted-foreground text-xs">{elementChildren.length}</span>
1487
+ )}
1488
+ </span>
1489
+ }
1490
+ onClick={() => setExpanded((value) => !value)}
1491
+ onToggle={() => setExpanded((value) => !value)}
1492
+ >
1493
+ {elementChildren.length === 0 ? (
1494
+ <div className="border-border/50 border-b py-2 pr-3 pl-[52px] text-muted-foreground text-xs">
1495
+ No elements on this level
1496
+ </div>
1497
+ ) : (
1498
+ <TreeNodeDragProvider>
1499
+ {elementChildren.map((childId, index) => (
1500
+ <TreeNode
1501
+ depth={2}
1502
+ isLast={index === elementChildren.length - 1}
1503
+ key={childId}
1504
+ nodeId={childId}
1505
+ />
1506
+ ))}
1507
+ </TreeNodeDragProvider>
1508
+ )}
1509
+ </TreeNodeWrapper>
1559
1510
  )
1560
1511
  })
1561
1512
 
@@ -1591,7 +1542,7 @@ const BuildingItem = memo(function BuildingItem({
1591
1542
  const handleSelect = () => {
1592
1543
  setSelection({ buildingId: building.id })
1593
1544
  if (phase === 'site') {
1594
- setPhase('structure')
1545
+ setPhase('building')
1595
1546
  }
1596
1547
  }
1597
1548
 
@@ -1713,7 +1664,8 @@ const BuildingItem = memo(function BuildingItem({
1713
1664
  projectId={projectId}
1714
1665
  />
1715
1666
  <UnitsSection buildingId={building.id} />
1716
- <LayerToggle />
1667
+ <RoomsSection />
1668
+ <CollectionsSection />
1717
1669
  </div>
1718
1670
  <div className="subtle-scrollbar relative min-h-0 flex-1 overflow-y-auto overflow-x-hidden">
1719
1671
  <MultiSelectionBadge />
@@ -1769,7 +1721,14 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
1769
1721
  : 'text-muted-foreground hover:bg-accent/30 hover:text-foreground',
1770
1722
  )}
1771
1723
  layout="position"
1772
- onClick={() => setPhase('site')}
1724
+ onClick={() => {
1725
+ setPhase('site')
1726
+ // Also SELECT the site node so the registry inspector mounts the
1727
+ // site kind's panel (address / parcel / setbacks / front edge).
1728
+ // The site kind opts out of 3D click selection, so this header is
1729
+ // its only selection entry point. (WS1)
1730
+ setSelection({ selectedIds: [siteNode.id as AnyNodeId], zoneId: null })
1731
+ }}
1773
1732
  >
1774
1733
  <div className="flex items-center gap-2">
1775
1734
  <img
@@ -1811,6 +1770,12 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
1811
1770
  transition={{ type: 'spring', bounce: 0, duration: 0.4 }}
1812
1771
  >
1813
1772
  <PropertyLineSection />
1773
+ {/* Address → parcel, setbacks, front edge: the site kind's own
1774
+ panel (packages/nodes/src/site/panel.tsx), loaded through
1775
+ the registry so the editor does not import the nodes package. */}
1776
+ <Suspense fallback={null}>
1777
+ <SiteKindPanel />
1778
+ </Suspense>
1814
1779
  </motion.div>
1815
1780
  )}
1816
1781
  </AnimatePresence>
@@ -1824,7 +1789,7 @@ export function SitePanel({ projectId, onUploadAsset, onDeleteAsset }: SitePanel
1824
1789
  <div className="flex min-h-0 flex-1 flex-col">
1825
1790
  {buildings.map((building) => {
1826
1791
  const isBuildingActive =
1827
- (phase === 'structure' || phase === 'furnish') &&
1792
+ phase === 'building' &&
1828
1793
  selectedBuildingId === building.id
1829
1794
 
1830
1795
  return (
@@ -3,25 +3,47 @@ import { Pencil } from 'lucide-react'
3
3
  import { memo, useCallback, useEffect, useRef, useState } from 'react'
4
4
  import { cn } from './../../../../../lib/utils'
5
5
 
6
- interface InlineRenameInputProps {
7
- nodeId: AnyNodeId
6
+ interface InlineRenameFieldProps {
7
+ name: string | undefined
8
+ /** Receives the trimmed name, or `undefined` when cleared. */
9
+ onRename: (name: string | undefined) => void
8
10
  isEditing: boolean
9
11
  onStopEditing: () => void
10
12
  defaultName: string
13
+ /** Shown instead of the stored name when not editing (a level's display name). */
14
+ displayName?: string
11
15
  className?: string
12
16
  onStartEditing?: () => void
13
17
  }
14
18
 
19
+ type InlineRenameInputProps = Omit<InlineRenameFieldProps, 'name' | 'onRename'> & {
20
+ nodeId: AnyNodeId
21
+ }
22
+
23
+ /** Renames a scene node through `updateNode`. */
15
24
  export const InlineRenameInput = memo(function InlineRenameInput({
16
25
  nodeId,
26
+ ...props
27
+ }: InlineRenameInputProps) {
28
+ const updateNode = useScene((s) => s.updateNode)
29
+ const name = useScene((s) => s.nodes[nodeId]?.name)
30
+ const handleRename = useCallback(
31
+ (next: string | undefined) => updateNode(nodeId, { name: next }),
32
+ [nodeId, updateNode],
33
+ )
34
+ return <InlineRenameField {...props} name={name} onRename={handleRename} />
35
+ })
36
+
37
+ export const InlineRenameField = memo(function InlineRenameField({
38
+ name,
39
+ onRename,
17
40
  isEditing,
18
41
  onStopEditing,
19
42
  defaultName,
43
+ displayName,
20
44
  className,
21
45
  onStartEditing,
22
- }: InlineRenameInputProps) {
23
- const updateNode = useScene((s) => s.updateNode)
24
- const name = useScene((s) => s.nodes[nodeId]?.name)
46
+ }: InlineRenameFieldProps) {
25
47
  const [value, setValue] = useState(name || '')
26
48
  const inputRef = useRef<HTMLInputElement>(null)
27
49
  const inputSize = Math.max((value || defaultName).length, 1)
@@ -42,10 +64,10 @@ export const InlineRenameInput = memo(function InlineRenameInput({
42
64
  const handleSave = useCallback(() => {
43
65
  const trimmed = value.trim()
44
66
  if (trimmed !== name) {
45
- updateNode(nodeId, { name: trimmed || undefined })
67
+ onRename(trimmed || undefined)
46
68
  }
47
69
  onStopEditing()
48
- }, [value, nodeId, name, updateNode, onStopEditing])
70
+ }, [value, name, onRename, onStopEditing])
49
71
 
50
72
  const handleKeyDown = (e: React.KeyboardEvent) => {
51
73
  if (e.key === 'Enter') {
@@ -61,7 +83,7 @@ export const InlineRenameInput = memo(function InlineRenameInput({
61
83
  return (
62
84
  <div className="group/rename flex h-5 min-w-0 items-center gap-1">
63
85
  <span className={cn('truncate border-transparent border-b', className)}>
64
- {name || defaultName}
86
+ {displayName ?? (name || defaultName)}
65
87
  </span>
66
88
  {onStartEditing && (
67
89
  <button
@@ -8,7 +8,6 @@ import { InlineRenameInput } from './inline-rename-input'
8
8
  import {
9
9
  focusTreeNode,
10
10
  handleTreeSelection,
11
- routeTreeSelectionToNode,
12
11
  TreeNode,
13
12
  TreeNodeWrapper,
14
13
  } from './tree-node'
@@ -71,9 +70,8 @@ export const ItemTreeNode = memo(function ItemTreeNode({
71
70
  (e: React.MouseEvent) => {
72
71
  e.stopPropagation()
73
72
  handleTreeSelection(e, nodeId, useViewer.getState().selection.selectedIds, setSelection)
74
- routeTreeSelectionToNode(node)
75
73
  },
76
- [node, nodeId, setSelection],
74
+ [nodeId, setSelection],
77
75
  )
78
76
 
79
77
  const handleDoubleClick = useCallback(() => focusTreeNode(nodeId), [nodeId])