@pascal-app/editor 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,175 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ buildUnitReport,
6
+ sceneRegistry,
7
+ useScene,
8
+ type ZoneNode,
9
+ } from '@pascal-app/core'
10
+ import { useViewer } from '@pascal-app/viewer'
11
+ import { useEffect, useMemo } from 'react'
12
+ import { useShallow } from 'zustand/react/shallow'
13
+ import { resolveRoomAssemblyHeights } from '../../lib/room-assembly-overlay'
14
+ import { roomFloorElevation } from '../../lib/room-handle-drag'
15
+ import { describeZoneOutline, type RoomSelectionRecord } from '../../lib/room-selection'
16
+ import {
17
+ selectViewerRoom,
18
+ type ViewerSource,
19
+ viewerRooms,
20
+ zoneLabel,
21
+ } from '../../lib/viewer-selection'
22
+ import { RoomAssemblyMesh } from '../editor/room-highlight'
23
+ import {
24
+ RoomLabelAnchor3D,
25
+ RoomPillChip,
26
+ RoomPillsLayer3D,
27
+ roomLabelEntries,
28
+ roomLabelsVisible,
29
+ useFadePresence,
30
+ } from '../editor/room-labels'
31
+
32
+ /**
33
+ * Rooms on every viewer surface, as the editor shows them: the editor's pills
34
+ * on the current floor while nothing but the floor is selected, and the
35
+ * editor's room highlight on the selected or hovered room (or a focused
36
+ * unit's rooms). Zones draw no tinted volume here, and their meshes are not
37
+ * mounted at all, so they never catch a click.
38
+ */
39
+ export function ViewerRooms({ source }: { source: ViewerSource }) {
40
+ useEffect(() => {
41
+ const before = useViewer.getState().showZones
42
+ useViewer.getState().setShowZones(false)
43
+ return () => useViewer.getState().setShowZones(before)
44
+ }, [])
45
+ const { levelId, zoneId, selectedIds } = useViewer(
46
+ useShallow((s) => ({
47
+ levelId: s.selection.levelId,
48
+ zoneId: s.selection.zoneId,
49
+ selectedIds: s.selection.selectedIds,
50
+ })),
51
+ )
52
+ const focusedUnitId = useViewer((s) => s.focusedUnitId)
53
+ const hoveredId = useViewer((s) => s.hoveredId)
54
+ const walkthrough = useViewer((s) => s.walkthroughMode)
55
+ const { nodes } = source
56
+
57
+ const pillsShown =
58
+ !!levelId &&
59
+ !walkthrough &&
60
+ roomLabelsVisible({
61
+ phase: 'building',
62
+ mode: 'select',
63
+ scopeIdle: true,
64
+ room: null,
65
+ zoneId,
66
+ focusedUnitId,
67
+ isCaptureMode: false,
68
+ isThumbnailCapture: false,
69
+ selectedTypes: selectedIds.map((id) => nodes[id]?.type),
70
+ })
71
+
72
+ // The layer stays mounted through its fade-out, like the editor's.
73
+ const pillsPresent = useFadePresence(pillsShown)
74
+
75
+ const rooms = useMemo(() => (levelId ? viewerRooms(levelId, nodes) : []), [levelId, nodes])
76
+ const entries = useMemo(() => {
77
+ if (!levelId) return []
78
+ const zones = Object.values(nodes).filter(
79
+ (node): node is ZoneNode =>
80
+ node.type === 'zone' && node.parentId === levelId && node.visible !== false,
81
+ )
82
+ return roomLabelEntries(
83
+ rooms.filter((room) => nodes[room.zoneId]?.visible !== false),
84
+ zones,
85
+ )
86
+ }, [levelId, nodes, rooms])
87
+
88
+ const highlighted = useMemo(() => {
89
+ if (walkthrough) return []
90
+ const ids = new Set<string>()
91
+ if (zoneId) ids.add(zoneId)
92
+ if (hoveredId && nodes[hoveredId]?.type === 'zone') ids.add(hoveredId)
93
+ const unit = focusedUnitId ? nodes[focusedUnitId] : undefined
94
+ if (unit?.type === 'unit') {
95
+ for (const member of buildUnitReport(unit, nodes).members)
96
+ if (member.levelId === levelId) ids.add(member.zoneId)
97
+ }
98
+ return [...ids].flatMap((id): RoomSelectionRecord[] => {
99
+ const zone = nodes[id]
100
+ if (zone?.type !== 'zone' || !zone.parentId) return []
101
+ const detected = viewerRooms(zone.parentId, nodes).find((room) => room.zoneId === id)
102
+ return [detected ?? describeZoneOutline(zone.parentId, zone)]
103
+ })
104
+ }, [focusedUnitId, hoveredId, levelId, nodes, walkthrough, zoneId])
105
+
106
+ return (
107
+ <>
108
+ {pillsPresent && levelId ? (
109
+ <RoomPillsLayer3D
110
+ entries={entries}
111
+ levelObject={() => source.objectFor(levelId)}
112
+ renderLabel={(entry, anchors, elements) => {
113
+ const zone = nodes[entry.zoneId]
114
+ if (zone?.type !== 'zone') return null
115
+ return (
116
+ <RoomLabelAnchor3D
117
+ anchors={anchors}
118
+ entry={entry}
119
+ floorY={roomFloorElevation(nodes as Record<string, AnyNode>, entry.zoneId)}
120
+ >
121
+ <RoomPillChip
122
+ areaM2={entry.area}
123
+ color={zone.color}
124
+ elements={elements}
125
+ name={zoneLabel(zone)}
126
+ onClick={() => selectViewerRoom(zone.id, source)}
127
+ onHover={() => useViewer.getState().setHoveredId(zone.id)}
128
+ onLeave={() => {
129
+ if (useViewer.getState().hoveredId === zone.id)
130
+ useViewer.getState().setHoveredId(null)
131
+ }}
132
+ view="3d"
133
+ zoneId={zone.id}
134
+ />
135
+ </RoomLabelAnchor3D>
136
+ )
137
+ }}
138
+ shown={pillsShown}
139
+ />
140
+ ) : null}
141
+ {highlighted.map((room) => (
142
+ <ViewerRoomHighlight key={room.zoneId} room={room} source={source} />
143
+ ))}
144
+ </>
145
+ )
146
+ }
147
+
148
+ const parametricObject = (id: string) => sceneRegistry.nodes.get(id)
149
+
150
+ /** `ViewerRooms` over the parametric scene store. */
151
+ export function ParametricViewerRooms() {
152
+ const nodes = useScene((s) => s.nodes)
153
+ const source = useMemo<ViewerSource>(() => ({ nodes, objectFor: parametricObject }), [nodes])
154
+ return <ViewerRooms source={source} />
155
+ }
156
+
157
+ function ViewerRoomHighlight({
158
+ room,
159
+ source,
160
+ }: {
161
+ room: RoomSelectionRecord
162
+ source: ViewerSource
163
+ }) {
164
+ const heights = useMemo(
165
+ () => resolveRoomAssemblyHeights(room, source.nodes as Record<string, AnyNode>),
166
+ [room, source.nodes],
167
+ )
168
+ return (
169
+ <RoomAssemblyMesh
170
+ heights={heights}
171
+ levelObject={() => source.objectFor(room.key.levelId)}
172
+ room={room}
173
+ />
174
+ )
175
+ }
@@ -1,104 +1,114 @@
1
1
  'use client'
2
2
 
3
- import {
4
- type AnyNode,
5
- type AnyNodeId,
6
- type BuildingNode,
7
- getLevelDisplayName,
8
- type LevelNode,
9
- useScene,
10
- type ZoneNode,
11
- } from '@pascal-app/core'
3
+ import { getLevelDisplayName, type LevelNode, sceneRegistry, useScene } from '@pascal-app/core'
12
4
  import { markPerfAction, useViewer } from '@pascal-app/viewer'
13
5
  import { ArrowLeft, ChevronRight, Layers } from 'lucide-react'
14
6
  import Link from 'next/link'
15
- import type { ReactNode } from 'react'
16
- import { useShallow } from 'zustand/react/shallow'
7
+ import { type ReactNode, useMemo } from 'react'
17
8
  import { cn } from '../../lib/utils'
18
- import { ViewerUnitsPanel } from './viewer-units-panel'
19
-
20
- const getNodeName = (node: AnyNode): string => {
21
- if ('name' in node && node.name) return node.name
22
- if (node.type === 'wall') return 'Wall'
23
- if (node.type === 'fence') return 'Fence'
24
- if (node.type === 'item') return (node as { asset: { name: string } }).asset?.name || 'Item'
25
- if (node.type === 'slab') return 'Slab'
26
- if (node.type === 'ceiling') return 'Ceiling'
27
- if (node.type === 'roof') return 'Roof'
28
- if (node.type === 'roof-segment') return 'Roof Segment'
29
- return node.type
30
- }
9
+ import {
10
+ applyViewerClick,
11
+ frameViewerNode,
12
+ roomOfObject,
13
+ selectViewerBuilding,
14
+ selectViewerLevel,
15
+ selectViewerRoom,
16
+ selectViewerUnit,
17
+ type ViewerCrumb,
18
+ type ViewerSidebarSection,
19
+ type ViewerSource,
20
+ viewerBreadcrumb,
21
+ viewerSidebarSections,
22
+ } from '../../lib/viewer-selection'
23
+ import { formatRoomArea } from '../editor/room-labels'
31
24
 
32
25
  export type ViewerSceneHeaderProps = {
33
26
  projectName?: string | null
34
27
  owner?: { username?: string | null } | null
35
28
  onBack?: () => void
36
29
  /** Fallback destination when no `onBack` handler is supplied. Must already be
37
- * sanitized by the caller. */
38
- backHref?: string
39
- /** Extra row under the project info (e.g. likes/fork actions). */
30
+ * sanitized by the caller. `null` shows no back arrow (an embedded viewer with
31
+ * nowhere to go back to). */
32
+ backHref?: string | null
33
+ /** Extra row under the project info (e.g. like and duplicate actions). */
40
34
  stats?: ReactNode
35
+ /** The scene the header navigates; the parametric scene store by default. */
36
+ source?: ViewerSource
41
37
  }
42
38
 
39
+ const SECTION_TITLES: Record<ViewerSidebarSection['kind'], string> = {
40
+ units: 'Units',
41
+ rooms: 'Rooms',
42
+ zones: 'Zones',
43
+ }
44
+
45
+ const parametricObject = (id: string) => sceneRegistry.nodes.get(id)
46
+
47
+ /**
48
+ * The navigation of every viewer surface: the project card, the bar (building
49
+ * › floor › room › element) and the floor list with, under the current floor,
50
+ * its units, rooms and zones. A row or a crumb selects and frames; the canvas
51
+ * selects in place (`lib/viewer-selection.ts`).
52
+ */
43
53
  export const ViewerSceneHeader = ({
44
54
  projectName,
45
55
  owner,
46
56
  onBack,
47
57
  backHref = '/',
48
58
  stats,
59
+ source: sourceProp,
49
60
  }: ViewerSceneHeaderProps) => {
61
+ const sceneNodes = useScene((s) => s.nodes)
62
+ const source = useMemo<ViewerSource>(
63
+ () => sourceProp ?? { nodes: sceneNodes, objectFor: parametricObject },
64
+ [sourceProp, sceneNodes],
65
+ )
66
+ const { nodes } = source
50
67
  const selection = useViewer((s) => s.selection)
68
+ const focusedUnitId = useViewer((s) => s.focusedUnitId)
69
+ const unit = useViewer((s) => s.unit)
51
70
 
52
- // Subscribe only to the specific nodes we read so that creating an unrelated
53
- // node elsewhere in the scene doesn't re-render this overlay.
54
- const firstSelectedId = selection.selectedIds[0] ?? null
55
- const building = useScene((s) =>
56
- selection.buildingId ? (s.nodes[selection.buildingId] as BuildingNode | undefined) : null,
57
- )
58
- const level = useScene((s) =>
59
- selection.levelId ? (s.nodes[selection.levelId] as LevelNode | undefined) : null,
60
- )
61
- const zone = useScene((s) =>
62
- selection.zoneId ? (s.nodes[selection.zoneId] as ZoneNode | undefined) : null,
63
- )
64
- const selectedNode = useScene((s) =>
65
- firstSelectedId ? (s.nodes[firstSelectedId as AnyNodeId] as AnyNode | undefined) : null,
71
+ const crumbs = useMemo(
72
+ () =>
73
+ viewerBreadcrumb(selection, focusedUnitId, nodes, (nodeId, levelId) =>
74
+ roomOfObject(nodeId, levelId, source),
75
+ ),
76
+ [selection, focusedUnitId, nodes, source],
66
77
  )
67
- const sceneNodes = useScene((s) => s.nodes)
78
+ const buildingId = crumbs.find((crumb) => crumb.kind === 'building')?.id ?? null
68
79
  // Highest first so the list reads top-down like a building section.
69
- const levels = useScene(
70
- useShallow((s) => {
71
- if (!building) return []
72
- return building.children
73
- .map((id) => s.nodes[id as AnyNodeId] as LevelNode | undefined)
74
- .filter((n): n is LevelNode => n?.type === 'level')
75
- .sort((a, b) => b.level - a.level)
76
- }),
80
+ const levels = useMemo(() => {
81
+ const building = buildingId ? nodes[buildingId] : undefined
82
+ if (building?.type !== 'building') return []
83
+ return building.children
84
+ .map((id) => nodes[id])
85
+ .filter((node): node is LevelNode => node?.type === 'level')
86
+ .sort((a, b) => b.level - a.level)
87
+ }, [buildingId, nodes])
88
+ const sections = useMemo(
89
+ () => (selection.levelId ? viewerSidebarSections(selection.levelId, nodes) : []),
90
+ [selection.levelId, nodes],
77
91
  )
78
92
 
79
- const handleLevelClick = (levelId: LevelNode['id']) => {
80
- // When switching levels, deselect zone and items
81
- if (levelId !== selection.levelId) markPerfAction('level-switch', levelId)
82
- useViewer.getState().setSelection({ levelId })
83
- }
84
-
85
- const handleBreadcrumbClick = (depth: 'root' | 'building' | 'level') => {
86
- switch (depth) {
87
- case 'root':
88
- useViewer.getState().resetSelection()
89
- break
93
+ const handleCrumb = (crumb: ViewerCrumb) => {
94
+ switch (crumb.kind) {
90
95
  case 'building':
91
- useViewer.getState().setSelection({ levelId: null })
92
- break
96
+ return selectViewerBuilding(source)
93
97
  case 'level':
94
- useViewer.getState().setSelection({ zoneId: null })
95
- break
98
+ return selectViewerLevel(crumb.id, source)
99
+ case 'unit':
100
+ return selectViewerUnit(crumb.id, source)
101
+ case 'room':
102
+ return selectViewerRoom(crumb.id, source)
103
+ case 'node':
104
+ applyViewerClick({ nodeId: crumb.id, point: null }, nodes)
105
+ return frameViewerNode(crumb.id, source)
96
106
  }
97
107
  }
98
108
 
99
109
  return (
100
- <div className="dark absolute top-4 left-4 z-20 flex flex-col gap-3 text-foreground">
101
- <div className="corner-smooth pointer-events-auto flex min-w-[200px] flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 shadow-elevation-4 backdrop-blur-xl transition-colors duration-200 ease-out">
110
+ <div className="dark absolute top-4 left-4 z-20 flex max-h-[calc(100%-7rem)] flex-col gap-3 text-foreground">
111
+ <div className="corner-smooth pointer-events-auto flex min-w-[200px] max-w-[min(28rem,calc(100vw-2rem))] flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 shadow-elevation-4 backdrop-blur-xl transition-colors duration-200 ease-out">
102
112
  {/* Project info + back */}
103
113
  <div className="flex items-center gap-3 px-3 py-2.5">
104
114
  {onBack ? (
@@ -110,7 +120,7 @@ export const ViewerSceneHeader = ({
110
120
  >
111
121
  <ArrowLeft className="h-4 w-4 text-muted-foreground" />
112
122
  </button>
113
- ) : (
123
+ ) : backHref === null ? null : (
114
124
  <Link
115
125
  aria-label="Back"
116
126
  className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-white/10"
@@ -139,101 +149,127 @@ export const ViewerSceneHeader = ({
139
149
  <div className="flex items-center gap-1 border-border/40 border-t px-3 py-2">{stats}</div>
140
150
  )}
141
151
 
142
- {/* Breadcrumb — only shown when navigated into a building */}
143
- {building && (
144
- <div className="border-border/40 border-t px-3 py-2">
145
- <div className="flex items-center gap-1.5 text-xs">
146
- <button
147
- className="text-muted-foreground transition-colors hover:text-foreground"
148
- onClick={() => handleBreadcrumbClick('root')}
149
- >
150
- Site
151
- </button>
152
-
153
- <ChevronRight className="h-3 w-3 text-muted-foreground/50" />
154
- <button
155
- className={`truncate transition-colors ${level ? 'text-muted-foreground hover:text-foreground' : 'font-medium text-foreground'}`}
156
- onClick={() => handleBreadcrumbClick('building')}
157
- >
158
- {building.name || 'Building'}
159
- </button>
160
-
161
- {level && (
162
- <>
163
- <ChevronRight className="h-3 w-3 text-muted-foreground/50" />
152
+ {crumbs.length > 0 && (
153
+ <nav
154
+ aria-label="Selection"
155
+ className="flex flex-wrap items-center gap-x-1.5 gap-y-1 border-border/40 border-t px-3 py-2 text-xs"
156
+ data-testid="viewer-breadcrumb"
157
+ >
158
+ {crumbs.map((crumb, index) => {
159
+ const isLast = index === crumbs.length - 1
160
+ return (
161
+ <span
162
+ className="flex min-w-0 items-center gap-1.5"
163
+ key={`${crumb.kind}:${crumb.id}`}
164
+ >
165
+ {index > 0 && <ChevronRight className="h-3 w-3 text-muted-foreground/50" />}
164
166
  <button
165
- className={`truncate transition-colors ${zone ? 'text-muted-foreground hover:text-foreground' : 'font-medium text-foreground'}`}
166
- onClick={() => handleBreadcrumbClick('level')}
167
+ className={cn(
168
+ 'truncate transition-colors',
169
+ isLast
170
+ ? 'font-medium text-foreground'
171
+ : 'text-muted-foreground hover:text-foreground',
172
+ )}
173
+ data-crumb={crumb.kind}
174
+ onClick={() => handleCrumb(crumb)}
175
+ type="button"
167
176
  >
168
- {getLevelDisplayName(level)}
177
+ {crumb.label}
169
178
  </button>
170
- </>
171
- )}
172
-
173
- {zone && (
174
- <>
175
- <ChevronRight className="h-3 w-3 text-muted-foreground/50" />
176
- <span
177
- className={`truncate transition-colors ${selectedNode ? 'text-muted-foreground' : 'font-medium text-foreground'}`}
178
- >
179
- {zone.name}
180
- </span>
181
- </>
182
- )}
183
-
184
- {selectedNode && zone && (
185
- <>
186
- <ChevronRight className="h-3 w-3 text-muted-foreground/50" />
187
- <span className="truncate font-medium text-foreground">
188
- {getNodeName(selectedNode)}
189
- </span>
190
- </>
191
- )}
192
- </div>
193
- </div>
179
+ </span>
180
+ )
181
+ })}
182
+ </nav>
194
183
  )}
195
184
  </div>
196
185
 
197
- {/* Level list (only when a building is selected) */}
198
- {building && levels.length > 0 && (
199
- <div className="corner-smooth pointer-events-auto flex w-48 flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 py-1 shadow-elevation-4 backdrop-blur-xl transition-colors duration-200 ease-out">
186
+ {levels.length > 0 && (
187
+ <div className="corner-smooth pointer-events-auto flex min-h-0 w-56 flex-col overflow-hidden rounded-2xl border border-border/40 bg-background/95 py-1 shadow-elevation-4 backdrop-blur-xl transition-colors duration-200 ease-out">
200
188
  <span className="px-3 py-2 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
201
189
  Levels
202
190
  </span>
203
- <div className="flex flex-col">
191
+ <div className="flex min-h-0 flex-col overflow-y-auto">
204
192
  {levels.map((lvl) => {
205
193
  const isSelected = lvl.id === selection.levelId
206
194
  return (
207
- <button
208
- className={cn(
209
- 'group/row relative flex h-8 w-full cursor-pointer select-none items-center border-border/50 border-r border-r-transparent border-b px-3 text-sm transition-all duration-200',
210
- isSelected
211
- ? 'border-r-3 border-r-white bg-accent/50 text-foreground'
212
- : 'text-muted-foreground hover:bg-accent/30 hover:text-foreground',
213
- )}
214
- key={lvl.id}
215
- onClick={() => handleLevelClick(lvl.id)}
216
- >
217
- <div className="flex min-w-0 flex-1 items-center gap-2">
218
- <span
219
- className={cn(
220
- 'flex h-4 w-4 shrink-0 items-center justify-center transition-all duration-200',
221
- !isSelected && 'opacity-60 grayscale',
222
- )}
223
- >
224
- <Layers className="h-3.5 w-3.5" />
225
- </span>
226
- <div className="min-w-0 flex-1 truncate text-left">
227
- {getLevelDisplayName(lvl)}
195
+ <div key={lvl.id}>
196
+ <button
197
+ className={cn(
198
+ 'group/row relative flex h-8 w-full cursor-pointer select-none items-center border-border/50 border-r border-r-transparent border-b px-3 text-sm transition-all duration-200',
199
+ isSelected
200
+ ? 'border-r-3 border-r-white bg-accent/50 text-foreground'
201
+ : 'text-muted-foreground hover:bg-accent/30 hover:text-foreground',
202
+ )}
203
+ onClick={() => {
204
+ if (!isSelected) markPerfAction('level-switch', lvl.id)
205
+ selectViewerLevel(lvl.id, source)
206
+ }}
207
+ type="button"
208
+ >
209
+ <div className="flex min-w-0 flex-1 items-center gap-2">
210
+ <span
211
+ className={cn(
212
+ 'flex h-4 w-4 shrink-0 items-center justify-center transition-all duration-200',
213
+ !isSelected && 'opacity-60 grayscale',
214
+ )}
215
+ >
216
+ <Layers className="h-3.5 w-3.5" />
217
+ </span>
218
+ <div className="min-w-0 flex-1 truncate text-left">
219
+ {getLevelDisplayName(lvl)}
220
+ </div>
228
221
  </div>
229
- </div>
230
- </button>
222
+ </button>
223
+ {isSelected &&
224
+ sections.map((section) => (
225
+ <div
226
+ className="border-border/50 border-b pb-1"
227
+ data-viewer-section={section.kind}
228
+ key={section.kind}
229
+ >
230
+ <span className="block px-3 pt-2 pb-1 pl-9 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
231
+ {SECTION_TITLES[section.kind]}
232
+ </span>
233
+ {section.rows.map((row) => {
234
+ const active =
235
+ section.kind === 'units'
236
+ ? row.id === focusedUnitId
237
+ : row.id === selection.zoneId
238
+ return (
239
+ <button
240
+ className={cn(
241
+ 'flex h-7 w-full cursor-pointer select-none items-center gap-2 pr-3 pl-9 text-left text-sm transition-colors',
242
+ active
243
+ ? 'bg-accent/50 text-foreground'
244
+ : 'text-muted-foreground hover:bg-accent/30 hover:text-foreground',
245
+ )}
246
+ key={row.id}
247
+ onClick={() =>
248
+ section.kind === 'units'
249
+ ? selectViewerUnit(row.id, source)
250
+ : selectViewerRoom(row.id, source)
251
+ }
252
+ type="button"
253
+ >
254
+ <span
255
+ className="h-2 w-2 shrink-0 rounded-full"
256
+ style={{ backgroundColor: row.color }}
257
+ />
258
+ <span className="min-w-0 flex-1 truncate">{row.label}</span>
259
+ <span className="shrink-0 font-mono text-[11px] text-muted-foreground tabular-nums">
260
+ {formatRoomArea(row.areaM2, unit)}
261
+ </span>
262
+ </button>
263
+ )
264
+ })}
265
+ </div>
266
+ ))}
267
+ </div>
231
268
  )
232
269
  })}
233
270
  </div>
234
271
  </div>
235
272
  )}
236
- {building && <ViewerUnitsPanel nodes={sceneNodes} />}
237
273
  </div>
238
274
  )
239
275
  }