@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.
- package/esm2022/lib/contracts/region-editor-api.contract.mjs +1 -1
- package/esm2022/lib/contracts/visualizer.contract.mjs +1 -1
- package/esm2022/lib/geometry/reconstruct.mjs +11 -1
- package/esm2022/lib/implementations/napari-js/napari-region-overlay.mjs +18 -6
- package/esm2022/lib/implementations/napari-js/napari-tool-bridge.mjs +29 -14
- package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +6 -1
- package/esm2022/lib/implementations/napari-js/region-overlay/region-svg-renderer.mjs +9 -4
- package/esm2022/lib/region-editor/region-editor.component.mjs +13 -1
- package/esm2022/lib/routing-visualizer.service.mjs +4 -1
- package/esm2022/lib/spatial/spatial-controls.facade.mjs +71 -21
- package/esm2022/lib/spatial/spatial-selection.mjs +70 -1
- package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +53 -11
- package/esm2022/lib/store/region-selection.mjs +21 -4
- package/esm2022/lib/store/region-store.service.mjs +22 -3
- package/esm2022/lib/store/spatial-selection.service.mjs +13 -1
- package/esm2022/lib/toolbar/toolbar-help-dialog/toolbar-help-dialog.component.mjs +3 -3
- 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
- package/fesm2022/jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DURPqs5a.mjs.map +1 -0
- 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
- 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
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs +2 -2
- package/lib/contracts/region-editor-api.contract.d.ts +3 -0
- package/lib/contracts/visualizer.contract.d.ts +25 -2
- package/lib/geometry/reconstruct.d.ts +6 -0
- package/lib/implementations/napari-js/napari-region-overlay.d.ts +13 -1
- package/lib/implementations/napari-js/napari-tool-bridge.d.ts +11 -0
- package/lib/implementations/napari-js/napari-visualizer.service.d.ts +3 -0
- package/lib/implementations/napari-js/region-overlay/region-svg-renderer.d.ts +6 -1
- package/lib/routing-visualizer.service.d.ts +1 -0
- package/lib/spatial/spatial-controls.facade.d.ts +16 -2
- package/lib/spatial/spatial-selection.d.ts +16 -1
- package/lib/spatial-controls/spatial-controls.component.d.ts +14 -3
- package/lib/store/region-selection.d.ts +14 -3
- package/lib/store/region-store.service.d.ts +14 -1
- package/lib/store/spatial-selection.service.d.ts +8 -0
- package/package.json +1 -1
- 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-
|
|
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 & 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 & 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 px), <b>Medium</b> (3 px), <b>Strong</b> (8 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 & 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&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 & 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 & 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 px), <b>Medium</b> (3 px), <b>Strong</b> (8 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 & 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&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 & 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 & 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 px), <b>Medium</b> (3 px), <b>Strong</b> (8 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 & 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&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 & 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 & 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 px), <b>Medium</b> (3 px), <b>Strong</b> (8 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 & 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&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
|
-
|
|
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
|
-
|
|
23886
|
-
|
|
23887
|
-
this.
|
|
23888
|
-
|
|
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 & 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 & 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 & 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 & 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`.
|
|
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()`
|
|
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
|
|
38269
|
-
|
|
38270
|
-
//
|
|
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
|
-
//
|
|
38276
|
-
|
|
38277
|
-
|
|
38278
|
-
|
|
38279
|
-
|
|
38280
|
-
|
|
38281
|
-
|
|
38282
|
-
|
|
38283
|
-
|
|
38284
|
-
|
|
38285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40243
|
-
|
|
40244
|
-
|
|
40245
|
-
|
|
40246
|
-
|
|
40247
|
-
|
|
40248
|
-
|
|
40249
|
-
|
|
40250
|
-
|
|
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-
|
|
49300
|
+
//# sourceMappingURL=jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DURPqs5a.mjs.map
|