Chase Hendrick
CT volume view (lib/ctvolume.js, lib/ctsource.js): a cube of the raw X-ray scan around a picked papyrus point (256 voxels, 128 on phones) as a Data3DTexture, raymarched MIP clipped to an oriented slab that starts square to the surface; own canvas beside the scroll, render on demand; panel group (Pick a point, Show volume) and floating panel with slab, tilt, window/level, size and detail controls; Settings, unrollView.ct.volume API and actions, ?ctvol URLs, scanGeom and scanPick hooks in app.js, glossary (MIP, slab, CT volume, raymarching), docs
42b53b4 Download lib/ctsource.js from SharpMeow/unroll: direct link, hf CLI and curl.
- Browser
- Download file 3.35 kB
-
https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/ctsource.js
- Command line
-
hf download hf://spaces/SharpMeow/unroll/lib/ctsource.js
-
curl -L -o ctsource.js https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/ctsource.js
3.35 kB
| // SPDX-License-Identifier: Apache-2.0 | |
| // Where the CT volume comes from: the public OME-Zarr scans in the Vesuvius Challenge open data bucket, | |
| // read straight from S3 over HTTPS (the bucket answers GET with Access-Control-Allow-Origin: *). | |
| // The global endpoint is used: the us-east-1 regional host answers 403 for the same keys from here. | |
| export const BUCKET = 'https://vesuvius-challenge-open-data.s3.amazonaws.com'; | |
| // scan volumes whose voxel grid the meshes of a scroll were traced on (tifxyz x, y, z = zarr x, y, z at level 0) | |
| export const VOLUMES = { | |
| PHerc0139: { zarr: `${BUCKET}/PHerc0139/volumes/20250728140407-9.362um-1.2m-113keV-masked.zarr`, voxelUM: 9.362 }, | |
| }; | |
| export function volumeFor(scroll, manifest) { | |
| const q = new URLSearchParams(location.search).get('ctzarr'); | |
| if (q && q.startsWith(BUCKET + '/')) return { zarr: q.replace(/\/$/, ''), voxelUM: +(new URLSearchParams(location.search).get('ctum')) || 9.362 }; | |
| if (manifest?.ctZarr?.startsWith?.(BUCKET + '/')) return { zarr: manifest.ctZarr, voxelUM: manifest.ctVoxelUM || 9.362 }; | |
| return VOLUMES[scroll] || null; | |
| } | |
| // level metadata (.zarray, zarr v2) | |
| const META = new Map(); | |
| export async function levelMeta(zarr, level) { | |
| const k = `${zarr}/${level}`; | |
| if (!META.has(k)) META.set(k, fetch(`${zarr}/${level}/.zarray`).then((r) => { if (!r.ok) throw new Error(`no level ${level} (${r.status})`); return r.json(); })); | |
| return META.get(k); | |
| } | |
| // Read the box [start, start + size) (z, y, x) of one level into a Uint8Array (C order, x fastest). | |
| // Simple path: uncompressed uint8 chunks fetched in parallel on the main thread; onChunk(box) reports each one. | |
| export async function readRegion(zarr, level, start, size, onChunk, signal) { | |
| const m = await levelMeta(zarr, level); | |
| if (m.dtype !== '|u1' || m.compressor) throw new Error(`level ${level}: ${m.dtype} ${m.compressor?.id || 'raw'} needs the streaming reader`); | |
| const [Z, Y, X] = size, out = new Uint8Array(Z * Y * X), C = m.chunks, sep = m.dimension_separator || '.'; | |
| const jobs = []; | |
| for (let cz = Math.floor(start[0] / C[0]); cz * C[0] < start[0] + Z; cz++) | |
| for (let cy = Math.floor(start[1] / C[1]); cy * C[1] < start[1] + Y; cy++) | |
| for (let cx = Math.floor(start[2] / C[2]); cx * C[2] < start[2] + X; cx++) jobs.push([cz, cy, cx]); | |
| let i = 0, n = 0; | |
| const one = async ([cz, cy, cx]) => { | |
| const r = await fetch(`${zarr}/${level}/${[cz, cy, cx].join(sep)}`, { signal }); | |
| const buf = r.ok ? new Uint8Array(await r.arrayBuffer()) : null; // a missing chunk is fill_value (0) | |
| const z0 = Math.max(start[0], cz * C[0]), z1 = Math.min(start[0] + Z, (cz + 1) * C[0]); | |
| const y0 = Math.max(start[1], cy * C[1]), y1 = Math.min(start[1] + Y, (cy + 1) * C[1]); | |
| const x0 = Math.max(start[2], cx * C[2]), x1 = Math.min(start[2] + X, (cx + 1) * C[2]); | |
| if (buf) for (let z = z0; z < z1; z++) for (let y = y0; y < y1; y++) { | |
| const src = ((z - cz * C[0]) * C[1] + (y - cy * C[1])) * C[2] + (x0 - cx * C[2]); | |
| out.set(buf.subarray(src, src + (x1 - x0)), ((z - start[0]) * Y + (y - start[1])) * X + (x0 - start[2])); | |
| } | |
| n++; onChunk?.({ done: n, total: jobs.length, bytes: buf ? buf.length : 0 }); | |
| }; | |
| await Promise.all(Array.from({ length: 6 }, async () => { while (i < jobs.length) await one(jobs[i++]); })); | |
| return { data: out, chunks: jobs.length }; | |
| } | |