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:
Anton
2026-06-03 15:57:05 -04:00
committed by GitHub
parent cefcb013d9
commit 2809b016f0
@@ -3,6 +3,7 @@
import { useViewer } from '@pascal-app/viewer' import { useViewer } from '@pascal-app/viewer'
import { useThree } from '@react-three/fiber' import { useThree } from '@react-three/fiber'
import { useEffect } from 'react' import { useEffect } from 'react'
import type { Mesh, Object3D } from 'three'
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js' import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js' import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js'
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.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 date = new Date().toISOString().split('T')[0]
const exportScene = prepareSceneForExport(sceneGroup)
if (format === 'stl') { if (format === 'stl') {
const exporter = new STLExporter() 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' }) const blob = new Blob([result], { type: 'model/stl' })
downloadBlob(blob, `model_${date}.stl`) downloadBlob(blob, `model_${date}.stl`)
return return
@@ -32,7 +34,7 @@ export function ExportManager() {
if (format === 'obj') { if (format === 'obj') {
const exporter = new OBJExporter() const exporter = new OBJExporter()
const result = exporter.parse(sceneGroup) const result = exporter.parse(exportScene)
const blob = new Blob([result], { type: 'model/obj' }) const blob = new Blob([result], { type: 'model/obj' })
downloadBlob(blob, `model_${date}.obj`) downloadBlob(blob, `model_${date}.obj`)
return return
@@ -43,7 +45,7 @@ export function ExportManager() {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
exporter.parse( exporter.parse(
sceneGroup, exportScene,
(gltf) => { (gltf) => {
const blob = new Blob([gltf as ArrayBuffer], { type: 'model/gltf-binary' }) const blob = new Blob([gltf as ArrayBuffer], { type: 'model/gltf-binary' })
downloadBlob(blob, `model_${date}.glb`) downloadBlob(blob, `model_${date}.glb`)
@@ -68,6 +70,33 @@ export function ExportManager() {
return null 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) { function downloadBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
const link = document.createElement('a') const link = document.createElement('a')