import { BackgroundRemovalRequiredError, CancelledError, } from '@ai3d/triposplat-webgpu' import { ImageToSplatGenerator, type ImageToSplatResult, } from './image-to-splat' const imageInput = document.querySelector('#image')! const generateButton = document.querySelector('#generate')! const cancelButton = document.querySelector('#cancel')! const plyButton = document.querySelector('#download-ply')! const splatButton = document.querySelector('#download-splat')! const status = document.querySelector('#status')! const modelBaseUrl = document.querySelector( 'meta[name="triposplat-model-base-url"]', )?.content if (!modelBaseUrl) throw new Error('Set the triposplat-model-base-url meta tag.') const generator = new ImageToSplatGenerator({ modelBaseUrl, manifestUrl: 'manifest.json', executionProviders: ['webgpu'], cache: 'opfs', }) let controller: AbortController | undefined let result: ImageToSplatResult | undefined function setResult(next?: ImageToSplatResult) { result = next plyButton.disabled = !next splatButton.disabled = !next } function download(blob: Blob, name: string) { const url = URL.createObjectURL(blob) const anchor = document.createElement('a') anchor.href = url anchor.download = name anchor.click() URL.revokeObjectURL(url) } const compatibility = await ImageToSplatGenerator.checkCompatibility() if (!compatibility.supported) { status.textContent = compatibility.blockers.join('\n') || 'WebGPU is unavailable.' generateButton.disabled = true } else { status.textContent = 'Ready. Select an alpha-bearing PNG or prepared image.' } cancelButton.addEventListener('click', () => controller?.abort()) plyButton.addEventListener('click', () => { if (result) download(result.ply, 'triposplat.ply') }) splatButton.addEventListener('click', () => { if (result) download(result.splat, 'triposplat.splat') }) generateButton.addEventListener('click', async () => { const file = imageInput.files?.[0] if (!file) { status.textContent = 'Choose an image first.' return } controller?.abort() controller = new AbortController() setResult() generateButton.disabled = true try { await generator.initialize({ signal: controller.signal, onProgress: (event) => { status.textContent = event.message }, }) const next = await generator.generate(file, { steps: 4, gaussianCount: 262144, seed: 42, signal: controller.signal, onProgress: (event) => { status.textContent = event.message }, }) setResult(next) status.textContent = [ `Generated ${next.count.toLocaleString()} Gaussians.`, `PLY: ${(next.ply.size / 1_048_576).toFixed(1)} MiB`, `.splat: ${(next.splat.size / 1_048_576).toFixed(1)} MiB`, `Elapsed: ${(next.elapsedMs / 1000).toFixed(1)} s`, ].join('\n') } catch (error) { if (error instanceof CancelledError) { status.textContent = 'Cancelled. The next run will create a clean worker.' } else if (error instanceof BackgroundRemovalRequiredError) { status.textContent = 'This opaque image needs a local background remover. Use an alpha-bearing PNG.' } else { status.textContent = error instanceof Error ? error.message : String(error) } } finally { generateButton.disabled = false } }) window.addEventListener('pagehide', () => { controller?.abort() void generator.dispose() }, { once: true })