|
|
import { useRef, useEffect, useState } from 'react' |
|
|
import { Niivue } from '@niivue/niivue' |
|
|
|
|
|
interface NiiVueViewerProps { |
|
|
backgroundUrl: string |
|
|
overlayUrl?: string |
|
|
onError?: (error: string) => void |
|
|
} |
|
|
|
|
|
export function NiiVueViewer({ backgroundUrl, overlayUrl, onError }: NiiVueViewerProps) { |
|
|
const canvasRef = useRef<HTMLCanvasElement>(null) |
|
|
const nvRef = useRef<Niivue | null>(null) |
|
|
const onErrorRef = useRef(onError) |
|
|
const [loadError, setLoadError] = useState<string | null>(null) |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
onErrorRef.current = onError |
|
|
}) |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (!canvasRef.current) return |
|
|
|
|
|
const nv = new Niivue({ |
|
|
backColor: [0.05, 0.05, 0.05, 1], |
|
|
show3Dcrosshair: true, |
|
|
crosshairColor: [1, 0, 0, 0.5], |
|
|
}) |
|
|
nv.attachToCanvas(canvasRef.current) |
|
|
nvRef.current = nv |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return () => { |
|
|
|
|
|
const gl = nv.gl |
|
|
try { |
|
|
|
|
|
|
|
|
nv.cleanup() |
|
|
|
|
|
if (gl) { |
|
|
const ext = gl.getExtension('WEBGL_lose_context') |
|
|
ext?.loseContext() |
|
|
} |
|
|
} catch { |
|
|
|
|
|
} |
|
|
nvRef.current = null |
|
|
} |
|
|
}, []) |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const nv = nvRef.current |
|
|
if (!nv) return |
|
|
|
|
|
let isCurrent = true |
|
|
|
|
|
|
|
|
|
|
|
setLoadError(null) |
|
|
|
|
|
const volumes: Array<{ url: string; colormap: string; opacity: number }> = [ |
|
|
{ url: backgroundUrl, colormap: 'gray', opacity: 1 }, |
|
|
] |
|
|
|
|
|
if (overlayUrl) { |
|
|
volumes.push({ |
|
|
url: overlayUrl, |
|
|
colormap: 'red', |
|
|
opacity: 0.5, |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
nv.loadVolumes(volumes).catch((err: unknown) => { |
|
|
if (!isCurrent) return |
|
|
const message = err instanceof Error ? err.message : 'Failed to load volume' |
|
|
setLoadError(message) |
|
|
onErrorRef.current?.(message) |
|
|
}) |
|
|
|
|
|
|
|
|
return () => { |
|
|
isCurrent = false |
|
|
} |
|
|
}, [backgroundUrl, overlayUrl]) |
|
|
|
|
|
return ( |
|
|
<div className="bg-gray-900 rounded-lg p-2"> |
|
|
<canvas ref={canvasRef} className="w-full h-[500px] rounded" /> |
|
|
{loadError && ( |
|
|
<div className="mt-2 p-2 bg-red-900/50 rounded text-red-300 text-sm"> |
|
|
Failed to load volume: {loadError} |
|
|
</div> |
|
|
)} |
|
|
<div className="flex gap-4 mt-2 text-xs text-gray-400"> |
|
|
<span>Scroll: Navigate slices</span> |
|
|
<span>Drag: Adjust contrast</span> |
|
|
<span>Right-click: Pan</span> |
|
|
</div> |
|
|
</div> |
|
|
) |
|
|
} |
|
|
|