File size: 7,546 Bytes
e31bb23
 
 
 
 
 
 
757768f
e31bb23
 
 
 
 
 
 
757768f
e31bb23
 
 
 
 
757768f
e31bb23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// 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();
}