Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/editor/src/lib/glb-export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
14 changes: 14 additions & 0 deletions packages/editor/src/lib/glb-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,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) {
Expand Down
Loading