Download lib/ctapi.js from SharpMeow/unroll: direct link, hf CLI and curl.
- Browser
- Download file 7.55 kB
-
https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/ctapi.js
- Command line
-
hf download hf://spaces/SharpMeow/unroll/lib/ctapi.js
-
curl -L -o ctapi.js https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/ctapi.js
7.55 kB
| // SPDX-License-Identifier: Apache-2.0 | |
| // window.unroll.ct: the CT tools (lib/dragres.js, lib/ctwl.js, lib/ctvolume.js, lib/ctlabels.js, lib/ctsource.js) | |
| // for agents and tests, plus data-testid / data-api on their controls and glossary links in their panel groups. | |
| // CT_SPECS is plain data so tools/build_api_json.mjs can write it into api.json. | |
| const p = (name, type, desc, extra = {}) => ({ name, type, desc, ...extra }); | |
| export const CT_SPECS = { | |
| dragRes: { summary: 'Lower render resolution while the camera moves: "off", "light" (0.75x) or "strong" (0.5x). Without an argument, returns the state.', params: [p('mode', 'string', 'off, light or strong', { required: false, enum: ['off', 'light', 'strong'] })] }, | |
| setWindow: { summary: 'X-ray window and level on the 3D scroll, both 0..1 (low = level - window / 2).', params: [p('window', 'number', 'Width, 0.005..1'), p('level', 'number', 'Center, 0..1')] }, | |
| autoContrast: { summary: 'X-ray auto contrast from the CT atlas percentiles (default from Settings, 1 and 99).', params: [p('pLow', 'number', 'Low percentile', { required: false }), p('pHigh', 'number', 'High percentile', { required: false })] }, | |
| fullRange: { summary: 'Show the full X-ray brightness range.', params: [] }, | |
| contrast: { summary: 'X-ray window/level state.', params: [] }, | |
| open: { summary: 'Open the CT volume (MIP in a slab) around a papyrus point: { winding, u, v } or { voxel: [x, y, z] } or { x, y } screen point, with level? (0..5) and size? (64, 128, 192, 256). Streams OME-Zarr chunks from the public S3 bucket.', params: [p('where', 'object', 'Point'), ] }, | |
| close: { summary: 'Close the CT volume panel.', params: [] }, | |
| state: { summary: 'CT volume state: point, level, size, voxel size, source URL, slab, window, load stats, labels.', params: [] }, | |
| setSlab: { summary: 'Slab clip of the CT volume: { on, thickness (voxels), offset (voxels), tiltX, tiltY (degrees), face (point the camera down the normal) }.', params: [p('slab', 'object', 'Slab fields to change')] }, | |
| volumeWindow: { summary: 'Window and level of the CT volume MIP, both 0..1.', params: [p('window', 'number', 'Width'), p('level', 'number', 'Center')] }, | |
| volumeAuto: { summary: 'Auto contrast of the CT volume from its own histogram.', params: [] }, | |
| setDetail: { summary: 'Detail level of the CT volume: 0 is the full 9.4 µm scan, each step halves the resolution. Reloads.', params: [p('level', 'number', '0..5')] }, | |
| setSize: { summary: 'Cube size of the CT volume in voxels. Reloads.', params: [p('size', 'number', '64, 128, 192 or 256')] }, | |
| pick: { summary: 'Turn pick mode on or off: the next click on the papyrus opens the CT volume there.', params: [p('on', 'boolean', 'On', { required: false })] }, | |
| image: { summary: 'The CT volume view as a PNG data URL.', params: [] }, | |
| labels: { summary: 'Labeled segments: color every traced segment inside the CT volume cube. Returns the segments found with their colors and voxel counts.', params: [p('on', 'boolean', 'On', { required: false })] }, | |
| highlight: { summary: 'Highlight one labeled segment by winding id (null clears).', params: [p('id', 'string|null', 'Winding id, for example w035')] }, | |
| source: { summary: 'Where the CT volume of the current scroll streams from (OME-Zarr on s3://vesuvius-challenge-open-data) and the levels it has.', params: [] }, | |
| }; | |
| const PANEL = { // control id -> [testid, data-api] | |
| dragResSel: ['dragres-select', 'ct.dragRes(mode)'], ctwlWin: ['ct-window', 'ct.setWindow(window, level)'], ctwlLev: ['ct-level', 'ct.setWindow(window, level)'], | |
| ctwlAuto: ['ct-auto', 'ct.autoContrast()'], ctwlReset: ['ct-full-range', 'ct.fullRange()'], ctvolPick: ['ctvol-pick', 'ct.pick(on)'], ctvolBtn: ['ctvol-show', 'ct.open(where)'], | |
| cvClose: ['ctvol-close', 'ct.close()'], ctVolCanvas: ['ctvol-canvas', 'ct.image()'], cvSlabOn: ['ctvol-slab-on', 'ct.setSlab(slab)'], cvThick: ['ctvol-slab-thickness', 'ct.setSlab(slab)'], | |
| cvOff: ['ctvol-slab-offset', 'ct.setSlab(slab)'], cvTiltX: ['ctvol-tilt-across', 'ct.setSlab(slab)'], cvTiltY: ['ctvol-tilt-along', 'ct.setSlab(slab)'], | |
| cvWin: ['ctvol-window', 'ct.volumeWindow(window, level)'], cvLev: ['ctvol-level', 'ct.volumeWindow(window, level)'], cvSize: ['ctvol-size', 'ct.setSize(size)'], | |
| cvLevel: ['ctvol-detail', 'ct.setDetail(level)'], cvAuto: ['ctvol-auto', 'ct.volumeAuto()'], cvFace: ['ctvol-face', 'ct.setSlab(slab)'], cvRepick: ['ctvol-repick', 'ct.pick(on)'], | |
| }; | |
| const TERMS = { uiPerf: [['pixelratio', 'pixel ratio'], ['resolution', 'resolution']], uiCT: [['windowlevel', 'window/level'], ['percentile', 'percentiles'], ['ct', 'X-ray CT']], | |
| uiCTVol: [['ctvolume', 'CT volume'], ['mip', 'MIP'], ['slab', 'slab'], ['raymarching', 'raymarching'], ['zarr', 'OME-Zarr'], ['labels', 'labeled segments']] }; | |
| function tag() { | |
| for (const [id, [t, a]] of Object.entries(PANEL)) { | |
| const el = document.getElementById(id); if (!el) continue; | |
| if (!el.dataset.testid) { el.dataset.testid = t; el.dataset.api = a; } | |
| const lab = el.closest('label'); if (lab && !lab.dataset.testid) { lab.dataset.testid = t + '-label'; lab.dataset.api = a; } | |
| } | |
| for (const [gid, list] of Object.entries(TERMS)) { | |
| const g = document.getElementById(gid); if (!g || g.querySelector('.ct-terms')) continue; | |
| const pEl = document.createElement('p'); pEl.className = 'mono dim small ct-terms'; | |
| pEl.innerHTML = 'Glossary: ' + list.map(([k, txt]) => `<a class="ui-term" href="#g-${k}" data-term="${k}" data-testid="gloss-${gid}-${k}" data-api="ui.glossary('${k}')">${txt}</a>`).join(', '); | |
| g.append(pEl); | |
| } | |
| } | |
| if (typeof window !== 'undefined') { | |
| const V = () => window.unrollView, vol = () => V()?.ct?.volume, need = (x, what) => { if (!x) throw new Error(`${what} is not ready yet`); return x; }; | |
| const methods = { | |
| dragRes: (m) => { const d = need(V()?.dragRes, 'dragRes'); if (m !== undefined) d.set(m); return d.state(); }, | |
| setWindow: (w, l) => need(V()?.ct?.setWindow, 'X-ray contrast')(w, l), autoContrast: (a, b) => need(V()?.ct?.auto, 'X-ray contrast')(a, b), | |
| fullRange: () => need(V()?.ct?.reset, 'X-ray contrast')(), contrast: () => need(V()?.ct?.state, 'X-ray contrast')(), | |
| open: (w = {}) => need(vol(), 'CT volume').open(w), close: () => need(vol(), 'CT volume').close(), | |
| state: () => ({ ...need(vol(), 'CT volume').state(), labels: vol().labels?.state() || null }), | |
| setSlab: (o = {}) => need(vol(), 'CT volume').setSlab(o), volumeWindow: (w, l) => need(vol(), 'CT volume').setWindow(w, l), | |
| volumeAuto: () => need(vol(), 'CT volume').auto(), setDetail: (l) => need(vol(), 'CT volume').setLevel(l), setSize: (n) => need(vol(), 'CT volume').setSize(n), | |
| pick: (on) => ({ picking: need(vol(), 'CT volume').pick(on ?? true) }), image: () => ({ png: need(vol(), 'CT volume').image() }), | |
| labels: (on) => need(vol()?.labels, 'labeled segments').show(on ?? true), highlight: (id) => need(vol()?.labels, 'labeled segments').highlight(id), | |
| source: async () => { const m = await import('./ctsource.js'); const s = m.volumeFor(V()?.scanGeom?.()?.scroll, null); return { bucket: m.BUCKET, zarr: s?.zarr || null, voxelUM: s?.voxelUM || null }; }, | |
| }; | |
| const reg = () => { const u = window.unroll; if (!u?.register) return setTimeout(reg, 200); if (!u.ct) u.register('ct', methods, CT_SPECS); }; | |
| reg(); | |
| let q = false; const go = () => { if (q) return; q = true; requestAnimationFrame(() => { q = false; tag(); }); }; | |
| new MutationObserver(go).observe(document.documentElement, { childList: true, subtree: true }); | |
| go(); | |
| } | |