@pascal-app/editor 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -2,10 +2,13 @@
2
2
 
3
3
  import { getSceneTheme, useViewer } from '@pascal-app/viewer'
4
4
  import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
5
+ import { useShownSidebarPanel } from '../../lib/sidebar-panel'
5
6
  import useEditor from '../../store/use-editor'
6
7
  import { MobileTabBar } from '../ui/sidebar/mobile-tab-bar'
7
8
  import type { SidebarTab } from '../ui/sidebar/tab-bar'
8
9
  import { BottomSheet, type BottomSheetHandle } from './bottom-sheet'
10
+ import { useSceneSpan } from './views/scene-region'
11
+ import { ViewTabs } from './views/view-bar'
9
12
 
10
13
  const MIN_SNAP = 0
11
14
  const MAX_SNAP = 1
@@ -40,6 +43,7 @@ export interface EditorLayoutMobileProps {
40
43
  viewerToolbarRight?: ReactNode
41
44
  viewerContent: ReactNode
42
45
  overlays?: ReactNode
46
+ viewBar?: boolean
43
47
  }
44
48
 
45
49
  export function EditorLayoutMobile({
@@ -51,9 +55,11 @@ export function EditorLayoutMobile({
51
55
  viewerToolbarRight,
52
56
  viewerContent,
53
57
  overlays,
58
+ viewBar = true,
54
59
  }: EditorLayoutMobileProps) {
60
+ const hasScene = useSceneSpan() !== null
55
61
  const isCaptureMode = useEditor((s) => s.isCaptureMode)
56
- const activePanel = useEditor((s) => s.activeSidebarPanel)
62
+ const activePanel = useShownSidebarPanel(sidebarTabs)
57
63
  const setActivePanel = useEditor((s) => s.setActiveSidebarPanel)
58
64
  const panelSheetHeight = useEditor((s) => s.mobilePanelSheetHeight)
59
65
  const isDark = useViewer((s) => getSceneTheme(s.sceneTheme).appearance === 'dark')
@@ -70,30 +76,18 @@ export function EditorLayoutMobile({
70
76
 
71
77
  const currentTab = sidebarTabs.find((t) => t.id === activePanel)
72
78
 
73
- // Keep active panel valid
74
- useEffect(() => {
75
- if (sidebarTabs.length > 0 && !sidebarTabs.some((t) => t.id === activePanel)) {
76
- setActivePanel(sidebarTabs[0]!.id)
77
- }
78
- }, [sidebarTabs, activePanel, setActivePanel])
79
-
80
- // Sync editor phase / mode with the active tab:
79
+ // Sync the editor mode with the active tab:
81
80
  // - Entering Chat always drops to Select (chat is a composing context).
82
- // - Entering Items snaps the editor into furnish-build (matches the
83
- // desktop "Furnish" action which itself opens the Items panel).
84
- // - Leaving Items while still furnishing exits the build mode.
81
+ // - Leaving Items with the item tool still armed exits the build mode.
82
+ // Arming the item tool on entering Items belongs to the tap (`handleTabPress`):
83
+ // a tab restored on load opens in select mode.
85
84
  useEffect(() => {
86
- const { armToolMode, phase, mode, setPhase } = useEditor.getState()
85
+ const { armToolMode, mode, tool } = useEditor.getState()
87
86
  if (activePanel === 'ai' && mode === 'build') {
88
87
  armToolMode({ mode: 'select' })
89
88
  return
90
89
  }
91
- if (activePanel === 'items') {
92
- if (phase !== 'furnish') setPhase('furnish')
93
- if (mode !== 'build') armToolMode({ mode: 'build', tool: 'item' })
94
- return
95
- }
96
- if (phase === 'furnish' && mode === 'build') {
90
+ if (activePanel !== 'items' && mode === 'build' && tool === 'item') {
97
91
  armToolMode({ mode: 'select' })
98
92
  }
99
93
  }, [activePanel])
@@ -148,9 +142,17 @@ export function EditorLayoutMobile({
148
142
  if (middleH <= 0) return
149
143
  const tab = sidebarTabs.find((t) => t.id === id)
150
144
  if (!tab) return
145
+ if (tab.onSelect) {
146
+ tab.onSelect()
147
+ return
148
+ }
151
149
  const defaultPx = getDefaultSnap(tab) * middleH
152
150
  if (id !== activePanel) {
153
151
  setActivePanel(id)
152
+ // Tapping Items arms the item tool, as F does on desktop.
153
+ if (id === 'items' && useEditor.getState().mode !== 'build') {
154
+ useEditor.getState().armToolMode({ mode: 'build', tool: 'item' })
155
+ }
154
156
  sheetRef.current?.snapTo(defaultPx)
155
157
  return
156
158
  }
@@ -223,14 +225,23 @@ export function EditorLayoutMobile({
223
225
  {/* Viewer column: sized by committed sheet height */}
224
226
  <div className="absolute inset-x-0 top-0 overflow-hidden" style={{ height: viewerHeight }}>
225
227
  <div className="relative h-full w-full">
226
- {(viewerToolbarLeft || viewerToolbarRight) && !isCaptureMode && (
228
+ {(viewBar || viewerToolbarLeft || viewerToolbarRight) && !isCaptureMode && (
227
229
  <div className="pointer-events-none absolute top-3 right-3 left-3 z-20 flex items-center justify-between gap-2">
228
- <div className="pointer-events-auto flex items-center gap-2">
229
- {viewerToolbarLeft}
230
- </div>
231
- <div className="pointer-events-auto flex items-center gap-2">
232
- {viewerToolbarRight}
230
+ <div className="flex items-center gap-2">
231
+ {viewBar && (
232
+ <div data-view-bar={0}>
233
+ <ViewTabs className="shadow-elevation-4" pane={0} />
234
+ </div>
235
+ )}
236
+ <div className="pointer-events-auto flex items-center gap-2">
237
+ {viewerToolbarLeft}
238
+ </div>
233
239
  </div>
240
+ {hasScene && (
241
+ <div className="pointer-events-auto flex items-center gap-2">
242
+ {viewerToolbarRight}
243
+ </div>
244
+ )}
234
245
  </div>
235
246
  )}
236
247
  <div className="relative h-full w-full overflow-hidden">{viewerContent}</div>
@@ -2,13 +2,21 @@
2
2
 
3
3
  import { type ReactNode, useCallback, useEffect, useRef } from 'react'
4
4
  import { useIsMobile } from '../../hooks/use-mobile'
5
+ import {
6
+ openSidebarPanel,
7
+ SIDEBAR_MIN_WIDTH,
8
+ setSidebarTabIds,
9
+ showSidebarPanel,
10
+ useShownSidebarPanel,
11
+ } from '../../lib/sidebar-panel'
5
12
  import useEditor from '../../store/use-editor'
6
13
 
7
14
  import { useSidebarStore } from '../ui/primitives/sidebar'
8
15
  import { IconRail, type SidebarTab } from '../ui/sidebar/tab-bar'
9
16
  import { EditorLayoutMobile } from './editor-layout-mobile'
17
+ import { useSceneSpan } from './views/scene-region'
18
+ import { ViewBarRow } from './views/view-bar'
10
19
 
11
- const SIDEBAR_MIN_WIDTH = 300
12
20
  const SIDEBAR_MAX_WIDTH = 800
13
21
  const SIDEBAR_COLLAPSE_THRESHOLD = 220
14
22
  // Matches the `w-14` rail in <IconRail>; the resize math is relative to it.
@@ -31,27 +39,49 @@ function LeftColumn({
31
39
  const setWidth = useSidebarStore((s) => s.setWidth)
32
40
  const isDragging = useSidebarStore((s) => s.isDragging)
33
41
  const setIsDragging = useSidebarStore((s) => s.setIsDragging)
34
- const activePanel = useEditor((s) => s.activeSidebarPanel)
42
+ const activePanel = useShownSidebarPanel(tabs)
35
43
  const setActivePanel = useEditor((s) => s.setActiveSidebarPanel)
36
44
 
37
45
  const isResizing = useRef(false)
38
46
 
39
- // Ensure active panel is a valid tab
47
+ // Publish the rail's tabs so keyboard shortcuts (B, P) can open a panel.
40
48
  useEffect(() => {
41
- if (tabs.length > 0 && !tabs.some((t) => t.id === activePanel)) {
42
- setActivePanel(tabs[0]!.id)
43
- }
44
- }, [tabs, activePanel, setActivePanel])
49
+ setSidebarTabIds(tabs.map((tab) => tab.id))
50
+ return () => setSidebarTabIds([])
51
+ }, [tabs])
52
+
53
+ // A stage entry whose surface has left the stage (its view closed from the
54
+ // view bar) hands the rail back to the panel that was open before it.
55
+ const lastPanelTab = useRef<string | null>(null)
56
+ const activeTab = tabs.find((t) => t.id === activePanel)
57
+ if (activeTab && !activeTab.noPanel) lastPanelTab.current = activeTab.id
58
+ useEffect(() => {
59
+ if (activeTab?.stageActive !== false) return
60
+ const fallback = tabs.find((t) => t.id === lastPanelTab.current) ?? tabs.find((t) => !t.noPanel)
61
+ if (fallback) setActivePanel(fallback.id)
62
+ }, [activeTab, tabs, setActivePanel])
45
63
 
46
- // Leaving the items tab while furnishing should drop back to select mode
64
+ // Leaving the items tab with the item tool in hand drops back to select mode
47
65
  useEffect(() => {
48
66
  if (activePanel === 'items') return
49
- const { phase, mode, setMode } = useEditor.getState()
50
- if (phase === 'furnish' && mode === 'build') {
67
+ const { mode, tool, setMode } = useEditor.getState()
68
+ if (mode === 'build' && tool === 'item') {
51
69
  setMode('select')
52
70
  }
53
71
  }, [activePanel])
54
72
 
73
+ // Paint mode entered without the panel (command palette, a material's "paint
74
+ // with") brings the host's Paint panel along, so the rail reflects the mode.
75
+ useEffect(
76
+ () =>
77
+ useEditor.subscribe((state, prev) => {
78
+ if (state.mode !== prev.mode && state.mode === 'material-paint') {
79
+ openSidebarPanel(['paint'])
80
+ }
81
+ }),
82
+ [],
83
+ )
84
+
55
85
  // Closing (collapsing) the sidebar disarms any build tool back to select
56
86
  useEffect(() => {
57
87
  if (!isCollapsed) return
@@ -72,20 +102,20 @@ function LeftColumn({
72
102
  [setIsDragging],
73
103
  )
74
104
 
75
- // Rail click: reopen a collapsed panel, collapse when re-clicking the open
76
- // tab, otherwise switch tabs. Reopening clamps below-min persisted widths
77
- // up to the minimum so the panel always returns to a usable size.
105
+ // Rail click: reopen a collapsed panel (at a usable width), collapse when
106
+ // re-clicking the open tab, otherwise switch tabs.
78
107
  const handleRailClick = useCallback(
79
108
  (id: string) => {
109
+ const tab = tabs.find((t) => t.id === id)
110
+ if (id !== activePanel) tabs.find((t) => t.id === activePanel)?.onDeselect?.()
111
+ tab?.onSelect?.()
80
112
  // noPanel tabs drive the stage, not the panel — leave collapse state alone.
81
- if (tabs.find((t) => t.id === id)?.noPanel) {
113
+ if (tab?.noPanel) {
82
114
  setActivePanel(id)
83
115
  return
84
116
  }
85
117
  if (isCollapsed) {
86
- setIsCollapsed(false)
87
- if (width < SIDEBAR_MIN_WIDTH) setWidth(SIDEBAR_MIN_WIDTH)
88
- setActivePanel(id)
118
+ showSidebarPanel(id)
89
119
  return
90
120
  }
91
121
  if (id === activePanel) {
@@ -94,7 +124,7 @@ function LeftColumn({
94
124
  }
95
125
  setActivePanel(id)
96
126
  },
97
- [tabs, isCollapsed, width, activePanel, setIsCollapsed, setWidth, setActivePanel],
127
+ [tabs, isCollapsed, activePanel, setIsCollapsed, setActivePanel],
98
128
  )
99
129
 
100
130
  useEffect(() => {
@@ -159,59 +189,97 @@ function LeftColumn({
159
189
 
160
190
  // ── Right column: viewer area with toolbar ───────────────────────────────────
161
191
 
192
+ /**
193
+ * Host toolbars floating over the stage. The right one carries scene controls
194
+ * (wall mode, display), so it sits at the right edge of the scene panes; the
195
+ * left one floats only when there is no view bar row to carry it.
196
+ */
197
+ function StageToolbar({
198
+ toolbarLeft,
199
+ toolbarRight,
200
+ }: {
201
+ toolbarLeft?: ReactNode
202
+ toolbarRight?: ReactNode
203
+ }) {
204
+ const scene = useSceneSpan()
205
+ return (
206
+ <div className="pointer-events-none absolute top-3 right-0 left-0 z-20 h-8">
207
+ {toolbarLeft && (
208
+ <div className="pointer-events-auto absolute top-0 left-3 flex items-center gap-2">
209
+ {toolbarLeft}
210
+ </div>
211
+ )}
212
+ {toolbarRight && scene && (
213
+ <div
214
+ className="pointer-events-auto absolute top-0 flex items-center gap-2"
215
+ style={{ right: `calc(${(1 - scene.right) * 100}% + 12px)` }}
216
+ >
217
+ {toolbarRight}
218
+ </div>
219
+ )}
220
+ </div>
221
+ )
222
+ }
223
+
162
224
  function RightColumn({
163
225
  toolbarLeft,
164
226
  toolbarRight,
227
+ viewBar,
165
228
  children,
166
229
  overlays,
167
230
  stageOverlay,
168
231
  }: {
169
232
  toolbarLeft?: ReactNode
170
233
  toolbarRight?: ReactNode
234
+ viewBar: boolean
171
235
  children: ReactNode
172
236
  overlays?: ReactNode
173
237
  stageOverlay?: ReactNode
174
238
  }) {
175
239
  return (
176
- <div
177
- className="relative flex min-w-0 flex-1 flex-col overflow-hidden"
178
- style={{
179
- borderTopLeftRadius: 16,
180
- clipPath: 'inset(0 0 0 0 round 16px 0 0 0)',
181
- boxShadow: '-4px -2px 16px rgba(0, 0, 0, 0.08), -1px 0 4px rgba(0, 0, 0, 0.04)',
182
- }}
183
- >
184
- {/* Viewer toolbar */}
185
- {(toolbarLeft || toolbarRight) && (
186
- <div className="pointer-events-none absolute top-3 right-3 left-3 z-20 flex items-center justify-between gap-2">
187
- <div className="pointer-events-auto flex items-center gap-2">{toolbarLeft}</div>
188
- <div className="pointer-events-auto flex items-center gap-2">{toolbarRight}</div>
189
- </div>
190
- )}
191
- {/* Canvas area. `isolate` matters: drei's `<Html>` computes a z-index
192
- from camera distance and defaults to a range topping out at
193
- 16,777,271, and without a stacking context here those values compete
194
- directly with the viewer toolbar (z-20), the stage overlay (z-10) and
195
- the overlay band (z-30) — so an in-scene tool badge painted over all
196
- three. Isolating pins every in-scene HTML layer inside the canvas,
197
- where it belongs, and leaves their order relative to each other
198
- untouched. */}
199
- <div className="relative isolate flex-1 overflow-hidden">{children}</div>
200
- {/* Stage overlay — replaces the canvas visually (e.g. studio gallery)
201
- while keeping it mounted. Sits below the viewer toolbar (z-20) so
202
- the stage switch stays reachable. */}
203
- {stageOverlay && <div className="absolute inset-0 z-10">{stageOverlay}</div>}
204
- {/* Overlays scoped to the viewer column. `data-viewer-bounds` marks the
205
- draggable region the floating inspector clamps itself to. */}
206
- {overlays && (
207
- <div
208
- className="pointer-events-none absolute inset-0 z-30"
209
- data-viewer-bounds
210
- style={{ transform: 'translateZ(0)' }}
211
- >
212
- {overlays}
213
- </div>
214
- )}
240
+ <div className="relative flex min-w-0 flex-1 flex-col overflow-hidden">
241
+ {/* The view bar row sits above the stage, one bar per pane; the host's
242
+ left toolbar rides after the first pane's views. */}
243
+ {viewBar && <ViewBarRow leading={toolbarLeft} />}
244
+ <div
245
+ className="relative flex min-h-0 flex-1 flex-col overflow-hidden"
246
+ style={{
247
+ borderTopLeftRadius: 16,
248
+ clipPath: 'inset(0 0 0 0 round 16px 0 0 0)',
249
+ boxShadow: '-4px -2px 16px rgba(0, 0, 0, 0.08), -1px 0 4px rgba(0, 0, 0, 0.04)',
250
+ }}
251
+ >
252
+ {((!viewBar && toolbarLeft) || toolbarRight) && (
253
+ <StageToolbar
254
+ toolbarLeft={viewBar ? undefined : toolbarLeft}
255
+ toolbarRight={toolbarRight}
256
+ />
257
+ )}
258
+ {/* Canvas area. `isolate` matters: drei's `<Html>` computes a z-index
259
+ from camera distance and defaults to a range topping out at
260
+ 16,777,271, and without a stacking context here those values compete
261
+ directly with the viewer toolbar (z-20), the stage overlay (z-10) and
262
+ the overlay band (z-30) — so an in-scene tool badge painted over all
263
+ three. Isolating pins every in-scene HTML layer inside the canvas,
264
+ where it belongs, and leaves their order relative to each other
265
+ untouched. */}
266
+ <div className="relative isolate flex-1 overflow-hidden">{children}</div>
267
+ {/* Stage overlay — replaces the whole stage visually (e.g. the item
268
+ builder) while keeping the canvas mounted. The view bar row steps
269
+ aside for it (see `EditorLayoutV2`); the host's toolbars stay above. */}
270
+ {stageOverlay && <div className="absolute inset-0 z-10">{stageOverlay}</div>}
271
+ {/* Overlays scoped to the stage. `data-viewer-bounds` marks the
272
+ draggable region the floating inspector clamps itself to. */}
273
+ {overlays && (
274
+ <div
275
+ className="pointer-events-none absolute inset-0 z-30"
276
+ data-viewer-bounds
277
+ style={{ transform: 'translateZ(0)' }}
278
+ >
279
+ {overlays}
280
+ </div>
281
+ )}
282
+ </div>
215
283
  </div>
216
284
  )
217
285
  }
@@ -228,6 +296,8 @@ export interface EditorLayoutV2Props {
228
296
  viewerContent: ReactNode
229
297
  overlays?: ReactNode
230
298
  stageOverlay?: ReactNode
299
+ /** Show each pane's view bar. Off for hosts that present one fixed view. */
300
+ viewBar?: boolean
231
301
  }
232
302
 
233
303
  export function EditorLayoutV2({
@@ -240,6 +310,7 @@ export function EditorLayoutV2({
240
310
  viewerContent,
241
311
  overlays,
242
312
  stageOverlay,
313
+ viewBar = true,
243
314
  }: EditorLayoutV2Props) {
244
315
  const isCaptureMode = useEditor((s) => s.isCaptureMode)
245
316
  const isMobile = useIsMobile()
@@ -251,8 +322,9 @@ export function EditorLayoutV2({
251
322
  overlays={overlays}
252
323
  renderTabContent={renderTabContent}
253
324
  sidebarOverlay={sidebarOverlay}
254
- sidebarTabs={sidebarTabs.filter((t) => !t.noPanel)}
325
+ sidebarTabs={sidebarTabs.filter((t) => !t.noPanel || t.onSelect)}
255
326
  viewerContent={viewerContent}
327
+ viewBar={viewBar}
256
328
  viewerToolbarLeft={viewerToolbarLeft}
257
329
  viewerToolbarRight={viewerToolbarRight}
258
330
  />
@@ -278,6 +350,7 @@ export function EditorLayoutV2({
278
350
  stageOverlay={stageOverlay}
279
351
  toolbarLeft={isCaptureMode ? undefined : viewerToolbarLeft}
280
352
  toolbarRight={isCaptureMode ? undefined : viewerToolbarRight}
353
+ viewBar={viewBar && !isCaptureMode && !stageOverlay}
281
354
  >
282
355
  {viewerContent}
283
356
  </RightColumn>
@@ -1,13 +1,18 @@
1
1
  'use client'
2
2
 
3
- import { emitter, useScene } from '@pascal-app/core'
3
+ import { emitter } from '@pascal-app/core'
4
4
  import { disposeObject3DResources, snapLevelsToTruePositions, useViewer } from '@pascal-app/viewer'
5
5
  import { useThree } from '@react-three/fiber'
6
6
  import { useEffect } from 'react'
7
7
  import * as THREE from 'three'
8
8
  import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js'
9
9
  import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js'
10
- import { exportSceneToGlb, nextFrames, prepareSceneForExport } from '../../lib/glb-export'
10
+ import {
11
+ exportSceneToGlb,
12
+ prepareSceneForExport,
13
+ waitForExportGeometry,
14
+ } from '../../lib/glb-export'
15
+ import { exportPreparedSceneToIfc, ifcFileName } from '../../lib/ifc-export'
11
16
  import { exportSceneLevelsForPrint } from '../../lib/level-print-export'
12
17
  import type { ModelExport, ModelExportArtifact } from '../../lib/model-export'
13
18
  import {
@@ -20,6 +25,7 @@ import { filterPreparedSceneForPrintContent } from '../../lib/print-content-scop
20
25
  import { exportSceneToPrintStl, mergePrintExportDiagnostics } from '../../lib/print-export'
21
26
  import { applySemanticPrintFeatureThickness } from '../../lib/print-feature-thickness'
22
27
  import { compileSemanticPrintShellWithManifold } from '../../lib/print-shell-compiler-manifold-worker'
28
+ import { getSavedSceneDocument } from '../../lib/scene'
23
29
  import { exportSceneToUsdz } from '../../lib/usdz-export'
24
30
  import useEditor from '../../store/use-editor'
25
31
 
@@ -51,7 +57,10 @@ export function ExportManager() {
51
57
  const setModelExport = useEditor((state) => state.setModelExport)
52
58
 
53
59
  useEffect(() => {
54
- const exportFn: ModelExport = async (format = 'glb', options = {}) => {
60
+ const exportFn: ModelExport = async (format = 'glb', requested = {}) => {
61
+ // Resolve once: scene preparation prunes hidden nodes by default, and the
62
+ // IFC writer must drop the same nodes it rebuilds parametrically.
63
+ const options = { ...requested, onlyVisible: requested.onlyVisible ?? true }
55
64
  // Find the scene renderer group by name
56
65
  const sceneGroup = scene.getObjectByName('scene-renderer')
57
66
  if (!sceneGroup) {
@@ -67,11 +76,12 @@ export function ExportManager() {
67
76
  // without it every plant exports as its raycast collider, a white box).
68
77
  useViewer.getState().setExporting(true)
69
78
  try {
70
- await nextFrames()
79
+ await waitForExportGeometry(getSavedSceneDocument().nodes, options)
80
+ const { nodes, collections } = getSavedSceneDocument()
71
81
 
72
82
  if (format === 'glb') {
73
83
  const warnings: string[] = []
74
- const buffer = await exportSceneToGlb(sceneGroup, useScene.getState().nodes, {
84
+ const buffer = await exportSceneToGlb(sceneGroup, nodes, {
75
85
  ...options,
76
86
  onWarning: (warning) => warnings.push(warning),
77
87
  })
@@ -81,7 +91,7 @@ export function ExportManager() {
81
91
 
82
92
  if (format === 'usdz') {
83
93
  const warnings: string[] = []
84
- const data = await exportSceneToUsdz(sceneGroup, useScene.getState().nodes, {
94
+ const data = await exportSceneToUsdz(sceneGroup, nodes, {
85
95
  ...options,
86
96
  onWarning: (warning) => warnings.push(warning),
87
97
  })
@@ -95,12 +105,11 @@ export function ExportManager() {
95
105
  // window, so the export snapshots the clean building, then restore.
96
106
  emitter.emit('thumbnail:before-capture', undefined)
97
107
  const restoreLevels = snapLevelsToTruePositions()
98
- const nodes = useScene.getState().nodes
99
108
  let prepared: ReturnType<typeof prepareSceneForExport>
100
109
  try {
101
110
  prepared = prepareSceneForExport(sceneGroup, nodes, {
102
111
  ...options,
103
- requireSynchronousBake: format === 'stl' || format === 'obj',
112
+ requireSynchronousBake: format === 'stl' || format === 'obj' || format === 'ifc',
104
113
  })
105
114
  } finally {
106
115
  restoreLevels()
@@ -206,6 +215,23 @@ export function ExportManager() {
206
215
  return finishArtifact(blob, `model_${date}.stl`, options.download)
207
216
  }
208
217
 
218
+ if (format === 'ifc') {
219
+ const { data, warnings } = exportPreparedSceneToIfc(exportScene, nodes, {
220
+ collections,
221
+ projectName: options.projectName,
222
+ onlyVisible: options.onlyVisible,
223
+ excludedNodeTypes: options.excludedNodeTypes,
224
+ })
225
+ const blob = new Blob([data], { type: 'application/x-step' })
226
+ return finishArtifact(
227
+ blob,
228
+ ifcFileName(options.projectName, `model_${date}`),
229
+ options.download,
230
+ undefined,
231
+ warnings,
232
+ )
233
+ }
234
+
209
235
  if (format === 'obj') {
210
236
  const exporter = new OBJExporter()
211
237
  const result = exporter.parse(exportScene)
@@ -93,6 +93,57 @@ describe('buildFirstPersonColliderWorldFromRegistry', () => {
93
93
  world?.dispose()
94
94
  })
95
95
 
96
+ test('keeps a procedural static body but excludes moving groups', () => {
97
+ registerColliderDefinition('shelf', ShelfNode, 'furnish')
98
+ const shelf = ShelfNode.parse({ id: 'shelf_motion_test' })
99
+ setSceneNodes([shelf])
100
+ mountNode(shelf, [1, 1, 1], [0, 0.5, 0])
101
+ const root = sceneRegistry.nodes.get(shelf.id)!
102
+ const motion = new Group()
103
+ motion.userData.proceduralMotion = { nodeId: shelf.id, partId: 'drawer' }
104
+ const movingMesh = new Mesh(new BoxGeometry(1, 1, 1), new MeshBasicMaterial())
105
+ movingMesh.position.set(5, 0.5, 0)
106
+ motion.add(movingMesh)
107
+ root.add(motion)
108
+ root.updateMatrixWorld(true)
109
+
110
+ const world = buildFirstPersonColliderWorldFromRegistry()!
111
+ expect(world.bounds?.min.x).toBeCloseTo(-0.5)
112
+ expect(world.bounds?.max.x).toBeCloseTo(0.5)
113
+ world.dispose()
114
+ })
115
+
116
+ test('an idle joint tree collides with its static split parts, not its merged rest pose', () => {
117
+ registerColliderDefinition('shelf', ShelfNode, 'furnish')
118
+ const shelf = ShelfNode.parse({ id: 'shelf_rest_test' })
119
+ setSceneNodes([shelf])
120
+ mountRegistryGroup(shelf)
121
+ const root = sceneRegistry.nodes.get(shelf.id)!
122
+ const box = (x: number) => {
123
+ const mesh = new Mesh(new BoxGeometry(1, 1, 1), new MeshBasicMaterial())
124
+ mesh.position.set(x, 0.5, 0)
125
+ return mesh
126
+ }
127
+ // The rest pose merges the body with the gate leaf at x = 5; the hidden split draws them apart.
128
+ const rest = new Group()
129
+ rest.userData.pascalProceduralRest = true
130
+ rest.add(box(0), box(5))
131
+ const split = new Group()
132
+ split.userData.pascalProceduralSplit = true
133
+ split.visible = false
134
+ const motion = new Group()
135
+ motion.userData.proceduralMotion = { nodeId: shelf.id, partId: 'gate' }
136
+ motion.add(box(5))
137
+ split.add(box(0), motion)
138
+ root.add(rest, split)
139
+ root.updateMatrixWorld(true)
140
+
141
+ const world = buildFirstPersonColliderWorldFromRegistry()!
142
+ expect(world.bounds?.min.x).toBeCloseTo(-0.5)
143
+ expect(world.bounds?.max.x).toBeCloseTo(0.5)
144
+ world.dispose()
145
+ })
146
+
96
147
  test('standing clearance and floor hits survive a slab source joining and leaving a batch', () => {
97
148
  registerColliderDefinition('slab', SlabNode, 'structure', 'floor')
98
149
  const slab = SlabNode.parse({ id: 'slab_clearance_batch', polygon: [] })
@@ -356,8 +356,12 @@ function collectColliderGeometriesFromNode(
356
356
  visitedMeshes.add(object)
357
357
 
358
358
  // Prune hidden subtrees — children of an invisible group never render,
359
- // so they must not collide either (see isEffectivelyVisible).
360
- if (!object.visible) return
359
+ // so they must not collide either (see isEffectivelyVisible). An idle joint
360
+ // tree draws a merged rest pose that includes its moving parts; its hidden
361
+ // split group holds the same static parts apart from them.
362
+ if (object.userData.pascalProceduralRest) return
363
+ if (!object.visible && !object.userData.pascalProceduralSplit) return
364
+ if (object.userData.proceduralMotion) return
361
365
 
362
366
  if (
363
367
  isMesh(object) &&
@@ -0,0 +1,75 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeId,
5
+ registerNode,
6
+ sceneRegistry,
7
+ useInteractive,
8
+ useScene,
9
+ } from '@pascal-app/core'
10
+ import { Object3D } from 'three'
11
+ import { mechanismHudInteract, mechanismTargetIds } from './mechanism-targets'
12
+
13
+ const switchMechanism = {
14
+ has: (node: AnyNode) => (node as unknown as { joints: number }).joints > 0,
15
+ isOn: (node: AnyNode, state: ReturnType<typeof useInteractive.getState>) =>
16
+ Boolean(state.mechanisms[node.id]),
17
+ set: (node: AnyNode, on: boolean) => useInteractive.getState().setMechanism(node.id, on),
18
+ }
19
+
20
+ const mounted: [string, string][] = []
21
+
22
+ function mount(kind: string, id: string, fields: Record<string, unknown> = {}): AnyNode {
23
+ const node = { object: 'node', id, type: kind, ...fields } as unknown as AnyNode
24
+ useScene.setState((state) => ({ nodes: { ...state.nodes, [id]: node } }) as never)
25
+ sceneRegistry.nodes.set(id, new Object3D())
26
+ sceneRegistry.byType[kind]!.add(id)
27
+ mounted.push([kind, id])
28
+ return node
29
+ }
30
+
31
+ function registerKind(capabilities: Record<string, unknown>): string {
32
+ const kind = `test:walk-${crypto.randomUUID()}`
33
+ registerNode({
34
+ kind,
35
+ schemaVersion: 1,
36
+ category: 'furnish',
37
+ defaults: () => ({}),
38
+ capabilities,
39
+ } as never)
40
+ return kind
41
+ }
42
+
43
+ afterEach(() => {
44
+ for (const [kind, id] of mounted.splice(0)) {
45
+ sceneRegistry.nodes.delete(id)
46
+ sceneRegistry.byType[kind]!.delete(id)
47
+ useInteractive.getState().removeMechanism(id as AnyNodeId)
48
+ }
49
+ })
50
+
51
+ describe('walkthrough mechanism targets', () => {
52
+ test('any mounted node whose kind declares a mechanism is a target, once', () => {
53
+ const kind = registerKind({ mechanism: switchMechanism })
54
+ mount(kind, 'articulated_1', { joints: 2 })
55
+ mount(kind, 'articulated_still', { joints: 0 })
56
+ mount(registerKind({}), 'plain_1')
57
+ const covered = mount(kind, 'articulated_covered', { joints: 1 })
58
+
59
+ expect(mechanismTargetIds(new Set([covered.id]))).toEqual(['articulated_1' as AnyNodeId])
60
+ })
61
+
62
+ test('the HUD names the node and says what E will do', () => {
63
+ const kind = registerKind({ mechanism: switchMechanism })
64
+ const node = mount(kind, 'articulated_hud', { joints: 1, name: 'Desk lamp' })
65
+ expect(mechanismHudInteract(node)).toEqual({ label: 'Desk lamp', verb: 'turn on' })
66
+ useInteractive.getState().setMechanism(node.id, true)
67
+ expect(mechanismHudInteract(node)).toEqual({ label: 'Desk lamp', verb: 'turn off' })
68
+
69
+ const lid = mount(registerKind({ mechanism: { ...switchMechanism, verb: 'open' } }), 'lid_1', {
70
+ joints: 1,
71
+ })
72
+ expect(mechanismHudInteract(lid)).toEqual({ label: 'item', verb: 'open' })
73
+ expect(mechanismHudInteract(mount(registerKind({}), 'plain_hud'))).toBeNull()
74
+ })
75
+ })