@pascal-app/editor 1.0.3 → 1.0.5

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 (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -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
  }
@@ -0,0 +1,124 @@
1
+ 'use client'
2
+
3
+ import { type AnyNodeId, emitter, type NodeEvent, sceneRegistry, useScene } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
5
+ import { useThree } from '@react-three/fiber'
6
+ import { useEffect } from 'react'
7
+ import { selectionModifiersFromEvent } from '../../lib/selection-routing'
8
+ import {
9
+ applyViewerClick,
10
+ applyViewerHover,
11
+ viewerPickFromNodeEvent,
12
+ } from '../../lib/viewer-selection'
13
+
14
+ /**
15
+ * Selection for a parametric viewer surface (the published viewer's fallback,
16
+ * its embed, the editor's Preview): free-form and room-first, through the same
17
+ * rules as the baked viewer. Mount it with `<Viewer selectionManager="custom">`.
18
+ */
19
+ export function ViewerSelectionManager() {
20
+ const gl = useThree((state) => state.gl)
21
+
22
+ useEffect(() => {
23
+ let clickHandled = false
24
+ let hoveredFrom: string | null = null
25
+ const pendingClicks = new Set<number>()
26
+
27
+ const onMove = (event: NodeEvent) => {
28
+ if (useViewer.getState().walkthroughMode) return
29
+ const pick = viewerPickFromNodeEvent(event, useScene.getState().nodes)
30
+ if (!pick) return
31
+ event.stopPropagation()
32
+ hoveredFrom = event.node.id
33
+ applyViewerHover(pick, useScene.getState().nodes)
34
+ }
35
+ const onLeave = (event: NodeEvent) => {
36
+ if (event.node.id !== hoveredFrom) return
37
+ hoveredFrom = null
38
+ useViewer.getState().setHoveredId(null)
39
+ }
40
+ const onClick = (event: NodeEvent) => {
41
+ if (useViewer.getState().walkthroughMode) return
42
+ const pick = viewerPickFromNodeEvent(event, useScene.getState().nodes)
43
+ if (!pick) return
44
+ event.stopPropagation()
45
+ clickHandled = true
46
+ applyViewerClick(
47
+ pick,
48
+ useScene.getState().nodes,
49
+ selectionModifiersFromEvent(event.nativeEvent),
50
+ )
51
+ }
52
+ // A click on nothing selectable clears the room and the element.
53
+ const onCanvasClick = (event: MouseEvent) => {
54
+ const viewer = useViewer.getState()
55
+ if (event.button !== 0 || viewer.walkthroughMode) return
56
+ if (viewer.cameraDragging || viewer.inputDragging) return
57
+ const frame = requestAnimationFrame(() => {
58
+ pendingClicks.delete(frame)
59
+ if (useViewer.getState().walkthroughMode) return
60
+ if (clickHandled) {
61
+ clickHandled = false
62
+ return
63
+ }
64
+ applyViewerClick(null, useScene.getState().nodes)
65
+ })
66
+ pendingClicks.add(frame)
67
+ }
68
+
69
+ emitter.on('node:move', onMove)
70
+ emitter.on('node:enter', onMove)
71
+ emitter.on('node:leave', onLeave)
72
+ emitter.on('node:click', onClick)
73
+ const canvas = gl.domElement
74
+ canvas.addEventListener('click', onCanvasClick)
75
+ return () => {
76
+ emitter.off('node:move', onMove)
77
+ emitter.off('node:enter', onMove)
78
+ emitter.off('node:leave', onLeave)
79
+ emitter.off('node:click', onClick)
80
+ canvas.removeEventListener('click', onCanvasClick)
81
+ for (const frame of pendingClicks) cancelAnimationFrame(frame)
82
+ useViewer.getState().setHoveredId(null)
83
+ }
84
+ }, [gl])
85
+
86
+ return <ViewerOutlineSync />
87
+ }
88
+
89
+ /** The selected elements and the hovered one, outlined (rooms highlight instead). */
90
+ function ViewerOutlineSync() {
91
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
92
+ const externalSelectedIds = useViewer((s) => s.externalSelectedIds)
93
+ const hoveredId = useViewer((s) => s.hoveredId)
94
+ const outliner = useViewer((s) => s.outliner)
95
+ const geometryRevision = useViewer((s) => s.geometryRevision)
96
+ const walkthrough = useViewer((s) => s.walkthroughMode)
97
+ const nodes = useScene((s) => s.nodes)
98
+
99
+ useEffect(() => {
100
+ void geometryRevision
101
+ outliner.selectedObjects.length = 0
102
+ outliner.hoveredObjects.length = 0
103
+ if (walkthrough) return
104
+ for (const id of new Set([...selectedIds, ...externalSelectedIds])) {
105
+ const object = sceneRegistry.nodes.get(id)
106
+ if (object) outliner.selectedObjects.push(object)
107
+ }
108
+ const hovered = hoveredId ? nodes[hoveredId as AnyNodeId] : undefined
109
+ if (hovered && hovered.type !== 'zone') {
110
+ const object = sceneRegistry.nodes.get(hovered.id)
111
+ if (object) outliner.hoveredObjects.push(object)
112
+ }
113
+ }, [selectedIds, externalSelectedIds, hoveredId, outliner, nodes, geometryRevision, walkthrough])
114
+
115
+ useEffect(
116
+ () => () => {
117
+ outliner.selectedObjects.length = 0
118
+ outliner.hoveredObjects.length = 0
119
+ },
120
+ [outliner],
121
+ )
122
+
123
+ return null
124
+ }
@@ -60,7 +60,6 @@ function selectViewerLevel(nodes: Record<string, AnyNode>, levelId: string) {
60
60
  selectedIds: [],
61
61
  zoneId: null,
62
62
  })
63
- viewer.setLevelMode('solo')
64
63
  }
65
64
 
66
65
  export function ViewerStage({
@@ -137,7 +136,8 @@ export function ViewerStage({
137
136
  if (notify && nextLevelId !== useViewer.getState().selection.levelId) {
138
137
  markPerfAction('level-switch', nextLevelId)
139
138
  }
140
- selectViewerLevel(scene?.nodes ?? useScene.getState().nodes, nextLevelId)
139
+ if (nextLevelId !== useViewer.getState().selection.levelId)
140
+ selectViewerLevel(scene?.nodes ?? useScene.getState().nodes, nextLevelId)
141
141
  if (notify) onLevelChange?.(nextLevelId)
142
142
  },
143
143
  [onLevelChange, scene],
@@ -11,7 +11,6 @@ import useInteractionScope from '../store/use-interaction-scope'
11
11
  export const ViewerZoneSystem = () => {
12
12
  useFrame(() => {
13
13
  const { levelId, zoneId } = useViewer.getState().selection
14
- const structureLayer = useEditor.getState().structureLayer
15
14
  const nodes = useScene.getState().nodes
16
15
  // Snapshot capture is a clean, camera-only surface — zone geometry and
17
16
  // tags stay out of the framed shot (mirrors the editor ZoneSystem's gate).
@@ -29,17 +28,13 @@ export const ViewerZoneSystem = () => {
29
28
 
30
29
  const isOnSelectedLevel = zone.parentId === levelId
31
30
 
32
- // Keep group visible (so <Html> labels stay active), hide/show meshes only.
33
- // Zone geometry: visible in zone mode on the right level, OR when this zone is selected.
34
- // The editor ZoneSystem handles the selected zone's opacity animation.
31
+ // Keep group visible (so <Html> labels stay active), hide/show meshes only:
32
+ // only the selected zone shows its volume.
35
33
  const isSelected = id === zoneId
36
34
  // A zone the author hid (sidebar eye) takes the group with it — this
37
35
  // per-frame write would otherwise undo the renderer's `visible` prop.
38
36
  const nodeVisible = zone.visible !== false
39
- const shouldShowGeometry =
40
- nodeVisible &&
41
- !isCaptureMode &&
42
- ((structureLayer === 'zones' && !!levelId && isOnSelectedLevel) || isSelected)
37
+ const shouldShowGeometry = nodeVisible && !isCaptureMode && isSelected
43
38
  if (obj.visible !== nodeVisible) obj.visible = nodeVisible
44
39
  obj.traverse((child) => {
45
40
  if ((child as Mesh).isMesh) {