fix(editor): prune only invalid-geometry meshes before export (#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.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useThree } from '@react-three/fiber'
|
||||
import { useEffect } from 'react'
|
||||
import type { Mesh, Object3D } from 'three'
|
||||
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'
|
||||
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js'
|
||||
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js'
|
||||
@@ -21,10 +22,11 @@ export function ExportManager() {
|
||||
}
|
||||
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const exportScene = prepareSceneForExport(sceneGroup)
|
||||
|
||||
if (format === 'stl') {
|
||||
const exporter = new STLExporter()
|
||||
const result = exporter.parse(sceneGroup, { binary: true })
|
||||
const result = exporter.parse(exportScene, { binary: true })
|
||||
const blob = new Blob([result], { type: 'model/stl' })
|
||||
downloadBlob(blob, `model_${date}.stl`)
|
||||
return
|
||||
@@ -32,7 +34,7 @@ export function ExportManager() {
|
||||
|
||||
if (format === 'obj') {
|
||||
const exporter = new OBJExporter()
|
||||
const result = exporter.parse(sceneGroup)
|
||||
const result = exporter.parse(exportScene)
|
||||
const blob = new Blob([result], { type: 'model/obj' })
|
||||
downloadBlob(blob, `model_${date}.obj`)
|
||||
return
|
||||
@@ -43,7 +45,7 @@ export function ExportManager() {
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
exporter.parse(
|
||||
sceneGroup,
|
||||
exportScene,
|
||||
(gltf) => {
|
||||
const blob = new Blob([gltf as ArrayBuffer], { type: 'model/gltf-binary' })
|
||||
downloadBlob(blob, `model_${date}.glb`)
|
||||
@@ -68,6 +70,33 @@ export function ExportManager() {
|
||||
return null
|
||||
}
|
||||
|
||||
function prepareSceneForExport(source: Object3D) {
|
||||
const clone = source.clone(true)
|
||||
const meshesToRemove: Mesh[] = []
|
||||
|
||||
clone.traverse((object) => {
|
||||
if (isMeshWithInvalidGeometry(object)) meshesToRemove.push(object)
|
||||
})
|
||||
|
||||
for (const mesh of meshesToRemove) {
|
||||
mesh.removeFromParent()
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
function isMeshWithInvalidGeometry(object: Object3D): object is Mesh {
|
||||
if (!isMesh(object)) return false
|
||||
|
||||
// Three exporters can crash when a Mesh has no readable position attribute.
|
||||
const position = object.geometry?.getAttribute('position')
|
||||
return !position || position.count === 0
|
||||
}
|
||||
|
||||
function isMesh(object: Object3D): object is Mesh {
|
||||
return (object as Mesh).isMesh === true
|
||||
}
|
||||
|
||||
function downloadBlob(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
|
||||
Reference in New Issue
Block a user