-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathviewer-adapter.js
More file actions
157 lines (125 loc) · 8.35 KB
/
Copy pathviewer-adapter.js
File metadata and controls
157 lines (125 loc) · 8.35 KB
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/*
* viewer-adapter.js — the CONTRACT between the host viewer and the ROI-drawing code.
*
* This is the porting boundary: the pure `core/` and the `ui/` talk only to a ViewerAdapter,
* never to a specific viewer's internals. To run ROI drawing on a different WebGL surface
* viewer, implement this interface (see pycortex-adapter.js for the reference implementation).
*
* It is a documented interface. The class is a convenience base (both PycortexAdapter and the test
* fake `extends` it, and `REQUIRED` below drives a conformance test), but any object with these
* methods works — the required methods `throw` here so an incomplete adapter fails loudly.
*
* Coordinate conventions:
* - "screen px": CSS pixels relative to the surface canvas's top-left (what the lasso uses).
* - "subject index" (g): the surface's canonical per-hemisphere vertex index. Selections and
* exports are in subject indices so they port across viewers on the same surface.
* - "uv": the vertex's flat-overlay texture coordinate in [0,1] (for SVG-overlay path coords).
*/
export class ViewerAdapter {
/* --- surface identity ------------------------------------------------------------- */
/** @returns {string} an id for the surface (e.g. "fsaverage"), stamped into exports. */
surfaceId() { throw new Error("ViewerAdapter.surfaceId not implemented"); }
/** @returns {boolean} whether the surface is fully flattened (drawing is flat-only). */
isFlat() { throw new Error("ViewerAdapter.isFlat not implemented"); }
/** @returns {{width:number, height:number}} the surface canvas size in CSS px. */
viewportSize() { throw new Error("ViewerAdapter.viewportSize not implemented"); }
/** @returns {HTMLCanvasElement} the surface's WebGL canvas (for positioning the overlay). */
canvas() { throw new Error("ViewerAdapter.canvas not implemented"); }
/* --- projection (host-specific: morph + camera) ----------------------------------- */
/**
* Project the surface's vertices to screen px at the CURRENT view, dropping anything
* behind the camera. Used for lasso selection (the host's own view framing is internal).
* @param {{subsample?:number}} [opts] keep ~1 of every `subsample` vertices (a stride; selection passes 1).
* @returns {{left:{idx:number[], px:[number,number][]}, right:{...}}} in-frustum verts.
*/
projectVertices(_opts) { throw new Error("ViewerAdapter.projectVertices not implemented"); }
/**
* Every vertex's subject index + flat-UV, per hemi. View-INDEPENDENT (no camera): the basis
* for uv-space ROI membership, so a reloaded bezier selects the same vertices at any view.
* @returns {{left:{idx:number[], uv:[number,number][]}, right:{...}}}
*/
allVertexUV() { throw new Error("ViewerAdapter.allVertexUV not implemented"); }
/** @returns {[number,number]|null} flat-UV ([0,1]) of one subject vertex {h,g}, or null. */
vertexUV(_o) { throw new Error("ViewerAdapter.vertexUV not implemented"); }
/**
* Project ONLY the vertices whose flat-UV falls within `bounds`, reporting each one's uv AND
* current-view px. The bezier edit overlay fits a LOCAL uv<->px homography from these (one
* global homography drifts where the flatmap isn't perfectly planar; locally it's near-exact).
* @param {{minu:number,maxu:number,minv:number,maxv:number}} bounds
* @returns {{left:{uv:[number,number][], px:[number,number][]}, right:{...}}}
*/
projectVerticesInUvBounds(_bounds) { throw new Error("ViewerAdapter.projectVerticesInUvBounds not implemented"); }
/* --- overlay layer (the occlusion-correct ROI rendering) -------------------------- */
/**
* Create/replace a named overlay layer rendered INTO the surface (so it occludes and morphs
* like built-in ROIs). `shapes` carries, per shape, its `kind` ("roi" | "sulcus"), a label
* vertex, a display color, and an editable flat-UV `bezier` the adapter renders as a cubic
* path — closed for an ROI, open (no `Z`) for a sulcus. ROIs may also carry a boundary ring
* (`outline`) used as a fallback for files predating the bezier.
* @param {string} name
* @param {Array<{kind, name, color?, outline?:[{h,g}], labelVert:{h,g}, bezier?}>} shapes
* @returns {boolean} false if the host's overlay machinery isn't ready yet (the caller retries);
* true once the layer is in place. Never silently drop shapes — they'd be listed but undrawn.
*/
setOverlayLayer(_name, _shapes) { throw new Error("ViewerAdapter.setOverlayLayer not implemented"); }
/** Show/hide the outlines and labels of a previously-created layer. */
setLayerVisible(_name, _shapes, _labels) { throw new Error("ViewerAdapter.setLayerVisible not implemented"); }
/* --- camera / transitions --------------------------------------------------------- */
/** Smoothly flatten the surface (mix -> 1). */
flatten() { throw new Error("ViewerAdapter.flatten not implemented"); }
/** Aim the camera at a world point [x,y,z] (keeps the center of mass framed). */
setCameraTarget(_xyz) { throw new Error("ViewerAdapter.setCameraTarget not implemented"); }
/** Set the camera orbit radius (zoom). */
setCameraRadius(_r) { throw new Error("ViewerAdapter.setCameraRadius not implemented"); }
/** @returns {number} current camera orbit radius. */
cameraRadius() { throw new Error("ViewerAdapter.cameraRadius not implemented"); }
/** Request a render (viewers render on demand). */
requestRender() { throw new Error("ViewerAdapter.requestRender not implemented"); }
/* --- events ----------------------------------------------------------------------- */
/** Subscribe to surface morph changes; cb() runs on every mix frame. @returns {function} unsubscribe */
onMixChange(_cb) { throw new Error("ViewerAdapter.onMixChange not implemented"); }
/* --- optional niceties (sensible defaults; override if the host supports them) ----- */
/** Forward a click to the host's own picker (Shift-inspect while drawing). */
inspectAt(_x, _y) {}
/** Zoom the surface by a mouse-wheel delta (lets the user draw fine detail). */
zoom(_deltaY) {}
/** Pan the surface by a screen-pixel drag delta (reposition while drawing). */
pan(_dx, _dy) {}
/** @returns {DOMRect|null} the host control panel's screen rect, for placing UI beside it. */
controlPanelRect() { return null; }
/** Collapse the host's own control panel on startup. */
collapseControlPanel() {}
/** Show/hide the host's control panel when switching Display/Draw modes. */
setControlPanelVisible(_visible) {}
/** Release any host listeners/timers the adapter installed. Called by ROIDrawer.destroy(). */
destroy() {}
}
// The methods an implementation MUST provide (every one above that throws). The "optional niceties"
// — inspectAt/zoom/pan/controlPanelRect/collapseControlPanel/setControlPanelVisible — have working
// defaults. Declared explicitly (not inferred from the source text) so the conformance test has a
// stable contract to check against. Keep in sync when adding a required method.
ViewerAdapter.REQUIRED = [
"surfaceId", "isFlat", "viewportSize", "canvas",
"projectVertices", "allVertexUV", "vertexUV", "projectVerticesInUvBounds",
"setOverlayLayer", "setLayerVisible", "flatten",
"setCameraTarget", "setCameraRadius", "cameraRadius", "requestRender", "onMixChange",
];
/* Everything: the bounds that select the whole flatmap for projectVerticesInUvBounds. */
export const ALL_UV_BOUNDS = Object.freeze({ minu: -Infinity, maxu: Infinity, minv: -Infinity, maxv: Infinity });
/*
* uv->px correspondences for a homography fit, from the adapter's projectVerticesInUvBounds
* flattened across both hemispheres into parallel `src` (uv) / `dst` (px) arrays. The edit overlay
* (LOCAL bounds around the shape) and the sulcus trace pipeline (the whole flatmap) both fit the
* same kind of transform from the same kind of data — this is the one place that data is shaped.
* `bounds` defaults to the whole flatmap.
*/
export function uvPxCorrespondences(adapter, bounds = ALL_UV_BOUNDS) {
const proj = adapter.projectVerticesInUvBounds(bounds);
const src = [], dst = [];
for (const h of ["left", "right"]) {
const p = proj[h];
if (!p) continue;
for (let i = 0; i < p.uv.length; i++) { src.push(p.uv[i]); dst.push(p.px[i]); }
}
return { src, dst };
}