fix(editor): guard GLB export against undefined array-material slots (Sentry MONOREPO-EDITOR-79) (#459)

Co-authored-by: Anton Pascal <anton-pascal@users.noreply.github.com>
This commit is contained in:
Anton
2026-07-19 17:40:01 +02:00
committed by GitHub
co-authored by Anton Pascal
parent 6cc10c929e
commit d0ac9fb739
2 changed files with 34 additions and 0 deletions
@@ -107,6 +107,26 @@ describe('prepareSceneForExport', () => {
expect(visibleChildren).toHaveLength(1)
})
test('fills undefined slots in an array material so no undefined survives the prune', () => {
// Multi-material / group geometry where one slot was never assigned:
// `mesh.material = [validMat, undefined]`. The scalar-null guard doesn't
// catch this (an array is never `== null`), so the undefined slot used to
// reach GLTFExporter and crash on `material.isShaderMaterial`.
const root = new THREE.Group()
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1))
mesh.material = [nodeMaterial(), undefined as unknown as THREE.Material]
root.add(mesh)
const { scene } = prepareSceneForExport(root, {})
const exported = scene.children[0] as THREE.Mesh
const materials = exported.material as THREE.Material[]
expect(Array.isArray(materials)).toBe(true)
expect(materials).toHaveLength(2)
// No undefined/null slot survives; every slot is a real material.
expect(materials.every((m) => m != null)).toBe(true)
})
test('stamps identity from the scene registry and strips other userData', () => {
const root = new THREE.Group()
const doorGroup = new THREE.Group()
+14
View File
@@ -228,6 +228,20 @@ function pruneNonRenderableMeshes(root: THREE.Object3D, identityNodes: Set<THREE
}
return
}
// The scalar branch above only catches `material == null`; an ARRAY material
// (multi-material / group geometry) is never `== null`, so a slot that was
// never assigned — e.g. `[validMat, undefined]` — slips through and reaches
// GLTFExporter, which reads `material.isShaderMaterial` on every group's
// material and crashes on the undefined slot. Don't drop the mesh (the other
// slots may be real geometry): fill any null/undefined holes with the hidden
// placeholder so the exporter never sees an undefined material.
if (
(renderable.isMesh === true || renderable.isLine === true || renderable.isPoints === true) &&
Array.isArray(renderable.material) &&
renderable.material.some((m) => m == null)
) {
renderable.material = renderable.material.map((m) => m ?? PLACEHOLDER_MATERIAL)
}
const mesh = object as THREE.Mesh
if (!mesh.isMesh || isRenderableMesh(mesh)) return
if (mesh.children.length > 0) {