@pascal-app/editor 1.0.0-beta.5 → 1.0.0

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 (245) hide show
  1. package/LICENSE +21 -0
  2. package/bunfig.toml +4 -0
  3. package/package.json +13 -7
  4. package/scripts/generate-print-golden-house.ts +89 -0
  5. package/src/components/editor/bake-exporter.tsx +1 -0
  6. package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
  7. package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
  8. package/src/components/editor/capture-camera-rig.tsx +59 -0
  9. package/src/components/editor/custom-camera-controls.tsx +20 -11
  10. package/src/components/editor/editor-layout-mobile.tsx +6 -6
  11. package/src/components/editor/editor-layout-v2.tsx +9 -2
  12. package/src/components/editor/export-manager.tsx +175 -33
  13. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  14. package/src/components/editor/first-person-controls.tsx +180 -12
  15. package/src/components/editor/floating-action-menu.tsx +42 -51
  16. package/src/components/editor/floorplan-panel.tsx +76 -12
  17. package/src/components/editor/grid.tsx +12 -7
  18. package/src/components/editor/group-actions.ts +34 -50
  19. package/src/components/editor/group-move-3d.ts +51 -21
  20. package/src/components/editor/group-transform-shared.test.ts +78 -0
  21. package/src/components/editor/group-transform-shared.ts +74 -7
  22. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  23. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  24. package/src/components/editor/handles/linear-resize-drag.ts +67 -0
  25. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  26. package/src/components/editor/handles/resize-snap.ts +10 -1
  27. package/src/components/editor/handles/use-handle-drag.ts +18 -1
  28. package/src/components/editor/index.tsx +140 -21
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +47 -58
  31. package/src/components/editor/selection-manager.tsx +104 -42
  32. package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
  33. package/src/components/editor/snapshot-capture.test.ts +422 -0
  34. package/src/components/editor/snapshot-capture.ts +145 -0
  35. package/src/components/editor/thumbnail-generator.tsx +284 -237
  36. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  37. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  38. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  39. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  41. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  42. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  43. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  44. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  45. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  46. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +180 -32
  47. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  48. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  49. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  50. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  51. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  52. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  53. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  54. package/src/components/systems/selection-affordance-services.ts +19 -0
  55. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  56. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  57. package/src/components/tools/fence/fence-drafting.ts +6 -25
  58. package/src/components/tools/item/draft-creation.test.ts +12 -0
  59. package/src/components/tools/item/draft-creation.ts +10 -0
  60. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  61. package/src/components/tools/item/face-host-commit.ts +56 -0
  62. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  63. package/src/components/tools/item/face-host-preview.ts +82 -0
  64. package/src/components/tools/item/move-tool.tsx +4 -3
  65. package/src/components/tools/item/placement-math.test.ts +11 -1
  66. package/src/components/tools/item/placement-math.ts +0 -20
  67. package/src/components/tools/item/placement-strategies.test.ts +331 -4
  68. package/src/components/tools/item/placement-strategies.ts +135 -2
  69. package/src/components/tools/item/placement-surface.test.ts +61 -0
  70. package/src/components/tools/item/placement-surface.ts +24 -0
  71. package/src/components/tools/item/placement-types.ts +6 -0
  72. package/src/components/tools/item/test-face-host.ts +121 -0
  73. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  74. package/src/components/tools/item/use-draft-node.ts +26 -4
  75. package/src/components/tools/item/use-placement-coordinator.tsx +428 -153
  76. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  77. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  78. package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
  79. package/src/components/tools/registry-tool-context.tsx +30 -0
  80. package/src/components/tools/select/box-select-tool.tsx +3 -1
  81. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  82. package/src/components/tools/select/marquee-footprint.ts +29 -0
  83. package/src/components/tools/shared/placement-box.tsx +57 -7
  84. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  85. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  86. package/src/components/tools/shared/pointer-support-cap.ts +45 -8
  87. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  88. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  89. package/src/components/tools/stair/stair-tool.tsx +127 -74
  90. package/src/components/tools/tool-manager.tsx +25 -8
  91. package/src/components/tools/wall/wall-drafting.test.ts +320 -2
  92. package/src/components/tools/wall/wall-drafting.ts +23 -114
  93. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  94. package/src/components/ui/action-menu/index.tsx +11 -1
  95. package/src/components/ui/command-palette/editor-commands.tsx +9 -12
  96. package/src/components/ui/command-palette/index.tsx +7 -2
  97. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  98. package/src/components/ui/controls/material-picker.tsx +4 -4
  99. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  100. package/src/components/ui/controls/segmented-control.tsx +8 -1
  101. package/src/components/ui/controls/slider-control.tsx +19 -3
  102. package/src/components/ui/controls/toggle-control.tsx +18 -5
  103. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  104. package/src/components/ui/floating-level-selector.tsx +22 -5
  105. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  106. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  107. package/src/components/ui/helpers/item-helper.tsx +1 -1
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  109. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  110. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  111. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  112. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  113. package/src/components/ui/panels/multi-field-value.ts +163 -0
  114. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  115. package/src/components/ui/panels/multi-parametric-inspector.tsx +263 -0
  116. package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
  117. package/src/components/ui/panels/node-display.ts +4 -0
  118. package/src/components/ui/panels/panel-manager.tsx +77 -3
  119. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  120. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  121. package/src/components/ui/panels/parametric-inspector.tsx +29 -162
  122. package/src/components/ui/panels/reference-panel.tsx +31 -1
  123. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  124. package/src/components/ui/scene-loader.tsx +35 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
  127. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
  128. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
  129. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  130. package/src/components/ui/sidebar/panels/site-panel/index.tsx +116 -46
  131. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  132. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  133. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  134. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +14 -6
  135. package/src/components/ui/sidebar/tab-bar.tsx +2 -0
  136. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  137. package/src/components/viewer/viewer-scene-header.tsx +2 -1
  138. package/src/components/viewer/viewer-stage.tsx +4 -1
  139. package/src/components/viewer-overlay.tsx +1 -22
  140. package/src/components/viewer-zone-system.tsx +6 -2
  141. package/src/components/walkthrough-hud.tsx +19 -10
  142. package/src/hooks/use-auto-save.test.ts +73 -1
  143. package/src/hooks/use-auto-save.ts +76 -5
  144. package/src/hooks/use-ceiling-events.test.ts +122 -0
  145. package/src/hooks/use-drag-action.ts +17 -3
  146. package/src/hooks/use-grid-events.test.ts +240 -0
  147. package/src/hooks/use-grid-events.ts +190 -11
  148. package/src/hooks/use-keyboard.test.ts +244 -0
  149. package/src/hooks/use-keyboard.ts +129 -62
  150. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  151. package/src/hooks/use-save-shortcut.ts +33 -0
  152. package/src/index.tsx +82 -4
  153. package/src/lib/active-placement-surface.test.ts +16 -0
  154. package/src/lib/active-placement-surface.ts +14 -0
  155. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  156. package/src/lib/ceiling-surface-raycast.ts +12 -0
  157. package/src/lib/contextual-help-extension.ts +17 -0
  158. package/src/lib/continuation.test.ts +11 -0
  159. package/src/lib/continuation.ts +8 -1
  160. package/src/lib/direct-manipulation.test.ts +132 -0
  161. package/src/lib/direct-manipulation.ts +38 -1
  162. package/src/lib/editor-api.ts +6 -3
  163. package/src/lib/elevation-guides.test.ts +28 -1
  164. package/src/lib/elevation-guides.ts +22 -7
  165. package/src/lib/floating-menu-scale.test.ts +29 -0
  166. package/src/lib/floating-menu-scale.ts +14 -0
  167. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  168. package/src/lib/floorplan/apply-alignment.ts +10 -1
  169. package/src/lib/floorplan/floorplan-export.test.ts +397 -1
  170. package/src/lib/floorplan/floorplan-export.tsx +111 -16
  171. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  172. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  173. package/src/lib/floorplan/floorplan-readonly.ts +1 -0
  174. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  175. package/src/lib/floorplan-grid-event-point.ts +13 -0
  176. package/src/lib/glb-export.test.ts +1169 -21
  177. package/src/lib/glb-export.ts +629 -101
  178. package/src/lib/grid-event-presentation.ts +16 -0
  179. package/src/lib/history.test.ts +528 -147
  180. package/src/lib/history.ts +81 -7
  181. package/src/lib/host-tree-children.test.ts +27 -0
  182. package/src/lib/host-tree-children.ts +79 -0
  183. package/src/lib/interaction/hot-set.test.ts +10 -1
  184. package/src/lib/interaction/hot-set.ts +3 -1
  185. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  186. package/src/lib/interaction/overlay-policy.ts +16 -0
  187. package/src/lib/interaction/registered-drafting.ts +36 -0
  188. package/src/lib/interaction/scope.ts +31 -0
  189. package/src/lib/level-print-export.test.ts +484 -0
  190. package/src/lib/level-print-export.ts +526 -0
  191. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  192. package/src/lib/local-project-presentation-persistence.ts +335 -0
  193. package/src/lib/material-paint.ts +2 -0
  194. package/src/lib/model-export.ts +29 -0
  195. package/src/lib/paint-preview-owner.ts +71 -0
  196. package/src/lib/planar-cursor-placement.test.ts +178 -1
  197. package/src/lib/planar-cursor-placement.ts +61 -5
  198. package/src/lib/plugin-panels.test.ts +92 -1
  199. package/src/lib/plugin-panels.ts +48 -0
  200. package/src/lib/portable-export.test.ts +366 -0
  201. package/src/lib/portable-export.ts +969 -0
  202. package/src/lib/print-3mf.test.ts +86 -0
  203. package/src/lib/print-3mf.ts +161 -0
  204. package/src/lib/print-content-scope.test.ts +107 -0
  205. package/src/lib/print-content-scope.ts +90 -0
  206. package/src/lib/print-export.test.ts +294 -0
  207. package/src/lib/print-export.ts +735 -0
  208. package/src/lib/print-feature-thickness.test.ts +147 -0
  209. package/src/lib/print-feature-thickness.ts +180 -0
  210. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  211. package/src/lib/print-golden-house.test.ts +249 -0
  212. package/src/lib/print-roof-solids.test.ts +133 -0
  213. package/src/lib/print-roof-solids.ts +630 -0
  214. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  215. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  216. package/src/lib/print-shell-compiler-manifold-core.ts +330 -0
  217. package/src/lib/print-shell-compiler-manifold-worker.ts +187 -0
  218. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  219. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  220. package/src/lib/print-shell-compiler-protocol.ts +30 -0
  221. package/src/lib/print-shell-compiler.ts +273 -0
  222. package/src/lib/print-wall-solids.test.ts +194 -0
  223. package/src/lib/print-wall-solids.ts +328 -0
  224. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  225. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  226. package/src/lib/scene.ts +22 -8
  227. package/src/lib/selection-routing.test.ts +105 -1
  228. package/src/lib/selection-routing.ts +19 -0
  229. package/src/lib/sfx-bus.ts +4 -0
  230. package/src/lib/sfx-player.ts +8 -0
  231. package/src/lib/snapping-mode.test.ts +32 -4
  232. package/src/lib/snapping-mode.ts +11 -7
  233. package/src/lib/usdz-export.ts +38 -0
  234. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  235. package/src/store/live-draft-preview-stores.test.ts +3 -0
  236. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  237. package/src/store/tool-mode.test.ts +87 -0
  238. package/src/store/use-camera-hint-focus.ts +48 -0
  239. package/src/store/use-editor.tsx +299 -188
  240. package/src/store/use-floorplan-draft-preview.ts +9 -0
  241. package/src/store/use-interaction-scope.test.ts +23 -1
  242. package/src/store/use-interaction-scope.ts +63 -2
  243. package/src/store/use-placement-preview.ts +56 -3
  244. package/src/components/tools/roof/roof-tool.tsx +0 -656
  245. package/src/components/ui/helpers/roof-helper.tsx +0 -14
@@ -1,32 +1,46 @@
1
1
  'use client'
2
2
 
3
3
  import { emitter, useScene } from '@pascal-app/core'
4
- import { useViewer } from '@pascal-app/viewer'
4
+ import { disposeObject3DResources, snapLevelsToTruePositions, useViewer } from '@pascal-app/viewer'
5
5
  import { useThree } from '@react-three/fiber'
6
6
  import { useEffect } from 'react'
7
7
  import * as THREE from 'three'
8
8
  import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js'
9
9
  import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js'
10
10
  import { exportSceneToGlb, nextFrames, prepareSceneForExport } from '../../lib/glb-export'
11
+ import { exportSceneLevelsForPrint } from '../../lib/level-print-export'
12
+ import type { ModelExport, ModelExportArtifact } from '../../lib/model-export'
13
+ import {
14
+ expandInstancedMeshes,
15
+ fixReflectedMeshWinding,
16
+ freezeDeformedMeshes,
17
+ } from '../../lib/portable-export'
18
+ import { exportSceneToPrint3mf } from '../../lib/print-3mf'
19
+ import { filterPreparedSceneForPrintContent } from '../../lib/print-content-scope'
20
+ import { exportSceneToPrintStl, mergePrintExportDiagnostics } from '../../lib/print-export'
21
+ import { applySemanticPrintFeatureThickness } from '../../lib/print-feature-thickness'
22
+ import { compileSemanticPrintShellWithManifold } from '../../lib/print-shell-compiler-manifold-worker'
23
+ import { exportSceneToUsdz } from '../../lib/usdz-export'
24
+ import useEditor from '../../store/use-editor'
11
25
 
12
26
  // prepareSceneForExport neutralises container meshes (door/window hitbox roots,
13
27
  // material-less renderables) with an attribute-less geometry — GLTFExporter
14
28
  // emits those as plain transform nodes, but STL/OBJExporter read
15
29
  // `position.count` unconditionally and crash. Swap in a geometry with an empty
16
- // (count-0) position so they iterate zero vertices instead. Shared: the export
17
- // scene is a throwaway clone, only its geometry *ref* is swapped.
18
- const EMPTY_POSITION_GEOMETRY = new THREE.BufferGeometry()
19
- EMPTY_POSITION_GEOMETRY.setAttribute(
20
- 'position',
21
- new THREE.Float32BufferAttribute(new Float32Array(0), 3),
22
- )
30
+ // (count-0) position so they iterate zero vertices instead. Each replacement
31
+ // is export-owned and disposed with its prepared scene.
32
+ function createEmptyPositionGeometry(): THREE.BufferGeometry {
33
+ const geometry = new THREE.BufferGeometry()
34
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(0), 3))
35
+ return geometry
36
+ }
23
37
 
24
38
  function ensurePositionAttributes(root: THREE.Object3D) {
25
39
  root.traverse((object) => {
26
40
  const renderable = object as THREE.Mesh & { isLine?: boolean; isPoints?: boolean }
27
41
  if (!(renderable.isMesh || renderable.isLine || renderable.isPoints)) return
28
42
  if (!renderable.geometry?.getAttribute('position')) {
29
- renderable.geometry = EMPTY_POSITION_GEOMETRY
43
+ renderable.geometry = createEmptyPositionGeometry()
30
44
  }
31
45
  })
32
46
  }
@@ -34,14 +48,15 @@ function ensurePositionAttributes(root: THREE.Object3D) {
34
48
  export function ExportManager() {
35
49
  const scene = useThree((state) => state.scene)
36
50
  const setExportScene = useViewer((state) => state.setExportScene)
51
+ const setModelExport = useEditor((state) => state.setModelExport)
37
52
 
38
53
  useEffect(() => {
39
- const exportFn = async (format: 'glb' | 'stl' | 'obj' = 'glb') => {
54
+ const exportFn: ModelExport = async (format = 'glb', options = {}) => {
40
55
  // Find the scene renderer group by name
41
56
  const sceneGroup = scene.getObjectByName('scene-renderer')
42
57
  if (!sceneGroup) {
43
58
  console.error('scene-renderer group not found')
44
- return
59
+ return null
45
60
  }
46
61
 
47
62
  const date = new Date().toISOString().split('T')[0]
@@ -55,10 +70,23 @@ export function ExportManager() {
55
70
  await nextFrames()
56
71
 
57
72
  if (format === 'glb') {
58
- const buffer = await exportSceneToGlb(sceneGroup, useScene.getState().nodes)
73
+ const warnings: string[] = []
74
+ const buffer = await exportSceneToGlb(sceneGroup, useScene.getState().nodes, {
75
+ ...options,
76
+ onWarning: (warning) => warnings.push(warning),
77
+ })
59
78
  const blob = new Blob([buffer], { type: 'model/gltf-binary' })
60
- downloadBlob(blob, `model_${date}.glb`)
61
- return
79
+ return finishArtifact(blob, `model_${date}.glb`, options.download, undefined, warnings)
80
+ }
81
+
82
+ if (format === 'usdz') {
83
+ const warnings: string[] = []
84
+ const data = await exportSceneToUsdz(sceneGroup, useScene.getState().nodes, {
85
+ ...options,
86
+ onWarning: (warning) => warnings.push(warning),
87
+ })
88
+ const blob = new Blob([data], { type: 'model/vnd.usdz+zip' })
89
+ return finishArtifact(blob, `model_${date}.usdz`, options.download, undefined, warnings)
62
90
  }
63
91
 
64
92
  // Hide editor affordances that live on the scene layer (selection handles,
@@ -66,45 +94,159 @@ export function ExportManager() {
66
94
  // synchronous capture path thumbnails use. We clone the scene inside the
67
95
  // window, so the export snapshots the clean building, then restore.
68
96
  emitter.emit('thumbnail:before-capture', undefined)
97
+ const restoreLevels = snapLevelsToTruePositions()
98
+ const nodes = useScene.getState().nodes
69
99
  let prepared: ReturnType<typeof prepareSceneForExport>
70
100
  try {
71
- prepared = prepareSceneForExport(sceneGroup, useScene.getState().nodes)
101
+ prepared = prepareSceneForExport(sceneGroup, nodes, {
102
+ ...options,
103
+ requireSynchronousBake: format === 'stl' || format === 'obj',
104
+ })
72
105
  } finally {
106
+ restoreLevels()
73
107
  emitter.emit('thumbnail:after-capture', undefined)
74
108
  }
75
- const { scene: exportScene } = prepared
76
- ensurePositionAttributes(exportScene)
77
-
78
- if (format === 'stl') {
79
- const exporter = new STLExporter()
80
- const result = exporter.parse(exportScene, { binary: true })
81
- const blob = new Blob([result], { type: 'model/stl' })
82
- downloadBlob(blob, `model_${date}.stl`)
83
- return
84
- }
109
+ try {
110
+ let { scene: exportScene } = prepared
111
+ const printContent = options.printContent ?? 'structure'
112
+ const isPrintFormat = format === 'print-stl' || format === 'print-3mf'
113
+ if (isPrintFormat) {
114
+ exportScene = filterPreparedSceneForPrintContent(exportScene, nodes, printContent)
115
+ }
116
+ if (!isPrintFormat) {
117
+ expandInstancedMeshes(exportScene)
118
+ exportScene.updateMatrixWorld(true)
119
+ freezeDeformedMeshes(exportScene)
120
+ fixReflectedMeshWinding(exportScene)
121
+ }
122
+ ensurePositionAttributes(exportScene)
123
+
124
+ if (isPrintFormat) {
125
+ const printFormat = format === 'print-3mf' ? '3mf' : 'stl'
126
+ const scale = options.printScale ?? 100
127
+ const compileShells = printContent === 'structure'
128
+ const minimumFeatureMm = compileShells ? options.printMinimumFeatureMm : undefined
129
+ if (options.printScope === 'levels') {
130
+ const plinth =
131
+ options.printBase === 'plinth'
132
+ ? {
133
+ marginMm: options.printPlinthMarginMm ?? 2,
134
+ thicknessMm: options.printPlinthThicknessMm ?? 2,
135
+ }
136
+ : undefined
137
+ const { data, report } = await exportSceneLevelsForPrint(exportScene, nodes, {
138
+ scale,
139
+ format: printFormat,
140
+ plinth,
141
+ minimumFeatureMm,
142
+ compileShells,
143
+ compileShell: compileShells ? compileSemanticPrintShellWithManifold : undefined,
144
+ })
145
+ const blob = new Blob([data], {
146
+ type: printFormat === '3mf' ? 'model/3mf' : 'application/zip',
147
+ })
148
+ return finishArtifact(
149
+ blob,
150
+ `print_levels_1-${scale}_${date}.${printFormat === '3mf' ? '3mf' : 'zip'}`,
151
+ options.download,
152
+ report,
153
+ )
154
+ }
155
+ if (options.printBase === 'plinth') {
156
+ throw new Error('Plinth generation is available only for per-level print packages.')
157
+ }
158
+ const compiled = compileShells
159
+ ? await compileSemanticPrintShellWithManifold(exportScene, nodes)
160
+ : null
161
+ try {
162
+ const printSource = compiled ? (compiled.scene ?? new THREE.Group()) : exportScene
163
+ const printOptions = {
164
+ scale,
165
+ compiled: compiled?.status === 'compiled',
166
+ indexedTopology: compiled?.backend === 'manifold-3d',
167
+ }
168
+ const output =
169
+ printFormat === '3mf'
170
+ ? exportSceneToPrint3mf(printSource, printOptions)
171
+ : exportSceneToPrintStl(printSource, printOptions)
172
+ let report = compiled
173
+ ? mergePrintExportDiagnostics(
174
+ output.report,
175
+ compiled.diagnostics,
176
+ new Set(['compiler_pending']),
177
+ )
178
+ : output.report
179
+ if (compiled) {
180
+ report = applySemanticPrintFeatureThickness(
181
+ report,
182
+ nodes,
183
+ compiled.sourceNodeIds,
184
+ minimumFeatureMm,
185
+ )
186
+ }
187
+ const { buffer } = output
188
+ const blob = new Blob([buffer], {
189
+ type: printFormat === '3mf' ? 'model/3mf' : 'model/stl',
190
+ })
191
+ return finishArtifact(
192
+ blob,
193
+ `print_model_1-${scale}_${date}.${printFormat}`,
194
+ options.download,
195
+ report,
196
+ )
197
+ } finally {
198
+ if (compiled?.scene) disposeObject3DResources(compiled.scene)
199
+ }
200
+ }
85
201
 
86
- if (format === 'obj') {
87
- const exporter = new OBJExporter()
88
- const result = exporter.parse(exportScene)
89
- const blob = new Blob([result], { type: 'model/obj' })
90
- downloadBlob(blob, `model_${date}.obj`)
91
- return
202
+ if (format === 'stl') {
203
+ const exporter = new STLExporter()
204
+ const result = exporter.parse(exportScene, { binary: true })
205
+ const blob = new Blob([result], { type: 'model/stl' })
206
+ return finishArtifact(blob, `model_${date}.stl`, options.download)
207
+ }
208
+
209
+ if (format === 'obj') {
210
+ const exporter = new OBJExporter()
211
+ const result = exporter.parse(exportScene)
212
+ const blob = new Blob([result], { type: 'model/obj' })
213
+ return finishArtifact(blob, `model_${date}.obj`, options.download)
214
+ }
215
+
216
+ return null
217
+ } finally {
218
+ prepared.dispose()
92
219
  }
93
220
  } finally {
94
221
  useViewer.getState().setExporting(false)
95
222
  }
96
223
  }
97
224
 
98
- setExportScene(exportFn)
225
+ setModelExport(exportFn)
226
+ setExportScene(async (format = 'glb') => {
227
+ await exportFn(format, { onlyVisible: true })
228
+ })
99
229
 
100
230
  return () => {
231
+ setModelExport(null)
101
232
  setExportScene(null)
102
233
  }
103
- }, [scene, setExportScene])
234
+ }, [scene, setExportScene, setModelExport])
104
235
 
105
236
  return null
106
237
  }
107
238
 
239
+ function finishArtifact(
240
+ blob: Blob,
241
+ filename: string,
242
+ download: boolean | undefined,
243
+ metadata?: unknown,
244
+ warnings?: readonly string[],
245
+ ): ModelExportArtifact {
246
+ if (download !== false) downloadBlob(blob, filename)
247
+ return { blob, filename, metadata, warnings: warnings?.length ? warnings : undefined }
248
+ }
249
+
108
250
  function downloadBlob(blob: Blob, filename: string) {
109
251
  const url = URL.createObjectURL(blob)
110
252
  const link = document.createElement('a')
@@ -15,9 +15,11 @@ import {
15
15
  registerNode,
16
16
  ShelfNode,
17
17
  SiteNode,
18
+ SlabNode,
18
19
  sceneRegistry,
19
20
  useScene,
20
21
  } from '@pascal-app/core'
22
+ import { hideFromScene, STAND_CLEARANCE, showInScene } from '@pascal-app/viewer'
21
23
  import { BoxGeometry, Group, Mesh, MeshBasicMaterial, Raycaster, Vector3 } from 'three'
22
24
  import { buildFirstPersonColliderWorldFromRegistry } from './build-collider-world'
23
25
 
@@ -91,6 +93,34 @@ describe('buildFirstPersonColliderWorldFromRegistry', () => {
91
93
  world?.dispose()
92
94
  })
93
95
 
96
+ test('standing clearance and floor hits survive a slab source joining and leaving a batch', () => {
97
+ registerColliderDefinition('slab', SlabNode, 'structure', 'floor')
98
+ const slab = SlabNode.parse({ id: 'slab_clearance_batch', polygon: [] })
99
+ setSceneNodes([slab])
100
+ mountNode(slab, [4, 0.2, 4], [0, 2, 0])
101
+ const source = sceneRegistry.nodes.get(slab.id)!.children[0] as Mesh
102
+ const raycaster = new Raycaster()
103
+ for (const batched of [false, true, false]) {
104
+ if (batched) hideFromScene(source, 'batched')
105
+ else showInScene(source, 'batched')
106
+ const world = buildFirstPersonColliderWorldFromRegistry()!
107
+ expect(world).not.toBeNull()
108
+ try {
109
+ raycaster.set(new Vector3(0, 3, 0), new Vector3(0, -1, 0))
110
+ raycaster.far = STAND_CLEARANCE
111
+ expect(raycaster.intersectObject(world.mesh, false)[0]!.point.y).toBeCloseTo(2.1)
112
+ raycaster.set(new Vector3(0, 1, 0), new Vector3(0, 1, 0))
113
+ expect(raycaster.intersectObjects([world.mesh], false)[0]!.point.y).toBeCloseTo(1.9)
114
+ raycaster.set(new Vector3(0, 2.5, 0), new Vector3(0, 1, 0))
115
+ expect(raycaster.intersectObjects([world.mesh], false)).toHaveLength(0)
116
+ } finally {
117
+ world.dispose()
118
+ }
119
+ }
120
+ source.geometry.dispose()
121
+ ;(source.material as MeshBasicMaterial).dispose()
122
+ })
123
+
94
124
  test('excludes ceiling surfaces so the walkthrough player passes through them', () => {
95
125
  registerColliderDefinition('column', ColumnNode, 'structure')
96
126
  registerColliderDefinition('ceiling', CeilingNode, 'structure', 'ceiling')