@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,173 @@
1
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test'
2
+ import type { AnyNodeDefinition } from '@pascal-app/core'
3
+ import { BaseNode, loadPlugin, nodeRegistry, nodeType, objectId, useScene } from '@pascal-app/core'
4
+ import { ANIMATION_SPEEDS } from '@pascal-app/viewer'
5
+ import { createJSONStorage } from 'zustand/middleware'
6
+ import useConstructionReveal, {
7
+ CONSTRUCTION_REVEAL_LEVELS,
8
+ CONSTRUCTION_REVEAL_STORAGE_KEY,
9
+ FRAMING_KIND,
10
+ getConstructionRevealLevel,
11
+ isFramingAvailable,
12
+ resolveConstructionLevel,
13
+ } from './use-construction-reveal'
14
+
15
+ function memoryStorage() {
16
+ const items = new Map<string, string>()
17
+ return {
18
+ items,
19
+ storage: createJSONStorage(() => ({
20
+ getItem: (key: string) => items.get(key) ?? null,
21
+ setItem: (key: string, value: string) => void items.set(key, value),
22
+ removeItem: (key: string) => void items.delete(key),
23
+ })),
24
+ }
25
+ }
26
+
27
+ afterEach(() => {
28
+ useConstructionReveal.setState({ level: 'full', chosen: false, speed: 1 })
29
+ })
30
+
31
+ describe('the construction animation preference', () => {
32
+ test('starts at full, with no choice of the person’s own, and offers four levels', () => {
33
+ expect(useConstructionReveal.getState().level).toBe('full')
34
+ expect(useConstructionReveal.getState().chosen).toBe(false)
35
+ expect(CONSTRUCTION_REVEAL_LEVELS).toEqual(['off', 'simple', 'full', 'framing'])
36
+ })
37
+
38
+ test('persists the chosen level and the speed under its own key', () => {
39
+ const { items, storage } = memoryStorage()
40
+ useConstructionReveal.persist.setOptions({ storage })
41
+ useConstructionReveal.getState().setLevel('simple')
42
+ expect(JSON.parse(items.get(CONSTRUCTION_REVEAL_STORAGE_KEY)!).state).toEqual({
43
+ level: 'simple',
44
+ chosen: true,
45
+ speed: 1,
46
+ })
47
+ expect(CONSTRUCTION_REVEAL_STORAGE_KEY).not.toBe('pascal-audio-settings')
48
+ })
49
+
50
+ test('restores a saved level, and an unknown one falls back to full', async () => {
51
+ const { items, storage } = memoryStorage()
52
+ useConstructionReveal.persist.setOptions({ storage })
53
+ items.set(CONSTRUCTION_REVEAL_STORAGE_KEY, JSON.stringify({ state: { level: 'off' } }))
54
+ await useConstructionReveal.persist.rehydrate()
55
+ expect(useConstructionReveal.getState().level).toBe('off')
56
+
57
+ items.set(CONSTRUCTION_REVEAL_STORAGE_KEY, JSON.stringify({ state: { level: 'cinematic' } }))
58
+ await useConstructionReveal.persist.rehydrate()
59
+ expect(useConstructionReveal.getState().level).toBe('full')
60
+ })
61
+ })
62
+
63
+ describe('the animation speed preference', () => {
64
+ test('starts at ×1', () => {
65
+ expect(useConstructionReveal.getState().speed).toBe(1)
66
+ })
67
+
68
+ test.each([...ANIMATION_SPEEDS])('×%d persists and reloads', async (speed) => {
69
+ const { items, storage } = memoryStorage()
70
+ useConstructionReveal.persist.setOptions({ storage })
71
+ useConstructionReveal.getState().setSpeed(speed)
72
+ const saved = items.get(CONSTRUCTION_REVEAL_STORAGE_KEY)!
73
+ expect(JSON.parse(saved).state.speed).toBe(speed)
74
+ // A new page: the store starts from its defaults and reads what was saved.
75
+ useConstructionReveal.setState({ speed: 1 })
76
+ items.set(CONSTRUCTION_REVEAL_STORAGE_KEY, saved)
77
+ await useConstructionReveal.persist.rehydrate()
78
+ expect(useConstructionReveal.getState().speed).toBe(speed)
79
+ })
80
+
81
+ test('a person saved before the setting is at ×1, and a stray value snaps to a step', async () => {
82
+ const { items, storage } = memoryStorage()
83
+ useConstructionReveal.persist.setOptions({ storage })
84
+ items.set(CONSTRUCTION_REVEAL_STORAGE_KEY, JSON.stringify({ state: { level: 'full' } }))
85
+ await useConstructionReveal.persist.rehydrate()
86
+ expect(useConstructionReveal.getState().speed).toBe(1)
87
+ items.set(CONSTRUCTION_REVEAL_STORAGE_KEY, JSON.stringify({ state: { speed: 1.6 } }))
88
+ await useConstructionReveal.persist.rehydrate()
89
+ expect(useConstructionReveal.getState().speed).toBe(1.5)
90
+ items.set(CONSTRUCTION_REVEAL_STORAGE_KEY, JSON.stringify({ state: { speed: 'fast' } }))
91
+ await useConstructionReveal.persist.rehydrate()
92
+ expect(useConstructionReveal.getState().speed).toBe(1)
93
+ })
94
+ })
95
+
96
+ describe('the level that plays follows the project', () => {
97
+ const none = { level: 'full', chosen: false } as const
98
+
99
+ test('with no choice of the person’s own: Framing with the framing plugin, Full without', () => {
100
+ expect(resolveConstructionLevel(none, true)).toBe('framing')
101
+ expect(resolveConstructionLevel(none, false)).toBe('full')
102
+ })
103
+
104
+ test('a choice is kept while the plugin stays installed', () => {
105
+ for (const level of CONSTRUCTION_REVEAL_LEVELS)
106
+ expect(resolveConstructionLevel({ level, chosen: true }, true)).toBe(level)
107
+ })
108
+
109
+ test('without the plugin Framing falls back to Full; every other choice stays', () => {
110
+ expect(resolveConstructionLevel({ level: 'framing', chosen: true }, false)).toBe('full')
111
+ expect(resolveConstructionLevel({ level: 'off', chosen: true }, false)).toBe('off')
112
+ expect(resolveConstructionLevel({ level: 'simple', chosen: true }, false)).toBe('simple')
113
+ // The saved choice is not erased: the plugin coming back brings Framing back.
114
+ expect(resolveConstructionLevel({ level: 'framing', chosen: true }, true)).toBe('framing')
115
+ })
116
+
117
+ test('a level saved before the default followed the project is a choice', async () => {
118
+ const { items, storage } = memoryStorage()
119
+ useConstructionReveal.persist.setOptions({ storage })
120
+ items.set(CONSTRUCTION_REVEAL_STORAGE_KEY, JSON.stringify({ state: { level: 'full' } }))
121
+ await useConstructionReveal.persist.rehydrate()
122
+ expect(useConstructionReveal.getState().chosen).toBe(true)
123
+ items.set(CONSTRUCTION_REVEAL_STORAGE_KEY, JSON.stringify({ state: {} }))
124
+ await useConstructionReveal.persist.rehydrate()
125
+ expect(useConstructionReveal.getState().chosen).toBe(false)
126
+ })
127
+ })
128
+
129
+ describe('the framing plugin of a project', () => {
130
+ const framing = {
131
+ kind: FRAMING_KIND,
132
+ schemaVersion: 1,
133
+ schema: BaseNode.extend({ id: objectId('framing'), type: nodeType(FRAMING_KIND) }),
134
+ category: 'structure',
135
+ defaults: () => ({}),
136
+ capabilities: {},
137
+ geometry: () => null,
138
+ } as unknown as AnyNodeDefinition
139
+ let restore: () => void = () => {}
140
+ const wasInstalled = useScene.getState().installedPlugins
141
+
142
+ beforeAll(async () => {
143
+ restore = nodeRegistry._snapshot()
144
+ nodeRegistry._reset()
145
+ await loadPlugin({ id: 'fixture:framing', apiVersion: 1, nodes: [framing] })
146
+ })
147
+ afterAll(() => {
148
+ restore()
149
+ useScene.setState({ installedPlugins: wasInstalled })
150
+ })
151
+
152
+ test('is available where the plugin that carries the kind is installed', () => {
153
+ expect(isFramingAvailable([])).toBe(false)
154
+ expect(isFramingAvailable(['fixture:other'])).toBe(false)
155
+ expect(isFramingAvailable(['fixture:framing'])).toBe(true)
156
+ })
157
+
158
+ test('is not available where no loaded plugin carries the kind at all', () => {
159
+ const snapshot = nodeRegistry._snapshot()
160
+ nodeRegistry._reset()
161
+ expect(isFramingAvailable(['fixture:framing'])).toBe(false)
162
+ snapshot()
163
+ })
164
+
165
+ test('the level that plays is read from the open project', () => {
166
+ useScene.setState({ installedPlugins: [] })
167
+ expect(getConstructionRevealLevel()).toBe('full')
168
+ useScene.setState({ installedPlugins: ['fixture:framing'] })
169
+ expect(getConstructionRevealLevel()).toBe('framing')
170
+ useConstructionReveal.getState().setLevel('full')
171
+ expect(getConstructionRevealLevel()).toBe('full')
172
+ })
173
+ })
@@ -0,0 +1,145 @@
1
+ 'use client'
2
+
3
+ import { isNodeKindEnabled, nodeRegistry, useRegistryVersion, useScene } from '@pascal-app/core'
4
+ import {
5
+ type AnimationSpeed,
6
+ type ConstructionRevealLevel,
7
+ DEFAULT_ANIMATION_SPEED,
8
+ normalizeAnimationSpeed,
9
+ } from '@pascal-app/viewer'
10
+ import { create } from 'zustand'
11
+ import { createJSONStorage, persist } from 'zustand/middleware'
12
+
13
+ export const CONSTRUCTION_REVEAL_LEVELS: readonly ConstructionRevealLevel[] = [
14
+ 'off',
15
+ 'simple',
16
+ 'full',
17
+ 'framing',
18
+ ]
19
+
20
+ export const CONSTRUCTION_REVEAL_STORAGE_KEY = 'pascal-construction-reveal'
21
+
22
+ /**
23
+ * The kind the Framing level draws: it plays only in a project where this kind is registered and
24
+ * its plugin is installed. Read through the kind, not a plugin id, so it holds wherever the framing
25
+ * kinds live (the Bones plugin, or the procedural plugin that carries them).
26
+ */
27
+ export const FRAMING_KIND = 'bones:framing'
28
+
29
+ const DEFAULT_LEVEL: ConstructionRevealLevel = 'full'
30
+
31
+ function normalizeLevel(value: unknown): ConstructionRevealLevel {
32
+ return CONSTRUCTION_REVEAL_LEVELS.includes(value as ConstructionRevealLevel)
33
+ ? (value as ConstructionRevealLevel)
34
+ : DEFAULT_LEVEL
35
+ }
36
+
37
+ /** Reads `localStorage` on each call, so a page without it (a test, a sandbox) still has the store's persist API. */
38
+ const browserStorage = createJSONStorage(() => ({
39
+ getItem: (key: string) => globalThis.localStorage?.getItem(key) ?? null,
40
+ setItem: (key: string, value: string) => globalThis.localStorage?.setItem(key, value),
41
+ removeItem: (key: string) => globalThis.localStorage?.removeItem(key),
42
+ }))
43
+
44
+ type ConstructionRevealState = {
45
+ /**
46
+ * The level the person last chose (see `chosen`). Hosts read the level that plays through
47
+ * `useConstructionRevealLevel`, which also knows whether the framing plugin is installed.
48
+ */
49
+ level: ConstructionRevealLevel
50
+ /** Whether the person has chosen a level themselves; until then the default follows the project. */
51
+ chosen: boolean
52
+ setLevel: (level: ConstructionRevealLevel) => void
53
+ /**
54
+ * How fast builds play, a multiple of the shipped pace (×0.5 to ×2, higher is faster); the host
55
+ * passes it to `ConstructionReveal`. A change applies to the next build, never to one playing.
56
+ */
57
+ speed: AnimationSpeed
58
+ setSpeed: (speed: AnimationSpeed) => void
59
+ }
60
+
61
+ /** The person's construction-animation preferences (level and speed), kept across sessions. */
62
+ const useConstructionReveal = create<ConstructionRevealState>()(
63
+ persist(
64
+ (set) => ({
65
+ level: DEFAULT_LEVEL,
66
+ chosen: false,
67
+ setLevel: (level) => set({ level, chosen: true }),
68
+ speed: DEFAULT_ANIMATION_SPEED,
69
+ setSpeed: (speed) => set({ speed: normalizeAnimationSpeed(speed) }),
70
+ }),
71
+ {
72
+ name: CONSTRUCTION_REVEAL_STORAGE_KEY,
73
+ storage: browserStorage,
74
+ partialize: (state) => ({ level: state.level, chosen: state.chosen, speed: state.speed }),
75
+ merge: (persisted, current) => {
76
+ const saved = persisted as
77
+ | { level?: unknown; chosen?: unknown; speed?: unknown }
78
+ | undefined
79
+ return {
80
+ ...current,
81
+ level: normalizeLevel(saved?.level),
82
+ // A level saved before the default followed the project was the person's own choice.
83
+ chosen: typeof saved?.chosen === 'boolean' ? saved.chosen : saved?.level !== undefined,
84
+ // A person saved before the setting existed is at ×1; a stray value snaps to a step.
85
+ speed: normalizeAnimationSpeed(saved?.speed),
86
+ }
87
+ },
88
+ },
89
+ ),
90
+ )
91
+
92
+ /**
93
+ * The level that plays. With no choice of the person's own it is Framing where the framing plugin
94
+ * is available in the project and Full where it is not; a choice is kept, except that Framing falls
95
+ * back to Full while the plugin is missing (the choice comes back with the plugin).
96
+ */
97
+ export function resolveConstructionLevel(
98
+ saved: Pick<ConstructionRevealState, 'level' | 'chosen'>,
99
+ framingInstalled: boolean,
100
+ ): ConstructionRevealLevel {
101
+ if (!saved.chosen) return framingInstalled ? 'framing' : 'full'
102
+ return saved.level === 'framing' && !framingInstalled ? 'full' : saved.level
103
+ }
104
+
105
+ /**
106
+ * A click on a level in the control. The level that plays is not always the saved one (a saved Framing
107
+ * plays as Full while the plugin is gone), and a click on that pressed level is not a choice: it keeps
108
+ * the saved one, so it plays again with the plugin.
109
+ */
110
+ export function chooseConstructionLevel(
111
+ level: ConstructionRevealLevel,
112
+ framingInstalled: boolean,
113
+ ): void {
114
+ const state = useConstructionReveal.getState()
115
+ if (state.chosen && resolveConstructionLevel(state, framingInstalled) === level) return
116
+ state.setLevel(level)
117
+ }
118
+
119
+ /** Whether the project has what draws the framing: the kind is registered and its plugin installed. */
120
+ export function isFramingAvailable(installedPlugins: readonly string[]): boolean {
121
+ return nodeRegistry.has(FRAMING_KIND) && isNodeKindEnabled(FRAMING_KIND, installedPlugins)
122
+ }
123
+
124
+ /** The same, for a component: it re-reads when a plugin registers after the first render. */
125
+ export function useFramingAvailable(): boolean {
126
+ useRegistryVersion()
127
+ return useScene((state) => isFramingAvailable(state.installedPlugins))
128
+ }
129
+
130
+ /** The level that plays now, for a component: the person's choice or the project's default. */
131
+ export function useConstructionRevealLevel(): ConstructionRevealLevel {
132
+ const level = useConstructionReveal((state) => state.level)
133
+ const chosen = useConstructionReveal((state) => state.chosen)
134
+ return resolveConstructionLevel({ level, chosen }, useFramingAvailable())
135
+ }
136
+
137
+ /** The level that plays now, read once (a tool's executor, not a component). */
138
+ export function getConstructionRevealLevel(): ConstructionRevealLevel {
139
+ return resolveConstructionLevel(
140
+ useConstructionReveal.getState(),
141
+ isFramingAvailable(useScene.getState().installedPlugins),
142
+ )
143
+ }
144
+
145
+ export default useConstructionReveal
@@ -0,0 +1,24 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { CORNER_BLOCK_NOTES, cornerHintRows } from './use-corner-snap-hint'
3
+
4
+ // The HUD near a wall end: what a click does where a window can join, and why not where it cannot,
5
+ // in one wording for the placement HUD and the move HUD.
6
+ describe('cornerHintRows', () => {
7
+ test('says nothing away from a wall end', () => {
8
+ expect(cornerHintRows(null)).toEqual([])
9
+ })
10
+
11
+ test('says what a click does where the window can join, and how to skip it with Alt', () => {
12
+ for (const hint of ['join', 'new'] as const)
13
+ expect(cornerHintRows(hint).map((row) => row.keys)).toEqual([['Left click'], ['Alt']])
14
+ expect(cornerHintRows('join')[0]?.label).not.toBe(cornerHintRows('new')[0]?.label)
15
+ })
16
+
17
+ test('gives each reason its own sentence where the end is no corner, and offers no click', () => {
18
+ const reasons = Object.keys(CORNER_BLOCK_NOTES) as (keyof typeof CORNER_BLOCK_NOTES)[]
19
+ const rows = reasons.map((reason) => cornerHintRows(reason))
20
+
21
+ expect(new Set(rows.map((row) => row[0]?.label)).size).toBe(reasons.length)
22
+ expect(rows.flat().every((row) => !row.keys.includes('Left click'))).toBe(true)
23
+ })
24
+ })
@@ -0,0 +1,47 @@
1
+ import type { CornerBlockReason } from '@pascal-app/core'
2
+ import { create } from 'zustand'
3
+
4
+ /**
5
+ * What the window being placed or dragged near a wall end says: `join` when a window already waits
6
+ * on the other wall, `new` when one would be created, the reason when the end is not a corner a
7
+ * window can wrap, null away from a wall end. The tools write it; the HUD reads it, so the hint is
8
+ * there only inside the zone.
9
+ */
10
+ export type CornerSnapHint = 'join' | 'new' | CornerBlockReason | null
11
+
12
+ export const useCornerSnapHint = create<{
13
+ hint: CornerSnapHint
14
+ setHint: (hint: CornerSnapHint) => void
15
+ }>((set) => ({
16
+ hint: null,
17
+ setHint: (hint) => set((state) => (state.hint === hint ? state : { hint })),
18
+ }))
19
+
20
+ /** One line each, in the HUD's voice: what is in the way, not what to do about it. */
21
+ export const CORNER_BLOCK_NOTES: Record<CornerBlockReason, string> = {
22
+ free_end: 'No wall meets here',
23
+ several_walls: 'Three or more walls meet here',
24
+ angle: 'The walls meet at too sharp or too flat an angle',
25
+ curved: 'A curved wall takes no corner window',
26
+ curtain: 'A curtain wall takes no corner window',
27
+ no_room: 'No room for the matching window on the other wall',
28
+ in_the_way: 'Another opening is in the way at the corner',
29
+ too_tall: 'The window waiting there is taller than this wall',
30
+ }
31
+
32
+ export type CornerHintRow = { keys: string[]; label: string }
33
+
34
+ /** The HUD rows for a hint, shared by the placement tool and the move HUD. */
35
+ export function cornerHintRows(hint: CornerSnapHint): CornerHintRow[] {
36
+ if (!hint) return []
37
+ if (hint === 'join' || hint === 'new')
38
+ return [
39
+ {
40
+ keys: ['Left click'],
41
+ label: hint === 'join' ? 'Join as a corner window' : 'Corner window',
42
+ },
43
+ { keys: ['Alt'], label: 'Place without the corner snap' },
44
+ ]
45
+ // The HUD's notice row, as for a placement that does not fit: a note, not a key to press.
46
+ return [{ keys: ['!'], label: CORNER_BLOCK_NOTES[hint] }]
47
+ }
@@ -1,6 +1,33 @@
1
+ import type { DeleteZonePayload } from '@pascal-app/core'
1
2
  import { create } from 'zustand'
2
3
 
3
- type DeleteConfirmationRequest = {
4
+ /** Changing one of a room's construction parts, with what it takes along. */
5
+ export type RoomConstructionChange = {
6
+ part: 'floor' | 'walls' | 'ceiling'
7
+ /** Walls can be confirmed on the way in too; everything else asks on removal. */
8
+ action?: 'add' | 'remove'
9
+ roomName: string
10
+ /**
11
+ * Walls: hosted openings and objects removed with them. Ceiling: items
12
+ * hanging from it. Floor: items standing on it.
13
+ */
14
+ hostedIds: string[]
15
+ /** Walls turned into separators, or built on them. */
16
+ wallCount?: number
17
+ sharedWalls?: boolean
18
+ /** Walls added: clear floor area the room gives up to them, m². */
19
+ areaLoss?: number
20
+ /** Ceiling: names of the hand-drawn ceilings removed ('' when unnamed). */
21
+ manualCeilings?: string[]
22
+ /** Ceiling: other rooms those hand-drawn ceilings also cover. */
23
+ alsoCovers?: string[]
24
+ }
25
+
26
+ export type DeleteConfirmationRequest = {
27
+ room?: DeleteZonePayload
28
+ construction?: RoomConstructionChange
29
+ conflict?: string
30
+ onKeepContents?: () => void
4
31
  count: number
5
32
  onConfirm: () => void
6
33
  }
@@ -1,10 +1,31 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import useDrawingView, { normalizeAnnotationLayoutOverrides } from './use-drawing-view'
2
+ import useDrawingView, {
3
+ EDITOR_DRAWING_TYPE_OPTIONS,
4
+ normalizeAnnotationLayoutOverrides,
5
+ normalizeEditorDrawingType,
6
+ } from './use-drawing-view'
3
7
 
4
8
  describe('drawing type', () => {
5
- test('keeps the workspace on the floor plan', () => {
9
+ test('starts on the floor plan', () => {
6
10
  expect(useDrawingView.getState().drawingType).toBe('floor-plan')
7
- expect('setDrawingType' in useDrawingView.getState()).toBe(false)
11
+ })
12
+
13
+ test('switches between the drawing types the editor can render', () => {
14
+ useDrawingView.getState().setDrawingType('site-plan')
15
+ expect(useDrawingView.getState().drawingType).toBe('site-plan')
16
+ useDrawingView.getState().setDrawingType('floor-plan')
17
+ expect(useDrawingView.getState().drawingType).toBe('floor-plan')
18
+ })
19
+
20
+ test('only offers the two types that have a renderer', () => {
21
+ expect(EDITOR_DRAWING_TYPE_OPTIONS.map((o) => o.id)).toEqual(['floor-plan', 'site-plan'])
22
+ })
23
+
24
+ test('a persisted value the editor cannot render falls back to the floor plan', () => {
25
+ expect(normalizeEditorDrawingType('site-plan')).toBe('site-plan')
26
+ expect(normalizeEditorDrawingType('roof-plan')).toBe('floor-plan')
27
+ expect(normalizeEditorDrawingType(undefined)).toBe('floor-plan')
28
+ expect(normalizeEditorDrawingType(42)).toBe('floor-plan')
8
29
  })
9
30
  })
10
31
 
@@ -20,8 +20,24 @@ export type DrawingAnnotationLayoutOverride = {
20
20
 
21
21
  export type DrawingAnnotationLayoutOverrides = Record<string, DrawingAnnotationLayoutOverride>
22
22
 
23
+ /**
24
+ * Drawing types the 2D editor can actually render today. `floor-plan` is the
25
+ * default; `site-plan` renders through
26
+ * `lib/floorplan/site-plan/buildSitePlanDrawing`. The remaining
27
+ * `ConstructionDrawingType` members exist on annotations (they gate dimension
28
+ * visibility) but have no editor renderer yet, so the switch does not offer
29
+ * them — widen this union as each one lands.
30
+ */
31
+ export type EditorDrawingType = Extract<ConstructionDrawingType, 'floor-plan' | 'site-plan'>
32
+
33
+ export const EDITOR_DRAWING_TYPE_OPTIONS = [
34
+ { id: 'floor-plan', label: 'Floor plan' },
35
+ { id: 'site-plan', label: 'Site plan' },
36
+ ] as const satisfies readonly { id: EditorDrawingType; label: string }[]
37
+
23
38
  type DrawingViewState = {
24
- drawingType: Extract<ConstructionDrawingType, 'floor-plan'>
39
+ drawingType: EditorDrawingType
40
+ setDrawingType: (drawingType: EditorDrawingType) => void
25
41
  annotationLayoutOverrides: DrawingAnnotationLayoutOverrides
26
42
  setAnnotationLayoutOverride: (
27
43
  id: string,
@@ -52,10 +68,18 @@ export function normalizeAnnotationLayoutOverrides(
52
68
  return out
53
69
  }
54
70
 
71
+ /** Persisted value → a drawing type the editor can render. Unknown → floor-plan. */
72
+ export function normalizeEditorDrawingType(value: unknown): EditorDrawingType {
73
+ return EDITOR_DRAWING_TYPE_OPTIONS.some((o) => o.id === value)
74
+ ? (value as EditorDrawingType)
75
+ : 'floor-plan'
76
+ }
77
+
55
78
  const useDrawingView = create<DrawingViewState>()(
56
79
  persist(
57
80
  (set) => ({
58
81
  drawingType: 'floor-plan',
82
+ setDrawingType: (drawingType) => set({ drawingType }),
59
83
  annotationLayoutOverrides: {},
60
84
  setAnnotationLayoutOverride: (id, override) =>
61
85
  set((state) => {
@@ -69,12 +93,16 @@ const useDrawingView = create<DrawingViewState>()(
69
93
  name: 'pascal-floorplan-drawing-view',
70
94
  merge: (persistedState, currentState) => ({
71
95
  ...currentState,
96
+ drawingType: normalizeEditorDrawingType(
97
+ (persistedState as { drawingType?: unknown } | undefined)?.drawingType,
98
+ ),
72
99
  annotationLayoutOverrides: normalizeAnnotationLayoutOverrides(
73
100
  (persistedState as { annotationLayoutOverrides?: unknown } | undefined)
74
101
  ?.annotationLayoutOverrides,
75
102
  ),
76
103
  }),
77
104
  partialize: (state) => ({
105
+ drawingType: state.drawingType,
78
106
  annotationLayoutOverrides: state.annotationLayoutOverrides,
79
107
  }),
80
108
  },