@pascal-app/editor 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (624) hide show
  1. package/README.md +106 -0
  2. package/package.json +42 -38
  3. package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
  4. package/src/components/editor/action-menu-button.tsx +83 -0
  5. package/src/components/editor/bake-thumbnail.tsx +307 -5
  6. package/src/components/editor/capture-level-picker.tsx +72 -0
  7. package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
  8. package/src/components/editor/construction-reveal-audio.tsx +40 -0
  9. package/src/components/editor/custom-camera-controls.tsx +275 -57
  10. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  11. package/src/components/editor/duplicate-node.ts +244 -0
  12. package/src/components/editor/editor-layout-mobile.tsx +37 -26
  13. package/src/components/editor/editor-layout-v2.tsx +132 -59
  14. package/src/components/editor/export-manager.tsx +47 -26
  15. package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
  16. package/src/components/editor/first-person/build-collider-world.ts +28 -7
  17. package/src/components/editor/first-person/ground-holes.test.ts +128 -0
  18. package/src/components/editor/first-person/ground-holes.ts +130 -0
  19. package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
  20. package/src/components/editor/first-person/mechanism-targets.ts +88 -0
  21. package/src/components/editor/first-person-controls.tsx +220 -9
  22. package/src/components/editor/floating-action-menu.tsx +63 -207
  23. package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
  24. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  25. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  26. package/src/components/editor/floor-region-controls.tsx +846 -0
  27. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  28. package/src/components/editor/floor-surface.test.ts +165 -0
  29. package/src/components/editor/floor-surface.ts +182 -0
  30. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  31. package/src/components/editor/floorplan-background-selection.ts +1 -3
  32. package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
  33. package/src/components/editor/floorplan-panel.tsx +414 -363
  34. package/src/components/editor/follow-pascal.test.tsx +912 -0
  35. package/src/components/editor/follow-pascal.tsx +354 -0
  36. package/src/components/editor/footprint-height-handle.tsx +204 -0
  37. package/src/components/editor/group-actions.ts +56 -2
  38. package/src/components/editor/group-move-3d.ts +2 -0
  39. package/src/components/editor/group-rotate-handle.tsx +2 -0
  40. package/src/components/editor/group-transform-shared.test.ts +460 -0
  41. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  42. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  43. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +269 -197
  45. package/src/components/editor/index.tsx +409 -197
  46. package/src/components/editor/join-walls-pill.tsx +44 -0
  47. package/src/components/editor/node-action-menu.tsx +68 -70
  48. package/src/components/editor/node-arrow-handles.tsx +25 -6
  49. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  50. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  52. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  53. package/src/components/editor/registry-action-contributions.tsx +80 -1
  54. package/src/components/editor/riser-diagram-panel.tsx +14 -7
  55. package/src/components/editor/room-controls.tsx +847 -0
  56. package/src/components/editor/room-floor-highlight.tsx +128 -0
  57. package/src/components/editor/room-handles-2d.tsx +354 -0
  58. package/src/components/editor/room-handles.tsx +675 -0
  59. package/src/components/editor/room-highlight.tsx +233 -0
  60. package/src/components/editor/room-labels.test.ts +850 -0
  61. package/src/components/editor/room-labels.tsx +1890 -0
  62. package/src/components/editor/room-transform-ghost.tsx +256 -0
  63. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  64. package/src/components/editor/selection-manager.tsx +474 -419
  65. package/src/components/editor/slab-hole-highlights.tsx +5 -2
  66. package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
  67. package/src/components/editor/snapshot-capture.test.ts +119 -60
  68. package/src/components/editor/snapshot-capture.ts +36 -5
  69. package/src/components/editor/thumbnail-generator.test.ts +654 -0
  70. package/src/components/editor/thumbnail-generator.tsx +1103 -223
  71. package/src/components/editor/use-floorplan-background-placement.ts +34 -12
  72. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  73. package/src/components/editor/views/host-view.tsx +61 -0
  74. package/src/components/editor/views/plugins-view.tsx +663 -0
  75. package/src/components/editor/views/scene-region.tsx +59 -0
  76. package/src/components/editor/views/use-editor-views.ts +57 -0
  77. package/src/components/editor/views/view-bar.test.tsx +54 -0
  78. package/src/components/editor/views/view-bar.tsx +343 -0
  79. package/src/components/editor/wall-justification.test.ts +140 -0
  80. package/src/components/editor/wall-move-side-handles.tsx +43 -142
  81. package/src/components/editor/wall-push-arrow.tsx +92 -0
  82. package/src/components/editor/wall-region-handles.tsx +509 -0
  83. package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
  84. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  85. package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
  86. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  87. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  88. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
  89. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  90. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
  91. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  92. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  93. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  94. package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
  95. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  96. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
  97. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
  98. package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
  99. package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
  100. package/src/components/editor-2d/svg-paths.ts +12 -15
  101. package/src/components/point-ask/bubble.tsx +538 -0
  102. package/src/components/point-ask/hover-chip.tsx +91 -0
  103. package/src/components/point-ask/mode-pill.tsx +92 -0
  104. package/src/components/point-ask/optional-motion.ts +22 -0
  105. package/src/components/point-ask/outline-corners.tsx +93 -0
  106. package/src/components/point-ask/pins.tsx +711 -0
  107. package/src/components/point-ask/point-ask-layer.tsx +83 -0
  108. package/src/components/point-ask/point-ask-styles.ts +105 -0
  109. package/src/components/point-ask/region-marquee-styles.ts +15 -0
  110. package/src/components/point-ask/region-marquee.tsx +64 -0
  111. package/src/components/procedural-items/inspector.tsx +40 -15
  112. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
  113. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  114. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  115. package/src/components/systems/roof/roof-edit-system.tsx +1 -0
  116. package/src/components/systems/zone/zone-system.test.ts +47 -0
  117. package/src/components/systems/zone/zone-system.tsx +29 -68
  118. package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
  119. package/src/components/tools/distribution/connection-compatibility.ts +43 -0
  120. package/src/components/tools/distribution/distribution-public.ts +9 -0
  121. package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
  122. package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
  123. package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
  124. package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
  125. package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
  126. package/src/components/tools/distribution/draw-alignment.ts +35 -0
  127. package/src/components/tools/distribution/ports.test.ts +56 -0
  128. package/src/components/tools/distribution/ports.ts +378 -0
  129. package/src/components/tools/distribution/run-cursor.test.ts +135 -0
  130. package/src/components/tools/distribution/run-cursor.ts +36 -0
  131. package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
  132. package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
  133. package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
  134. package/src/components/tools/distribution/run-port-snap.ts +25 -0
  135. package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
  136. package/src/components/tools/distribution/run-screen-direction.ts +30 -0
  137. package/src/components/tools/elevator/elevator-tool.tsx +3 -6
  138. package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
  139. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
  140. package/src/components/tools/item/placement-history.test.tsx +512 -0
  141. package/src/components/tools/item/placement-strategies.test.ts +46 -1
  142. package/src/components/tools/item/placement-strategies.ts +11 -10
  143. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
  144. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  145. package/src/components/tools/item/use-draft-node.ts +166 -95
  146. package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
  147. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  148. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  149. package/src/components/tools/registry/item-surface-move.test.ts +62 -63
  150. package/src/components/tools/registry/item-surface-move.ts +2 -0
  151. package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
  152. package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
  153. package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
  154. package/src/components/tools/select/box-select-tool.tsx +2 -12
  155. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  156. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  157. package/src/components/tools/select/select-candidates.test.ts +37 -4
  158. package/src/components/tools/select/select-candidates.ts +15 -11
  159. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  160. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  161. package/src/components/tools/tool-manager.tsx +59 -24
  162. package/src/components/tools/wall/wall-drafting.test.ts +175 -47
  163. package/src/components/tools/zone/zone-tool.tsx +4 -12
  164. package/src/components/ui/action-menu/action-button.tsx +9 -1
  165. package/src/components/ui/action-menu/camera-actions.tsx +9 -4
  166. package/src/components/ui/action-menu/control-modes.tsx +15 -42
  167. package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
  168. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
  169. package/src/components/ui/action-menu/index.tsx +56 -33
  170. package/src/components/ui/action-menu/measurement-control.tsx +14 -19
  171. package/src/components/ui/action-menu/placement.ts +20 -0
  172. package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
  173. package/src/components/ui/action-menu/view-toggles.tsx +47 -42
  174. package/src/components/ui/build-panel/build-panel.tsx +242 -0
  175. package/src/components/ui/command-palette/editor-commands.tsx +7 -42
  176. package/src/components/ui/command-palette/index.tsx +50 -39
  177. package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
  178. package/src/components/ui/controls/action-button.tsx +5 -2
  179. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  180. package/src/components/ui/controls/material-paint-panel.tsx +97 -26
  181. package/src/components/ui/controls/material-picker.tsx +4 -1
  182. package/src/components/ui/controls/metric-control.tsx +1 -1
  183. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  184. package/src/components/ui/controls/segmented-control.tsx +21 -8
  185. package/src/components/ui/controls/select-control.tsx +65 -0
  186. package/src/components/ui/controls/slider-control.tsx +1 -1
  187. package/src/components/ui/controls/toggle-control.tsx +3 -3
  188. package/src/components/ui/controls/tool-options-panel.tsx +1 -1
  189. package/src/components/ui/floating-level-selector.tsx +159 -59
  190. package/src/components/ui/helpers/building-helper.tsx +4 -1
  191. package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
  192. package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
  193. package/src/components/ui/helpers/helper-manager.tsx +331 -14
  194. package/src/components/ui/helpers/item-helper.tsx +17 -1
  195. package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
  196. package/src/components/ui/icon-ref.tsx +45 -1
  197. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  198. package/src/components/ui/node-icon-image.tsx +16 -0
  199. package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
  200. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  201. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  202. package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
  203. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
  204. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  205. package/src/components/ui/panels/multi-field-value.ts +6 -3
  206. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  207. package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
  208. package/src/components/ui/panels/node-display.ts +8 -3
  209. package/src/components/ui/panels/panel-manager.tsx +103 -68
  210. package/src/components/ui/panels/panel-wrapper.tsx +130 -62
  211. package/src/components/ui/panels/parametric-field-control.tsx +3 -3
  212. package/src/components/ui/panels/parametric-inspector.tsx +7 -7
  213. package/src/components/ui/panels/reference-panel.tsx +2 -3
  214. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  215. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  216. package/src/components/ui/panels/room-name-options.ts +47 -0
  217. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  218. package/src/components/ui/panels/room-panel.tsx +790 -0
  219. package/src/components/ui/panels/shape-choice.tsx +58 -0
  220. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  221. package/src/components/ui/primitives/badge.tsx +8 -0
  222. package/src/components/ui/primitives/color-dot.tsx +36 -24
  223. package/src/components/ui/primitives/sidebar.tsx +3 -1
  224. package/src/components/ui/right-stack.test.ts +29 -0
  225. package/src/components/ui/right-stack.tsx +164 -0
  226. package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
  227. package/src/components/ui/sidebar/icon-rail.tsx +8 -3
  228. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  229. package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
  230. package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
  231. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
  232. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
  233. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
  234. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
  235. package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
  236. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
  237. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
  238. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
  239. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  240. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  241. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  242. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  243. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  244. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  245. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  246. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  247. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  248. package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
  249. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  250. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  251. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  252. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  253. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  254. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  255. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  256. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  257. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  258. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  259. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
  260. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  261. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  262. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  263. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  264. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
  265. package/src/components/ui/sidebar/tab-bar.tsx +14 -4
  266. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  267. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  268. package/src/components/viewer/floorplan-preview.tsx +5 -6
  269. package/src/components/viewer/viewer-controls-bar.tsx +18 -6
  270. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  271. package/src/components/viewer/viewer-rooms.tsx +191 -0
  272. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  273. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  274. package/src/components/viewer/viewer-stage.tsx +2 -2
  275. package/src/components/viewer-zone-system.tsx +3 -8
  276. package/src/hooks/use-auto-save.integration.test.tsx +215 -43
  277. package/src/hooks/use-auto-save.ts +60 -9
  278. package/src/hooks/use-ceiling-events.test.ts +123 -118
  279. package/src/hooks/use-grid-events.test.ts +105 -105
  280. package/src/hooks/use-keyboard.test.ts +243 -19
  281. package/src/hooks/use-keyboard.ts +118 -83
  282. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  283. package/src/hooks/use-open-wall-ends.ts +238 -0
  284. package/src/hooks/use-placement-notice.test.ts +71 -0
  285. package/src/hooks/use-placement-notice.ts +73 -0
  286. package/src/hooks/use-selected-room.ts +143 -0
  287. package/src/index.tsx +312 -58
  288. package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
  289. package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
  290. package/src/lib/add-storey.test.ts +177 -0
  291. package/src/lib/add-storey.ts +59 -0
  292. package/src/lib/below-grade.test.tsx +171 -0
  293. package/src/lib/below-grade.ts +133 -0
  294. package/src/lib/camera-facing.test.ts +24 -0
  295. package/src/lib/camera-facing.ts +24 -0
  296. package/src/lib/ceiling-edit-session.test.ts +321 -0
  297. package/src/lib/ceiling-edit-session.ts +206 -0
  298. package/src/lib/ceiling-region-session.test.ts +179 -0
  299. package/src/lib/continuation.test.ts +19 -1
  300. package/src/lib/continuation.ts +25 -4
  301. package/src/lib/direct-manipulation.ts +2 -0
  302. package/src/lib/door-interaction.ts +14 -0
  303. package/src/lib/download-project-file.ts +17 -0
  304. package/src/lib/editor-level-display.test.ts +96 -0
  305. package/src/lib/editor-level-display.ts +55 -0
  306. package/src/lib/editor-views.ts +178 -0
  307. package/src/lib/export-geometry-wait.test.ts +90 -0
  308. package/src/lib/export-texture-utils.ts +2 -4
  309. package/src/lib/floor-edit-session.ts +153 -0
  310. package/src/lib/floor-footprints.test.ts +563 -0
  311. package/src/lib/floor-footprints.ts +395 -0
  312. package/src/lib/floor-foundation-support.test.ts +284 -0
  313. package/src/lib/floor-opening-draft.ts +218 -0
  314. package/src/lib/floor-openings.test.ts +216 -0
  315. package/src/lib/floor-region-geometry.ts +191 -0
  316. package/src/lib/floor-region-session.test.ts +485 -0
  317. package/src/lib/floor-region-session.ts +589 -0
  318. package/src/lib/floor-region-snap.ts +101 -0
  319. package/src/lib/floor-step-hover.test.ts +32 -0
  320. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  321. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  322. package/src/lib/floorplan/dimension-drive.ts +479 -0
  323. package/src/lib/floorplan/floorplan-export.test.ts +12 -8
  324. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  325. package/src/lib/floorplan/floorplan-extension.ts +32 -0
  326. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  327. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  328. package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
  329. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
  330. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
  331. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  332. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  333. package/src/lib/floorplan/geometry.ts +13 -6
  334. package/src/lib/floorplan/index.ts +0 -9
  335. package/src/lib/floorplan/items.ts +5 -2
  336. package/src/lib/floorplan/marks.test.ts +213 -0
  337. package/src/lib/floorplan/marks.ts +238 -0
  338. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  339. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  340. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  341. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  342. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  343. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  344. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  345. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  346. package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
  347. package/src/lib/floorplan/schedules.test.ts +189 -0
  348. package/src/lib/floorplan/schedules.ts +339 -0
  349. package/src/lib/floorplan/sheet-export.test.ts +124 -0
  350. package/src/lib/floorplan/sheet-export.ts +318 -0
  351. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
  352. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  353. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  354. package/src/lib/floorplan/site-plan/coverage.ts +230 -0
  355. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  356. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  357. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  358. package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
  359. package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
  360. package/src/lib/floorplan/site-plan/geometry.ts +142 -0
  361. package/src/lib/floorplan/site-plan/index.ts +55 -0
  362. package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
  363. package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
  364. package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
  365. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  366. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  367. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  368. package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
  369. package/src/lib/floorplan/types.ts +1 -27
  370. package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
  371. package/src/lib/follow-pascal-exterior.test.ts +171 -0
  372. package/src/lib/follow-pascal-exterior.ts +39 -0
  373. package/src/lib/follow-pascal-presentation.test.ts +203 -0
  374. package/src/lib/follow-pascal-presentation.ts +129 -0
  375. package/src/lib/follow-pascal-sight.ts +84 -0
  376. package/src/lib/follow-pascal-targets.test.ts +406 -0
  377. package/src/lib/follow-pascal-targets.ts +198 -0
  378. package/src/lib/follow-pascal.test.ts +350 -0
  379. package/src/lib/follow-pascal.ts +284 -0
  380. package/src/lib/fresh-planar-placement.test.ts +148 -0
  381. package/src/lib/fresh-planar-placement.ts +114 -37
  382. package/src/lib/geometry-script/author.ts +157 -0
  383. package/src/lib/geometry-script/client.ts +159 -0
  384. package/src/lib/geometry-script/finish-scripts.ts +127 -0
  385. package/src/lib/geometry-script/protocol.ts +13 -0
  386. package/src/lib/gesture-lifecycle.test.ts +595 -0
  387. package/src/lib/gesture-lifecycle.ts +290 -0
  388. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  389. package/src/lib/glb-export.test.ts +636 -126
  390. package/src/lib/glb-export.ts +241 -120
  391. package/src/lib/history.test.ts +18 -7
  392. package/src/lib/history.ts +7 -0
  393. package/src/lib/hud-title.ts +109 -0
  394. package/src/lib/hydration-gated-storage.ts +49 -0
  395. package/src/lib/ifc-export.test.ts +174 -0
  396. package/src/lib/ifc-export.ts +201 -0
  397. package/src/lib/ifc-parts.ts +15 -0
  398. package/src/lib/inspector-expanded.test.ts +44 -0
  399. package/src/lib/inspector-expanded.ts +36 -0
  400. package/src/lib/interaction/scope.ts +22 -0
  401. package/src/lib/keyboard-pan.test.ts +129 -7
  402. package/src/lib/keyboard-pan.ts +161 -12
  403. package/src/lib/level-selection.test.ts +57 -0
  404. package/src/lib/level-selection.ts +16 -1
  405. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  406. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  407. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  408. package/src/lib/lot/clean-ring.test.ts +187 -0
  409. package/src/lib/lot/clean-ring.ts +217 -0
  410. package/src/lib/lot/dossier.test.ts +202 -0
  411. package/src/lib/lot/dossier.ts +498 -0
  412. package/src/lib/lot/drop-in.test.ts +326 -0
  413. package/src/lib/lot/drop-in.ts +392 -0
  414. package/src/lib/lot/index.ts +43 -0
  415. package/src/lib/lot/lot-patch.test.ts +340 -0
  416. package/src/lib/lot/lot-patch.ts +394 -0
  417. package/src/lib/lot/parcel-provider.ts +43 -0
  418. package/src/lib/lot/terrain.test.ts +161 -0
  419. package/src/lib/lot/terrain.ts +266 -0
  420. package/src/lib/material-catalog-model.ts +3 -1
  421. package/src/lib/material-paint-panel-model.ts +9 -5
  422. package/src/lib/material-paint.ts +4 -0
  423. package/src/lib/measurement-kind.test.ts +6 -8
  424. package/src/lib/measurements.test.ts +23 -0
  425. package/src/lib/mezzanine-actions.test.ts +342 -0
  426. package/src/lib/mezzanine-draft.test.ts +501 -0
  427. package/src/lib/mezzanine-draft.ts +278 -0
  428. package/src/lib/mezzanine-messages.ts +26 -0
  429. package/src/lib/mezzanine-selection.test.ts +207 -0
  430. package/src/lib/mezzanine-stairs.ts +46 -0
  431. package/src/lib/model-export.ts +3 -1
  432. package/src/lib/node-action-movement.ts +17 -6
  433. package/src/lib/node-display-label.ts +37 -0
  434. package/src/lib/orbit-floor-clearance.test.ts +201 -0
  435. package/src/lib/orbit-floor-clearance.ts +154 -0
  436. package/src/lib/paint-commit-route.test.ts +37 -0
  437. package/src/lib/paint-commit-route.ts +17 -0
  438. package/src/lib/paint-eyedropper.test.tsx +165 -0
  439. package/src/lib/paint-eyedropper.ts +99 -0
  440. package/src/lib/paint-pass-through.test.ts +122 -0
  441. package/src/lib/paint-pass-through.ts +78 -0
  442. package/src/lib/paint-region-hover.ts +25 -0
  443. package/src/lib/paint-region-mode.ts +154 -0
  444. package/src/lib/paint-regions.test.ts +166 -0
  445. package/src/lib/paint-regions.ts +192 -0
  446. package/src/lib/paint-scope.test.ts +317 -35
  447. package/src/lib/paint-scope.ts +267 -25
  448. package/src/lib/panel-rows.ts +21 -0
  449. package/src/lib/parametric-node-update.ts +16 -7
  450. package/src/lib/plate-paint-affected.test.ts +300 -0
  451. package/src/lib/plate-paint-affected.ts +177 -0
  452. package/src/lib/plugin-chat.ts +65 -0
  453. package/src/lib/plugin-panels.test.ts +25 -0
  454. package/src/lib/plugin-panels.ts +94 -0
  455. package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
  456. package/src/lib/point-ask/__fixtures__/house.ts +81 -0
  457. package/src/lib/point-ask/ask-created.test.ts +195 -0
  458. package/src/lib/point-ask/ask-flow.test.ts +380 -0
  459. package/src/lib/point-ask/build-context.test.ts +173 -0
  460. package/src/lib/point-ask/build-context.ts +94 -0
  461. package/src/lib/point-ask/capture-crop.ts +129 -0
  462. package/src/lib/point-ask/choreography.ts +69 -0
  463. package/src/lib/point-ask/compare.test.ts +145 -0
  464. package/src/lib/point-ask/compare.ts +73 -0
  465. package/src/lib/point-ask/describe-target.test.ts +165 -0
  466. package/src/lib/point-ask/describe-target.ts +234 -0
  467. package/src/lib/point-ask/dictation.ts +79 -0
  468. package/src/lib/point-ask/edge-marker.test.ts +61 -0
  469. package/src/lib/point-ask/edge-marker.ts +27 -0
  470. package/src/lib/point-ask/frame-loop.ts +25 -0
  471. package/src/lib/point-ask/history-marks.test.ts +94 -0
  472. package/src/lib/point-ask/history-marks.ts +39 -0
  473. package/src/lib/point-ask/host-api.ts +32 -0
  474. package/src/lib/point-ask/outline-box.test.ts +30 -0
  475. package/src/lib/point-ask/outline-box.ts +11 -0
  476. package/src/lib/point-ask/overlay-metrics.ts +6 -0
  477. package/src/lib/point-ask/pick.test.ts +220 -0
  478. package/src/lib/point-ask/pick.ts +121 -0
  479. package/src/lib/point-ask/pin-machine.test.ts +494 -0
  480. package/src/lib/point-ask/pin-machine.ts +217 -0
  481. package/src/lib/point-ask/placement.test.ts +203 -0
  482. package/src/lib/point-ask/placement.ts +141 -0
  483. package/src/lib/point-ask/pointer-tracker.ts +34 -0
  484. package/src/lib/point-ask/projector.ts +83 -0
  485. package/src/lib/point-ask/region.test.ts +157 -0
  486. package/src/lib/point-ask/region.ts +136 -0
  487. package/src/lib/point-ask/retry.test.ts +217 -0
  488. package/src/lib/point-ask/retry.ts +37 -0
  489. package/src/lib/point-ask/scene-commit.test.ts +312 -0
  490. package/src/lib/point-ask/scene-commit.ts +32 -0
  491. package/src/lib/point-ask/session.ts +1391 -0
  492. package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
  493. package/src/lib/point-ask/spring-keyframes.ts +38 -0
  494. package/src/lib/point-ask/spring.test.ts +63 -0
  495. package/src/lib/point-ask/spring.ts +107 -0
  496. package/src/lib/point-ask/suggestions.test.ts +80 -0
  497. package/src/lib/point-ask/suggestions.ts +125 -0
  498. package/src/lib/point-ask/target-scope.test.ts +622 -0
  499. package/src/lib/point-ask/target-scope.ts +338 -0
  500. package/src/lib/point-ask/types.ts +48 -0
  501. package/src/lib/portable-export.test.ts +34 -1
  502. package/src/lib/portable-export.ts +119 -53
  503. package/src/lib/print-feature-thickness.ts +10 -1
  504. package/src/lib/print-roof-solids.test.ts +39 -1
  505. package/src/lib/print-roof-solids.ts +43 -10
  506. package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
  507. package/src/lib/print-shell-compiler.ts +1 -0
  508. package/src/lib/registered-draft-snap.ts +12 -0
  509. package/src/lib/roof-duplication.ts +10 -2
  510. package/src/lib/room-assembly-overlay.test.ts +275 -0
  511. package/src/lib/room-assembly-overlay.ts +265 -0
  512. package/src/lib/room-built-on.test.ts +379 -0
  513. package/src/lib/room-built-on.ts +138 -0
  514. package/src/lib/room-construction-commands.ts +712 -0
  515. package/src/lib/room-construction.test.ts +746 -0
  516. package/src/lib/room-construction.ts +385 -0
  517. package/src/lib/room-controls-phase6.test.ts +630 -0
  518. package/src/lib/room-divide-session.ts +523 -0
  519. package/src/lib/room-handle-drag.ts +1068 -0
  520. package/src/lib/room-name-catalog.test.ts +90 -0
  521. package/src/lib/room-name-catalog.ts +168 -0
  522. package/src/lib/room-push-dimensions.test.ts +311 -0
  523. package/src/lib/room-push-dimensions.ts +251 -0
  524. package/src/lib/room-selection-commands.ts +62 -0
  525. package/src/lib/room-selection.test.ts +1017 -0
  526. package/src/lib/room-selection.ts +417 -0
  527. package/src/lib/room-structure-commands.ts +94 -0
  528. package/src/lib/room-structure.test.ts +921 -0
  529. package/src/lib/room-transform-session.ts +570 -0
  530. package/src/lib/room-zone-routing.test.ts +294 -0
  531. package/src/lib/room-zone-routing.ts +197 -0
  532. package/src/lib/scene-clipboard.test.ts +610 -1
  533. package/src/lib/scene-clipboard.ts +269 -58
  534. package/src/lib/scene-material-refs.test.ts +79 -0
  535. package/src/lib/scene-material-refs.ts +79 -0
  536. package/src/lib/scene.test.ts +24 -36
  537. package/src/lib/scene.ts +33 -57
  538. package/src/lib/selection-routing.test.ts +129 -45
  539. package/src/lib/selection-routing.ts +54 -75
  540. package/src/lib/session-writes.test.ts +120 -0
  541. package/src/lib/session-writes.ts +205 -0
  542. package/src/lib/sfx-bus.ts +13 -0
  543. package/src/lib/sfx-player.test.ts +70 -9
  544. package/src/lib/sfx-player.ts +6 -0
  545. package/src/lib/sidebar-panel.ts +62 -0
  546. package/src/lib/site-boundary.test.ts +4 -6
  547. package/src/lib/site-boundary.ts +1 -1
  548. package/src/lib/snapping-mode.test.ts +4 -0
  549. package/src/lib/snapping-mode.ts +8 -1
  550. package/src/lib/stair-levels.test.ts +25 -0
  551. package/src/lib/stair-levels.ts +22 -3
  552. package/src/lib/terrace-draft.test.ts +145 -0
  553. package/src/lib/terrace-draft.ts +214 -0
  554. package/src/lib/tool-cursor-icon.test.ts +16 -0
  555. package/src/lib/tool-cursor-icon.ts +11 -0
  556. package/src/lib/typing-target.ts +9 -0
  557. package/src/lib/units.ts +77 -39
  558. package/src/lib/vector-edges.test.ts +58 -0
  559. package/src/lib/vector-edges.ts +400 -0
  560. package/src/lib/viewer-selection.test.ts +577 -0
  561. package/src/lib/viewer-selection.ts +580 -0
  562. package/src/lib/wall-draw-variant.test.ts +71 -0
  563. package/src/lib/wall-draw-variant.ts +91 -0
  564. package/src/lib/wall-paint-region-session.ts +246 -0
  565. package/src/lib/wall-paint-region.test.ts +373 -0
  566. package/src/lib/wall-placement-pointer.test.ts +42 -0
  567. package/src/lib/wall-placement-pointer.ts +57 -0
  568. package/src/lib/wall-polygon-draft.test.ts +262 -0
  569. package/src/lib/wall-polygon-draft.ts +286 -0
  570. package/src/lib/wall-push.test.ts +453 -0
  571. package/src/lib/wall-region-face.ts +352 -0
  572. package/src/lib/wall-region-handles.test.ts +304 -0
  573. package/src/lib/wall-region-handles.ts +368 -0
  574. package/src/lib/wall-region-list.test.ts +119 -0
  575. package/src/lib/wall-region-list.ts +136 -0
  576. package/src/lib/wall-region-snap.ts +110 -0
  577. package/src/lib/window-changes.ts +35 -0
  578. package/src/lib/window-interaction.ts +14 -0
  579. package/src/lib/zone-content.ts +4 -1
  580. package/src/lib/zone-name.test.ts +17 -0
  581. package/src/lib/zone-name.ts +17 -0
  582. package/src/store/opens-in-select-mode.test.ts +223 -0
  583. package/src/store/paint-mode.test.ts +188 -0
  584. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  585. package/src/store/tool-mode.test.ts +137 -6
  586. package/src/store/use-audio.test.ts +146 -0
  587. package/src/store/use-audio.tsx +127 -3
  588. package/src/store/use-construction-reveal.test.ts +173 -0
  589. package/src/store/use-construction-reveal.ts +145 -0
  590. package/src/store/use-corner-snap-hint.test.ts +24 -0
  591. package/src/store/use-corner-snap-hint.ts +47 -0
  592. package/src/store/use-delete-confirmation.ts +28 -1
  593. package/src/store/use-drawing-view.test.ts +24 -3
  594. package/src/store/use-drawing-view.ts +29 -1
  595. package/src/store/use-editor.tsx +432 -321
  596. package/src/store/use-floorplan-draft-preview.ts +13 -0
  597. package/src/store/use-floorplan-marquee.ts +12 -2
  598. package/src/store/use-follow-pascal.test.ts +62 -0
  599. package/src/store/use-follow-pascal.ts +53 -0
  600. package/src/store/use-point-ask.ts +247 -0
  601. package/src/store/use-quick-measurement-hud.ts +3 -3
  602. package/src/store/use-stair-build-preview.ts +33 -5
  603. package/src/store/use-wall-move-ghosts.ts +4 -2
  604. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  605. package/src/store/use-wall-region-handles-selection.ts +139 -0
  606. package/src/store/view-layout.test.ts +201 -0
  607. package/src/store/view-layout.ts +199 -0
  608. package/src/test-utils/immediate-animation-frames.ts +16 -0
  609. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  610. package/src/test-utils/render-current-markup.tsx +16 -0
  611. package/src/test-utils/selection-harness.ts +82 -0
  612. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  613. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  614. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  615. package/src/components/tools/fence/fence-drafting.ts +0 -266
  616. package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
  617. package/src/components/tools/stair/stair-click-guard.ts +0 -72
  618. package/src/components/tools/stair/stair-defaults.ts +0 -18
  619. package/src/components/tools/stair/stair-tool.tsx +0 -634
  620. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  621. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  622. package/src/lib/floorplan/stairs.ts +0 -489
  623. package/src/lib/level-duplication.test.ts +0 -144
  624. package/src/lib/level-duplication.ts +0 -195
@@ -0,0 +1,206 @@
1
+ import {
2
+ type AnyNode,
3
+ type AnyNodeId,
4
+ type BuildingNode,
5
+ type CeilingNode,
6
+ derivedFieldViolations,
7
+ emitter,
8
+ isDerivedNode,
9
+ type LevelNode,
10
+ resolveLevelId,
11
+ useScene,
12
+ } from '@pascal-app/core'
13
+ import { useViewer } from '@pascal-app/viewer'
14
+ import { useEffect } from 'react'
15
+ import { create } from 'zustand'
16
+ import { markToolCancelConsumed } from '../hooks/use-keyboard'
17
+ import useEditor from '../store/use-editor'
18
+ import useInteractionScope from '../store/use-interaction-scope'
19
+ import { selectRoom } from './room-selection-commands'
20
+ import { completeElementAction } from './room-zone-routing'
21
+
22
+ /**
23
+ * The explicit "Edit ceiling" session: while it holds a ceiling, and only then,
24
+ * that ceiling shows its corner brackets, polygon editor and height handle.
25
+ *
26
+ * A tiny store rather than an interaction scope: a non-idle scope turns off
27
+ * selection picking and hides every overlay, while the session is a standing
28
+ * mode in which the rest of the scene must stay hoverable and clickable. The
29
+ * polygon drags inside it still run their own `reshaping` scope.
30
+ */
31
+ export type CeilingEditSession = {
32
+ ceilingId: CeilingNode['id']
33
+ /** The room the ceiling belonged to at start; a polygon edit detaches the link. */
34
+ zoneId: string | null
35
+ levelId: string
36
+ }
37
+
38
+ export const useCeilingEditSession = create<{ session: CeilingEditSession | null }>(() => ({
39
+ session: null,
40
+ }))
41
+
42
+ /** The ceiling whose edit controls may show, or null outside a session. */
43
+ export function useCeilingEditCeilingId(): string | null {
44
+ return useCeilingEditSession((state) => state.session?.ceilingId ?? null)
45
+ }
46
+
47
+ export function getCeilingEditSession(): CeilingEditSession | null {
48
+ return useCeilingEditSession.getState().session
49
+ }
50
+
51
+ type Nodes = Record<string, AnyNode>
52
+
53
+ function resolveCeiling(nodes: Nodes, target: string): CeilingNode | null {
54
+ const node = nodes[target]
55
+ if (node?.type === 'ceiling') return node
56
+ if (node?.type !== 'zone') return null
57
+ const linked = Object.values(nodes).filter(
58
+ (candidate): candidate is CeilingNode =>
59
+ candidate.type === 'ceiling' && candidate.zoneId === target,
60
+ )
61
+ return (
62
+ linked.find((ceiling) => ceiling.parentId === node.parentId && ceiling.boundary === 'auto') ??
63
+ linked.find((ceiling) => ceiling.parentId === node.parentId) ??
64
+ linked[0] ??
65
+ null
66
+ )
67
+ }
68
+
69
+ let stopLifecycle: (() => void) | null = null
70
+
71
+ /**
72
+ * Starts editing the ceiling `target` names: a ceiling id, or a room zone id
73
+ * (its linked ceiling). Selects the ceiling on its level, going inside the
74
+ * building first. Returns false when there is no such ceiling.
75
+ */
76
+ export function startCeilingEdit(target: string): boolean {
77
+ const nodes = useScene.getState().nodes as Nodes
78
+ const ceiling = resolveCeiling(nodes, target)
79
+ if (!ceiling) return false
80
+ endCeilingEdit()
81
+
82
+ const levelId = resolveLevelId(ceiling, nodes)
83
+ const zone = ceiling.zoneId ? nodes[ceiling.zoneId] : nodes[target]
84
+ const zoneId = zone?.type === 'zone' ? zone.id : null
85
+ if (useEditor.getState().phase === 'site') useEditor.getState().setPhase('building')
86
+
87
+ useCeilingEditSession.setState({ session: { ceilingId: ceiling.id, zoneId, levelId } })
88
+ const level = nodes[levelId]
89
+ const selection = useViewer.getState().selection
90
+ useViewer.getState().setSelection({
91
+ ...(level?.parentId && level.parentId !== selection.buildingId
92
+ ? { buildingId: level.parentId as BuildingNode['id'] }
93
+ : {}),
94
+ ...(levelId !== selection.levelId ? { levelId: levelId as LevelNode['id'] } : {}),
95
+ selectedIds: [ceiling.id],
96
+ })
97
+ stopLifecycle = watchSession()
98
+ return true
99
+ }
100
+
101
+ /**
102
+ * The scene-graph entry: a plain tree click that left exactly this ceiling
103
+ * selected opens its session. Modifier multi-selection does not.
104
+ */
105
+ export function startCeilingEditFromTreeSelection(nodeId: string): boolean {
106
+ const { selectedIds } = useViewer.getState().selection
107
+ if (selectedIds.length !== 1 || selectedIds[0] !== nodeId) return false
108
+ return startCeilingEdit(nodeId)
109
+ }
110
+
111
+ export function endCeilingEdit() {
112
+ stopLifecycle?.()
113
+ stopLifecycle = null
114
+ const session = getCeilingEditSession()
115
+ if (!session) return
116
+ useCeilingEditSession.setState({ session: null })
117
+ // The hole editor mounts only inside the session; a hole scope it leaves behind
118
+ // would keep selection picking off with no editor to end it.
119
+ useInteractionScope
120
+ .getState()
121
+ .endIf(
122
+ (scope) =>
123
+ scope.kind === 'reshaping' &&
124
+ scope.reshape === 'hole' &&
125
+ scope.nodeId === session.ceilingId,
126
+ )
127
+ }
128
+
129
+ /** Ends the session with the editor that owns it, so none outlives an unmount. */
130
+ export function useCeilingEditSessionOwner() {
131
+ useEffect(() => endCeilingEdit, [])
132
+ }
133
+
134
+ /**
135
+ * Writes a ceiling's holes. Manual holes are authored data the derived-node
136
+ * guard allows on an auto ceiling, so this is a plain update that keeps the
137
+ * room link (only a polygon edit detaches); a write that would change a
138
+ * room-cut hole is refused (returns false).
139
+ */
140
+ export function setCeilingHoles(
141
+ ceilingId: string,
142
+ patch: Pick<CeilingNode, 'holes'> & Partial<Pick<CeilingNode, 'holeMetadata'>>,
143
+ ): boolean {
144
+ const scene = useScene.getState()
145
+ const ceiling = scene.nodes[ceilingId as AnyNodeId]
146
+ if (ceiling?.type !== 'ceiling') return false
147
+ if (isDerivedNode(ceiling) && derivedFieldViolations(ceiling, patch).length > 0) return false
148
+ scene.updateNode(ceilingId as AnyNodeId, patch)
149
+ return true
150
+ }
151
+
152
+ /** Escape: leaves the session for the ceiling's room, or for nothing when it has none. */
153
+ export function exitCeilingEditToRoom(): boolean {
154
+ const session = getCeilingEditSession()
155
+ if (!session) return false
156
+ endCeilingEdit()
157
+ const zone = session.zoneId ? useScene.getState().nodes[session.zoneId as AnyNodeId] : undefined
158
+ if (zone?.type !== 'zone') {
159
+ useViewer.getState().setSelection({ selectedIds: [] })
160
+ return true
161
+ }
162
+ const room = { levelId: session.levelId, zoneId: zone.id }
163
+ // A room the index no longer knows (its walls just opened) is still where the user came from.
164
+ if (!completeElementAction({ room, point: null })) selectRoom(room)
165
+ return true
166
+ }
167
+
168
+ function watchSession() {
169
+ const active = () => getCeilingEditSession()
170
+ const stopViewer = useViewer.subscribe((state) => {
171
+ const session = active()
172
+ if (!session) return
173
+ const { selectedIds, levelId } = state.selection
174
+ if (
175
+ levelId !== session.levelId ||
176
+ selectedIds.length !== 1 ||
177
+ selectedIds[0] !== session.ceilingId
178
+ )
179
+ endCeilingEdit()
180
+ })
181
+ const stopEditor = useEditor.subscribe((state, previous) => {
182
+ if (state.phase !== previous.phase) endCeilingEdit()
183
+ })
184
+ const stopScene = useScene.subscribe((state) => {
185
+ const session = active()
186
+ if (session && state.nodes[session.ceilingId as AnyNodeId]?.type !== 'ceiling') endCeilingEdit()
187
+ })
188
+ // A reshape in flight owns Escape (it cancels the drag); only an idle session exits.
189
+ const onToolCancel = () => {
190
+ if (
191
+ !active() ||
192
+ useEditor.getState().mode !== 'select' ||
193
+ useInteractionScope.getState().scope.kind !== 'idle'
194
+ )
195
+ return
196
+ exitCeilingEditToRoom()
197
+ markToolCancelConsumed()
198
+ }
199
+ emitter.on('tool:cancel', onToolCancel)
200
+ return () => {
201
+ stopViewer()
202
+ stopEditor()
203
+ stopScene()
204
+ emitter.off('tool:cancel', onToolCancel)
205
+ }
206
+ }
@@ -0,0 +1,179 @@
1
+ import { afterAll, afterEach, beforeEach, expect, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeId,
5
+ area,
6
+ CeilingNode,
7
+ clearSceneHistory,
8
+ LevelNode,
9
+ useScene,
10
+ ZoneNode,
11
+ } from '@pascal-app/core'
12
+ import useEditor from '../store/use-editor'
13
+ import {
14
+ describeCeilingRegionRoom,
15
+ floorRegionRoomGone,
16
+ moveFloorRegion,
17
+ PICK_MATERIAL_MESSAGE,
18
+ pressFloorRegion,
19
+ releaseFloorRegion,
20
+ useFloorRegionDraft,
21
+ } from './floor-region-session'
22
+ import type { FloorRegionPoint } from './floor-region-snap'
23
+ import { paintRegionHoverHint, usePaintRegionMode } from './paint-region-mode'
24
+
25
+ // Rectangle and Polygon on a ceiling: the floor's outline gesture, drawn on the
26
+ // ceiling's underside, committing one region to the room (an automatic
27
+ // ceiling) or to the ceiling (a manual one) in one undo step.
28
+
29
+ const square = (x: number, z: number, size: number): [number, number][] => [
30
+ [x, z],
31
+ [x + size, z],
32
+ [x + size, z + size],
33
+ [x, z + size],
34
+ ]
35
+ const OFF = { mode: 'off', step: 0.5 } as const
36
+ const CLOSE = 0.2
37
+
38
+ const level = LevelNode.parse({ id: 'level_ceiling_region' })
39
+ const zone = ZoneNode.parse({
40
+ id: 'zone_ceiling_region',
41
+ name: 'Room',
42
+ parentId: level.id,
43
+ polygon: square(0, 0, 4),
44
+ floor: { regions: [{ id: 'rug', polygon: square(1, 1, 1), finish: 'library:tile' }] },
45
+ })
46
+ // A floor opening cut the room's ceiling in two; both parts are the room's.
47
+ const west = CeilingNode.parse({
48
+ id: 'ceiling_west',
49
+ parentId: level.id,
50
+ polygon: [
51
+ [0, 0],
52
+ [2, 0],
53
+ [2, 4],
54
+ [0, 4],
55
+ ],
56
+ boundary: 'auto',
57
+ zoneId: zone.id,
58
+ height: 2.5,
59
+ })
60
+ const east = CeilingNode.parse({
61
+ ...west,
62
+ id: 'ceiling_east',
63
+ polygon: [
64
+ [2.5, 0],
65
+ [4, 0],
66
+ [4, 4],
67
+ [2.5, 4],
68
+ ],
69
+ })
70
+ const manual = CeilingNode.parse({
71
+ id: 'ceiling_manual',
72
+ parentId: level.id,
73
+ polygon: square(6, 0, 2),
74
+ height: 2.2,
75
+ })
76
+ const nodes = Object.fromEntries(
77
+ [level, zone, west, east, manual].map((node) => [node.id, node]),
78
+ ) as Record<string, AnyNode>
79
+
80
+ const stubbed: string[] = []
81
+ const before = useScene.getState()
82
+ const node = <T extends AnyNode>(id: string) => useScene.getState().nodes[id as AnyNodeId] as T
83
+ const history = () => useScene.temporal.getState().pastStates.length
84
+
85
+ function drag(ceiling: CeilingNode, from: FloorRegionPoint, to: FloorRegionPoint) {
86
+ const room = describeCeilingRegionRoom(useScene.getState().nodes, ceiling)
87
+ pressFloorRegion('rectangle', room, from, OFF, CLOSE)
88
+ moveFloorRegion(to, OFF, CLOSE)
89
+ releaseFloorRegion(CLOSE)
90
+ }
91
+
92
+ beforeEach(() => {
93
+ globalThis.requestAnimationFrame ??= (callback) => {
94
+ callback(0)
95
+ return 0
96
+ }
97
+ globalThis.cancelAnimationFrame ??= () => {}
98
+ if (!globalThis.window) {
99
+ globalThis.window = new EventTarget() as Window & typeof globalThis
100
+ stubbed.push('window')
101
+ }
102
+ useScene.setState({ nodes, materials: {}, readOnly: false })
103
+ useScene.temporal.getState().resume()
104
+ clearSceneHistory()
105
+ useEditor.setState({
106
+ mode: 'material-paint',
107
+ activePaintMaterial: { materialPreset: 'library:blue', sourceTarget: 'ceiling' } as never,
108
+ })
109
+ usePaintRegionMode.getState().setMode('rectangle')
110
+ useFloorRegionDraft.setState({ draft: null, hover: null })
111
+ })
112
+ afterEach(() => {
113
+ useScene.setState(before)
114
+ usePaintRegionMode.getState().setMode('surface')
115
+ useFloorRegionDraft.setState({ draft: null, hover: null })
116
+ useEditor.setState({ mode: 'select', activePaintMaterial: null })
117
+ })
118
+ afterAll(async () => {
119
+ await new Promise((resolve) => setTimeout(resolve, 50))
120
+ for (const key of stubbed.splice(0)) delete (globalThis as Record<string, unknown>)[key]
121
+ })
122
+
123
+ test('an automatic ceiling draws on every part of its room, at the plane it draws at', () => {
124
+ const room = describeCeilingRegionRoom(nodes, west)
125
+ expect(room).toMatchObject({ zoneId: zone.id, ceilingId: west.id, levelId: level.id })
126
+ expect(room.elevation).toBeCloseTo(2.49)
127
+ expect(area(room.clear)).toBeCloseTo(8 + 6)
128
+ expect(describeCeilingRegionRoom(nodes, manual)).toMatchObject({ zoneId: manual.id })
129
+ })
130
+
131
+ test('a rectangle on a room ceiling paints one room region, clipped to the ceiling', () => {
132
+ drag(west, [1, 1], [3, 3])
133
+ const regions = node<ZoneNode>(zone.id).ceiling?.regions ?? []
134
+ expect(regions).toHaveLength(1)
135
+ expect(regions[0]!.finish).toBe('library:blue')
136
+ // Across the opening gap: 1 × 2 on the west part, 0.5 × 2 on the east part.
137
+ expect(area([{ outer: regions[0]!.polygon, holes: [] }])).toBeGreaterThan(2.9)
138
+ expect(history()).toBe(1)
139
+ // The floor is untouched.
140
+ expect(node<ZoneNode>(zone.id).floor).toEqual(zone.floor)
141
+ expect(useFloorRegionDraft.getState().draft).toBeNull()
142
+ })
143
+
144
+ test('a polygon on a manual ceiling paints the ceiling itself', () => {
145
+ usePaintRegionMode.getState().setMode('polygon')
146
+ const room = describeCeilingRegionRoom(useScene.getState().nodes, manual)
147
+ for (const point of [
148
+ [6.2, 0.2],
149
+ [7.5, 0.2],
150
+ [7.5, 1.5],
151
+ [6.2, 0.2],
152
+ ] as FloorRegionPoint[]) {
153
+ pressFloorRegion('polygon', room, point, OFF, CLOSE)
154
+ releaseFloorRegion(CLOSE)
155
+ }
156
+ expect(node<CeilingNode>(manual.id).regions).toHaveLength(1)
157
+ expect(node<ZoneNode>(zone.id).ceiling).toBeUndefined()
158
+ expect(history()).toBe(1)
159
+ })
160
+
161
+ test('outside the ceiling, or with nothing to paint with, nothing is written', () => {
162
+ drag(manual, [10, 10], [12, 12])
163
+ expect(node<CeilingNode>(manual.id).regions).toBeUndefined()
164
+ useEditor.setState({ activePaintMaterial: null })
165
+ drag(manual, [6.5, 0.5], [7.5, 1.5])
166
+ expect(node<CeilingNode>(manual.id).regions).toBeUndefined()
167
+ expect(usePaintRegionMode.getState().notice).toBe(PICK_MATERIAL_MESSAGE)
168
+ expect(history()).toBe(0)
169
+ })
170
+
171
+ test('a ceiling draft ends when its ceiling goes; the hints name ceilings', () => {
172
+ const room = describeCeilingRegionRoom(nodes, manual)
173
+ pressFloorRegion('rectangle', room, [6.5, 0.5], OFF, CLOSE)
174
+ const { [manual.id]: _, ...rest } = nodes
175
+ expect(floorRegionRoomGone(rest)).toBe(true)
176
+ expect(floorRegionRoomGone(nodes)).toBe(false)
177
+ expect(paintRegionHoverHint('polygon')).toBe('Hover a floor or ceiling')
178
+ expect(paintRegionHoverHint('rectangle')).toBe('Hover a wall, floor or ceiling')
179
+ })
@@ -1,5 +1,10 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { CONTINUATION_PROFILES, continuationContextOf, nextContinuation } from './continuation'
2
+ import {
3
+ CONTINUATION_PROFILES,
4
+ continuationContextOf,
5
+ keyCyclableContinuationContext,
6
+ nextContinuation,
7
+ } from './continuation'
3
8
 
4
9
  describe('canopy continuation', () => {
5
10
  test('maps the canopy tool to its own single and continuous profile', () => {
@@ -9,3 +14,16 @@ describe('canopy continuation', () => {
9
14
  expect(nextContinuation('canopy', 'continuous')).toBe('single')
10
15
  })
11
16
  })
17
+
18
+ describe('wall drawing mode', () => {
19
+ test('is picked in the Build panel, never cycled by C or a HUD chip', () => {
20
+ expect(CONTINUATION_PROFILES.wall.options).toEqual(['room', 'single', 'rectangle'])
21
+ expect(keyCyclableContinuationContext('wall')).toBeNull()
22
+ })
23
+
24
+ test('other contexts still cycle from the keyboard', () => {
25
+ expect(keyCyclableContinuationContext('fence')).toBe('fence')
26
+ expect(keyCyclableContinuationContext('point')).toBe('point')
27
+ expect(keyCyclableContinuationContext(null)).toBeNull()
28
+ })
29
+ })
@@ -8,26 +8,37 @@ export const CONTINUATION_PROFILES: Record<
8
8
  default: ContinuationMode
9
9
  labels: Record<string, string>
10
10
  icons: Record<string, string>
11
+ /**
12
+ * Picked once from the Build panel before drawing (`wall-draw-variant.ts`),
13
+ * never cycled by C or a HUD chip mid-draw.
14
+ */
15
+ chosenInPanel?: boolean
11
16
  }
12
17
  > = {
18
+ // The wall tool's drawing mode: a chain that closes into a room ('room', the
19
+ // Build panel's Polygon), one wall per two clicks ('single', Walls), or a
20
+ // two-corner box of four walls ('rectangle', Rectangle).
13
21
  wall: {
14
- options: ['room', 'single'],
22
+ options: ['room', 'single', 'rectangle'],
15
23
  default: 'room',
16
- labels: { room: 'Room (auto-close)', single: 'Single wall' },
17
- icons: { room: 'lucide:square', single: 'lucide:minus' },
24
+ labels: { room: 'Polygon room', single: 'Walls', rectangle: 'Rectangle room' },
25
+ icons: { room: 'lucide:pentagon', single: 'lucide:minus', rectangle: 'lucide:square' },
26
+ chosenInPanel: true,
18
27
  },
19
28
  fence: {
20
- options: ['single', 'continuous', 'curved'],
29
+ options: ['single', 'continuous', 'curved', 'freehand'],
21
30
  default: 'continuous',
22
31
  labels: {
23
32
  continuous: 'Continuous',
24
33
  single: 'Single fence',
25
34
  curved: 'Curved fence',
35
+ freehand: 'Freehand fence',
26
36
  },
27
37
  icons: {
28
38
  continuous: 'lucide:waypoints',
29
39
  single: 'lucide:minus',
30
40
  curved: 'lucide:spline',
41
+ freehand: 'lucide:scribble',
31
42
  },
32
43
  },
33
44
  point: {
@@ -62,6 +73,16 @@ export function nextContinuation(
62
73
  return profile.options[(index + 1) % profile.options.length] ?? profile.default
63
74
  }
64
75
 
76
+ /**
77
+ * The context C and the HUD chip may cycle — null when there is none, or when
78
+ * its mode is chosen in the Build panel instead.
79
+ */
80
+ export function keyCyclableContinuationContext(
81
+ context: ContinuationContext | null,
82
+ ): ContinuationContext | null {
83
+ return context && !CONTINUATION_PROFILES[context].chosenInPanel ? context : null
84
+ }
85
+
65
86
  export function continuationContextOf(kind: string): ContinuationContext | null {
66
87
  if (kind === 'wall') return 'wall'
67
88
  if (kind === 'fence') return 'fence'
@@ -107,6 +107,7 @@ export function resolveDirectManipulationNode(
107
107
  ): AnyNode {
108
108
  const target = nodes[resolveSelectionProxyId(node, nodes)] ?? node
109
109
  const parentFrame = nodeRegistry.get(target.type)?.capabilities?.movable?.parentFrame
110
+ if (parentFrame?.independent) return target
110
111
  const parent = parentFrame?.resolveParent(target, nodes as Readonly<Record<string, AnyNode>>)
111
112
  return parent && canDirectRotateNode(parent) ? parent : target
112
113
  }
@@ -116,6 +117,7 @@ export function resolveMoveActionNode(
116
117
  nodes: Readonly<Record<string, AnyNode | undefined>>,
117
118
  ): AnyNode {
118
119
  const parentFrame = nodeRegistry.get(node.type)?.capabilities?.movable?.parentFrame
120
+ if (parentFrame?.independent) return node
119
121
  const parent = parentFrame?.resolveParent(node, nodes as Readonly<Record<string, AnyNode>>)
120
122
  return parent && (parent.type === node.type || canDirectRotateNode(parent)) ? parent : node
121
123
  }
@@ -86,3 +86,17 @@ export function closeDoorOpenState(doorId: AnyNodeId, options?: DoorOpenAnimatio
86
86
  const currentSwingAngle = getDisplayedDoorValue(doorId, 'swingAngle', node.swingAngle)
87
87
  startDoorOpenAnimation(doorId, 'swingAngle', currentSwingAngle, 0, options)
88
88
  }
89
+
90
+ export function openDoorOpenState(doorId: AnyNodeId, options?: DoorOpenAnimationOptions) {
91
+ const node = useScene.getState().nodes[doorId]
92
+ if (node?.type !== 'door' || node.openingKind === 'opening') return
93
+
94
+ if (isOperationDoorType(node.doorType)) {
95
+ const currentOpenAmount = getDisplayedDoorValue(doorId, 'operationState', node.operationState)
96
+ startDoorOpenAnimation(doorId, 'operationState', currentOpenAmount, 1, options)
97
+ return
98
+ }
99
+
100
+ const currentSwingAngle = getDisplayedDoorValue(doorId, 'swingAngle', node.swingAngle)
101
+ startDoorOpenAnimation(doorId, 'swingAngle', currentSwingAngle, DOOR_SWING_OPEN_ANGLE, options)
102
+ }
@@ -0,0 +1,17 @@
1
+ import { getProjectFileSink, type ProjectFileSink } from '@pascal-app/core'
2
+
3
+ /** Persist before reporting a successful download; a failed save is shown by the export UI. */
4
+ export async function downloadProjectFile(
5
+ blob: Blob,
6
+ name: string,
7
+ sink: ProjectFileSink | null = getProjectFileSink(),
8
+ ) {
9
+ await sink?.({ blob, name })
10
+ if (typeof document === 'undefined') return
11
+ const url = URL.createObjectURL(blob)
12
+ const anchor = document.createElement('a')
13
+ anchor.href = url
14
+ anchor.download = name
15
+ anchor.click()
16
+ setTimeout(() => URL.revokeObjectURL(url), 0)
17
+ }
@@ -0,0 +1,96 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import { BuildingNode, LevelNode, useScene } from '@pascal-app/core'
3
+ import { useViewer } from '@pascal-app/viewer'
4
+ import useEditor, { type CaptureMode } from '../store/use-editor'
5
+ import { showsWholeBuilding } from './editor-level-display'
6
+
7
+ const building = BuildingNode.parse({ id: 'building_capture', children: [] })
8
+ const levels = [0, 1, 2].map((level) =>
9
+ LevelNode.parse({ id: `level_capture_${level}`, level, parentId: building.id }),
10
+ )
11
+ const [ground, first, second] = levels.map((level) => level.id) as [
12
+ LevelNode['id'],
13
+ LevelNode['id'],
14
+ LevelNode['id'],
15
+ ]
16
+
17
+ const wholeBuilding = () => {
18
+ const state = useEditor.getState()
19
+ return showsWholeBuilding({
20
+ isPreviewMode: state.isPreviewMode,
21
+ isFirstPersonMode: state.isFirstPersonMode,
22
+ captureMode: state.captureMode,
23
+ captureLevelId: state.captureLevelId,
24
+ })
25
+ }
26
+ const activeLevel = () => useViewer.getState().selection.levelId
27
+
28
+ beforeEach(() => {
29
+ useScene.setState({
30
+ nodes: Object.fromEntries(
31
+ [{ ...building, children: levels.map((level) => level.id) }, ...levels].map((node) => [
32
+ node.id,
33
+ node,
34
+ ]),
35
+ ),
36
+ rootNodeIds: [building.id],
37
+ } as never)
38
+ useViewer.getState().setSelection({ buildingId: building.id, levelId: second })
39
+ })
40
+ afterEach(() => useEditor.getState().setCaptureMode(false))
41
+
42
+ describe('capture level picker', () => {
43
+ test('editing hides the levels above; capture opens on the whole building', () => {
44
+ expect(wholeBuilding()).toBe(false)
45
+ useEditor.getState().setCaptureMode(true)
46
+ expect(useEditor.getState().captureLevelId).toBeNull()
47
+ expect(wholeBuilding()).toBe(true)
48
+ expect(activeLevel()).toBe(second)
49
+ })
50
+
51
+ test('picking a level shows it and the levels below, as while editing', () => {
52
+ useEditor.getState().setCaptureMode(true)
53
+ useEditor.getState().setCaptureLevel(first)
54
+ expect(wholeBuilding()).toBe(false)
55
+ expect(activeLevel()).toBe(first)
56
+ useEditor.getState().setCaptureLevel(null)
57
+ expect(wholeBuilding()).toBe(true)
58
+ })
59
+
60
+ test("leaving capture restores the editor's active level and the editing rule", () => {
61
+ useEditor.getState().setCaptureMode(true)
62
+ useEditor.getState().setCaptureLevel(ground)
63
+ useEditor.getState().setCaptureMode(false)
64
+ expect(activeLevel()).toBe(second)
65
+ expect(useEditor.getState().captureLevelId).toBeNull()
66
+ expect(wholeBuilding()).toBe(false)
67
+ // The next capture opens on the whole building again.
68
+ useEditor.getState().setCaptureMode({ mode: 'standard', crop: 'area' })
69
+ expect(wholeBuilding()).toBe(true)
70
+ })
71
+
72
+ test('a level picked outside capture is ignored', () => {
73
+ useEditor.getState().setCaptureLevel(ground)
74
+ expect(activeLevel()).toBe(second)
75
+ expect(useEditor.getState().captureLevelId).toBeNull()
76
+ })
77
+
78
+ test('preset captures and the walkthrough keep their own rules', () => {
79
+ const preset: CaptureMode = { mode: 'preset', isolated: [] }
80
+ const base = { isPreviewMode: false, isFirstPersonMode: false, captureLevelId: null }
81
+ expect(showsWholeBuilding({ ...base, captureMode: preset })).toBe(false)
82
+ expect(showsWholeBuilding({ ...base, captureMode: { mode: 'idle' } })).toBe(false)
83
+ expect(
84
+ showsWholeBuilding({ ...base, isFirstPersonMode: true, captureMode: { mode: 'idle' } }),
85
+ ).toBe(true)
86
+ // A walk capture on a picked level still shows only that level and below.
87
+ expect(
88
+ showsWholeBuilding({
89
+ ...base,
90
+ isFirstPersonMode: true,
91
+ captureMode: { mode: 'standard' },
92
+ captureLevelId: first,
93
+ }),
94
+ ).toBe(false)
95
+ })
96
+ })
@@ -0,0 +1,55 @@
1
+ import { useViewer } from '@pascal-app/viewer'
2
+ import { useEffect } from 'react'
3
+ import useEditor, { type CaptureMode } from '../store/use-editor'
4
+ import { useBelowGrade, useGroundWindowBox } from './below-grade'
5
+
6
+ /**
7
+ * Whether the editor shows every level. Preview and the walkthrough show the
8
+ * whole building; a scene capture shows what its level picker says (the
9
+ * whole building, or a level and the ones below it). A preset capture frames
10
+ * its isolated item and keeps the editing rule.
11
+ */
12
+ export function showsWholeBuilding(state: {
13
+ isPreviewMode: boolean
14
+ isFirstPersonMode: boolean
15
+ captureMode: CaptureMode
16
+ captureLevelId: string | null
17
+ }): boolean {
18
+ if (state.captureMode.mode === 'standard') return state.captureLevelId === null
19
+ return state.isPreviewMode || state.isFirstPersonMode
20
+ }
21
+
22
+ /**
23
+ * The editor's one level display: stacked, with every level above the
24
+ * selected one hidden — the selected level and the ones below stay, so the
25
+ * storey being worked on is never covered. There is no Stack / Exploded /
26
+ * Solo choice while editing; a mode left over from the viewer (or persisted
27
+ * by an older build) is set back to stacked, and kept there.
28
+ *
29
+ * The walkthrough shows the whole building, so the upper levels come back
30
+ * while it is on; Preview hands the display to the viewer's own floor rule
31
+ * (`useViewerFloorDisplay`). A level below grade is marked as shown, and its
32
+ * ground veiled, except on the site, where the ground is what is being worked on.
33
+ */
34
+ export function useEditorLevelDisplay(showWholeBuilding: boolean) {
35
+ const sitePhase = useEditor((state) => state.phase === 'site')
36
+ useBelowGrade(!(showWholeBuilding || sitePhase))
37
+ // The ground window (a hillside in the eye's way) follows the shown level, and the whole
38
+ // building while it stands whole — except on the site, where the ground is the work.
39
+ useGroundWindowBox(!sitePhase)
40
+ useEffect(() => {
41
+ if (showWholeBuilding) return
42
+ const keepStacked = (mode: string) => {
43
+ if (mode !== 'stacked') useViewer.getState().setLevelMode('stacked')
44
+ }
45
+ useViewer.setState({ hideLevelsAboveSelection: true })
46
+ keepStacked(useViewer.getState().levelMode)
47
+ const unsubscribe = useViewer.subscribe((state, previous) => {
48
+ if (state.levelMode !== previous.levelMode) keepStacked(state.levelMode)
49
+ })
50
+ return () => {
51
+ unsubscribe()
52
+ useViewer.setState({ hideLevelsAboveSelection: false })
53
+ }
54
+ }, [showWholeBuilding])
55
+ }