@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
@@ -1,20 +1,194 @@
1
1
  'use client'
2
2
 
3
+ import { type AnyNodeId, sceneRegistry, useInteractive, useScene } from '@pascal-app/core'
4
+ import { evaluateRecipe } from '@pascal-app/core/procedural-items'
3
5
  import {
4
6
  computeHeroFraming,
5
7
  createSnapshotPipeline,
6
8
  GRID_LAYER,
9
+ type HeroFraming,
7
10
  heroCameraPose,
11
+ proceduralSlotMeshes,
12
+ setKeyLightDirectionOverride,
13
+ setProceduralEmission,
14
+ setSlotDefaultOverrides,
15
+ siteSurfaces,
8
16
  temporarilyHideNodeTypes,
17
+ temporarilyWithoutAmbiance,
9
18
  useSceneAtmosphere,
10
19
  useViewer,
11
20
  } from '@pascal-app/viewer'
12
21
  import { useThree } from '@react-three/fiber'
13
22
  import { useEffect, useRef } from 'react'
14
- import { PerspectiveCamera } from 'three'
15
- import type { WebGPURenderer } from 'three/webgpu'
23
+ import {
24
+ Box3,
25
+ CanvasTexture,
26
+ type Material,
27
+ Mesh,
28
+ type Object3D,
29
+ PerspectiveCamera,
30
+ PlaneGeometry,
31
+ Vector3,
32
+ } from 'three'
33
+ import { MeshBasicNodeMaterial, ShadowNodeMaterial, type WebGPURenderer } from 'three/webgpu'
16
34
  import { EDITOR_LAYER } from '../../lib/constants'
17
35
 
36
+ // The published-thumbnail look. One look for every project, independent of
37
+ // the scene's own display preferences.
38
+ const THUMBNAIL_FOV_DEG = 40
39
+ const THUMBNAIL_PADDING = 1.12
40
+ /** Roofless scenes look down into the rooms; roofed ones show the roofline. */
41
+ const THUMBNAIL_ELEVATION_DEG = { roofed: 24, roofless: 35 }
42
+ /** Key light relative to the hero camera: 30° off its azimuth, 45° up — both
43
+ * facades lit, shadows raking across the ground. */
44
+ const THUMBNAIL_KEY_AZIMUTH_OFFSET_DEG = 30
45
+ const THUMBNAIL_KEY_ELEVATION_DEG = 45
46
+ const THUMBNAIL_BACKDROP = { inner: '#FAFAFA', outer: '#E4E4E4' }
47
+ const THUMBNAIL_SHADOW_CATCHER_OPACITY = 0.7
48
+ /** Soft shade hugging the ground-floor walls so the building never floats. */
49
+ const THUMBNAIL_WALL_CONTACT = { opacity: 0.5, spreadM: 0.9 }
50
+ /** Unpainted walls render as smooth plaster rather than the screw-dotted
51
+ * prepared-drywall default, which reads as unfinished at card size. */
52
+ const THUMBNAIL_SLOT_DEFAULT_OVERRIDES = {
53
+ 'library:concrete-drywall': 'library:concrete-plaster',
54
+ } as const
55
+ // Past the capture camera's far plane (1000): a catcher edge inside the frame
56
+ // leaves an SSGI AO line along its depth discontinuity.
57
+ const SHADOW_CATCHER_EXTENT = 4000
58
+
59
+ /**
60
+ * Forces the published-thumbnail look on the bake page. Call before the scene
61
+ * builds (the finish swap applies as materials are created); the returned
62
+ * cleanup restores the declared finishes.
63
+ */
64
+ export function prepareBakeThumbnailLook(): () => void {
65
+ const viewer = useViewer.getState()
66
+ viewer.setSceneTheme('studio')
67
+ viewer.setEdges('soft')
68
+ viewer.setShadows(true)
69
+ setSlotDefaultOverrides(THUMBNAIL_SLOT_DEFAULT_OVERRIDES)
70
+ return () => setSlotDefaultOverrides(null)
71
+ }
72
+
73
+ // Scene-ready fires before every system has finished its geometry: roofs
74
+ // rebuild their merged mesh a few frames later (measured: Starter House ridge
75
+ // 5.75 m at scene-ready, 6.24 m at capture), so a pose fit at scene-ready
76
+ // clips the roof. Re-measure until the framing bounds hold still.
77
+ async function settledHeroFraming() {
78
+ const boundsKey = (framing: HeroFraming | null) => {
79
+ if (!framing) return ''
80
+ const union = new Box3()
81
+ for (const box of framing.boxes) union.union(box)
82
+ return [...union.min.toArray(), ...union.max.toArray()].map((v) => v.toFixed(3)).join(',')
83
+ }
84
+ let framing = computeHeroFraming({ sitePlates: false })
85
+ let previous = boundsKey(framing)
86
+ let stableChecks = 0
87
+ for (let i = 0; i < 40 && stableChecks < 3; i++) {
88
+ await new Promise<void>((resolve) => setTimeout(resolve, 100))
89
+ framing = computeHeroFraming({ sitePlates: false })
90
+ const next = boundsKey(framing)
91
+ stableChecks = next === previous ? stableChecks + 1 : 0
92
+ previous = next
93
+ }
94
+ return framing
95
+ }
96
+
97
+ /**
98
+ * A soft contact shade on the ground along every ground-floor wall: the wall
99
+ * centrelines stroked onto a canvas in the building's facade axes, then
100
+ * blurred. Per wall rather than per footprint, so a walled lot shades its
101
+ * boundary walls and leaves the open courtyard clean.
102
+ */
103
+ function wallContactShade(yaw: number, groundY: number): Mesh | null {
104
+ const u = new Vector3(Math.cos(yaw), 0, Math.sin(yaw))
105
+ const v = new Vector3(-Math.sin(yaw), 0, Math.cos(yaw))
106
+ const segments: Array<{ a: [number, number]; b: [number, number]; thickness: number }> = []
107
+ const point = new Vector3()
108
+ // A wall mesh sits at its start, rotated along the run: `start`/`end` are
109
+ // in its parent's (the level's) frame.
110
+ const toUv = (x: number, z: number, object: Object3D): [number, number] => {
111
+ point.set(x, 0, z)
112
+ if (object.parent) point.applyMatrix4(object.parent.matrixWorld)
113
+ return [point.dot(u), point.dot(v)]
114
+ }
115
+ for (const wallId of sceneRegistry.byType.wall ?? []) {
116
+ const object = sceneRegistry.nodes.get(wallId)
117
+ const node = useScene.getState().nodes[wallId as AnyNodeId]
118
+ if (!(object && node?.type === 'wall' && node.visible !== false)) continue
119
+ const bounds = new Box3().setFromObject(object)
120
+ if (bounds.isEmpty() || bounds.min.y > groundY + 0.5) continue
121
+ segments.push({
122
+ a: toUv(node.start[0], node.start[1], object),
123
+ b: toUv(node.end[0], node.end[1], object),
124
+ thickness: node.thickness ?? 0.2,
125
+ })
126
+ }
127
+ if (segments.length === 0) return null
128
+
129
+ const { spreadM, opacity } = THUMBNAIL_WALL_CONTACT
130
+ let uMin = Number.POSITIVE_INFINITY
131
+ let uMax = Number.NEGATIVE_INFINITY
132
+ let vMin = Number.POSITIVE_INFINITY
133
+ let vMax = Number.NEGATIVE_INFINITY
134
+ for (const { a, b } of segments) {
135
+ uMin = Math.min(uMin, a[0], b[0])
136
+ uMax = Math.max(uMax, a[0], b[0])
137
+ vMin = Math.min(vMin, a[1], b[1])
138
+ vMax = Math.max(vMax, a[1], b[1])
139
+ }
140
+ const margin = spreadM * 2
141
+ const width = uMax - uMin + margin * 2
142
+ const depth = vMax - vMin + margin * 2
143
+ const scale = 1024 / Math.max(width, depth)
144
+ const canvas = document.createElement('canvas')
145
+ canvas.width = Math.max(8, Math.round(width * scale))
146
+ canvas.height = Math.max(8, Math.round(depth * scale))
147
+ const ctx = canvas.getContext('2d')
148
+ if (!ctx) return null
149
+ // alphaMap samples the green channel: white strokes on opaque black.
150
+ ctx.fillStyle = '#000'
151
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
152
+ ctx.filter = `blur(${Math.max(2, spreadM * scale * 0.35)}px)`
153
+ ctx.strokeStyle = '#fff'
154
+ ctx.lineCap = 'square'
155
+ for (const { a, b, thickness } of segments) {
156
+ ctx.lineWidth = (thickness + spreadM * 0.6) * scale
157
+ ctx.beginPath()
158
+ ctx.moveTo((a[0] - uMin + margin) * scale, (a[1] - vMin + margin) * scale)
159
+ ctx.lineTo((b[0] - uMin + margin) * scale, (b[1] - vMin + margin) * scale)
160
+ ctx.stroke()
161
+ }
162
+
163
+ const shade = new Mesh(
164
+ new PlaneGeometry(width, depth),
165
+ new MeshBasicNodeMaterial({
166
+ color: '#000000',
167
+ alphaMap: new CanvasTexture(canvas),
168
+ transparent: true,
169
+ depthWrite: false,
170
+ opacity,
171
+ }),
172
+ )
173
+ // Plane x → u; plane y → −v, so the canvas's downward rows run along +v.
174
+ shade.rotation.set(-Math.PI / 2, 0, -yaw)
175
+ shade.position
176
+ .copy(u)
177
+ .multiplyScalar((uMin + uMax) / 2)
178
+ .addScaledVector(v, (vMin + vMax) / 2)
179
+ .setY(groundY - 0.005)
180
+ shade.renderOrder = -1
181
+ return shade
182
+ }
183
+
184
+ function disposeMesh(mesh: Mesh) {
185
+ mesh.removeFromParent()
186
+ mesh.geometry.dispose()
187
+ const material = mesh.material as MeshBasicNodeMaterial
188
+ material.alphaMap?.dispose()
189
+ material.dispose()
190
+ }
191
+
18
192
  export function BakeThumbnail({
19
193
  active,
20
194
  onComplete,
@@ -35,10 +209,39 @@ export function BakeThumbnail({
35
209
 
36
210
  const run = async () => {
37
211
  const restoreNodeVisibility = temporarilyHideNodeTypes(['scan', 'guide', 'spawn'])
212
+ // The hero is the building in the world's own light, wherever the person's eye is.
213
+ const restoreAmbiance = temporarilyWithoutAmbiance()
214
+ const priorLights = new Map<string, boolean>()
215
+ const restGroups = new Map<string, [number, number, number]>()
216
+ for (const node of Object.values(useScene.getState().nodes)) {
217
+ if (node.type !== 'procedural-item') continue
218
+ const evaluation = evaluateRecipe(node.recipe, node.parameters)
219
+ if (evaluation.lights.length) {
220
+ priorLights.set(
221
+ node.id,
222
+ useInteractive.getState().procedural[node.id]?.lightsOn ??
223
+ useInteractive.getState().lampDefault,
224
+ )
225
+ useInteractive.getState().setProceduralLights(node.id, true)
226
+ }
227
+ for (const motion of evaluation.motions)
228
+ restGroups.set(`${node.id}:${motion.id}`, motion.pivot)
229
+ }
230
+ const transforms: Array<{
231
+ object: import('three').Object3D
232
+ position: import('three').Vector3
233
+ rotation: import('three').Euler
234
+ }> = []
235
+ const emission = new Map<Material, number>()
38
236
  let pipeline: Awaited<ReturnType<typeof createSnapshotPipeline>> = null
237
+ // The studio backdrop replaces the site's ground and horizon: a shadow
238
+ // catcher keeps cast shadows, the wall shade keeps contact. Items the
239
+ // framing leaves out are hidden so what's framed is what's shown.
240
+ const hidden: Object3D[] = []
241
+ const added: Mesh[] = []
39
242
 
40
243
  try {
41
- const framing = computeHeroFraming()
244
+ const framing = await settledHeroFraming()
42
245
  if (!framing) {
43
246
  onError('scene has no framable content')
44
247
  return
@@ -46,24 +249,73 @@ export function BakeThumbnail({
46
249
 
47
250
  const { width, height } = renderer.domElement
48
251
  const aspect = width / height
49
- const camera = new PerspectiveCamera(60, aspect, 0.1, 1000)
252
+ const camera = new PerspectiveCamera(THUMBNAIL_FOV_DEG, aspect, 0.1, 1000)
50
253
  camera.layers.disable(EDITOR_LAYER)
51
254
  camera.layers.disable(GRID_LAYER)
255
+ const hasRoof = (sceneRegistry.byType.roof?.size ?? 0) > 0
256
+ const elevationDeg = hasRoof
257
+ ? THUMBNAIL_ELEVATION_DEG.roofed
258
+ : THUMBNAIL_ELEVATION_DEG.roofless
52
259
  const pose = heroCameraPose({
53
260
  boxes: framing.boxes,
54
261
  aim: framing.aim,
55
262
  azimuthRad: framing.azimuthRad,
56
263
  aspect,
264
+ fovDeg: THUMBNAIL_FOV_DEG,
265
+ elevationRad: (elevationDeg * Math.PI) / 180,
266
+ padding: THUMBNAIL_PADDING,
267
+ tightFit: true,
57
268
  })
58
269
  camera.position.set(pose.position[0], pose.position[1], pose.position[2])
59
270
  camera.lookAt(pose.target[0], pose.target[1], pose.target[2])
60
271
  camera.updateMatrixWorld()
61
272
 
273
+ const keyAzimuth = framing.azimuthRad + (THUMBNAIL_KEY_AZIMUTH_OFFSET_DEG * Math.PI) / 180
274
+ const keyElevation = (THUMBNAIL_KEY_ELEVATION_DEG * Math.PI) / 180
275
+ setKeyLightDirectionOverride([
276
+ Math.sin(keyAzimuth) * Math.cos(keyElevation),
277
+ Math.sin(keyElevation),
278
+ Math.cos(keyAzimuth) * Math.cos(keyElevation),
279
+ ])
280
+
281
+ for (const object of [
282
+ ...siteSurfaces(),
283
+ ...framing.detachedItemIds.flatMap((id) => sceneRegistry.nodes.get(id) ?? []),
284
+ ]) {
285
+ if (!object.visible) continue
286
+ object.visible = false
287
+ hidden.push(object)
288
+ }
289
+ const union = new Box3()
290
+ for (const box of framing.boxes) union.union(box)
291
+ const groundY = Math.max(0, union.min.y)
292
+ const catcher = new Mesh(
293
+ new PlaneGeometry(SHADOW_CATCHER_EXTENT, SHADOW_CATCHER_EXTENT),
294
+ new ShadowNodeMaterial({ opacity: THUMBNAIL_SHADOW_CATCHER_OPACITY }),
295
+ )
296
+ catcher.rotation.x = -Math.PI / 2
297
+ catcher.position.set(framing.aim[0], groundY - 0.01, framing.aim[2])
298
+ catcher.receiveShadow = true
299
+ added.push(catcher)
300
+ const contact = wallContactShade(Math.PI / 4 - framing.azimuthRad, groundY)
301
+ if (contact) added.push(contact)
302
+ for (const mesh of added) scene.add(mesh)
303
+
304
+ // Backdrop centred on the building's mid-height, where it's lightest.
305
+ const center = new Vector3(
306
+ framing.aim[0],
307
+ (groundY + framing.aim[1]) / 2,
308
+ framing.aim[2],
309
+ ).project(camera)
62
310
  pipeline = await createSnapshotPipeline({
63
311
  renderer: renderer as unknown as WebGPURenderer,
64
312
  scene,
65
313
  camera,
66
314
  atmosphere,
315
+ studioBackdrop: {
316
+ ...THUMBNAIL_BACKDROP,
317
+ center: [(center.x + 1) / 2, (1 - center.y) / 2],
318
+ },
67
319
  })
68
320
  if (!pipeline) {
69
321
  onError('thumbnail pipeline failed to build')
@@ -71,12 +323,47 @@ export function BakeThumbnail({
71
323
  }
72
324
 
73
325
  pipeline.applyEnvironment({
74
- theme: useViewer.getState().sceneTheme,
326
+ theme: useViewer.getState().sceneAppearance,
75
327
  transparent: false,
76
328
  grade: true,
77
329
  edges: useViewer.getState().edges,
78
330
  camera,
79
331
  })
332
+ await new Promise<void>((resolve) => setTimeout(resolve, 250))
333
+ for (const node of Object.values(useScene.getState().nodes)) {
334
+ if (node.type !== 'procedural-item') continue
335
+ const slots = new Set(
336
+ evaluateRecipe(node.recipe, node.parameters).lights.flatMap((light) =>
337
+ light.emissiveSlot ? [light.emissiveSlot] : [],
338
+ ),
339
+ )
340
+ const object = sceneRegistry.nodes.get(node.id)
341
+ for (const mesh of object ? proceduralSlotMeshes(object, slots) : []) {
342
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
343
+ for (const material of materials) {
344
+ const current = material as Material & { emissiveIntensity?: number }
345
+ if (typeof current.emissiveIntensity !== 'number') continue
346
+ emission.set(material, current.emissiveIntensity)
347
+ setProceduralEmission(material, true)
348
+ }
349
+ }
350
+ }
351
+ scene.traverse((object) => {
352
+ const motion = object.userData.proceduralMotion as
353
+ | { nodeId?: string; groupId?: string }
354
+ | undefined
355
+ if (!motion) return
356
+ const pivot = restGroups.get(`${motion.nodeId}:${motion.groupId}`)
357
+ if (!pivot) return
358
+ transforms.push({
359
+ object,
360
+ position: object.position.clone(),
361
+ rotation: object.rotation.clone(),
362
+ })
363
+ object.position.set(...pivot)
364
+ object.rotation.set(0, 0, 0)
365
+ })
366
+ scene.updateMatrixWorld(true)
80
367
  const { blob, outW, outH } = await pipeline.capture({ captureMode: 'standard' })
81
368
  onComplete(blob, { w: outW, h: outH })
82
369
  } catch (error) {
@@ -87,7 +374,22 @@ export function BakeThumbnail({
87
374
  onError(error instanceof Error ? error.message : String(error))
88
375
  } finally {
89
376
  pipeline?.dispose()
377
+ setKeyLightDirectionOverride(null)
378
+ for (const mesh of added) disposeMesh(mesh)
379
+ for (const object of hidden) object.visible = true
380
+ for (const [material, intensity] of emission)
381
+ (material as Material & { emissiveIntensity: number }).emissiveIntensity = intensity
382
+ for (const { object, position, rotation } of transforms) {
383
+ object.position.copy(position)
384
+ object.rotation.copy(rotation)
385
+ }
386
+ scene.updateMatrixWorld(true)
387
+ for (const [id, on] of priorLights)
388
+ useInteractive
389
+ .getState()
390
+ .setProceduralLights(id as import('@pascal-app/core').AnyNodeId, on)
90
391
  restoreNodeVisibility()
392
+ restoreAmbiance()
91
393
  }
92
394
  }
93
395
 
@@ -0,0 +1,72 @@
1
+ 'use client'
2
+
3
+ import { type BuildingNode, getLevelDisplayName, type LevelNode, useScene } from '@pascal-app/core'
4
+ import { useViewer } from '@pascal-app/viewer'
5
+ import { useShallow } from 'zustand/react/shallow'
6
+ import { cn } from '../../lib/utils'
7
+ import useEditor from '../../store/use-editor'
8
+
9
+ const rowClass = (selected: boolean) =>
10
+ cn(
11
+ 'flex w-full min-w-0 items-center rounded-lg px-2.5 py-1.5 text-left font-medium text-xs transition-colors',
12
+ selected
13
+ ? 'bg-white/10 text-foreground'
14
+ : 'text-muted-foreground/70 hover:bg-white/5 hover:text-muted-foreground',
15
+ )
16
+
17
+ /**
18
+ * The capture view's level picker: the editor's level list, top floor first,
19
+ * under a "Whole building" row that shows every level (the default on entering
20
+ * capture). A level shows itself and the levels below it, as while editing.
21
+ */
22
+ export function CaptureLevelPicker() {
23
+ const captureLevelId = useEditor((s) => s.captureLevelId)
24
+ const setCaptureLevel = useEditor((s) => s.setCaptureLevel)
25
+ const buildingId = useViewer((s) => s.selection.buildingId)
26
+ const levels = useScene(
27
+ useShallow((state) => {
28
+ const building = (
29
+ buildingId
30
+ ? state.nodes[buildingId]
31
+ : Object.values(state.nodes).find((n) => n?.type === 'building')
32
+ ) as BuildingNode | undefined
33
+ if (building?.type !== 'building') return [] as LevelNode[]
34
+ return building.children
35
+ .map((id) => state.nodes[id])
36
+ .filter((node): node is LevelNode => node?.type === 'level')
37
+ .sort((a, b) => b.level - a.level)
38
+ }),
39
+ )
40
+ if (levels.length < 2) return null
41
+
42
+ return (
43
+ <div
44
+ aria-label="Levels in the shot"
45
+ className="pointer-events-auto absolute top-4 left-4 flex w-36 flex-col gap-0.5 rounded-xl border border-border bg-background/90 p-1 shadow-2xl backdrop-blur-md"
46
+ data-capture-level-picker
47
+ role="group"
48
+ >
49
+ <button
50
+ aria-pressed={captureLevelId === null}
51
+ className={rowClass(captureLevelId === null)}
52
+ onClick={() => setCaptureLevel(null)}
53
+ type="button"
54
+ >
55
+ <span className="truncate">Whole building</span>
56
+ </button>
57
+ <div className="mx-1.5 my-0.5 h-px bg-border/60" />
58
+ {levels.map((level) => (
59
+ <button
60
+ aria-pressed={captureLevelId === level.id}
61
+ className={rowClass(captureLevelId === level.id)}
62
+ key={level.id}
63
+ onClick={() => setCaptureLevel(level.id)}
64
+ title={getLevelDisplayName(level)}
65
+ type="button"
66
+ >
67
+ <span className="truncate">{getLevelDisplayName(level)}</span>
68
+ </button>
69
+ ))}
70
+ </div>
71
+ )
72
+ }
@@ -0,0 +1,150 @@
1
+ import { expect, spyOn, test } from 'bun:test'
2
+ import {
3
+ type AnyNode,
4
+ type AnyNodeDefinition,
5
+ BaseNode,
6
+ LevelNode,
7
+ nodeRegistry,
8
+ objectId,
9
+ registerNode,
10
+ runAsSceneCommitAuthor,
11
+ sceneRegistry,
12
+ useScene,
13
+ } from '@pascal-app/core'
14
+ import {
15
+ type RevealGroupEvent,
16
+ startConstructionReveal,
17
+ subscribeRevealEvents,
18
+ useViewer,
19
+ } from '@pascal-app/viewer'
20
+ import { act, create } from '@react-three/test-renderer'
21
+ import { BoxGeometry, Mesh, MeshBasicMaterial } from 'three'
22
+ import { sfxEmitter } from '../../lib/sfx-bus'
23
+ import { ConstructionRevealAudio } from './construction-reveal-audio'
24
+
25
+ test('real reveal landings respect audio controls and the shared cue clock', async () => {
26
+ const restore = nodeRegistry._snapshot()
27
+ const savedScene = useScene.getState()
28
+ const savedHistory = useScene.temporal.getState()
29
+ const savedViewer = useViewer.getState()
30
+ const savedObjects = new Map(sceneRegistry.nodes)
31
+ const actDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'IS_REACT_ACT_ENVIRONMENT')
32
+ const rafDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'requestAnimationFrame')
33
+ const cancelDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'cancelAnimationFrame')
34
+ Object.defineProperties(globalThis, {
35
+ IS_REACT_ACT_ENVIRONMENT: { configurable: true, value: true },
36
+ requestAnimationFrame: { configurable: true, value: () => 0 },
37
+ cancelAnimationFrame: { configurable: true, value: () => {} },
38
+ })
39
+ let audioClock = 0
40
+ const clock = spyOn(performance, 'now').mockImplementation(() => audioClock)
41
+ const cues: string[] = []
42
+ const observe = (name: string) => cues.push(name)
43
+ sfxEmitter.on('*', observe)
44
+ const events: RevealGroupEvent[] = []
45
+ const stopObserve = subscribeRevealEvents((event) => {
46
+ if (event.type === 'land') events.push(event)
47
+ })
48
+ let instant = false
49
+ let time = 0
50
+ let index = 0
51
+ const meshes: Mesh[] = []
52
+ const schema = BaseNode.extend({ id: objectId('audio-smoke') })
53
+ for (const [kind, phase] of [
54
+ ['audio:structure', 'structure'],
55
+ ['audio:furnishing', 'furnishing'],
56
+ ] as const) {
57
+ registerNode({
58
+ kind,
59
+ schemaVersion: 1,
60
+ schema,
61
+ category: 'utility',
62
+ defaults: () => ({}),
63
+ capabilities: { reveal: { phase, style: 'scale' } },
64
+ } as unknown as AnyNodeDefinition)
65
+ }
66
+ const level = LevelNode.parse({ id: 'level_audio_smoke' })
67
+ useScene.setState({ nodes: { [level.id]: level }, rootNodeIds: [level.id], readOnly: false })
68
+ useViewer.setState({ renderContext: 'editor', isExporting: false })
69
+ useViewer
70
+ .getState()
71
+ .setSelection({ levelId: level.id, buildingId: null, selectedIds: [], zoneId: null })
72
+ const driver = startConstructionReveal({
73
+ reveals: (commit) => commit.author === 'agent',
74
+ level: () => 'full',
75
+ prefersReducedMotion: () => instant,
76
+ })
77
+ const renderer = await create(<ConstructionRevealAudio enabled={false} level="full" />)
78
+ async function land(kind = 'audio:structure') {
79
+ const id = `audio-smoke_${++index}`
80
+ const node = { ...schema.parse({ id }), type: kind, parentId: level.id } as unknown as AnyNode
81
+ const mesh = new Mesh(new BoxGeometry(1, 1, 1), new MeshBasicMaterial())
82
+ meshes.push(mesh)
83
+ sceneRegistry.nodes.set(node.id, mesh)
84
+ const before = events.length
85
+ await act(async () =>
86
+ runAsSceneCommitAuthor('agent', () => useScene.getState().createNode(node, level.id)),
87
+ )
88
+ // A real CPU mesh supplies the renderer's ready handshake, not drawn pixels.
89
+ useScene.getState().clearDirty(node.id)
90
+ for (let frame = 0; frame < 150; frame++) {
91
+ time += 20
92
+ await act(async () => driver.tick(time, 20))
93
+ }
94
+ expect(events.length).toBeGreaterThan(before)
95
+ expect(Boolean(events.at(-1)?.instant)).toBe(instant)
96
+ }
97
+ try {
98
+ await land()
99
+ expect(cues).toEqual([])
100
+ await renderer.update(<ConstructionRevealAudio enabled={true} level="simple" />)
101
+ await land()
102
+ expect(cues).toEqual([])
103
+ await renderer.update(<ConstructionRevealAudio enabled={true} level="off" />)
104
+ await land()
105
+ expect(cues).toEqual([])
106
+ await renderer.update(<ConstructionRevealAudio enabled={true} level="full" />)
107
+ await land()
108
+ expect(cues).toEqual(['sfx:structure-build'])
109
+ await renderer.update(<ConstructionRevealAudio enabled={false} level="full" />)
110
+ await renderer.update(<ConstructionRevealAudio enabled={true} level="framing" />)
111
+ audioClock = 249
112
+ await land()
113
+ expect(cues).toEqual(['sfx:structure-build'])
114
+ audioClock = 250
115
+ await land('audio:furnishing')
116
+ expect(cues).toEqual(['sfx:structure-build', 'sfx:item-place'])
117
+ audioClock = 1000
118
+ instant = true
119
+ await land()
120
+ expect(cues).toHaveLength(2)
121
+ await renderer.unmount()
122
+ instant = false
123
+ await land()
124
+ expect(cues).toHaveLength(2)
125
+ } finally {
126
+ driver.stop()
127
+ await renderer.unmount()
128
+ stopObserve()
129
+ sfxEmitter.off('*', observe)
130
+ clock.mockRestore()
131
+ restore()
132
+ useScene.setState(savedScene, true)
133
+ useScene.temporal.setState(savedHistory, true)
134
+ useViewer.setState(savedViewer, true)
135
+ sceneRegistry.nodes.clear()
136
+ for (const [id, mesh] of savedObjects) sceneRegistry.nodes.set(id, mesh)
137
+ for (const mesh of meshes) {
138
+ mesh.geometry.dispose()
139
+ ;(mesh.material as MeshBasicMaterial).dispose()
140
+ }
141
+ for (const [name, descriptor] of [
142
+ ['IS_REACT_ACT_ENVIRONMENT', actDescriptor],
143
+ ['requestAnimationFrame', rafDescriptor],
144
+ ['cancelAnimationFrame', cancelDescriptor],
145
+ ] as const) {
146
+ if (descriptor) Object.defineProperty(globalThis, name, descriptor)
147
+ else Reflect.deleteProperty(globalThis, name)
148
+ }
149
+ }
150
+ })
@@ -0,0 +1,40 @@
1
+ 'use client'
2
+
3
+ import type { RevealPhase } from '@pascal-app/core'
4
+ import { type ConstructionRevealLevel, subscribeRevealEvents } from '@pascal-app/viewer'
5
+ import { useEffect, useRef } from 'react'
6
+ import { triggerSFX } from '../../lib/sfx-bus'
7
+
8
+ const REVEAL_PHASE_CUES = {
9
+ foundation: 'sfx:structure-build',
10
+ structure: 'sfx:structure-build',
11
+ circulation: 'sfx:structure-build',
12
+ openings: 'sfx:structure-build',
13
+ roof: 'sfx:structure-build',
14
+ furnishing: 'sfx:item-place',
15
+ } as const satisfies Record<RevealPhase, 'sfx:structure-build' | 'sfx:item-place'>
16
+
17
+ /** Mount once alongside ConstructionReveal; its driver owns the commit policy. */
18
+ export function ConstructionRevealAudio({
19
+ enabled,
20
+ level,
21
+ }: {
22
+ enabled: boolean
23
+ level: ConstructionRevealLevel
24
+ }) {
25
+ // Prop changes and Strict Effects must not reset the shared phase/level gate.
26
+ const lastPlayedMs = useRef(Number.NEGATIVE_INFINITY)
27
+
28
+ useEffect(() => {
29
+ if (!enabled || (level !== 'full' && level !== 'framing')) return
30
+ return subscribeRevealEvents((event) => {
31
+ if (event.type !== 'land' || event.instant) return
32
+ const nowMs = performance.now()
33
+ if (nowMs - lastPlayedMs.current < 250) return
34
+ lastPlayedMs.current = nowMs
35
+ triggerSFX(REVEAL_PHASE_CUES[event.phase])
36
+ })
37
+ }, [enabled, level])
38
+
39
+ return null
40
+ }