-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdraw-pipeline.js
More file actions
166 lines (153 loc) · 8.83 KB
/
Copy pathdraw-pipeline.js
File metadata and controls
166 lines (153 loc) · 8.83 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
158
159
160
161
162
163
164
165
166
/*
* draw-pipeline.js — the shape-derivation pipeline, using only a ViewerAdapter + the pure core.
* ROI: lasso (or an edited bezier) -> vertex membership + outline ring + label + bezier.
* Sulcus: traced stroke -> px->uv via a homography -> open bezier + a live-overlay label vertex.
* No DOM, no UI, no prompt — that stays in the controller — so this whole "does the geometry do
* what we think" path is testable headless against a synthetic-surface adapter.
*
* The bezier is the source of truth: when a lasso yields a fittable curve, membership is RE-DERIVED
* from the curve (in view-independent flat-UV) so the stored vertices match the editable outline.
*/
import { selectInPolygon } from "./core/selection.js";
import { buildOutline, pickLabelVertex } from "./core/outline.js";
import { fitClosedBezier, evalClosedBezier, fitOpenBezier, evalOpenBezier } from "./core/bezier.js";
import { fitHomography, applyHomography, invertHomography } from "./core/transform.js";
import { uvPxCorrespondences } from "./adapter/viewer-adapter.js";
const BEZIER_SAMPLES = 16; // samples/segment when rasterizing a bezier to a uv polygon for selection
const OUTLINE_EPS_UV = 0.003; // RDP tolerance (uv units) for the outline ring built in uv space
const TRACE_SAMPLES = 24; // samples/segment when locating a curve's parametric midpoint
// Map an outline ring [{h,g}] to flat-UV points [[u,v],...], dropping vertices with no uv.
function ringToUv(adapter, ring) {
if (!ring) return null;
const uv = [];
for (const o of ring) { const p = adapter.vertexUV(o); if (p) uv.push(p); }
return uv;
}
// Back-fill an editable bezier for a v1 ROI (one saved before the bezier feature) from its stored
// outline ring, so imported shapes edit just like freshly drawn ones. Returns a bezier or null.
export function backfillBezier(adapter, ring) {
return fitRingBezier(adapter, ring);
}
/* Fit a closed bezier to a vertex ring mapped into flat-UV, or null when fewer than 3 of its
* vertices have uv. The one rule for "ring -> bezier", shared by the lasso path and v1 back-fill. */
function fitRingBezier(adapter, ring) {
const ringUv = ringToUv(adapter, ring);
return ringUv && ringUv.length >= 3 ? fitClosedBezier(ringUv) : null;
}
// Pick a label vertex for an imported ROI whose file lacked one, using the SAME rule as freshly
// drawn ROIs (pickLabelVertex: the boundary vertex nearest the centroid, computed in flat-UV).
// Returns {h,g} or null. So reloaded and fresh ROIs label identically.
export function backfillLabel(adapter, ring) {
if (!ring) return null;
const sel = { left: [], right: [], px: { left: [], right: [] } };
for (const o of ring) {
const uv = adapter.vertexUV(o);
if (uv) { sel[o.h].push(o.g); sel.px[o.h].push(uv); } // feed uv where pickLabelVertex expects px
}
return pickLabelVertex(sel);
}
/*
* Derive ROI membership + outline + label from a bezier, entirely in flat-UV (view-independent, so a
* reloaded ROI selects the same vertices). selectInPolygon/buildOutline are coordinate-space
* agnostic, so we feed them uv where they'd normally get screen px.
* Returns { left, right, outline, labelVert, total } or null if the curve can't be rasterized.
*/
export function roiFromBezier(adapter, bezier) {
const poly = evalClosedBezier(bezier, BEZIER_SAMPLES);
if (poly.length < 3) return null;
const all = adapter.allVertexUV();
const projectedUv = { left: { idx: all.left.idx, px: all.left.uv }, right: { idx: all.right.idx, px: all.right.uv } };
const sel = selectInPolygon(projectedUv, poly);
const outline = buildOutline(poly, sel, { epsilon: OUTLINE_EPS_UV }); // uv tolerance, not px
return { left: sel.left, right: sel.right, outline, labelVert: pickLabelVertex(sel), total: sel.total };
}
/*
* Full lasso → derived ROI geometry. Selects the lassoed vertices at the current view, fits an
* editable bezier to the resulting ring (in flat-UV), then re-derives membership FROM the bezier so
* the stored vertices match the editable curve. Falls back to the raw lasso selection when no curve
* can be fit. Returns { left, right, outline, labelVert, bezier, total }; total === 0 means nothing
* was enclosed (the caller should abort the add).
*/
export function deriveRoiFromLasso(adapter, pts) {
const projected = adapter.projectVertices({ subsample: 1 });
const sel0 = selectInPolygon(projected, pts);
if (!sel0.total) return { left: [], right: [], outline: null, labelVert: null, bezier: null, total: 0 };
const lassoRing = buildOutline(pts, sel0); // px-space ring of the stroke
const fitted = fitRingBezier(adapter, lassoRing);
// Prefer bezier-derived membership so the stored vertices match the editable curve. But only keep
// the bezier if it actually encloses something: a curve that re-derives to zero vertices (a very
// thin/tiny ROI the smoothing shrank past every vertex) would leave the bezier — the source of
// truth for the drawn outline and future edits — disagreeing with the fallback lasso vertices. In
// that case drop it, so the ROI stays a consistent (non-editable) vertex set.
const derived = fitted ? roiFromBezier(adapter, fitted) : null;
if (derived && derived.total)
return { left: derived.left, right: derived.right, outline: derived.outline, labelVert: derived.labelVert, bezier: fitted, total: derived.total };
return {
left: sel0.left, right: sel0.right, outline: lassoRing,
labelVert: pickLabelVertex(sel0), bezier: null, total: sel0.total,
};
}
/*
* Nearest surface vertex to a flat-UV point. Brute force over every vertex: this runs once per
* drawn curve, not per frame. It is the roidraw analogue of pycortex's SVGOverlay.set_coords,
* which builds a cKDTree over the flat vertex coords purely to place a LABEL (`data-ptidx`).
* Returns {h,g} or null on an empty surface.
*/
export function nearestVertexTo(adapter, uv) {
const all = adapter.allVertexUV();
let best = null, bd = Infinity;
for (const h of ["left", "right"]) {
const p = all[h];
if (!p) continue;
for (let k = 0; k < p.uv.length; k++) {
const dx = p.uv[k][0] - uv[0], dy = p.uv[k][1] - uv[1], d = dx * dx + dy * dy;
if (d < bd) { bd = d; best = { h, g: p.idx[k] }; }
}
}
return best;
}
/* The label vertex for an OPEN curve: the surface vertex nearest the curve's midpoint sample.
* Shared by the initial trace and by every subsequent edit, so a reshaped sulcus relabels the
* same way a freshly traced one does. (Cf. backfillLabel, which does the analogous job for an
* ROI ring.) Returns {h,g} or null.
*
* FOR THE LIVE IN-VIEWER OVERLAY ONLY. The WebGL viewer places a label by vertex index
* (`data-ptidx`); the exported overlays.svg must NOT carry one, because pycortex computes sulcus
* label positions from the path geometry itself at load time. See core/svg-export.js.
*
* Assumes `bezier` is open: it samples with evalOpenBezier, which on a CLOSED ring would silently
* skip the wrap segment and pick a subtly wrong midpoint. Pass an ROI's bezier to roiFromBezier
* (centroid-nearest) instead. */
export function labelForCurve(adapter, bezier) {
const poly = evalOpenBezier(bezier, TRACE_SAMPLES);
const mid = poly[poly.length >> 1];
return mid ? nearestVertexTo(adapter, mid) : null;
}
/*
* A traced stroke -> an editable OPEN bezier, plus a label vertex.
*
* A sulcus stores NO vertex membership — pycortex stores none either (there is no
* get_sulci_verts; sulci are display geometry). The curve is the datum, so there is no
* re-derivation step and no way for stored vertices to disagree with the editable curve.
*
* The stroke arrives in screen px and must be stored in view-independent flat-uv. At full flat the
* flatmap is one plane, so uv->px is exactly a homography; we fit it from the (uv, px)
* correspondences the adapter already produces and invert it. PRECONDITION: the surface is flat
* (drawing is flat-only — see DrawModeMachine). Returns null on a degenerate stroke or a
* homography that won't fit (a collinear/degenerate view).
*/
export function curveFromTrace(adapter, pts) {
if (!pts || pts.length < 2) return null;
const c = uvPxCorrespondences(adapter); // the whole flatmap (a global fit)
if (c.src.length < 4) return null;
const H = fitHomography(c.src, c.dst);
if (!H) return null;
const Hinv = invertHomography(H);
if (!Hinv) return null;
// applyHomography always returns a point — it clamps a near-zero projective divisor rather than
// failing — so there is nothing to filter here. fitOpenBezier dedupes and rejects a stroke that
// collapses to fewer than 2 distinct points.
const bezier = fitOpenBezier(pts.map((p) => applyHomography(Hinv, p)));
if (!bezier) return null;
return { bezier, labelVert: labelForCurve(adapter, bezier) };
}