// One CT / MR example in the finding library: an axial slice drawn from the gzipped voxels (decoded in the browser), // the reference outline from the label mask in cyan, and a slice scrubber that starts on the finding's measured slice. // Decoding reuses the reading room's helpers (viewer/volume: gunzip, int16/uint8 decode, window LUT, marching // squares); nothing here duplicates them. Nothing about the learner's own cases is shown. import { useEffect, useMemo, useRef, useState } from 'react'; import { assetUrl } from '../api/client'; import { ProvenanceBadge } from '../app/ProvenanceBadge'; import { marchingSquares } from '../viewer/volume/marching'; import { planeGeom } from '../viewer/volume/planes'; import { cachedGunzip, decodeMask, decodeVolume, extractSlice, type MaskVolume, type Volume } from '../viewer/volume/volume'; import { paintSlice, windowLut } from '../viewer/volume/window'; import type { ReferenceExample } from './guard'; import s from './Reference.module.css'; type Loaded = { vol: Volume; mask: MaskVolume }; /** Fetch and decode both arrays once per URL pair (the viewer's cache keeps the gunzipped bytes). */ async function loadVolumeExample(v: NonNullable): Promise { const meta = { shape: v.shape, spacing: v.spacing }; const [vb, mb] = await Promise.all([cachedGunzip(assetUrl(v.volume_url)), cachedGunzip(assetUrl(v.mask_url))]); return { vol: decodeVolume(vb, meta), mask: decodeMask(mb, meta) }; } /** Which mask values are the finding: the example's own, else every non-zero value. */ const insideFn = (values: number[]) => (values.length ? (x: number) => values.includes(x) : (x: number) => x > 0); /** `thumb` (round 5): the measure slice only, no scrubber or caption — the debrief row's 120 px thumbnail. */ export function VolumeExample({ ex, outline, alt, small = false, thumb = false }: { ex: ReferenceExample; outline: boolean; alt: string; small?: boolean; thumb?: boolean }) { const v = ex.volume!; const [z, setZ] = useState(v.slice); const [data, setData] = useState(null); const [failed, setFailed] = useState(null); const canvas = useRef(null); const g = useMemo(() => planeGeom({ shape: v.shape, spacing: v.spacing }, 'axial'), [v.shape, v.spacing]); const lut = useMemo(() => windowLut(v.window.wc, v.window.ww), [v.window.wc, v.window.ww]); useEffect(() => { let live = true; loadVolumeExample(v).then((d) => { if (live) setData(d); }, (e: unknown) => { if (live) setFailed(e instanceof Error ? e.message : 'decode failed'); }); return () => { live = false; }; }, [v]); // Paint the current slice (w×h voxels; CSS scales it to the display aspect). useEffect(() => { const c = canvas.current; if (!c || !data) return; const ctx = c.getContext('2d'); if (!ctx) return; const img = ctx.createImageData(g.w, g.h); paintSlice(extractSlice(data.vol.data, data.vol.shape, 'axial', z), lut, img.data); ctx.putImageData(img, 0, 0); }, [data, z, g.w, g.h, lut]); const rings = useMemo(() => { if (!data || !outline) return []; return marchingSquares(extractSlice(data.mask.data, data.mask.shape, 'axial', z), g.w, g.h, insideFn(v.label_values)); }, [data, outline, z, g.w, g.h, v.label_values]); const range = ex.finding?.slice_range; const onFinding = range ? z >= range[0] && z <= range[1] : rings.length > 0; return (
{failed ?

This example scan could not be decoded. {failed}

: ( <> {!data &&

Decoding the scan…

} {outline && rings.length > 0 && ( )} )}
{!thumb &&
Slice {z + 1} of {g.n}{range ? (onFinding ? ' · on the finding' : ` · finding on ${range[0] + 1}–${range[1] + 1}`) : ''}
} {!thumb &&
{ex.finding?.relative_location ? `${ex.finding.relative_location.charAt(0).toUpperCase()}${ex.finding.relative_location.slice(1)}. ` : ''}
}
); }