@pascal-app/editor 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. package/package.json +11 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +202 -57
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +36 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +344 -342
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/group-actions.ts +54 -2
  29. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  30. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  31. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  32. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  33. package/src/components/editor/index.tsx +294 -169
  34. package/src/components/editor/join-walls-pill.tsx +44 -0
  35. package/src/components/editor/node-action-menu.tsx +68 -70
  36. package/src/components/editor/node-arrow-handles.tsx +25 -6
  37. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  38. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  39. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  40. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  41. package/src/components/editor/registry-action-contributions.tsx +80 -1
  42. package/src/components/editor/room-controls.tsx +847 -0
  43. package/src/components/editor/room-floor-highlight.tsx +128 -0
  44. package/src/components/editor/room-handles-2d.tsx +354 -0
  45. package/src/components/editor/room-handles.tsx +675 -0
  46. package/src/components/editor/room-highlight.tsx +225 -0
  47. package/src/components/editor/room-labels.test.ts +357 -0
  48. package/src/components/editor/room-labels.tsx +990 -0
  49. package/src/components/editor/room-transform-ghost.tsx +256 -0
  50. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  51. package/src/components/editor/selection-manager.tsx +451 -317
  52. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  53. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  54. package/src/components/editor/snapshot-capture.test.ts +52 -4
  55. package/src/components/editor/snapshot-capture.ts +30 -5
  56. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  57. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  58. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  59. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  60. package/src/components/editor/views/host-view.tsx +61 -0
  61. package/src/components/editor/views/plugins-view.tsx +670 -0
  62. package/src/components/editor/views/scene-region.tsx +59 -0
  63. package/src/components/editor/views/use-editor-views.ts +57 -0
  64. package/src/components/editor/views/view-bar.tsx +220 -0
  65. package/src/components/editor/wall-justification.test.ts +140 -0
  66. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  67. package/src/components/editor/wall-push-arrow.tsx +92 -0
  68. package/src/components/editor/wall-region-handles.tsx +509 -0
  69. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  71. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  72. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  73. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  74. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
  75. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  76. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  77. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  78. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  79. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  80. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  81. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
  82. package/src/components/editor-2d/svg-paths.ts +12 -15
  83. package/src/components/procedural-items/inspector.tsx +40 -15
  84. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  85. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  86. package/src/components/systems/zone/zone-system.test.ts +47 -0
  87. package/src/components/systems/zone/zone-system.tsx +29 -68
  88. package/src/components/tools/item/placement-history.test.tsx +474 -0
  89. package/src/components/tools/item/placement-strategies.ts +8 -4
  90. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  91. package/src/components/tools/item/use-draft-node.ts +166 -95
  92. package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
  93. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  94. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  95. package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
  96. package/src/components/tools/select/box-select-tool.tsx +2 -12
  97. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  98. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  99. package/src/components/tools/select/select-candidates.test.ts +37 -4
  100. package/src/components/tools/select/select-candidates.ts +15 -11
  101. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  102. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  103. package/src/components/tools/stair/stair-defaults.ts +0 -3
  104. package/src/components/tools/stair/stair-tool.tsx +90 -42
  105. package/src/components/tools/tool-manager.tsx +46 -20
  106. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  107. package/src/components/tools/zone/zone-tool.tsx +4 -12
  108. package/src/components/ui/action-menu/action-button.tsx +3 -0
  109. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  110. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  111. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  112. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  113. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  114. package/src/components/ui/command-palette/index.tsx +5 -24
  115. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  116. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  117. package/src/components/ui/controls/material-picker.tsx +4 -1
  118. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  119. package/src/components/ui/controls/select-control.tsx +65 -0
  120. package/src/components/ui/floating-level-selector.tsx +9 -14
  121. package/src/components/ui/helpers/building-helper.tsx +4 -1
  122. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  123. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  124. package/src/components/ui/helpers/helper-manager.tsx +325 -14
  125. package/src/components/ui/helpers/item-helper.tsx +8 -0
  126. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  127. package/src/components/ui/icon-ref.tsx +45 -1
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/node-icon-image.tsx +16 -0
  130. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  131. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  132. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  133. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  134. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  135. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  136. package/src/components/ui/panels/multi-field-value.ts +6 -3
  137. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  138. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  139. package/src/components/ui/panels/node-display.ts +8 -3
  140. package/src/components/ui/panels/panel-manager.tsx +103 -68
  141. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  142. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  143. package/src/components/ui/panels/parametric-inspector.tsx +4 -4
  144. package/src/components/ui/panels/reference-panel.tsx +2 -3
  145. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  146. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  147. package/src/components/ui/panels/room-name-options.ts +47 -0
  148. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  149. package/src/components/ui/panels/room-panel.tsx +725 -0
  150. package/src/components/ui/panels/shape-choice.tsx +58 -0
  151. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  152. package/src/components/ui/primitives/color-dot.tsx +36 -24
  153. package/src/components/ui/primitives/sidebar.tsx +2 -0
  154. package/src/components/ui/right-stack.test.ts +29 -0
  155. package/src/components/ui/right-stack.tsx +145 -0
  156. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  157. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
  158. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  159. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  160. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  161. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  162. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  163. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  164. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  165. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  166. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  167. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  168. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  169. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  170. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  171. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  172. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  173. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  174. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  175. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  176. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  177. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  178. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  179. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  180. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  181. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  182. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  183. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  184. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  185. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  186. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  187. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  188. package/src/components/viewer/floorplan-preview.tsx +5 -6
  189. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  190. package/src/components/viewer/viewer-rooms.tsx +175 -0
  191. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  192. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  193. package/src/components/viewer/viewer-stage.tsx +2 -2
  194. package/src/components/viewer-zone-system.tsx +3 -8
  195. package/src/hooks/use-auto-save.integration.test.tsx +123 -9
  196. package/src/hooks/use-auto-save.ts +34 -8
  197. package/src/hooks/use-keyboard.test.ts +178 -3
  198. package/src/hooks/use-keyboard.ts +89 -71
  199. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  200. package/src/hooks/use-open-wall-ends.ts +238 -0
  201. package/src/hooks/use-placement-notice.test.ts +48 -0
  202. package/src/hooks/use-placement-notice.ts +42 -0
  203. package/src/hooks/use-selected-room.ts +143 -0
  204. package/src/index.tsx +246 -39
  205. package/src/lib/ceiling-edit-session.test.ts +321 -0
  206. package/src/lib/ceiling-edit-session.ts +206 -0
  207. package/src/lib/ceiling-region-session.test.ts +179 -0
  208. package/src/lib/continuation.test.ts +19 -1
  209. package/src/lib/continuation.ts +25 -4
  210. package/src/lib/door-interaction.ts +14 -0
  211. package/src/lib/editor-level-display.test.ts +96 -0
  212. package/src/lib/editor-level-display.ts +48 -0
  213. package/src/lib/editor-views.ts +144 -0
  214. package/src/lib/export-geometry-wait.test.ts +90 -0
  215. package/src/lib/floor-edit-session.ts +153 -0
  216. package/src/lib/floor-footprints.test.ts +563 -0
  217. package/src/lib/floor-footprints.ts +395 -0
  218. package/src/lib/floor-foundation-support.test.ts +283 -0
  219. package/src/lib/floor-opening-draft.ts +218 -0
  220. package/src/lib/floor-openings.test.ts +216 -0
  221. package/src/lib/floor-region-geometry.ts +191 -0
  222. package/src/lib/floor-region-session.test.ts +485 -0
  223. package/src/lib/floor-region-session.ts +589 -0
  224. package/src/lib/floor-region-snap.ts +101 -0
  225. package/src/lib/floor-step-hover.test.ts +32 -0
  226. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  227. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  228. package/src/lib/floorplan/dimension-drive.ts +479 -0
  229. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  230. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  231. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  232. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  233. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  234. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  235. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  236. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  237. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  238. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  239. package/src/lib/floorplan/geometry.ts +13 -6
  240. package/src/lib/floorplan/index.ts +0 -9
  241. package/src/lib/floorplan/items.ts +5 -2
  242. package/src/lib/floorplan/marks.test.ts +213 -0
  243. package/src/lib/floorplan/marks.ts +238 -0
  244. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  245. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  246. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  247. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  248. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  249. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  250. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  251. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  252. package/src/lib/floorplan/schedules.test.ts +189 -0
  253. package/src/lib/floorplan/schedules.ts +339 -0
  254. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  255. package/src/lib/floorplan/sheet-export.ts +318 -0
  256. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  257. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  258. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  259. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  260. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  261. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  262. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  263. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  264. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  265. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  266. package/src/lib/floorplan/site-plan/index.ts +71 -0
  267. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  268. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  269. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  270. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  271. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  272. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  273. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  274. package/src/lib/floorplan/types.ts +1 -27
  275. package/src/lib/fresh-planar-placement.test.ts +36 -0
  276. package/src/lib/fresh-planar-placement.ts +59 -36
  277. package/src/lib/geometry-script/author.ts +152 -0
  278. package/src/lib/geometry-script/client.ts +152 -0
  279. package/src/lib/geometry-script/protocol.ts +12 -0
  280. package/src/lib/gesture-lifecycle.test.ts +595 -0
  281. package/src/lib/gesture-lifecycle.ts +290 -0
  282. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  283. package/src/lib/glb-export.test.ts +485 -0
  284. package/src/lib/glb-export.ts +237 -119
  285. package/src/lib/history.test.ts +6 -0
  286. package/src/lib/history.ts +7 -0
  287. package/src/lib/hud-title.ts +109 -0
  288. package/src/lib/hydration-gated-storage.ts +49 -0
  289. package/src/lib/ifc-export.test.ts +174 -0
  290. package/src/lib/ifc-export.ts +201 -0
  291. package/src/lib/ifc-parts.ts +15 -0
  292. package/src/lib/inspector-expanded.test.ts +44 -0
  293. package/src/lib/inspector-expanded.ts +36 -0
  294. package/src/lib/interaction/scope.ts +18 -0
  295. package/src/lib/keyboard-pan.test.ts +129 -7
  296. package/src/lib/keyboard-pan.ts +161 -12
  297. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  298. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  299. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  300. package/src/lib/lot/clean-ring.test.ts +186 -0
  301. package/src/lib/lot/clean-ring.ts +217 -0
  302. package/src/lib/lot/dossier.test.ts +202 -0
  303. package/src/lib/lot/dossier.ts +498 -0
  304. package/src/lib/lot/drop-in.test.ts +326 -0
  305. package/src/lib/lot/drop-in.ts +407 -0
  306. package/src/lib/lot/index.ts +43 -0
  307. package/src/lib/lot/lot-patch.test.ts +340 -0
  308. package/src/lib/lot/lot-patch.ts +394 -0
  309. package/src/lib/lot/parcel-provider.ts +43 -0
  310. package/src/lib/lot/terrain.test.ts +161 -0
  311. package/src/lib/lot/terrain.ts +266 -0
  312. package/src/lib/material-catalog-model.ts +3 -1
  313. package/src/lib/material-paint-panel-model.ts +9 -5
  314. package/src/lib/material-paint.ts +4 -0
  315. package/src/lib/mezzanine-actions.test.ts +342 -0
  316. package/src/lib/mezzanine-draft.test.ts +501 -0
  317. package/src/lib/mezzanine-draft.ts +278 -0
  318. package/src/lib/mezzanine-messages.ts +26 -0
  319. package/src/lib/mezzanine-selection.test.ts +207 -0
  320. package/src/lib/mezzanine-stairs.ts +46 -0
  321. package/src/lib/model-export.ts +3 -1
  322. package/src/lib/node-action-movement.ts +17 -6
  323. package/src/lib/node-display-label.ts +37 -0
  324. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  325. package/src/lib/orbit-floor-clearance.ts +106 -0
  326. package/src/lib/paint-commit-route.test.ts +37 -0
  327. package/src/lib/paint-commit-route.ts +17 -0
  328. package/src/lib/paint-eyedropper.test.tsx +165 -0
  329. package/src/lib/paint-eyedropper.ts +99 -0
  330. package/src/lib/paint-pass-through.test.ts +111 -0
  331. package/src/lib/paint-pass-through.ts +76 -0
  332. package/src/lib/paint-region-hover.ts +25 -0
  333. package/src/lib/paint-region-mode.ts +154 -0
  334. package/src/lib/paint-regions.test.ts +166 -0
  335. package/src/lib/paint-regions.ts +192 -0
  336. package/src/lib/paint-scope.test.ts +317 -35
  337. package/src/lib/paint-scope.ts +266 -25
  338. package/src/lib/panel-rows.ts +21 -0
  339. package/src/lib/parametric-node-update.ts +1 -1
  340. package/src/lib/plate-paint-affected.test.ts +300 -0
  341. package/src/lib/plate-paint-affected.ts +177 -0
  342. package/src/lib/plugin-chat.ts +50 -0
  343. package/src/lib/plugin-panels.test.ts +25 -0
  344. package/src/lib/plugin-panels.ts +94 -0
  345. package/src/lib/print-shell-compiler.ts +1 -0
  346. package/src/lib/registered-draft-snap.ts +12 -0
  347. package/src/lib/roof-duplication.ts +10 -2
  348. package/src/lib/room-assembly-overlay.test.ts +275 -0
  349. package/src/lib/room-assembly-overlay.ts +266 -0
  350. package/src/lib/room-built-on.test.ts +379 -0
  351. package/src/lib/room-built-on.ts +138 -0
  352. package/src/lib/room-construction-commands.ts +712 -0
  353. package/src/lib/room-construction.test.ts +746 -0
  354. package/src/lib/room-construction.ts +385 -0
  355. package/src/lib/room-controls-phase6.test.ts +630 -0
  356. package/src/lib/room-divide-session.ts +523 -0
  357. package/src/lib/room-handle-drag.ts +1068 -0
  358. package/src/lib/room-name-catalog.test.ts +90 -0
  359. package/src/lib/room-name-catalog.ts +168 -0
  360. package/src/lib/room-push-dimensions.test.ts +311 -0
  361. package/src/lib/room-push-dimensions.ts +251 -0
  362. package/src/lib/room-selection-commands.ts +62 -0
  363. package/src/lib/room-selection.test.ts +973 -0
  364. package/src/lib/room-selection.ts +417 -0
  365. package/src/lib/room-structure-commands.ts +94 -0
  366. package/src/lib/room-structure.test.ts +921 -0
  367. package/src/lib/room-transform-session.ts +570 -0
  368. package/src/lib/room-zone-routing.test.ts +294 -0
  369. package/src/lib/room-zone-routing.ts +197 -0
  370. package/src/lib/scene-clipboard.test.ts +364 -0
  371. package/src/lib/scene-clipboard.ts +229 -48
  372. package/src/lib/scene-material-refs.test.ts +79 -0
  373. package/src/lib/scene-material-refs.ts +79 -0
  374. package/src/lib/scene.test.ts +24 -36
  375. package/src/lib/scene.ts +33 -57
  376. package/src/lib/selection-routing.test.ts +129 -45
  377. package/src/lib/selection-routing.ts +53 -74
  378. package/src/lib/session-writes.test.ts +120 -0
  379. package/src/lib/session-writes.ts +205 -0
  380. package/src/lib/sidebar-panel.ts +62 -0
  381. package/src/lib/site-boundary.test.ts +4 -6
  382. package/src/lib/site-boundary.ts +1 -1
  383. package/src/lib/snapping-mode.test.ts +4 -0
  384. package/src/lib/snapping-mode.ts +8 -1
  385. package/src/lib/terrace-draft.test.ts +145 -0
  386. package/src/lib/terrace-draft.ts +213 -0
  387. package/src/lib/tool-cursor-icon.test.ts +16 -0
  388. package/src/lib/tool-cursor-icon.ts +11 -0
  389. package/src/lib/typing-target.ts +9 -0
  390. package/src/lib/units.ts +77 -39
  391. package/src/lib/vector-edges.test.ts +58 -0
  392. package/src/lib/vector-edges.ts +400 -0
  393. package/src/lib/viewer-selection.test.ts +577 -0
  394. package/src/lib/viewer-selection.ts +569 -0
  395. package/src/lib/wall-draw-variant.test.ts +71 -0
  396. package/src/lib/wall-draw-variant.ts +91 -0
  397. package/src/lib/wall-paint-region-session.ts +246 -0
  398. package/src/lib/wall-paint-region.test.ts +373 -0
  399. package/src/lib/wall-polygon-draft.test.ts +262 -0
  400. package/src/lib/wall-polygon-draft.ts +286 -0
  401. package/src/lib/wall-push.test.ts +453 -0
  402. package/src/lib/wall-region-face.ts +352 -0
  403. package/src/lib/wall-region-handles.test.ts +304 -0
  404. package/src/lib/wall-region-handles.ts +368 -0
  405. package/src/lib/wall-region-list.test.ts +119 -0
  406. package/src/lib/wall-region-list.ts +136 -0
  407. package/src/lib/wall-region-snap.ts +110 -0
  408. package/src/lib/window-interaction.ts +14 -0
  409. package/src/lib/zone-content.ts +4 -1
  410. package/src/lib/zone-name.test.ts +17 -0
  411. package/src/lib/zone-name.ts +17 -0
  412. package/src/store/opens-in-select-mode.test.ts +223 -0
  413. package/src/store/paint-mode.test.ts +188 -0
  414. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  415. package/src/store/tool-mode.test.ts +137 -6
  416. package/src/store/use-delete-confirmation.ts +28 -1
  417. package/src/store/use-drawing-view.test.ts +24 -3
  418. package/src/store/use-drawing-view.ts +29 -1
  419. package/src/store/use-editor.tsx +432 -321
  420. package/src/store/use-floorplan-draft-preview.ts +13 -0
  421. package/src/store/use-floorplan-marquee.ts +12 -2
  422. package/src/store/use-quick-measurement-hud.ts +3 -3
  423. package/src/store/use-stair-build-preview.ts +13 -5
  424. package/src/store/use-wall-move-ghosts.ts +4 -2
  425. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  426. package/src/store/use-wall-region-handles-selection.ts +139 -0
  427. package/src/store/view-layout.test.ts +175 -0
  428. package/src/store/view-layout.ts +196 -0
  429. package/src/test-utils/immediate-animation-frames.ts +16 -0
  430. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  431. package/src/test-utils/selection-harness.ts +82 -0
  432. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  433. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  434. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  435. package/src/components/tools/fence/fence-drafting.ts +0 -266
  436. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  437. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  438. package/src/lib/floorplan/stairs.ts +0 -489
  439. package/src/lib/level-duplication.test.ts +0 -144
  440. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,58 @@
1
+ 'use client'
2
+
3
+ import { Pentagon, Square } from 'lucide-react'
4
+ import { cn } from '../../../lib/utils'
5
+
6
+ export type OutlineShape = 'rectangle' | 'polygon'
7
+
8
+ const SHAPES = [
9
+ { id: 'rectangle', label: 'Rectangle', Icon: Square },
10
+ { id: 'polygon', label: 'Polygon', Icon: Pentagon },
11
+ ] as const
12
+
13
+ const pill =
14
+ 'inline-flex items-center gap-1 rounded-full border border-border/50 px-2 py-0.5 text-xs text-foreground transition-colors hover:bg-accent disabled:pointer-events-none disabled:opacity-50'
15
+
16
+ /**
17
+ * The outline shape, picked before drawing starts — the same two choices the
18
+ * Build panel's room tiles offer, so a cut or a mezzanine is started the way a
19
+ * room is.
20
+ */
21
+ export function ShapeChoice({
22
+ label,
23
+ onPick,
24
+ disabled,
25
+ className,
26
+ data,
27
+ }: {
28
+ label: string
29
+ onPick: (shape: OutlineShape) => void
30
+ disabled?: boolean
31
+ className?: string
32
+ /** `data-shape-choice` hook for tests and proofs. */
33
+ data?: string
34
+ }) {
35
+ return (
36
+ <div
37
+ className={cn('flex min-h-7 items-center justify-between gap-2', className)}
38
+ data-shape-choice={data}
39
+ >
40
+ <span className="whitespace-nowrap">{label}</span>
41
+ <div className="flex items-center gap-1.5">
42
+ {SHAPES.map(({ id, label: shapeLabel, Icon }) => (
43
+ <button
44
+ className={pill}
45
+ data-shape={id}
46
+ disabled={disabled}
47
+ key={id}
48
+ onClick={() => onPick(id)}
49
+ type="button"
50
+ >
51
+ <Icon aria-hidden className="h-3 w-3" />
52
+ {shapeLabel}
53
+ </button>
54
+ ))}
55
+ </div>
56
+ </div>
57
+ )
58
+ }
@@ -0,0 +1,109 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNodeId,
5
+ useLiveNodeOverrides,
6
+ useScene,
7
+ type WallFaceRegion,
8
+ } from '@pascal-app/core'
9
+ import { useViewer } from '@pascal-app/viewer'
10
+ import { Trash2 } from 'lucide-react'
11
+ import { useMemo } from 'react'
12
+ import { removeWallRegion } from '../../../lib/paint-regions'
13
+ import { sfxEmitter } from '../../../lib/sfx-bus'
14
+ import { cn } from '../../../lib/utils'
15
+ import {
16
+ wallRegionCapNotice,
17
+ wallRegionFaceLabels,
18
+ wallRegionRows,
19
+ } from '../../../lib/wall-region-list'
20
+ import { useWallRegionSelection } from '../../../store/use-wall-region-handles-selection'
21
+ import { PanelSection } from '../controls/panel-section'
22
+
23
+ const ROW_ACTION_CLASS =
24
+ 'flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-black/5 hover:text-foreground dark:hover:bg-white/10'
25
+
26
+ /** The wall panel's "Paint regions" section: one row per region, select or delete. */
27
+ export function WallPaintRegionList({ wallId }: { wallId: string }) {
28
+ const nodes = useScene((state) => state.nodes)
29
+ const materials = useScene((state) => state.materials)
30
+ const unit = useViewer((state) => state.unit)
31
+ const active = useWallRegionSelection((state) => state.active)
32
+ const setActive = useWallRegionSelection((state) => state.setActive)
33
+ // A handle drag previews through a live override; list the live bounds.
34
+ const liveRegions = useLiveNodeOverrides(
35
+ (state) =>
36
+ state.overrides.get(wallId as AnyNodeId)?.faceRegions as WallFaceRegion[] | undefined,
37
+ )
38
+ const sceneWall = nodes[wallId as AnyNodeId]
39
+ const wall = sceneWall?.type === 'wall' ? sceneWall : null
40
+ const faceLabels = useMemo(() => wallRegionFaceLabels(nodes, wallId), [nodes, wallId])
41
+
42
+ if (!wall) return null
43
+ const faceRegions = liveRegions ?? wall.faceRegions
44
+ if (!faceRegions?.length) return null
45
+ const rows = wallRegionRows({ faceRegions }, faceLabels, materials, unit)
46
+ const capNotice = wallRegionCapNotice({ faceRegions }, faceLabels)
47
+
48
+ return (
49
+ // Focus inside this list still lets Delete target the active region.
50
+ <div className="contents" data-wall-region-list={wallId}>
51
+ <PanelSection title="Paint regions">
52
+ <div className="flex flex-col gap-1">
53
+ {rows.map((row) => {
54
+ const isActive = active?.wallId === wallId && active.regionId === row.id
55
+ return (
56
+ <div
57
+ className={cn(
58
+ 'flex h-9 items-center gap-2 rounded-md px-1 transition-colors',
59
+ isActive ? 'bg-accent' : 'hover:bg-accent/50',
60
+ )}
61
+ data-wall-region-row={row.id}
62
+ key={row.id}
63
+ >
64
+ <button
65
+ aria-pressed={isActive}
66
+ className="flex min-w-0 flex-1 cursor-pointer items-center gap-2 text-left"
67
+ onClick={() => setActive({ wallId, regionId: row.id })}
68
+ type="button"
69
+ >
70
+ <span
71
+ aria-hidden="true"
72
+ className="h-4 w-4 shrink-0 rounded-sm border border-border/50 bg-center bg-cover"
73
+ style={{
74
+ backgroundColor: row.finish.color,
75
+ backgroundImage: row.finish.imageUrl
76
+ ? `url(${row.finish.imageUrl})`
77
+ : undefined,
78
+ }}
79
+ />
80
+ <span className="flex min-w-0 flex-col leading-tight">
81
+ <span className="truncate text-sm">{row.finish.name}</span>
82
+ <span className="truncate text-[11px] text-muted-foreground">
83
+ {row.faceLabel} · {row.boundsText}
84
+ </span>
85
+ </span>
86
+ </button>
87
+ <button
88
+ aria-label="Delete region"
89
+ className={ROW_ACTION_CLASS}
90
+ onClick={() => {
91
+ if (isActive) setActive(null)
92
+ if (removeWallRegion(wallId, row.id)) sfxEmitter.emit('sfx:structure-delete')
93
+ }}
94
+ title="Delete region"
95
+ type="button"
96
+ >
97
+ <Trash2 className="h-3 w-3" />
98
+ </button>
99
+ </div>
100
+ )
101
+ })}
102
+ {capNotice && (
103
+ <div className="px-1 text-[11px] text-muted-foreground">{capNotice}</div>
104
+ )}
105
+ </div>
106
+ </PanelSection>
107
+ </div>
108
+ )
109
+ }
@@ -1,43 +1,55 @@
1
1
  'use client'
2
2
 
3
+ import { ZONE_COLOR_PALETTE } from '@pascal-app/core'
3
4
  import { useState } from 'react'
4
5
  import { cn } from '../../../lib/utils'
5
6
  import { Popover, PopoverContent, PopoverTrigger } from './popover'
7
+ import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip'
6
8
 
7
- export const PALETTE_COLORS = [
8
- '#ef4444', // Red 0°
9
- '#f97316', // Orange 30°
10
- '#f59e0b', // Amber 45°
11
- '#84cc16', // Lime 85°
12
- '#22c55e', // Green 142°
13
- '#10b981', // Emerald 160°
14
- '#06b6d4', // Cyan 190°
15
- '#3b82f6', // Blue 217°
16
- '#6366f1', // Indigo 239°
17
- '#a855f7', // Violet 270°
18
- '#64748b', // Dark gray
19
- '#cccccc', // Light gray
20
- ]
9
+ export const PALETTE_COLORS = ZONE_COLOR_PALETTE
21
10
 
22
11
  interface ColorDotProps {
23
12
  color: string
24
13
  onChange: (color: string) => void
14
+ /** Says what the colour is for, on hover and to assistive tech. */
15
+ label?: string
16
+ side?: 'left' | 'bottom'
25
17
  }
26
18
 
27
- export function ColorDot({ color, onChange }: ColorDotProps) {
19
+ export function ColorDot({ color, onChange, label, side = 'left' }: ColorDotProps) {
28
20
  const [open, setOpen] = useState(false)
21
+ const trigger = (
22
+ <PopoverTrigger asChild>
23
+ <button
24
+ aria-label={label}
25
+ className="relative h-3 w-3 shrink-0 cursor-pointer rounded-sm border border-border/50 transition-all hover:ring-1 hover:ring-ring/50"
26
+ data-color-dot
27
+ onClick={(e) => e.stopPropagation()}
28
+ style={{ backgroundColor: color }}
29
+ type="button"
30
+ />
31
+ </PopoverTrigger>
32
+ )
29
33
 
30
34
  return (
31
35
  <Popover onOpenChange={setOpen} open={open}>
32
- <PopoverTrigger asChild>
33
- <button
34
- className="relative h-3 w-3 shrink-0 cursor-pointer rounded-sm border border-border/50 transition-all hover:ring-1 hover:ring-ring/50"
35
- onClick={(e) => e.stopPropagation()}
36
- style={{ backgroundColor: color }}
37
- type="button"
38
- />
39
- </PopoverTrigger>
40
- <PopoverContent align="center" className="w-auto p-1.5" side="left" sideOffset={6}>
36
+ {label ? (
37
+ <Tooltip>
38
+ <TooltipTrigger asChild>{trigger}</TooltipTrigger>
39
+ <TooltipContent side="bottom">{label}</TooltipContent>
40
+ </Tooltip>
41
+ ) : (
42
+ trigger
43
+ )}
44
+ <PopoverContent
45
+ align={side === 'bottom' ? 'start' : 'center'}
46
+ className="w-auto p-1.5"
47
+ // Keep the pick inside the palette: in a panel header, a pointerdown
48
+ // bubbling out of the portal would start dragging the panel.
49
+ onPointerDown={(event) => event.stopPropagation()}
50
+ side={side}
51
+ sideOffset={6}
52
+ >
41
53
  <div className="grid grid-cols-4 gap-1">
42
54
  {PALETTE_COLORS.map((c) => (
43
55
  <button
@@ -24,6 +24,7 @@ import {
24
24
  TooltipTrigger,
25
25
  } from './../../../components/ui/primitives/tooltip'
26
26
  import { useIsMobile } from './../../../hooks/use-mobile'
27
+ import { hydrationGatedStorage } from './../../../lib/hydration-gated-storage'
27
28
  import { cn } from './../../../lib/utils'
28
29
 
29
30
  const SIDEBAR_COOKIE_NAME = 'sidebar_state'
@@ -62,6 +63,7 @@ export const useSidebarStore = create<SidebarStore>()(
62
63
  }),
63
64
  {
64
65
  name: 'sidebar-preferences',
66
+ storage: hydrationGatedStorage((): boolean => useSidebarStore.persist.hasHydrated()),
65
67
  partialize: (state) => ({ width: state.width, isCollapsed: state.isCollapsed }),
66
68
  skipHydration: true,
67
69
  },
@@ -0,0 +1,29 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { rightStackFloors } from './right-stack'
3
+
4
+ const tall = { headerHeight: 52, cardHeight: 240 }
5
+
6
+ test('the inspector keeps 180 px, or its own height if shorter, above the full card', () => {
7
+ expect(rightStackFloors({ ...tall, naturalHeight: 500, stackHeight: 700 })).toEqual({
8
+ inspector: 180,
9
+ card: 240,
10
+ })
11
+ expect(rightStackFloors({ ...tall, naturalHeight: 120, stackHeight: 700 }).inspector).toBe(120)
12
+ expect(
13
+ rightStackFloors({ naturalHeight: 0, headerHeight: 0, cardHeight: 240, stackHeight: 700 }),
14
+ ).toEqual({ inspector: 0, card: 240 })
15
+ })
16
+
17
+ test('on a short window the inspector gives way first, down to its header', () => {
18
+ expect(rightStackFloors({ ...tall, naturalHeight: 500, stackHeight: 400 })).toEqual({
19
+ inspector: 152,
20
+ card: 240,
21
+ })
22
+ })
23
+
24
+ test('on a very short window the inspector keeps its header and the card scrolls', () => {
25
+ expect(rightStackFloors({ ...tall, naturalHeight: 500, stackHeight: 258 })).toEqual({
26
+ inspector: 52,
27
+ card: 198,
28
+ })
29
+ })
@@ -0,0 +1,145 @@
1
+ 'use client'
2
+
3
+ import { createContext, type ReactNode, useContext, useEffect, useRef } from 'react'
4
+ import { useIsMobile } from '../../hooks/use-mobile'
5
+
6
+ const RightStackContext = createContext(false)
7
+
8
+ /** Whether a panel renders inside the shared right column (in flow) rather than on its own. */
9
+ export function useInRightStack() {
10
+ return useContext(RightStackContext)
11
+ }
12
+
13
+ /** The inspector never shrinks below this (or its own height, if shorter) for the card, px. */
14
+ const INSPECTOR_MIN_HEIGHT = 180
15
+
16
+ /** Gap between the inspector and the card, px (`gap-2`). */
17
+ const GAP = 8
18
+
19
+ /**
20
+ * The heights neither panel shrinks below. The inspector keeps 180 px (or its
21
+ * own height, if shorter) while the full card fits under it, and never less
22
+ * than its header row. The card shows in full whenever the column allows; on a
23
+ * very short window it gives way and scrolls on itself — both stay usable.
24
+ */
25
+ export function rightStackFloors(args: {
26
+ naturalHeight: number
27
+ headerHeight: number
28
+ cardHeight: number
29
+ stackHeight: number
30
+ }): { inspector: number; card: number } {
31
+ const { naturalHeight, headerHeight, cardHeight, stackHeight } = args
32
+ const gap = naturalHeight > 0 && cardHeight > 0 ? GAP : 0
33
+ const header = Math.min(headerHeight, naturalHeight)
34
+ const inspector = Math.max(
35
+ header,
36
+ Math.min(INSPECTOR_MIN_HEIGHT, naturalHeight, stackHeight - cardHeight - gap),
37
+ )
38
+ const card = Math.max(0, Math.min(cardHeight, stackHeight - gap - inspector))
39
+ return { inspector, card }
40
+ }
41
+
42
+ /** The inspector's full height: its header plus everything its scroller holds. */
43
+ function inspectorHeights(slot: HTMLElement): { natural: number; header: number } {
44
+ const panel = slot.querySelector<HTMLElement>('[data-panel-wrapper]')
45
+ if (!panel) return { natural: 0, header: 0 }
46
+ const header = panel.querySelector<HTMLElement>('[data-panel-header]')?.offsetHeight ?? 0
47
+ const scroller = panel.querySelector<HTMLElement>('[data-panel-scroll]')
48
+ if (!scroller) return { natural: panel.offsetHeight, header }
49
+ return { natural: panel.offsetHeight - scroller.clientHeight + scroller.scrollHeight, header }
50
+ }
51
+
52
+ /**
53
+ * The right-hand column: the shortcuts card shows in full at the bottom, and
54
+ * the node inspector takes the space above it, growing down from the top and
55
+ * scrolling on itself. On a very short window the inspector keeps its header
56
+ * and the card scrolls instead. Neither hides the other. On a phone the two
57
+ * keep their own layouts (sheet / no card).
58
+ */
59
+ export function RightStack({ inspector, helper }: { inspector: ReactNode; helper: ReactNode }) {
60
+ const isMobile = useIsMobile()
61
+ const stackRef = useRef<HTMLDivElement>(null)
62
+ const inspectorRef = useRef<HTMLDivElement>(null)
63
+ const cardRef = useRef<HTMLDivElement>(null)
64
+
65
+ // `min(180px, its own height)` has no CSS form for a flex item that scrolls,
66
+ // so both floors are measured (`rightStackFloors`).
67
+ useEffect(() => {
68
+ if (isMobile) return
69
+ const stack = stackRef.current
70
+ const slot = inspectorRef.current
71
+ const card = cardRef.current
72
+ if (!(stack && slot && card)) return
73
+ let frame = 0
74
+ const fit = () => {
75
+ frame = 0
76
+ const { natural, header } = inspectorHeights(slot)
77
+ const floors = rightStackFloors({
78
+ naturalHeight: natural,
79
+ headerHeight: header,
80
+ cardHeight: card.scrollHeight,
81
+ stackHeight: stack.clientHeight,
82
+ })
83
+ const inspectorMin = `${floors.inspector}px`
84
+ const cardMin = `${floors.card}px`
85
+ if (slot.style.minHeight !== inspectorMin) slot.style.minHeight = inspectorMin
86
+ if (card.style.minHeight !== cardMin) card.style.minHeight = cardMin
87
+ // The card is click-through; it takes the pointer only when it has to scroll.
88
+ const pointer = floors.card < card.scrollHeight ? 'auto' : ''
89
+ if (card.style.pointerEvents !== pointer) card.style.pointerEvents = pointer
90
+ }
91
+ const schedule = () => {
92
+ if (!frame) frame = requestAnimationFrame(fit)
93
+ }
94
+ const resize = new ResizeObserver(schedule)
95
+ resize.observe(stack)
96
+ resize.observe(card)
97
+ // Content growing inside the scroller changes no outer size, hence the mutation watch.
98
+ // The same for the card: a new tool swaps its rows without resizing the slot.
99
+ const mutation = new MutationObserver(schedule)
100
+ for (const target of [slot, card])
101
+ mutation.observe(target, {
102
+ subtree: true,
103
+ childList: true,
104
+ attributes: true,
105
+ attributeFilter: ['class', 'hidden', 'aria-expanded'],
106
+ characterData: true,
107
+ })
108
+ fit()
109
+ return () => {
110
+ if (frame) cancelAnimationFrame(frame)
111
+ resize.disconnect()
112
+ mutation.disconnect()
113
+ }
114
+ }, [isMobile])
115
+
116
+ if (isMobile) {
117
+ return (
118
+ <>
119
+ <div className="pointer-events-auto">{inspector}</div>
120
+ <div className="pointer-events-auto">{helper}</div>
121
+ </>
122
+ )
123
+ }
124
+
125
+ return (
126
+ <RightStackContext.Provider value={true}>
127
+ <div
128
+ className="pointer-events-none fixed top-20 right-4 bottom-[74px] z-40 flex flex-col items-end gap-2"
129
+ data-right-stack
130
+ ref={stackRef}
131
+ >
132
+ <div className="flex min-h-0 flex-[0_1_auto] flex-col items-end" ref={inspectorRef}>
133
+ {inspector}
134
+ </div>
135
+ <div
136
+ className="no-scrollbar mt-auto flex min-h-0 flex-[0_1_auto] flex-col overflow-y-auto rounded-xl"
137
+ data-right-stack-card
138
+ ref={cardRef}
139
+ >
140
+ {helper}
141
+ </div>
142
+ </div>
143
+ </RightStackContext.Provider>
144
+ )
145
+ }
@@ -17,6 +17,11 @@ export type ExtraPanel = {
17
17
  label: string
18
18
  component: ComponentType
19
19
  pluginId?: string
20
+ /** Rail entry that drives the stage (see `SidebarTab.noPanel`); `component` serves the v1 sidebar. */
21
+ noPanel?: boolean
22
+ onSelect?: () => void
23
+ stageActive?: boolean
24
+ onDeselect?: () => void
20
25
  }
21
26
 
22
27
  interface IconRailProps {
@@ -2,6 +2,7 @@ import {
2
2
  clearSceneHistory,
3
3
  DEFAULT_LEVEL_HEIGHT,
4
4
  emitter,
5
+ getArtifactStore,
5
6
  getLevelDisplayName,
6
7
  LevelNode,
7
8
  type AnyNodeId,
@@ -64,7 +65,9 @@ import {
64
65
  import { Input } from './../../../../../components/ui/primitives/input'
65
66
  import { Switch } from './../../../../../components/ui/primitives/switch'
66
67
  import { cn } from './../../../../../lib/utils'
68
+ import { refusedObjectsNotice } from './../../../../../components/editor/group-actions'
67
69
  import { deleteLevelWithFallbackSelection } from './../../../../../lib/level-selection'
70
+ import { bringBuildArtifacts } from './../../../../../lib/scene-clipboard'
68
71
  import useEditor, { selectDefaultBuildingAndLevel } from './../../../../../store/use-editor'
69
72
  import useFloorplanMode from './../../../../../store/use-floorplan-mode'
70
73
  import { type SendToAppStep, useSendToApp } from './../../../../../store/use-send-to-app'
@@ -101,6 +104,7 @@ const MODEL_EXPORT_FORMATS = [
101
104
  { format: 'usdz', label: 'USDZ' },
102
105
  { format: 'stl', label: 'STL' },
103
106
  { format: 'obj', label: 'OBJ' },
107
+ { format: 'ifc', label: 'IFC' },
104
108
  ] as const
105
109
 
106
110
  type ModelExportFormat = (typeof MODEL_EXPORT_FORMATS)[number]['format']
@@ -540,7 +544,16 @@ export function SettingsPanel({
540
544
  // Materials ride along: nodes reference them by `scene:<id>` slot
541
545
  // refs, so a save without the table produces a file whose custom
542
546
  // finishes revert to defaults on the very Load Build path below.
543
- const sceneData = { nodes, rootNodeIds, installedPlugins, materials, collections }
547
+ // The project rides along too: a scripted object's artifacts live there, and
548
+ // loading the file into another project copies them over as a paste does.
549
+ const sceneData = {
550
+ nodes,
551
+ rootNodeIds,
552
+ installedPlugins,
553
+ materials,
554
+ collections,
555
+ projectId: useViewer.getState().projectId,
556
+ }
544
557
  const json = JSON.stringify(sceneData, null, 2)
545
558
  const blob = new Blob([json], { type: 'application/json' })
546
559
  const url = URL.createObjectURL(blob)
@@ -596,7 +609,20 @@ export function SettingsPanel({
596
609
  e.target.value = ''
597
610
  }
598
611
 
599
- const handleConfirmImport = (parsed: ParsedBuildJson) => {
612
+ const handleConfirmImport = async (file: ParsedBuildJson) => {
613
+ const targetProjectId = useViewer.getState().projectId
614
+ const artifactStore = getArtifactStore()
615
+ const { build: parsed, refusedIds, refusal } = await bringBuildArtifacts(file)
616
+ if (
617
+ targetProjectId !== useViewer.getState().projectId ||
618
+ artifactStore !== getArtifactStore()
619
+ )
620
+ return
621
+ if (refusal) {
622
+ useFloorplanMode
623
+ .getState()
624
+ .showNotice(refusedObjectsNotice(refusedIds.length, refusal, 'loaded'))
625
+ }
600
626
  const currentScene = useScene.getState()
601
627
  setScene(
602
628
  parsed.nodes as Parameters<typeof setScene>[0],
@@ -626,7 +652,7 @@ export function SettingsPanel({
626
652
  // old scene (or land on the empty intermediate `unloadScene` state).
627
653
  clearSceneHistory()
628
654
  resetSelection()
629
- setPhase('structure')
655
+ setPhase('building')
630
656
  selectDefaultBuildingAndLevel()
631
657
  }
632
658
 
@@ -688,6 +714,7 @@ export function SettingsPanel({
688
714
  setModelExportWarning(null)
689
715
  try {
690
716
  const artifact = await modelExport(format, {
717
+ projectName,
691
718
  onlyVisible: exportOnlyVisible,
692
719
  excludedNodeTypes,
693
720
  includedPresentationIds:
@@ -903,7 +930,7 @@ export function SettingsPanel({
903
930
  <legend className="px-1 font-medium text-sm">Include in file</legend>
904
931
  <p className="text-muted-foreground text-xs">
905
932
  Choose which procedural content is baked into model files. GLB and USDZ use the
906
- textured portable path; STL and OBJ remain geometry-only.
933
+ textured portable path; STL, OBJ and IFC remain geometry-only.
907
934
  </p>
908
935
  {exportableNodeTypes.length > 0 ? (
909
936
  <div className="space-y-2 pt-1">
@@ -28,11 +28,11 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [
28
28
  {
29
29
  title: 'Editor Navigation',
30
30
  shortcuts: [
31
- { keys: ['1'], action: 'Switch to Site phase' },
32
- { keys: ['2'], action: 'Switch to Structure phase' },
33
- { keys: ['3'], action: 'Switch to Furnish phase' },
34
- { keys: ['F'], action: 'Switch to Furnish layer' },
35
- { keys: ['Z'], action: 'Switch to Zones layer' },
31
+ { keys: ['1'], action: 'Switch to Site' },
32
+ { keys: ['2'], action: 'Switch to Building' },
33
+ { keys: ['\\'], action: 'Open or close the split view' },
34
+ { keys: ['F'], action: 'Open the items catalog with the item tool' },
35
+ { keys: ['Z'], action: 'Draw a zone' },
36
36
  {
37
37
  keys: ['Cmd/Ctrl', 'Arrow Up'],
38
38
  action: 'Select next level in the active building',
@@ -3,8 +3,8 @@ import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
4
  import { memo, useCallback, useEffect, useState } from 'react'
5
5
  import { useShallow } from 'zustand/react/shallow'
6
+ import { startCeilingEditFromTreeSelection } from './../../../../../lib/ceiling-edit-session'
6
7
  import { formatAreaLabel } from './../../../../../lib/measurements'
7
- import useEditor from './../../../../../store/use-editor'
8
8
  import { InlineRenameInput } from './inline-rename-input'
9
9
  import { focusTreeNode, handleTreeSelection, TreeNode, TreeNodeWrapper } from './tree-node'
10
10
  import { TreeNodeActions } from './tree-node-actions'
@@ -63,9 +63,9 @@ export const CeilingTreeNode = memo(function CeilingTreeNode({
63
63
  useViewer.getState().selection.selectedIds,
64
64
  setSelection,
65
65
  )
66
- if (!handled && useEditor.getState().phase === 'furnish') {
67
- useEditor.getState().setPhase('structure')
68
- }
66
+ if (handled) return
67
+ // Selecting a ceiling in the scene graph opens its Edit ceiling session.
68
+ startCeilingEditFromTreeSelection(nodeId)
69
69
  },
70
70
  [nodeId, setSelection],
71
71
  )
@@ -3,7 +3,6 @@ import { useViewer } from '@pascal-app/viewer'
3
3
  import Image from 'next/image'
4
4
  import { memo, useCallback, useState } from 'react'
5
5
  import { SnapTargetIcon } from '../../../snap-target-badge'
6
- import useEditor from './../../../../../store/use-editor'
7
6
  import { InlineRenameInput } from './inline-rename-input'
8
7
  import { focusTreeNode, handleTreeSelection, TreeNodeWrapper } from './tree-node'
9
8
  import { TreeNodeActions } from './tree-node-actions'
@@ -30,15 +29,12 @@ export const ChimneyTreeNode = memo(function ChimneyTreeNode({
30
29
  const handleClick = useCallback(
31
30
  (e: React.MouseEvent) => {
32
31
  e.stopPropagation()
33
- const handled = handleTreeSelection(
32
+ handleTreeSelection(
34
33
  e,
35
34
  nodeId,
36
35
  useViewer.getState().selection.selectedIds,
37
36
  setSelection,
38
37
  )
39
- if (!handled && useEditor.getState().phase === 'furnish') {
40
- useEditor.getState().setPhase('structure')
41
- }
42
38
  },
43
39
  [nodeId, setSelection],
44
40
  )