@pascal-app/editor 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,34 @@
1
+ 'use client'
2
+
3
+ import { useScene } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
5
+ import { nodeDisplayLabel } from '../../lib/node-display-label'
6
+ import { type ViewerNodes, zoneLabel } from '../../lib/viewer-selection'
7
+
8
+ /** The hovered node's name, for the bottom pill. A room reads as the room. */
9
+ export function viewerHoverLabel(hoveredId: string | null, nodes: ViewerNodes): string | null {
10
+ const node = hoveredId ? nodes[hoveredId] : undefined
11
+ if (!node) return null
12
+ return node.type === 'zone' ? zoneLabel(node) : nodeDisplayLabel(node)
13
+ }
14
+
15
+ /**
16
+ * What a click would select, named in a pill above the controls bar. Every
17
+ * viewer surface's hit source sets the hover (`applyViewerHover`); hidden in
18
+ * the walkthrough and on devices without a hovering pointer. Names come from
19
+ * the parametric scene store unless the surface passes its own graph.
20
+ */
21
+ export function ViewerHoverLabel({ nodes: nodesProp }: { nodes?: ViewerNodes }) {
22
+ const sceneNodes = useScene((s) => s.nodes)
23
+ const nodes = nodesProp ?? sceneNodes
24
+ const label = useViewer((s) => (s.walkthroughMode ? null : viewerHoverLabel(s.hoveredId, nodes)))
25
+ if (!label) return null
26
+ return (
27
+ <div
28
+ className="pointer-events-none absolute bottom-24 left-1/2 z-20 -translate-x-1/2 rounded-full border border-border/60 bg-background/90 px-3 py-1 text-foreground text-sm shadow-elevation-3 backdrop-blur [@media(hover:none)]:hidden"
29
+ data-testid="viewer-hover-label"
30
+ >
31
+ {label}
32
+ </div>
33
+ )
34
+ }
@@ -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
  }