Skip to content

Commit 2809b01

Browse files
authored
fix(editor): prune only invalid-geometry meshes before export (pascalorg#313)
Three exporters (STL/OBJ/GLTF) crash when traversing a Mesh with missing/disposed/empty geometry (EDITOR-6H/6G/79). `prepareSceneForExport()` clones the scene and removes ONLY meshes whose position attribute is missing or zero-count — collected in a single traverse and removed afterward (no mutation during traversal). Lines, points, groups, cameras, lights, bones and valid skinned meshes are preserved, so OBJ/GLB output is no longer corrupted. STL/OBJ still throw and GLTF still rejects on other errors — no silent no-op. Reimplemented against main without the original PR's all-non-mesh pruning, mutate-during-traverse loop, and error-swallowing try/catch. Verified: `tsc -p apps/editor` clean, biome clean.
1 parent cefcb01 commit 2809b01

1 file changed

Lines changed: 32 additions & 3 deletions

File tree

packages/editor/src/components/editor/export-manager.tsx

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useViewer } from '@pascal-app/viewer'
44
import { useThree } from '@react-three/fiber'
55
import { useEffect } from 'react'
6+
import type { Mesh, Object3D } from 'three'
67
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'
78
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js'
89
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js'
@@ -21,18 +22,19 @@ export function ExportManager() {
2122
}
2223

2324
const date = new Date().toISOString().split('T')[0]
25+
const exportScene = prepareSceneForExport(sceneGroup)
2426

2527
if (format === 'stl') {
2628
const exporter = new STLExporter()
27-
const result = exporter.parse(sceneGroup, { binary: true })
29+
const result = exporter.parse(exportScene, { binary: true })
2830
const blob = new Blob([result], { type: 'model/stl' })
2931
downloadBlob(blob, `model_${date}.stl`)
3032
return
3133
}
3234

3335
if (format === 'obj') {
3436
const exporter = new OBJExporter()
35-
const result = exporter.parse(sceneGroup)
37+
const result = exporter.parse(exportScene)
3638
const blob = new Blob([result], { type: 'model/obj' })
3739
downloadBlob(blob, `model_${date}.obj`)
3840
return
@@ -43,7 +45,7 @@ export function ExportManager() {
4345

4446
return new Promise<void>((resolve, reject) => {
4547
exporter.parse(
46-
sceneGroup,
48+
exportScene,
4749
(gltf) => {
4850
const blob = new Blob([gltf as ArrayBuffer], { type: 'model/gltf-binary' })
4951
downloadBlob(blob, `model_${date}.glb`)
@@ -68,6 +70,33 @@ export function ExportManager() {
6870
return null
6971
}
7072

73+
function prepareSceneForExport(source: Object3D) {
74+
const clone = source.clone(true)
75+
const meshesToRemove: Mesh[] = []
76+
77+
clone.traverse((object) => {
78+
if (isMeshWithInvalidGeometry(object)) meshesToRemove.push(object)
79+
})
80+
81+
for (const mesh of meshesToRemove) {
82+
mesh.removeFromParent()
83+
}
84+
85+
return clone
86+
}
87+
88+
function isMeshWithInvalidGeometry(object: Object3D): object is Mesh {
89+
if (!isMesh(object)) return false
90+
91+
// Three exporters can crash when a Mesh has no readable position attribute.
92+
const position = object.geometry?.getAttribute('position')
93+
return !position || position.count === 0
94+
}
95+
96+
function isMesh(object: Object3D): object is Mesh {
97+
return (object as Mesh).isMesh === true
98+
}
99+
71100
function downloadBlob(blob: Blob, filename: string) {
72101
const url = URL.createObjectURL(blob)
73102
const link = document.createElement('a')

0 commit comments

Comments
 (0)