@jax-data-science/sci-image-visualizer 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/esm2022/lib/contracts/region-editor-api.contract.mjs +1 -1
  2. package/esm2022/lib/contracts/visualizer.contract.mjs +1 -1
  3. package/esm2022/lib/geometry/reconstruct.mjs +11 -1
  4. package/esm2022/lib/implementations/napari-js/napari-region-overlay.mjs +18 -6
  5. package/esm2022/lib/implementations/napari-js/napari-tool-bridge.mjs +29 -14
  6. package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +6 -1
  7. package/esm2022/lib/implementations/napari-js/region-overlay/region-svg-renderer.mjs +9 -4
  8. package/esm2022/lib/region-editor/region-editor.component.mjs +13 -1
  9. package/esm2022/lib/routing-visualizer.service.mjs +4 -1
  10. package/esm2022/lib/spatial/spatial-controls.facade.mjs +71 -21
  11. package/esm2022/lib/spatial/spatial-selection.mjs +70 -1
  12. package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +53 -11
  13. package/esm2022/lib/store/region-selection.mjs +21 -4
  14. package/esm2022/lib/store/region-store.service.mjs +22 -3
  15. package/esm2022/lib/store/spatial-selection.service.mjs +13 -1
  16. package/esm2022/lib/toolbar/toolbar-help-dialog/toolbar-help-dialog.component.mjs +3 -3
  17. package/fesm2022/{jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-a68Hw5rw.mjs → jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DURPqs5a.mjs} +770 -500
  18. package/fesm2022/jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DURPqs5a.mjs.map +1 -0
  19. package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-CvadfzjS.mjs → jax-data-science-sci-image-visualizer-onnx-sam-session-D0IQ6PjZ.mjs} +2 -2
  20. package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-CvadfzjS.mjs.map → jax-data-science-sci-image-visualizer-onnx-sam-session-D0IQ6PjZ.mjs.map} +1 -1
  21. package/fesm2022/jax-data-science-sci-image-visualizer.mjs +2 -2
  22. package/lib/contracts/region-editor-api.contract.d.ts +3 -0
  23. package/lib/contracts/visualizer.contract.d.ts +25 -2
  24. package/lib/geometry/reconstruct.d.ts +6 -0
  25. package/lib/implementations/napari-js/napari-region-overlay.d.ts +13 -1
  26. package/lib/implementations/napari-js/napari-tool-bridge.d.ts +11 -0
  27. package/lib/implementations/napari-js/napari-visualizer.service.d.ts +3 -0
  28. package/lib/implementations/napari-js/region-overlay/region-svg-renderer.d.ts +6 -1
  29. package/lib/routing-visualizer.service.d.ts +1 -0
  30. package/lib/spatial/spatial-controls.facade.d.ts +16 -2
  31. package/lib/spatial/spatial-selection.d.ts +16 -1
  32. package/lib/spatial-controls/spatial-controls.component.d.ts +14 -3
  33. package/lib/store/region-selection.d.ts +14 -3
  34. package/lib/store/region-store.service.d.ts +14 -1
  35. package/lib/store/spatial-selection.service.d.ts +8 -0
  36. package/package.json +1 -1
  37. package/fesm2022/jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-a68Hw5rw.mjs.map +0 -1
@@ -5,8 +5,8 @@ import { HttpClient, HttpErrorResponse } from '@angular/common/http';
5
5
  import { LruCache, Colormap, buildLut, parseColor as parseColor$1, worldViewport, luminance8, WHEEL_DELTA_CLAMP, bitmapToScalar, decodeImageRGBA, readLevel, chooseStitchLevel, assembleVolume, colormapFromLut, tintColormap, reverseColormap, histogramScalar, MultiChannelImageView, projectPoint, MultiChannelVolumeView, heightField, pointSymbolCode, PointPicker, Viewer, windowGamma, resolveColormap } from 'napari-js';
6
6
  import { BehaviorSubject, firstValueFrom, Subject, Subscription, of, Observable, combineLatest, defer, from, EMPTY, merge } from 'rxjs';
7
7
  import { timeout, debounceTime, takeUntil, switchMap, catchError, startWith, map } from 'rxjs/operators';
8
- import * as ort from 'onnxruntime-web';
9
8
  import { smoothMesh, surfaceNets, pointInRing as pointInRing$2, ringArea as ringArea$1, traceContours, rasterizePolygon as rasterizePolygon$1, floodFill } from 'napari-js/geometry';
9
+ import * as ort from 'onnxruntime-web';
10
10
  import { encode } from 'fast-png';
11
11
  import { decompress } from 'fzstd';
12
12
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -2927,6 +2927,457 @@ function spatialContinuousLut(colormapValue, reverse = false, override) {
2927
2927
  return isGrayscaleLut(lut) ? buildColormapLut('Viridis', reverse) : lut;
2928
2928
  }
2929
2929
 
2930
+ /** Points per line at most: the longest line is resampled at about one point per `step` px. */
2931
+ const MAX_POINTS = 2048;
2932
+ /** Points per line when matching lines across slices. */
2933
+ const MATCH_POINTS = 32;
2934
+ /**
2935
+ * The sheet through `slices`' lines, in image pixels (x, y) and slice indices (z: slice k's line at
2936
+ * z = k). Every line is resampled at the same number of points, evenly along its length (about one
2937
+ * per `step` px of the longest), so a line shared by the strips above and below it is one row of
2938
+ * vertices. Lines are matched between neighbouring slices closest first, each at most once, and only
2939
+ * when their boxes overlap; a line without a match ends the sheet there. Empty without two slices
2940
+ * with lines.
2941
+ */
2942
+ function loftLines(slices, step = 1) {
2943
+ const stack = slices
2944
+ .map((s) => ({ z: s.z, lines: s.lines.filter((l) => l.length >= 4) }))
2945
+ .filter((s) => s.lines.length)
2946
+ .sort((a, b) => a.z - b.z);
2947
+ if (stack.length < 2)
2948
+ return { vertices: new Float32Array(0), faces: new Uint32Array(0) };
2949
+ let longest = 0;
2950
+ for (const s of stack)
2951
+ for (const l of s.lines)
2952
+ longest = Math.max(longest, lineLength(l));
2953
+ const n = Math.min(MAX_POINTS, Math.max(2, Math.ceil(longest / Math.max(step, 1e-6)) + 1));
2954
+ // Each line's resampled points, once: rows of n vertices.
2955
+ const rows = [];
2956
+ const verts = [];
2957
+ for (const s of stack) {
2958
+ rows.push(s.lines.map((l) => {
2959
+ const first = verts.length / 3;
2960
+ const pts = resample(l, n);
2961
+ for (let i = 0; i < n; i++)
2962
+ verts.push(pts[2 * i], pts[2 * i + 1], s.z);
2963
+ return { first, probe: resample(l, MATCH_POINTS), box: lineBox(l) };
2964
+ }));
2965
+ }
2966
+ const faces = [];
2967
+ for (let k = 0; k + 1 < rows.length; k++) {
2968
+ for (const { a, b, reversed } of matchLines(rows[k], rows[k + 1])) {
2969
+ const at = (i) => b.first + (reversed ? n - 1 - i : i);
2970
+ for (let i = 0; i + 1 < n; i++) {
2971
+ const a0 = a.first + i, a1 = a0 + 1, b0 = at(i), b1 = at(i + 1);
2972
+ faces.push(a0, b0, a1, a1, b0, b1);
2973
+ }
2974
+ }
2975
+ }
2976
+ if (!faces.length)
2977
+ return { vertices: new Float32Array(0), faces: new Uint32Array(0) };
2978
+ return { vertices: Float32Array.from(verts), faces: Uint32Array.from(faces) };
2979
+ }
2980
+ /** Pairs of lines of two neighbouring slices: closest first (mean distance, either direction), each once. */
2981
+ function matchLines(as, bs) {
2982
+ const pairs = [];
2983
+ as.forEach((a, i) => bs.forEach((b, j) => {
2984
+ if (!boxesOverlap(a.box, b.box))
2985
+ return;
2986
+ const forward = meanDistance(a.probe, b.probe, false);
2987
+ const backward = meanDistance(a.probe, b.probe, true);
2988
+ pairs.push({ a: i, b: j, cost: Math.min(forward, backward), reversed: backward < forward });
2989
+ }));
2990
+ pairs.sort((p, q) => p.cost - q.cost);
2991
+ const usedA = new Set();
2992
+ const usedB = new Set();
2993
+ const out = [];
2994
+ for (const p of pairs) {
2995
+ if (usedA.has(p.a) || usedB.has(p.b))
2996
+ continue;
2997
+ usedA.add(p.a);
2998
+ usedB.add(p.b);
2999
+ out.push({ a: as[p.a], b: bs[p.b], reversed: p.reversed });
3000
+ }
3001
+ return out;
3002
+ }
3003
+ function meanDistance(a, b, reversed) {
3004
+ const n = a.length >> 1;
3005
+ let sum = 0;
3006
+ for (let i = 0; i < n; i++) {
3007
+ const j = reversed ? n - 1 - i : i;
3008
+ sum += Math.hypot(a[2 * i] - b[2 * j], a[2 * i + 1] - b[2 * j + 1]);
3009
+ }
3010
+ return sum / n;
3011
+ }
3012
+ /** `n` points evenly spaced along the polyline, both ends kept. */
3013
+ function resample(line, n) {
3014
+ const m = line.length >> 1;
3015
+ const cumulative = new Float64Array(m);
3016
+ for (let i = 1; i < m; i++) {
3017
+ cumulative[i] =
3018
+ cumulative[i - 1] + Math.hypot(line[2 * i] - line[2 * i - 2], line[2 * i + 1] - line[2 * i - 1]);
3019
+ }
3020
+ const total = cumulative[m - 1];
3021
+ const out = new Float64Array(2 * n);
3022
+ let seg = 1;
3023
+ for (let i = 0; i < n; i++) {
3024
+ const d = n === 1 ? 0 : (total * i) / (n - 1);
3025
+ while (seg < m - 1 && cumulative[seg] < d)
3026
+ seg++;
3027
+ const span = cumulative[seg] - cumulative[seg - 1];
3028
+ const t = span > 0 ? Math.min(1, Math.max(0, (d - cumulative[seg - 1]) / span)) : 0;
3029
+ out[2 * i] = line[2 * seg - 2] + t * (line[2 * seg] - line[2 * seg - 2]);
3030
+ out[2 * i + 1] = line[2 * seg - 1] + t * (line[2 * seg + 1] - line[2 * seg - 1]);
3031
+ }
3032
+ return out;
3033
+ }
3034
+ function lineLength(line) {
3035
+ let len = 0;
3036
+ for (let i = 2; i + 1 < line.length; i += 2)
3037
+ len += Math.hypot(line[i] - line[i - 2], line[i + 1] - line[i - 1]);
3038
+ return len;
3039
+ }
3040
+ function lineBox(line) {
3041
+ const box = [Infinity, Infinity, -Infinity, -Infinity];
3042
+ for (let i = 0; i + 1 < line.length; i += 2) {
3043
+ box[0] = Math.min(box[0], line[i]);
3044
+ box[1] = Math.min(box[1], line[i + 1]);
3045
+ box[2] = Math.max(box[2], line[i]);
3046
+ box[3] = Math.max(box[3], line[i + 1]);
3047
+ }
3048
+ return box;
3049
+ }
3050
+ /** Whether the boxes overlap, each grown by a tenth of its larger side (a flat line has a thin box). */
3051
+ function boxesOverlap(a, b) {
3052
+ const ga = 0.1 * Math.max(a[2] - a[0], a[3] - a[1]);
3053
+ const gb = 0.1 * Math.max(b[2] - b[0], b[3] - b[1]);
3054
+ const g = ga + gb;
3055
+ return a[0] - g <= b[2] && b[0] - g <= a[2] && a[1] - g <= b[3] && b[1] - g <= a[3];
3056
+ }
3057
+
3058
+ /**
3059
+ * The 3D reconstruction of a region from its outlines on a stack of slices: each slice's outlines
3060
+ * filled (each feature even-odd, the features as their union) into a voxel mask, the mask's
3061
+ * boundary meshed by surface nets and smoothed; and its open lines lofted into a sheet.
3062
+ *
3063
+ * Pure: usable from workers.
3064
+ */
3065
+ /**
3066
+ * The mesh of the region outlined by `slices`, in image pixels (x, y) and slice indices (z: slice
3067
+ * k's plane at z = k): the filled outlines' surface, and the sheet through the lines (see
3068
+ * {@link loftLines}). Empty when no slice has an area and no two slices have lines.
3069
+ */
3070
+ function reconstruct(slices, opts = {}) {
3071
+ const filled = fill(slices, opts);
3072
+ const lines = slices.filter((s) => s.lines?.length);
3073
+ if (!lines.length)
3074
+ return filled;
3075
+ const sheet = loftLines(lines.map((s) => ({ z: s.z, lines: s.lines ?? [] })), opts.pixelStep ?? 1);
3076
+ return mergeMeshes(filled, sheet);
3077
+ }
3078
+ /** The surface of the filled outlines. */
3079
+ function fill(slices, opts) {
3080
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity, z0 = Infinity, z1 = -Infinity;
3081
+ for (const s of slices) {
3082
+ for (const r of s.rings) {
3083
+ for (let i = 0; i + 1 < r.length; i += 2) {
3084
+ if (r[i] < x0)
3085
+ x0 = r[i];
3086
+ if (r[i] > x1)
3087
+ x1 = r[i];
3088
+ if (r[i + 1] < y0)
3089
+ y0 = r[i + 1];
3090
+ if (r[i + 1] > y1)
3091
+ y1 = r[i + 1];
3092
+ }
3093
+ }
3094
+ if (s.rings.length) {
3095
+ z0 = Math.min(z0, s.z);
3096
+ z1 = Math.max(z1, s.z);
3097
+ }
3098
+ }
3099
+ if (!(x1 > x0) || !(y1 > y0))
3100
+ return { vertices: new Float32Array(0), faces: new Uint32Array(0) };
3101
+ const nz = z1 - z0 + 1;
3102
+ const maxVoxels = opts.maxVoxels ?? 32_000_000;
3103
+ let step = Math.max(opts.pixelStep ?? 1, 1e-6);
3104
+ while (Math.ceil((x1 - x0) / step) * Math.ceil((y1 - y0) / step) * nz > maxVoxels)
3105
+ step *= 1.25;
3106
+ const nx = Math.max(1, Math.ceil((x1 - x0) / step));
3107
+ const ny = Math.max(1, Math.ceil((y1 - y0) / step));
3108
+ const mask = new Uint8Array(nx * ny * nz);
3109
+ for (const s of slices) {
3110
+ for (const rings of featureRings(s.rings, s.ringGroups)) {
3111
+ fillSlice(mask, nx, ny, (s.z - z0) * nx * ny, rings, x0, y0, step);
3112
+ }
3113
+ }
3114
+ // Voxel i spanning [i, i + 1) (napari-js centres it on i): the scaling below maps cell edges to px.
3115
+ const mesh = smoothMesh(surfaceNets(mask, [nx, ny, nz], { origin: [0.5, 0.5, 0.5] }), {
3116
+ iterations: opts.smooth ?? 2,
3117
+ });
3118
+ const v = mesh.vertices;
3119
+ for (let i = 0; i < v.length; i += 3) {
3120
+ v[i] = x0 + v[i] * step;
3121
+ v[i + 1] = y0 + v[i + 1] * step;
3122
+ // Voxel k spans [k, k+1) with its centre (the slice) at k + 0.5.
3123
+ v[i + 2] = z0 + v[i + 2] - 0.5;
3124
+ }
3125
+ return mesh;
3126
+ }
3127
+ /**
3128
+ * Fill one slice's outlines into `mask`, a `width × height` grid of image px (pixel i spanning
3129
+ * [i, i + 1)), as the reconstruction fills them: each feature even-odd, the features as their
3130
+ * union, sampled at pixel centres. Cells already set stay set. Open lines enclose nothing.
3131
+ */
3132
+ function fillOutlines(mask, width, height, outlines) {
3133
+ for (const rings of featureRings(outlines.rings, outlines.ringGroups)) {
3134
+ fillSlice(mask, width, height, 0, rings, 0, 0, 1);
3135
+ }
3136
+ }
3137
+ /** `a` and `b` as one mesh. */
3138
+ function mergeMeshes(a, b) {
3139
+ if (!b.faces.length)
3140
+ return a;
3141
+ if (!a.faces.length)
3142
+ return b;
3143
+ const vertices = new Float32Array(a.vertices.length + b.vertices.length);
3144
+ vertices.set(a.vertices);
3145
+ vertices.set(b.vertices, a.vertices.length);
3146
+ const faces = new Uint32Array(a.faces.length + b.faces.length);
3147
+ faces.set(a.faces);
3148
+ const offset = a.vertices.length / 3;
3149
+ for (let i = 0; i < b.faces.length; i++)
3150
+ faces[a.faces.length + i] = b.faces[i] + offset;
3151
+ return { vertices, faces };
3152
+ }
3153
+ /**
3154
+ * Fill one feature into one slice of the mask: even-odd over its `rings`, sampled at voxel
3155
+ * centres. Cells already set stay set, so the features of a slice fill as their union.
3156
+ */
3157
+ function fillSlice(mask, nx, ny, offset, rings, x0, y0, step) {
3158
+ const crossings = [];
3159
+ // Only the rows the feature spans.
3160
+ let top = Infinity, bottom = -Infinity;
3161
+ for (const r of rings) {
3162
+ for (let i = 1; i < r.length; i += 2) {
3163
+ if (r[i] < top)
3164
+ top = r[i];
3165
+ if (r[i] > bottom)
3166
+ bottom = r[i];
3167
+ }
3168
+ }
3169
+ const j0 = Math.max(0, Math.floor((top - y0) / step - 0.5));
3170
+ const j1 = Math.min(ny - 1, Math.ceil((bottom - y0) / step - 0.5));
3171
+ for (let j = j0; j <= j1; j++) {
3172
+ const y = y0 + (j + 0.5) * step;
3173
+ crossings.length = 0;
3174
+ for (const r of rings) {
3175
+ const n = r.length >> 1;
3176
+ for (let i = 0, k = n - 1; i < n; k = i++) {
3177
+ const ya = r[2 * i + 1], yb = r[2 * k + 1];
3178
+ if (ya > y !== yb > y) {
3179
+ const xa = r[2 * i], xb = r[2 * k];
3180
+ crossings.push(xa + ((y - ya) * (xb - xa)) / (yb - ya));
3181
+ }
3182
+ }
3183
+ }
3184
+ if (crossings.length < 2)
3185
+ continue;
3186
+ crossings.sort((a, b) => a - b);
3187
+ const row = offset + j * nx;
3188
+ for (let c = 0; c + 1 < crossings.length; c += 2) {
3189
+ const from = Math.max(0, Math.ceil((crossings[c] - x0) / step - 0.5));
3190
+ const to = Math.min(nx - 1, Math.floor((crossings[c + 1] - x0) / step - 0.5));
3191
+ for (let i = from; i <= to; i++)
3192
+ mask[row + i] = 1;
3193
+ }
3194
+ }
3195
+ }
3196
+ /**
3197
+ * The area inside `rings` (interleaved `x, y`), even-odd: a ring inside an odd number of others is
3198
+ * a hole. Exact for rings that do not cross (a polygon set's exteriors and holes).
3199
+ */
3200
+ function ringsArea(rings) {
3201
+ const boxes = rings.map(ringBox);
3202
+ let area = 0;
3203
+ for (let i = 0; i < rings.length; i++) {
3204
+ const r = rings[i];
3205
+ if (r.length < 6)
3206
+ continue;
3207
+ const x = r[0], y = r[1];
3208
+ let depth = 0;
3209
+ for (let j = 0; j < rings.length; j++) {
3210
+ if (j === i)
3211
+ continue;
3212
+ const b = boxes[j];
3213
+ if (x < b[0] || x > b[2] || y < b[1] || y > b[3])
3214
+ continue;
3215
+ if (pointInRing$2(x, y, rings[j]))
3216
+ depth++;
3217
+ }
3218
+ area += (depth % 2 ? -1 : 1) * Math.abs(ringArea$1(r));
3219
+ }
3220
+ return Math.max(0, area);
3221
+ }
3222
+ /**
3223
+ * The rings of each feature (see {@link SliceOutlines.ringGroups}), in the order first met; one
3224
+ * feature of every ring without `groups`.
3225
+ */
3226
+ function featureRings(rings, groups) {
3227
+ if (!groups || !rings.length)
3228
+ return rings.length ? [rings.slice()] : [];
3229
+ const byGroup = new Map();
3230
+ rings.forEach((r, i) => {
3231
+ const g = groups[i] ?? 0;
3232
+ const feature = byGroup.get(g);
3233
+ if (feature)
3234
+ feature.push(r);
3235
+ else
3236
+ byGroup.set(g, [r]);
3237
+ });
3238
+ return [...byGroup.values()];
3239
+ }
3240
+ /**
3241
+ * The area of a slice's outlines (px²): each feature's (even-odd, see {@link ringsArea}), the
3242
+ * features as their union, so a feature inside or over another of the slice adds only what it
3243
+ * covers beyond it. Features whose extents overlap are measured together, by the length of their
3244
+ * union along each row, exact between the rows through their vertices where no outline crosses
3245
+ * another (and refined where one does).
3246
+ */
3247
+ function outlinesArea(rings, groups) {
3248
+ const features = featureRings(rings, groups).map((f) => ({ rings: f, box: featureBox(f) }));
3249
+ if (features.length < 2)
3250
+ return features.length ? ringsArea(features[0].rings) : 0;
3251
+ // Features whose extents overlap, joined (by x, so a feature meets only those it can overlap).
3252
+ features.sort((a, b) => a.box[0] - b.box[0]);
3253
+ const parent = features.map((_, i) => i);
3254
+ const root = (i) => (parent[i] === i ? i : (parent[i] = root(parent[i])));
3255
+ for (let i = 0; i < features.length; i++) {
3256
+ const a = features[i].box;
3257
+ for (let j = i + 1; j < features.length && features[j].box[0] < a[2]; j++) {
3258
+ const b = features[j].box;
3259
+ if (b[1] < a[3] && a[1] < b[3])
3260
+ parent[root(j)] = root(i);
3261
+ }
3262
+ }
3263
+ const clusters = new Map();
3264
+ features.forEach((f, i) => {
3265
+ const r = root(i);
3266
+ const cluster = clusters.get(r);
3267
+ if (cluster)
3268
+ cluster.push(f.rings);
3269
+ else
3270
+ clusters.set(r, [f.rings]);
3271
+ });
3272
+ let area = 0;
3273
+ for (const cluster of clusters.values())
3274
+ area += cluster.length === 1 ? ringsArea(cluster[0]) : unionArea(cluster);
3275
+ return area;
3276
+ }
3277
+ /**
3278
+ * The volume of the region outlined by `slices`, in px² × slices (one slice thick per outline):
3279
+ * the sum of each slice's area (see {@link outlinesArea}; Cavalieri's estimator). Times µm per px²
3280
+ * and per slice, µm³.
3281
+ */
3282
+ function outlinedVolume(slices) {
3283
+ let v = 0;
3284
+ for (const s of slices)
3285
+ v += outlinesArea(s.rings, s.ringGroups);
3286
+ return v;
3287
+ }
3288
+ /** The area of the union of `features` (each even-odd over its rings), row band by row band. */
3289
+ function unionArea(features) {
3290
+ const ys = [];
3291
+ for (const f of features)
3292
+ for (const r of f)
3293
+ for (let i = 1; i < r.length; i += 2)
3294
+ ys.push(r[i]);
3295
+ ys.sort((a, b) => a - b);
3296
+ let area = 0;
3297
+ for (let k = 0; k + 1 < ys.length; k++)
3298
+ if (ys[k + 1] > ys[k])
3299
+ area += bandArea(features, ys[k], ys[k + 1], 0);
3300
+ return area;
3301
+ }
3302
+ /**
3303
+ * The union's area between rows `a` and `b`. Between two vertices' rows its width along a row is
3304
+ * linear in y unless outlines cross there: then the band is halved until it is (or thin enough).
3305
+ */
3306
+ function bandArea(features, a, b, depth) {
3307
+ const h = b - a;
3308
+ const at = (t) => unionWidth(features, a + t * h);
3309
+ const mid = at(0.5);
3310
+ if (depth >= 10)
3311
+ return h * mid;
3312
+ const e = 1e-6;
3313
+ const tol = 1e-9 * (1 + Math.abs(mid));
3314
+ const linear = Math.abs(at(0.25) + at(0.75) - 2 * mid) <= tol && Math.abs(at(e) + at(1 - e) - 2 * mid) <= tol;
3315
+ if (linear)
3316
+ return h * mid;
3317
+ const m = a + h / 2;
3318
+ return bandArea(features, a, m, depth + 1) + bandArea(features, m, b, depth + 1);
3319
+ }
3320
+ /** The length of the union of `features` along the row `y`. */
3321
+ function unionWidth(features, y) {
3322
+ const spans = [];
3323
+ const xs = [];
3324
+ for (const f of features) {
3325
+ xs.length = 0;
3326
+ for (const r of f) {
3327
+ const n = r.length >> 1;
3328
+ for (let i = 0, k = n - 1; i < n; k = i++) {
3329
+ const ya = r[2 * i + 1], yb = r[2 * k + 1];
3330
+ if (ya > y !== yb > y)
3331
+ xs.push(r[2 * i] + ((y - ya) * (r[2 * k] - r[2 * i])) / (yb - ya));
3332
+ }
3333
+ }
3334
+ xs.sort((p, q) => p - q);
3335
+ for (let c = 0; c + 1 < xs.length; c += 2)
3336
+ spans.push([xs[c], xs[c + 1]]);
3337
+ }
3338
+ spans.sort((p, q) => p[0] - q[0]);
3339
+ let width = 0, from = -Infinity, to = -Infinity;
3340
+ for (const [s0, s1] of spans) {
3341
+ if (s0 > to) {
3342
+ if (to > from)
3343
+ width += to - from;
3344
+ from = s0;
3345
+ to = s1;
3346
+ }
3347
+ else if (s1 > to)
3348
+ to = s1;
3349
+ }
3350
+ if (to > from)
3351
+ width += to - from;
3352
+ return width;
3353
+ }
3354
+ /** The extent of a feature's rings. */
3355
+ function featureBox(rings) {
3356
+ const box = [Infinity, Infinity, -Infinity, -Infinity];
3357
+ for (const r of rings) {
3358
+ const [x0, y0, x1, y1] = ringBox(r);
3359
+ box[0] = Math.min(box[0], x0);
3360
+ box[1] = Math.min(box[1], y0);
3361
+ box[2] = Math.max(box[2], x1);
3362
+ box[3] = Math.max(box[3], y1);
3363
+ }
3364
+ return box;
3365
+ }
3366
+ function ringBox(r) {
3367
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
3368
+ for (let i = 0; i + 1 < r.length; i += 2) {
3369
+ if (r[i] < x0)
3370
+ x0 = r[i];
3371
+ if (r[i] > x1)
3372
+ x1 = r[i];
3373
+ if (r[i + 1] < y0)
3374
+ y0 = r[i + 1];
3375
+ if (r[i + 1] > y1)
3376
+ y1 = r[i + 1];
3377
+ }
3378
+ return [x0, y0, x1, y1];
3379
+ }
3380
+
2930
3381
  /**
2931
3382
  * Pure ring (closed polyline) helpers: bounds, containment, vertex dropping and
2932
3383
  * simplification. No DOM, no Angular, no `this` — usable from workers.
@@ -3344,6 +3795,74 @@ function selectInRegionsProjected(screen, count, regions) {
3344
3795
  }
3345
3796
  return { mask, count: hits };
3346
3797
  }
3798
+ /**
3799
+ * Observations inside the VOLUME a stack of outlines encloses: a region class (an
3800
+ * atlas structure with its substructures) through every section of the dataset's
3801
+ * reference volume, as its 3D reconstruction shows it.
3802
+ *
3803
+ * The outlines are in the volume's own grid — x, y in voxels, z the slice — which
3804
+ * is where regions imported onto the volume's stack sit. An observation is in voxel
3805
+ * `(⌊x / vx⌋, ⌊y / vy⌋, ⌊z / vz⌋)` (see `volumeImageRef`), and selected when its
3806
+ * slice's outlines cover that voxel, filled as the reconstruction fills them. One
3807
+ * slice is rasterised at a time, so a structure through the whole brain costs one
3808
+ * slice's mask, not the volume's. An observation outside the volume, or without a
3809
+ * z, is never selected.
3810
+ */
3811
+ function selectInClassVolumes(observations, volume, slices) {
3812
+ const n = observations.count;
3813
+ const mask = new Uint8Array(n);
3814
+ const z = observations.z;
3815
+ const { width, height, depth } = volume;
3816
+ const [vx, vy, vz] = volume.voxelSize;
3817
+ const bySlice = new Map();
3818
+ for (const s of slices) {
3819
+ if (!s.rings.length || s.z < 0 || s.z >= depth)
3820
+ continue;
3821
+ const list = bySlice.get(s.z);
3822
+ if (list)
3823
+ list.push(s);
3824
+ else
3825
+ bySlice.set(s.z, [s]);
3826
+ }
3827
+ if (!z || !bySlice.size)
3828
+ return { mask, count: 0 };
3829
+ // The observations of each outlined slice (a counting sort by slice).
3830
+ const sliceOf = new Int32Array(n).fill(-1);
3831
+ const counts = new Uint32Array(depth + 1);
3832
+ for (let i = 0; i < n; i++) {
3833
+ const k = Math.floor(z[i] / vz);
3834
+ if (!bySlice.has(k))
3835
+ continue;
3836
+ sliceOf[i] = k;
3837
+ counts[k + 1]++;
3838
+ }
3839
+ for (let k = 0; k < depth; k++)
3840
+ counts[k + 1] += counts[k];
3841
+ const order = new Uint32Array(counts[depth]);
3842
+ const at = counts.slice(0, depth);
3843
+ for (let i = 0; i < n; i++)
3844
+ if (sliceOf[i] >= 0)
3845
+ order[at[sliceOf[i]]++] = i;
3846
+ const plane = new Uint8Array(width * height);
3847
+ let count = 0;
3848
+ for (const [k, outlines] of bySlice) {
3849
+ plane.fill(0);
3850
+ for (const s of outlines)
3851
+ fillOutlines(plane, width, height, s);
3852
+ for (let o = counts[k]; o < counts[k + 1]; o++) {
3853
+ const i = order[o];
3854
+ const px = Math.floor(observations.x[i] / vx);
3855
+ const py = Math.floor(observations.y[i] / vy);
3856
+ if (px < 0 || py < 0 || px >= width || py >= height)
3857
+ continue;
3858
+ if (plane[py * width + px]) {
3859
+ mask[i] = 1;
3860
+ count++;
3861
+ }
3862
+ }
3863
+ }
3864
+ return { mask, count };
3865
+ }
3347
3866
  /**
3348
3867
  * Whether two selections hold exactly the same observations.
3349
3868
  *
@@ -3389,6 +3908,16 @@ function selectByCategory(codes, code) {
3389
3908
  class SpatialSelectionStore {
3390
3909
  constructor() {
3391
3910
  this.selection$ = new BehaviorSubject(emptySelection());
3911
+ /** Bumped by every write, a clear of nothing included (see {@link revision}). */
3912
+ this.writes = 0;
3913
+ }
3914
+ /**
3915
+ * How many times the selection was written (set or cleared, by any writer). A selection
3916
+ * computed asynchronously compares it before publishing: a newer write — a Clear, a legend
3917
+ * click, a linked chart's brush — supersedes it.
3918
+ */
3919
+ revision() {
3920
+ return this.writes;
3392
3921
  }
3393
3922
  /** Emits on every change, including a clear. */
3394
3923
  getSelection$() {
@@ -3406,9 +3935,11 @@ class SpatialSelectionStore {
3406
3935
  * "selected nothing" ROI visibly clears rather than silently keeping the
3407
3936
  * previous selection on screen. */
3408
3937
  set(selection) {
3938
+ this.writes++;
3409
3939
  this.selection$.next(selection);
3410
3940
  }
3411
3941
  clear() {
3942
+ this.writes++;
3412
3943
  if (this.selection$.value.count === 0 && this.selection$.value.mask.length === 0)
3413
3944
  return;
3414
3945
  this.selection$.next(emptySelection());
@@ -4135,447 +4666,6 @@ function abortable(promise, signal) {
4135
4666
  });
4136
4667
  }
4137
4668
 
4138
- /** Points per line at most: the longest line is resampled at about one point per `step` px. */
4139
- const MAX_POINTS = 2048;
4140
- /** Points per line when matching lines across slices. */
4141
- const MATCH_POINTS = 32;
4142
- /**
4143
- * The sheet through `slices`' lines, in image pixels (x, y) and slice indices (z: slice k's line at
4144
- * z = k). Every line is resampled at the same number of points, evenly along its length (about one
4145
- * per `step` px of the longest), so a line shared by the strips above and below it is one row of
4146
- * vertices. Lines are matched between neighbouring slices closest first, each at most once, and only
4147
- * when their boxes overlap; a line without a match ends the sheet there. Empty without two slices
4148
- * with lines.
4149
- */
4150
- function loftLines(slices, step = 1) {
4151
- const stack = slices
4152
- .map((s) => ({ z: s.z, lines: s.lines.filter((l) => l.length >= 4) }))
4153
- .filter((s) => s.lines.length)
4154
- .sort((a, b) => a.z - b.z);
4155
- if (stack.length < 2)
4156
- return { vertices: new Float32Array(0), faces: new Uint32Array(0) };
4157
- let longest = 0;
4158
- for (const s of stack)
4159
- for (const l of s.lines)
4160
- longest = Math.max(longest, lineLength(l));
4161
- const n = Math.min(MAX_POINTS, Math.max(2, Math.ceil(longest / Math.max(step, 1e-6)) + 1));
4162
- // Each line's resampled points, once: rows of n vertices.
4163
- const rows = [];
4164
- const verts = [];
4165
- for (const s of stack) {
4166
- rows.push(s.lines.map((l) => {
4167
- const first = verts.length / 3;
4168
- const pts = resample(l, n);
4169
- for (let i = 0; i < n; i++)
4170
- verts.push(pts[2 * i], pts[2 * i + 1], s.z);
4171
- return { first, probe: resample(l, MATCH_POINTS), box: lineBox(l) };
4172
- }));
4173
- }
4174
- const faces = [];
4175
- for (let k = 0; k + 1 < rows.length; k++) {
4176
- for (const { a, b, reversed } of matchLines(rows[k], rows[k + 1])) {
4177
- const at = (i) => b.first + (reversed ? n - 1 - i : i);
4178
- for (let i = 0; i + 1 < n; i++) {
4179
- const a0 = a.first + i, a1 = a0 + 1, b0 = at(i), b1 = at(i + 1);
4180
- faces.push(a0, b0, a1, a1, b0, b1);
4181
- }
4182
- }
4183
- }
4184
- if (!faces.length)
4185
- return { vertices: new Float32Array(0), faces: new Uint32Array(0) };
4186
- return { vertices: Float32Array.from(verts), faces: Uint32Array.from(faces) };
4187
- }
4188
- /** Pairs of lines of two neighbouring slices: closest first (mean distance, either direction), each once. */
4189
- function matchLines(as, bs) {
4190
- const pairs = [];
4191
- as.forEach((a, i) => bs.forEach((b, j) => {
4192
- if (!boxesOverlap(a.box, b.box))
4193
- return;
4194
- const forward = meanDistance(a.probe, b.probe, false);
4195
- const backward = meanDistance(a.probe, b.probe, true);
4196
- pairs.push({ a: i, b: j, cost: Math.min(forward, backward), reversed: backward < forward });
4197
- }));
4198
- pairs.sort((p, q) => p.cost - q.cost);
4199
- const usedA = new Set();
4200
- const usedB = new Set();
4201
- const out = [];
4202
- for (const p of pairs) {
4203
- if (usedA.has(p.a) || usedB.has(p.b))
4204
- continue;
4205
- usedA.add(p.a);
4206
- usedB.add(p.b);
4207
- out.push({ a: as[p.a], b: bs[p.b], reversed: p.reversed });
4208
- }
4209
- return out;
4210
- }
4211
- function meanDistance(a, b, reversed) {
4212
- const n = a.length >> 1;
4213
- let sum = 0;
4214
- for (let i = 0; i < n; i++) {
4215
- const j = reversed ? n - 1 - i : i;
4216
- sum += Math.hypot(a[2 * i] - b[2 * j], a[2 * i + 1] - b[2 * j + 1]);
4217
- }
4218
- return sum / n;
4219
- }
4220
- /** `n` points evenly spaced along the polyline, both ends kept. */
4221
- function resample(line, n) {
4222
- const m = line.length >> 1;
4223
- const cumulative = new Float64Array(m);
4224
- for (let i = 1; i < m; i++) {
4225
- cumulative[i] =
4226
- cumulative[i - 1] + Math.hypot(line[2 * i] - line[2 * i - 2], line[2 * i + 1] - line[2 * i - 1]);
4227
- }
4228
- const total = cumulative[m - 1];
4229
- const out = new Float64Array(2 * n);
4230
- let seg = 1;
4231
- for (let i = 0; i < n; i++) {
4232
- const d = n === 1 ? 0 : (total * i) / (n - 1);
4233
- while (seg < m - 1 && cumulative[seg] < d)
4234
- seg++;
4235
- const span = cumulative[seg] - cumulative[seg - 1];
4236
- const t = span > 0 ? Math.min(1, Math.max(0, (d - cumulative[seg - 1]) / span)) : 0;
4237
- out[2 * i] = line[2 * seg - 2] + t * (line[2 * seg] - line[2 * seg - 2]);
4238
- out[2 * i + 1] = line[2 * seg - 1] + t * (line[2 * seg + 1] - line[2 * seg - 1]);
4239
- }
4240
- return out;
4241
- }
4242
- function lineLength(line) {
4243
- let len = 0;
4244
- for (let i = 2; i + 1 < line.length; i += 2)
4245
- len += Math.hypot(line[i] - line[i - 2], line[i + 1] - line[i - 1]);
4246
- return len;
4247
- }
4248
- function lineBox(line) {
4249
- const box = [Infinity, Infinity, -Infinity, -Infinity];
4250
- for (let i = 0; i + 1 < line.length; i += 2) {
4251
- box[0] = Math.min(box[0], line[i]);
4252
- box[1] = Math.min(box[1], line[i + 1]);
4253
- box[2] = Math.max(box[2], line[i]);
4254
- box[3] = Math.max(box[3], line[i + 1]);
4255
- }
4256
- return box;
4257
- }
4258
- /** Whether the boxes overlap, each grown by a tenth of its larger side (a flat line has a thin box). */
4259
- function boxesOverlap(a, b) {
4260
- const ga = 0.1 * Math.max(a[2] - a[0], a[3] - a[1]);
4261
- const gb = 0.1 * Math.max(b[2] - b[0], b[3] - b[1]);
4262
- const g = ga + gb;
4263
- return a[0] - g <= b[2] && b[0] - g <= a[2] && a[1] - g <= b[3] && b[1] - g <= a[3];
4264
- }
4265
-
4266
- /**
4267
- * The 3D reconstruction of a region from its outlines on a stack of slices: each slice's outlines
4268
- * filled (each feature even-odd, the features as their union) into a voxel mask, the mask's
4269
- * boundary meshed by surface nets and smoothed; and its open lines lofted into a sheet.
4270
- *
4271
- * Pure: usable from workers.
4272
- */
4273
- /**
4274
- * The mesh of the region outlined by `slices`, in image pixels (x, y) and slice indices (z: slice
4275
- * k's plane at z = k): the filled outlines' surface, and the sheet through the lines (see
4276
- * {@link loftLines}). Empty when no slice has an area and no two slices have lines.
4277
- */
4278
- function reconstruct(slices, opts = {}) {
4279
- const filled = fill(slices, opts);
4280
- const lines = slices.filter((s) => s.lines?.length);
4281
- if (!lines.length)
4282
- return filled;
4283
- const sheet = loftLines(lines.map((s) => ({ z: s.z, lines: s.lines ?? [] })), opts.pixelStep ?? 1);
4284
- return mergeMeshes(filled, sheet);
4285
- }
4286
- /** The surface of the filled outlines. */
4287
- function fill(slices, opts) {
4288
- let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity, z0 = Infinity, z1 = -Infinity;
4289
- for (const s of slices) {
4290
- for (const r of s.rings) {
4291
- for (let i = 0; i + 1 < r.length; i += 2) {
4292
- if (r[i] < x0)
4293
- x0 = r[i];
4294
- if (r[i] > x1)
4295
- x1 = r[i];
4296
- if (r[i + 1] < y0)
4297
- y0 = r[i + 1];
4298
- if (r[i + 1] > y1)
4299
- y1 = r[i + 1];
4300
- }
4301
- }
4302
- if (s.rings.length) {
4303
- z0 = Math.min(z0, s.z);
4304
- z1 = Math.max(z1, s.z);
4305
- }
4306
- }
4307
- if (!(x1 > x0) || !(y1 > y0))
4308
- return { vertices: new Float32Array(0), faces: new Uint32Array(0) };
4309
- const nz = z1 - z0 + 1;
4310
- const maxVoxels = opts.maxVoxels ?? 32_000_000;
4311
- let step = Math.max(opts.pixelStep ?? 1, 1e-6);
4312
- while (Math.ceil((x1 - x0) / step) * Math.ceil((y1 - y0) / step) * nz > maxVoxels)
4313
- step *= 1.25;
4314
- const nx = Math.max(1, Math.ceil((x1 - x0) / step));
4315
- const ny = Math.max(1, Math.ceil((y1 - y0) / step));
4316
- const mask = new Uint8Array(nx * ny * nz);
4317
- for (const s of slices) {
4318
- for (const rings of featureRings(s.rings, s.ringGroups)) {
4319
- fillSlice(mask, nx, ny, (s.z - z0) * nx * ny, rings, x0, y0, step);
4320
- }
4321
- }
4322
- // Voxel i spanning [i, i + 1) (napari-js centres it on i): the scaling below maps cell edges to px.
4323
- const mesh = smoothMesh(surfaceNets(mask, [nx, ny, nz], { origin: [0.5, 0.5, 0.5] }), {
4324
- iterations: opts.smooth ?? 2,
4325
- });
4326
- const v = mesh.vertices;
4327
- for (let i = 0; i < v.length; i += 3) {
4328
- v[i] = x0 + v[i] * step;
4329
- v[i + 1] = y0 + v[i + 1] * step;
4330
- // Voxel k spans [k, k+1) with its centre (the slice) at k + 0.5.
4331
- v[i + 2] = z0 + v[i + 2] - 0.5;
4332
- }
4333
- return mesh;
4334
- }
4335
- /** `a` and `b` as one mesh. */
4336
- function mergeMeshes(a, b) {
4337
- if (!b.faces.length)
4338
- return a;
4339
- if (!a.faces.length)
4340
- return b;
4341
- const vertices = new Float32Array(a.vertices.length + b.vertices.length);
4342
- vertices.set(a.vertices);
4343
- vertices.set(b.vertices, a.vertices.length);
4344
- const faces = new Uint32Array(a.faces.length + b.faces.length);
4345
- faces.set(a.faces);
4346
- const offset = a.vertices.length / 3;
4347
- for (let i = 0; i < b.faces.length; i++)
4348
- faces[a.faces.length + i] = b.faces[i] + offset;
4349
- return { vertices, faces };
4350
- }
4351
- /**
4352
- * Fill one feature into one slice of the mask: even-odd over its `rings`, sampled at voxel
4353
- * centres. Cells already set stay set, so the features of a slice fill as their union.
4354
- */
4355
- function fillSlice(mask, nx, ny, offset, rings, x0, y0, step) {
4356
- const crossings = [];
4357
- // Only the rows the feature spans.
4358
- let top = Infinity, bottom = -Infinity;
4359
- for (const r of rings) {
4360
- for (let i = 1; i < r.length; i += 2) {
4361
- if (r[i] < top)
4362
- top = r[i];
4363
- if (r[i] > bottom)
4364
- bottom = r[i];
4365
- }
4366
- }
4367
- const j0 = Math.max(0, Math.floor((top - y0) / step - 0.5));
4368
- const j1 = Math.min(ny - 1, Math.ceil((bottom - y0) / step - 0.5));
4369
- for (let j = j0; j <= j1; j++) {
4370
- const y = y0 + (j + 0.5) * step;
4371
- crossings.length = 0;
4372
- for (const r of rings) {
4373
- const n = r.length >> 1;
4374
- for (let i = 0, k = n - 1; i < n; k = i++) {
4375
- const ya = r[2 * i + 1], yb = r[2 * k + 1];
4376
- if (ya > y !== yb > y) {
4377
- const xa = r[2 * i], xb = r[2 * k];
4378
- crossings.push(xa + ((y - ya) * (xb - xa)) / (yb - ya));
4379
- }
4380
- }
4381
- }
4382
- if (crossings.length < 2)
4383
- continue;
4384
- crossings.sort((a, b) => a - b);
4385
- const row = offset + j * nx;
4386
- for (let c = 0; c + 1 < crossings.length; c += 2) {
4387
- const from = Math.max(0, Math.ceil((crossings[c] - x0) / step - 0.5));
4388
- const to = Math.min(nx - 1, Math.floor((crossings[c + 1] - x0) / step - 0.5));
4389
- for (let i = from; i <= to; i++)
4390
- mask[row + i] = 1;
4391
- }
4392
- }
4393
- }
4394
- /**
4395
- * The area inside `rings` (interleaved `x, y`), even-odd: a ring inside an odd number of others is
4396
- * a hole. Exact for rings that do not cross (a polygon set's exteriors and holes).
4397
- */
4398
- function ringsArea(rings) {
4399
- const boxes = rings.map(ringBox);
4400
- let area = 0;
4401
- for (let i = 0; i < rings.length; i++) {
4402
- const r = rings[i];
4403
- if (r.length < 6)
4404
- continue;
4405
- const x = r[0], y = r[1];
4406
- let depth = 0;
4407
- for (let j = 0; j < rings.length; j++) {
4408
- if (j === i)
4409
- continue;
4410
- const b = boxes[j];
4411
- if (x < b[0] || x > b[2] || y < b[1] || y > b[3])
4412
- continue;
4413
- if (pointInRing$2(x, y, rings[j]))
4414
- depth++;
4415
- }
4416
- area += (depth % 2 ? -1 : 1) * Math.abs(ringArea$1(r));
4417
- }
4418
- return Math.max(0, area);
4419
- }
4420
- /**
4421
- * The rings of each feature (see {@link SliceOutlines.ringGroups}), in the order first met; one
4422
- * feature of every ring without `groups`.
4423
- */
4424
- function featureRings(rings, groups) {
4425
- if (!groups || !rings.length)
4426
- return rings.length ? [rings.slice()] : [];
4427
- const byGroup = new Map();
4428
- rings.forEach((r, i) => {
4429
- const g = groups[i] ?? 0;
4430
- const feature = byGroup.get(g);
4431
- if (feature)
4432
- feature.push(r);
4433
- else
4434
- byGroup.set(g, [r]);
4435
- });
4436
- return [...byGroup.values()];
4437
- }
4438
- /**
4439
- * The area of a slice's outlines (px²): each feature's (even-odd, see {@link ringsArea}), the
4440
- * features as their union, so a feature inside or over another of the slice adds only what it
4441
- * covers beyond it. Features whose extents overlap are measured together, by the length of their
4442
- * union along each row, exact between the rows through their vertices where no outline crosses
4443
- * another (and refined where one does).
4444
- */
4445
- function outlinesArea(rings, groups) {
4446
- const features = featureRings(rings, groups).map((f) => ({ rings: f, box: featureBox(f) }));
4447
- if (features.length < 2)
4448
- return features.length ? ringsArea(features[0].rings) : 0;
4449
- // Features whose extents overlap, joined (by x, so a feature meets only those it can overlap).
4450
- features.sort((a, b) => a.box[0] - b.box[0]);
4451
- const parent = features.map((_, i) => i);
4452
- const root = (i) => (parent[i] === i ? i : (parent[i] = root(parent[i])));
4453
- for (let i = 0; i < features.length; i++) {
4454
- const a = features[i].box;
4455
- for (let j = i + 1; j < features.length && features[j].box[0] < a[2]; j++) {
4456
- const b = features[j].box;
4457
- if (b[1] < a[3] && a[1] < b[3])
4458
- parent[root(j)] = root(i);
4459
- }
4460
- }
4461
- const clusters = new Map();
4462
- features.forEach((f, i) => {
4463
- const r = root(i);
4464
- const cluster = clusters.get(r);
4465
- if (cluster)
4466
- cluster.push(f.rings);
4467
- else
4468
- clusters.set(r, [f.rings]);
4469
- });
4470
- let area = 0;
4471
- for (const cluster of clusters.values())
4472
- area += cluster.length === 1 ? ringsArea(cluster[0]) : unionArea(cluster);
4473
- return area;
4474
- }
4475
- /**
4476
- * The volume of the region outlined by `slices`, in px² × slices (one slice thick per outline):
4477
- * the sum of each slice's area (see {@link outlinesArea}; Cavalieri's estimator). Times µm per px²
4478
- * and per slice, µm³.
4479
- */
4480
- function outlinedVolume(slices) {
4481
- let v = 0;
4482
- for (const s of slices)
4483
- v += outlinesArea(s.rings, s.ringGroups);
4484
- return v;
4485
- }
4486
- /** The area of the union of `features` (each even-odd over its rings), row band by row band. */
4487
- function unionArea(features) {
4488
- const ys = [];
4489
- for (const f of features)
4490
- for (const r of f)
4491
- for (let i = 1; i < r.length; i += 2)
4492
- ys.push(r[i]);
4493
- ys.sort((a, b) => a - b);
4494
- let area = 0;
4495
- for (let k = 0; k + 1 < ys.length; k++)
4496
- if (ys[k + 1] > ys[k])
4497
- area += bandArea(features, ys[k], ys[k + 1], 0);
4498
- return area;
4499
- }
4500
- /**
4501
- * The union's area between rows `a` and `b`. Between two vertices' rows its width along a row is
4502
- * linear in y unless outlines cross there: then the band is halved until it is (or thin enough).
4503
- */
4504
- function bandArea(features, a, b, depth) {
4505
- const h = b - a;
4506
- const at = (t) => unionWidth(features, a + t * h);
4507
- const mid = at(0.5);
4508
- if (depth >= 10)
4509
- return h * mid;
4510
- const e = 1e-6;
4511
- const tol = 1e-9 * (1 + Math.abs(mid));
4512
- const linear = Math.abs(at(0.25) + at(0.75) - 2 * mid) <= tol && Math.abs(at(e) + at(1 - e) - 2 * mid) <= tol;
4513
- if (linear)
4514
- return h * mid;
4515
- const m = a + h / 2;
4516
- return bandArea(features, a, m, depth + 1) + bandArea(features, m, b, depth + 1);
4517
- }
4518
- /** The length of the union of `features` along the row `y`. */
4519
- function unionWidth(features, y) {
4520
- const spans = [];
4521
- const xs = [];
4522
- for (const f of features) {
4523
- xs.length = 0;
4524
- for (const r of f) {
4525
- const n = r.length >> 1;
4526
- for (let i = 0, k = n - 1; i < n; k = i++) {
4527
- const ya = r[2 * i + 1], yb = r[2 * k + 1];
4528
- if (ya > y !== yb > y)
4529
- xs.push(r[2 * i] + ((y - ya) * (r[2 * k] - r[2 * i])) / (yb - ya));
4530
- }
4531
- }
4532
- xs.sort((p, q) => p - q);
4533
- for (let c = 0; c + 1 < xs.length; c += 2)
4534
- spans.push([xs[c], xs[c + 1]]);
4535
- }
4536
- spans.sort((p, q) => p[0] - q[0]);
4537
- let width = 0, from = -Infinity, to = -Infinity;
4538
- for (const [s0, s1] of spans) {
4539
- if (s0 > to) {
4540
- if (to > from)
4541
- width += to - from;
4542
- from = s0;
4543
- to = s1;
4544
- }
4545
- else if (s1 > to)
4546
- to = s1;
4547
- }
4548
- if (to > from)
4549
- width += to - from;
4550
- return width;
4551
- }
4552
- /** The extent of a feature's rings. */
4553
- function featureBox(rings) {
4554
- const box = [Infinity, Infinity, -Infinity, -Infinity];
4555
- for (const r of rings) {
4556
- const [x0, y0, x1, y1] = ringBox(r);
4557
- box[0] = Math.min(box[0], x0);
4558
- box[1] = Math.min(box[1], y0);
4559
- box[2] = Math.max(box[2], x1);
4560
- box[3] = Math.max(box[3], y1);
4561
- }
4562
- return box;
4563
- }
4564
- function ringBox(r) {
4565
- let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
4566
- for (let i = 0; i + 1 < r.length; i += 2) {
4567
- if (r[i] < x0)
4568
- x0 = r[i];
4569
- if (r[i] > x1)
4570
- x1 = r[i];
4571
- if (r[i + 1] < y0)
4572
- y0 = r[i + 1];
4573
- if (r[i + 1] > y1)
4574
- y1 = r[i + 1];
4575
- }
4576
- return [x0, y0, x1, y1];
4577
- }
4578
-
4579
4669
  /**
4580
4670
  * Where to put a label inside a polygon: its pole of inaccessibility, the inside point farthest
4581
4671
  * from every edge (Garcia-Castellanos & Lombardo's grid search, as in Mapbox's polylabel). Unlike
@@ -10399,7 +10489,7 @@ function frameToRgba(cached, frameIndex) {
10399
10489
  /** Lazy-import so onnxruntime-web is never pulled into unit tests or the
10400
10490
  * initial bundle — only when segmentation actually runs. */
10401
10491
  const onnxSessionFactory = async () => {
10402
- const { OnnxSamSession } = await import('./jax-data-science-sci-image-visualizer-onnx-sam-session-CvadfzjS.mjs');
10492
+ const { OnnxSamSession } = await import('./jax-data-science-sci-image-visualizer-onnx-sam-session-D0IQ6PjZ.mjs');
10403
10493
  return new OnnxSamSession();
10404
10494
  };
10405
10495
  /** Thrown inside a load that a model switch superseded. */
@@ -12902,11 +12992,11 @@ class ToolbarHelpDialogComponent {
12902
12992
  }
12903
12993
  }
12904
12994
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ToolbarHelpDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
12905
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: ToolbarHelpDialogComponent, isStandalone: true, selector: "toolbar-help-dialog", inputs: { visible: "visible", contributedTools: "contributedTools", dialogTools: "dialogTools" }, outputs: { visibleChange: "visibleChange" }, usesOnChanges: true, ngImport: i0, template: "<p-dialog\n header=\"Plotting help\"\n [visible]=\"visible\"\n (visibleChange)=\"visibleChange.emit($event)\"\n [modal]=\"true\"\n [style]=\"{ width: '50vw' }\"\n [maximizable]=\"true\"\n [draggable]=\"false\"\n [resizable]=\"false\"\n>\n <p>\n Click a file in the left-hand tree to display it. By default it opens in the <b>Image</b> view \u2014 a natively\n tiled, deeply zoomable raster (OpenSeadragon) with a navigator minimap and a physical-units scale bar.\n <b>Click the image to zoom</b> toward the clicked point; scroll to zoom and drag to pan. When a stack is open,\n a slice slider (Image view) or image-mode toggle (other views) appears. Regions can also be edited in the\n <b>Region Editor</b>, opened with the\n <img src=\"assets/plotting/region-editor.svg\" width=\"14\" height=\"14\" alt=\"Region Editor\" />\n toolbar button as a floating dialog. Drag the <i class=\"pi pi-bars\"></i> handle to detach this toolbar into a\n floating window.\n </p>\n\n <h3>Plot-type dropdown</h3>\n <p>\n The <b>plot-type dropdown</b> switches how the file is visualized: <b>Image</b>, <b>Heatmap</b>,\n <b>Contour</b>, <b>Surface</b>, <b>Volume</b>, <b>Isosurface</b>, and \u2014 when a spatial-omics dataset is open \u2014\n <b>Spatial omics</b> and <b>Spatial omics 3D</b>.\n </p>\n <p>\n <b>Only the modes the data can actually support are offered</b>, so the list changes with what is loaded rather\n than showing entries that would come up blank:\n </p>\n <ul>\n <li>\n <b>Contour</b>, <b>Volume</b> and <b>Isosurface</b> need a grayscale image; the two 3D ones also need a\n z-stack.\n </li>\n <li>\n <b>Spatial omics</b> needs a published dataset, and <b>Spatial omics 3D</b> needs one whose coordinates carry\n a z.\n </li>\n <li>\n The pixel modes (<b>Image</b>, <b>Heatmap</b>, <b>Surface</b>) are withheld for a dataset that brings no\n tissue image \u2014 seqFISH records coordinates and no section, so there is nothing for them to draw.\n </li>\n </ul>\n <ul>\n <li>\n \"<b>Isosurface band</b>\" slider (Isosurface): sets the intensity band the surfaces are drawn for, mapped onto\n the volume's real intensity range.\n </li>\n <li>\n \"<b>3D camera</b>\" (Surface / Scatter 3D / Isosurface): zoom <i class=\"pi pi-search\"></i>, pan\n <i class=\"pi pi-arrows-alt\"></i>, orbit <i class=\"pi pi-globe\"></i>, turntable <i class=\"pi pi-sync\"></i>,\n and reset camera <i class=\"pi pi-home\"></i>; scroll to zoom the 3D scene.\n </li>\n </ul>\n\n <h3>Image manipulation tools</h3>\n <ul>\n <li>\n \"<b>Region Editor</b>\" <img src=\"assets/plotting/region-editor.svg\" width=\"15\" height=\"15\" />: opens the\n Region Editor as a floating, draggable dialog \u2014 a per-image table of every region (class, colour, area) with\n bulk select/label/colour/delete and GeoJSON import/export. It stays in sync with the image, so you can keep\n it open while drawing.\n </li>\n <li>\n \"<b>Intensity profile lines</b>\" <img src=\"assets/plotting/spline-chart.svg\" width=\"15\" height=\"15\" /> (<i\n >work in progress \u2014 not yet stable</i\n >): adds a coloured line ROI; a floating inset chart plots intensity along each line, updates live as you\n drag it, and re-samples at the current zoom resolution. Each added line gets its own colour; select a line\n and use the trash tool <i class=\"pi pi-trash\"></i> to remove it (and its trace).\n </li>\n <li>\n \"<b>Channels &amp; Histogram</b>\" <img src=\"assets/plotting/invert.svg\" width=\"15\" height=\"15\" />: opens a\n floating dialog for brightness/contrast, gamma, and per-channel display, plus the\n <b>colormap / LUT</b> controls (grayscale only) \u2014 a <b>reverse</b> toggle and a colormap dropdown (default:\n inverted greys).\n </li>\n <li>\n \"<b>Smoothen</b>\" <img src=\"assets/plotting/smooth.svg\" width=\"15\" height=\"15\" /> (Image view only): the\n image opens showing <b>raw pixels</b> (nearest-neighbour), so zooming in renders crisp blocks for pixel\n inspection. Toggle this on for bilinear smoothing; the icon turns white while smoothing is active.\n </li>\n </ul>\n\n <h3>Download &amp; view tools</h3>\n <ul>\n <li>\"<b>Download</b>\" <i class=\"pi pi-download\"></i>: download the current image as a PNG.</li>\n <li>\n \"<b>Fit to view</b>\" / autoscale <i class=\"pi pi-window-maximize\"></i>: resets the image to its original\n dimensions. In stack mode it also resets to single-image mode.\n </li>\n </ul>\n\n <h3>Zoom tools</h3>\n <ul>\n <li>\n \"<b>Zoom box</b>\" <img src=\"assets/plotting/zoom-box.svg\" width=\"15\" height=\"15\" /> (<kbd>b</kbd>): drag a\n box to select an area to zoom into.\n </li>\n <li>\"<b>Pan</b>\" <i class=\"pi pi-arrows-alt\"></i> (<kbd>p</kbd>): drag to pan the image.</li>\n <li>\"<b>Zoom in</b>\" <i class=\"pi pi-search-plus\"></i> (<kbd>+</kbd>): zoom in one step.</li>\n <li>\n \"<b>Zoom out</b>\" <i class=\"pi pi-search-minus\"></i> (<kbd>-</kbd>): zoom out one step, stopping at the\n original image dimensions.\n </li>\n </ul>\n\n <h3>Region tools</h3>\n <p>\n By default no tool is selected (all greyed-out); selecting a tool highlights it. The vertex-editing tools\n (polygon, add/delete vertex, move, B\u00E9zier conversions) are available in the <b>Image</b> view only. On a\n <b>donut</b> (a region with a hole), the inner-ring vertices are editable too \u2014 drag, add, and delete them just\n like the outer ring.\n </p>\n <ul>\n <li>\n \"<b>Selection</b>\" <i class=\"pi pi-arrow-up-right\"></i> (<kbd>s</kbd>): a neutral mode that turns off\n whatever drawing tool was active (wand, vertex eraser, rectangle, etc.). Use it to step out of editing\n without picking another tool.\n </li>\n <li>\n \"<b>Rectangular region</b>\" <i class=\"pi pi-stop\"></i> (<kbd>r</kbd>): click and drag to create a rectangular\n region.\n </li>\n <li>\n \"<b>Polyline</b>\" <img src=\"assets/plotting/polyline.svg\" width=\"14\" height=\"14\" /> (<kbd>l</kbd>): draw an\n open (non-closed) polyline by clicking and dragging. Polylines can be edited and exported to GeoJSON as a\n <code>LineString</code> feature.\n </li>\n <li>\n \"<b>Freeform region</b>\" <i class=\"pi pi-pencil\"></i> (<kbd>f</kbd>): click and drag to draw a closed\n freeform polygon.\n </li>\n <li>\n \"<b>Brush</b>\" <img src=\"assets/plotting/paintbrush.svg\" width=\"15\" height=\"15\" />: click and drag to paint a\n region with a circular brush \u2014 the painted stroke is the region. A <b>Size</b> slider (brush diameter, in\n image pixels) appears in the toolbar while the brush is active. Hold <kbd>Shift</kbd> while painting to\n <b>erase</b> (subtract) from a region instead. Painting over an existing region extends it; brushing across\n two regions merges them. Painting a closed ring (or shift-erasing an interior pocket) leaves the enclosed\n area empty \u2014 a <b>donut</b> (Image view).\n </li>\n <li>\n \"<b>Polygon</b>\" <img src=\"assets/plotting/polygon-vertices.svg\" width=\"15\" height=\"15\" />: click to place\n each vertex; click the first point to close the polygon.\n </li>\n <li>\n \"<b>Add vertex</b>\" <img src=\"assets/plotting/vertex-add.svg\" width=\"15\" height=\"15\" />: click an edge of the\n selected polygon to insert a new vertex.\n </li>\n <li>\n \"<b>Delete vertex</b>\" <img src=\"assets/plotting/vertex-delete.svg\" width=\"15\" height=\"15\" />: click a vertex\n of the selected polygon to remove it.\n </li>\n <li>\n <b>Moving a region</b> <img src=\"assets/plotting/region-move.svg\" width=\"15\" height=\"15\" />: with the Select\n tool, drag the selected region's body (or a rectangle's handles to resize it).\n </li>\n <li>\n <b>Convert to B\u00E9zier</b> <img src=\"assets/plotting/to-bezier.svg\" width=\"15\" height=\"15\" /> /\n <b>Convert to polygon</b> <img src=\"assets/plotting/to-polygon.svg\" width=\"15\" height=\"15\" /> (Image view):\n right-click the selected region and pick the conversion in the context menu to turn it into a smooth B\u00E9zier\n curve, or back into a straight-edged polygon.\n </li>\n <li>\n \"<b>Wand</b>\" <img src=\"assets/plotting/wand.svg\" width=\"15\" height=\"15\" /> (<kbd>w</kbd>): click to grow a\n region from pixels with similar values around the click point. Hold <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> for an\n exact-match flood fill; drag to keep extending the selection while moving; hold <kbd>Shift</kbd> while\n clicking or dragging on an existing region to <b>erase</b> pixels from it. Use the <b>Sens.</b> slider that\n appears to make the wand stricter (higher = smaller selection) or looser. Subsequent clicks keep extending\n the same region; clicking inside an existing region adopts it, and growing into another region merges them.\n </li>\n <li>\n \"<b>Vertex eraser</b>\" <i class=\"pi pi-eraser\"></i> (<kbd>e</kbd>): click or click-and-drag to remove polygon\n vertices that fall inside the cursor circle from any region, including a region's inner rings (donut holes).\n Use the <b>Radius</b> slider to set the eraser size in image pixels. Polygons reduced below 3 vertices (or\n polylines below 2) are deleted entirely; a hole reduced below 3 vertices is removed.\n </li>\n <li>\n \"<b>Undo</b>\" <img src=\"assets/plotting/undo.svg\" width=\"15\" height=\"15\" />\n (<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Z</kbd>): reverts the last region action (draw, delete, edit, wand/brush\n stroke, segmentation, \u2026). Up to the last\n <b>10</b> actions can be undone; the button greys out when there's nothing left to undo.\n </li>\n <li>\n \"<b>Redo</b>\"\n <img src=\"assets/plotting/redo.svg\" width=\"15\" height=\"15\" />\n (<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> or\n <kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Y</kbd>): re-applies the most recently undone action. Performing a new\n region action clears what can be redone.\n </li>\n <li>\n \"<b>Delete region</b>\" <i class=\"pi pi-trash\"></i> (<kbd>Del</kbd>): deletes the selected region. This can\n also be done from the Region Editor.\n </li>\n </ul>\n\n <h3>Region actions (right-click the image)</h3>\n <p>\n Right-click a region \u2014 or the current multi-selection \u2014 to open the \"<b>Selected region(s)</b>\" group at the\n top of the context menu. These act immediately on the selection (they're not drawing modes) and are fully\n undo/redo-able:\n </p>\n <ul>\n <li>\n \"<b>Select all regions</b>\" <i class=\"pi pi-check-square\"></i>: select every region on the image (also in the\n Region Editor toolbar).\n </li>\n <li>\n \"<b>Merge / group</b>\": combine 2+ selected regions into one. If the pieces don't touch, the result is a\n single multi-part region (sparse \"bits\").\n </li>\n <li>\"<b>Ungroup</b>\": split a multi-part region back into one region per piece.</li>\n <li>\n \"<b>Inverse</b>\": replace the selection with everything <i>outside</i> it \u2014 the image rectangle with the\n selected regions punched out as holes. Works at any image size.\n </li>\n <li>\n \"<b>Simplify</b>\": reduce a region's vertex count (Douglas\u2013Peucker) by an altitude threshold in pixels \u2014 pick\n <b>Light</b> (1&nbsp;px), <b>Medium</b> (3&nbsp;px), <b>Strong</b> (8&nbsp;px), or <b>Custom\u2026</b> for a\n slider. Applies to the exterior and any holes (and every part of a multi-part region).\n </li>\n <li>\n \"<b>Convert to B\u00E9zier</b>\" / \"<b>Convert to polygon</b>\" (Image view) and \"<b>Delete</b>\" also live in this\n group; the conversions are only here, not in the toolbar.\n </li>\n </ul>\n\n <h3>Segmentation &amp; detection tools</h3>\n <p>\n AI-assisted segmentation and detection that run entirely in the browser. The first run of any model downloads\n it (a progress toast is shown); it's then cached.\n </p>\n <p>\n Two shapes of tool: the <b>prompted</b> ones (Segment, Point prompts, Cellpose) act on rectangles you draw, and\n generated regions inherit the color of the rectangle they came from<span *ngIf=\"contributedTools.length\"\n >; the <b>no-prompt</b> ones ({{ contributedToolNames }}) sweep the whole current view, so there is nothing\n to draw first</span\n >.\n </p>\n <p>\n Each model dropdown <i class=\"pi pi-chevron-down\"></i> lists the available checkpoints with a\n <i class=\"pi pi-info-circle\"></i> beside each one \u2014 hover it for that model's size, speed, training domain and\n accuracy, so you can pick without leaving the toolbar.\n </p>\n <ul>\n <li>\n \"<b>Segment</b>\" <img src=\"assets/plotting/sam-box-prompt.svg\" width=\"15\" height=\"15\" /> (box prompt): draw\n one or more <b>Rectangular regions</b> around the objects of interest, then click Segment. Each rectangle is\n replaced by the SAM mask of the object inside it.\n </li>\n <li>\n \"<b>Point prompts</b>\" <img src=\"assets/plotting/sam-point-prompt.svg\" width=\"15\" height=\"15\" />: click\n directly on an object to segment it as a <b>new</b> region. Each click starts a fresh object \u2014 clicking\n another object won't grow the previous one. <kbd>Shift</kbd>/<kbd>Alt</kbd>-click adds an\n <i>exclude</i> point that refines the current object; <kbd>Esc</kbd> undoes it, <kbd>Enter</kbd> commits it.\n </li>\n <li>\n \"<b>Model picker</b>\" <i class=\"pi pi-chevron-down\"></i>: the dropdown attached to the Segment button chooses\n the SAM model \u2014 <b>micro-sam ViT-T</b> (default; small, fast) or <b>micro-sam ViT-B</b> (larger, highest\n accuracy) for light microscopy, or <b>patho-sam ViT-B</b> for histopathology (H&amp;E / stained tissue). All\n are promptable Segment Anything models finetuned for biomedical images; they differ in the\n <i>image encoder</i> (the heavy once-per-image step) and the training domain \u2014 box and point prompting behave\n the same. The choice applies to <i>both</i> the box and point tools, which share one loaded model and image\n encoding, so the first run after switching models (or images) downloads and encodes once for both.\n </li>\n <li>\n \"<b>Cellpose</b>\" <img src=\"assets/plotting/cells.svg\" width=\"15\" height=\"15\" />: draw rectangles, then click\n Cellpose to automatically segment every cell inside each rectangle (client-side cellpose-SAM) \u2014 one region\n per detected cell. No clicking on individual cells needed.\n </li>\n <!-- Contributed tools describe themselves. This library ships none, so an\n open build's help lists only the prompted tools above \u2014 the text no\n longer promises buttons that are not there. -->\n <ng-container *ngFor=\"let tool of contributedTools\">\n <li *ngIf=\"tool.help\" [innerHTML]=\"tool.help.body\"></li>\n </ng-container>\n <ng-container *ngFor=\"let tool of dialogTools\">\n <li *ngIf=\"tool.help\" [innerHTML]=\"tool.help.body\"></li>\n </ng-container>\n </ul>\n\n <h3>Spatial omics</h3>\n <p>\n In <b>Spatial omics</b> mode each observation \u2014 a cell or a Visium spot \u2014 is drawn at its own coordinates, over\n the tissue image where the dataset registers onto one. The\n <img src=\"assets/plotting/spatial-omics.svg\" width=\"14\" height=\"14\" alt=\"Spatial omics\" />\n panel colours them by a <b>column</b> (a cell type, a cluster) or by a <b>gene</b>, and the two are exclusive\n so what drives the colours is never ambiguous.\n </p>\n <ul>\n <li>\n \"<b>Select from ROIs</b>\": turns the regions you have drawn into a selection of observations. Everything else\n then follows that selection.\n </li>\n <li>\"<b>Point size</b>\" / \"<b>Opacity</b>\": how the markers are drawn, not what is selected.</li>\n </ul>\n\n <h3>Distribution charts</h3>\n <p>\n The <b>Distribution</b> section of the spatial-omics panel charts whatever the map is coloured by, so the two\n can never disagree. Each tab answers a different question, and the <i class=\"pi pi-question-circle\"></i> beside\n them explains the one on screen \u2014 including what it must <em>not</em> be read for.\n </p>\n <ul>\n <li>\n \"<b>Counts</b>\": how many observations per category. A category is a label, not a magnitude, so a frequency\n is the only distribution it has.\n </li>\n <li>\n \"<b>Histogram</b>\" / \"<b>Violin</b>\" / \"<b>Box</b>\": how a continuous value is distributed \u2014 over everything,\n or split by a grouping column.\n </li>\n <li>\n \"<b>Heatmap</b>\": mean expression of the genes you pick within each group. Z-scored per gene by default, so a\n colour means \"above or below this gene's own average\" rather than \"highly expressed\".\n </li>\n <li>\n \"<b>Embedding</b>\": a UMAP, PCA or t-SNE over the same cells. <b>Lasso it to select on the map</b> \u2014 both\n views read the same selection. Only PCA labels its axes with a percentage, because only its axes explain a\n measurable share of the variance; UMAP and t-SNE coordinates are arbitrary, so distance between distant\n clusters should not be read.\n </li>\n <li>\n \"<b>Detach</b>\": moves the charts into their own window so they can sit beside the map instead of inside the\n panel. Switching tabs keeps the window where it is.\n </li>\n </ul>\n\n <h3>Help</h3>\n <p>\"<b>Help</b>\" <i class=\"pi pi-question-circle\"></i>: opens this dialog.</p>\n\n <h3>Keyboard shortcuts</h3>\n <table class=\"shortcuts-table\">\n <tr>\n <td><kbd>+</kbd> / <kbd>=</kbd></td>\n <td>Zoom in</td>\n </tr>\n <tr>\n <td><kbd>-</kbd></td>\n <td>Zoom out</td>\n </tr>\n <tr>\n <td><kbd>b</kbd></td>\n <td>Toggle zoom box mode</td>\n </tr>\n <tr>\n <td><kbd>p</kbd></td>\n <td>Toggle pan mode</td>\n </tr>\n <tr>\n <td><kbd>f</kbd></td>\n <td>Toggle freeform region drawing</td>\n </tr>\n <tr>\n <td><kbd>l</kbd></td>\n <td>Toggle polyline drawing</td>\n </tr>\n <tr>\n <td><kbd>r</kbd></td>\n <td>Toggle rectangular region drawing</td>\n </tr>\n <tr>\n <td><kbd>w</kbd></td>\n <td>Toggle wand tool</td>\n </tr>\n <tr>\n <td>Wand + <kbd>Shift</kbd></td>\n <td>Erase pixels from the active region</td>\n </tr>\n <tr>\n <td>Wand + <kbd>Ctrl</kbd>/<kbd>Cmd</kbd></td>\n <td>Exact-match flood fill (no smoothing)</td>\n </tr>\n <tr>\n <td><kbd>e</kbd></td>\n <td>Toggle vertex eraser</td>\n </tr>\n <tr>\n <td><kbd>s</kbd></td>\n <td>Selection mode (deactivates drawing tools)</td>\n </tr>\n <tr>\n <td>Point prompt + <kbd>Shift</kbd>/<kbd>Alt</kbd></td>\n <td>Add an exclude point (refine the current object)</td>\n </tr>\n <tr>\n <td><kbd>Enter</kbd></td>\n <td>Commit the current SAM point object</td>\n </tr>\n <tr>\n <td><kbd>Esc</kbd></td>\n <td>Undo the current SAM point object</td>\n </tr>\n <tr>\n <td><kbd>Del</kbd> / <kbd>\u232B</kbd></td>\n <td>Delete selected region</td>\n </tr>\n <tr>\n <td><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Z</kbd></td>\n <td>Undo the last region action (up to 10)</td>\n </tr>\n <tr>\n <td><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> / <kbd>Ctrl</kbd>+<kbd>Y</kbd></td>\n <td>Redo the last undone region action</td>\n </tr>\n </table>\n\n <ng-template pTemplate=\"footer\">\n <p-button icon=\"pi pi-check\" (click)=\"visibleChange.emit(false)\" label=\"Ok\" styleClass=\"p-button-text\">\n </p-button>\n </ng-template>\n</p-dialog>\n", styles: ["kbd{background:#e8e8e8;border:1px solid #bbb;border-radius:3px;padding:1px 5px;font-family:monospace;font-size:.85em}.shortcuts-table{border-collapse:collapse}.shortcuts-table td{padding:.25rem .75rem .25rem 0;vertical-align:middle}.shortcuts-table td:first-child{white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i5.Dialog, selector: "p-dialog", inputs: ["header", "draggable", "resizable", "positionLeft", "positionTop", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "responsive", "appendTo", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "breakpoint", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "visible", "style", "position"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
12995
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: ToolbarHelpDialogComponent, isStandalone: true, selector: "toolbar-help-dialog", inputs: { visible: "visible", contributedTools: "contributedTools", dialogTools: "dialogTools" }, outputs: { visibleChange: "visibleChange" }, usesOnChanges: true, ngImport: i0, template: "<p-dialog\n header=\"Plotting help\"\n [visible]=\"visible\"\n (visibleChange)=\"visibleChange.emit($event)\"\n [modal]=\"true\"\n [style]=\"{ width: '50vw' }\"\n [maximizable]=\"true\"\n [draggable]=\"false\"\n [resizable]=\"false\"\n>\n <p>\n Click a file in the left-hand tree to display it. By default it opens in the <b>Image</b> view \u2014 a natively\n tiled, deeply zoomable raster (OpenSeadragon) with a navigator minimap and a physical-units scale bar.\n <b>Click the image to zoom</b> toward the clicked point; scroll to zoom and drag to pan. When a stack is open,\n a slice slider (Image view) or image-mode toggle (other views) appears. Regions can also be edited in the\n <b>Region Editor</b>, opened with the\n <img src=\"assets/plotting/region-editor.svg\" width=\"14\" height=\"14\" alt=\"Region Editor\" />\n toolbar button as a floating dialog. Drag the <i class=\"pi pi-bars\"></i> handle to detach this toolbar into a\n floating window.\n </p>\n\n <h3>Plot-type dropdown</h3>\n <p>\n The <b>plot-type dropdown</b> switches how the file is visualized: <b>Image</b>, <b>Heatmap</b>,\n <b>Contour</b>, <b>Surface</b>, <b>Volume</b>, <b>Isosurface</b>, and \u2014 when a spatial-omics dataset is open \u2014\n <b>Spatial omics</b> and <b>Spatial omics 3D</b>.\n </p>\n <p>\n <b>Only the modes the data can actually support are offered</b>, so the list changes with what is loaded rather\n than showing entries that would come up blank:\n </p>\n <ul>\n <li>\n <b>Contour</b>, <b>Volume</b> and <b>Isosurface</b> need a grayscale image; the two 3D ones also need a\n z-stack.\n </li>\n <li>\n <b>Spatial omics</b> needs a published dataset, and <b>Spatial omics 3D</b> needs one whose coordinates carry\n a z.\n </li>\n <li>\n The pixel modes (<b>Image</b>, <b>Heatmap</b>, <b>Surface</b>) are withheld for a dataset that brings no\n tissue image \u2014 seqFISH records coordinates and no section, so there is nothing for them to draw.\n </li>\n </ul>\n <ul>\n <li>\n \"<b>Isosurface band</b>\" slider (Isosurface): sets the intensity band the surfaces are drawn for, mapped onto\n the volume's real intensity range.\n </li>\n <li>\n \"<b>3D camera</b>\" (Surface / Scatter 3D / Isosurface): zoom <i class=\"pi pi-search\"></i>, pan\n <i class=\"pi pi-arrows-alt\"></i>, orbit <i class=\"pi pi-globe\"></i>, turntable <i class=\"pi pi-sync\"></i>,\n and reset camera <i class=\"pi pi-home\"></i>; scroll to zoom the 3D scene.\n </li>\n </ul>\n\n <h3>Image manipulation tools</h3>\n <ul>\n <li>\n \"<b>Region Editor</b>\" <img src=\"assets/plotting/region-editor.svg\" width=\"15\" height=\"15\" />: opens the\n Region Editor as a floating, draggable dialog \u2014 a per-image table of every region (class, colour, area) with\n bulk select/label/colour/delete and GeoJSON import/export. It stays in sync with the image, so you can keep\n it open while drawing.\n </li>\n <li>\n \"<b>Intensity profile lines</b>\" <img src=\"assets/plotting/spline-chart.svg\" width=\"15\" height=\"15\" /> (<i\n >work in progress \u2014 not yet stable</i\n >): adds a coloured line ROI; a floating inset chart plots intensity along each line, updates live as you\n drag it, and re-samples at the current zoom resolution. Each added line gets its own colour; select a line\n and use the trash tool <i class=\"pi pi-trash\"></i> to remove it (and its trace).\n </li>\n <li>\n \"<b>Channels &amp; Histogram</b>\" <img src=\"assets/plotting/invert.svg\" width=\"15\" height=\"15\" />: opens a\n floating dialog for brightness/contrast, gamma, and per-channel display, plus the\n <b>colormap / LUT</b> controls (grayscale only) \u2014 a <b>reverse</b> toggle and a colormap dropdown (default:\n inverted greys).\n </li>\n <li>\n \"<b>Smoothen</b>\" <img src=\"assets/plotting/smooth.svg\" width=\"15\" height=\"15\" /> (Image view only): the\n image opens showing <b>raw pixels</b> (nearest-neighbour), so zooming in renders crisp blocks for pixel\n inspection. Toggle this on for bilinear smoothing; the icon turns white while smoothing is active.\n </li>\n </ul>\n\n <h3>Download &amp; view tools</h3>\n <ul>\n <li>\"<b>Download</b>\" <i class=\"pi pi-download\"></i>: download the current image as a PNG.</li>\n <li>\n \"<b>Fit to view</b>\" / autoscale <i class=\"pi pi-window-maximize\"></i>: resets the image to its original\n dimensions. In stack mode it also resets to single-image mode.\n </li>\n </ul>\n\n <h3>Zoom tools</h3>\n <ul>\n <li>\n \"<b>Zoom box</b>\" <img src=\"assets/plotting/zoom-box.svg\" width=\"15\" height=\"15\" /> (<kbd>b</kbd>): drag a\n box to select an area to zoom into.\n </li>\n <li>\"<b>Pan</b>\" <i class=\"pi pi-arrows-alt\"></i> (<kbd>p</kbd>): drag to pan the image.</li>\n <li>\"<b>Zoom in</b>\" <i class=\"pi pi-search-plus\"></i> (<kbd>+</kbd>): zoom in one step.</li>\n <li>\n \"<b>Zoom out</b>\" <i class=\"pi pi-search-minus\"></i> (<kbd>-</kbd>): zoom out one step, stopping at the\n original image dimensions.\n </li>\n </ul>\n\n <h3>Region tools</h3>\n <p>\n By default no tool is selected (all greyed-out); selecting a tool highlights it. The vertex-editing tools\n (polygon, add/delete vertex, move, B\u00E9zier conversions) are available in the <b>Image</b> view only. On a\n <b>donut</b> (a region with a hole), the inner-ring vertices are editable too \u2014 drag, add, and delete them just\n like the outer ring.\n </p>\n <ul>\n <li>\n \"<b>Selection</b>\" <i class=\"pi pi-arrow-up-right\"></i> (<kbd>s</kbd>): a neutral mode that turns off\n whatever drawing tool was active (wand, vertex eraser, rectangle, etc.). Use it to step out of editing\n without picking another tool.\n </li>\n <li>\n \"<b>Rectangular region</b>\" <i class=\"pi pi-stop\"></i> (<kbd>r</kbd>): click and drag to create a rectangular\n region.\n </li>\n <li>\n \"<b>Polyline</b>\" <img src=\"assets/plotting/polyline.svg\" width=\"14\" height=\"14\" /> (<kbd>l</kbd>): draw an\n open (non-closed) polyline by clicking and dragging. Polylines can be edited and exported to GeoJSON as a\n <code>LineString</code> feature.\n </li>\n <li>\n \"<b>Freeform region</b>\" <i class=\"pi pi-pencil\"></i> (<kbd>f</kbd>): click and drag to draw a closed\n freeform polygon.\n </li>\n <li>\n \"<b>Brush</b>\" <img src=\"assets/plotting/paintbrush.svg\" width=\"15\" height=\"15\" />: click and drag to paint a\n region with a circular brush \u2014 the painted stroke is the region. A <b>Size</b> slider (brush diameter, in\n image pixels) appears in the toolbar while the brush is active. Hold <kbd>Shift</kbd> while painting to\n <b>erase</b> (subtract) from a region instead. Painting over an existing region extends it; brushing across\n two regions merges them. Painting a closed ring (or shift-erasing an interior pocket) leaves the enclosed\n area empty \u2014 a <b>donut</b> (Image view).\n </li>\n <li>\n \"<b>Polygon</b>\" <img src=\"assets/plotting/polygon-vertices.svg\" width=\"15\" height=\"15\" />: click to place\n each vertex; click the first point to close the polygon.\n </li>\n <li>\n \"<b>Add vertex</b>\" <img src=\"assets/plotting/vertex-add.svg\" width=\"15\" height=\"15\" />: click an edge of the\n selected polygon to insert a new vertex.\n </li>\n <li>\n \"<b>Delete vertex</b>\" <img src=\"assets/plotting/vertex-delete.svg\" width=\"15\" height=\"15\" />: click a vertex\n of the selected polygon to remove it.\n </li>\n <li>\n <b>Moving a region</b> <img src=\"assets/plotting/region-move.svg\" width=\"15\" height=\"15\" />: with the Select\n tool, drag the selected region's body (or a rectangle's handles to resize it).\n </li>\n <li>\n <b>Convert to B\u00E9zier</b> <img src=\"assets/plotting/to-bezier.svg\" width=\"15\" height=\"15\" /> /\n <b>Convert to polygon</b> <img src=\"assets/plotting/to-polygon.svg\" width=\"15\" height=\"15\" /> (Image view):\n right-click the selected region and pick the conversion in the context menu to turn it into a smooth B\u00E9zier\n curve, or back into a straight-edged polygon.\n </li>\n <li>\n \"<b>Wand</b>\" <img src=\"assets/plotting/wand.svg\" width=\"15\" height=\"15\" /> (<kbd>w</kbd>): click to grow a\n region from pixels with similar values around the click point. Hold <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> for an\n exact-match flood fill; drag to keep extending the selection while moving; hold <kbd>Shift</kbd> while\n clicking or dragging on an existing region to <b>erase</b> pixels from it. Use the <b>Sens.</b> slider that\n appears to make the wand stricter (higher = smaller selection) or looser. Subsequent clicks keep extending\n the same region; clicking inside an existing region adopts it, and growing into another region merges them.\n </li>\n <li>\n \"<b>Vertex eraser</b>\" <i class=\"pi pi-eraser\"></i> (<kbd>e</kbd>): click or click-and-drag to remove polygon\n vertices that fall inside the cursor circle from any region, including a region's inner rings (donut holes).\n Use the <b>Radius</b> slider to set the eraser size in image pixels. Polygons reduced below 3 vertices (or\n polylines below 2) are deleted entirely; a hole reduced below 3 vertices is removed.\n </li>\n <li>\n \"<b>Undo</b>\" <img src=\"assets/plotting/undo.svg\" width=\"15\" height=\"15\" />\n (<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Z</kbd>): reverts the last region action (draw, delete, edit, wand/brush\n stroke, segmentation, \u2026). Up to the last\n <b>10</b> actions can be undone; the button greys out when there's nothing left to undo.\n </li>\n <li>\n \"<b>Redo</b>\"\n <img src=\"assets/plotting/redo.svg\" width=\"15\" height=\"15\" />\n (<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> or\n <kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Y</kbd>): re-applies the most recently undone action. Performing a new\n region action clears what can be redone.\n </li>\n <li>\n \"<b>Delete region</b>\" <i class=\"pi pi-trash\"></i> (<kbd>Del</kbd>): deletes the selected region. This can\n also be done from the Region Editor.\n </li>\n </ul>\n\n <h3>Region actions (right-click the image)</h3>\n <p>\n Right-click a region \u2014 or the current multi-selection \u2014 to open the \"<b>Selected region(s)</b>\" group at the\n top of the context menu. These act immediately on the selection (they're not drawing modes) and are fully\n undo/redo-able:\n </p>\n <ul>\n <li>\n \"<b>Select all regions</b>\" <i class=\"pi pi-check-square\"></i>: select every region on the image (also in the\n Region Editor toolbar).\n </li>\n <li>\n \"<b>Merge / group</b>\": combine 2+ selected regions into one. If the pieces don't touch, the result is a\n single multi-part region (sparse \"bits\").\n </li>\n <li>\"<b>Ungroup</b>\": split a multi-part region back into one region per piece.</li>\n <li>\n \"<b>Inverse</b>\": replace the selection with everything <i>outside</i> it \u2014 the image rectangle with the\n selected regions punched out as holes. Works at any image size.\n </li>\n <li>\n \"<b>Simplify</b>\": reduce a region's vertex count (Douglas\u2013Peucker) by an altitude threshold in pixels \u2014 pick\n <b>Light</b> (1&nbsp;px), <b>Medium</b> (3&nbsp;px), <b>Strong</b> (8&nbsp;px), or <b>Custom\u2026</b> for a\n slider. Applies to the exterior and any holes (and every part of a multi-part region).\n </li>\n <li>\n \"<b>Convert to B\u00E9zier</b>\" / \"<b>Convert to polygon</b>\" (Image view) and \"<b>Delete</b>\" also live in this\n group; the conversions are only here, not in the toolbar.\n </li>\n </ul>\n\n <h3>Segmentation &amp; detection tools</h3>\n <p>\n AI-assisted segmentation and detection that run entirely in the browser. The first run of any model downloads\n it (a progress toast is shown); it's then cached.\n </p>\n <p>\n Two shapes of tool: the <b>prompted</b> ones (Segment, Point prompts, Cellpose) act on rectangles you draw, and\n generated regions inherit the color of the rectangle they came from<span *ngIf=\"contributedTools.length\"\n >; the <b>no-prompt</b> ones ({{ contributedToolNames }}) sweep the whole current view, so there is nothing\n to draw first</span\n >.\n </p>\n <p>\n Each model dropdown <i class=\"pi pi-chevron-down\"></i> lists the available checkpoints with a\n <i class=\"pi pi-info-circle\"></i> beside each one \u2014 hover it for that model's size, speed, training domain and\n accuracy, so you can pick without leaving the toolbar.\n </p>\n <ul>\n <li>\n \"<b>Segment</b>\" <img src=\"assets/plotting/sam-box-prompt.svg\" width=\"15\" height=\"15\" /> (box prompt): draw\n one or more <b>Rectangular regions</b> around the objects of interest, then click Segment. Each rectangle is\n replaced by the SAM mask of the object inside it.\n </li>\n <li>\n \"<b>Point prompts</b>\" <img src=\"assets/plotting/sam-point-prompt.svg\" width=\"15\" height=\"15\" />: click\n directly on an object to segment it as a <b>new</b> region. Each click starts a fresh object \u2014 clicking\n another object won't grow the previous one. <kbd>Shift</kbd>/<kbd>Alt</kbd>-click adds an\n <i>exclude</i> point that refines the current object; <kbd>Esc</kbd> undoes it, <kbd>Enter</kbd> commits it.\n </li>\n <li>\n \"<b>Model picker</b>\" <i class=\"pi pi-chevron-down\"></i>: the dropdown attached to the Segment button chooses\n the SAM model \u2014 <b>micro-sam ViT-T</b> (default; small, fast) or <b>micro-sam ViT-B</b> (larger, highest\n accuracy) for light microscopy, or <b>patho-sam ViT-B</b> for histopathology (H&amp;E / stained tissue). All\n are promptable Segment Anything models finetuned for biomedical images; they differ in the\n <i>image encoder</i> (the heavy once-per-image step) and the training domain \u2014 box and point prompting behave\n the same. The choice applies to <i>both</i> the box and point tools, which share one loaded model and image\n encoding, so the first run after switching models (or images) downloads and encodes once for both.\n </li>\n <li>\n \"<b>Cellpose</b>\" <img src=\"assets/plotting/cells.svg\" width=\"15\" height=\"15\" />: draw rectangles, then click\n Cellpose to automatically segment every cell inside each rectangle (client-side cellpose-SAM) \u2014 one region\n per detected cell. No clicking on individual cells needed.\n </li>\n <!-- Contributed tools describe themselves. This library ships none, so an\n open build's help lists only the prompted tools above \u2014 the text no\n longer promises buttons that are not there. -->\n <ng-container *ngFor=\"let tool of contributedTools\">\n <li *ngIf=\"tool.help\" [innerHTML]=\"tool.help.body\"></li>\n </ng-container>\n <ng-container *ngFor=\"let tool of dialogTools\">\n <li *ngIf=\"tool.help\" [innerHTML]=\"tool.help.body\"></li>\n </ng-container>\n </ul>\n\n <h3>Spatial omics</h3>\n <p>\n In <b>Spatial omics</b> mode each observation \u2014 a cell or a Visium spot \u2014 is drawn at its own coordinates, over\n the tissue image where the dataset registers onto one. The\n <img src=\"assets/plotting/spatial-omics.svg\" width=\"14\" height=\"14\" alt=\"Spatial omics\" />\n panel colours them by a <b>column</b> (a cell type, a cluster) or by a <b>gene</b>, and the two are exclusive\n so what drives the colours is never ambiguous.\n </p>\n <ul>\n <li>\n \"<b>Select from ROIs</b>\": turns the regions you have drawn into a selection of observations. Everything else\n then follows that selection. In 3D, with classes picked in the Region Editor's class tree (or their surfaces\n clicked), it selects the observations inside those classes' volumes.\n </li>\n <li>\"<b>Point size</b>\" / \"<b>Opacity</b>\": how the markers are drawn, not what is selected.</li>\n </ul>\n\n <h3>Distribution charts</h3>\n <p>\n The <b>Distribution</b> section of the spatial-omics panel charts whatever the map is coloured by, so the two\n can never disagree. Each tab answers a different question, and the <i class=\"pi pi-question-circle\"></i> beside\n them explains the one on screen \u2014 including what it must <em>not</em> be read for.\n </p>\n <ul>\n <li>\n \"<b>Counts</b>\": how many observations per category. A category is a label, not a magnitude, so a frequency\n is the only distribution it has.\n </li>\n <li>\n \"<b>Histogram</b>\" / \"<b>Violin</b>\" / \"<b>Box</b>\": how a continuous value is distributed \u2014 over everything,\n or split by a grouping column.\n </li>\n <li>\n \"<b>Heatmap</b>\": mean expression of the genes you pick within each group. Z-scored per gene by default, so a\n colour means \"above or below this gene's own average\" rather than \"highly expressed\".\n </li>\n <li>\n \"<b>Embedding</b>\": a UMAP, PCA or t-SNE over the same cells. <b>Lasso it to select on the map</b> \u2014 both\n views read the same selection. Only PCA labels its axes with a percentage, because only its axes explain a\n measurable share of the variance; UMAP and t-SNE coordinates are arbitrary, so distance between distant\n clusters should not be read.\n </li>\n <li>\n \"<b>Detach</b>\": moves the charts into their own window so they can sit beside the map instead of inside the\n panel. Switching tabs keeps the window where it is.\n </li>\n </ul>\n\n <h3>Help</h3>\n <p>\"<b>Help</b>\" <i class=\"pi pi-question-circle\"></i>: opens this dialog.</p>\n\n <h3>Keyboard shortcuts</h3>\n <table class=\"shortcuts-table\">\n <tr>\n <td><kbd>+</kbd> / <kbd>=</kbd></td>\n <td>Zoom in</td>\n </tr>\n <tr>\n <td><kbd>-</kbd></td>\n <td>Zoom out</td>\n </tr>\n <tr>\n <td><kbd>b</kbd></td>\n <td>Toggle zoom box mode</td>\n </tr>\n <tr>\n <td><kbd>p</kbd></td>\n <td>Toggle pan mode</td>\n </tr>\n <tr>\n <td><kbd>f</kbd></td>\n <td>Toggle freeform region drawing</td>\n </tr>\n <tr>\n <td><kbd>l</kbd></td>\n <td>Toggle polyline drawing</td>\n </tr>\n <tr>\n <td><kbd>r</kbd></td>\n <td>Toggle rectangular region drawing</td>\n </tr>\n <tr>\n <td><kbd>w</kbd></td>\n <td>Toggle wand tool</td>\n </tr>\n <tr>\n <td>Wand + <kbd>Shift</kbd></td>\n <td>Erase pixels from the active region</td>\n </tr>\n <tr>\n <td>Wand + <kbd>Ctrl</kbd>/<kbd>Cmd</kbd></td>\n <td>Exact-match flood fill (no smoothing)</td>\n </tr>\n <tr>\n <td><kbd>e</kbd></td>\n <td>Toggle vertex eraser</td>\n </tr>\n <tr>\n <td><kbd>s</kbd></td>\n <td>Selection mode (deactivates drawing tools)</td>\n </tr>\n <tr>\n <td>Point prompt + <kbd>Shift</kbd>/<kbd>Alt</kbd></td>\n <td>Add an exclude point (refine the current object)</td>\n </tr>\n <tr>\n <td><kbd>Enter</kbd></td>\n <td>Commit the current SAM point object</td>\n </tr>\n <tr>\n <td><kbd>Esc</kbd></td>\n <td>Undo the current SAM point object</td>\n </tr>\n <tr>\n <td><kbd>Del</kbd> / <kbd>\u232B</kbd></td>\n <td>Delete selected region</td>\n </tr>\n <tr>\n <td><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Z</kbd></td>\n <td>Undo the last region action (up to 10)</td>\n </tr>\n <tr>\n <td><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> / <kbd>Ctrl</kbd>+<kbd>Y</kbd></td>\n <td>Redo the last undone region action</td>\n </tr>\n </table>\n\n <ng-template pTemplate=\"footer\">\n <p-button icon=\"pi pi-check\" (click)=\"visibleChange.emit(false)\" label=\"Ok\" styleClass=\"p-button-text\">\n </p-button>\n </ng-template>\n</p-dialog>\n", styles: ["kbd{background:#e8e8e8;border:1px solid #bbb;border-radius:3px;padding:1px 5px;font-family:monospace;font-size:.85em}.shortcuts-table{border-collapse:collapse}.shortcuts-table td{padding:.25rem .75rem .25rem 0;vertical-align:middle}.shortcuts-table td:first-child{white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i5.Dialog, selector: "p-dialog", inputs: ["header", "draggable", "resizable", "positionLeft", "positionTop", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "responsive", "appendTo", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "breakpoint", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "visible", "style", "position"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
12906
12996
  }
12907
12997
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ToolbarHelpDialogComponent, decorators: [{
12908
12998
  type: Component,
12909
- args: [{ selector: 'toolbar-help-dialog', standalone: true, imports: [CommonModule, SharedModule, ButtonModule, DialogModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<p-dialog\n header=\"Plotting help\"\n [visible]=\"visible\"\n (visibleChange)=\"visibleChange.emit($event)\"\n [modal]=\"true\"\n [style]=\"{ width: '50vw' }\"\n [maximizable]=\"true\"\n [draggable]=\"false\"\n [resizable]=\"false\"\n>\n <p>\n Click a file in the left-hand tree to display it. By default it opens in the <b>Image</b> view \u2014 a natively\n tiled, deeply zoomable raster (OpenSeadragon) with a navigator minimap and a physical-units scale bar.\n <b>Click the image to zoom</b> toward the clicked point; scroll to zoom and drag to pan. When a stack is open,\n a slice slider (Image view) or image-mode toggle (other views) appears. Regions can also be edited in the\n <b>Region Editor</b>, opened with the\n <img src=\"assets/plotting/region-editor.svg\" width=\"14\" height=\"14\" alt=\"Region Editor\" />\n toolbar button as a floating dialog. Drag the <i class=\"pi pi-bars\"></i> handle to detach this toolbar into a\n floating window.\n </p>\n\n <h3>Plot-type dropdown</h3>\n <p>\n The <b>plot-type dropdown</b> switches how the file is visualized: <b>Image</b>, <b>Heatmap</b>,\n <b>Contour</b>, <b>Surface</b>, <b>Volume</b>, <b>Isosurface</b>, and \u2014 when a spatial-omics dataset is open \u2014\n <b>Spatial omics</b> and <b>Spatial omics 3D</b>.\n </p>\n <p>\n <b>Only the modes the data can actually support are offered</b>, so the list changes with what is loaded rather\n than showing entries that would come up blank:\n </p>\n <ul>\n <li>\n <b>Contour</b>, <b>Volume</b> and <b>Isosurface</b> need a grayscale image; the two 3D ones also need a\n z-stack.\n </li>\n <li>\n <b>Spatial omics</b> needs a published dataset, and <b>Spatial omics 3D</b> needs one whose coordinates carry\n a z.\n </li>\n <li>\n The pixel modes (<b>Image</b>, <b>Heatmap</b>, <b>Surface</b>) are withheld for a dataset that brings no\n tissue image \u2014 seqFISH records coordinates and no section, so there is nothing for them to draw.\n </li>\n </ul>\n <ul>\n <li>\n \"<b>Isosurface band</b>\" slider (Isosurface): sets the intensity band the surfaces are drawn for, mapped onto\n the volume's real intensity range.\n </li>\n <li>\n \"<b>3D camera</b>\" (Surface / Scatter 3D / Isosurface): zoom <i class=\"pi pi-search\"></i>, pan\n <i class=\"pi pi-arrows-alt\"></i>, orbit <i class=\"pi pi-globe\"></i>, turntable <i class=\"pi pi-sync\"></i>,\n and reset camera <i class=\"pi pi-home\"></i>; scroll to zoom the 3D scene.\n </li>\n </ul>\n\n <h3>Image manipulation tools</h3>\n <ul>\n <li>\n \"<b>Region Editor</b>\" <img src=\"assets/plotting/region-editor.svg\" width=\"15\" height=\"15\" />: opens the\n Region Editor as a floating, draggable dialog \u2014 a per-image table of every region (class, colour, area) with\n bulk select/label/colour/delete and GeoJSON import/export. It stays in sync with the image, so you can keep\n it open while drawing.\n </li>\n <li>\n \"<b>Intensity profile lines</b>\" <img src=\"assets/plotting/spline-chart.svg\" width=\"15\" height=\"15\" /> (<i\n >work in progress \u2014 not yet stable</i\n >): adds a coloured line ROI; a floating inset chart plots intensity along each line, updates live as you\n drag it, and re-samples at the current zoom resolution. Each added line gets its own colour; select a line\n and use the trash tool <i class=\"pi pi-trash\"></i> to remove it (and its trace).\n </li>\n <li>\n \"<b>Channels &amp; Histogram</b>\" <img src=\"assets/plotting/invert.svg\" width=\"15\" height=\"15\" />: opens a\n floating dialog for brightness/contrast, gamma, and per-channel display, plus the\n <b>colormap / LUT</b> controls (grayscale only) \u2014 a <b>reverse</b> toggle and a colormap dropdown (default:\n inverted greys).\n </li>\n <li>\n \"<b>Smoothen</b>\" <img src=\"assets/plotting/smooth.svg\" width=\"15\" height=\"15\" /> (Image view only): the\n image opens showing <b>raw pixels</b> (nearest-neighbour), so zooming in renders crisp blocks for pixel\n inspection. Toggle this on for bilinear smoothing; the icon turns white while smoothing is active.\n </li>\n </ul>\n\n <h3>Download &amp; view tools</h3>\n <ul>\n <li>\"<b>Download</b>\" <i class=\"pi pi-download\"></i>: download the current image as a PNG.</li>\n <li>\n \"<b>Fit to view</b>\" / autoscale <i class=\"pi pi-window-maximize\"></i>: resets the image to its original\n dimensions. In stack mode it also resets to single-image mode.\n </li>\n </ul>\n\n <h3>Zoom tools</h3>\n <ul>\n <li>\n \"<b>Zoom box</b>\" <img src=\"assets/plotting/zoom-box.svg\" width=\"15\" height=\"15\" /> (<kbd>b</kbd>): drag a\n box to select an area to zoom into.\n </li>\n <li>\"<b>Pan</b>\" <i class=\"pi pi-arrows-alt\"></i> (<kbd>p</kbd>): drag to pan the image.</li>\n <li>\"<b>Zoom in</b>\" <i class=\"pi pi-search-plus\"></i> (<kbd>+</kbd>): zoom in one step.</li>\n <li>\n \"<b>Zoom out</b>\" <i class=\"pi pi-search-minus\"></i> (<kbd>-</kbd>): zoom out one step, stopping at the\n original image dimensions.\n </li>\n </ul>\n\n <h3>Region tools</h3>\n <p>\n By default no tool is selected (all greyed-out); selecting a tool highlights it. The vertex-editing tools\n (polygon, add/delete vertex, move, B\u00E9zier conversions) are available in the <b>Image</b> view only. On a\n <b>donut</b> (a region with a hole), the inner-ring vertices are editable too \u2014 drag, add, and delete them just\n like the outer ring.\n </p>\n <ul>\n <li>\n \"<b>Selection</b>\" <i class=\"pi pi-arrow-up-right\"></i> (<kbd>s</kbd>): a neutral mode that turns off\n whatever drawing tool was active (wand, vertex eraser, rectangle, etc.). Use it to step out of editing\n without picking another tool.\n </li>\n <li>\n \"<b>Rectangular region</b>\" <i class=\"pi pi-stop\"></i> (<kbd>r</kbd>): click and drag to create a rectangular\n region.\n </li>\n <li>\n \"<b>Polyline</b>\" <img src=\"assets/plotting/polyline.svg\" width=\"14\" height=\"14\" /> (<kbd>l</kbd>): draw an\n open (non-closed) polyline by clicking and dragging. Polylines can be edited and exported to GeoJSON as a\n <code>LineString</code> feature.\n </li>\n <li>\n \"<b>Freeform region</b>\" <i class=\"pi pi-pencil\"></i> (<kbd>f</kbd>): click and drag to draw a closed\n freeform polygon.\n </li>\n <li>\n \"<b>Brush</b>\" <img src=\"assets/plotting/paintbrush.svg\" width=\"15\" height=\"15\" />: click and drag to paint a\n region with a circular brush \u2014 the painted stroke is the region. A <b>Size</b> slider (brush diameter, in\n image pixels) appears in the toolbar while the brush is active. Hold <kbd>Shift</kbd> while painting to\n <b>erase</b> (subtract) from a region instead. Painting over an existing region extends it; brushing across\n two regions merges them. Painting a closed ring (or shift-erasing an interior pocket) leaves the enclosed\n area empty \u2014 a <b>donut</b> (Image view).\n </li>\n <li>\n \"<b>Polygon</b>\" <img src=\"assets/plotting/polygon-vertices.svg\" width=\"15\" height=\"15\" />: click to place\n each vertex; click the first point to close the polygon.\n </li>\n <li>\n \"<b>Add vertex</b>\" <img src=\"assets/plotting/vertex-add.svg\" width=\"15\" height=\"15\" />: click an edge of the\n selected polygon to insert a new vertex.\n </li>\n <li>\n \"<b>Delete vertex</b>\" <img src=\"assets/plotting/vertex-delete.svg\" width=\"15\" height=\"15\" />: click a vertex\n of the selected polygon to remove it.\n </li>\n <li>\n <b>Moving a region</b> <img src=\"assets/plotting/region-move.svg\" width=\"15\" height=\"15\" />: with the Select\n tool, drag the selected region's body (or a rectangle's handles to resize it).\n </li>\n <li>\n <b>Convert to B\u00E9zier</b> <img src=\"assets/plotting/to-bezier.svg\" width=\"15\" height=\"15\" /> /\n <b>Convert to polygon</b> <img src=\"assets/plotting/to-polygon.svg\" width=\"15\" height=\"15\" /> (Image view):\n right-click the selected region and pick the conversion in the context menu to turn it into a smooth B\u00E9zier\n curve, or back into a straight-edged polygon.\n </li>\n <li>\n \"<b>Wand</b>\" <img src=\"assets/plotting/wand.svg\" width=\"15\" height=\"15\" /> (<kbd>w</kbd>): click to grow a\n region from pixels with similar values around the click point. Hold <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> for an\n exact-match flood fill; drag to keep extending the selection while moving; hold <kbd>Shift</kbd> while\n clicking or dragging on an existing region to <b>erase</b> pixels from it. Use the <b>Sens.</b> slider that\n appears to make the wand stricter (higher = smaller selection) or looser. Subsequent clicks keep extending\n the same region; clicking inside an existing region adopts it, and growing into another region merges them.\n </li>\n <li>\n \"<b>Vertex eraser</b>\" <i class=\"pi pi-eraser\"></i> (<kbd>e</kbd>): click or click-and-drag to remove polygon\n vertices that fall inside the cursor circle from any region, including a region's inner rings (donut holes).\n Use the <b>Radius</b> slider to set the eraser size in image pixels. Polygons reduced below 3 vertices (or\n polylines below 2) are deleted entirely; a hole reduced below 3 vertices is removed.\n </li>\n <li>\n \"<b>Undo</b>\" <img src=\"assets/plotting/undo.svg\" width=\"15\" height=\"15\" />\n (<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Z</kbd>): reverts the last region action (draw, delete, edit, wand/brush\n stroke, segmentation, \u2026). Up to the last\n <b>10</b> actions can be undone; the button greys out when there's nothing left to undo.\n </li>\n <li>\n \"<b>Redo</b>\"\n <img src=\"assets/plotting/redo.svg\" width=\"15\" height=\"15\" />\n (<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> or\n <kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Y</kbd>): re-applies the most recently undone action. Performing a new\n region action clears what can be redone.\n </li>\n <li>\n \"<b>Delete region</b>\" <i class=\"pi pi-trash\"></i> (<kbd>Del</kbd>): deletes the selected region. This can\n also be done from the Region Editor.\n </li>\n </ul>\n\n <h3>Region actions (right-click the image)</h3>\n <p>\n Right-click a region \u2014 or the current multi-selection \u2014 to open the \"<b>Selected region(s)</b>\" group at the\n top of the context menu. These act immediately on the selection (they're not drawing modes) and are fully\n undo/redo-able:\n </p>\n <ul>\n <li>\n \"<b>Select all regions</b>\" <i class=\"pi pi-check-square\"></i>: select every region on the image (also in the\n Region Editor toolbar).\n </li>\n <li>\n \"<b>Merge / group</b>\": combine 2+ selected regions into one. If the pieces don't touch, the result is a\n single multi-part region (sparse \"bits\").\n </li>\n <li>\"<b>Ungroup</b>\": split a multi-part region back into one region per piece.</li>\n <li>\n \"<b>Inverse</b>\": replace the selection with everything <i>outside</i> it \u2014 the image rectangle with the\n selected regions punched out as holes. Works at any image size.\n </li>\n <li>\n \"<b>Simplify</b>\": reduce a region's vertex count (Douglas\u2013Peucker) by an altitude threshold in pixels \u2014 pick\n <b>Light</b> (1&nbsp;px), <b>Medium</b> (3&nbsp;px), <b>Strong</b> (8&nbsp;px), or <b>Custom\u2026</b> for a\n slider. Applies to the exterior and any holes (and every part of a multi-part region).\n </li>\n <li>\n \"<b>Convert to B\u00E9zier</b>\" / \"<b>Convert to polygon</b>\" (Image view) and \"<b>Delete</b>\" also live in this\n group; the conversions are only here, not in the toolbar.\n </li>\n </ul>\n\n <h3>Segmentation &amp; detection tools</h3>\n <p>\n AI-assisted segmentation and detection that run entirely in the browser. The first run of any model downloads\n it (a progress toast is shown); it's then cached.\n </p>\n <p>\n Two shapes of tool: the <b>prompted</b> ones (Segment, Point prompts, Cellpose) act on rectangles you draw, and\n generated regions inherit the color of the rectangle they came from<span *ngIf=\"contributedTools.length\"\n >; the <b>no-prompt</b> ones ({{ contributedToolNames }}) sweep the whole current view, so there is nothing\n to draw first</span\n >.\n </p>\n <p>\n Each model dropdown <i class=\"pi pi-chevron-down\"></i> lists the available checkpoints with a\n <i class=\"pi pi-info-circle\"></i> beside each one \u2014 hover it for that model's size, speed, training domain and\n accuracy, so you can pick without leaving the toolbar.\n </p>\n <ul>\n <li>\n \"<b>Segment</b>\" <img src=\"assets/plotting/sam-box-prompt.svg\" width=\"15\" height=\"15\" /> (box prompt): draw\n one or more <b>Rectangular regions</b> around the objects of interest, then click Segment. Each rectangle is\n replaced by the SAM mask of the object inside it.\n </li>\n <li>\n \"<b>Point prompts</b>\" <img src=\"assets/plotting/sam-point-prompt.svg\" width=\"15\" height=\"15\" />: click\n directly on an object to segment it as a <b>new</b> region. Each click starts a fresh object \u2014 clicking\n another object won't grow the previous one. <kbd>Shift</kbd>/<kbd>Alt</kbd>-click adds an\n <i>exclude</i> point that refines the current object; <kbd>Esc</kbd> undoes it, <kbd>Enter</kbd> commits it.\n </li>\n <li>\n \"<b>Model picker</b>\" <i class=\"pi pi-chevron-down\"></i>: the dropdown attached to the Segment button chooses\n the SAM model \u2014 <b>micro-sam ViT-T</b> (default; small, fast) or <b>micro-sam ViT-B</b> (larger, highest\n accuracy) for light microscopy, or <b>patho-sam ViT-B</b> for histopathology (H&amp;E / stained tissue). All\n are promptable Segment Anything models finetuned for biomedical images; they differ in the\n <i>image encoder</i> (the heavy once-per-image step) and the training domain \u2014 box and point prompting behave\n the same. The choice applies to <i>both</i> the box and point tools, which share one loaded model and image\n encoding, so the first run after switching models (or images) downloads and encodes once for both.\n </li>\n <li>\n \"<b>Cellpose</b>\" <img src=\"assets/plotting/cells.svg\" width=\"15\" height=\"15\" />: draw rectangles, then click\n Cellpose to automatically segment every cell inside each rectangle (client-side cellpose-SAM) \u2014 one region\n per detected cell. No clicking on individual cells needed.\n </li>\n <!-- Contributed tools describe themselves. This library ships none, so an\n open build's help lists only the prompted tools above \u2014 the text no\n longer promises buttons that are not there. -->\n <ng-container *ngFor=\"let tool of contributedTools\">\n <li *ngIf=\"tool.help\" [innerHTML]=\"tool.help.body\"></li>\n </ng-container>\n <ng-container *ngFor=\"let tool of dialogTools\">\n <li *ngIf=\"tool.help\" [innerHTML]=\"tool.help.body\"></li>\n </ng-container>\n </ul>\n\n <h3>Spatial omics</h3>\n <p>\n In <b>Spatial omics</b> mode each observation \u2014 a cell or a Visium spot \u2014 is drawn at its own coordinates, over\n the tissue image where the dataset registers onto one. The\n <img src=\"assets/plotting/spatial-omics.svg\" width=\"14\" height=\"14\" alt=\"Spatial omics\" />\n panel colours them by a <b>column</b> (a cell type, a cluster) or by a <b>gene</b>, and the two are exclusive\n so what drives the colours is never ambiguous.\n </p>\n <ul>\n <li>\n \"<b>Select from ROIs</b>\": turns the regions you have drawn into a selection of observations. Everything else\n then follows that selection.\n </li>\n <li>\"<b>Point size</b>\" / \"<b>Opacity</b>\": how the markers are drawn, not what is selected.</li>\n </ul>\n\n <h3>Distribution charts</h3>\n <p>\n The <b>Distribution</b> section of the spatial-omics panel charts whatever the map is coloured by, so the two\n can never disagree. Each tab answers a different question, and the <i class=\"pi pi-question-circle\"></i> beside\n them explains the one on screen \u2014 including what it must <em>not</em> be read for.\n </p>\n <ul>\n <li>\n \"<b>Counts</b>\": how many observations per category. A category is a label, not a magnitude, so a frequency\n is the only distribution it has.\n </li>\n <li>\n \"<b>Histogram</b>\" / \"<b>Violin</b>\" / \"<b>Box</b>\": how a continuous value is distributed \u2014 over everything,\n or split by a grouping column.\n </li>\n <li>\n \"<b>Heatmap</b>\": mean expression of the genes you pick within each group. Z-scored per gene by default, so a\n colour means \"above or below this gene's own average\" rather than \"highly expressed\".\n </li>\n <li>\n \"<b>Embedding</b>\": a UMAP, PCA or t-SNE over the same cells. <b>Lasso it to select on the map</b> \u2014 both\n views read the same selection. Only PCA labels its axes with a percentage, because only its axes explain a\n measurable share of the variance; UMAP and t-SNE coordinates are arbitrary, so distance between distant\n clusters should not be read.\n </li>\n <li>\n \"<b>Detach</b>\": moves the charts into their own window so they can sit beside the map instead of inside the\n panel. Switching tabs keeps the window where it is.\n </li>\n </ul>\n\n <h3>Help</h3>\n <p>\"<b>Help</b>\" <i class=\"pi pi-question-circle\"></i>: opens this dialog.</p>\n\n <h3>Keyboard shortcuts</h3>\n <table class=\"shortcuts-table\">\n <tr>\n <td><kbd>+</kbd> / <kbd>=</kbd></td>\n <td>Zoom in</td>\n </tr>\n <tr>\n <td><kbd>-</kbd></td>\n <td>Zoom out</td>\n </tr>\n <tr>\n <td><kbd>b</kbd></td>\n <td>Toggle zoom box mode</td>\n </tr>\n <tr>\n <td><kbd>p</kbd></td>\n <td>Toggle pan mode</td>\n </tr>\n <tr>\n <td><kbd>f</kbd></td>\n <td>Toggle freeform region drawing</td>\n </tr>\n <tr>\n <td><kbd>l</kbd></td>\n <td>Toggle polyline drawing</td>\n </tr>\n <tr>\n <td><kbd>r</kbd></td>\n <td>Toggle rectangular region drawing</td>\n </tr>\n <tr>\n <td><kbd>w</kbd></td>\n <td>Toggle wand tool</td>\n </tr>\n <tr>\n <td>Wand + <kbd>Shift</kbd></td>\n <td>Erase pixels from the active region</td>\n </tr>\n <tr>\n <td>Wand + <kbd>Ctrl</kbd>/<kbd>Cmd</kbd></td>\n <td>Exact-match flood fill (no smoothing)</td>\n </tr>\n <tr>\n <td><kbd>e</kbd></td>\n <td>Toggle vertex eraser</td>\n </tr>\n <tr>\n <td><kbd>s</kbd></td>\n <td>Selection mode (deactivates drawing tools)</td>\n </tr>\n <tr>\n <td>Point prompt + <kbd>Shift</kbd>/<kbd>Alt</kbd></td>\n <td>Add an exclude point (refine the current object)</td>\n </tr>\n <tr>\n <td><kbd>Enter</kbd></td>\n <td>Commit the current SAM point object</td>\n </tr>\n <tr>\n <td><kbd>Esc</kbd></td>\n <td>Undo the current SAM point object</td>\n </tr>\n <tr>\n <td><kbd>Del</kbd> / <kbd>\u232B</kbd></td>\n <td>Delete selected region</td>\n </tr>\n <tr>\n <td><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Z</kbd></td>\n <td>Undo the last region action (up to 10)</td>\n </tr>\n <tr>\n <td><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> / <kbd>Ctrl</kbd>+<kbd>Y</kbd></td>\n <td>Redo the last undone region action</td>\n </tr>\n </table>\n\n <ng-template pTemplate=\"footer\">\n <p-button icon=\"pi pi-check\" (click)=\"visibleChange.emit(false)\" label=\"Ok\" styleClass=\"p-button-text\">\n </p-button>\n </ng-template>\n</p-dialog>\n", styles: ["kbd{background:#e8e8e8;border:1px solid #bbb;border-radius:3px;padding:1px 5px;font-family:monospace;font-size:.85em}.shortcuts-table{border-collapse:collapse}.shortcuts-table td{padding:.25rem .75rem .25rem 0;vertical-align:middle}.shortcuts-table td:first-child{white-space:nowrap}\n"] }]
12999
+ args: [{ selector: 'toolbar-help-dialog', standalone: true, imports: [CommonModule, SharedModule, ButtonModule, DialogModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<p-dialog\n header=\"Plotting help\"\n [visible]=\"visible\"\n (visibleChange)=\"visibleChange.emit($event)\"\n [modal]=\"true\"\n [style]=\"{ width: '50vw' }\"\n [maximizable]=\"true\"\n [draggable]=\"false\"\n [resizable]=\"false\"\n>\n <p>\n Click a file in the left-hand tree to display it. By default it opens in the <b>Image</b> view \u2014 a natively\n tiled, deeply zoomable raster (OpenSeadragon) with a navigator minimap and a physical-units scale bar.\n <b>Click the image to zoom</b> toward the clicked point; scroll to zoom and drag to pan. When a stack is open,\n a slice slider (Image view) or image-mode toggle (other views) appears. Regions can also be edited in the\n <b>Region Editor</b>, opened with the\n <img src=\"assets/plotting/region-editor.svg\" width=\"14\" height=\"14\" alt=\"Region Editor\" />\n toolbar button as a floating dialog. Drag the <i class=\"pi pi-bars\"></i> handle to detach this toolbar into a\n floating window.\n </p>\n\n <h3>Plot-type dropdown</h3>\n <p>\n The <b>plot-type dropdown</b> switches how the file is visualized: <b>Image</b>, <b>Heatmap</b>,\n <b>Contour</b>, <b>Surface</b>, <b>Volume</b>, <b>Isosurface</b>, and \u2014 when a spatial-omics dataset is open \u2014\n <b>Spatial omics</b> and <b>Spatial omics 3D</b>.\n </p>\n <p>\n <b>Only the modes the data can actually support are offered</b>, so the list changes with what is loaded rather\n than showing entries that would come up blank:\n </p>\n <ul>\n <li>\n <b>Contour</b>, <b>Volume</b> and <b>Isosurface</b> need a grayscale image; the two 3D ones also need a\n z-stack.\n </li>\n <li>\n <b>Spatial omics</b> needs a published dataset, and <b>Spatial omics 3D</b> needs one whose coordinates carry\n a z.\n </li>\n <li>\n The pixel modes (<b>Image</b>, <b>Heatmap</b>, <b>Surface</b>) are withheld for a dataset that brings no\n tissue image \u2014 seqFISH records coordinates and no section, so there is nothing for them to draw.\n </li>\n </ul>\n <ul>\n <li>\n \"<b>Isosurface band</b>\" slider (Isosurface): sets the intensity band the surfaces are drawn for, mapped onto\n the volume's real intensity range.\n </li>\n <li>\n \"<b>3D camera</b>\" (Surface / Scatter 3D / Isosurface): zoom <i class=\"pi pi-search\"></i>, pan\n <i class=\"pi pi-arrows-alt\"></i>, orbit <i class=\"pi pi-globe\"></i>, turntable <i class=\"pi pi-sync\"></i>,\n and reset camera <i class=\"pi pi-home\"></i>; scroll to zoom the 3D scene.\n </li>\n </ul>\n\n <h3>Image manipulation tools</h3>\n <ul>\n <li>\n \"<b>Region Editor</b>\" <img src=\"assets/plotting/region-editor.svg\" width=\"15\" height=\"15\" />: opens the\n Region Editor as a floating, draggable dialog \u2014 a per-image table of every region (class, colour, area) with\n bulk select/label/colour/delete and GeoJSON import/export. It stays in sync with the image, so you can keep\n it open while drawing.\n </li>\n <li>\n \"<b>Intensity profile lines</b>\" <img src=\"assets/plotting/spline-chart.svg\" width=\"15\" height=\"15\" /> (<i\n >work in progress \u2014 not yet stable</i\n >): adds a coloured line ROI; a floating inset chart plots intensity along each line, updates live as you\n drag it, and re-samples at the current zoom resolution. Each added line gets its own colour; select a line\n and use the trash tool <i class=\"pi pi-trash\"></i> to remove it (and its trace).\n </li>\n <li>\n \"<b>Channels &amp; Histogram</b>\" <img src=\"assets/plotting/invert.svg\" width=\"15\" height=\"15\" />: opens a\n floating dialog for brightness/contrast, gamma, and per-channel display, plus the\n <b>colormap / LUT</b> controls (grayscale only) \u2014 a <b>reverse</b> toggle and a colormap dropdown (default:\n inverted greys).\n </li>\n <li>\n \"<b>Smoothen</b>\" <img src=\"assets/plotting/smooth.svg\" width=\"15\" height=\"15\" /> (Image view only): the\n image opens showing <b>raw pixels</b> (nearest-neighbour), so zooming in renders crisp blocks for pixel\n inspection. Toggle this on for bilinear smoothing; the icon turns white while smoothing is active.\n </li>\n </ul>\n\n <h3>Download &amp; view tools</h3>\n <ul>\n <li>\"<b>Download</b>\" <i class=\"pi pi-download\"></i>: download the current image as a PNG.</li>\n <li>\n \"<b>Fit to view</b>\" / autoscale <i class=\"pi pi-window-maximize\"></i>: resets the image to its original\n dimensions. In stack mode it also resets to single-image mode.\n </li>\n </ul>\n\n <h3>Zoom tools</h3>\n <ul>\n <li>\n \"<b>Zoom box</b>\" <img src=\"assets/plotting/zoom-box.svg\" width=\"15\" height=\"15\" /> (<kbd>b</kbd>): drag a\n box to select an area to zoom into.\n </li>\n <li>\"<b>Pan</b>\" <i class=\"pi pi-arrows-alt\"></i> (<kbd>p</kbd>): drag to pan the image.</li>\n <li>\"<b>Zoom in</b>\" <i class=\"pi pi-search-plus\"></i> (<kbd>+</kbd>): zoom in one step.</li>\n <li>\n \"<b>Zoom out</b>\" <i class=\"pi pi-search-minus\"></i> (<kbd>-</kbd>): zoom out one step, stopping at the\n original image dimensions.\n </li>\n </ul>\n\n <h3>Region tools</h3>\n <p>\n By default no tool is selected (all greyed-out); selecting a tool highlights it. The vertex-editing tools\n (polygon, add/delete vertex, move, B\u00E9zier conversions) are available in the <b>Image</b> view only. On a\n <b>donut</b> (a region with a hole), the inner-ring vertices are editable too \u2014 drag, add, and delete them just\n like the outer ring.\n </p>\n <ul>\n <li>\n \"<b>Selection</b>\" <i class=\"pi pi-arrow-up-right\"></i> (<kbd>s</kbd>): a neutral mode that turns off\n whatever drawing tool was active (wand, vertex eraser, rectangle, etc.). Use it to step out of editing\n without picking another tool.\n </li>\n <li>\n \"<b>Rectangular region</b>\" <i class=\"pi pi-stop\"></i> (<kbd>r</kbd>): click and drag to create a rectangular\n region.\n </li>\n <li>\n \"<b>Polyline</b>\" <img src=\"assets/plotting/polyline.svg\" width=\"14\" height=\"14\" /> (<kbd>l</kbd>): draw an\n open (non-closed) polyline by clicking and dragging. Polylines can be edited and exported to GeoJSON as a\n <code>LineString</code> feature.\n </li>\n <li>\n \"<b>Freeform region</b>\" <i class=\"pi pi-pencil\"></i> (<kbd>f</kbd>): click and drag to draw a closed\n freeform polygon.\n </li>\n <li>\n \"<b>Brush</b>\" <img src=\"assets/plotting/paintbrush.svg\" width=\"15\" height=\"15\" />: click and drag to paint a\n region with a circular brush \u2014 the painted stroke is the region. A <b>Size</b> slider (brush diameter, in\n image pixels) appears in the toolbar while the brush is active. Hold <kbd>Shift</kbd> while painting to\n <b>erase</b> (subtract) from a region instead. Painting over an existing region extends it; brushing across\n two regions merges them. Painting a closed ring (or shift-erasing an interior pocket) leaves the enclosed\n area empty \u2014 a <b>donut</b> (Image view).\n </li>\n <li>\n \"<b>Polygon</b>\" <img src=\"assets/plotting/polygon-vertices.svg\" width=\"15\" height=\"15\" />: click to place\n each vertex; click the first point to close the polygon.\n </li>\n <li>\n \"<b>Add vertex</b>\" <img src=\"assets/plotting/vertex-add.svg\" width=\"15\" height=\"15\" />: click an edge of the\n selected polygon to insert a new vertex.\n </li>\n <li>\n \"<b>Delete vertex</b>\" <img src=\"assets/plotting/vertex-delete.svg\" width=\"15\" height=\"15\" />: click a vertex\n of the selected polygon to remove it.\n </li>\n <li>\n <b>Moving a region</b> <img src=\"assets/plotting/region-move.svg\" width=\"15\" height=\"15\" />: with the Select\n tool, drag the selected region's body (or a rectangle's handles to resize it).\n </li>\n <li>\n <b>Convert to B\u00E9zier</b> <img src=\"assets/plotting/to-bezier.svg\" width=\"15\" height=\"15\" /> /\n <b>Convert to polygon</b> <img src=\"assets/plotting/to-polygon.svg\" width=\"15\" height=\"15\" /> (Image view):\n right-click the selected region and pick the conversion in the context menu to turn it into a smooth B\u00E9zier\n curve, or back into a straight-edged polygon.\n </li>\n <li>\n \"<b>Wand</b>\" <img src=\"assets/plotting/wand.svg\" width=\"15\" height=\"15\" /> (<kbd>w</kbd>): click to grow a\n region from pixels with similar values around the click point. Hold <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> for an\n exact-match flood fill; drag to keep extending the selection while moving; hold <kbd>Shift</kbd> while\n clicking or dragging on an existing region to <b>erase</b> pixels from it. Use the <b>Sens.</b> slider that\n appears to make the wand stricter (higher = smaller selection) or looser. Subsequent clicks keep extending\n the same region; clicking inside an existing region adopts it, and growing into another region merges them.\n </li>\n <li>\n \"<b>Vertex eraser</b>\" <i class=\"pi pi-eraser\"></i> (<kbd>e</kbd>): click or click-and-drag to remove polygon\n vertices that fall inside the cursor circle from any region, including a region's inner rings (donut holes).\n Use the <b>Radius</b> slider to set the eraser size in image pixels. Polygons reduced below 3 vertices (or\n polylines below 2) are deleted entirely; a hole reduced below 3 vertices is removed.\n </li>\n <li>\n \"<b>Undo</b>\" <img src=\"assets/plotting/undo.svg\" width=\"15\" height=\"15\" />\n (<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Z</kbd>): reverts the last region action (draw, delete, edit, wand/brush\n stroke, segmentation, \u2026). Up to the last\n <b>10</b> actions can be undone; the button greys out when there's nothing left to undo.\n </li>\n <li>\n \"<b>Redo</b>\"\n <img src=\"assets/plotting/redo.svg\" width=\"15\" height=\"15\" />\n (<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> or\n <kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Y</kbd>): re-applies the most recently undone action. Performing a new\n region action clears what can be redone.\n </li>\n <li>\n \"<b>Delete region</b>\" <i class=\"pi pi-trash\"></i> (<kbd>Del</kbd>): deletes the selected region. This can\n also be done from the Region Editor.\n </li>\n </ul>\n\n <h3>Region actions (right-click the image)</h3>\n <p>\n Right-click a region \u2014 or the current multi-selection \u2014 to open the \"<b>Selected region(s)</b>\" group at the\n top of the context menu. These act immediately on the selection (they're not drawing modes) and are fully\n undo/redo-able:\n </p>\n <ul>\n <li>\n \"<b>Select all regions</b>\" <i class=\"pi pi-check-square\"></i>: select every region on the image (also in the\n Region Editor toolbar).\n </li>\n <li>\n \"<b>Merge / group</b>\": combine 2+ selected regions into one. If the pieces don't touch, the result is a\n single multi-part region (sparse \"bits\").\n </li>\n <li>\"<b>Ungroup</b>\": split a multi-part region back into one region per piece.</li>\n <li>\n \"<b>Inverse</b>\": replace the selection with everything <i>outside</i> it \u2014 the image rectangle with the\n selected regions punched out as holes. Works at any image size.\n </li>\n <li>\n \"<b>Simplify</b>\": reduce a region's vertex count (Douglas\u2013Peucker) by an altitude threshold in pixels \u2014 pick\n <b>Light</b> (1&nbsp;px), <b>Medium</b> (3&nbsp;px), <b>Strong</b> (8&nbsp;px), or <b>Custom\u2026</b> for a\n slider. Applies to the exterior and any holes (and every part of a multi-part region).\n </li>\n <li>\n \"<b>Convert to B\u00E9zier</b>\" / \"<b>Convert to polygon</b>\" (Image view) and \"<b>Delete</b>\" also live in this\n group; the conversions are only here, not in the toolbar.\n </li>\n </ul>\n\n <h3>Segmentation &amp; detection tools</h3>\n <p>\n AI-assisted segmentation and detection that run entirely in the browser. The first run of any model downloads\n it (a progress toast is shown); it's then cached.\n </p>\n <p>\n Two shapes of tool: the <b>prompted</b> ones (Segment, Point prompts, Cellpose) act on rectangles you draw, and\n generated regions inherit the color of the rectangle they came from<span *ngIf=\"contributedTools.length\"\n >; the <b>no-prompt</b> ones ({{ contributedToolNames }}) sweep the whole current view, so there is nothing\n to draw first</span\n >.\n </p>\n <p>\n Each model dropdown <i class=\"pi pi-chevron-down\"></i> lists the available checkpoints with a\n <i class=\"pi pi-info-circle\"></i> beside each one \u2014 hover it for that model's size, speed, training domain and\n accuracy, so you can pick without leaving the toolbar.\n </p>\n <ul>\n <li>\n \"<b>Segment</b>\" <img src=\"assets/plotting/sam-box-prompt.svg\" width=\"15\" height=\"15\" /> (box prompt): draw\n one or more <b>Rectangular regions</b> around the objects of interest, then click Segment. Each rectangle is\n replaced by the SAM mask of the object inside it.\n </li>\n <li>\n \"<b>Point prompts</b>\" <img src=\"assets/plotting/sam-point-prompt.svg\" width=\"15\" height=\"15\" />: click\n directly on an object to segment it as a <b>new</b> region. Each click starts a fresh object \u2014 clicking\n another object won't grow the previous one. <kbd>Shift</kbd>/<kbd>Alt</kbd>-click adds an\n <i>exclude</i> point that refines the current object; <kbd>Esc</kbd> undoes it, <kbd>Enter</kbd> commits it.\n </li>\n <li>\n \"<b>Model picker</b>\" <i class=\"pi pi-chevron-down\"></i>: the dropdown attached to the Segment button chooses\n the SAM model \u2014 <b>micro-sam ViT-T</b> (default; small, fast) or <b>micro-sam ViT-B</b> (larger, highest\n accuracy) for light microscopy, or <b>patho-sam ViT-B</b> for histopathology (H&amp;E / stained tissue). All\n are promptable Segment Anything models finetuned for biomedical images; they differ in the\n <i>image encoder</i> (the heavy once-per-image step) and the training domain \u2014 box and point prompting behave\n the same. The choice applies to <i>both</i> the box and point tools, which share one loaded model and image\n encoding, so the first run after switching models (or images) downloads and encodes once for both.\n </li>\n <li>\n \"<b>Cellpose</b>\" <img src=\"assets/plotting/cells.svg\" width=\"15\" height=\"15\" />: draw rectangles, then click\n Cellpose to automatically segment every cell inside each rectangle (client-side cellpose-SAM) \u2014 one region\n per detected cell. No clicking on individual cells needed.\n </li>\n <!-- Contributed tools describe themselves. This library ships none, so an\n open build's help lists only the prompted tools above \u2014 the text no\n longer promises buttons that are not there. -->\n <ng-container *ngFor=\"let tool of contributedTools\">\n <li *ngIf=\"tool.help\" [innerHTML]=\"tool.help.body\"></li>\n </ng-container>\n <ng-container *ngFor=\"let tool of dialogTools\">\n <li *ngIf=\"tool.help\" [innerHTML]=\"tool.help.body\"></li>\n </ng-container>\n </ul>\n\n <h3>Spatial omics</h3>\n <p>\n In <b>Spatial omics</b> mode each observation \u2014 a cell or a Visium spot \u2014 is drawn at its own coordinates, over\n the tissue image where the dataset registers onto one. The\n <img src=\"assets/plotting/spatial-omics.svg\" width=\"14\" height=\"14\" alt=\"Spatial omics\" />\n panel colours them by a <b>column</b> (a cell type, a cluster) or by a <b>gene</b>, and the two are exclusive\n so what drives the colours is never ambiguous.\n </p>\n <ul>\n <li>\n \"<b>Select from ROIs</b>\": turns the regions you have drawn into a selection of observations. Everything else\n then follows that selection. In 3D, with classes picked in the Region Editor's class tree (or their surfaces\n clicked), it selects the observations inside those classes' volumes.\n </li>\n <li>\"<b>Point size</b>\" / \"<b>Opacity</b>\": how the markers are drawn, not what is selected.</li>\n </ul>\n\n <h3>Distribution charts</h3>\n <p>\n The <b>Distribution</b> section of the spatial-omics panel charts whatever the map is coloured by, so the two\n can never disagree. Each tab answers a different question, and the <i class=\"pi pi-question-circle\"></i> beside\n them explains the one on screen \u2014 including what it must <em>not</em> be read for.\n </p>\n <ul>\n <li>\n \"<b>Counts</b>\": how many observations per category. A category is a label, not a magnitude, so a frequency\n is the only distribution it has.\n </li>\n <li>\n \"<b>Histogram</b>\" / \"<b>Violin</b>\" / \"<b>Box</b>\": how a continuous value is distributed \u2014 over everything,\n or split by a grouping column.\n </li>\n <li>\n \"<b>Heatmap</b>\": mean expression of the genes you pick within each group. Z-scored per gene by default, so a\n colour means \"above or below this gene's own average\" rather than \"highly expressed\".\n </li>\n <li>\n \"<b>Embedding</b>\": a UMAP, PCA or t-SNE over the same cells. <b>Lasso it to select on the map</b> \u2014 both\n views read the same selection. Only PCA labels its axes with a percentage, because only its axes explain a\n measurable share of the variance; UMAP and t-SNE coordinates are arbitrary, so distance between distant\n clusters should not be read.\n </li>\n <li>\n \"<b>Detach</b>\": moves the charts into their own window so they can sit beside the map instead of inside the\n panel. Switching tabs keeps the window where it is.\n </li>\n </ul>\n\n <h3>Help</h3>\n <p>\"<b>Help</b>\" <i class=\"pi pi-question-circle\"></i>: opens this dialog.</p>\n\n <h3>Keyboard shortcuts</h3>\n <table class=\"shortcuts-table\">\n <tr>\n <td><kbd>+</kbd> / <kbd>=</kbd></td>\n <td>Zoom in</td>\n </tr>\n <tr>\n <td><kbd>-</kbd></td>\n <td>Zoom out</td>\n </tr>\n <tr>\n <td><kbd>b</kbd></td>\n <td>Toggle zoom box mode</td>\n </tr>\n <tr>\n <td><kbd>p</kbd></td>\n <td>Toggle pan mode</td>\n </tr>\n <tr>\n <td><kbd>f</kbd></td>\n <td>Toggle freeform region drawing</td>\n </tr>\n <tr>\n <td><kbd>l</kbd></td>\n <td>Toggle polyline drawing</td>\n </tr>\n <tr>\n <td><kbd>r</kbd></td>\n <td>Toggle rectangular region drawing</td>\n </tr>\n <tr>\n <td><kbd>w</kbd></td>\n <td>Toggle wand tool</td>\n </tr>\n <tr>\n <td>Wand + <kbd>Shift</kbd></td>\n <td>Erase pixels from the active region</td>\n </tr>\n <tr>\n <td>Wand + <kbd>Ctrl</kbd>/<kbd>Cmd</kbd></td>\n <td>Exact-match flood fill (no smoothing)</td>\n </tr>\n <tr>\n <td><kbd>e</kbd></td>\n <td>Toggle vertex eraser</td>\n </tr>\n <tr>\n <td><kbd>s</kbd></td>\n <td>Selection mode (deactivates drawing tools)</td>\n </tr>\n <tr>\n <td>Point prompt + <kbd>Shift</kbd>/<kbd>Alt</kbd></td>\n <td>Add an exclude point (refine the current object)</td>\n </tr>\n <tr>\n <td><kbd>Enter</kbd></td>\n <td>Commit the current SAM point object</td>\n </tr>\n <tr>\n <td><kbd>Esc</kbd></td>\n <td>Undo the current SAM point object</td>\n </tr>\n <tr>\n <td><kbd>Del</kbd> / <kbd>\u232B</kbd></td>\n <td>Delete selected region</td>\n </tr>\n <tr>\n <td><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Z</kbd></td>\n <td>Undo the last region action (up to 10)</td>\n </tr>\n <tr>\n <td><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> / <kbd>Ctrl</kbd>+<kbd>Y</kbd></td>\n <td>Redo the last undone region action</td>\n </tr>\n </table>\n\n <ng-template pTemplate=\"footer\">\n <p-button icon=\"pi pi-check\" (click)=\"visibleChange.emit(false)\" label=\"Ok\" styleClass=\"p-button-text\">\n </p-button>\n </ng-template>\n</p-dialog>\n", styles: ["kbd{background:#e8e8e8;border:1px solid #bbb;border-radius:3px;padding:1px 5px;font-family:monospace;font-size:.85em}.shortcuts-table{border-collapse:collapse}.shortcuts-table td{padding:.25rem .75rem .25rem 0;vertical-align:middle}.shortcuts-table td:first-child{white-space:nowrap}\n"] }]
12910
13000
  }], propDecorators: { visible: [{
12911
13001
  type: Input
12912
13002
  }], visibleChange: [{
@@ -15963,6 +16053,18 @@ class RegionEditorComponent {
15963
16053
  .getRegionPicked3d$?.()
15964
16054
  ?.pipe(takeUntilDestroyed(this.destroyRef))
15965
16055
  .subscribe(({ key, additive }) => this.onPicked3d(key, additive));
16056
+ // Another selection replaced the class tree's pick (a region drawn or clicked on the canvas).
16057
+ this.regionApi
16058
+ .getPickedRegionClasses$?.()
16059
+ ?.pipe(takeUntilDestroyed(this.destroyRef))
16060
+ .subscribe((classes) => {
16061
+ if (classes.length || !this.pickedClasses.length)
16062
+ return;
16063
+ this.pickedClass = null;
16064
+ this.pickedClasses = [];
16065
+ this.updateSelectionHighlight();
16066
+ this.changed();
16067
+ });
15966
16068
  this.persistence
15967
16069
  .fileExists(this.saveAsCheck$)
15968
16070
  .pipe(takeUntilDestroyed(this.destroyRef))
@@ -23767,6 +23869,12 @@ class SpatialControlsComponent {
23767
23869
  /** An ROI selection that matched nothing, so the UI can say so rather than
23768
23870
  * looking like the button did nothing. */
23769
23871
  this.selectionMissed = false;
23872
+ /** The mode of the selection that {@link selectionMissed}: what it found nothing inside. */
23873
+ this.missedMode = 'regions';
23874
+ /** The region classes picked in the Region Editor: in 3D, "Select from ROIs" selects inside them. */
23875
+ this.pickedClasses = [];
23876
+ /** While "Select from ROIs" reads the picked classes' outlines. */
23877
+ this.selectingFromRegions = false;
23770
23878
  /**
23771
23879
  * Whether the Distribution section is expanded. Collapsed by default: the
23772
23880
  * panel's primary job is the colour controls, and the chart roughly doubles
@@ -23845,12 +23953,19 @@ class SpatialControlsComponent {
23845
23953
  this.controls
23846
23954
  .getSelection$()
23847
23955
  .pipe(untilDestroyed())
23848
- .subscribe((selection) => {
23956
+ .subscribe((selection) =>
23957
+ // Set from outside the zone too: a selection from the region classes' volumes.
23958
+ this.runAndMark(() => {
23849
23959
  this.selection = selection;
23850
23960
  if (selection.count === 0)
23851
23961
  this.selectedCategory = null;
23852
- this.cdr.markForCheck();
23853
- });
23962
+ }));
23963
+ this.controls
23964
+ .getPickedRegionClasses$?.()
23965
+ ?.pipe(untilDestroyed())
23966
+ .subscribe((classes) => this.runAndMark(() => {
23967
+ this.pickedClasses = classes;
23968
+ }));
23854
23969
  // The colour bar must use the colormap the renderer is using.
23855
23970
  combineLatest([this.viz.getColormap(), this.viz.getReverseScale()])
23856
23971
  .pipe(untilDestroyed())
@@ -23881,11 +23996,40 @@ class SpatialControlsComponent {
23881
23996
  return this.genes.ensureList();
23882
23997
  }
23883
23998
  // ── selection ───────────────────────────────────────────────────────────
23884
- /** Select every observation inside the drawn regions (their union). */
23885
- selectFromRegions() {
23886
- const count = this.controls?.selectFromRegions() ?? 0;
23887
- this.selectedCategory = null;
23888
- this.selectionMissed = count === 0;
23999
+ /** Select every observation inside the drawn regions (their union) — in 3D, inside the
24000
+ * picked classes' volumes. */
24001
+ async selectFromRegions() {
24002
+ if (!this.controls || this.selectingFromRegions)
24003
+ return;
24004
+ this.selectingFromRegions = true;
24005
+ // The mode the selection runs in (decided as it starts), for what a miss says it missed.
24006
+ const mode = this.roiSelectionMode;
24007
+ this.cdr.markForCheck();
24008
+ let count = 0;
24009
+ try {
24010
+ count = await this.controls.selectFromRegions();
24011
+ }
24012
+ catch (err) {
24013
+ console.warn('[spatial] could not select from the regions', err);
24014
+ }
24015
+ // The classes' outlines are read in a worker, whose answer lands outside the zone.
24016
+ this.runAndMark(() => {
24017
+ this.selectingFromRegions = false;
24018
+ // Superseded (null): a newer selection stands, and says what it is itself.
24019
+ if (count === null)
24020
+ return;
24021
+ this.selectedCategory = null;
24022
+ this.selectionMissed = count === 0;
24023
+ this.missedMode = mode;
24024
+ });
24025
+ }
24026
+ /** In 3D with classes picked, "Select from ROIs" selects inside their volumes. */
24027
+ get selectsClassVolumes() {
24028
+ return this.roiSelectionMode === 'class-volumes';
24029
+ }
24030
+ /** How "Select from ROIs" selects now: the controls' own rule, the one the selection runs by. */
24031
+ get roiSelectionMode() {
24032
+ return this.controls?.roiSelectionMode?.() ?? 'regions';
23889
24033
  }
23890
24034
  /** Legend click: select one category. Clicking the active row clears it, so a
23891
24035
  * second click is an undo rather than a no-op. */
@@ -23977,7 +24121,7 @@ class SpatialControlsComponent {
23977
24121
  return this.key.refresh(this.controls, this.dataset, this.view, this.colormap, this.reverse);
23978
24122
  }
23979
24123
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialControlsComponent, deps: [{ token: VISUALIZER }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); }
23980
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialControlsComponent, isStandalone: true, selector: "spatial-controls", inputs: { visible: "visible", is3d: "is3d" }, outputs: { visibleChange: "visibleChange" }, viewQueries: [{ propertyName: "charts", first: true, predicate: SpatialChartsComponent, descendants: true }], ngImport: i0, template: "<p-dialog\n header=\"Spatial omics\"\n [visible]=\"visible\"\n (visibleChange)=\"onVisibleChange($event)\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n position=\"left\"\n (onResizeEnd)=\"onResizeEnd()\"\n [style]=\"{ width: '30rem' }\"\n [breakpoints]=\"{ '960px': '90vw' }\"\n styleClass=\"spatial-controls-dialog\"\n>\n <!-- No port bound: say why rather than showing controls that do nothing. -->\n <div class=\"sc-empty\" *ngIf=\"!controls\">\n No spatial-omics data source is configured. A host provides one by binding\n <code>SPATIAL_DATA_PORT</code>.\n </div>\n\n <div class=\"sc-empty\" *ngIf=\"controls && !dataset\">No dataset is loaded.</div>\n\n <div class=\"sc-body\" *ngIf=\"controls && dataset as ds\">\n <div class=\"sc-dataset\">\n <span class=\"sc-dataset-name\">{{ ds.name }}</span>\n <span class=\"sc-dataset-count\">{{ ds.observations.count | number }} observations</span>\n </div>\n\n <!-- Organised as Xenium Explorer's side panel: one collapsible section per layer,\n each with its own on/off switch in the header. -->\n\n <!-- Images -->\n <div class=\"sc-panel\" *ngIf=\"ds.imageRef && !is3d\" [class.open]=\"open.images\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('images')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showImage\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowImage($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Images</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.images\" [class.pi-chevron-right]=\"!open.images\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.images\">\n <small class=\"sc-hint\"> Channels, colours and contrast are in the Channels &amp; Histogram dialog. </small>\n </div>\n </div>\n\n <!-- Cells -->\n <spatial-cells-panel\n *ngIf=\"hasCells && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [colorBarCss]=\"colorBarCss\"\n [open]=\"open.cells\"\n (openChange)=\"setOpen('cells', $event)\"\n ></spatial-cells-panel>\n\n <!-- Transcripts -->\n <spatial-transcripts-panel\n *ngIf=\"hasTranscripts && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [estimate]=\"estimate\"\n [densityStats]=\"densityStats\"\n [geneCounts]=\"geneCounts\"\n [colormapOptions]=\"colormapOptions\"\n [open]=\"open.transcripts\"\n (openChange)=\"setOpen('transcripts', $event)\"\n >\n </spatial-transcripts-panel>\n\n <!-- Annotations: the drawn regions, and selecting what they enclose. -->\n <div class=\"sc-panel\" [class.open]=\"open.annotations\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('annotations')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showAnnotations !== false\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowAnnotations($event.checked)\"\n pTooltip=\"Show the drawn regions\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Annotations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.annotations\" [class.pi-chevron-right]=\"!open.annotations\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.annotations\">\n <div class=\"sc-row\">\n <p-button\n label=\"Select from ROIs\"\n icon=\"pi pi-object-group\"\n size=\"small\"\n [outlined]=\"true\"\n (onClick)=\"selectFromRegions()\"\n pTooltip=\"Select every observation inside the drawn regions\"\n tooltipPosition=\"top\"\n ></p-button>\n <p-button\n *ngIf=\"hasSelection\"\n label=\"Clear\"\n size=\"small\"\n [text]=\"true\"\n (onClick)=\"clearSelection()\"\n ></p-button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"hasSelection\">\n {{ selection.count | number }} of {{ ds.observations.count | number }} selected \u2014 the rest are muted.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectionMissed\"> No observations fell inside the drawn regions. </small>\n <small class=\"sc-hint\" *ngIf=\"is3d\">\n In 3D a region cuts through the cloud's full depth. Orbit and draw again to cut from another angle.\n </small>\n </div>\n </div>\n\n <!-- Observations, then what else draws them: the gene map, the 3D scene, and the\n continuous-only knobs. -->\n <spatial-observations-panel\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selection]=\"selection\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n [is3d]=\"is3d\"\n [open]=\"open.observations\"\n (openChange)=\"setOpen('observations', $event)\"\n (categoryClicked)=\"selectCategory($event)\"\n ></spatial-observations-panel>\n\n <!-- Distributions of whatever the map is coloured by. In the SAME dialog as\n the colour controls on purpose: changing the gene and watching the\n distribution move is one action, and two floating windows made that\n harder to see rather than easier. -->\n <div class=\"sc-section sc-charts\">\n <button\n type=\"button\"\n class=\"sc-disclosure\"\n (click)=\"toggleCharts()\"\n [attr.aria-expanded]=\"chartsOpen\"\n [attr.aria-controls]=\"chartsBodyId\"\n >\n <i class=\"pi\" [class.pi-chevron-right]=\"!chartsOpen\" [class.pi-chevron-down]=\"chartsOpen\"></i>\n <span>Distribution</span>\n <small *ngIf=\"!chartsOpen && view.colorBy\">of {{ colorByLabel }}</small>\n </button>\n <!-- Kept MOUNTED when collapsed, only hidden: destroying it would throw\n away the chart kind and grouping the user picked. `active` is what\n stops it drawing while it has no size. -->\n <div [id]=\"chartsBodyId\" [hidden]=\"!chartsOpen\">\n <spatial-charts [active]=\"visible && chartsOpen\"></spatial-charts>\n </div>\n </div>\n\n <div class=\"sc-btns\">\n <p-button label=\"Reset\" size=\"small\" [text]=\"true\" (onClick)=\"reset()\"></p-button>\n </div>\n </div>\n</p-dialog>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}:host ::ng-deep .spatial-controls-dialog .p-dialog-content{padding:.75rem 1rem 1rem}.sc-empty{padding:.5rem 0;font-size:.85rem;color:var(--text-color-secondary, #6c757d);line-height:1.4}.sc-empty code{font-size:.8rem}.sc-body{display:flex;flex-direction:column;gap:.85rem}.sc-dataset{display:flex;flex-direction:column;gap:.1rem;padding-bottom:.5rem;border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-dataset-name{font-weight:600;font-size:.9rem}.sc-dataset-count{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-btns{display:flex;justify-content:flex-end}.sc-charts{padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6);gap:0}.sc-disclosure{display:flex;align-items:center;gap:.4rem;width:100%;padding:.2rem 0;font:inherit;font-size:.82rem;font-weight:600;text-align:left;background:none;border:0;cursor:pointer;color:inherit}.sc-disclosure i{font-size:.7rem;color:var(--text-color-secondary, #6c757d)}.sc-disclosure small{font-weight:400;color:var(--text-color-secondary, #6c757d);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-disclosure:hover{color:var(--primary-color, #3b82f6)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$3.Checkbox, selector: "p-checkbox", inputs: ["value", "name", "disabled", "binary", "label", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "styleClass", "labelStyleClass", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i5.Dialog, selector: "p-dialog", inputs: ["header", "draggable", "resizable", "positionLeft", "positionTop", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "responsive", "appendTo", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "breakpoint", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "visible", "style", "position"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: SpatialCellsPanelComponent, selector: "spatial-cells-panel", inputs: ["controls", "dataset", "view", "genes", "colorBarCss", "open"], outputs: ["openChange"] }, { kind: "component", type: SpatialTranscriptsPanelComponent, selector: "spatial-transcripts-panel", inputs: ["controls", "dataset", "view", "genes", "estimate", "densityStats", "geneCounts", "colormapOptions", "open"], outputs: ["openChange"] }, { kind: "component", type: SpatialObservationsPanelComponent, selector: "spatial-observations-panel", inputs: ["controls", "dataset", "view", "genes", "legend", "colorBarCss", "selection", "selectedCategory", "colormapOptions", "is3d", "open"], outputs: ["openChange", "categoryClicked"] }, { kind: "component", type: SpatialChartsComponent, selector: "spatial-charts", inputs: ["active"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
24124
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialControlsComponent, isStandalone: true, selector: "spatial-controls", inputs: { visible: "visible", is3d: "is3d" }, outputs: { visibleChange: "visibleChange" }, viewQueries: [{ propertyName: "charts", first: true, predicate: SpatialChartsComponent, descendants: true }], ngImport: i0, template: "<p-dialog\n header=\"Spatial omics\"\n [visible]=\"visible\"\n (visibleChange)=\"onVisibleChange($event)\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n position=\"left\"\n (onResizeEnd)=\"onResizeEnd()\"\n [style]=\"{ width: '30rem' }\"\n [breakpoints]=\"{ '960px': '90vw' }\"\n styleClass=\"spatial-controls-dialog\"\n>\n <!-- No port bound: say why rather than showing controls that do nothing. -->\n <div class=\"sc-empty\" *ngIf=\"!controls\">\n No spatial-omics data source is configured. A host provides one by binding\n <code>SPATIAL_DATA_PORT</code>.\n </div>\n\n <div class=\"sc-empty\" *ngIf=\"controls && !dataset\">No dataset is loaded.</div>\n\n <div class=\"sc-body\" *ngIf=\"controls && dataset as ds\">\n <div class=\"sc-dataset\">\n <span class=\"sc-dataset-name\">{{ ds.name }}</span>\n <span class=\"sc-dataset-count\">{{ ds.observations.count | number }} observations</span>\n </div>\n\n <!-- Organised as Xenium Explorer's side panel: one collapsible section per layer,\n each with its own on/off switch in the header. -->\n\n <!-- Images -->\n <div class=\"sc-panel\" *ngIf=\"ds.imageRef && !is3d\" [class.open]=\"open.images\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('images')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showImage\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowImage($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Images</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.images\" [class.pi-chevron-right]=\"!open.images\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.images\">\n <small class=\"sc-hint\"> Channels, colours and contrast are in the Channels &amp; Histogram dialog. </small>\n </div>\n </div>\n\n <!-- Cells -->\n <spatial-cells-panel\n *ngIf=\"hasCells && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [colorBarCss]=\"colorBarCss\"\n [open]=\"open.cells\"\n (openChange)=\"setOpen('cells', $event)\"\n ></spatial-cells-panel>\n\n <!-- Transcripts -->\n <spatial-transcripts-panel\n *ngIf=\"hasTranscripts && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [estimate]=\"estimate\"\n [densityStats]=\"densityStats\"\n [geneCounts]=\"geneCounts\"\n [colormapOptions]=\"colormapOptions\"\n [open]=\"open.transcripts\"\n (openChange)=\"setOpen('transcripts', $event)\"\n >\n </spatial-transcripts-panel>\n\n <!-- Annotations: the drawn regions, and selecting what they enclose. -->\n <div class=\"sc-panel\" [class.open]=\"open.annotations\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('annotations')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showAnnotations !== false\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowAnnotations($event.checked)\"\n pTooltip=\"Show the drawn regions\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Annotations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.annotations\" [class.pi-chevron-right]=\"!open.annotations\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.annotations\">\n <div class=\"sc-row\">\n <p-button\n label=\"Select from ROIs\"\n icon=\"pi pi-object-group\"\n size=\"small\"\n [outlined]=\"true\"\n [loading]=\"selectingFromRegions\"\n (onClick)=\"selectFromRegions()\"\n [pTooltip]=\"\n selectsClassVolumes\n ? 'Select every observation inside the volumes of the classes picked in the Region Editor'\n : 'Select every observation inside the drawn regions'\n \"\n tooltipPosition=\"top\"\n ></p-button>\n <p-button\n *ngIf=\"hasSelection\"\n label=\"Clear\"\n size=\"small\"\n [text]=\"true\"\n (onClick)=\"clearSelection()\"\n ></p-button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"hasSelection\">\n {{ selection.count | number }} of {{ ds.observations.count | number }} selected \u2014 the rest are muted.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectionMissed\">\n No observations fell inside the\n {{ missedMode === 'class-volumes' ? 'picked classes' : 'drawn regions' }}.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectsClassVolumes\">\n Selects inside the volume of {{ pickedClasses[0]\n }}{{ pickedClasses.length > 1 ? ' and ' + (pickedClasses.length - 1 | number) + ' more classes' : '' }},\n picked in the Region Editor. Draw a region to select by it instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes && pickedClasses.length\">\n The classes picked in the Region Editor are not on the reference volume shown here, so this selects by\n the regions drawn over the cloud instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes\">\n In 3D a region cuts through the cloud's full depth. Orbit and draw again to cut from another angle.\n <ng-container *ngIf=\"!pickedClasses.length\">\n Pick classes in the Region Editor's class tree to select inside their volumes.\n </ng-container>\n </small>\n </div>\n </div>\n\n <!-- Observations, then what else draws them: the gene map, the 3D scene, and the\n continuous-only knobs. -->\n <spatial-observations-panel\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selection]=\"selection\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n [is3d]=\"is3d\"\n [open]=\"open.observations\"\n (openChange)=\"setOpen('observations', $event)\"\n (categoryClicked)=\"selectCategory($event)\"\n ></spatial-observations-panel>\n\n <!-- Distributions of whatever the map is coloured by. In the SAME dialog as\n the colour controls on purpose: changing the gene and watching the\n distribution move is one action, and two floating windows made that\n harder to see rather than easier. -->\n <div class=\"sc-section sc-charts\">\n <button\n type=\"button\"\n class=\"sc-disclosure\"\n (click)=\"toggleCharts()\"\n [attr.aria-expanded]=\"chartsOpen\"\n [attr.aria-controls]=\"chartsBodyId\"\n >\n <i class=\"pi\" [class.pi-chevron-right]=\"!chartsOpen\" [class.pi-chevron-down]=\"chartsOpen\"></i>\n <span>Distribution</span>\n <small *ngIf=\"!chartsOpen && view.colorBy\">of {{ colorByLabel }}</small>\n </button>\n <!-- Kept MOUNTED when collapsed, only hidden: destroying it would throw\n away the chart kind and grouping the user picked. `active` is what\n stops it drawing while it has no size. -->\n <div [id]=\"chartsBodyId\" [hidden]=\"!chartsOpen\">\n <spatial-charts [active]=\"visible && chartsOpen\"></spatial-charts>\n </div>\n </div>\n\n <div class=\"sc-btns\">\n <p-button label=\"Reset\" size=\"small\" [text]=\"true\" (onClick)=\"reset()\"></p-button>\n </div>\n </div>\n</p-dialog>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}:host ::ng-deep .spatial-controls-dialog .p-dialog-content{padding:.75rem 1rem 1rem}.sc-empty{padding:.5rem 0;font-size:.85rem;color:var(--text-color-secondary, #6c757d);line-height:1.4}.sc-empty code{font-size:.8rem}.sc-body{display:flex;flex-direction:column;gap:.85rem}.sc-dataset{display:flex;flex-direction:column;gap:.1rem;padding-bottom:.5rem;border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-dataset-name{font-weight:600;font-size:.9rem}.sc-dataset-count{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-btns{display:flex;justify-content:flex-end}.sc-charts{padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6);gap:0}.sc-disclosure{display:flex;align-items:center;gap:.4rem;width:100%;padding:.2rem 0;font:inherit;font-size:.82rem;font-weight:600;text-align:left;background:none;border:0;cursor:pointer;color:inherit}.sc-disclosure i{font-size:.7rem;color:var(--text-color-secondary, #6c757d)}.sc-disclosure small{font-weight:400;color:var(--text-color-secondary, #6c757d);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-disclosure:hover{color:var(--primary-color, #3b82f6)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$3.Checkbox, selector: "p-checkbox", inputs: ["value", "name", "disabled", "binary", "label", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "styleClass", "labelStyleClass", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i5.Dialog, selector: "p-dialog", inputs: ["header", "draggable", "resizable", "positionLeft", "positionTop", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "responsive", "appendTo", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "breakpoint", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "visible", "style", "position"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: SpatialCellsPanelComponent, selector: "spatial-cells-panel", inputs: ["controls", "dataset", "view", "genes", "colorBarCss", "open"], outputs: ["openChange"] }, { kind: "component", type: SpatialTranscriptsPanelComponent, selector: "spatial-transcripts-panel", inputs: ["controls", "dataset", "view", "genes", "estimate", "densityStats", "geneCounts", "colormapOptions", "open"], outputs: ["openChange"] }, { kind: "component", type: SpatialObservationsPanelComponent, selector: "spatial-observations-panel", inputs: ["controls", "dataset", "view", "genes", "legend", "colorBarCss", "selection", "selectedCategory", "colormapOptions", "is3d", "open"], outputs: ["openChange", "categoryClicked"] }, { kind: "component", type: SpatialChartsComponent, selector: "spatial-charts", inputs: ["active"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
23981
24125
  }
23982
24126
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialControlsComponent, decorators: [{
23983
24127
  type: Component,
@@ -23992,7 +24136,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
23992
24136
  SpatialTranscriptsPanelComponent,
23993
24137
  SpatialObservationsPanelComponent,
23994
24138
  SpatialChartsComponent,
23995
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<p-dialog\n header=\"Spatial omics\"\n [visible]=\"visible\"\n (visibleChange)=\"onVisibleChange($event)\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n position=\"left\"\n (onResizeEnd)=\"onResizeEnd()\"\n [style]=\"{ width: '30rem' }\"\n [breakpoints]=\"{ '960px': '90vw' }\"\n styleClass=\"spatial-controls-dialog\"\n>\n <!-- No port bound: say why rather than showing controls that do nothing. -->\n <div class=\"sc-empty\" *ngIf=\"!controls\">\n No spatial-omics data source is configured. A host provides one by binding\n <code>SPATIAL_DATA_PORT</code>.\n </div>\n\n <div class=\"sc-empty\" *ngIf=\"controls && !dataset\">No dataset is loaded.</div>\n\n <div class=\"sc-body\" *ngIf=\"controls && dataset as ds\">\n <div class=\"sc-dataset\">\n <span class=\"sc-dataset-name\">{{ ds.name }}</span>\n <span class=\"sc-dataset-count\">{{ ds.observations.count | number }} observations</span>\n </div>\n\n <!-- Organised as Xenium Explorer's side panel: one collapsible section per layer,\n each with its own on/off switch in the header. -->\n\n <!-- Images -->\n <div class=\"sc-panel\" *ngIf=\"ds.imageRef && !is3d\" [class.open]=\"open.images\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('images')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showImage\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowImage($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Images</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.images\" [class.pi-chevron-right]=\"!open.images\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.images\">\n <small class=\"sc-hint\"> Channels, colours and contrast are in the Channels &amp; Histogram dialog. </small>\n </div>\n </div>\n\n <!-- Cells -->\n <spatial-cells-panel\n *ngIf=\"hasCells && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [colorBarCss]=\"colorBarCss\"\n [open]=\"open.cells\"\n (openChange)=\"setOpen('cells', $event)\"\n ></spatial-cells-panel>\n\n <!-- Transcripts -->\n <spatial-transcripts-panel\n *ngIf=\"hasTranscripts && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [estimate]=\"estimate\"\n [densityStats]=\"densityStats\"\n [geneCounts]=\"geneCounts\"\n [colormapOptions]=\"colormapOptions\"\n [open]=\"open.transcripts\"\n (openChange)=\"setOpen('transcripts', $event)\"\n >\n </spatial-transcripts-panel>\n\n <!-- Annotations: the drawn regions, and selecting what they enclose. -->\n <div class=\"sc-panel\" [class.open]=\"open.annotations\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('annotations')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showAnnotations !== false\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowAnnotations($event.checked)\"\n pTooltip=\"Show the drawn regions\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Annotations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.annotations\" [class.pi-chevron-right]=\"!open.annotations\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.annotations\">\n <div class=\"sc-row\">\n <p-button\n label=\"Select from ROIs\"\n icon=\"pi pi-object-group\"\n size=\"small\"\n [outlined]=\"true\"\n (onClick)=\"selectFromRegions()\"\n pTooltip=\"Select every observation inside the drawn regions\"\n tooltipPosition=\"top\"\n ></p-button>\n <p-button\n *ngIf=\"hasSelection\"\n label=\"Clear\"\n size=\"small\"\n [text]=\"true\"\n (onClick)=\"clearSelection()\"\n ></p-button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"hasSelection\">\n {{ selection.count | number }} of {{ ds.observations.count | number }} selected \u2014 the rest are muted.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectionMissed\"> No observations fell inside the drawn regions. </small>\n <small class=\"sc-hint\" *ngIf=\"is3d\">\n In 3D a region cuts through the cloud's full depth. Orbit and draw again to cut from another angle.\n </small>\n </div>\n </div>\n\n <!-- Observations, then what else draws them: the gene map, the 3D scene, and the\n continuous-only knobs. -->\n <spatial-observations-panel\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selection]=\"selection\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n [is3d]=\"is3d\"\n [open]=\"open.observations\"\n (openChange)=\"setOpen('observations', $event)\"\n (categoryClicked)=\"selectCategory($event)\"\n ></spatial-observations-panel>\n\n <!-- Distributions of whatever the map is coloured by. In the SAME dialog as\n the colour controls on purpose: changing the gene and watching the\n distribution move is one action, and two floating windows made that\n harder to see rather than easier. -->\n <div class=\"sc-section sc-charts\">\n <button\n type=\"button\"\n class=\"sc-disclosure\"\n (click)=\"toggleCharts()\"\n [attr.aria-expanded]=\"chartsOpen\"\n [attr.aria-controls]=\"chartsBodyId\"\n >\n <i class=\"pi\" [class.pi-chevron-right]=\"!chartsOpen\" [class.pi-chevron-down]=\"chartsOpen\"></i>\n <span>Distribution</span>\n <small *ngIf=\"!chartsOpen && view.colorBy\">of {{ colorByLabel }}</small>\n </button>\n <!-- Kept MOUNTED when collapsed, only hidden: destroying it would throw\n away the chart kind and grouping the user picked. `active` is what\n stops it drawing while it has no size. -->\n <div [id]=\"chartsBodyId\" [hidden]=\"!chartsOpen\">\n <spatial-charts [active]=\"visible && chartsOpen\"></spatial-charts>\n </div>\n </div>\n\n <div class=\"sc-btns\">\n <p-button label=\"Reset\" size=\"small\" [text]=\"true\" (onClick)=\"reset()\"></p-button>\n </div>\n </div>\n</p-dialog>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}:host ::ng-deep .spatial-controls-dialog .p-dialog-content{padding:.75rem 1rem 1rem}.sc-empty{padding:.5rem 0;font-size:.85rem;color:var(--text-color-secondary, #6c757d);line-height:1.4}.sc-empty code{font-size:.8rem}.sc-body{display:flex;flex-direction:column;gap:.85rem}.sc-dataset{display:flex;flex-direction:column;gap:.1rem;padding-bottom:.5rem;border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-dataset-name{font-weight:600;font-size:.9rem}.sc-dataset-count{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-btns{display:flex;justify-content:flex-end}.sc-charts{padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6);gap:0}.sc-disclosure{display:flex;align-items:center;gap:.4rem;width:100%;padding:.2rem 0;font:inherit;font-size:.82rem;font-weight:600;text-align:left;background:none;border:0;cursor:pointer;color:inherit}.sc-disclosure i{font-size:.7rem;color:var(--text-color-secondary, #6c757d)}.sc-disclosure small{font-weight:400;color:var(--text-color-secondary, #6c757d);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-disclosure:hover{color:var(--primary-color, #3b82f6)}\n"] }]
24139
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<p-dialog\n header=\"Spatial omics\"\n [visible]=\"visible\"\n (visibleChange)=\"onVisibleChange($event)\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n position=\"left\"\n (onResizeEnd)=\"onResizeEnd()\"\n [style]=\"{ width: '30rem' }\"\n [breakpoints]=\"{ '960px': '90vw' }\"\n styleClass=\"spatial-controls-dialog\"\n>\n <!-- No port bound: say why rather than showing controls that do nothing. -->\n <div class=\"sc-empty\" *ngIf=\"!controls\">\n No spatial-omics data source is configured. A host provides one by binding\n <code>SPATIAL_DATA_PORT</code>.\n </div>\n\n <div class=\"sc-empty\" *ngIf=\"controls && !dataset\">No dataset is loaded.</div>\n\n <div class=\"sc-body\" *ngIf=\"controls && dataset as ds\">\n <div class=\"sc-dataset\">\n <span class=\"sc-dataset-name\">{{ ds.name }}</span>\n <span class=\"sc-dataset-count\">{{ ds.observations.count | number }} observations</span>\n </div>\n\n <!-- Organised as Xenium Explorer's side panel: one collapsible section per layer,\n each with its own on/off switch in the header. -->\n\n <!-- Images -->\n <div class=\"sc-panel\" *ngIf=\"ds.imageRef && !is3d\" [class.open]=\"open.images\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('images')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showImage\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowImage($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Images</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.images\" [class.pi-chevron-right]=\"!open.images\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.images\">\n <small class=\"sc-hint\"> Channels, colours and contrast are in the Channels &amp; Histogram dialog. </small>\n </div>\n </div>\n\n <!-- Cells -->\n <spatial-cells-panel\n *ngIf=\"hasCells && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [colorBarCss]=\"colorBarCss\"\n [open]=\"open.cells\"\n (openChange)=\"setOpen('cells', $event)\"\n ></spatial-cells-panel>\n\n <!-- Transcripts -->\n <spatial-transcripts-panel\n *ngIf=\"hasTranscripts && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [estimate]=\"estimate\"\n [densityStats]=\"densityStats\"\n [geneCounts]=\"geneCounts\"\n [colormapOptions]=\"colormapOptions\"\n [open]=\"open.transcripts\"\n (openChange)=\"setOpen('transcripts', $event)\"\n >\n </spatial-transcripts-panel>\n\n <!-- Annotations: the drawn regions, and selecting what they enclose. -->\n <div class=\"sc-panel\" [class.open]=\"open.annotations\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('annotations')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showAnnotations !== false\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowAnnotations($event.checked)\"\n pTooltip=\"Show the drawn regions\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Annotations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.annotations\" [class.pi-chevron-right]=\"!open.annotations\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.annotations\">\n <div class=\"sc-row\">\n <p-button\n label=\"Select from ROIs\"\n icon=\"pi pi-object-group\"\n size=\"small\"\n [outlined]=\"true\"\n [loading]=\"selectingFromRegions\"\n (onClick)=\"selectFromRegions()\"\n [pTooltip]=\"\n selectsClassVolumes\n ? 'Select every observation inside the volumes of the classes picked in the Region Editor'\n : 'Select every observation inside the drawn regions'\n \"\n tooltipPosition=\"top\"\n ></p-button>\n <p-button\n *ngIf=\"hasSelection\"\n label=\"Clear\"\n size=\"small\"\n [text]=\"true\"\n (onClick)=\"clearSelection()\"\n ></p-button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"hasSelection\">\n {{ selection.count | number }} of {{ ds.observations.count | number }} selected \u2014 the rest are muted.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectionMissed\">\n No observations fell inside the\n {{ missedMode === 'class-volumes' ? 'picked classes' : 'drawn regions' }}.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectsClassVolumes\">\n Selects inside the volume of {{ pickedClasses[0]\n }}{{ pickedClasses.length > 1 ? ' and ' + (pickedClasses.length - 1 | number) + ' more classes' : '' }},\n picked in the Region Editor. Draw a region to select by it instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes && pickedClasses.length\">\n The classes picked in the Region Editor are not on the reference volume shown here, so this selects by\n the regions drawn over the cloud instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes\">\n In 3D a region cuts through the cloud's full depth. Orbit and draw again to cut from another angle.\n <ng-container *ngIf=\"!pickedClasses.length\">\n Pick classes in the Region Editor's class tree to select inside their volumes.\n </ng-container>\n </small>\n </div>\n </div>\n\n <!-- Observations, then what else draws them: the gene map, the 3D scene, and the\n continuous-only knobs. -->\n <spatial-observations-panel\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selection]=\"selection\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n [is3d]=\"is3d\"\n [open]=\"open.observations\"\n (openChange)=\"setOpen('observations', $event)\"\n (categoryClicked)=\"selectCategory($event)\"\n ></spatial-observations-panel>\n\n <!-- Distributions of whatever the map is coloured by. In the SAME dialog as\n the colour controls on purpose: changing the gene and watching the\n distribution move is one action, and two floating windows made that\n harder to see rather than easier. -->\n <div class=\"sc-section sc-charts\">\n <button\n type=\"button\"\n class=\"sc-disclosure\"\n (click)=\"toggleCharts()\"\n [attr.aria-expanded]=\"chartsOpen\"\n [attr.aria-controls]=\"chartsBodyId\"\n >\n <i class=\"pi\" [class.pi-chevron-right]=\"!chartsOpen\" [class.pi-chevron-down]=\"chartsOpen\"></i>\n <span>Distribution</span>\n <small *ngIf=\"!chartsOpen && view.colorBy\">of {{ colorByLabel }}</small>\n </button>\n <!-- Kept MOUNTED when collapsed, only hidden: destroying it would throw\n away the chart kind and grouping the user picked. `active` is what\n stops it drawing while it has no size. -->\n <div [id]=\"chartsBodyId\" [hidden]=\"!chartsOpen\">\n <spatial-charts [active]=\"visible && chartsOpen\"></spatial-charts>\n </div>\n </div>\n\n <div class=\"sc-btns\">\n <p-button label=\"Reset\" size=\"small\" [text]=\"true\" (onClick)=\"reset()\"></p-button>\n </div>\n </div>\n</p-dialog>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}:host ::ng-deep .spatial-controls-dialog .p-dialog-content{padding:.75rem 1rem 1rem}.sc-empty{padding:.5rem 0;font-size:.85rem;color:var(--text-color-secondary, #6c757d);line-height:1.4}.sc-empty code{font-size:.8rem}.sc-body{display:flex;flex-direction:column;gap:.85rem}.sc-dataset{display:flex;flex-direction:column;gap:.1rem;padding-bottom:.5rem;border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-dataset-name{font-weight:600;font-size:.9rem}.sc-dataset-count{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-btns{display:flex;justify-content:flex-end}.sc-charts{padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6);gap:0}.sc-disclosure{display:flex;align-items:center;gap:.4rem;width:100%;padding:.2rem 0;font:inherit;font-size:.82rem;font-weight:600;text-align:left;background:none;border:0;cursor:pointer;color:inherit}.sc-disclosure i{font-size:.7rem;color:var(--text-color-secondary, #6c757d)}.sc-disclosure small{font-weight:400;color:var(--text-color-secondary, #6c757d);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-disclosure:hover{color:var(--primary-color, #3b82f6)}\n"] }]
23996
24140
  }], ctorParameters: () => [{ type: undefined, decorators: [{
23997
24141
  type: Inject,
23998
24142
  args: [VISUALIZER]
@@ -28953,13 +29097,26 @@ class RegionHistory {
28953
29097
  * owner calls {@link sync} once the region array it projects onto is final,
28954
29098
  * which prunes ids that no longer exist and emits {@link indices$} only when
28955
29099
  * the index set changed.
29100
+ *
29101
+ * A selection made by picking classes (the class tree, a surface in 3D) also
29102
+ * holds those classes: a class is a volume through the whole stack, whether or
29103
+ * not it has a region on the slice. Any other selection drops them; an edit
29104
+ * ({@link retain}, {@link sync}) keeps them.
28956
29105
  */
28957
29106
  class RegionSelection {
28958
29107
  constructor() {
28959
29108
  this.selected = [];
29109
+ this.picked = [];
28960
29110
  this.indicesSubject = new BehaviorSubject([]);
29111
+ this.classesSubject = new BehaviorSubject([]);
28961
29112
  /** The selected regions' current array indices (emits on change only). */
28962
29113
  this.indices$ = this.indicesSubject.asObservable();
29114
+ /** The classes the selection was picked as ([] for any other selection; emits on change only). */
29115
+ this.classes$ = this.classesSubject.asObservable();
29116
+ }
29117
+ /** The classes the selection was picked as, as last emitted by {@link sync}. */
29118
+ get classes() {
29119
+ return this.classesSubject.value;
28963
29120
  }
28964
29121
  /** The selected indices as last projected by {@link sync}. */
28965
29122
  get indices() {
@@ -28969,9 +29126,10 @@ class RegionSelection {
28969
29126
  get ids() {
28970
29127
  return this.selected;
28971
29128
  }
28972
- /** Replace the selected ids (no emit until {@link sync}). */
28973
- replace(ids) {
29129
+ /** Replace the selected ids, picked as `classes` if they were (no emit until {@link sync}). */
29130
+ replace(ids, classes = []) {
28974
29131
  this.selected = ids.slice();
29132
+ this.picked = classes.slice();
28975
29133
  }
28976
29134
  /** Keep only the ids `keep` accepts (no emit until {@link sync}). */
28977
29135
  retain(keep) {
@@ -28994,10 +29152,11 @@ class RegionSelection {
28994
29152
  }
28995
29153
  }
28996
29154
  this.selected = ids;
29155
+ this.picked = [];
28997
29156
  this.sync(regions);
28998
29157
  }
28999
29158
  /** Project the selected ids onto `regions`, pruning ids it no longer holds,
29000
- * and emit if the index set changed. */
29159
+ * and emit if the index set (or the picked classes) changed. */
29001
29160
  sync(regions) {
29002
29161
  const indexOf = new Map();
29003
29162
  regions.forEach((r, i) => {
@@ -29016,6 +29175,8 @@ class RegionSelection {
29016
29175
  this.selected = live;
29017
29176
  if (!sameIndices(this.indicesSubject.value, indices))
29018
29177
  this.indicesSubject.next(indices);
29178
+ if (!sameIndices(this.classesSubject.value, this.picked))
29179
+ this.classesSubject.next(this.picked);
29019
29180
  }
29020
29181
  }
29021
29182
  function sameIndices(a, b) {
@@ -30879,7 +31040,8 @@ class RegionStore {
30879
31040
  /**
30880
31041
  * Select every region of these classes on the slice — the regions', and the region source's,
30881
31042
  * which are taken into the regions (at most {@link SELECT_CLASSES_LIMIT} of them). Added to the
30882
- * selection with `additive`. Resolves the selected region ids.
31043
+ * selection with `additive`. The selection holds the classes too (see {@link getPickedClasses}).
31044
+ * Resolves the selected region ids.
30883
31045
  */
30884
31046
  async selectClasses(classes, additive = false) {
30885
31047
  const wanted = new Set(classes);
@@ -30895,10 +31057,28 @@ class RegionStore {
30895
31057
  await this.takeSourceFeatures(page.rows.map((r) => r.id));
30896
31058
  }
30897
31059
  const ids = this.regions.filter((r) => !r.isProfile?.() && r.label && wanted.has(r.label)).map((r) => r.id);
30898
- this.selection.replace(additive ? [...new Set([...this.selection.ids, ...ids])] : ids);
31060
+ this.selection.replace(additive ? [...new Set([...this.selection.ids, ...ids])] : ids, additive ? [...new Set([...this.selection.classes, ...classes])] : classes);
30899
31061
  this.emitSelection();
30900
31062
  return ids;
30901
31063
  }
31064
+ /**
31065
+ * The classes the selection was picked as by {@link selectClasses} (a class tree node with its
31066
+ * subclasses, a surface clicked in 3D); [] once anything else selected. Each is a volume through
31067
+ * the whole stack, whether or not it has a region on the slice.
31068
+ */
31069
+ getPickedClasses() {
31070
+ return this.selection.classes;
31071
+ }
31072
+ getPickedClasses$() {
31073
+ return this.selection.classes$;
31074
+ }
31075
+ /**
31076
+ * Every slice's exact outlines of these classes, the region sources' and the regions' (as the
31077
+ * 3D reconstruction reads them at the stack's own resolution): what is inside their volume.
31078
+ */
31079
+ classSliceOutlines(classes) {
31080
+ return this.classOutlines(classes, 0);
31081
+ }
30902
31082
  /**
30903
31083
  * The 3D reconstruction of each group of classes over every slice of the stack (an atlas
30904
31084
  * structure with its substructures, say): the outlines of the regions of those classes on every
@@ -38144,9 +38324,11 @@ class SpatialControlsFacade {
38144
38324
  constructor(port, ctx) {
38145
38325
  this.port = port;
38146
38326
  this.ctx = ctx;
38147
- /** Latest dataset, mirrored so `selectFromRegions()` can answer synchronously — it
38148
- * runs from a button click and must not await a round-trip. */
38327
+ /** Latest dataset, mirrored so `selectFromRegions()` answers from the dataset on
38328
+ * screen — it runs from a button click and must not await a round-trip. */
38149
38329
  this.dataset = null;
38330
+ /** Bumped per class-volume selection (and on dispose): only the latest may publish. */
38331
+ this.roiRequest = 0;
38150
38332
  // Mirror the dataset (and drop a stale selection when it changes — the masks are
38151
38333
  // index-based, so they are meaningless against different observations).
38152
38334
  this.sub = port.getDataset$().subscribe((dataset) => {
@@ -38167,6 +38349,23 @@ class SpatialControlsFacade {
38167
38349
  /** Release the port subscription (the router's injector is being destroyed). */
38168
38350
  dispose() {
38169
38351
  this.sub.unsubscribe();
38352
+ // A class-volume selection still reading its outlines publishes nothing.
38353
+ this.roiRequest++;
38354
+ }
38355
+ /**
38356
+ * How "Select from ROIs" selects now — the one rule its executor and the panel's description
38357
+ * share: inside the picked classes' volumes in the 3D cloud, when the dataset has a reference
38358
+ * volume (and no tissue image) and the plotted image is that volume's stack, so the regions sit
38359
+ * in its grid; by the drawn regions otherwise.
38360
+ */
38361
+ roiSelectionMode() {
38362
+ const dataset = this.dataset;
38363
+ const eligible = !!dataset?.volume &&
38364
+ !dataset.imageRef &&
38365
+ isSpatialOmics3d(this.ctx.plotType()) &&
38366
+ this.ctx.regionStore.getPickedClasses().length > 0 &&
38367
+ this.ctx.napari.regionVoxelStep() !== null;
38368
+ return eligible ? 'class-volumes' : 'regions';
38170
38369
  }
38171
38370
  build() {
38172
38371
  const port = this.port;
@@ -38261,31 +38460,62 @@ class SpatialControlsFacade {
38261
38460
  return obs ? sectionsOf(obs) : null;
38262
38461
  },
38263
38462
  getSelection$: () => this.ctx.selectionStore.getSelection$(),
38264
- selectFromRegions: () => {
38463
+ selectFromRegions: async () => {
38265
38464
  const dataset = this.dataset;
38266
38465
  if (!dataset)
38267
38466
  return 0;
38268
- const regions = this.ctx.regionStore.getRegions();
38269
- // The union of every drawn region — so every ROI tool the library
38270
- // already has doubles as a spatial selection tool.
38467
+ const is3d = isSpatialOmics3d(this.ctx.plotType());
38468
+ const volume = !dataset.imageRef ? dataset.volume : undefined;
38469
+ // In the 3D cloud, region classes picked in the Region Editor (a class
38470
+ // tree node with its subclasses, a surface clicked in 3D) select the
38471
+ // cells inside their volumes — the structures shown among the cells.
38472
+ if (volume && this.roiSelectionMode() === 'class-volumes') {
38473
+ const classes = this.ctx.regionStore.getPickedClasses();
38474
+ const request = ++this.roiRequest;
38475
+ const revision = this.ctx.selectionStore.revision();
38476
+ const outlines = await this.ctx.regionStore.classSliceOutlines(classes);
38477
+ // Superseded while the outlines were read: a newer request, any newer selection
38478
+ // (a Clear, a legend click, a linked chart's brush), another dataset, view or pick,
38479
+ // or the facade disposed. Its mask would overwrite what the user did since.
38480
+ const current = this.ctx.regionStore.getPickedClasses();
38481
+ if (request !== this.roiRequest ||
38482
+ revision !== this.ctx.selectionStore.revision() ||
38483
+ this.dataset !== dataset ||
38484
+ this.roiSelectionMode() !== 'class-volumes' ||
38485
+ current.length !== classes.length ||
38486
+ current.some((c, i) => c !== classes[i])) {
38487
+ return null;
38488
+ }
38489
+ const selection = selectInClassVolumes(dataset.observations, volume, outlines);
38490
+ this.ctx.selectionStore.set(selection);
38491
+ return selection.count;
38492
+ }
38493
+ // Otherwise the union of every drawn region — so every ROI tool the
38494
+ // library already has doubles as a spatial selection tool.
38271
38495
  //
38272
38496
  // In the 3D cloud there is no data-space affine to push a drawn shape
38273
38497
  // through, so the observations are projected to canvas pixels by the
38274
- // renderer (which owns the camera) and tested in screen space instead.
38275
- // Falls back to the 2D path whenever the cloud is not mounted.
38276
- const projected = isSpatialOmics3d(this.ctx.plotType())
38277
- ? this.ctx.napari.getSpatialScreenProjection(dataset.observations)
38278
- : null;
38279
- // In the 2D view of a volume-backed dataset the shape was drawn over ONE
38280
- // section, in the volume's pixel grid: it selects the cells of that
38281
- // section, not the whole depth of brain standing behind them.
38282
- const volume = !dataset.imageRef ? dataset.volume : undefined;
38283
- const selection = projected
38284
- ? selectInRegionsProjected(projected, dataset.observations.count, regions)
38285
- : selectInRegions(dataset.observations, volume ? volumeImageRef(volume, dataset.micronsPerUnit) : dataset.imageRef, regions, volume ? observationsInSlice(dataset.observations, volume, this.ctx.zIndex()) : undefined);
38498
+ // renderer (which owns the camera) and tested in screen space instead —
38499
+ // against the lassos drawn there only: every other region is in image
38500
+ // space. Falls back to the 2D path whenever the cloud is not mounted.
38501
+ const projected = is3d ? this.ctx.napari.getSpatialScreenProjection(dataset.observations) : null;
38502
+ let selection;
38503
+ if (projected) {
38504
+ const lassos = new Set(this.ctx.napari.getSpatialLassoIds());
38505
+ const regions = this.ctx.regionStore.getRegions().filter((r) => lassos.has(r.id));
38506
+ selection = selectInRegionsProjected(projected, dataset.observations.count, regions);
38507
+ }
38508
+ else {
38509
+ // In the 2D view of a volume-backed dataset the shape was drawn over ONE
38510
+ // section, in the volume's pixel grid: it selects the cells of that
38511
+ // section, not the whole depth of brain standing behind them.
38512
+ selection = selectInRegions(dataset.observations, volume ? volumeImageRef(volume, dataset.micronsPerUnit) : dataset.imageRef, this.ctx.regionStore.getRegions(), volume ? observationsInSlice(dataset.observations, volume, this.ctx.zIndex()) : undefined);
38513
+ }
38286
38514
  this.ctx.selectionStore.set(selection);
38287
38515
  return selection.count;
38288
38516
  },
38517
+ roiSelectionMode: () => this.roiSelectionMode(),
38518
+ getPickedRegionClasses$: () => this.ctx.regionStore.getPickedClasses$(),
38289
38519
  selectCategory: async (column, categoryIndex) => {
38290
38520
  const loaded = await port.getColumn(column);
38291
38521
  if (!isCategoricalColumn(loaded)) {
@@ -39235,16 +39465,21 @@ const HANDLE_SIZE = 7;
39235
39465
  * the draft being drawn and the rubber-band marquee.
39236
39466
  */
39237
39467
  class NapariRegionSvgRenderer {
39238
- constructor(svg, store) {
39468
+ /**
39469
+ * @param draws which regions are this overlay's to draw (default: every one); the others are
39470
+ * left out of the shapes and handles, keeping the store's indices
39471
+ */
39472
+ constructor(svg, store, draws = () => true) {
39239
39473
  this.svg = svg;
39240
39474
  this.store = store;
39475
+ this.draws = draws;
39241
39476
  /** Dedicated marquee rect element, updated directly during the drag (no full region redraw). */
39242
39477
  this.marqueeEl = null;
39243
39478
  this.renderer = new SvgRegionRenderer(svg, {
39244
39479
  rectElement: 'rect',
39245
39480
  styleShape: (el, region, selected) => this.style(el, this.strokeOf(region), selected),
39246
39481
  decorate: (region, _i, _selected, layer) => this.drawLabel(region, layer),
39247
- skip: (region) => !!region.isProfile?.() || store.isClassHidden(region.label),
39482
+ skip: (region) => !!region.isProfile?.() || store.isClassHidden(region.label) || !draws(region),
39248
39483
  });
39249
39484
  }
39250
39485
  /** Follow the camera: one transform attribute plus the screen-space elements. */
@@ -39260,7 +39495,7 @@ class NapariRegionSvgRenderer {
39260
39495
  this.renderer.renderOverlay((layer) => {
39261
39496
  for (const i of selected) {
39262
39497
  const region = regions[i];
39263
- if (region && !region.isProfile?.())
39498
+ if (region && !region.isProfile?.() && this.draws(region))
39264
39499
  this.drawHandles(region, layer);
39265
39500
  }
39266
39501
  this.drawDraft(draft, layer);
@@ -39802,6 +40037,7 @@ class NapariRegionOverlay {
39802
40037
  this.commitPolygon(pts, true);
39803
40038
  this.redraw();
39804
40039
  };
40040
+ this.own = opts.screenSpace ? new Set() : null;
39805
40041
  if (getComputedStyle(host).position === 'static')
39806
40042
  host.style.position = 'relative';
39807
40043
  this.svg = document.createElementNS(SVG_NS, 'svg');
@@ -39815,7 +40051,7 @@ class NapariRegionOverlay {
39815
40051
  touchAction: 'none',
39816
40052
  });
39817
40053
  this.host.appendChild(this.svg);
39818
- this.renderer = new NapariRegionSvgRenderer(this.svg, this.store);
40054
+ this.renderer = new NapariRegionSvgRenderer(this.svg, this.store, (r) => this.draws(r));
39819
40055
  this.projection = new OverlayProjection(this.svg, this.viewer);
39820
40056
  this.sourceLayer =
39821
40057
  opts.regionSource === false ? null : new RegionSourceLayer(this.svg, this.store.getRegionSourceView$());
@@ -39948,11 +40184,20 @@ class NapariRegionOverlay {
39948
40184
  this.draftPath.push([ix, iy]);
39949
40185
  }
39950
40186
  }
39951
- /** The currently-selected region (single selection), or null. */
40187
+ /** The currently-selected region (single selection) this overlay draws, or null. */
39952
40188
  selectedRegion() {
39953
40189
  const regions = this.store.getRegions();
39954
40190
  const i = this.selected[0];
39955
- return i != null && i >= 0 && i < regions.length ? regions[i] : null;
40191
+ const region = i != null && i >= 0 && i < regions.length ? regions[i] : null;
40192
+ return region && this.draws(region) ? region : null;
40193
+ }
40194
+ /** Whether this overlay draws (and lets the pointer pick) `region`: see {@link own}. */
40195
+ draws(region) {
40196
+ return !this.own || this.own.has(region.id);
40197
+ }
40198
+ /** The regions this overlay drew in screen space (the 3D view's lassos); [] in image space. */
40199
+ ownRegionIds() {
40200
+ return this.own ? [...this.own] : [];
39956
40201
  }
39957
40202
  /**
39958
40203
  * Start a select/move interaction: grab a handle (rectangle corner) or vertex of the already-
@@ -39975,7 +40220,7 @@ class NapariRegionOverlay {
39975
40220
  const regions = this.store.getRegions();
39976
40221
  const [wx, wy] = this.projection.toWorld(e.clientX, e.clientY);
39977
40222
  for (let i = regions.length - 1; i >= 0; i--) {
39978
- if (this.store.isClassHidden(regions[i].label))
40223
+ if (this.store.isClassHidden(regions[i].label) || !this.draws(regions[i]))
39979
40224
  continue;
39980
40225
  if (regionContains(regions[i], wx, wy, { toScreen: this.projection.toScreen, tolPx: OPEN_PATH_HIT_PX })) {
39981
40226
  this.store.selectRegion(regions[i]);
@@ -40056,7 +40301,7 @@ class NapariRegionOverlay {
40056
40301
  const regions = this.store.getRegions();
40057
40302
  this.store.setSelectedShapeIndices(regionsInRect(regions, x0, y0, x1, y1, {
40058
40303
  skipProfiles: true,
40059
- skip: (r) => this.store.isClassHidden(r.label),
40304
+ skip: (r) => this.store.isClassHidden(r.label) || !this.draws(r),
40060
40305
  }));
40061
40306
  const features = this.sourceLayer?.featuresInRect(x0, y0, x1, y1) ?? [];
40062
40307
  if (features.length) {
@@ -40074,6 +40319,7 @@ class NapariRegionOverlay {
40074
40319
  region.bounds = rect;
40075
40320
  region.color = this.store.getShapeColor();
40076
40321
  this.store.addRegion(region);
40322
+ this.own?.add(region.id);
40077
40323
  }
40078
40324
  commitPolygon(points, closed) {
40079
40325
  const poly = new Polygon();
@@ -40087,6 +40333,7 @@ class NapariRegionOverlay {
40087
40333
  region.color = this.store.getShapeColor();
40088
40334
  region.label = 'Region';
40089
40335
  this.store.addRegion(region);
40336
+ this.own?.add(region.id);
40090
40337
  }
40091
40338
  // ── rendering ─────────────────────────────────────────────────────────────
40092
40339
  /** Show or hide the drawn regions. A region being drawn stays visible either way. */
@@ -40228,27 +40475,38 @@ class NapariToolBridge {
40228
40475
  * A screen-space shape stops meaning anything the moment the camera moves, so an orbit clears
40229
40476
  * the regions drawn in this view (and only those). The SELECTION they produced is kept: that is
40230
40477
  * the durable artefact, and the highlighted points stay highlighted from every angle.
40478
+ *
40479
+ * Every other region in the store is in image space - the image's own, an imported set,
40480
+ * structures a tree click took from a region source - so the overlay neither draws nor picks it
40481
+ * (it would lie flat at canvas pixels over the cloud), and an orbit leaves it alone: they show
40482
+ * in 3D as their reconstructions instead.
40231
40483
  */
40232
40484
  install3dInteraction(viewer, host) {
40233
40485
  const { regionStore } = this.deps;
40234
40486
  // World here is the canvas, not the image: no region source to draw.
40235
- this.regionOverlay = new NapariRegionOverlay(host, this.screenSpaceViewer(viewer), regionStore, {
40487
+ const overlay = new NapariRegionOverlay(host, this.screenSpaceViewer(viewer), regionStore, {
40236
40488
  regionSource: false,
40489
+ screenSpace: true,
40237
40490
  });
40491
+ this.regionOverlay = overlay;
40238
40492
  this.buildCoordinateTransform();
40239
- // Regions already in the shared store when the 3D view mounted (the image's own, say) are
40240
- // not screen-space shapes and must survive an orbit; only those drawn here are cleared.
40241
40493
  // (No mid-gesture case to guard: the overlay disables the camera controls while drawing.)
40242
- const foreign = new Set(regionStore.getRegions().map((r) => r.id));
40243
- this.cameraOff = viewer.camera3d.changed.connect(() => {
40244
- const drawn = regionStore
40245
- .getRegions()
40246
- .filter((r) => !foreign.has(r.id))
40247
- .map((r) => r.id);
40248
- // Not an edit of the user's: no undo step (NAPARI-SVC-11).
40249
- if (drawn.length)
40250
- this.host.inZone(() => regionStore.removeRegions(drawn, { recordUndo: false }));
40251
- });
40494
+ this.cameraOff = viewer.camera3d.changed.connect(() => this.clearOwnLassos());
40495
+ }
40496
+ /**
40497
+ * Remove the regions the 3D overlay drew in screen space (only those) from the store: a lasso
40498
+ * means nothing once the camera it was drawn under moves or the scene goes. Not an edit of the
40499
+ * user's: no undo step (NAPARI-SVC-11).
40500
+ */
40501
+ clearOwnLassos() {
40502
+ const overlay = this.regionOverlay;
40503
+ if (!overlay)
40504
+ return;
40505
+ const { regionStore } = this.deps;
40506
+ const present = new Set(regionStore.getRegions().map((r) => r.id));
40507
+ const drawn = overlay.ownRegionIds().filter((id) => present.has(id));
40508
+ if (drawn.length)
40509
+ this.host.inZone(() => regionStore.removeRegions(drawn, { recordUndo: false }));
40252
40510
  }
40253
40511
  /**
40254
40512
  * The {@link OverlayViewer} the 3D region overlay draws through: "world" is the canvas, in
@@ -40270,6 +40528,10 @@ class NapariToolBridge {
40270
40528
  /** End the scene's interaction: the overlay, the camera hook, the pending timers and the
40271
40529
  * pixels read from the old canvas. The armed tool and the pixel-tool set persist. */
40272
40530
  teardown() {
40531
+ // The overlay's record of its lassos goes with it: remove them first, or they would stay in
40532
+ // the store unowned - hidden by the next 3D overlay, beyond an orbit's reach, and still
40533
+ // feeding "Select from ROIs" with screen coordinates of a camera that is gone.
40534
+ this.clearOwnLassos();
40273
40535
  this.regionOverlay?.destroy();
40274
40536
  this.regionOverlay = null;
40275
40537
  this.cachedImage = null;
@@ -46743,6 +47005,11 @@ class NapariVisualizerService extends BaseStoreVisualizer {
46743
47005
  getSpatialScreenProjection(obs) {
46744
47006
  return this.scene?.screenProjection?.(obs) ?? null;
46745
47007
  }
47008
+ /** The ids of the regions drawn over the 3D cloud in screen space (its lassos); [] in 2D. Every
47009
+ * other region in the store is in image space, and means nothing at canvas pixels. */
47010
+ getSpatialLassoIds() {
47011
+ return this.tools.regionOverlay?.ownRegionIds() ?? [];
47012
+ }
46746
47013
  fitCameraSoon() {
46747
47014
  const run = () => {
46748
47015
  if (this.viewer && this.canvas && this.imageW > 0 && this.imageH > 0) {
@@ -48522,6 +48789,9 @@ class RoutingVisualizerService {
48522
48789
  async selectRegionClasses(classes, additive) {
48523
48790
  await this.regionStore.selectClasses(classes, additive);
48524
48791
  }
48792
+ getPickedRegionClasses$() {
48793
+ return this.regionStore.getPickedClasses$();
48794
+ }
48525
48795
  getGeoJsonStringWithSource(regions, slice) {
48526
48796
  return this.regionStore.getGeoJsonWithSource(regions, slice);
48527
48797
  }
@@ -49027,4 +49297,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
49027
49297
  */
49028
49298
 
49029
49299
  export { TILE_ACCESS_PORT as $, ALL_GENES as A, ProcessingImage as B, CANVAS_TOOL_IDS as C, DEFAULT_CATEGORICAL_PALETTE as D, REGION_IO_PORT as E, REGION_SOURCE_MIN_CHARS as F, Rectangle as G, HexColorPickerComponent as H, IMAGE_STATE_PORT as I, Region as J, RegionEditorComponent as K, LUT_COLORS as L, MISSING_COLOR as M, NAPARI_DECIMATE_OPTIONS as N, RoutingVisualizerService as O, PLOT_MODE_CONTEXT as P, SEMANTIC_SEGMENTER as Q, REGION_EDITOR_API as R, SAM_MODELS as S, SPATIAL_DATA_PORT as T, SPATIAL_WIRE_VERSION as U, ShapeSelection as V, SpatialChartsComponent as W, SpatialControlsComponent as X, SpatialDataHttpService as Y, SpatialSelectionStore as Z, SupersededError as _, runDecoder as a, openGeoJsonRegionSource as a$, TOOLBAR_TOOLS as a0, VISUALIZER as a1, VIZ_ALERT_TOAST_KEY as a2, VIZ_CONFIG as a3, VIZ_TOAST_KEY as a4, ViewerFeature as a5, VisualizationModule as a6, VisualizerComponent as a7, assertManifestVersion as a8, benefitsFromGrouping as a9, isBuiltinPlotType as aA, isCanvasToolId as aB, isCategoricalColumn as aC, isContinuousColumn as aD, isDialogToolContribution as aE, isEmptyPresetSet as aF, isLabelVolumeName as aG, isLittleEndian as aH, isNapari3d as aI, isNapariIsosurface as aJ, isNapariScatter as aK, isNapariScatter3d as aL, isNapariSurface as aM, isNapariVolume as aN, isSamModelReady as aO, isSpatialOmics as aP, isSpatialOmics3d as aQ, isThreeDimensional as aR, isZipName as aS, labelClasses as aT, labelSliceGeoJson as aU, lutFor as aV, markerDiameters as aW, maskToIndices as aX, mutedFromSelection as aY, omicsLayout as aZ, openAnnotations as a_, buildOmicsTraces as aa, capabilitiesOf as ab, clearSamModelCache as ac, contrastWindow as ad, contributedPlotTypeOption as ae, countMask as af, cropImage as ag, datasetFromManifest as ah, decodeColumn as ai, decodeCoords as aj, decodeFeatureVector as ak, decodePolygons as al, decodeRadius as am, defaultPresetSet as an, dialogToolContributions as ao, emptySelection as ap, encodeCategorical as aq, encodeContinuous as ar, expandZips as as, findColumnMeta as at, getDefaultSamModelId as au, getPlotTypeDescriptor as av, getSamModel as aw, hasZarrMetadata as ax, hydrateBounds as ay, importFile as az, ALL_TOOLBAR_TOOLS as b, openHttpRegionSource as b0, parsePresetSet as b1, planRegionImport as b2, pointInRing as b3, provideVisualization as b4, readAnnotationSlices as b5, readLabelVolume as b6, readOmeZarrLabels as b7, readZip as b8, rendererOwnsWheel as b9, resolveCategoryColors as ba, selectByCategory as bb, selectInRegions as bc, setDefaultSamModel as bd, setOrtWasmBase as be, setSamModelUrls as bf, sortToolContributions as bg, visibleToolContributions as bh, ANNOTATION_IMAGE_MAX_DIM as c, CELL_SEGMENTER as d, CHANNEL_HISTOGRAM_API as e, fetchModel as f, getOrtWasmBase as g, CellposeSegmenterService as h, ChannelHistogramComponent as i, DEFAULT_FALLBACK_PALETTE as j, DEFAULT_MUTED_OPACITY as k, DEFAULT_SAM_MODEL_ID as l, DEFAULT_SPATIAL_VIEW as m, INSTANCE_SEGMENTER as n, ImageConverterService as o, MultiPolygon as p, NAPARI_DEFAULT_DECIMATE as q, runEncoder as r, NO_CATEGORY as s, NO_OBSERVATION as t, PLOT_MODE_SESSION as u, PLOT_TYPE_CONTRIBUTIONS as v, PLOT_TYPE_DESCRIPTORS as w, PREFERENCES_PORT as x, PlotType as y, Polygon as z };
49030
- //# sourceMappingURL=jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-a68Hw5rw.mjs.map
49300
+ //# sourceMappingURL=jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DURPqs5a.mjs.map