File size: 2,890 Bytes
31c7d49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/** Alpha lifecycle example; opaque photos additionally require a local removeBackground callback. */
import { useEffect, useRef, useState } from 'react'
import {
  CancelledError,
  TripoSplatWebGPU,
  type GaussianScene,
  type GenerationProgress,
} from '@ai3d/triposplat-webgpu'

export function TripoSplatPanel({ modelBaseUrl }: { modelBaseUrl: string }) {
  const modelRef = useRef<TripoSplatWebGPU | null>(null)
  const sceneRef = useRef<GaussianScene | null>(null)
  const operationRef = useRef<AbortController | null>(null)
  const [status, setStatus] = useState('Checking WebGPU…')
  const [ready, setReady] = useState(false)

  useEffect(() => {
    const lifecycle = new AbortController()
    const model = new TripoSplatWebGPU({ modelBaseUrl, cache: 'opfs' })
    modelRef.current = model

    void (async () => {
      const report = await TripoSplatWebGPU.checkCompatibility()
      if (!report.supported) throw new Error(report.blockers.join('\n'))
      await model.load({
        signal: lifecycle.signal,
        onProgress: ({ message }) => setStatus(message),
      })
      if (!lifecycle.signal.aborted) {
        setReady(true)
        setStatus('Ready')
      }
    })().catch((error: unknown) => {
      if (!lifecycle.signal.aborted) setStatus(String(error))
    })

    return () => {
      lifecycle.abort()
      operationRef.current?.abort()
      sceneRef.current?.dispose()
      modelRef.current = null
      sceneRef.current = null
      void model.dispose()
    }
  }, [modelBaseUrl])

  async function generate(file: File) {
    const model = modelRef.current
    if (!model) return

    operationRef.current?.abort()
    sceneRef.current?.dispose()
    const controller = new AbortController()
    operationRef.current = controller

    const onProgress = (progress: GenerationProgress) => {
      const calls = progress.totalInvocations
        ? ` (${progress.invocation}/${progress.totalInvocations} DiT calls)`
        : ''
      setStatus(`${progress.message}${calls}`)
    }

    try {
      const scene = await model.generate(file, {
        steps: 20,
        guidanceScale: 3,
        gaussianCount: 262144,
        seed: 42,
        signal: controller.signal,
        onProgress,
      })
      sceneRef.current = scene
      setStatus(`Generated ${scene.count.toLocaleString()} Gaussians`)
    } catch (error) {
      setStatus(error instanceof CancelledError ? 'Cancelled' : String(error))
    }
  }

  return (
    <section>
      <p>{status}</p>
      <input
        aria-label="Source image"
        type="file"
        accept="image/*"
        disabled={!ready}
        onChange={(event) => {
          const file = event.currentTarget.files?.[0]
          if (file) void generate(file)
        }}
      />
      <button type="button" onClick={() => operationRef.current?.abort()}>
        Cancel
      </button>
    </section>
  )
}