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 { 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')
|
||||||
|
|||||||
Reference in New Issue
Block a user