@pascal-app/editor 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (498) hide show
  1. package/package.json +16 -9
  2. package/src/components/editor/action-menu-button.tsx +83 -0
  3. package/src/components/editor/bake-thumbnail.tsx +315 -4
  4. package/src/components/editor/capture-level-picker.tsx +72 -0
  5. package/src/components/editor/custom-camera-controls.tsx +221 -128
  6. package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
  7. package/src/components/editor/duplicate-node.ts +244 -0
  8. package/src/components/editor/editor-layout-mobile.tsx +37 -25
  9. package/src/components/editor/editor-layout-v2.tsx +131 -58
  10. package/src/components/editor/export-manager.tsx +34 -8
  11. package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
  12. package/src/components/editor/first-person/build-collider-world.ts +6 -2
  13. package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
  14. package/src/components/editor/first-person/mechanism-targets.ts +36 -0
  15. package/src/components/editor/first-person-controls.tsx +205 -2
  16. package/src/components/editor/floating-action-menu.tsx +63 -207
  17. package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
  18. package/src/components/editor/floor-aware-camera-controls.ts +324 -0
  19. package/src/components/editor/floor-openings-overlay.tsx +243 -0
  20. package/src/components/editor/floor-region-controls.tsx +846 -0
  21. package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
  22. package/src/components/editor/floor-surface.test.ts +165 -0
  23. package/src/components/editor/floor-surface.ts +182 -0
  24. package/src/components/editor/floorplan-background-selection.test.ts +11 -2
  25. package/src/components/editor/floorplan-background-selection.ts +1 -3
  26. package/src/components/editor/floorplan-panel.tsx +536 -487
  27. package/src/components/editor/footprint-height-handle.tsx +204 -0
  28. package/src/components/editor/grid.tsx +31 -7
  29. package/src/components/editor/group-actions.ts +59 -11
  30. package/src/components/editor/group-floating-action-menu.tsx +19 -9
  31. package/src/components/editor/group-move-3d.ts +4 -6
  32. package/src/components/editor/group-rotate-handle.tsx +81 -35
  33. package/src/components/editor/group-selection-box-3d.tsx +25 -17
  34. package/src/components/editor/group-transform-shared.test.ts +126 -1
  35. package/src/components/editor/group-transform-shared.ts +79 -73
  36. package/src/components/editor/handles/handle-arrow.tsx +72 -2
  37. package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
  38. package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
  39. package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
  40. package/src/components/editor/handles/use-handle-drag.ts +272 -131
  41. package/src/components/editor/index.tsx +316 -176
  42. package/src/components/editor/join-walls-pill.tsx +44 -0
  43. package/src/components/editor/node-action-menu.tsx +70 -70
  44. package/src/components/editor/node-arrow-handles.tsx +167 -14
  45. package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
  46. package/src/components/editor/open-wall-ends-hint.tsx +89 -0
  47. package/src/components/editor/quick-measurement-hud.tsx +3 -2
  48. package/src/components/editor/registry-action-contributions.test.tsx +63 -0
  49. package/src/components/editor/registry-action-contributions.tsx +132 -0
  50. package/src/components/editor/room-controls.tsx +847 -0
  51. package/src/components/editor/room-floor-highlight.tsx +128 -0
  52. package/src/components/editor/room-handles-2d.tsx +354 -0
  53. package/src/components/editor/room-handles.tsx +675 -0
  54. package/src/components/editor/room-highlight.tsx +225 -0
  55. package/src/components/editor/room-labels.test.ts +357 -0
  56. package/src/components/editor/room-labels.tsx +990 -0
  57. package/src/components/editor/room-transform-ghost.tsx +256 -0
  58. package/src/components/editor/room-webgpu-materials.test.ts +27 -0
  59. package/src/components/editor/selection-manager.tsx +451 -317
  60. package/src/components/editor/slab-hole-highlights.tsx +3 -0
  61. package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
  62. package/src/components/editor/snapshot-capture.test.ts +52 -4
  63. package/src/components/editor/snapshot-capture.ts +30 -5
  64. package/src/components/editor/thumbnail-generator.test.ts +308 -0
  65. package/src/components/editor/thumbnail-generator.tsx +1035 -229
  66. package/src/components/editor/use-floorplan-background-placement.ts +31 -11
  67. package/src/components/editor/vector-edge-extractor.tsx +121 -0
  68. package/src/components/editor/views/host-view.tsx +61 -0
  69. package/src/components/editor/views/plugins-view.tsx +670 -0
  70. package/src/components/editor/views/scene-region.tsx +59 -0
  71. package/src/components/editor/views/use-editor-views.ts +57 -0
  72. package/src/components/editor/views/view-bar.tsx +220 -0
  73. package/src/components/editor/wall-justification.test.ts +140 -0
  74. package/src/components/editor/wall-move-side-handles.tsx +114 -163
  75. package/src/components/editor/wall-opening-highlights.tsx +2 -3
  76. package/src/components/editor/wall-push-arrow.tsx +92 -0
  77. package/src/components/editor/wall-region-handles.tsx +509 -0
  78. package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
  79. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
  80. package/src/components/editor-2d/floorplan-group-move.tsx +17 -24
  81. package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
  82. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
  83. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +33 -12
  84. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
  85. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
  86. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
  88. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
  89. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +166 -0
  90. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
  91. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
  92. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
  93. package/src/components/editor-2d/svg-paths.ts +12 -15
  94. package/src/components/procedural-items/inspector.tsx +40 -15
  95. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
  96. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
  97. package/src/components/systems/zone/zone-system.test.ts +47 -0
  98. package/src/components/systems/zone/zone-system.tsx +31 -66
  99. package/src/components/tools/__fixtures__/hosting.ts +10 -0
  100. package/src/components/tools/item/item-surface-placement.test.ts +3 -9
  101. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
  102. package/src/components/tools/item/placement-history.test.tsx +474 -0
  103. package/src/components/tools/item/placement-strategies.ts +63 -47
  104. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
  105. package/src/components/tools/item/use-draft-node.test.tsx +432 -1
  106. package/src/components/tools/item/use-draft-node.ts +207 -97
  107. package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
  108. package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
  109. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
  110. package/src/components/tools/registry/item-surface-move.test.ts +10 -2
  111. package/src/components/tools/registry/item-surface-move.ts +46 -24
  112. package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
  114. package/src/components/tools/select/box-select-tool.tsx +2 -12
  115. package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
  116. package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
  117. package/src/components/tools/select/select-candidates.test.ts +37 -4
  118. package/src/components/tools/select/select-candidates.ts +15 -11
  119. package/src/components/tools/shared/affordance-dispatch.test.ts +24 -0
  120. package/src/components/tools/shared/affordance-dispatch.ts +13 -0
  121. package/src/components/tools/shared/cursor-sphere.tsx +36 -12
  122. package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
  123. package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
  124. package/src/components/tools/shared/pointer-support-cap.ts +17 -5
  125. package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
  126. package/src/components/tools/shared/surface-rejection.tsx +14 -0
  127. package/src/components/tools/site/site-boundary-editor.tsx +1 -2
  128. package/src/components/tools/stair/stair-defaults.ts +0 -3
  129. package/src/components/tools/stair/stair-tool.tsx +90 -42
  130. package/src/components/tools/tool-manager.tsx +82 -23
  131. package/src/components/tools/wall/wall-drafting.test.ts +19 -4
  132. package/src/components/tools/zone/zone-tool.tsx +4 -12
  133. package/src/components/ui/action-menu/action-button.tsx +3 -0
  134. package/src/components/ui/action-menu/camera-actions.tsx +2 -1
  135. package/src/components/ui/action-menu/control-modes.tsx +2 -35
  136. package/src/components/ui/action-menu/measurement-control.tsx +3 -10
  137. package/src/components/ui/build-panel/build-panel.tsx +235 -0
  138. package/src/components/ui/command-palette/editor-commands.tsx +4 -30
  139. package/src/components/ui/command-palette/index.tsx +5 -24
  140. package/src/components/ui/controls/finish-swatch.tsx +110 -0
  141. package/src/components/ui/controls/material-paint-panel.tsx +101 -91
  142. package/src/components/ui/controls/material-picker.tsx +8 -67
  143. package/src/components/ui/controls/scene-material-list.tsx +4 -32
  144. package/src/components/ui/controls/select-control.tsx +65 -0
  145. package/src/components/ui/controls/slider-control.tsx +76 -31
  146. package/src/components/ui/floating-level-selector.tsx +9 -14
  147. package/src/components/ui/helpers/building-helper.tsx +4 -1
  148. package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
  149. package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
  150. package/src/components/ui/helpers/helper-manager.tsx +348 -19
  151. package/src/components/ui/helpers/item-helper.tsx +8 -0
  152. package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
  153. package/src/components/ui/icon-ref.tsx +45 -1
  154. package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
  155. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  156. package/src/components/ui/node-icon-image.tsx +16 -0
  157. package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
  158. package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
  159. package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
  160. package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
  161. package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
  162. package/src/components/ui/panels/multi-field-value.test.ts +26 -3
  163. package/src/components/ui/panels/multi-field-value.ts +6 -3
  164. package/src/components/ui/panels/multi-height-mode.tsx +27 -21
  165. package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
  166. package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
  167. package/src/components/ui/panels/node-display.ts +8 -3
  168. package/src/components/ui/panels/panel-manager.tsx +103 -68
  169. package/src/components/ui/panels/panel-wrapper.tsx +78 -36
  170. package/src/components/ui/panels/parametric-field-control.tsx +1 -1
  171. package/src/components/ui/panels/parametric-inspector.tsx +7 -19
  172. package/src/components/ui/panels/reference-panel.tsx +2 -3
  173. package/src/components/ui/panels/room-construction-rows.tsx +774 -0
  174. package/src/components/ui/panels/room-name-options.test.ts +55 -0
  175. package/src/components/ui/panels/room-name-options.ts +47 -0
  176. package/src/components/ui/panels/room-openings-field.tsx +110 -0
  177. package/src/components/ui/panels/room-panel.tsx +725 -0
  178. package/src/components/ui/panels/shape-choice.tsx +58 -0
  179. package/src/components/ui/panels/wall-region-list.tsx +109 -0
  180. package/src/components/ui/primitives/color-dot.tsx +36 -24
  181. package/src/components/ui/primitives/shortcut-token.tsx +4 -0
  182. package/src/components/ui/primitives/sidebar.tsx +2 -0
  183. package/src/components/ui/right-stack.test.ts +29 -0
  184. package/src/components/ui/right-stack.tsx +145 -0
  185. package/src/components/ui/sidebar/icon-rail.tsx +5 -0
  186. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  187. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
  188. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
  189. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  190. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
  191. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
  192. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
  193. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
  194. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
  195. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
  196. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
  197. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
  198. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
  199. package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
  200. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
  211. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
  215. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
  216. package/src/components/ui/sidebar/tab-bar.tsx +10 -0
  217. package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
  218. package/src/components/viewer/floorplan-compass-button.tsx +9 -1
  219. package/src/components/viewer/floorplan-preview-visibility.test.ts +35 -0
  220. package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
  221. package/src/components/viewer/floorplan-preview.tsx +6 -18
  222. package/src/components/viewer/viewer-hover-label.tsx +34 -0
  223. package/src/components/viewer/viewer-rooms.tsx +175 -0
  224. package/src/components/viewer/viewer-scene-header.tsx +181 -145
  225. package/src/components/viewer/viewer-selection-manager.tsx +124 -0
  226. package/src/components/viewer/viewer-stage.tsx +2 -2
  227. package/src/components/viewer-zone-system.tsx +9 -9
  228. package/src/hooks/use-auto-save.integration.test.tsx +311 -0
  229. package/src/hooks/use-auto-save.ts +55 -14
  230. package/src/hooks/use-ceiling-events.test.ts +1 -0
  231. package/src/hooks/use-ceiling-events.ts +2 -0
  232. package/src/hooks/use-grid-events.test.ts +2 -0
  233. package/src/hooks/use-grid-events.ts +16 -11
  234. package/src/hooks/use-keyboard.test.ts +182 -0
  235. package/src/hooks/use-keyboard.ts +122 -91
  236. package/src/hooks/use-open-wall-ends.test.ts +99 -0
  237. package/src/hooks/use-open-wall-ends.ts +238 -0
  238. package/src/hooks/use-placement-notice.test.ts +48 -0
  239. package/src/hooks/use-placement-notice.ts +42 -0
  240. package/src/hooks/use-selected-room.ts +143 -0
  241. package/src/index.tsx +306 -39
  242. package/src/lib/catalog-panel-model.ts +39 -0
  243. package/src/lib/catalog-selection.test.ts +27 -0
  244. package/src/lib/catalog-selection.ts +8 -0
  245. package/src/lib/ceiling-edit-session.test.ts +321 -0
  246. package/src/lib/ceiling-edit-session.ts +206 -0
  247. package/src/lib/ceiling-region-session.test.ts +179 -0
  248. package/src/lib/contextual-help.test.ts +27 -1
  249. package/src/lib/contextual-help.ts +16 -9
  250. package/src/lib/continuation.test.ts +19 -1
  251. package/src/lib/continuation.ts +25 -4
  252. package/src/lib/door-interaction.ts +14 -0
  253. package/src/lib/editor-level-display.test.ts +96 -0
  254. package/src/lib/editor-level-display.ts +48 -0
  255. package/src/lib/editor-views.ts +144 -0
  256. package/src/lib/export-geometry-wait.test.ts +90 -0
  257. package/src/lib/floor-edit-session.ts +153 -0
  258. package/src/lib/floor-footprints.test.ts +563 -0
  259. package/src/lib/floor-footprints.ts +395 -0
  260. package/src/lib/floor-foundation-support.test.ts +283 -0
  261. package/src/lib/floor-opening-draft.ts +218 -0
  262. package/src/lib/floor-openings.test.ts +216 -0
  263. package/src/lib/floor-region-geometry.ts +191 -0
  264. package/src/lib/floor-region-session.test.ts +485 -0
  265. package/src/lib/floor-region-session.ts +589 -0
  266. package/src/lib/floor-region-snap.ts +101 -0
  267. package/src/lib/floor-step-hover.test.ts +32 -0
  268. package/src/lib/floorplan/annotation-visibility.ts +17 -0
  269. package/src/lib/floorplan/dimension-drive.test.ts +360 -0
  270. package/src/lib/floorplan/dimension-drive.ts +479 -0
  271. package/src/lib/floorplan/floorplan-export.test.ts +15 -9
  272. package/src/lib/floorplan/floorplan-export.tsx +32 -8
  273. package/src/lib/floorplan/floorplan-extension.ts +48 -0
  274. package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
  275. package/src/lib/floorplan/floorplan-mode.ts +2 -0
  276. package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
  277. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
  278. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
  279. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
  280. package/src/lib/floorplan/floorplan-readonly.ts +6 -0
  281. package/src/lib/floorplan/geometry.ts +13 -6
  282. package/src/lib/floorplan/index.ts +0 -9
  283. package/src/lib/floorplan/items.ts +5 -2
  284. package/src/lib/floorplan/marks.test.ts +213 -0
  285. package/src/lib/floorplan/marks.ts +238 -0
  286. package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
  287. package/src/lib/floorplan/open-wall-ends.ts +162 -0
  288. package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
  289. package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
  290. package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
  291. package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
  292. package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
  293. package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
  294. package/src/lib/floorplan/schedules.test.ts +189 -0
  295. package/src/lib/floorplan/schedules.ts +339 -0
  296. package/src/lib/floorplan/sheet-export.test.ts +100 -0
  297. package/src/lib/floorplan/sheet-export.ts +318 -0
  298. package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
  299. package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
  300. package/src/lib/floorplan/site-plan/contributors.ts +97 -0
  301. package/src/lib/floorplan/site-plan/coverage.ts +232 -0
  302. package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
  303. package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
  304. package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
  305. package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
  306. package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
  307. package/src/lib/floorplan/site-plan/geometry.ts +557 -0
  308. package/src/lib/floorplan/site-plan/index.ts +71 -0
  309. package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
  310. package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
  311. package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
  312. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
  313. package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
  314. package/src/lib/floorplan/site-plan/street-name.ts +302 -0
  315. package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
  316. package/src/lib/floorplan/types.ts +1 -27
  317. package/src/lib/fresh-planar-placement.test.ts +36 -0
  318. package/src/lib/fresh-planar-placement.ts +191 -36
  319. package/src/lib/geometry-script/author.ts +152 -0
  320. package/src/lib/geometry-script/client.ts +152 -0
  321. package/src/lib/geometry-script/protocol.ts +12 -0
  322. package/src/lib/gesture-lifecycle.test.ts +595 -0
  323. package/src/lib/gesture-lifecycle.ts +290 -0
  324. package/src/lib/glb-export-zone-holes.test.ts +41 -0
  325. package/src/lib/glb-export.test.ts +544 -16
  326. package/src/lib/glb-export.ts +256 -125
  327. package/src/lib/grid-frame.test.ts +19 -0
  328. package/src/lib/grid-frame.ts +18 -0
  329. package/src/lib/history.test.ts +6 -0
  330. package/src/lib/history.ts +7 -0
  331. package/src/lib/hud-title.ts +109 -0
  332. package/src/lib/hydration-gated-storage.ts +49 -0
  333. package/src/lib/ifc-export.test.ts +174 -0
  334. package/src/lib/ifc-export.ts +201 -0
  335. package/src/lib/ifc-parts.ts +15 -0
  336. package/src/lib/inspector-expanded.test.ts +44 -0
  337. package/src/lib/inspector-expanded.ts +36 -0
  338. package/src/lib/interaction/scope.ts +27 -1
  339. package/src/lib/keyboard-pan.test.ts +184 -0
  340. package/src/lib/keyboard-pan.ts +238 -0
  341. package/src/lib/live-gesture-round-trip.test.ts +360 -0
  342. package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
  343. package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
  344. package/src/lib/lot/clean-ring.test.ts +186 -0
  345. package/src/lib/lot/clean-ring.ts +217 -0
  346. package/src/lib/lot/dossier.test.ts +202 -0
  347. package/src/lib/lot/dossier.ts +498 -0
  348. package/src/lib/lot/drop-in.test.ts +326 -0
  349. package/src/lib/lot/drop-in.ts +407 -0
  350. package/src/lib/lot/index.ts +43 -0
  351. package/src/lib/lot/lot-patch.test.ts +340 -0
  352. package/src/lib/lot/lot-patch.ts +394 -0
  353. package/src/lib/lot/parcel-provider.ts +43 -0
  354. package/src/lib/lot/terrain.test.ts +161 -0
  355. package/src/lib/lot/terrain.ts +266 -0
  356. package/src/lib/material-catalog-model.ts +74 -0
  357. package/src/lib/material-paint-panel-model.ts +80 -0
  358. package/src/lib/material-paint.ts +4 -0
  359. package/src/lib/mezzanine-actions.test.ts +342 -0
  360. package/src/lib/mezzanine-draft.test.ts +501 -0
  361. package/src/lib/mezzanine-draft.ts +278 -0
  362. package/src/lib/mezzanine-messages.ts +26 -0
  363. package/src/lib/mezzanine-selection.test.ts +207 -0
  364. package/src/lib/mezzanine-stairs.ts +46 -0
  365. package/src/lib/model-export.ts +3 -1
  366. package/src/lib/node-action-movement.ts +17 -6
  367. package/src/lib/node-display-label.ts +37 -0
  368. package/src/lib/orbit-floor-clearance.test.ts +165 -0
  369. package/src/lib/orbit-floor-clearance.ts +106 -0
  370. package/src/lib/overlay-transform.test.ts +28 -0
  371. package/src/lib/overlay-transform.ts +12 -0
  372. package/src/lib/paint-commit-route.test.ts +37 -0
  373. package/src/lib/paint-commit-route.ts +17 -0
  374. package/src/lib/paint-eyedropper.test.tsx +165 -0
  375. package/src/lib/paint-eyedropper.ts +99 -0
  376. package/src/lib/paint-pass-through.test.ts +111 -0
  377. package/src/lib/paint-pass-through.ts +76 -0
  378. package/src/lib/paint-region-hover.ts +25 -0
  379. package/src/lib/paint-region-mode.ts +154 -0
  380. package/src/lib/paint-regions.test.ts +166 -0
  381. package/src/lib/paint-regions.ts +192 -0
  382. package/src/lib/paint-scope.test.ts +317 -35
  383. package/src/lib/paint-scope.ts +266 -25
  384. package/src/lib/panel-rows.ts +96 -0
  385. package/src/lib/panel-tool-options.test.ts +34 -0
  386. package/src/lib/panel-tool-options.ts +46 -0
  387. package/src/lib/parametric-node-update.ts +37 -0
  388. package/src/lib/plate-paint-affected.test.ts +300 -0
  389. package/src/lib/plate-paint-affected.ts +177 -0
  390. package/src/lib/plugin-chat.ts +50 -0
  391. package/src/lib/plugin-panels.test.ts +25 -0
  392. package/src/lib/plugin-panels.ts +94 -0
  393. package/src/lib/print-shell-compiler.ts +1 -0
  394. package/src/lib/registered-draft-snap.ts +12 -0
  395. package/src/lib/roof-duplication.ts +10 -2
  396. package/src/lib/room-assembly-overlay.test.ts +275 -0
  397. package/src/lib/room-assembly-overlay.ts +266 -0
  398. package/src/lib/room-built-on.test.ts +379 -0
  399. package/src/lib/room-built-on.ts +138 -0
  400. package/src/lib/room-construction-commands.ts +712 -0
  401. package/src/lib/room-construction.test.ts +746 -0
  402. package/src/lib/room-construction.ts +385 -0
  403. package/src/lib/room-controls-phase6.test.ts +630 -0
  404. package/src/lib/room-divide-session.ts +523 -0
  405. package/src/lib/room-handle-drag.ts +1068 -0
  406. package/src/lib/room-name-catalog.test.ts +90 -0
  407. package/src/lib/room-name-catalog.ts +168 -0
  408. package/src/lib/room-push-dimensions.test.ts +311 -0
  409. package/src/lib/room-push-dimensions.ts +251 -0
  410. package/src/lib/room-selection-commands.ts +62 -0
  411. package/src/lib/room-selection.test.ts +973 -0
  412. package/src/lib/room-selection.ts +417 -0
  413. package/src/lib/room-structure-commands.ts +94 -0
  414. package/src/lib/room-structure.test.ts +921 -0
  415. package/src/lib/room-transform-session.ts +570 -0
  416. package/src/lib/room-zone-routing.test.ts +294 -0
  417. package/src/lib/room-zone-routing.ts +197 -0
  418. package/src/lib/scene-clipboard.test.ts +364 -0
  419. package/src/lib/scene-clipboard.ts +229 -48
  420. package/src/lib/scene-material-refs.test.ts +79 -0
  421. package/src/lib/scene-material-refs.ts +79 -0
  422. package/src/lib/scene.test.ts +87 -0
  423. package/src/lib/scene.ts +39 -45
  424. package/src/lib/selection-routing.test.ts +129 -45
  425. package/src/lib/selection-routing.ts +53 -74
  426. package/src/lib/session-writes.test.ts +120 -0
  427. package/src/lib/session-writes.ts +205 -0
  428. package/src/lib/sidebar-panel.ts +62 -0
  429. package/src/lib/site-boundary.test.ts +4 -6
  430. package/src/lib/site-boundary.ts +1 -1
  431. package/src/lib/snapping-mode.test.ts +17 -2
  432. package/src/lib/snapping-mode.ts +19 -7
  433. package/src/lib/spatial-drag-plane.test.ts +53 -0
  434. package/src/lib/spatial-drag-plane.ts +19 -0
  435. package/src/lib/spatial-pointer-input.test.ts +72 -0
  436. package/src/lib/spatial-pointer-input.ts +62 -0
  437. package/src/lib/spatial-snap.test.ts +47 -0
  438. package/src/lib/terrace-draft.test.ts +145 -0
  439. package/src/lib/terrace-draft.ts +213 -0
  440. package/src/lib/terrain-panel-model.ts +117 -0
  441. package/src/lib/tool-cursor-icon.test.ts +16 -0
  442. package/src/lib/tool-cursor-icon.ts +11 -0
  443. package/src/lib/typing-target.ts +9 -0
  444. package/src/lib/units.ts +77 -39
  445. package/src/lib/vector-edges.test.ts +58 -0
  446. package/src/lib/vector-edges.ts +400 -0
  447. package/src/lib/viewer-selection.test.ts +577 -0
  448. package/src/lib/viewer-selection.ts +569 -0
  449. package/src/lib/wall-draw-variant.test.ts +71 -0
  450. package/src/lib/wall-draw-variant.ts +91 -0
  451. package/src/lib/wall-paint-region-session.ts +246 -0
  452. package/src/lib/wall-paint-region.test.ts +373 -0
  453. package/src/lib/wall-polygon-draft.test.ts +262 -0
  454. package/src/lib/wall-polygon-draft.ts +286 -0
  455. package/src/lib/wall-push.test.ts +453 -0
  456. package/src/lib/wall-region-face.ts +352 -0
  457. package/src/lib/wall-region-handles.test.ts +304 -0
  458. package/src/lib/wall-region-handles.ts +368 -0
  459. package/src/lib/wall-region-list.test.ts +119 -0
  460. package/src/lib/wall-region-list.ts +136 -0
  461. package/src/lib/wall-region-snap.ts +110 -0
  462. package/src/lib/window-interaction.ts +14 -0
  463. package/src/lib/world-grid-snap.test.ts +48 -0
  464. package/src/lib/world-grid-snap.ts +9 -5
  465. package/src/lib/zone-content.ts +4 -1
  466. package/src/lib/zone-name.test.ts +17 -0
  467. package/src/lib/zone-name.ts +17 -0
  468. package/src/store/opens-in-select-mode.test.ts +223 -0
  469. package/src/store/paint-mode.test.ts +188 -0
  470. package/src/store/rotation-snapping.test.ts +53 -0
  471. package/src/store/terrain-sculpt-mode.test.ts +19 -21
  472. package/src/store/tool-mode.test.ts +137 -6
  473. package/src/store/use-delete-confirmation.ts +28 -1
  474. package/src/store/use-drawing-view.test.ts +24 -3
  475. package/src/store/use-drawing-view.ts +29 -1
  476. package/src/store/use-editor.tsx +442 -323
  477. package/src/store/use-floorplan-draft-preview.ts +23 -1
  478. package/src/store/use-floorplan-marquee.ts +12 -2
  479. package/src/store/use-interaction-scope.ts +155 -6
  480. package/src/store/use-quick-measurement-hud.ts +3 -3
  481. package/src/store/use-stair-build-preview.ts +13 -5
  482. package/src/store/use-wall-move-ghosts.ts +4 -2
  483. package/src/store/use-wall-region-handles-selection.test.ts +222 -0
  484. package/src/store/use-wall-region-handles-selection.ts +139 -0
  485. package/src/store/view-layout.test.ts +175 -0
  486. package/src/store/view-layout.ts +196 -0
  487. package/src/test-utils/immediate-animation-frames.ts +16 -0
  488. package/src/test-utils/live-gesture-round-trip.ts +224 -0
  489. package/src/test-utils/selection-harness.ts +82 -0
  490. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
  491. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
  492. package/src/components/tools/fence/fence-drafting.test.ts +0 -100
  493. package/src/components/tools/fence/fence-drafting.ts +0 -266
  494. package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
  495. package/src/components/viewer/viewer-units-panel.tsx +0 -116
  496. package/src/lib/floorplan/stairs.ts +0 -489
  497. package/src/lib/level-duplication.test.ts +0 -144
  498. package/src/lib/level-duplication.ts +0 -195
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/editor",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "Pascal building editor component",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,8 +14,8 @@
14
14
  "test": "bun test src"
15
15
  },
16
16
  "peerDependencies": {
17
- "@pascal-app/core": "^1.0.1",
18
- "@pascal-app/viewer": "^1.0.1",
17
+ "@pascal-app/core": "^1.0.4",
18
+ "@pascal-app/viewer": "^1.0.4",
19
19
  "@react-three/drei": "^10",
20
20
  "@react-three/fiber": "^9",
21
21
  "next": ">=15",
@@ -29,6 +29,8 @@
29
29
  "@dnd-kit/utilities": "^3.2.2",
30
30
  "@iconify/react": "^6.0.2",
31
31
  "@number-flow/react": "^0.6.0",
32
+ "@pascal-app/geometry-script": "^1.0.4",
33
+ "@pascal-app/ifc-converter": "^1.0.4",
32
34
  "@pascal-app/lingo": "^0.2.0",
33
35
  "@radix-ui/react-alert-dialog": "^1.1.15",
34
36
  "@radix-ui/react-context-menu": "^2.2.16",
@@ -62,18 +64,23 @@
62
64
  "zustand": "^5.0.11"
63
65
  },
64
66
  "devDependencies": {
65
- "@pascal-app/core": "^1.0.1",
66
- "@pascal-app/viewer": "^1.0.1",
67
+ "@pascal-app/core": "^1.0.4",
68
+ "@pascal-app/viewer": "^1.0.4",
67
69
  "@pascal/typescript-config": "*",
68
70
  "@react-three/test-renderer": "^9.1.0",
69
71
  "@types/blob-stream": "^0.1.33",
70
- "@types/bun": "^1.3.0",
72
+ "@types/bun": "^1.4.2",
71
73
  "@types/howler": "^2.2.12",
72
74
  "@types/pdfkit": "^0.17.6",
73
- "@types/react": "19.2.2",
74
- "@types/react-dom": "19.2.2",
75
+ "@types/react": "19.3.0",
76
+ "@types/react-dom": "19.3.0",
75
77
  "@types/three": "^0.184.0",
76
- "fast-xml-parser": "^5.4.2",
78
+ "fast-xml-parser": "^5.11.2",
77
79
  "typescript": "6.0.3"
80
+ },
81
+ "repository": {
82
+ "type": "git",
83
+ "url": "git+https://github.com/pascalorg/editor.git",
84
+ "directory": "packages/editor"
78
85
  }
79
86
  }
@@ -0,0 +1,83 @@
1
+ 'use client'
2
+
3
+ import type { MouseEventHandler, ReactNode } from 'react'
4
+ import { cn } from '../../lib/utils'
5
+ import { shortcutDisplayValue } from '../ui/primitives/shortcut-token'
6
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip'
7
+
8
+ const BUTTON =
9
+ 'rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground'
10
+ const DESTRUCTIVE = 'hover:bg-destructive/10 hover:text-destructive'
11
+ const DISABLED = 'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-muted-foreground'
12
+
13
+ /**
14
+ * One button of an action pill, with the editor's tooltip: its name, and the
15
+ * keys that do the same where there are any. A disabled button still shows
16
+ * its tooltip (it stays hoverable) so it can say why it is off.
17
+ */
18
+ export function ActionMenuButton({
19
+ label,
20
+ keys,
21
+ onClick,
22
+ destructive = false,
23
+ disabled = false,
24
+ disabledReason,
25
+ pressed,
26
+ children,
27
+ }: {
28
+ label: string
29
+ /** Keys pressed together, as `ShortcutToken` names them (`Cmd/Ctrl`, `G`). */
30
+ keys?: string[]
31
+ onClick?: MouseEventHandler<HTMLButtonElement>
32
+ destructive?: boolean
33
+ disabled?: boolean
34
+ /** The tooltip while disabled; the label otherwise. */
35
+ disabledReason?: string
36
+ /** A toggle's state: sets `aria-pressed` and the active look. */
37
+ pressed?: boolean
38
+ children: ReactNode
39
+ }) {
40
+ return (
41
+ <Tooltip>
42
+ <TooltipTrigger asChild>
43
+ <button
44
+ aria-disabled={disabled || undefined}
45
+ aria-label={label}
46
+ aria-pressed={pressed}
47
+ className={cn(
48
+ BUTTON,
49
+ destructive && DESTRUCTIVE,
50
+ disabled && DISABLED,
51
+ pressed && 'bg-accent text-foreground',
52
+ )}
53
+ data-action-disabled={disabled || undefined}
54
+ onClick={(event) => {
55
+ if (disabled) {
56
+ event.stopPropagation()
57
+ return
58
+ }
59
+ onClick?.(event)
60
+ }}
61
+ type="button"
62
+ >
63
+ {children}
64
+ </button>
65
+ </TooltipTrigger>
66
+ <TooltipContent className="flex items-center gap-2" side="top" sideOffset={6}>
67
+ <span>{disabled && disabledReason ? disabledReason : label}</span>
68
+ {!disabled && keys?.length ? (
69
+ <span className="flex items-center gap-0.5">
70
+ {keys.map((key) => (
71
+ <kbd
72
+ className="rounded border border-background/30 px-1 font-mono text-[10px] leading-4 opacity-80"
73
+ key={key}
74
+ >
75
+ {shortcutDisplayValue(key)}
76
+ </kbd>
77
+ ))}
78
+ </span>
79
+ ) : null}
80
+ </TooltipContent>
81
+ </Tooltip>
82
+ )
83
+ }
@@ -1,20 +1,206 @@
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,
8
15
  temporarilyHideNodeTypes,
9
16
  useSceneAtmosphere,
10
17
  useViewer,
11
18
  } from '@pascal-app/viewer'
12
19
  import { useThree } from '@react-three/fiber'
13
20
  import { useEffect, useRef } from 'react'
14
- import { PerspectiveCamera } from 'three'
15
- import type { WebGPURenderer } from 'three/webgpu'
21
+ import {
22
+ Box3,
23
+ CanvasTexture,
24
+ type Material,
25
+ Mesh,
26
+ type Object3D,
27
+ PerspectiveCamera,
28
+ PlaneGeometry,
29
+ Vector3,
30
+ } from 'three'
31
+ import { MeshBasicNodeMaterial, ShadowNodeMaterial, type WebGPURenderer } from 'three/webgpu'
16
32
  import { EDITOR_LAYER } from '../../lib/constants'
17
33
 
34
+ // The published-thumbnail look. One look for every project, independent of
35
+ // the scene's own display preferences.
36
+ const THUMBNAIL_FOV_DEG = 40
37
+ const THUMBNAIL_PADDING = 1.12
38
+ /** Roofless scenes look down into the rooms; roofed ones show the roofline. */
39
+ const THUMBNAIL_ELEVATION_DEG = { roofed: 24, roofless: 35 }
40
+ /** Key light relative to the hero camera: 30° off its azimuth, 45° up — both
41
+ * facades lit, shadows raking across the ground. */
42
+ const THUMBNAIL_KEY_AZIMUTH_OFFSET_DEG = 30
43
+ const THUMBNAIL_KEY_ELEVATION_DEG = 45
44
+ const THUMBNAIL_BACKDROP = { inner: '#FAFAFA', outer: '#E4E4E4' }
45
+ const THUMBNAIL_SHADOW_CATCHER_OPACITY = 0.7
46
+ /** Soft shade hugging the ground-floor walls so the building never floats. */
47
+ const THUMBNAIL_WALL_CONTACT = { opacity: 0.5, spreadM: 0.9 }
48
+ /** Unpainted walls render as smooth plaster rather than the screw-dotted
49
+ * prepared-drywall default, which reads as unfinished at card size. */
50
+ const THUMBNAIL_SLOT_DEFAULT_OVERRIDES = {
51
+ 'library:concrete-drywall': 'library:concrete-plaster',
52
+ } as const
53
+ // Past the capture camera's far plane (1000): a catcher edge inside the frame
54
+ // leaves an SSGI AO line along its depth discontinuity.
55
+ const SHADOW_CATCHER_EXTENT = 4000
56
+
57
+ /**
58
+ * Forces the published-thumbnail look on the bake page. Call before the scene
59
+ * builds (the finish swap applies as materials are created); the returned
60
+ * cleanup restores the declared finishes.
61
+ */
62
+ export function prepareBakeThumbnailLook(): () => void {
63
+ const viewer = useViewer.getState()
64
+ viewer.setSceneTheme('studio')
65
+ viewer.setEdges('soft')
66
+ viewer.setShadows(true)
67
+ setSlotDefaultOverrides(THUMBNAIL_SLOT_DEFAULT_OVERRIDES)
68
+ return () => setSlotDefaultOverrides(null)
69
+ }
70
+
71
+ // Scene-ready fires before every system has finished its geometry: roofs
72
+ // rebuild their merged mesh a few frames later (measured: Starter House ridge
73
+ // 5.75 m at scene-ready, 6.24 m at capture), so a pose fit at scene-ready
74
+ // clips the roof. Re-measure until the framing bounds hold still.
75
+ async function settledHeroFraming() {
76
+ const boundsKey = (framing: HeroFraming | null) => {
77
+ if (!framing) return ''
78
+ const union = new Box3()
79
+ for (const box of framing.boxes) union.union(box)
80
+ return [...union.min.toArray(), ...union.max.toArray()].map((v) => v.toFixed(3)).join(',')
81
+ }
82
+ let framing = computeHeroFraming({ sitePlates: false })
83
+ let previous = boundsKey(framing)
84
+ let stableChecks = 0
85
+ for (let i = 0; i < 40 && stableChecks < 3; i++) {
86
+ await new Promise<void>((resolve) => setTimeout(resolve, 100))
87
+ framing = computeHeroFraming({ sitePlates: false })
88
+ const next = boundsKey(framing)
89
+ stableChecks = next === previous ? stableChecks + 1 : 0
90
+ previous = next
91
+ }
92
+ return framing
93
+ }
94
+
95
+ /** The site's own surfaces (ground fill, horizon disc, property ribbons) —
96
+ * not its registered children (buildings, items), which are groups. */
97
+ function siteSurfaces(): Object3D[] {
98
+ const surfaces: Object3D[] = []
99
+ for (const siteId of sceneRegistry.byType.site ?? []) {
100
+ for (const child of sceneRegistry.nodes.get(siteId)?.children ?? []) {
101
+ if (child.visible && !(child.type === 'Group' && child.children.length > 0)) {
102
+ surfaces.push(child)
103
+ }
104
+ }
105
+ }
106
+ return surfaces
107
+ }
108
+
109
+ /**
110
+ * A soft contact shade on the ground along every ground-floor wall: the wall
111
+ * centrelines stroked onto a canvas in the building's facade axes, then
112
+ * blurred. Per wall rather than per footprint, so a walled lot shades its
113
+ * boundary walls and leaves the open courtyard clean.
114
+ */
115
+ function wallContactShade(yaw: number, groundY: number): Mesh | null {
116
+ const u = new Vector3(Math.cos(yaw), 0, Math.sin(yaw))
117
+ const v = new Vector3(-Math.sin(yaw), 0, Math.cos(yaw))
118
+ const segments: Array<{ a: [number, number]; b: [number, number]; thickness: number }> = []
119
+ const point = new Vector3()
120
+ // A wall mesh sits at its start, rotated along the run: `start`/`end` are
121
+ // in its parent's (the level's) frame.
122
+ const toUv = (x: number, z: number, object: Object3D): [number, number] => {
123
+ point.set(x, 0, z)
124
+ if (object.parent) point.applyMatrix4(object.parent.matrixWorld)
125
+ return [point.dot(u), point.dot(v)]
126
+ }
127
+ for (const wallId of sceneRegistry.byType.wall ?? []) {
128
+ const object = sceneRegistry.nodes.get(wallId)
129
+ const node = useScene.getState().nodes[wallId as AnyNodeId]
130
+ if (!(object && node?.type === 'wall' && node.visible !== false)) continue
131
+ const bounds = new Box3().setFromObject(object)
132
+ if (bounds.isEmpty() || bounds.min.y > groundY + 0.5) continue
133
+ segments.push({
134
+ a: toUv(node.start[0], node.start[1], object),
135
+ b: toUv(node.end[0], node.end[1], object),
136
+ thickness: node.thickness ?? 0.2,
137
+ })
138
+ }
139
+ if (segments.length === 0) return null
140
+
141
+ const { spreadM, opacity } = THUMBNAIL_WALL_CONTACT
142
+ let uMin = Number.POSITIVE_INFINITY
143
+ let uMax = Number.NEGATIVE_INFINITY
144
+ let vMin = Number.POSITIVE_INFINITY
145
+ let vMax = Number.NEGATIVE_INFINITY
146
+ for (const { a, b } of segments) {
147
+ uMin = Math.min(uMin, a[0], b[0])
148
+ uMax = Math.max(uMax, a[0], b[0])
149
+ vMin = Math.min(vMin, a[1], b[1])
150
+ vMax = Math.max(vMax, a[1], b[1])
151
+ }
152
+ const margin = spreadM * 2
153
+ const width = uMax - uMin + margin * 2
154
+ const depth = vMax - vMin + margin * 2
155
+ const scale = 1024 / Math.max(width, depth)
156
+ const canvas = document.createElement('canvas')
157
+ canvas.width = Math.max(8, Math.round(width * scale))
158
+ canvas.height = Math.max(8, Math.round(depth * scale))
159
+ const ctx = canvas.getContext('2d')
160
+ if (!ctx) return null
161
+ // alphaMap samples the green channel: white strokes on opaque black.
162
+ ctx.fillStyle = '#000'
163
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
164
+ ctx.filter = `blur(${Math.max(2, spreadM * scale * 0.35)}px)`
165
+ ctx.strokeStyle = '#fff'
166
+ ctx.lineCap = 'square'
167
+ for (const { a, b, thickness } of segments) {
168
+ ctx.lineWidth = (thickness + spreadM * 0.6) * scale
169
+ ctx.beginPath()
170
+ ctx.moveTo((a[0] - uMin + margin) * scale, (a[1] - vMin + margin) * scale)
171
+ ctx.lineTo((b[0] - uMin + margin) * scale, (b[1] - vMin + margin) * scale)
172
+ ctx.stroke()
173
+ }
174
+
175
+ const shade = new Mesh(
176
+ new PlaneGeometry(width, depth),
177
+ new MeshBasicNodeMaterial({
178
+ color: '#000000',
179
+ alphaMap: new CanvasTexture(canvas),
180
+ transparent: true,
181
+ depthWrite: false,
182
+ opacity,
183
+ }),
184
+ )
185
+ // Plane x → u; plane y → −v, so the canvas's downward rows run along +v.
186
+ shade.rotation.set(-Math.PI / 2, 0, -yaw)
187
+ shade.position
188
+ .copy(u)
189
+ .multiplyScalar((uMin + uMax) / 2)
190
+ .addScaledVector(v, (vMin + vMax) / 2)
191
+ .setY(groundY - 0.005)
192
+ shade.renderOrder = -1
193
+ return shade
194
+ }
195
+
196
+ function disposeMesh(mesh: Mesh) {
197
+ mesh.removeFromParent()
198
+ mesh.geometry.dispose()
199
+ const material = mesh.material as MeshBasicNodeMaterial
200
+ material.alphaMap?.dispose()
201
+ material.dispose()
202
+ }
203
+
18
204
  export function BakeThumbnail({
19
205
  active,
20
206
  onComplete,
@@ -35,10 +221,37 @@ export function BakeThumbnail({
35
221
 
36
222
  const run = async () => {
37
223
  const restoreNodeVisibility = temporarilyHideNodeTypes(['scan', 'guide', 'spawn'])
224
+ const priorLights = new Map<string, boolean>()
225
+ const restGroups = new Map<string, [number, number, number]>()
226
+ for (const node of Object.values(useScene.getState().nodes)) {
227
+ if (node.type !== 'procedural-item') continue
228
+ const evaluation = evaluateRecipe(node.recipe, node.parameters)
229
+ if (evaluation.lights.length) {
230
+ priorLights.set(
231
+ node.id,
232
+ useInteractive.getState().procedural[node.id]?.lightsOn ??
233
+ useInteractive.getState().lampDefault,
234
+ )
235
+ useInteractive.getState().setProceduralLights(node.id, true)
236
+ }
237
+ for (const motion of evaluation.motions)
238
+ restGroups.set(`${node.id}:${motion.id}`, motion.pivot)
239
+ }
240
+ const transforms: Array<{
241
+ object: import('three').Object3D
242
+ position: import('three').Vector3
243
+ rotation: import('three').Euler
244
+ }> = []
245
+ const emission = new Map<Material, number>()
38
246
  let pipeline: Awaited<ReturnType<typeof createSnapshotPipeline>> = null
247
+ // The studio backdrop replaces the site's ground and horizon: a shadow
248
+ // catcher keeps cast shadows, the wall shade keeps contact. Items the
249
+ // framing leaves out are hidden so what's framed is what's shown.
250
+ const hidden: Object3D[] = []
251
+ const added: Mesh[] = []
39
252
 
40
253
  try {
41
- const framing = computeHeroFraming()
254
+ const framing = await settledHeroFraming()
42
255
  if (!framing) {
43
256
  onError('scene has no framable content')
44
257
  return
@@ -46,24 +259,73 @@ export function BakeThumbnail({
46
259
 
47
260
  const { width, height } = renderer.domElement
48
261
  const aspect = width / height
49
- const camera = new PerspectiveCamera(60, aspect, 0.1, 1000)
262
+ const camera = new PerspectiveCamera(THUMBNAIL_FOV_DEG, aspect, 0.1, 1000)
50
263
  camera.layers.disable(EDITOR_LAYER)
51
264
  camera.layers.disable(GRID_LAYER)
265
+ const hasRoof = (sceneRegistry.byType.roof?.size ?? 0) > 0
266
+ const elevationDeg = hasRoof
267
+ ? THUMBNAIL_ELEVATION_DEG.roofed
268
+ : THUMBNAIL_ELEVATION_DEG.roofless
52
269
  const pose = heroCameraPose({
53
270
  boxes: framing.boxes,
54
271
  aim: framing.aim,
55
272
  azimuthRad: framing.azimuthRad,
56
273
  aspect,
274
+ fovDeg: THUMBNAIL_FOV_DEG,
275
+ elevationRad: (elevationDeg * Math.PI) / 180,
276
+ padding: THUMBNAIL_PADDING,
277
+ tightFit: true,
57
278
  })
58
279
  camera.position.set(pose.position[0], pose.position[1], pose.position[2])
59
280
  camera.lookAt(pose.target[0], pose.target[1], pose.target[2])
60
281
  camera.updateMatrixWorld()
61
282
 
283
+ const keyAzimuth = framing.azimuthRad + (THUMBNAIL_KEY_AZIMUTH_OFFSET_DEG * Math.PI) / 180
284
+ const keyElevation = (THUMBNAIL_KEY_ELEVATION_DEG * Math.PI) / 180
285
+ setKeyLightDirectionOverride([
286
+ Math.sin(keyAzimuth) * Math.cos(keyElevation),
287
+ Math.sin(keyElevation),
288
+ Math.cos(keyAzimuth) * Math.cos(keyElevation),
289
+ ])
290
+
291
+ for (const object of [
292
+ ...siteSurfaces(),
293
+ ...framing.detachedItemIds.flatMap((id) => sceneRegistry.nodes.get(id) ?? []),
294
+ ]) {
295
+ if (!object.visible) continue
296
+ object.visible = false
297
+ hidden.push(object)
298
+ }
299
+ const union = new Box3()
300
+ for (const box of framing.boxes) union.union(box)
301
+ const groundY = Math.max(0, union.min.y)
302
+ const catcher = new Mesh(
303
+ new PlaneGeometry(SHADOW_CATCHER_EXTENT, SHADOW_CATCHER_EXTENT),
304
+ new ShadowNodeMaterial({ opacity: THUMBNAIL_SHADOW_CATCHER_OPACITY }),
305
+ )
306
+ catcher.rotation.x = -Math.PI / 2
307
+ catcher.position.set(framing.aim[0], groundY - 0.01, framing.aim[2])
308
+ catcher.receiveShadow = true
309
+ added.push(catcher)
310
+ const contact = wallContactShade(Math.PI / 4 - framing.azimuthRad, groundY)
311
+ if (contact) added.push(contact)
312
+ for (const mesh of added) scene.add(mesh)
313
+
314
+ // Backdrop centred on the building's mid-height, where it's lightest.
315
+ const center = new Vector3(
316
+ framing.aim[0],
317
+ (groundY + framing.aim[1]) / 2,
318
+ framing.aim[2],
319
+ ).project(camera)
62
320
  pipeline = await createSnapshotPipeline({
63
321
  renderer: renderer as unknown as WebGPURenderer,
64
322
  scene,
65
323
  camera,
66
324
  atmosphere,
325
+ studioBackdrop: {
326
+ ...THUMBNAIL_BACKDROP,
327
+ center: [(center.x + 1) / 2, (1 - center.y) / 2],
328
+ },
67
329
  })
68
330
  if (!pipeline) {
69
331
  onError('thumbnail pipeline failed to build')
@@ -77,6 +339,41 @@ export function BakeThumbnail({
77
339
  edges: useViewer.getState().edges,
78
340
  camera,
79
341
  })
342
+ await new Promise<void>((resolve) => setTimeout(resolve, 250))
343
+ for (const node of Object.values(useScene.getState().nodes)) {
344
+ if (node.type !== 'procedural-item') continue
345
+ const slots = new Set(
346
+ evaluateRecipe(node.recipe, node.parameters).lights.flatMap((light) =>
347
+ light.emissiveSlot ? [light.emissiveSlot] : [],
348
+ ),
349
+ )
350
+ const object = sceneRegistry.nodes.get(node.id)
351
+ for (const mesh of object ? proceduralSlotMeshes(object, slots) : []) {
352
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
353
+ for (const material of materials) {
354
+ const current = material as Material & { emissiveIntensity?: number }
355
+ if (typeof current.emissiveIntensity !== 'number') continue
356
+ emission.set(material, current.emissiveIntensity)
357
+ setProceduralEmission(material, true)
358
+ }
359
+ }
360
+ }
361
+ scene.traverse((object) => {
362
+ const motion = object.userData.proceduralMotion as
363
+ | { nodeId?: string; groupId?: string }
364
+ | undefined
365
+ if (!motion) return
366
+ const pivot = restGroups.get(`${motion.nodeId}:${motion.groupId}`)
367
+ if (!pivot) return
368
+ transforms.push({
369
+ object,
370
+ position: object.position.clone(),
371
+ rotation: object.rotation.clone(),
372
+ })
373
+ object.position.set(...pivot)
374
+ object.rotation.set(0, 0, 0)
375
+ })
376
+ scene.updateMatrixWorld(true)
80
377
  const { blob, outW, outH } = await pipeline.capture({ captureMode: 'standard' })
81
378
  onComplete(blob, { w: outW, h: outH })
82
379
  } catch (error) {
@@ -87,6 +384,20 @@ export function BakeThumbnail({
87
384
  onError(error instanceof Error ? error.message : String(error))
88
385
  } finally {
89
386
  pipeline?.dispose()
387
+ setKeyLightDirectionOverride(null)
388
+ for (const mesh of added) disposeMesh(mesh)
389
+ for (const object of hidden) object.visible = true
390
+ for (const [material, intensity] of emission)
391
+ (material as Material & { emissiveIntensity: number }).emissiveIntensity = intensity
392
+ for (const { object, position, rotation } of transforms) {
393
+ object.position.copy(position)
394
+ object.rotation.copy(rotation)
395
+ }
396
+ scene.updateMatrixWorld(true)
397
+ for (const [id, on] of priorLights)
398
+ useInteractive
399
+ .getState()
400
+ .setProceduralLights(id as import('@pascal-app/core').AnyNodeId, on)
90
401
  restoreNodeVisibility()
91
402
  }
92
403
  }
@@ -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
+ }