@pascal-app/editor 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,278 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ area,
5
+ containsPoint,
6
+ createMezzanine,
7
+ generateId,
8
+ getStoredLevelHeight,
9
+ type LevelNode,
10
+ type MultiPolygon,
11
+ type StructurePlan,
12
+ useScene,
13
+ } from '@pascal-app/core'
14
+ import { useViewer } from '@pascal-app/viewer'
15
+ import { create } from 'zustand'
16
+ import { getRoomSelectionIndex } from '../hooks/use-selected-room'
17
+ import useEditor from '../store/use-editor'
18
+ import {
19
+ CROSSING_MESSAGE,
20
+ clipFloorRegion,
21
+ floorRegionAxisAngle,
22
+ floorRegionSelfIntersects,
23
+ } from './floor-region-geometry'
24
+ import {
25
+ cancelFloorRegion,
26
+ type FloorDraftCommit,
27
+ type FloorDraftPurpose,
28
+ type FloorRegionRoom,
29
+ useFloorRegionDraft,
30
+ } from './floor-region-session'
31
+ import { type FloorRegionPoint, floorRegionSnapTargets } from './floor-region-snap'
32
+ import { beginGesture, type GestureHandle } from './gesture-lifecycle'
33
+ import {
34
+ MEZZANINE_FAILED_MESSAGE,
35
+ MEZZANINE_OUTSIDE_MESSAGE,
36
+ MEZZANINE_TOO_SMALL_MESSAGE,
37
+ mezzanineConflictMessage,
38
+ } from './mezzanine-messages'
39
+ import { applyRoomPlan } from './room-structure-commands'
40
+ import { sfxEmitter } from './sfx-bus'
41
+
42
+ // "Add mezzanine": draws a mezzanine outline inside one room with the floor
43
+ // region draft (rectangle or polygon), snapped to the room's walls, and builds
44
+ // it with core's `createMezzanine` at the default half-storey elevation (one
45
+ // undo step). The tool is one lifecycle gesture from `startMezzanineDraft`
46
+ // until the mezzanine is built or the tool is cancelled (Escape, a mode,
47
+ // level or selection change, the room gone); refused outlines stay red with a
48
+ // short label and the tool stays armed for another try.
49
+
50
+ export const MEZZANINE_DRAFT_HANDLE = 'mezzanine-draft'
51
+
52
+ export type MezzanineShape = 'rectangle' | 'polygon'
53
+
54
+ export const useMezzanineDraft = create<{
55
+ /** The room the mezzanine is drawn in while the tool is armed. */
56
+ host: FloorRegionRoom | null
57
+ shape: MezzanineShape
58
+ }>(() => ({ host: null, shape: 'rectangle' }))
59
+
60
+ /** The mezzanine's default elevation: half the storey, as `createMezzanine` rounds it. */
61
+ export function defaultMezzanineElevation(level: LevelNode) {
62
+ return Math.round(getStoredLevelHeight(level) * 10) / 20
63
+ }
64
+
65
+ const ON_BOUNDARY = 1e-4
66
+
67
+ function distanceToSegment(p: FloorRegionPoint, a: FloorRegionPoint, b: FloorRegionPoint) {
68
+ const dx = b[0] - a[0]
69
+ const dz = b[1] - a[1]
70
+ const lengthSq = dx * dx + dz * dz
71
+ const t = lengthSq
72
+ ? Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dz) / lengthSq))
73
+ : 0
74
+ return Math.hypot(a[0] + t * dx - p[0], a[1] + t * dz - p[1])
75
+ }
76
+
77
+ /** Inside the polygon, or on its boundary (a point snapped onto a wall line). */
78
+ function insideOrOn(polygon: MultiPolygon, point: FloorRegionPoint) {
79
+ if (containsPoint(polygon, point)) return true
80
+ return polygon.some(({ outer, holes }) =>
81
+ [outer, ...holes].some((ring) =>
82
+ ring.some((a, i) => distanceToSegment(point, a, ring[(i + 1) % ring.length]!) <= ON_BOUNDARY),
83
+ ),
84
+ )
85
+ }
86
+
87
+ function withoutRepeats(ring: FloorRegionPoint[]) {
88
+ return ring.filter((point, i) => {
89
+ const next = ring[(i + 1) % ring.length]!
90
+ return Math.hypot(next[0] - point[0], next[1] - point[1]) > ON_BOUNDARY
91
+ })
92
+ }
93
+
94
+ function refuse(message: string): FloorDraftCommit {
95
+ return { ok: false, message }
96
+ }
97
+
98
+ /**
99
+ * Why a mezzanine outline cannot be built, or its plan. The outline is clipped
100
+ * to the host's clear floor (inside the wall faces) first; what the clip
101
+ * leaves must be one piece of at least 1 m².
102
+ */
103
+ export function planMezzanine(
104
+ nodes: Record<string, AnyNode>,
105
+ host: FloorRegionRoom,
106
+ polygon: readonly FloorRegionPoint[],
107
+ ): { plan: StructurePlan & { zoneId: string } } | { message: string } {
108
+ const clip = clipFloorRegion(polygon, host.clear)
109
+ if (!clip)
110
+ return {
111
+ message:
112
+ area([{ outer: polygon.map(([x, z]) => [x, z]), holes: [] }]) < 1
113
+ ? MEZZANINE_TOO_SMALL_MESSAGE
114
+ : MEZZANINE_OUTSIDE_MESSAGE,
115
+ }
116
+ if (clip.pieces.length !== 1) return { message: MEZZANINE_OUTSIDE_MESSAGE }
117
+ if (clip.area < 1) return { message: MEZZANINE_TOO_SMALL_MESSAGE }
118
+ const outline = withoutRepeats(clip.polygon)
119
+ let plan: StructurePlan & { zoneId: string }
120
+ try {
121
+ // The height the preview was drawn at, not one recomputed from a storey
122
+ // that may have changed while the tool was armed.
123
+ plan = createMezzanine(nodes, {
124
+ hostZoneId: host.zoneId,
125
+ polygon: outline,
126
+ elevation: host.elevation,
127
+ mintId: generateId,
128
+ })
129
+ } catch {
130
+ return {
131
+ message: floorRegionSelfIntersects(outline)
132
+ ? CROSSING_MESSAGE
133
+ : area([{ outer: outline, holes: [] }]) < 1
134
+ ? MEZZANINE_TOO_SMALL_MESSAGE
135
+ : MEZZANINE_FAILED_MESSAGE,
136
+ }
137
+ }
138
+ const conflict = plan.conflicts?.[0]
139
+ if (!conflict) return { plan }
140
+ return { message: mezzanineConflictMessage(conflict.code) ?? MEZZANINE_FAILED_MESSAGE }
141
+ }
142
+
143
+ // The tool's lifecycle handle, from start until built or cancelled.
144
+ let tool: GestureHandle | null = null
145
+
146
+ function reset() {
147
+ if (useMezzanineDraft.getState().host) useMezzanineDraft.setState({ host: null })
148
+ const { draft } = useFloorRegionDraft.getState()
149
+ if (draft?.room.purpose === MEZZANINE) useFloorRegionDraft.setState({ draft: null, hover: null })
150
+ }
151
+
152
+ function commitMezzanine(host: FloorRegionRoom, polygon: readonly FloorRegionPoint[]) {
153
+ const result = planMezzanine(useScene.getState().nodes, host, polygon)
154
+ if ('message' in result) return refuse(result.message)
155
+ const handle = tool
156
+ tool = null
157
+ try {
158
+ if (handle?.active) handle.finish(() => applyRoomPlan(result.plan))
159
+ else applyRoomPlan(result.plan)
160
+ } finally {
161
+ reset()
162
+ }
163
+ sfxEmitter.emit('sfx:structure-build')
164
+ useEditor.getState().selectRoom({ levelId: host.levelId, zoneId: result.plan.zoneId })
165
+ return { ok: true } as const
166
+ }
167
+
168
+ function hostGone(host: FloorRegionRoom) {
169
+ const scene = useScene.getState()
170
+ const zone = scene.nodes[host.zoneId as AnyNodeId]
171
+ return (
172
+ scene.readOnly ||
173
+ zone?.type !== 'zone' ||
174
+ zone.parentId !== host.levelId ||
175
+ useEditor.getState().room?.zoneId !== host.zoneId
176
+ )
177
+ }
178
+
179
+ function beginTool(host: FloorRegionRoom) {
180
+ const handle = beginGesture({
181
+ kind: 'mezzanine-draft',
182
+ scope: { kind: 'handle-drag', nodeId: host.zoneId, handle: MEZZANINE_DRAFT_HANDLE },
183
+ stale: () => hostGone(host),
184
+ onCancel: () => {
185
+ if (tool === handle) tool = null
186
+ reset()
187
+ },
188
+ })
189
+ tool = handle
190
+ useMezzanineDraft.setState({ host })
191
+ return handle
192
+ }
193
+
194
+ const MEZZANINE: FloorDraftPurpose = {
195
+ persistent: true,
196
+ own: (room) => (tool?.active ? tool : beginTool(room)),
197
+ commit: commitMezzanine,
198
+ pointMessage: (room, point) => (insideOrOn(room.clear, point) ? null : MEZZANINE_OUTSIDE_MESSAGE),
199
+ fill: '#818cf8',
200
+ }
201
+
202
+ /**
203
+ * The room a mezzanine is drawn in: its clear floor (inside the wall faces,
204
+ * which is what core accepts) bounds the outline, which snaps to those faces
205
+ * and the level's other mezzanines. Null when the room cannot host one.
206
+ */
207
+ export function describeMezzanineHost(
208
+ nodes: Record<string, AnyNode>,
209
+ hostZoneId: string,
210
+ ): FloorRegionRoom | null {
211
+ const zone = nodes[hostZoneId]
212
+ if (zone?.type !== 'zone' || zone.floor?.support === 'open' || !zone.parentId) return null
213
+ const level = nodes[zone.parentId]
214
+ if (level?.type !== 'level') return null
215
+ const record = getRoomSelectionIndex(level.id)
216
+ .update(nodes)
217
+ .find((room) => room.zoneId === hostZoneId)
218
+ if (!record) return null
219
+ const clear: MultiPolygon = record.clearPolygon
220
+ const mezzanines = Object.values(nodes).flatMap((node) =>
221
+ node.type === 'zone' && node.floor?.support === 'open' && node.parentId === level.id
222
+ ? [node.polygon]
223
+ : [],
224
+ )
225
+ return {
226
+ zoneId: hostZoneId,
227
+ levelId: level.id,
228
+ clear,
229
+ elevation: defaultMezzanineElevation(level),
230
+ angle: floorRegionAxisAngle(clear),
231
+ targets: floorRegionSnapTargets(clear, mezzanines),
232
+ purpose: MEZZANINE,
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Arms "Add mezzanine" in `hostZoneId`: select mode, the room's level and the
238
+ * room selected first, then the tool's gesture, so setting them up cannot
239
+ * cancel it. Returns false (changing nothing) when the room cannot host one.
240
+ */
241
+ export function startMezzanineDraft(
242
+ hostZoneId: string,
243
+ shape: MezzanineShape = 'rectangle',
244
+ ): boolean {
245
+ const nodes = useScene.getState().nodes
246
+ if (useScene.getState().readOnly) return false
247
+ const host = describeMezzanineHost(nodes, hostZoneId)
248
+ if (!host) return false
249
+ cancelMezzanineDraft()
250
+ cancelFloorRegion()
251
+ useMezzanineDraft.setState({ shape })
252
+ if (useEditor.getState().mode !== 'select') useEditor.getState().setMode('select')
253
+ if (useViewer.getState().selection.levelId !== host.levelId)
254
+ useViewer.getState().setSelection({ levelId: host.levelId as LevelNode['id'] })
255
+ if (useEditor.getState().room?.zoneId !== hostZoneId)
256
+ useEditor.getState().selectRoom({ levelId: host.levelId, zoneId: hostZoneId })
257
+ beginTool(host)
258
+ return true
259
+ }
260
+
261
+ export function cancelMezzanineDraft() {
262
+ const current = tool
263
+ tool = null
264
+ if (current?.active) current.cancel()
265
+ else reset()
266
+ }
267
+
268
+ export function mezzanineDraftActive() {
269
+ return !!useMezzanineDraft.getState().host
270
+ }
271
+
272
+ /** Switches rectangle ↔ polygon; the outline in hand is dropped, the tool stays. */
273
+ export function setMezzanineShape(shape: MezzanineShape) {
274
+ if (useMezzanineDraft.getState().shape === shape) return
275
+ const { draft } = useFloorRegionDraft.getState()
276
+ if (draft?.room.purpose === MEZZANINE) useFloorRegionDraft.setState({ draft: null })
277
+ useMezzanineDraft.setState({ shape })
278
+ }
@@ -0,0 +1,26 @@
1
+ import { CROSSING_MESSAGE } from './floor-region-geometry'
2
+
3
+ // Short labels for what core refuses a mezzanine — drawing, moving, turning,
4
+ // copying, pushing an edge or adding its stair all read the same words.
5
+
6
+ export const MEZZANINE_OUTSIDE_MESSAGE = 'Outside the room'
7
+ export const MEZZANINE_TOO_SMALL_MESSAGE = 'At least 1 m²'
8
+ export const MEZZANINE_OVERLAP_MESSAGE = 'Overlaps a mezzanine'
9
+ export const MEZZANINE_ELEVATION_MESSAGE = 'Too high or too low'
10
+ export const MEZZANINE_FAILED_MESSAGE = "Can't add a mezzanine here"
11
+ export const MEZZANINE_NO_STAIR_MESSAGE = 'No room for stairs'
12
+
13
+ const CONFLICT_MESSAGES: Record<string, string> = {
14
+ 'outside-host': MEZZANINE_OUTSIDE_MESSAGE,
15
+ 'overlaps-mezzanine': MEZZANINE_OVERLAP_MESSAGE,
16
+ 'mezzanine-overlap': MEZZANINE_OVERLAP_MESSAGE,
17
+ 'too-small': MEZZANINE_TOO_SMALL_MESSAGE,
18
+ 'self-intersecting': CROSSING_MESSAGE,
19
+ 'mezzanine-elevation': MEZZANINE_ELEVATION_MESSAGE,
20
+ 'no-room-for-stair': MEZZANINE_NO_STAIR_MESSAGE,
21
+ }
22
+
23
+ /** The label for a core mezzanine conflict code, or null when it is not one of them. */
24
+ export function mezzanineConflictMessage(code: string | undefined): string | null {
25
+ return (code && CONFLICT_MESSAGES[code]) || null
26
+ }
@@ -0,0 +1,207 @@
1
+ import { afterAll, afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeId,
5
+ BuildingNode,
6
+ clearSceneHistory,
7
+ createMezzanine,
8
+ createZone,
9
+ deleteZone,
10
+ generateId,
11
+ initSpaceDetectionSync,
12
+ LevelNode,
13
+ type SlabNode,
14
+ useScene,
15
+ type ZoneNode,
16
+ } from '@pascal-app/core'
17
+ import { useViewer } from '@pascal-app/viewer'
18
+ import { deleteConfirmationContent } from '../components/editor/delete-confirmation-dialog'
19
+ import { resolveFloorRegionRoom } from '../components/editor/floor-region-controls'
20
+ import { MEZZANINE_GESTURE_HINTS, mezzanineGesture } from '../components/ui/helpers/helper-manager'
21
+ import {
22
+ MEZZANINE_EDGE_DRAG_LABEL,
23
+ mezzanineElevationBounds,
24
+ ROOM_ELEVATION_DRAG_LABEL,
25
+ } from './room-handle-drag'
26
+ import { RoomSelectionIndex, resolveRoomHit } from './room-selection'
27
+ import { applyRoomPlan } from './room-structure-commands'
28
+ import { ROOM_MOVE_DRAG_LABEL } from './room-transform-session'
29
+ import { zoneAtLevelPoint } from './units'
30
+
31
+ const LEVEL = 'level_mezzanine_selection'
32
+ let hostId = '' as ZoneNode['id']
33
+ let mezzanineId = '' as ZoneNode['id']
34
+ let stop = () => {}
35
+ const savedRaf = globalThis.requestAnimationFrame
36
+ const savedCaf = globalThis.cancelAnimationFrame
37
+
38
+ beforeEach(() => {
39
+ globalThis.requestAnimationFrame ??= ((callback: (time: number) => void) => {
40
+ callback(0)
41
+ return 0
42
+ }) as typeof requestAnimationFrame
43
+ globalThis.cancelAnimationFrame ??= (() => {}) as typeof cancelAnimationFrame
44
+ const building = BuildingNode.parse({ id: 'building_mezzanine_selection', children: [LEVEL] })
45
+ const level = LevelNode.parse({ id: LEVEL, parentId: building.id, height: 5 })
46
+ useScene.setState({
47
+ nodes: { [building.id]: building, [level.id]: level },
48
+ rootNodeIds: [building.id],
49
+ dirtyNodes: new Set(),
50
+ materials: {},
51
+ collections: {},
52
+ readOnly: false,
53
+ })
54
+ useScene.temporal.getState().resume()
55
+ stop = initSpaceDetectionSync(useScene, { getState: () => ({ spaces: {}, setSpaces: () => {} }) })
56
+ const host = createZone(useScene.getState().nodes, {
57
+ levelId: LEVEL,
58
+ polygon: [
59
+ [0, 0],
60
+ [8, 0],
61
+ [8, 6],
62
+ [0, 6],
63
+ ],
64
+ enclose: true,
65
+ mintId: generateId,
66
+ })
67
+ applyRoomPlan(host)
68
+ hostId = host.zoneId as ZoneNode['id']
69
+ const mezzanine = createMezzanine(useScene.getState().nodes, {
70
+ hostZoneId: hostId,
71
+ polygon: [
72
+ [0.5, 0.5],
73
+ [4, 0.5],
74
+ [4, 3],
75
+ [0.5, 3],
76
+ ],
77
+ mintId: generateId,
78
+ })
79
+ expect(mezzanine.conflicts ?? []).toEqual([])
80
+ applyRoomPlan(mezzanine)
81
+ mezzanineId = mezzanine.zoneId as ZoneNode['id']
82
+ useViewer.getState().setSelection({ buildingId: building.id, levelId: LEVEL, selectedIds: [] })
83
+ clearSceneHistory()
84
+ })
85
+
86
+ afterEach(() => stop())
87
+
88
+ afterAll(() => {
89
+ globalThis.requestAnimationFrame = savedRaf
90
+ globalThis.cancelAnimationFrame = savedCaf
91
+ })
92
+
93
+ const nodes = () => useScene.getState().nodes as Record<string, AnyNode>
94
+
95
+ describe('mezzanine selection', () => {
96
+ test('the room index lists the mezzanine as its own room beside the untouched host', () => {
97
+ const index = new RoomSelectionIndex(LEVEL)
98
+ const records = index.update(nodes())
99
+ const host = records.find((room) => room.zoneId === hostId)!
100
+ const mezzanine = records.find((room) => room.zoneId === mezzanineId)!
101
+ expect(host.mezzanine).toBeNull()
102
+ expect(host.area).toBeCloseTo(48, 0)
103
+ expect(mezzanine.mezzanine).toEqual({ hostZoneId: hostId })
104
+ expect(mezzanine.name).toBe('Mezzanine')
105
+ expect(mezzanine.area).toBeCloseTo(8.75)
106
+ expect(mezzanine.spans).toEqual([])
107
+ expect(mezzanine.boundaryWallIds).toEqual([])
108
+ const plate = nodes()[mezzanine.slabId as AnyNodeId] as SlabNode
109
+ expect(plate.support).toBe('open')
110
+ expect(host.slabId).not.toBe(mezzanine.slabId)
111
+ })
112
+
113
+ test('the mezzanine plate and railing pick the mezzanine; the host floor under it picks the host', () => {
114
+ const index = new RoomSelectionIndex(LEVEL)
115
+ const records = index.update(nodes())
116
+ const mezzanine = records.find((room) => room.zoneId === mezzanineId)!
117
+ const host = records.find((room) => room.zoneId === hostId)!
118
+ const plate = nodes()[mezzanine.slabId as AnyNodeId]!
119
+ const hostFloor = nodes()[host.slabId as AnyNodeId]!
120
+ // Anywhere on the mezzanine plate — its top, underside or a railing post.
121
+ expect(resolveRoomHit(index, LEVEL, plate, [2, 1.5], '3d')?.key.zoneId).toBe(mezzanineId)
122
+ expect(resolveRoomHit(index, LEVEL, plate, [3.99, 2.99], '3d')?.key.zoneId).toBe(mezzanineId)
123
+ // The host floor under the mezzanine, and a point-only (plan) hit there.
124
+ expect(resolveRoomHit(index, LEVEL, hostFloor, [2, 1.5], '3d')?.key.zoneId).toBe(hostId)
125
+ expect(resolveRoomHit(index, LEVEL, null, [2, 1.5])?.key.zoneId).toBe(hostId)
126
+ expect(resolveRoomHit(index, LEVEL, hostFloor, [6, 4], '3d')?.key.zoneId).toBe(hostId)
127
+ })
128
+
129
+ test('units pick the zone by the hit height: on the mezzanine or under it', () => {
130
+ const elevation = (nodes()[mezzanineId as AnyNodeId] as ZoneNode).floor!.elevation!
131
+ expect(elevation).toBeCloseTo(2.5)
132
+ expect(zoneAtLevelPoint(2, 1.5, elevation)?.id).toBe(mezzanineId)
133
+ expect(zoneAtLevelPoint(2, 1.5, elevation - 0.2)?.id).toBe(mezzanineId)
134
+ expect(zoneAtLevelPoint(2, 1.5, 0.05)?.id).toBe(hostId)
135
+ expect(zoneAtLevelPoint(6, 4, elevation)?.id).toBe(hostId)
136
+ // No height (the plan, from above): the mezzanine on top.
137
+ expect(zoneAtLevelPoint(2, 1.5)?.id).toBe(mezzanineId)
138
+ })
139
+
140
+ test('the elevation handle stays inside the heights core accepts', () => {
141
+ expect(mezzanineElevationBounds(nodes(), hostId)).toBeNull()
142
+ const bounds = mezzanineElevationBounds(nodes(), mezzanineId)!
143
+ expect(bounds.min).toBeCloseTo(0.21)
144
+ expect(bounds.max).toBeCloseTo(4.7)
145
+ })
146
+
147
+ test('a mezzanine covering its whole host still wins on its plate (units)', () => {
148
+ // Stretch the mezzanine over the host's whole footprint: same area, host listed first.
149
+ const hostPolygon = (nodes()[hostId as AnyNodeId] as ZoneNode).polygon
150
+ useScene.getState().updateNode(mezzanineId as AnyNodeId, { polygon: hostPolygon } as never)
151
+ const elevation = (nodes()[mezzanineId as AnyNodeId] as ZoneNode).floor!.elevation!
152
+ expect(zoneAtLevelPoint(2, 1.5, elevation)?.id).toBe(mezzanineId)
153
+ expect(zoneAtLevelPoint(6, 4, elevation)?.id).toBe(mezzanineId)
154
+ expect(zoneAtLevelPoint(6, 4)?.id).toBe(mezzanineId)
155
+ expect(zoneAtLevelPoint(2, 1.5, 0.05)?.id).toBe(hostId)
156
+ })
157
+
158
+ test('plan-view floor drafts pick the topmost floor: the mezzanine over its host', () => {
159
+ expect(resolveFloorRegionRoom(LEVEL, [2, 1.5])?.zoneId).toBe(mezzanineId)
160
+ expect(resolveFloorRegionRoom(LEVEL, [6, 4])?.zoneId).toBe(hostId)
161
+ })
162
+
163
+ test('raising, resizing and moving a mezzanine are titled Mezzanine; a room keeps its HUDs', () => {
164
+ const gesture = (nodeId: string, label: string) => mezzanineGesture({ nodeId, label }, nodes())
165
+ expect(gesture(mezzanineId, ROOM_ELEVATION_DRAG_LABEL)).toBe('raise')
166
+ expect(gesture(mezzanineId, MEZZANINE_EDGE_DRAG_LABEL)).toBe('resize')
167
+ expect(gesture(mezzanineId, ROOM_MOVE_DRAG_LABEL)).toBe('move')
168
+ expect(gesture(hostId, ROOM_ELEVATION_DRAG_LABEL)).toBeNull()
169
+ expect(gesture(hostId, ROOM_MOVE_DRAG_LABEL)).toBeNull()
170
+ expect(gesture(mezzanineId, 'height')).toBeNull()
171
+ expect(mezzanineGesture(null, nodes())).toBeNull()
172
+ expect(MEZZANINE_GESTURE_HINTS.move[0]!.label).toBe('Place inside the room')
173
+ })
174
+
175
+ test('the delete dialog speaks of the mezzanine, and of its items only when it has some', () => {
176
+ const request = (contents: 'delete' | 'keep' = 'delete') => ({
177
+ count: 1,
178
+ room: deleteZone(nodes(), { zoneId: mezzanineId, contents }).payload,
179
+ onConfirm: () => {},
180
+ onKeepContents: () => {},
181
+ })
182
+ const empty = deleteConfirmationContent(request(), nodes())
183
+ expect(empty.title).toBe('Delete Mezzanine?')
184
+ expect(empty.description).toMatch(/^Removes the mezzanine floor/)
185
+ expect(empty.description).not.toContain('item')
186
+ expect(empty.keepLabel).toBeNull()
187
+ const hasCeiling = Object.values(nodes()).some(
188
+ (node) => node.type === 'ceiling' && node.zoneId === mezzanineId,
189
+ )
190
+ expect(empty.description.includes('ceiling')).toBe(hasCeiling)
191
+ const withItems = request()
192
+ withItems.room = { ...withItems.room, itemIds: ['item_a', 'item_b'] }
193
+ const loaded = deleteConfirmationContent(withItems, nodes())
194
+ expect(loaded.description).toContain('2 items are on it. Keep them on the floor below')
195
+ expect(loaded.keepLabel).toBe('Keep items')
196
+ // A room's dialog is unchanged.
197
+ const room = deleteConfirmationContent(
198
+ {
199
+ count: 1,
200
+ room: deleteZone(nodes(), { zoneId: hostId, contents: 'delete' }).payload,
201
+ onConfirm: () => {},
202
+ },
203
+ nodes(),
204
+ )
205
+ expect(room.description).not.toContain('mezzanine')
206
+ })
207
+ })
@@ -0,0 +1,46 @@
1
+ import {
2
+ type AnyNodeId,
3
+ planMezzanineStair,
4
+ runAsSingleSceneHistoryStep,
5
+ useScene,
6
+ } from '@pascal-app/core'
7
+ import { useViewer } from '@pascal-app/viewer'
8
+ import useEditor from '../store/use-editor'
9
+ import { MEZZANINE_NO_STAIR_MESSAGE, mezzanineConflictMessage } from './mezzanine-messages'
10
+ import { applyRoomPlan } from './room-structure-commands'
11
+ import { sfxEmitter } from './sfx-bus'
12
+
13
+ export type MezzanineStairResult = { ok: true; stairId: string } | { ok: false; message: string }
14
+
15
+ /**
16
+ * "Add stairs": core places a straight flight from the host floor up to one of
17
+ * the mezzanine's open edges (its railing opens there), as one undo step. The
18
+ * new stair is selected, like a new mezzanine is, so it can be adjusted at once.
19
+ */
20
+ export function addMezzanineStairs(zoneId: string): MezzanineStairResult {
21
+ const nodes = useScene.getState().nodes
22
+ if (useScene.getState().readOnly || !nodes[zoneId as AnyNodeId])
23
+ return { ok: false, message: MEZZANINE_NO_STAIR_MESSAGE }
24
+ let plan: ReturnType<typeof planMezzanineStair>
25
+ try {
26
+ plan = planMezzanineStair(nodes, zoneId)
27
+ } catch {
28
+ return { ok: false, message: MEZZANINE_NO_STAIR_MESSAGE }
29
+ }
30
+ const conflict = plan.conflicts?.[0]
31
+ if (conflict || !plan.stairId)
32
+ return {
33
+ ok: false,
34
+ message: mezzanineConflictMessage(conflict?.code) ?? MEZZANINE_NO_STAIR_MESSAGE,
35
+ }
36
+ // The stair and its flight land as one step.
37
+ let applied = false
38
+ runAsSingleSceneHistoryStep(useScene, () => {
39
+ applied = applyRoomPlan(plan)
40
+ })
41
+ if (!applied) return { ok: false, message: MEZZANINE_NO_STAIR_MESSAGE }
42
+ sfxEmitter.emit('sfx:structure-build')
43
+ useEditor.getState().clearRoom()
44
+ useViewer.getState().setSelection({ selectedIds: [plan.stairId as AnyNodeId] })
45
+ return { ok: true, stairId: plan.stairId! }
46
+ }
@@ -1,12 +1,14 @@
1
1
  import type { GlbExportOptions } from './glb-export'
2
2
 
3
- export type ModelExportFormat = 'glb' | 'usdz' | 'stl' | 'obj' | 'print-stl' | 'print-3mf'
3
+ export type ModelExportFormat = 'glb' | 'usdz' | 'stl' | 'obj' | 'ifc' | 'print-stl' | 'print-3mf'
4
4
 
5
5
  export type ModelExportOptions = Pick<
6
6
  GlbExportOptions,
7
7
  'onlyVisible' | 'excludedNodeTypes' | 'includedPresentationIds'
8
8
  > & {
9
9
  download?: boolean
10
+ /** Names the IFC project and its file. */
11
+ projectName?: string
10
12
  printScale?: number
11
13
  printScope?: 'whole' | 'levels'
12
14
  printContent?: 'structure' | 'everything'
@@ -1,4 +1,13 @@
1
- import { type AnyNode, createSceneApi, isMovable, nodeRegistry, useScene } from '@pascal-app/core'
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ cloneNodesInto,
5
+ collectSubtree,
6
+ isMovable,
7
+ nodeRegistry,
8
+ useScene,
9
+ } from '@pascal-app/core'
10
+ import { copyCreateOps } from './fresh-planar-placement'
2
11
 
3
12
  export function registryMoveDisabled(node: AnyNode): boolean {
4
13
  const def = nodeRegistry.get(node.type)
@@ -11,11 +20,13 @@ export function registryMoveDisabled(node: AnyNode): boolean {
11
20
  }
12
21
 
13
22
  export function duplicateWithoutMove(node: AnyNode) {
14
- const api = createSceneApi(useScene)
15
- const subtree = api.getSubtree(node.id)
16
- if (!subtree || !node.parentId) return null
17
- return api.cloneNodesInto([subtree.root, ...subtree.descendants], {
23
+ const subtree = collectSubtree(useScene.getState().nodes, node.id)
24
+ const parentId = node.parentId as AnyNodeId | null
25
+ if (!subtree || !parentId) return null
26
+ const cloned = cloneNodesInto([subtree.root, ...subtree.descendants], {
18
27
  rootId: node.id,
19
- parentId: node.parentId as AnyNode['id'],
28
+ parentId,
20
29
  })
30
+ useScene.getState().createNodes(copyCreateOps(cloned, parentId))
31
+ return cloned.rootId
21
32
  }
@@ -0,0 +1,37 @@
1
+ import type { AnyNode } from '@pascal-app/core'
2
+
3
+ /**
4
+ * A node's human label, the same in the baked artifact (`extras.label`) and the
5
+ * viewer's breadcrumb: an explicit name wins, items fall back to their catalog
6
+ * asset name, other kinds to a type word.
7
+ */
8
+ export function nodeDisplayLabel(node: AnyNode): string {
9
+ if (node.name) return node.name
10
+ switch (node.type) {
11
+ case 'item':
12
+ return (node as { asset?: { name?: string } }).asset?.name || 'Item'
13
+ case 'wall':
14
+ return 'Wall'
15
+ case 'door':
16
+ return 'Door'
17
+ case 'window':
18
+ return 'Window'
19
+ case 'cabinet':
20
+ case 'cabinet-module':
21
+ return 'Cabinet'
22
+ case 'slab':
23
+ return 'Slab'
24
+ case 'ceiling':
25
+ return 'Ceiling'
26
+ case 'roof':
27
+ return 'Roof'
28
+ case 'fence':
29
+ return 'Fence'
30
+ case 'column':
31
+ return 'Column'
32
+ case 'stair':
33
+ return 'Stairs'
34
+ default:
35
+ return node.type
36
+ }
37
+ }