fix: 3D asset disappears when switching to image output in app mode

Add onUnmounted cleanup to Preview3d to release WebGL context when
the component is destroyed by Vue's v-if chain.
This commit is contained in:
bymyself
2026-03-07 22:14:07 -08:00
parent b077a658f8
commit 0217e061b7
5 changed files with 20 additions and 53 deletions

View File

@@ -49,7 +49,6 @@ const attrs = useAttrs()
/>
<Preview3d
v-else-if="getMediaType(output) === '3d'"
:key="output.url"
:class="attrs.class as string"
:model-url="output.url"
/>

View File

@@ -75,25 +75,6 @@ describe('Preview3d', () => {
expect(cleanup).toHaveBeenCalledOnce()
})
it('cleans up before reinitializing when modelUrl changes', async () => {
const wrapper = await mountPreview3d('http://localhost/view?filename=a.glb')
vi.clearAllMocks()
await wrapper.setProps({
modelUrl: 'http://localhost/view?filename=b.glb'
})
await nextTick()
await nextTick()
const cleanupOrder = cleanup.mock.invocationCallOrder[0]
const initOrder = initializeStandaloneViewer.mock.invocationCallOrder[0]
expect(cleanupOrder).toBeLessThan(initOrder)
expect(initializeStandaloneViewer).toHaveBeenCalledWith(
expect.any(HTMLElement),
'http://localhost/view?filename=b.glb'
)
})
it('reinitializes correctly after unmount and remount', async () => {
const url = 'http://localhost/view?filename=model.glb'

View File

@@ -13,19 +13,10 @@ const containerRef = useTemplateRef('containerRef')
const viewer = ref(useLoad3dViewer())
watch(
[containerRef, () => modelUrl],
async () => {
if (!containerRef.value || !modelUrl) return
watch([containerRef, () => modelUrl], async () => {
if (!containerRef.value || !modelUrl) return
viewer.value.cleanup()
await viewer.value.initializeStandaloneViewer(containerRef.value, modelUrl)
},
{ flush: 'post' }
)
onUnmounted(() => {
viewer.value.cleanup()
await viewer.value.initializeStandaloneViewer(containerRef.value, modelUrl)
})
onUnmounted(() => {

View File

@@ -119,4 +119,19 @@ describe(flattenNodeOutput, () => {
expect(result).toHaveLength(1)
expect(result[0].mediaType).toBe('images')
})
it('excludes partial ResultItem objects missing required fields', () => {
const output = {
images: [
{ filename: 'valid.png', subfolder: '', type: 'output' },
{ filename: 'no-subfolder.png' },
{ subfolder: '', type: 'output' }
]
} as unknown as NodeExecutionOutput
const result = flattenNodeOutput(['1', output])
expect(result).toHaveLength(1)
expect(result[0].filename).toBe('valid.png')
})
})

View File

@@ -11,19 +11,8 @@ function isResultItemLike(item: unknown): item is ResultItem {
const candidate = item as Record<string, unknown>
if (
candidate.filename !== undefined &&
typeof candidate.filename !== 'string'
) {
return false
}
if (
candidate.subfolder !== undefined &&
typeof candidate.subfolder !== 'string'
) {
return false
}
if (typeof candidate.filename !== 'string') return false
if (typeof candidate.subfolder !== 'string') return false
if (
candidate.type !== undefined &&
@@ -32,14 +21,6 @@ function isResultItemLike(item: unknown): item is ResultItem {
return false
}
if (
candidate.filename === undefined &&
candidate.subfolder === undefined &&
candidate.type === undefined
) {
return false
}
return true
}