@pascal-app/editor 0.9.1 → 1.0.0-beta.1

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 (366) hide show
  1. package/package.json +14 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +49 -0
  4. package/src/components/editor/bake-thumbnail.tsx +95 -0
  5. package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
  6. package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
  7. package/src/components/editor/custom-camera-controls.tsx +332 -199
  8. package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
  9. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  10. package/src/components/editor/editor-layout-v2.tsx +18 -0
  11. package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
  12. package/src/components/editor/export-manager.tsx +71 -63
  13. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  14. package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
  15. package/src/components/editor/first-person/build-collider-world.ts +52 -5
  16. package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
  17. package/src/components/editor/first-person/terrain-collider.ts +212 -0
  18. package/src/components/editor/first-person-controls.tsx +410 -136
  19. package/src/components/editor/floating-action-menu.tsx +435 -64
  20. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  21. package/src/components/editor/floorplan-background-selection.test.ts +72 -0
  22. package/src/components/editor/floorplan-background-selection.ts +7 -20
  23. package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
  24. package/src/components/editor/floorplan-camera-sync.ts +213 -0
  25. package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
  26. package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
  27. package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
  28. package/src/components/editor/floorplan-panel.tsx +2517 -1492
  29. package/src/components/editor/grid.tsx +166 -39
  30. package/src/components/editor/group-actions.ts +597 -0
  31. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  32. package/src/components/editor/group-move-3d.ts +397 -0
  33. package/src/components/editor/group-rotate-handle.tsx +102 -64
  34. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  35. package/src/components/editor/group-transform-shared.test.ts +192 -1
  36. package/src/components/editor/group-transform-shared.ts +240 -7
  37. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  38. package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
  39. package/src/components/editor/handles/handle-drag-history.ts +14 -0
  40. package/src/components/editor/handles/preview-overrides.test.ts +26 -0
  41. package/src/components/editor/handles/preview-overrides.ts +13 -0
  42. package/src/components/editor/handles/resize-snap.test.ts +52 -0
  43. package/src/components/editor/handles/resize-snap.ts +23 -0
  44. package/src/components/editor/handles/use-handle-drag.ts +49 -6
  45. package/src/components/editor/index.tsx +289 -35
  46. package/src/components/editor/measurement-pill.tsx +68 -23
  47. package/src/components/editor/node-action-menu.tsx +14 -1
  48. package/src/components/editor/node-arrow-handles.tsx +409 -180
  49. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  50. package/src/components/editor/quick-measurement-card.tsx +76 -0
  51. package/src/components/editor/quick-measurement-hud.tsx +34 -0
  52. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  53. package/src/components/editor/selection-manager.tsx +814 -466
  54. package/src/components/editor/site-edge-labels.tsx +28 -6
  55. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  56. package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
  57. package/src/components/editor/three-context-bridge.ts +22 -0
  58. package/src/components/editor/thumbnail-generator.tsx +118 -253
  59. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  60. package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
  61. package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
  62. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  63. package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
  64. package/src/components/editor/wall-measurement-label.tsx +10 -28
  65. package/src/components/editor/wall-move-side-handles.tsx +229 -39
  66. package/src/components/editor/wall-opening-highlights.tsx +63 -32
  67. package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
  68. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  69. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
  70. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
  71. package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
  72. package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
  73. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  74. package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
  75. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  76. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
  77. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  78. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
  79. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
  80. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
  81. package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
  82. package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
  83. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  84. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
  85. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
  86. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
  87. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
  88. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
  89. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
  90. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
  91. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
  92. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
  93. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
  94. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
  95. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
  96. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  97. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
  98. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  99. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  100. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  101. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  102. package/src/components/systems/zone/zone-system.tsx +42 -13
  103. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  104. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  105. package/src/components/tools/fence/fence-drafting.ts +107 -8
  106. package/src/components/tools/item/move-tool.tsx +17 -13
  107. package/src/components/tools/item/placement-math.test.ts +13 -1
  108. package/src/components/tools/item/placement-math.ts +28 -2
  109. package/src/components/tools/item/placement-strategies.ts +246 -5
  110. package/src/components/tools/item/placement-types.ts +13 -1
  111. package/src/components/tools/item/use-draft-node.ts +146 -57
  112. package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
  113. package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
  114. package/src/components/tools/roof/roof-tool.tsx +336 -94
  115. package/src/components/tools/select/box-select-state.ts +18 -7
  116. package/src/components/tools/select/box-select-tool.tsx +176 -40
  117. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  118. package/src/components/tools/select/marquee-geometry.ts +155 -0
  119. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  120. package/src/components/tools/select/select-candidates.ts +1 -1
  121. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  122. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  123. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  124. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  125. package/src/components/tools/shared/floor-stack-preview.ts +4 -0
  126. package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
  127. package/src/components/tools/shared/placement-box.tsx +183 -1
  128. package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
  129. package/src/components/tools/shared/pointer-support-cap.ts +273 -0
  130. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  131. package/src/components/tools/site/site-boundary-editor.tsx +101 -42
  132. package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
  133. package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
  134. package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
  135. package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
  136. package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
  137. package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
  138. package/src/components/tools/stair/stair-click-guard.ts +72 -0
  139. package/src/components/tools/stair/stair-defaults.ts +1 -0
  140. package/src/components/tools/stair/stair-tool.tsx +164 -42
  141. package/src/components/tools/tool-manager.tsx +149 -30
  142. package/src/components/tools/wall/wall-drafting.test.ts +447 -0
  143. package/src/components/tools/wall/wall-drafting.ts +304 -108
  144. package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
  145. package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
  146. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  147. package/src/components/tools/zone/zone-tool.tsx +98 -88
  148. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  149. package/src/components/ui/action-menu/control-modes.tsx +44 -94
  150. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  151. package/src/components/ui/action-menu/index.tsx +2 -4
  152. package/src/components/ui/action-menu/measurement-control.tsx +267 -0
  153. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  154. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  155. package/src/components/ui/command-palette/editor-commands.tsx +16 -3
  156. package/src/components/ui/command-palette/index.tsx +4 -3
  157. package/src/components/ui/controls/material-paint-panel.tsx +93 -18
  158. package/src/components/ui/controls/material-picker.tsx +162 -152
  159. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  160. package/src/components/ui/controls/metric-control.tsx +118 -44
  161. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  162. package/src/components/ui/controls/slider-control.tsx +88 -38
  163. package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
  164. package/src/components/ui/floating-level-selector.tsx +74 -20
  165. package/src/components/ui/helpers/building-helper.tsx +10 -23
  166. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  167. package/src/components/ui/helpers/helper-manager.tsx +273 -14
  168. package/src/components/ui/helpers/item-helper.tsx +28 -32
  169. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  170. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  171. package/src/components/ui/icon-ref.tsx +47 -0
  172. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  173. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  174. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  175. package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
  176. package/src/components/ui/panels/node-display.ts +17 -17
  177. package/src/components/ui/panels/panel-manager.tsx +36 -14
  178. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  179. package/src/components/ui/panels/parametric-inspector.tsx +19 -4
  180. package/src/components/ui/panels/reference-panel.tsx +54 -19
  181. package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
  182. package/src/components/ui/panels/selection-breakdown.ts +20 -0
  183. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  184. package/src/components/ui/primitives/sidebar.tsx +1 -0
  185. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  186. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  187. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  188. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
  189. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
  190. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  191. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
  192. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  193. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  194. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  195. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  196. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  197. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  198. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  199. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  200. package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
  201. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  202. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  203. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  204. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  205. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  206. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  207. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  208. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  209. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  210. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  211. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  212. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  213. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  214. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  215. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  216. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  217. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  218. package/src/components/ui/snap-target-badge.tsx +88 -0
  219. package/src/components/viewer/viewer-controls-bar.tsx +466 -0
  220. package/src/components/viewer/viewer-scene-header.tsx +235 -0
  221. package/src/components/viewer-overlay.tsx +36 -596
  222. package/src/components/viewer-zone-system.tsx +6 -1
  223. package/src/components/walkthrough-hud.tsx +111 -0
  224. package/src/hooks/use-auto-save.test.ts +14 -0
  225. package/src/hooks/use-auto-save.ts +54 -7
  226. package/src/hooks/use-ceiling-events.ts +3 -2
  227. package/src/hooks/use-drag-action.ts +4 -1
  228. package/src/hooks/use-grid-events.ts +24 -0
  229. package/src/hooks/use-keyboard.ts +438 -70
  230. package/src/index.tsx +355 -9
  231. package/src/lib/active-placement-surface.test.ts +24 -0
  232. package/src/lib/active-placement-surface.ts +42 -0
  233. package/src/lib/camera-pose.test.ts +237 -0
  234. package/src/lib/camera-pose.ts +172 -0
  235. package/src/lib/ceiling-plan-snap.ts +24 -0
  236. package/src/lib/contextual-help.test.ts +112 -0
  237. package/src/lib/contextual-help.ts +144 -0
  238. package/src/lib/continuation.ts +64 -0
  239. package/src/lib/direct-manipulation.test.ts +275 -0
  240. package/src/lib/direct-manipulation.ts +118 -0
  241. package/src/lib/door-interaction.ts +1 -1
  242. package/src/lib/editor-api.ts +23 -35
  243. package/src/lib/elevation-guides.test.ts +107 -0
  244. package/src/lib/elevation-guides.ts +236 -0
  245. package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
  246. package/src/lib/floorplan/annotation-visibility.ts +133 -0
  247. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  248. package/src/lib/floorplan/apply-alignment.ts +18 -12
  249. package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
  250. package/src/lib/floorplan/drawing-coordination.ts +13 -0
  251. package/src/lib/floorplan/floorplan-export.test.ts +319 -0
  252. package/src/lib/floorplan/floorplan-export.tsx +978 -0
  253. package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
  254. package/src/lib/floorplan/floorplan-extension.ts +181 -0
  255. package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
  256. package/src/lib/floorplan/floorplan-mode.ts +73 -0
  257. package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
  258. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
  259. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
  260. package/src/lib/floorplan/geometry.ts +52 -0
  261. package/src/lib/floorplan/index.ts +3 -0
  262. package/src/lib/floorplan/items.ts +5 -2
  263. package/src/lib/floorplan/plan-coords.ts +21 -0
  264. package/src/lib/fresh-planar-placement.test.ts +283 -3
  265. package/src/lib/fresh-planar-placement.ts +85 -1
  266. package/src/lib/glb-export.test.ts +556 -0
  267. package/src/lib/glb-export.ts +1071 -0
  268. package/src/lib/ground-surface.ts +95 -0
  269. package/src/lib/history.test.ts +96 -0
  270. package/src/lib/history.ts +31 -0
  271. package/src/lib/interaction/hot-set.test.ts +133 -0
  272. package/src/lib/interaction/hot-set.ts +67 -0
  273. package/src/lib/interaction/overlay-policy.test.ts +52 -0
  274. package/src/lib/interaction/overlay-policy.ts +59 -0
  275. package/src/lib/interaction/scope.ts +243 -0
  276. package/src/lib/level-duplication.test.ts +2 -1
  277. package/src/lib/level-selection.ts +2 -2
  278. package/src/lib/material-paint.ts +36 -49
  279. package/src/lib/measurement-kind.test.ts +30 -0
  280. package/src/lib/measurement-kind.ts +19 -0
  281. package/src/lib/measurement-label.test.ts +47 -0
  282. package/src/lib/measurement-label.ts +68 -0
  283. package/src/lib/measurement-parser.ts +116 -0
  284. package/src/lib/measurements.test.ts +188 -0
  285. package/src/lib/measurements.ts +210 -0
  286. package/src/lib/paint-scope.test.ts +454 -0
  287. package/src/lib/paint-scope.ts +461 -0
  288. package/src/lib/placement-drag-release.ts +23 -0
  289. package/src/lib/planar-cursor-placement.test.ts +57 -0
  290. package/src/lib/plugin-panels.test.ts +19 -0
  291. package/src/lib/plugin-panels.ts +91 -0
  292. package/src/lib/quick-action-feedback.ts +33 -0
  293. package/src/lib/quick-action-nodes.test.ts +94 -0
  294. package/src/lib/quick-action-nodes.ts +47 -0
  295. package/src/lib/quick-measurement.test.ts +77 -0
  296. package/src/lib/quick-measurement.ts +109 -0
  297. package/src/lib/roof-duplication.ts +6 -6
  298. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  299. package/src/lib/roof-wall-hit.ts +164 -0
  300. package/src/lib/scene-clipboard.test.ts +325 -0
  301. package/src/lib/scene-clipboard.ts +310 -21
  302. package/src/lib/scene.ts +28 -5
  303. package/src/lib/selection-routing.test.ts +313 -0
  304. package/src/lib/selection-routing.ts +180 -0
  305. package/src/lib/sfx/index.ts +1 -0
  306. package/src/lib/sfx/movement-tick.test.ts +65 -0
  307. package/src/lib/sfx/movement-tick.ts +18 -0
  308. package/src/lib/sfx-bus.ts +82 -14
  309. package/src/lib/sfx-player.test.ts +266 -0
  310. package/src/lib/sfx-player.ts +262 -69
  311. package/src/lib/site-boundary.test.ts +53 -0
  312. package/src/lib/site-boundary.ts +27 -0
  313. package/src/lib/slab-plan-snap.test.ts +93 -0
  314. package/src/lib/slab-plan-snap.ts +108 -0
  315. package/src/lib/snapping-mode.test.ts +165 -0
  316. package/src/lib/snapping-mode.ts +191 -0
  317. package/src/lib/stair-duplication.test.ts +103 -0
  318. package/src/lib/stair-duplication.ts +58 -86
  319. package/src/lib/stair-levels.ts +2 -0
  320. package/src/lib/surface-plan-snap.test.ts +71 -0
  321. package/src/lib/surface-plan-snap.ts +256 -0
  322. package/src/lib/terrain-sculpt.test.ts +429 -0
  323. package/src/lib/terrain-sculpt.ts +301 -0
  324. package/src/lib/terrain-verb-color.test.ts +51 -0
  325. package/src/lib/terrain-verb-color.ts +58 -0
  326. package/src/lib/touch-gesture-priority.test.ts +45 -0
  327. package/src/lib/touch-gesture-priority.ts +30 -0
  328. package/src/lib/use-linear-display.ts +40 -0
  329. package/src/lib/window-interaction.ts +1 -1
  330. package/src/lib/world-grid-snap.ts +37 -5
  331. package/src/store/camera-pose-store.test.ts +29 -0
  332. package/src/store/camera-pose-store.ts +27 -0
  333. package/src/store/live-draft-preview-stores.test.ts +136 -0
  334. package/src/store/navigation-sync-pose-store.test.ts +34 -0
  335. package/src/store/navigation-sync-pose-store.ts +27 -0
  336. package/src/store/terrain-sculpt-mode.test.ts +223 -0
  337. package/src/store/use-delete-confirmation.ts +27 -0
  338. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  339. package/src/store/use-drawing-view.test.ts +23 -0
  340. package/src/store/use-drawing-view.ts +84 -0
  341. package/src/store/use-editor.tsx +665 -126
  342. package/src/store/use-elevation-guides.ts +40 -0
  343. package/src/store/use-facing-pose.ts +44 -0
  344. package/src/store/use-fence-curve-draft.ts +51 -0
  345. package/src/store/use-first-person-hud.ts +40 -0
  346. package/src/store/use-floorplan-annotation-visibility.ts +60 -0
  347. package/src/store/use-floorplan-draft-preview.ts +151 -0
  348. package/src/store/use-floorplan-marquee.ts +50 -0
  349. package/src/store/use-floorplan-mode.ts +110 -0
  350. package/src/store/use-interaction-scope.test.ts +206 -0
  351. package/src/store/use-interaction-scope.ts +140 -0
  352. package/src/store/use-measurement-draft.test.ts +559 -0
  353. package/src/store/use-measurement-draft.ts +570 -0
  354. package/src/store/use-opening-guides.ts +41 -0
  355. package/src/store/use-path-draft-preview.ts +90 -0
  356. package/src/store/use-placement-preview.ts +11 -3
  357. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  358. package/src/store/use-quick-measurement-hud.ts +77 -0
  359. package/src/store/use-segment-draft-chain.ts +28 -0
  360. package/src/store/use-stair-build-preview.ts +53 -0
  361. package/src/store/use-wall-snap-indicator.ts +2 -0
  362. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  363. package/src/components/editor/group-move-handle.tsx +0 -316
  364. package/src/components/ui/panels/paint-panel.tsx +0 -163
  365. package/src/lib/floorplan/selection-tool.ts +0 -271
  366. package/src/lib/level-name.ts +0 -11
@@ -1,34 +1,25 @@
1
1
  'use client'
2
2
 
3
- import { emitter, sceneRegistry } from '@pascal-app/core'
4
- import { GRID_LAYER, SSGI_PARAMS, snapLevelsToTruePositions, useViewer } from '@pascal-app/viewer'
3
+ import { emitter } from '@pascal-app/core'
4
+ import {
5
+ computeHeroFraming,
6
+ createSnapshotPipeline,
7
+ GRID_LAYER,
8
+ heroCameraPose,
9
+ type SnapshotPipeline,
10
+ snapLevelsToTruePositions,
11
+ THUMBNAIL_HEIGHT,
12
+ THUMBNAIL_WIDTH,
13
+ temporarilyHideNodeTypes,
14
+ useViewer,
15
+ } from '@pascal-app/viewer'
5
16
  import type { CameraControls } from '@react-three/drei'
6
17
  import { useThree } from '@react-three/fiber'
7
18
  import { useCallback, useEffect, useRef } from 'react'
8
19
  import * as THREE from 'three'
9
- import { UnsignedByteType } from 'three'
10
- import { ssgi } from 'three/addons/tsl/display/SSGINode.js'
11
- import { denoise } from 'three/examples/jsm/tsl/display/DenoiseNode.js'
12
- import { fxaa } from 'three/examples/jsm/tsl/display/FXAANode.js'
13
- import {
14
- colorToDirection,
15
- convertToTexture,
16
- diffuseColor,
17
- directionToColor,
18
- float,
19
- mrt,
20
- normalView,
21
- output,
22
- pass,
23
- sample,
24
- vec4,
25
- } from 'three/tsl'
26
- import { RenderPipeline, RenderTarget, type WebGPURenderer } from 'three/webgpu'
20
+ import type { WebGPURenderer } from 'three/webgpu'
27
21
  import { EDITOR_LAYER } from '../../lib/constants'
28
22
 
29
- const THUMBNAIL_WIDTH = 1920
30
- const THUMBNAIL_HEIGHT = 1080
31
-
32
23
  export interface SnapshotCameraData {
33
24
  position: [number, number, number]
34
25
  target: [number, number, number] | null
@@ -51,8 +42,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
51
42
  const onThumbnailCaptureRef = useRef(onThumbnailCapture)
52
43
 
53
44
  const thumbnailCameraRef = useRef<THREE.PerspectiveCamera | null>(null)
54
- const pipelineRef = useRef<RenderPipeline | null>(null)
55
- const renderTargetRef = useRef<RenderTarget | null>(null)
45
+ const pipelineRef = useRef<SnapshotPipeline | null>(null)
56
46
 
57
47
  useEffect(() => {
58
48
  onThumbnailCaptureRef.current = onThumbnailCapture
@@ -68,81 +58,24 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
68
58
  let mounted = true
69
59
 
70
60
  const buildPipeline = async () => {
71
- try {
72
- if ((gl as any).init) await (gl as any).init()
73
- if (!mounted) return
74
-
75
- // pass() handles MRT internally for all material types, including custom
76
- // shaders — unlike renderer.setMRT() which crashes on non-NodeMaterials.
77
- // pass() also respects camera.layers, so EDITOR_LAYER + GRID_LAYER objects are filtered.
78
- const scenePass = pass(scene, cam)
79
- scenePass.setMRT(
80
- mrt({
81
- output,
82
- diffuseColor,
83
- normal: directionToColor(normalView),
84
- }),
85
- )
86
-
87
- const scenePassColor = scenePass.getTextureNode('output')
88
- const scenePassDepth = scenePass.getTextureNode('depth')
89
- const scenePassNormal = scenePass.getTextureNode('normal')
90
-
91
- scenePass.getTexture('diffuseColor').type = UnsignedByteType
92
- scenePass.getTexture('normal').type = UnsignedByteType
93
-
94
- const sceneNormal = sample((uv) => colorToDirection(scenePassNormal.sample(uv)))
95
-
96
- const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, cam as any)
97
- giPass.sliceCount.value = SSGI_PARAMS.sliceCount
98
- giPass.stepCount.value = SSGI_PARAMS.stepCount
99
- giPass.radius.value = SSGI_PARAMS.radius
100
- giPass.expFactor.value = SSGI_PARAMS.expFactor
101
- giPass.thickness.value = SSGI_PARAMS.thickness
102
- giPass.backfaceLighting.value = SSGI_PARAMS.backfaceLighting
103
- giPass.aoIntensity.value = SSGI_PARAMS.aoIntensity
104
- giPass.giIntensity.value = SSGI_PARAMS.giIntensity
105
- giPass.useLinearThickness.value = SSGI_PARAMS.useLinearThickness
106
- giPass.useScreenSpaceSampling.value = SSGI_PARAMS.useScreenSpaceSampling
107
- giPass.useTemporalFiltering = SSGI_PARAMS.useTemporalFiltering
108
-
109
- const giTexture = (giPass as any).getTextureNode()
110
- const aoAsRgb = vec4(giTexture.a, giTexture.a, giTexture.a, float(1))
111
- const denoisePass = denoise(aoAsRgb, scenePassDepth, sceneNormal, cam)
112
- denoisePass.index.value = 0
113
- denoisePass.radius.value = 4
114
-
115
- const ao = (denoisePass as any).r
116
- const finalOutput = vec4(scenePassColor.rgb.mul(ao), scenePassColor.a)
117
-
118
- // FXAA requires a texture node as input; convertToTexture renders finalOutput
119
- // into an intermediate RT so FXAA can sample it with neighbour UV offsets.
120
- const aaOutput = fxaa(convertToTexture(finalOutput))
121
-
122
- const pipeline = new RenderPipeline(gl as unknown as WebGPURenderer)
123
- pipeline.outputNode = aaOutput
124
- pipelineRef.current = pipeline
125
-
126
- // Dedicated render target — pipeline outputs here instead of the canvas,
127
- // so R3F's main render loop can never overwrite our capture.
128
- const { width, height } = gl.domElement
129
- renderTargetRef.current = new RenderTarget(width, height, { depthBuffer: true })
130
- } catch (error) {
131
- console.error(
132
- '[thumbnail] Failed to build post-processing pipeline, will use fallback render.',
133
- error,
134
- )
61
+ const pipeline = await createSnapshotPipeline({
62
+ renderer: gl as unknown as WebGPURenderer,
63
+ scene,
64
+ camera: cam,
65
+ })
66
+ if (!mounted) {
67
+ pipeline?.dispose()
68
+ return
135
69
  }
70
+ pipelineRef.current = pipeline
136
71
  }
137
72
 
138
- buildPipeline()
73
+ void buildPipeline()
139
74
 
140
75
  return () => {
141
76
  mounted = false
142
77
  pipelineRef.current?.dispose()
143
78
  pipelineRef.current = null
144
- renderTargetRef.current?.dispose()
145
- renderTargetRef.current = null
146
79
  }
147
80
  }, [gl, scene])
148
81
 
@@ -151,7 +84,11 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
151
84
  snapLevels: boolean,
152
85
  captureMode?: 'standard' | 'viewport' | 'area',
153
86
  cropRegion?: { x: number; y: number; width: number; height: number },
87
+ standardSize?: { w: number; h: number },
88
+ transparent = false,
154
89
  ) => {
90
+ const standardW = standardSize?.w ?? THUMBNAIL_WIDTH
91
+ const standardH = standardSize?.h ?? THUMBNAIL_HEIGHT
155
92
  if (isGenerating.current) return
156
93
  if (!onThumbnailCaptureRef.current) return
157
94
 
@@ -173,6 +110,20 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
173
110
  const { width, height } = gl.domElement
174
111
  thumbnailCamera.aspect = width / height
175
112
  thumbnailCamera.updateProjectionMatrix()
113
+ // The capture camera never joins the scene graph, so its matrixWorld
114
+ // is only refreshed by the render itself — too late for the backdrop
115
+ // uniforms below.
116
+ thumbnailCamera.updateMatrixWorld()
117
+
118
+ const pipeline = pipelineRef.current
119
+ pipeline?.applyEnvironment({
120
+ theme: useViewer.getState().sceneTheme,
121
+ transparent,
122
+ grade: useViewer.getState().shading === 'rendered',
123
+ // Preset/item captures stay clean; scene captures mirror the canvas.
124
+ edges: transparent ? 'off' : useViewer.getState().edges,
125
+ camera: thumbnailCamera,
126
+ })
176
127
 
177
128
  // Capture camera data for snapshot storage
178
129
  const pos = mainCamera.position
@@ -202,174 +153,82 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
202
153
  restoreLevels = snapLevelsToTruePositions()
203
154
  }
204
155
 
205
- // Hide scan and guide nodes directly so they are excluded from the
206
- // thumbnail regardless of whether ScanSystem/GuideSystem listeners are
207
- // registered. Returns a function that restores the original visibility.
208
- const restoreNodeVisibility = (() => {
209
- const saved = new Map<THREE.Object3D, boolean>()
210
- for (const type of ['scan', 'guide'] as const) {
211
- const ids = sceneRegistry.byType[type]!
212
- ids.forEach((id) => {
213
- const node = sceneRegistry.nodes.get(id)
214
- if (node) {
215
- saved.set(node, node.visible)
216
- node.visible = false
217
- }
156
+ // Hide scan, guide, and spawn nodes directly so they are excluded from
157
+ // the thumbnail regardless of whether ScanSystem/GuideSystem listeners
158
+ // are registered. Spawn renders on SCENE_LAYER for occlusion, so the
159
+ // thumbnail camera's layer mask can't filter it either. Returns a
160
+ // function that restores the original visibility.
161
+ const restoreNodeVisibility = temporarilyHideNodeTypes(['scan', 'guide', 'spawn'])
162
+
163
+ // Auto-save shots don't copy the user's mid-edit camera — they re-pose
164
+ // onto the same computed hero angle the published thumbnail uses, so a
165
+ // project's card never shows a half-zoomed working view. Measured after
166
+ // the level snap so stacked positions frame correctly. User-driven
167
+ // captures (captureMode set) keep the exact viewport pose.
168
+ if (snapLevels) {
169
+ const framing = computeHeroFraming()
170
+ if (framing) {
171
+ const pose = heroCameraPose({
172
+ boxes: framing.boxes,
173
+ aim: framing.aim,
174
+ azimuthRad: framing.azimuthRad,
175
+ aspect: width / height,
218
176
  })
219
- }
220
- return () => {
221
- saved.forEach((wasVisible, node) => {
222
- node.visible = wasVisible
177
+ thumbnailCamera.position.set(pose.position[0], pose.position[1], pose.position[2])
178
+ thumbnailCamera.lookAt(pose.target[0], pose.target[1], pose.target[2])
179
+ thumbnailCamera.updateMatrixWorld()
180
+ pipeline?.applyEnvironment({
181
+ theme: useViewer.getState().sceneTheme,
182
+ transparent,
183
+ grade: useViewer.getState().shading === 'rendered',
184
+ edges: transparent ? 'off' : useViewer.getState().edges,
185
+ camera: thumbnailCamera,
223
186
  })
187
+ cameraData.position = pose.position
188
+ cameraData.target = pose.target
224
189
  }
225
- })()
190
+ }
226
191
 
227
192
  let blob: Blob
228
193
 
229
- if (pipelineRef.current && renderTargetRef.current) {
230
- const rt = renderTargetRef.current
231
-
232
- // Resize RT if the canvas dimensions changed
233
- if (rt.width !== width || rt.height !== height) {
234
- rt.setSize(width, height)
235
- }
236
-
237
- const renderer = gl as unknown as WebGPURenderer
194
+ if (pipeline) {
195
+ let capturePromise: ReturnType<SnapshotPipeline['capture']>
238
196
 
239
197
  // Notify other systems (wall cutouts, selection manager) to restore
240
198
  // their overrides before capture and re-apply them after.
241
- emitter.emit('thumbnail:before-capture', undefined)
242
- ;(renderer as any).setClearAlpha(0)
243
- renderer.setRenderTarget(rt)
244
- pipelineRef.current.render()
245
- renderer.setRenderTarget(null)
246
- emitter.emit('thumbnail:after-capture', undefined)
247
-
248
- // Restore level positions, levelMode, and node visibility immediately after the
249
- // render before the async GPU readback.
250
- restoreLevels()
251
- restoreLevelMode?.()
252
- restoreNodeVisibility()
253
-
254
- // Read pixels from the RT asynchronously.
255
- // WebGPU copyTextureToBuffer aligns each row to 256 bytes, so we must
256
- // depad the rows before constructing ImageData.
257
- const pixels = (await (renderer as any).readRenderTargetPixelsAsync(
258
- rt,
259
- 0,
260
- 0,
261
- width,
262
- height,
263
- )) as Uint8Array
264
-
265
- const actualBytesPerRow = width * 4
266
- const tightTotal = actualBytesPerRow * height
267
- const paddedBytesPerRow = Math.ceil(actualBytesPerRow / 256) * 256
268
- // Two readback shapes to handle:
269
- // - WebGPU (`copyTextureToBuffer`): top-down + 256-byte row padding
270
- // when width*4 isn't already a multiple of 256.
271
- // - WebGL2 fallback (iOS Chrome, etc.): tightly-packed but bottom-up
272
- // (OpenGL framebuffer convention).
273
- // `isWebGPURenderer` lies — it stays true even when the renderer
274
- // falls back to the WebGL backend. Inspect the actual backend
275
- // instead (presence of a GPU device, or backend constructor name).
276
- const backend = (renderer as any).backend
277
- const isWebGPU =
278
- !!backend?.device ||
279
- backend?.isWebGPUBackend === true ||
280
- backend?.constructor?.name === 'WebGPUBackend'
281
- let tightPixels: Uint8ClampedArray
282
- if (isWebGPU) {
283
- // WebGPU: depad rows if needed; orientation is already top-down.
284
- if (paddedBytesPerRow === actualBytesPerRow) {
285
- tightPixels = new Uint8ClampedArray(
286
- pixels.buffer,
287
- pixels.byteOffset,
288
- Math.min(pixels.byteLength, tightTotal),
289
- )
290
- } else {
291
- tightPixels = new Uint8ClampedArray(tightTotal)
292
- for (let row = 0; row < height; row++) {
293
- tightPixels.set(
294
- pixels.subarray(
295
- row * paddedBytesPerRow,
296
- row * paddedBytesPerRow + actualBytesPerRow,
297
- ),
298
- row * actualBytesPerRow,
299
- )
300
- }
301
- }
302
- } else {
303
- // WebGL2: tight buffer in bottom-up order — flip rows.
304
- tightPixels = new Uint8ClampedArray(tightTotal)
305
- for (let row = 0; row < height; row++) {
306
- const srcStart = (height - 1 - row) * actualBytesPerRow
307
- tightPixels.set(
308
- pixels.subarray(srcStart, srcStart + actualBytesPerRow),
309
- row * actualBytesPerRow,
310
- )
311
- }
199
+ try {
200
+ emitter.emit('thumbnail:before-capture', undefined)
201
+ capturePromise = pipeline.capture({
202
+ captureMode,
203
+ cropRegion,
204
+ standardSize,
205
+ })
206
+ } finally {
207
+ // Restore level positions, levelMode, and node visibility immediately
208
+ // after the render — before the async GPU readback. Runs in `finally`
209
+ // so a render failure can't leave helpers permanently hidden.
210
+ emitter.emit('thumbnail:after-capture', undefined)
211
+ restoreLevels()
212
+ restoreLevelMode?.()
213
+ restoreNodeVisibility()
312
214
  }
313
215
 
314
- const imageData = new ImageData(
315
- tightPixels as unknown as Uint8ClampedArray<ArrayBuffer>,
316
- width,
317
- height,
318
- )
319
- const srcCanvas = new OffscreenCanvas(width, height)
320
- srcCanvas.getContext('2d')!.putImageData(imageData, 0, 0)
321
-
322
- let outW: number
323
- let outH: number
324
-
325
- if (captureMode === 'viewport') {
326
- outW = width
327
- outH = height
328
- const offscreen = new OffscreenCanvas(outW, outH)
329
- offscreen.getContext('2d')!.drawImage(srcCanvas, 0, 0)
330
- blob = await offscreen.convertToBlob({ type: 'image/png' })
331
- } else if (captureMode === 'area' && cropRegion) {
332
- const sx = Math.round(cropRegion.x * width)
333
- const sy = Math.round(cropRegion.y * height)
334
- outW = Math.round(cropRegion.width * width)
335
- outH = Math.round(cropRegion.height * height)
336
- const offscreen = new OffscreenCanvas(outW, outH)
337
- offscreen.getContext('2d')!.drawImage(srcCanvas, sx, sy, outW, outH, 0, 0, outW, outH)
338
- blob = await offscreen.convertToBlob({ type: 'image/png' })
339
- } else {
340
- // Standard: center-crop to 1920×1080 aspect ratio
341
- const srcAspect = width / height
342
- const dstAspect = THUMBNAIL_WIDTH / THUMBNAIL_HEIGHT
343
- let sx = 0,
344
- sy = 0,
345
- sWidth = width,
346
- sHeight = height
347
- if (srcAspect > dstAspect) {
348
- sWidth = Math.round(height * dstAspect)
349
- sx = Math.round((width - sWidth) / 2)
350
- } else if (srcAspect < dstAspect) {
351
- sHeight = Math.round(width / dstAspect)
352
- sy = Math.round((height - sHeight) / 2)
353
- }
354
- outW = THUMBNAIL_WIDTH
355
- outH = THUMBNAIL_HEIGHT
356
- const offscreen = new OffscreenCanvas(outW, outH)
357
- offscreen
358
- .getContext('2d')!
359
- .drawImage(srcCanvas, sx, sy, sWidth, sHeight, 0, 0, outW, outH)
360
- blob = await offscreen.convertToBlob({ type: 'image/png' })
361
- }
216
+ const result = await capturePromise
217
+ blob = result.blob
362
218
 
363
219
  if (captureMode !== undefined) cameraData.captureMode = captureMode
364
- cameraData.resolution = { w: outW, h: outH }
220
+ cameraData.resolution = { w: result.outW, h: result.outH }
365
221
  } else {
366
222
  // Fallback: plain render directly to the canvas
367
- emitter.emit('thumbnail:before-capture', undefined)
368
- gl.render(scene, thumbnailCamera)
369
- emitter.emit('thumbnail:after-capture', undefined)
370
- restoreLevels()
371
- restoreLevelMode?.()
372
- restoreNodeVisibility()
223
+ try {
224
+ emitter.emit('thumbnail:before-capture', undefined)
225
+ gl.render(scene, thumbnailCamera)
226
+ } finally {
227
+ emitter.emit('thumbnail:after-capture', undefined)
228
+ restoreLevels()
229
+ restoreLevelMode?.()
230
+ restoreNodeVisibility()
231
+ }
373
232
 
374
233
  let outW: number
375
234
  let outH: number
@@ -406,7 +265,7 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
406
265
  )
407
266
  } else {
408
267
  const srcAspect = width / height
409
- const dstAspect = THUMBNAIL_WIDTH / THUMBNAIL_HEIGHT
268
+ const dstAspect = standardW / standardH
410
269
  let sx = 0,
411
270
  sy = 0,
412
271
  sWidth = width,
@@ -418,8 +277,8 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
418
277
  sHeight = Math.round(width / dstAspect)
419
278
  sy = Math.round((height - sHeight) / 2)
420
279
  }
421
- outW = THUMBNAIL_WIDTH
422
- outH = THUMBNAIL_HEIGHT
280
+ outW = standardW
281
+ outH = standardH
423
282
  const offscreen = document.createElement('canvas')
424
283
  offscreen.width = outW
425
284
  offscreen.height = outH
@@ -460,14 +319,20 @@ export const ThumbnailGenerator = ({ onThumbnailCapture }: ThumbnailGeneratorPro
460
319
  const handleGenerateThumbnail = async (event: {
461
320
  captureMode?: 'standard' | 'viewport' | 'area'
462
321
  cropRegion?: { x: number; y: number; width: number; height: number }
322
+ standardSize?: { w: number; h: number }
463
323
  snapLevels?: boolean
464
- // `transparent` is informational here the render pipeline already
465
- // captures with alpha (see `setClearAlpha(0)` above)the flag is
466
- // forwarded so future tweaks (suppressing the ground occluder, theme
467
- // background bits) can branch on it without touching the emitter.
324
+ // Preset/item captures keep the alpha channel (their thumbnails compose
325
+ // onto arbitrary palette backgrounds); scene snapshots studio renders
326
+ // and project thumbnails composite the theme backdrop + sky.
468
327
  transparent?: boolean
469
328
  }) => {
470
- await generate(event.snapLevels === true, event.captureMode, event.cropRegion)
329
+ await generate(
330
+ event.snapLevels === true,
331
+ event.captureMode,
332
+ event.cropRegion,
333
+ event.standardSize,
334
+ event.transparent === true,
335
+ )
471
336
  }
472
337
 
473
338
  emitter.on('camera-controls:generate-thumbnail', handleGenerateThumbnail)