@jax-data-science/sci-image-visualizer 0.4.0 → 0.5.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/README.md +60 -0
- package/esm2022/index.mjs +2 -1
- package/esm2022/lib/contracts/plot-type-contribution.contract.mjs +66 -0
- package/esm2022/lib/contracts/plot-type.mjs +6 -1
- package/esm2022/lib/contracts/visualizer.contract.mjs +1 -1
- package/esm2022/lib/implementations/osd/openseadragon-visualizer.service.mjs +69 -6
- package/esm2022/lib/implementations/osd/osd-coordinate-transform.mjs +8 -1
- package/esm2022/lib/plot-mode/plot-mode-controller.mjs +329 -0
- package/esm2022/lib/routing-visualizer.service.mjs +6 -1
- package/esm2022/lib/toolbar/toolbar.component.mjs +37 -20
- package/esm2022/lib/visualizer.component.mjs +262 -40
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs +772 -62
- package/fesm2022/jax-data-science-sci-image-visualizer.mjs.map +1 -1
- package/index.d.ts +1 -0
- package/lib/contracts/plot-type-contribution.contract.d.ts +172 -0
- package/lib/contracts/plot-type.d.ts +10 -0
- package/lib/contracts/visualizer.contract.d.ts +5 -0
- package/lib/implementations/osd/openseadragon-visualizer.service.d.ts +21 -0
- package/lib/implementations/osd/osd-coordinate-transform.d.ts +6 -0
- package/lib/plot-mode/plot-mode-controller.d.ts +130 -0
- package/lib/routing-visualizer.service.d.ts +4 -0
- package/lib/toolbar/toolbar.component.d.ts +18 -6
- package/lib/visualizer.component.d.ts +68 -7
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, inject, Optional, Inject, EventEmitter, Output, Input, Component, HostListener, ViewChild, ChangeDetectionStrategy, NgModule } from '@angular/core';
|
|
2
|
+
import { InjectionToken, Injectable, inject, Optional, Inject, EventEmitter, Output, Input, Component, HostListener, ViewChild, ChangeDetectionStrategy, Injector, NgModule } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/common/http';
|
|
4
|
-
import { BehaviorSubject, firstValueFrom, Subject, Subscription, combineLatest, EMPTY, of, from } from 'rxjs';
|
|
5
|
-
import { timeout, debounceTime, switchMap, takeUntil, map } from 'rxjs/operators';
|
|
4
|
+
import { BehaviorSubject, firstValueFrom, Subject, Subscription, combineLatest, EMPTY, of, from, defer } from 'rxjs';
|
|
5
|
+
import { timeout, debounceTime, switchMap, takeUntil, startWith, map } from 'rxjs/operators';
|
|
6
6
|
import * as i1$1 from '@angular/common';
|
|
7
7
|
import { CommonModule } from '@angular/common';
|
|
8
8
|
import * as i2 from '@angular/forms';
|
|
@@ -198,6 +198,11 @@ var PlotType;
|
|
|
198
198
|
* {@link SPATIAL_OMICS}: a single-plane dataset has no z to render. */
|
|
199
199
|
PlotType["SPATIAL_OMICS_3D"] = "spatial-omics-3d";
|
|
200
200
|
})(PlotType || (PlotType = {}));
|
|
201
|
+
const BUILTIN_PLOT_TYPES = new Set(Object.values(PlotType));
|
|
202
|
+
/** Whether an id names a built-in plot type (as opposed to a contributed one). */
|
|
203
|
+
function isBuiltinPlotType(id) {
|
|
204
|
+
return typeof id === 'string' && BUILTIN_PLOT_TYPES.has(id);
|
|
205
|
+
}
|
|
201
206
|
/**
|
|
202
207
|
* Single source of truth for plot-type metadata. LINE is intentionally absent —
|
|
203
208
|
* intensity profiles are now Region-based line ROIs (available in Heatmap/Image
|
|
@@ -370,6 +375,71 @@ function visibleToolContributions(tools) {
|
|
|
370
375
|
return sortToolContributions(tools ?? []).filter((t) => t.models().length > 0);
|
|
371
376
|
}
|
|
372
377
|
|
|
378
|
+
/**
|
|
379
|
+
* How a plot mode gets into the plot-type selector without this library knowing
|
|
380
|
+
* what it is.
|
|
381
|
+
*
|
|
382
|
+
* A package contributes a mode by providing a {@link PlotTypeContribution} on the
|
|
383
|
+
* {@link PLOT_TYPE_CONTRIBUTIONS} multi-provider token, the same way
|
|
384
|
+
* `TOOLBAR_TOOLS` contributes toolbar tools. Nothing registers at import time:
|
|
385
|
+
* installing a package makes a mode available, and adding its provider turns it
|
|
386
|
+
* on. A plain object works, so the contributing package needs no Angular
|
|
387
|
+
* compiler and no decorators:
|
|
388
|
+
*
|
|
389
|
+
* { provide: PLOT_TYPE_CONTRIBUTIONS, useValue: myContribution, multi: true }
|
|
390
|
+
* { provide: PLOT_TYPE_CONTRIBUTIONS, useFactory: () => makeContribution(), multi: true }
|
|
391
|
+
*
|
|
392
|
+
* Provide nothing and the selector, the render path and the panel area behave
|
|
393
|
+
* exactly as they did before this extension point existed.
|
|
394
|
+
*
|
|
395
|
+
* WHY A CONTRIBUTED MODE RIDES ON A BUILT-IN ONE
|
|
396
|
+
* A contributed mode does not bring a renderer. It names a built-in
|
|
397
|
+
* {@link ContributedPlotTypeDescriptor.baseType} — in v1 only `PlotType.IMAGE`,
|
|
398
|
+
* the OpenSeadragon view — and the visualizer plots exactly as if that type had
|
|
399
|
+
* been selected: same backend, same toolbar, same region tools, same wheel
|
|
400
|
+
* handling. The contribution then draws over it through the
|
|
401
|
+
* {@link PlotModeViewport} and talks to the viewer through the public
|
|
402
|
+
* {@link IVisualizer}. That keeps every rendering decision in one place and means
|
|
403
|
+
* a contribution can never be half-supported by some code path that switches on
|
|
404
|
+
* the plot type.
|
|
405
|
+
*
|
|
406
|
+
* WHAT THE VISUALIZER GUARANTEES
|
|
407
|
+
* 1. Contributed descriptors are appended after the built-in ones and curated by
|
|
408
|
+
* the same `productionLabel` / test-mode / `requires*` rules.
|
|
409
|
+
* 2. Selecting one plots with `baseType`'s backend, then calls `activate(ctx)`
|
|
410
|
+
* once the viewport is ready.
|
|
411
|
+
* 3. Leaving the mode — another type, an image switch (or any re-render of the
|
|
412
|
+
* base view), or the visualizer being destroyed — calls `session.deactivate()`
|
|
413
|
+
* exactly once, before the next mode draws. If the mode is still selected
|
|
414
|
+
* when the new image has plotted, a fresh session is activated for it.
|
|
415
|
+
* 4. `panel` is shown only while a session is live.
|
|
416
|
+
* 5. A thrown error or rejected promise from `activate`, `deactivate`, `mount`
|
|
417
|
+
* or a mount's teardown is caught and logged; none escapes, so a contribution
|
|
418
|
+
* cannot break the viewer. Failing to start (`activate`, `mount`, or a panel
|
|
419
|
+
* component throwing while it is created / first rendered) falls back
|
|
420
|
+
* to `baseType`. Failing to clean up falls back when that same mode is being
|
|
421
|
+
* re-activated (base re-render, image switch) — immediately if it already is
|
|
422
|
+
* live again — and is only logged when the user has moved to another mode; the
|
|
423
|
+
* user explicitly selecting it again gets a fresh attempt.
|
|
424
|
+
* 6. {@link PLOT_TYPE_CONTRIBUTIONS} has no factory: no providers, no modes.
|
|
425
|
+
*/
|
|
426
|
+
/**
|
|
427
|
+
* Multi-provider token for contributed plot modes.
|
|
428
|
+
*
|
|
429
|
+
* Deliberately has no `factory`: unregistered means no contributed modes, and the
|
|
430
|
+
* library behaves exactly as before. The visualizer injects it `{ optional: true }`
|
|
431
|
+
* and treats null as empty.
|
|
432
|
+
*/
|
|
433
|
+
const PLOT_TYPE_CONTRIBUTIONS = new InjectionToken('PLOT_TYPE_CONTRIBUTIONS');
|
|
434
|
+
/** The active mode's context, in a component panel's injector. */
|
|
435
|
+
const PLOT_MODE_CONTEXT = new InjectionToken('PLOT_MODE_CONTEXT');
|
|
436
|
+
/** The active mode's session, in a component panel's injector. */
|
|
437
|
+
const PLOT_MODE_SESSION = new InjectionToken('PLOT_MODE_SESSION');
|
|
438
|
+
/** The selector entry for a contributed mode, given its base type's descriptor. */
|
|
439
|
+
function contributedPlotTypeOption(d, base) {
|
|
440
|
+
return { ...d, source: base.source };
|
|
441
|
+
}
|
|
442
|
+
|
|
373
443
|
/**
|
|
374
444
|
* Optional features a viewer backend may or may not support. Consumers query
|
|
375
445
|
* `IImageViewer.capabilities` and degrade gracefully (hide a toolbar item,
|
|
@@ -5074,6 +5144,334 @@ class SliceScrubber {
|
|
|
5074
5144
|
}
|
|
5075
5145
|
}
|
|
5076
5146
|
|
|
5147
|
+
/** Base types a contributed mode can ride on in this version. */
|
|
5148
|
+
const SUPPORTED_CONTRIBUTION_BASE_TYPES = [PlotType.IMAGE];
|
|
5149
|
+
/** How many animation frames to wait for the viewport before giving up. */
|
|
5150
|
+
const MAX_READY_FRAMES = 180;
|
|
5151
|
+
const TAG = '[visualizer] plot-type contribution';
|
|
5152
|
+
/**
|
|
5153
|
+
* Validate what arrived on `PLOT_TYPE_CONTRIBUTIONS`. A malformed or clashing
|
|
5154
|
+
* contribution is dropped with a warning rather than breaking the selector:
|
|
5155
|
+
* - a nested array (`useValue: [a, b], multi: true`) is flattened one level;
|
|
5156
|
+
* - `type` must be a non-empty string that is not a built-in `PlotType` value;
|
|
5157
|
+
* - the first contribution with a given `type` wins;
|
|
5158
|
+
* - `baseType` must be one of {@link SUPPORTED_CONTRIBUTION_BASE_TYPES};
|
|
5159
|
+
* - `activate` must be a function.
|
|
5160
|
+
*/
|
|
5161
|
+
function normalizeContributions(raw, log = console) {
|
|
5162
|
+
const out = [];
|
|
5163
|
+
const seen = new Set();
|
|
5164
|
+
const flat = (raw ?? []).flatMap((c) => (Array.isArray(c) ? c : [c]));
|
|
5165
|
+
for (const c of flat) {
|
|
5166
|
+
const d = c?.descriptor;
|
|
5167
|
+
const id = d?.type;
|
|
5168
|
+
if (!d || typeof id !== 'string' || !id || typeof c.activate !== 'function') {
|
|
5169
|
+
log.warn(`${TAG} ignored: it needs a descriptor with a string \`type\` and an \`activate\` function.`, c);
|
|
5170
|
+
continue;
|
|
5171
|
+
}
|
|
5172
|
+
if (isBuiltinPlotType(id)) {
|
|
5173
|
+
log.warn(`${TAG} '${id}' ignored: the id clashes with a built-in plot type.`);
|
|
5174
|
+
continue;
|
|
5175
|
+
}
|
|
5176
|
+
if (seen.has(id)) {
|
|
5177
|
+
log.warn(`${TAG} '${id}' ignored: another contribution already uses that id.`);
|
|
5178
|
+
continue;
|
|
5179
|
+
}
|
|
5180
|
+
if (!SUPPORTED_CONTRIBUTION_BASE_TYPES.includes(d.baseType)) {
|
|
5181
|
+
log.warn(`${TAG} '${id}' ignored: baseType '${String(d.baseType)}' is not supported `
|
|
5182
|
+
+ `(supported: ${SUPPORTED_CONTRIBUTION_BASE_TYPES.join(', ')}).`);
|
|
5183
|
+
continue;
|
|
5184
|
+
}
|
|
5185
|
+
seen.add(id);
|
|
5186
|
+
out.push(c);
|
|
5187
|
+
}
|
|
5188
|
+
return out;
|
|
5189
|
+
}
|
|
5190
|
+
class PlotModeController {
|
|
5191
|
+
constructor(raw, hooks, log = console) {
|
|
5192
|
+
this.hooks = hooks;
|
|
5193
|
+
this.log = log;
|
|
5194
|
+
this.active = null;
|
|
5195
|
+
this.teardown = null;
|
|
5196
|
+
/** Bumped by every activate/deactivate, so a pending async activation can
|
|
5197
|
+
* tell it has been superseded. */
|
|
5198
|
+
this.generation = 0;
|
|
5199
|
+
this.readyFrame = null;
|
|
5200
|
+
/** Contributions whose last cleanup (panel teardown / `deactivate()`) threw or
|
|
5201
|
+
* rejected. Their next automatic re-activation (base re-render, image switch)
|
|
5202
|
+
* falls back to the base type instead; an explicit selection clears the mark. */
|
|
5203
|
+
this.failedCleanup = new Map();
|
|
5204
|
+
this.readyCb = null;
|
|
5205
|
+
this.pendingId = null;
|
|
5206
|
+
this.contributions = normalizeContributions(raw, log);
|
|
5207
|
+
this.byId = new Map(this.contributions.map((c) => [c.descriptor.type, c]));
|
|
5208
|
+
}
|
|
5209
|
+
/** Descriptors of the usable contributions, in registration order. */
|
|
5210
|
+
descriptors() {
|
|
5211
|
+
return this.contributions.map((c) => c.descriptor);
|
|
5212
|
+
}
|
|
5213
|
+
/** The contribution for a selector id, if it is a contributed mode. */
|
|
5214
|
+
find(id) {
|
|
5215
|
+
return typeof id === 'string' ? this.byId.get(id) : undefined;
|
|
5216
|
+
}
|
|
5217
|
+
/**
|
|
5218
|
+
* The built-in type that renders `id`: a built-in type is itself, a
|
|
5219
|
+
* contributed id is its `baseType`, and an id that is neither (a stale
|
|
5220
|
+
* contributed id whose provider is gone) resolves to `PlotType.IMAGE`, the
|
|
5221
|
+
* default view.
|
|
5222
|
+
*/
|
|
5223
|
+
baseTypeOf(id) {
|
|
5224
|
+
if (isBuiltinPlotType(id))
|
|
5225
|
+
return id;
|
|
5226
|
+
return this.find(id)?.descriptor.baseType ?? PlotType.IMAGE;
|
|
5227
|
+
}
|
|
5228
|
+
/** The live session, if any. */
|
|
5229
|
+
get current() {
|
|
5230
|
+
return this.active;
|
|
5231
|
+
}
|
|
5232
|
+
/** Whether an activation is waiting on the contribution or the viewport. */
|
|
5233
|
+
get pending() {
|
|
5234
|
+
return this.pendingId !== null;
|
|
5235
|
+
}
|
|
5236
|
+
/**
|
|
5237
|
+
* Start a session for `contribution`. Ends any live one first. Resolves when
|
|
5238
|
+
* the attempt has finished (successfully or not); it never rejects.
|
|
5239
|
+
*/
|
|
5240
|
+
activate(contribution, ctx) {
|
|
5241
|
+
this.deactivate();
|
|
5242
|
+
const id = contribution.descriptor.type;
|
|
5243
|
+
if (this.failedCleanup.has(id)) {
|
|
5244
|
+
// Its previous session did not end cleanly; do not stack a new one on top.
|
|
5245
|
+
const err = this.failedCleanup.get(id);
|
|
5246
|
+
this.failedCleanup.delete(id);
|
|
5247
|
+
this.fail(contribution, err);
|
|
5248
|
+
return Promise.resolve();
|
|
5249
|
+
}
|
|
5250
|
+
const gen = ++this.generation;
|
|
5251
|
+
this.pendingId = id;
|
|
5252
|
+
return new Promise((resolve) => {
|
|
5253
|
+
this.whenReady(ctx, gen, (ready) => {
|
|
5254
|
+
if (gen !== this.generation)
|
|
5255
|
+
return resolve();
|
|
5256
|
+
if (!ready) {
|
|
5257
|
+
this.fail(contribution, new Error('the viewport never became ready'));
|
|
5258
|
+
return resolve();
|
|
5259
|
+
}
|
|
5260
|
+
let result;
|
|
5261
|
+
try {
|
|
5262
|
+
result = contribution.activate(ctx);
|
|
5263
|
+
}
|
|
5264
|
+
catch (err) {
|
|
5265
|
+
this.fail(contribution, err);
|
|
5266
|
+
return resolve();
|
|
5267
|
+
}
|
|
5268
|
+
if (isThenable(result)) {
|
|
5269
|
+
result.then((session) => { this.start(contribution, ctx, session, gen); resolve(); }, (err) => {
|
|
5270
|
+
if (gen === this.generation)
|
|
5271
|
+
this.fail(contribution, err);
|
|
5272
|
+
resolve();
|
|
5273
|
+
});
|
|
5274
|
+
}
|
|
5275
|
+
else {
|
|
5276
|
+
this.start(contribution, ctx, result, gen);
|
|
5277
|
+
resolve();
|
|
5278
|
+
}
|
|
5279
|
+
});
|
|
5280
|
+
});
|
|
5281
|
+
}
|
|
5282
|
+
/**
|
|
5283
|
+
* End the live session, if any: the component panel is dropped (via
|
|
5284
|
+
* `onDeactivating`), a `mount` panel's teardown runs, then
|
|
5285
|
+
* `session.deactivate()`, then the mount host is removed. Each step is
|
|
5286
|
+
* isolated. A pending activation is cancelled; if it resolves later its
|
|
5287
|
+
* session is deactivated on arrival. Idempotent.
|
|
5288
|
+
*/
|
|
5289
|
+
deactivate() {
|
|
5290
|
+
this.generation++;
|
|
5291
|
+
this.pendingId = null;
|
|
5292
|
+
this.cancelReadyWait();
|
|
5293
|
+
const a = this.active;
|
|
5294
|
+
if (!a)
|
|
5295
|
+
return;
|
|
5296
|
+
this.active = null;
|
|
5297
|
+
const teardown = this.teardown;
|
|
5298
|
+
this.teardown = null;
|
|
5299
|
+
const id = a.contribution.descriptor.type;
|
|
5300
|
+
this.guard(`'${id}' panel removal`, () => this.hooks.onDeactivating(a));
|
|
5301
|
+
if (teardown)
|
|
5302
|
+
this.cleanup(a.contribution, 'panel teardown', teardown);
|
|
5303
|
+
this.cleanup(a.contribution, 'deactivate()', () => a.session.deactivate());
|
|
5304
|
+
a.panelHost?.remove();
|
|
5305
|
+
}
|
|
5306
|
+
/**
|
|
5307
|
+
* Forget recorded cleanup failures. The host calls this when the USER picks a
|
|
5308
|
+
* plot type: an explicit choice gets a fresh attempt, only automatic
|
|
5309
|
+
* re-activations fall back.
|
|
5310
|
+
*/
|
|
5311
|
+
clearCleanupFailures() {
|
|
5312
|
+
this.failedCleanup.clear();
|
|
5313
|
+
}
|
|
5314
|
+
// ── internals ──────────────────────────────────────────────────────────
|
|
5315
|
+
start(contribution, ctx, session, gen) {
|
|
5316
|
+
const id = contribution.descriptor.type;
|
|
5317
|
+
if (!session || typeof session.deactivate !== 'function') {
|
|
5318
|
+
if (gen === this.generation) {
|
|
5319
|
+
this.fail(contribution, new Error('activate() did not return a PlotModeSession'));
|
|
5320
|
+
}
|
|
5321
|
+
return;
|
|
5322
|
+
}
|
|
5323
|
+
if (gen !== this.generation) {
|
|
5324
|
+
// Superseded while activate() was in flight — the user already left. The
|
|
5325
|
+
// session still gets its single deactivate(), just late.
|
|
5326
|
+
this.guardLogged(`'${id}' deactivate() of a superseded session`, () => session.deactivate());
|
|
5327
|
+
return;
|
|
5328
|
+
}
|
|
5329
|
+
this.pendingId = null;
|
|
5330
|
+
const panel = contribution.panel;
|
|
5331
|
+
let panelHost = null;
|
|
5332
|
+
let teardown = null;
|
|
5333
|
+
if (panel && 'mount' in panel && typeof panel.mount === 'function') {
|
|
5334
|
+
panelHost = document.createElement('div');
|
|
5335
|
+
panelHost.className = 'plot-mode-panel-host';
|
|
5336
|
+
try {
|
|
5337
|
+
const t = panel.mount(panelHost, ctx, session);
|
|
5338
|
+
teardown = typeof t === 'function' ? t : null;
|
|
5339
|
+
}
|
|
5340
|
+
catch (err) {
|
|
5341
|
+
panelHost.remove();
|
|
5342
|
+
this.guardLogged(`'${id}' deactivate()`, () => session.deactivate());
|
|
5343
|
+
this.fail(contribution, err);
|
|
5344
|
+
return;
|
|
5345
|
+
}
|
|
5346
|
+
}
|
|
5347
|
+
this.active = { contribution, ctx, session, panelHost };
|
|
5348
|
+
this.teardown = teardown;
|
|
5349
|
+
try {
|
|
5350
|
+
this.hooks.onActivated(this.active);
|
|
5351
|
+
}
|
|
5352
|
+
catch (err) {
|
|
5353
|
+
// The host could not show it (e.g. its panel failed to render): not a usable mode.
|
|
5354
|
+
this.failActive(err);
|
|
5355
|
+
}
|
|
5356
|
+
}
|
|
5357
|
+
/**
|
|
5358
|
+
* The host could not make the live mode usable (its panel threw while being
|
|
5359
|
+
* created or first rendered): end the session once, then fall back to its base.
|
|
5360
|
+
* No-op when nothing is live.
|
|
5361
|
+
*/
|
|
5362
|
+
failActive(err) {
|
|
5363
|
+
const a = this.active;
|
|
5364
|
+
if (!a)
|
|
5365
|
+
return;
|
|
5366
|
+
this.deactivate();
|
|
5367
|
+
this.fail(a.contribution, err);
|
|
5368
|
+
}
|
|
5369
|
+
fail(contribution, err) {
|
|
5370
|
+
this.pendingId = null;
|
|
5371
|
+
this.log.error(`${TAG} '${contribution.descriptor.type}' failed to activate — `
|
|
5372
|
+
+ `falling back to '${contribution.descriptor.baseType}'.`, err);
|
|
5373
|
+
try {
|
|
5374
|
+
this.hooks.onFailed(contribution, err);
|
|
5375
|
+
}
|
|
5376
|
+
catch (hookErr) {
|
|
5377
|
+
this.log.error(`${TAG} fallback failed.`, hookErr);
|
|
5378
|
+
}
|
|
5379
|
+
}
|
|
5380
|
+
guard(what, fn) {
|
|
5381
|
+
try {
|
|
5382
|
+
fn();
|
|
5383
|
+
}
|
|
5384
|
+
catch (err) {
|
|
5385
|
+
this.log.error(`${TAG} ${what} threw.`, err);
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
/** Like `guard`, for a contribution callback typed `void` that may still return a
|
|
5389
|
+
* promise: a rejection is logged instead of escaping as an unhandled rejection. */
|
|
5390
|
+
guardLogged(what, fn) {
|
|
5391
|
+
try {
|
|
5392
|
+
const r = fn();
|
|
5393
|
+
if (isThenable(r))
|
|
5394
|
+
r.then(undefined, (err) => this.log.error(`${TAG} ${what} rejected.`, err));
|
|
5395
|
+
}
|
|
5396
|
+
catch (err) {
|
|
5397
|
+
this.log.error(`${TAG} ${what} threw.`, err);
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
/** Run a live session's cleanup; a throw OR a rejection goes to `cleanupFailed`. */
|
|
5401
|
+
cleanup(contribution, what, fn) {
|
|
5402
|
+
try {
|
|
5403
|
+
const r = fn();
|
|
5404
|
+
if (isThenable(r))
|
|
5405
|
+
r.then(undefined, (err) => this.cleanupFailed(contribution, what, err));
|
|
5406
|
+
}
|
|
5407
|
+
catch (err) {
|
|
5408
|
+
this.cleanupFailed(contribution, what, err);
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
/**
|
|
5412
|
+
* A contribution's cleanup failed. If that same contribution is live or starting
|
|
5413
|
+
* again (a base re-render / image switch re-activated it — or, for an async
|
|
5414
|
+
* rejection, already has), end it and fall back to its base type. Otherwise the
|
|
5415
|
+
* user has moved on: remember it, so the next automatic re-activation falls back.
|
|
5416
|
+
*/
|
|
5417
|
+
cleanupFailed(contribution, what, err) {
|
|
5418
|
+
const id = contribution.descriptor.type;
|
|
5419
|
+
this.log.error(`${TAG} '${id}' ${what} failed.`, err);
|
|
5420
|
+
const again = this.active?.contribution === contribution || this.pendingId === id;
|
|
5421
|
+
if (again) {
|
|
5422
|
+
this.deactivate();
|
|
5423
|
+
this.fail(contribution, err);
|
|
5424
|
+
}
|
|
5425
|
+
else {
|
|
5426
|
+
this.failedCleanup.set(id, err);
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5429
|
+
/** Call `cb(true)` once the viewport reports ready (now, or on a later
|
|
5430
|
+
* animation frame), or `cb(false)` after {@link MAX_READY_FRAMES}. */
|
|
5431
|
+
whenReady(ctx, gen, cb) {
|
|
5432
|
+
const isReady = () => {
|
|
5433
|
+
try {
|
|
5434
|
+
return ctx.viewport.isReady();
|
|
5435
|
+
}
|
|
5436
|
+
catch {
|
|
5437
|
+
return false;
|
|
5438
|
+
}
|
|
5439
|
+
};
|
|
5440
|
+
if (isReady())
|
|
5441
|
+
return cb(true);
|
|
5442
|
+
let frames = 0;
|
|
5443
|
+
const settle = (ready) => {
|
|
5444
|
+
this.readyFrame = null;
|
|
5445
|
+
this.readyCb = null;
|
|
5446
|
+
cb(ready);
|
|
5447
|
+
};
|
|
5448
|
+
const tick = () => {
|
|
5449
|
+
this.readyFrame = null;
|
|
5450
|
+
if (gen !== this.generation)
|
|
5451
|
+
return settle(false);
|
|
5452
|
+
if (isReady())
|
|
5453
|
+
return settle(true);
|
|
5454
|
+
if (++frames >= MAX_READY_FRAMES)
|
|
5455
|
+
return settle(false);
|
|
5456
|
+
this.readyFrame = requestAnimationFrame(tick);
|
|
5457
|
+
};
|
|
5458
|
+
this.readyCb = settle;
|
|
5459
|
+
this.readyFrame = requestAnimationFrame(tick);
|
|
5460
|
+
}
|
|
5461
|
+
/** Stop waiting for the viewport; the waiting activation resolves as superseded. */
|
|
5462
|
+
cancelReadyWait() {
|
|
5463
|
+
if (this.readyFrame !== null)
|
|
5464
|
+
cancelAnimationFrame(this.readyFrame);
|
|
5465
|
+
this.readyFrame = null;
|
|
5466
|
+
const cb = this.readyCb;
|
|
5467
|
+
this.readyCb = null;
|
|
5468
|
+
cb?.(false);
|
|
5469
|
+
}
|
|
5470
|
+
}
|
|
5471
|
+
function isThenable(v) {
|
|
5472
|
+
return !!v && typeof v.then === 'function';
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5077
5475
|
/**
|
|
5078
5476
|
* Pure helpers for SAM inference — no DOM, no ORT, no `this` — so they're unit
|
|
5079
5477
|
* testable. The session uses these to assemble decoder inputs and to convert
|
|
@@ -5969,9 +6367,18 @@ const MODEL_INFO = {
|
|
|
5969
6367
|
*/
|
|
5970
6368
|
class ToolbarComponent {
|
|
5971
6369
|
constructor() {
|
|
5972
|
-
/** Plot types the active backend advertises. */
|
|
6370
|
+
/** Plot types the active backend advertises, then any contributed modes. */
|
|
5973
6371
|
this.plotTypeOptions = [];
|
|
6372
|
+
/** The selector's value: a built-in type or a contributed mode's id. */
|
|
5974
6373
|
this.selectedPlotType = PlotType.IMAGE;
|
|
6374
|
+
/**
|
|
6375
|
+
* The built-in type that renders {@link selectedPlotType} — its `baseType`
|
|
6376
|
+
* for a contributed mode. Every tool/control decision below reads
|
|
6377
|
+
* {@link effectivePlotType}, so a contributed mode gets exactly its base
|
|
6378
|
+
* type's toolbar. When unbound, a built-in selection is its own base and
|
|
6379
|
+
* anything else is treated as the default Image view.
|
|
6380
|
+
*/
|
|
6381
|
+
this.basePlotType = null;
|
|
5975
6382
|
/** Isosurface band as a 0–255 slider position (mapped onto the volume's real
|
|
5976
6383
|
* intensity range by the renderer). Defaults to the full range. */
|
|
5977
6384
|
this.isoRange = [0, 255];
|
|
@@ -6167,17 +6574,23 @@ class ToolbarComponent {
|
|
|
6167
6574
|
trackToolById(_index, tool) {
|
|
6168
6575
|
return tool.id;
|
|
6169
6576
|
}
|
|
6577
|
+
/** The built-in type whose tools and controls apply to the current selection. */
|
|
6578
|
+
get effectivePlotType() {
|
|
6579
|
+
if (this.basePlotType)
|
|
6580
|
+
return this.basePlotType;
|
|
6581
|
+
return isBuiltinPlotType(this.selectedPlotType) ? this.selectedPlotType : PlotType.IMAGE;
|
|
6582
|
+
}
|
|
6170
6583
|
/** The Image plot type renders as a natively pan/zoom-able raster, so the
|
|
6171
6584
|
* generic zoom/pan tools are hidden. */
|
|
6172
6585
|
get isImageView() {
|
|
6173
|
-
return this.
|
|
6586
|
+
return this.effectivePlotType === PlotType.IMAGE;
|
|
6174
6587
|
}
|
|
6175
6588
|
/** Either spatial-omics mode is active, so its controls are worth offering.
|
|
6176
6589
|
* The 3D cloud needs the panel MORE than the 2D view does, not less: colouring,
|
|
6177
6590
|
* the legend and category show/hide are the only way to make sense of a
|
|
6178
6591
|
* million overlapping points. */
|
|
6179
6592
|
get isSpatialMode() {
|
|
6180
|
-
return isSpatialOmics(this.
|
|
6593
|
+
return isSpatialOmics(this.effectivePlotType) || isSpatialOmics3d(this.effectivePlotType);
|
|
6181
6594
|
}
|
|
6182
6595
|
/** A plot-type icon is a PrimeNG font glyph (e.g. `pi pi-image`) rather than an
|
|
6183
6596
|
* SVG asset path — drives which element the selector item template renders. */
|
|
@@ -6187,8 +6600,8 @@ class ToolbarComponent {
|
|
|
6187
6600
|
/** Backends with a vertex-editing region overlay: OSD Image and napari-js WebGPU image
|
|
6188
6601
|
* (jit-ui#102). Gates the polygon / add-vertex / delete-vertex / Bézier-convert tools. */
|
|
6189
6602
|
get supportsRegionVertexTools() {
|
|
6190
|
-
return (this.
|
|
6191
|
-
this.
|
|
6603
|
+
return (this.effectivePlotType === PlotType.IMAGE ||
|
|
6604
|
+
this.effectivePlotType === PlotType.NAPARI_IMAGE);
|
|
6192
6605
|
}
|
|
6193
6606
|
/** Plot types that scrub a z-stack live (the renderer swaps the slice in place): the OSD Image
|
|
6194
6607
|
* view, the napari-js WebGPU image, the napari-js surface — which rebuilds the height field
|
|
@@ -6196,41 +6609,41 @@ class ToolbarComponent {
|
|
|
6196
6609
|
* registered volume is the image and the slider picks the section whose observations are
|
|
6197
6610
|
* drawn. Drives the per-slice slider. */
|
|
6198
6611
|
get showsLiveSliceScrubber() {
|
|
6199
|
-
return (this.
|
|
6200
|
-
this.
|
|
6201
|
-
isNapariSurface(this.
|
|
6202
|
-
isNapariScatter(this.
|
|
6203
|
-
isSpatialOmics(this.
|
|
6612
|
+
return (this.effectivePlotType === PlotType.IMAGE ||
|
|
6613
|
+
this.effectivePlotType === PlotType.NAPARI_IMAGE ||
|
|
6614
|
+
isNapariSurface(this.effectivePlotType) ||
|
|
6615
|
+
isNapariScatter(this.effectivePlotType) ||
|
|
6616
|
+
isSpatialOmics(this.effectivePlotType));
|
|
6204
6617
|
}
|
|
6205
6618
|
/** Any napari-js WebGPU plot type. The single-image/stack toggle + slice-number field are
|
|
6206
6619
|
* Plotly-only stack controls, so they're hidden for napari (jit-ui#102). */
|
|
6207
6620
|
get isNapariMode() {
|
|
6208
|
-
return (this.
|
|
6209
|
-
isNapariScatter(this.
|
|
6210
|
-
isNapari3d(this.
|
|
6621
|
+
return (this.effectivePlotType === PlotType.NAPARI_IMAGE ||
|
|
6622
|
+
isNapariScatter(this.effectivePlotType) ||
|
|
6623
|
+
isNapari3d(this.effectivePlotType));
|
|
6211
6624
|
}
|
|
6212
6625
|
/** ISOSURFACE (Plotly or napari-js WebGPU, either resolution) shows the isovalue range slider. */
|
|
6213
6626
|
get isIsosurfaceMode() {
|
|
6214
|
-
return (this.
|
|
6627
|
+
return (this.effectivePlotType === PlotType.ISOSURFACE || isNapariIsosurface(this.effectivePlotType));
|
|
6215
6628
|
}
|
|
6216
6629
|
/** The napari-js WebGPU surface — shows the wireframe toggle. */
|
|
6217
6630
|
get isNapariSurfaceMode() {
|
|
6218
|
-
return isNapariSurface(this.
|
|
6631
|
+
return isNapariSurface(this.effectivePlotType);
|
|
6219
6632
|
}
|
|
6220
6633
|
/** Any napari-js WebGPU 3D type (volume/isosurface/surface) — shows the Resolution control. */
|
|
6221
6634
|
get isNapari3dMode() {
|
|
6222
|
-
return isNapari3d(this.
|
|
6635
|
+
return isNapari3d(this.effectivePlotType);
|
|
6223
6636
|
}
|
|
6224
6637
|
/** Intensity profile lines are Region-based and available in the Heatmap and
|
|
6225
6638
|
* Image plot types, which show the Intensity tool group. */
|
|
6226
6639
|
get isIntensityCapable() {
|
|
6227
|
-
return this.
|
|
6640
|
+
return this.effectivePlotType === PlotType.HEATMAP || this.effectivePlotType === PlotType.IMAGE;
|
|
6228
6641
|
}
|
|
6229
6642
|
showHelp() {
|
|
6230
6643
|
this.displayHelpDialog = true;
|
|
6231
6644
|
}
|
|
6232
6645
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6233
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: ToolbarComponent, selector: "plotting-toolbar", inputs: { imageInfo: "imageInfo", plotTypeOptions: "plotTypeOptions", selectedPlotType: "selectedPlotType", isoRange: "isoRange", maxIndex: "maxIndex", zIndex: "zIndex", isHeatmap: "isHeatmap", is3dRegions: "is3dRegions", activeDragMode: "activeDragMode", activeSurface3dMode: "activeSurface3dMode", axesVisible: "axesVisible", wireframeActive: "wireframeActive", resolutionScale: "resolutionScale", wandSensitivity: "wandSensitivity", brushSize: "brushSize", vertexEraserRadius: "vertexEraserRadius", canUndo: "canUndo", canRedo: "canRedo", stackOptions: "stackOptions", tools: "tools", imageSmoothingEnabled: "imageSmoothingEnabled", samModels: "samModels", samModelId: "samModelId", contributedTools: "contributedTools", toolModelIds: "toolModelIds" }, outputs: { selectPlotType: "selectPlotType", addProfileLine: "addProfileLine", toggleImageSmoothing: "toggleImageSmoothing", isoRangeChange: "isoRangeChange", openChannelHistogram: "openChannelHistogram", openSpatialControls: "openSpatialControls", openRegionEditor: "openRegionEditor", selectStackOption: "selectStackOption", zScrub: "zScrub", zSlide: "zSlide", zIndexInput: "zIndexInput", reloadAndPlot: "reloadAndPlot", downloadImage: "downloadImage", autoscaleImage: "autoscaleImage", toggleDragMode: "toggleDragMode", zoomIn: "zoomIn", zoomOut: "zoomOut", toggleSurface3dMode: "toggleSurface3dMode", resetSurfaceCamera: "resetSurfaceCamera", toggleAxes: "toggleAxes", toggleWireframe: "toggleWireframe", selectResolution: "selectResolution", deleteRegion: "deleteRegion", undoRegion: "undoRegion", redoRegion: "redoRegion", segmentRegions: "segmentRegions", segmentCellpose: "segmentCellpose", runTool: "runTool", toolModelChange: "toolModelChange", openToolParams: "openToolParams", samModelChange: "samModelChange", wandSensitivityChange: "wandSensitivityChange", brushSizeChange: "brushSizeChange", vertexEraserRadiusChange: "vertexEraserRadiusChange" }, usesOnChanges: true, ngImport: i0, template: "<p-toolbar>\n <div class=\"p-toolbar-group-start flex-wrap align-items-center gap-1\">\n\n <!-- Host-provided custom toolbar buttons, shown first (e.g. app-specific\n actions the visualization view itself doesn't own). -->\n <ng-content></ng-content>\n\n <!-- Plot type: available for any image (grayscale or RGB). -->\n <ng-container *ngIf=\"imageInfo && tools.specialTools\">\n <span class=\"toolbar-separator\"></span>\n <p-dropdown [options]=\"plotTypeOptions\" optionLabel=\"label\" optionValue=\"type\"\n [(ngModel)]=\"selectedPlotType\" appendTo=\"body\"\n pTooltip=\"Select plot type\" tooltipPosition=\"bottom\"\n [style]=\"{'min-width': '11rem'}\"\n styleClass=\"plotmode-dropdown\" panelStyleClass=\"plotmode-panel\"\n (onChange)=\"selectPlotType.emit($event.value)\">\n <ng-template let-opt pTemplate=\"selectedItem\">\n <span class=\"plotmode-item\" *ngIf=\"opt\">\n <i *ngIf=\"isPiIcon(opt.icon)\" [class]=\"opt.icon\"></i>\n <img *ngIf=\"opt.icon && !isPiIcon(opt.icon)\" class=\"plotmode-icon-img\" [src]=\"opt.icon\" alt=\"\" />\n <span>{{ opt.label }}</span>\n </span>\n </ng-template>\n <ng-template let-opt pTemplate=\"item\">\n <span class=\"plotmode-item\">\n <i *ngIf=\"isPiIcon(opt.icon)\" [class]=\"opt.icon\"></i>\n <img *ngIf=\"opt.icon && !isPiIcon(opt.icon)\" class=\"plotmode-icon-img\" [src]=\"opt.icon\" alt=\"\" />\n <span>{{ opt.label }}</span>\n </span>\n </ng-template>\n </p-dropdown>\n <ng-container *ngIf=\"isIsosurfaceMode\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Iso</span>\n <p-slider [(ngModel)]=\"isoRange\" [range]=\"true\"\n [min]=\"isoValueMin\" [max]=\"isoValueMax\" [step]=\"isoValueStep\"\n (onChange)=\"isoRangeChange.emit($event.values)\"\n pTooltip=\"Isosurface band (0\u2013255), mapped onto the volume's\n actual intensity range. Surfaces are drawn for voxels whose\n intensity falls in the selected band \u2014 narrow it to isolate\n features, widen it to see the whole structure.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n styleClass=\"wand-sensitivity-slider\"></p-slider>\n <span class=\"flex align-items-center mx-1 toolbar-text wand-sensitivity-value\">\n {{ isoRange[0] }}\u2013{{ isoRange[1] }}</span>\n </ng-container>\n <!-- Intensity tools (Heatmap / Image mode): add another colored line ROI.\n Each added line gets the next bright palette colour and its own inset\n trace; select a line on the image and use the trash tool to remove it\n (and its trace). The lines are Region-based but excluded from the\n Region Editor and exports. -->\n <ng-container *ngIf=\"isIntensityCapable\">\n <span class=\"toolbar-separator\"></span>\n <p-button size=\"small\" [text]=\"true\"\n pTooltip=\"Add an intensity line (new colour). Click a line to select it,\n then use the trash tool to remove it.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"addProfileLine.emit()\">\n <img src=\"assets/plotting/spline-chart.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n </ng-container>\n </ng-container>\n <!-- Channels & Histogram: brightness/contrast, gamma, per-channel display,\n colormap/reverse/invert \u2014 opens the floating dialog. -->\n <p-button *ngIf=\"tools.specialTools\" pRipple size=\"small\" [text]=\"true\"\n pTooltip=\"Channels & Histogram (brightness/contrast, colormap)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openChannelHistogram.emit()\">\n <img src=\"assets/plotting/invert.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n\n <!-- Spatial-omics controls. Only meaningful in the spatial mode, which is\n itself only offered while a dataset is loaded \u2014 so this button appears\n exactly when there is something for it to control. -->\n <p-button *ngIf=\"tools.specialTools && isSpatialMode\" pRipple size=\"small\" [text]=\"true\"\n pTooltip=\"Spatial omics (colour by column or gene, legend, selection, distributions)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openSpatialControls.emit()\">\n <img src=\"assets/plotting/spatial-omics.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n\n <!-- Smoothen toggle: bilinear smoothing vs nearest-neighbour raw pixels (Image\n view only \u2014 it's an OpenSeadragon drawer setting). Highlighted when smoothing on. -->\n <p-button *ngIf=\"tools.specialTools && isImageView\" size=\"small\"\n [text]=\"!imageSmoothingEnabled\"\n pTooltip=\"Smoothen (turn on image smoothing)\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleImageSmoothing.emit()\">\n <img src=\"assets/plotting/smooth.svg\" style=\"width: 1rem\"\n [class]=\"imageSmoothingEnabled ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"/>\n </p-button>\n\n <!-- Stack controls -->\n <ng-container *ngIf=\"imageInfo?.isStack\">\n <span class=\"toolbar-separator\"></span>\n <!-- Image view: scrub z-slices live (the renderer swaps slices in place). -->\n <ng-container *ngIf=\"showsLiveSliceScrubber\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Slice</span>\n <p-slider [(ngModel)]=\"zIndex\" [min]=\"0\" [max]=\"maxIndex\"\n (onChange)=\"zScrub.emit($event.value)\" (onSlideEnd)=\"zSlide.emit($event.value)\"\n pTooltip=\"Slice through the stack (\u2190 / \u2192 keys)\" tooltipPosition=\"bottom\"\n [style]=\"{'width':'16rem'}\" styleClass=\"wand-sensitivity-slider\"></p-slider>\n <span class=\"flex align-items-center mx-1 toolbar-text\">{{zIndex}} of {{maxIndex}}</span>\n </ng-container>\n <!-- Heatmap view: single-image (high-def zoom) vs stack (frame slider). Plotly-only \u2014\n hidden for napari, which has no single/stack toggle or per-frame fetch, and for the\n spatial-omics modes: a 3D omics dataset publishes its reference volume as a z-stack\n image, and these modes draw observations, not one of its slices. -->\n <ng-container *ngIf=\"!showsLiveSliceScrubber && !isNapariMode && !isSpatialMode\">\n <p-button icon=\"pi pi-image\" size=\"small\"\n [text]=\"imageInfo?.showStack\"\n pTooltip=\"Single image mode: enables high-def zoom\" tooltipPosition=\"bottom\"\n (onClick)=\"selectStackOption.emit(stackOptions[0])\">\n </p-button>\n <p-button icon=\"pi pi-images\" size=\"small\"\n [text]=\"!imageInfo?.showStack\"\n pTooltip=\"Stack mode, enables slicing through image frames\" tooltipPosition=\"bottom\"\n (onClick)=\"selectStackOption.emit(stackOptions[1])\">\n </p-button>\n <ng-container *ngIf=\"!imageInfo?.showStack\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Slice</span>\n <div class=\"p-inputgroup\" style=\"width: auto\">\n <p-inputNumber styleClass=\"w-3rem toolbar-input\"\n\t\t\t\t\tpTooltip=\"View image in stack (⏎)\"\n\t\t\t\t\t[(ngModel)]=\"zIndex\" [min]=\"0\" [max]=\"maxIndex\"\n (onInput)=\"zIndexInput.emit(zIndex)\" (keydown.enter)=\"reloadAndPlot.emit()\"/>\n <p-button icon=\"pi pi-refresh\" size=\"small\" [text]=\"true\" (onClick)=\"reloadAndPlot.emit()\"\n pTooltip=\"Plot this slice\" tooltipPosition=\"bottom\">\n </p-button>\n </div>\n <span class=\"flex align-items-center mx-1 toolbar-text\">of {{maxIndex}}</span>\n </ng-container>\n </ng-container>\n </ng-container>\n\n <!-- Image actions (special tools) -->\n <ng-container *ngIf=\"tools.specialTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button icon=\"pi pi-download\" size=\"small\" [text]=\"true\"\n pTooltip=\"Download image as PNG\" tooltipPosition=\"bottom\"\n (onClick)=\"downloadImage.emit()\">\n </p-button>\n <p-button *ngIf=\"isHeatmap\" icon=\"pi pi-window-maximize\" size=\"small\" [text]=\"true\"\n pTooltip=\"Fit to view\" tooltipPosition=\"bottom\"\n (onClick)=\"autoscaleImage.emit()\">\n </p-button>\n </ng-container>\n\n <!-- Zoom tools. Plotly rubber-band drag-zoom: on OpenSeadragon the box-zoom\n covers this and a bare drag pans natively, so it's hidden in image view. -->\n <ng-container *ngIf=\"isHeatmap && tools.zoomTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button *ngIf=\"!isImageView\" icon=\"pi pi-search\" size=\"small\"\n [text]=\"activeDragMode !== 'zoom'\"\n pTooltip=\"Zoom drag\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('zoom')\">\n </p-button>\n <p-button size=\"small\"\n [text]=\"activeDragMode !== 'zoomToBox'\"\n pTooltip=\"Zoom box (<kbd>b</kbd>)\" [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('zoomToBox')\">\n <img src=\"assets/plotting/zoom-box.svg\"\n [class]=\"activeDragMode === 'zoomToBox' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n <p-button icon=\"pi pi-arrows-alt\" size=\"small\"\n [text]=\"activeDragMode !== 'pan'\"\n pTooltip=\"Pan the image (<kbd>p</kbd>)\" [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('pan')\">\n </p-button>\n <p-button icon=\"pi pi-search-plus\" size=\"small\" [text]=\"true\"\n pTooltip=\"Zoom in (<kbd>+</kbd>)\" [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"zoomIn.emit()\">\n </p-button>\n <p-button icon=\"pi pi-search-minus\" size=\"small\" [text]=\"true\"\n pTooltip=\"Zoom out (<kbd>-</kbd>)\" [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"zoomOut.emit()\">\n </p-button>\n </ng-container>\n\n <!-- Surface 3D controls -->\n <ng-container *ngIf=\"!isHeatmap\">\n <span class=\"toolbar-separator\"></span>\n <p-button icon=\"pi pi-search\" size=\"small\"\n [text]=\"activeSurface3dMode !== 'zoom'\"\n pTooltip=\"Zoom\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('zoom')\">\n </p-button>\n <p-button icon=\"pi pi-arrows-alt\" size=\"small\"\n [text]=\"activeSurface3dMode !== 'pan'\"\n pTooltip=\"Pan\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('pan')\">\n </p-button>\n <p-button icon=\"pi pi-globe\" size=\"small\"\n [text]=\"activeSurface3dMode !== 'orbit'\"\n pTooltip=\"Orbital rotation\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('orbit')\">\n </p-button>\n <p-button icon=\"pi pi-sync\" size=\"small\"\n [text]=\"activeSurface3dMode !== 'turntable'\"\n pTooltip=\"Turntable rotation\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('turntable')\">\n </p-button>\n <p-button icon=\"pi pi-home\" size=\"small\" [text]=\"true\"\n pTooltip=\"Reset camera to default\" tooltipPosition=\"bottom\"\n (onClick)=\"resetSurfaceCamera.emit()\">\n </p-button>\n <!-- 3D coordinate-axes / scale gizmo toggle (napari-js WebGPU volume/isosurface only).\n `p-button-icon-only` gives the same square sizing as the neighbouring pi-icon buttons\n (the projected <img> doesn't trigger PrimeNG's automatic icon-only sizing). -->\n <p-button size=\"small\" *ngIf=\"isNapariMode\" styleClass=\"p-button-icon-only\"\n [text]=\"!axesVisible\"\n pTooltip=\"Toggle 3D axes / scale\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleAxes.emit()\">\n <img src=\"assets/plotting/3d-coordinates.svg\"\n [class]=\"axesVisible ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n <!-- Surface wireframe toggle (napari-js WebGPU surface only). -->\n <p-button icon=\"pi pi-share-alt\" size=\"small\" *ngIf=\"isNapariSurfaceMode\"\n [text]=\"!wireframeActive\"\n pTooltip=\"Toggle wireframe\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleWireframe.emit()\">\n </p-button>\n <!-- Resolution (decimate factor) for the napari 3D types \u2014 reloads at coarser/finer sampling. -->\n <ng-container *ngIf=\"isNapari3dMode\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Res</span>\n <p-dropdown [options]=\"decimateOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"resolutionScale\" appendTo=\"body\"\n pTooltip=\"Resolution (decimate factor)\" tooltipPosition=\"bottom\"\n [style]=\"{'min-width': '6rem'}\"\n (onChange)=\"selectResolution.emit($event.value)\">\n </p-dropdown>\n </ng-container>\n </ng-container>\n\n <ng-container *ngIf=\"(isHeatmap || is3dRegions) && tools.regionTools\">\n <span class=\"toolbar-separator\"></span>\n <!-- Region Editor: opens the editor as a floating dialog (formerly the\n right-panel Regions tab). Sits with the region drawing tools. -->\n <p-button size=\"small\" [text]=\"true\"\n pTooltip=\"Open the Region Editor\" tooltipPosition=\"bottom\"\n (onClick)=\"openRegionEditor.emit()\">\n <img src=\"assets/plotting/region-editor.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n <!-- Region drawing tools -->\n <p-button icon=\"pi pi-arrow-up-right\" size=\"small\"\n [text]=\"activeDragMode !== 'select'\"\n pTooltip=\"Selection mode (<kbd>s</kbd>)\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('select')\">\n </p-button>\n <p-button icon=\"pi pi-stop\" size=\"small\"\n [text]=\"activeDragMode !== 'drawrect'\"\n pTooltip=\"Draw a rectangular region (<kbd>r</kbd>)\" [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('drawrect')\">\n </p-button>\n <p-button *ngIf=\"!is3dRegions\" size=\"small\"\n [text]=\"activeDragMode !== 'drawopenpath'\"\n pTooltip=\"Draw a polyline (<kbd>l</kbd>)\" [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('drawopenpath')\">\n <img src=\"assets/plotting/polyline.svg\"\n [class]=\"activeDragMode === 'drawopenpath' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n <p-button icon=\"pi pi-pencil\" size=\"small\"\n [text]=\"activeDragMode !== 'drawclosedpath'\"\n pTooltip=\"Draw a freeform region (<kbd>f</kbd>)\" [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('drawclosedpath')\">\n </p-button>\n <!-- Brush: paint a region by dragging a disc of the given size; the Size\n slider appears while it's active. Hold Shift to erase (subtract). -->\n <!-- Brush and polyline are absent from a 3D cloud: the brush paints in IMAGE\n pixels and there is no raster to sample, and an open polyline is a\n profile tool, not a closed region a selection can test against. -->\n <p-button *ngIf=\"!is3dRegions\" size=\"small\"\n [text]=\"activeDragMode !== 'brush'\"\n pTooltip=\"Brush: click and drag to paint a region.\n <br>Hold <kbd>Shift</kbd> while painting to erase from a region.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('brush')\">\n <img src=\"assets/plotting/paintbrush.svg\"\n [class]=\"activeDragMode === 'brush' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n <ng-container *ngIf=\"activeDragMode === 'brush'\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Size</span>\n <p-slider [(ngModel)]=\"brushSize\"\n [min]=\"brushSizeMin\" [max]=\"brushSizeMax\" [step]=\"brushSizeStep\"\n (onChange)=\"brushSizeChange.emit($event.value)\"\n pTooltip=\"Brush diameter in image pixels.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n styleClass=\"wand-sensitivity-slider\"></p-slider>\n <span class=\"flex align-items-center mx-1 toolbar-text wand-sensitivity-value\">\n {{ brushSize }}</span>\n </ng-container>\n\n <!-- Vertex editing \u2014 backends with a vertex-editing overlay (OSD + napari-js image). -->\n <ng-container *ngIf=\"supportsRegionVertexTools\">\n <p-button size=\"small\"\n [text]=\"activeDragMode !== 'drawpolygon'\"\n pTooltip=\"Polygon: click to place each vertex; click the first point to close\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('drawpolygon')\">\n <img src=\"assets/plotting/polygon-vertices.svg\"\n [class]=\"activeDragMode === 'drawpolygon' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n <p-button size=\"small\"\n [text]=\"activeDragMode !== 'addpoint'\"\n pTooltip=\"Add a vertex: click an edge of the selected polygon\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('addpoint')\">\n <img src=\"assets/plotting/vertex-add.svg\"\n [class]=\"activeDragMode === 'addpoint' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n <p-button size=\"small\"\n [text]=\"activeDragMode !== 'deletepoint'\"\n pTooltip=\"Delete a vertex: click a vertex of the selected polygon\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('deletepoint')\">\n <img src=\"assets/plotting/vertex-delete.svg\"\n [class]=\"activeDragMode === 'deletepoint' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n </ng-container>\n\n <p-button size=\"small\"\n [text]=\"activeDragMode !== 'wand'\"\n pTooltip=\"Wand: click and drag to grow a region by similar pixel values (<kbd>w</kbd>).\n <br>Hold <kbd>Shift</kbd> while clicking or dragging on a region to erase pixels from it.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('wand')\">\n <img src=\"assets/plotting/wand.svg\"\n [class]=\"activeDragMode === 'wand' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n <ng-container *ngIf=\"activeDragMode === 'wand'\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Sens.</span>\n <p-slider [(ngModel)]=\"wandSensitivity\"\n [min]=\"wandSensitivityMin\" [max]=\"wandSensitivityMax\" [step]=\"wandSensitivityStep\"\n (onChange)=\"wandSensitivityChange.emit($event.value)\"\n pTooltip=\"Higher = stricter (smaller selection); lower = looser.\n Takes effect from the next click/drag tick.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n styleClass=\"wand-sensitivity-slider\"></p-slider>\n <span class=\"flex align-items-center mx-1 toolbar-text wand-sensitivity-value\">\n {{ wandSensitivity.toFixed(1) }}</span>\n </ng-container>\n <p-button icon=\"pi pi-eraser\" size=\"small\"\n [text]=\"activeDragMode !== 'eraseVertex'\"\n pTooltip=\"Vertex eraser (<kbd>e</kbd>): click or drag to remove polygon vertices inside\n the cursor circle from any region.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('eraseVertex')\">\n </p-button>\n <ng-container *ngIf=\"activeDragMode === 'eraseVertex'\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Radius</span>\n <p-slider [(ngModel)]=\"vertexEraserRadius\"\n [min]=\"vertexEraserRadiusMin\" [max]=\"vertexEraserRadiusMax\" [step]=\"vertexEraserRadiusStep\"\n (onChange)=\"vertexEraserRadiusChange.emit($event.value)\"\n pTooltip=\"Eraser radius in image pixels.\n Vertices within this distance of the cursor are removed.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n styleClass=\"wand-sensitivity-slider\"></p-slider>\n <span class=\"flex align-items-center mx-1 toolbar-text wand-sensitivity-value\">\n {{ vertexEraserRadius }}</span>\n </ng-container>\n <p-button size=\"small\" [text]=\"true\"\n [disabled]=\"!canUndo\"\n pTooltip=\"Undo the last region action (<kbd>Ctrl</kbd>+<kbd>Z</kbd>).\n Up to 10 actions can be undone.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"undoRegion.emit()\">\n <img src=\"assets/plotting/undo.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n <p-button size=\"small\" [text]=\"true\"\n [disabled]=\"!canRedo\"\n pTooltip=\"Redo the last undone region action\n (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> / <kbd>Ctrl</kbd>+<kbd>Y</kbd>).\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"redoRegion.emit()\">\n <img src=\"assets/plotting/redo.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n <p-button icon=\"pi pi-trash\" size=\"small\" [text]=\"true\"\n pTooltip=\"Delete selected region (<kbd>Del</kbd>)\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"deleteRegion.emit()\">\n </p-button>\n <!-- SAM (jit-ui#90): box-prompt segment, interactive point prompts, and a\n model picker. -->\n <span class=\"toolbar-separator\"></span>\n <!-- SAM controls grouped: box-prompt Segment, then the point-prompt tool,\n then the model-picker dropdown (the chevron) which governs BOTH tools.\n Falls back to Segment + point with no model picker when none are set. -->\n <span *ngIf=\"samModels.length; else segmentPlain\" class=\"p-buttonset segment-split\">\n <p-button size=\"small\" [text]=\"true\"\n pTooltip=\"Segment the drawn rectangles with SAM (box prompt)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"segmentRegions.emit()\">\n <img src=\"assets/plotting/sam-box-prompt.svg\"\n class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n <ng-container [ngTemplateOutlet]=\"samPointBtn\"></ng-container>\n <p-button icon=\"pi pi-chevron-down\" size=\"small\" [text]=\"true\"\n pTooltip=\"Pick the SAM model (used by both segment tools)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"samModelMenu.toggle($event)\">\n </p-button>\n </span>\n <ng-template #segmentPlain>\n <span class=\"p-buttonset segment-split\">\n <p-button size=\"small\" [text]=\"true\"\n pTooltip=\"Segment the drawn rectangles with SAM (box prompt)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"segmentRegions.emit()\">\n <img src=\"assets/plotting/sam-box-prompt.svg\"\n class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n <ng-container [ngTemplateOutlet]=\"samPointBtn\"></ng-container>\n </span>\n </ng-template>\n <!-- Point-prompt tool \u2014 shared between the models / no-models layouts so it\n always sits just left of the model dropdown. -->\n <ng-template #samPointBtn>\n <p-button size=\"small\"\n [text]=\"activeDragMode !== 'samPoint'\"\n pTooltip=\"SAM point prompts: click a fiber to segment it as a new region;\n Shift/Alt-click to refine (exclude) the current one.<br>Esc undoes it.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('samPoint')\">\n <img src=\"assets/plotting/sam-point-prompt.svg\"\n [class]=\"activeDragMode === 'samPoint' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n </ng-template>\n <p-menu #samModelMenu styleClass=\"model-menu\" [model]=\"samMenuItems\" [popup]=\"true\" appendTo=\"body\">\n <ng-template pTemplate=\"item\" let-item>\n <ng-container [ngTemplateOutlet]=\"modelMenuItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"></ng-container>\n </ng-template>\n </p-menu>\n\n <!-- Shared item body for all three model pickers (SAM / YOLO / retinal):\n the standard check-or-blank icon and label, plus an info icon carrying\n the model's description as a hover tooltip. This replaced a single\n \"About the SAM models\" overlay panel \u2014 per-model text sits next to the\n model it describes, and YOLO/retinal get the same affordance.\n\n p-menu puts its click handler on the wrapping `.p-menuitem-content`,\n outside this template, so `command` still fires and the whole row stays\n clickable \u2014 which is also why the info icon has to stop propagation, or\n reading about a model would switch to it. -->\n <ng-template #modelMenuItem let-item>\n <a class=\"p-menuitem-link model-menu-item\">\n <span class=\"p-menuitem-icon\" [ngClass]=\"item.icon\"></span>\n <span class=\"p-menuitem-text\">{{ item.label }}</span>\n <!-- `bottom` like every other tooltip in this toolbar, and for room:\n these menus open under buttons that can sit near the right edge,\n where a `right` tooltip has a few hundred px and squeezes its text\n into a narrow column instead of using its max-width. -->\n <!-- A focusable <button>, not a bare <i>. As an <i> the description\n was reachable only by hovering a mouse: not tabbable, no\n accessible name, nothing for a screen reader to announce \u2014 so\n keyboard and AT users could pick a checkpoint but never read what\n it was. PrimeNG's tooltip shows on focus as well as hover, so\n making it a button restores it for free.\n\n `aria-describedby` is deliberately not used: the tooltip is\n rendered into `body` on demand and has no stable id to point at.\n The description is carried on `aria-label` instead, which is\n announced whether or not the tooltip is showing. It is stripped of\n markup first, since the copy contains `<b>`/`<br>` for the visual\n tooltip and those would otherwise be read out literally. -->\n <button *ngIf=\"item.tooltip\" type=\"button\"\n class=\"pi pi-info-circle model-info-icon\"\n [attr.aria-label]=\"'About ' + item.label + ': ' + plainText(item.tooltip)\"\n [pTooltip]=\"item.tooltip\" [escape]=\"false\" tooltipPosition=\"bottom\"\n appendTo=\"body\" tooltipStyleClass=\"model-info-tooltip\"\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"$event.stopPropagation()\"\n (keydown.space)=\"$event.stopPropagation()\"></button>\n </a>\n </ng-template>\n <p-button size=\"small\" [text]=\"true\"\n pTooltip=\"Auto-segment cells inside each drawn rectangle (cellpose-SAM)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"segmentCellpose.emit()\">\n <img src=\"assets/plotting/cells.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n\n <!-- Contributed no-prompt tools (TOOLBAR_TOOLS). Unlike the SAM and\n cellpose tools these take no prompt \u2014 they sweep the whole current\n view rather than being pointed at something \u2014 so there is nothing to\n draw first. Each renders the same split button: run, pick checkpoint,\n open parameters.\n\n Nothing here names a specific tool. The library ships none; a host\n registers them, which is what lets this package be published without\n the closed model dependencies. -->\n <ng-container *ngFor=\"let tool of contributedTools; trackBy: trackToolById\">\n <span class=\"p-buttonset segment-split\">\n <p-button size=\"small\" [text]=\"true\"\n [pTooltip]=\"tool.runTooltip\"\n tooltipPosition=\"bottom\"\n (onClick)=\"runTool.emit(tool.id)\">\n <!-- `toolbar-icon-dark` is hardcoded rather than bound, unlike the\n toggle tools: these fire a run and have no active state. -->\n <img *ngIf=\"tool.icon.src\" [src]=\"tool.icon.src\" class=\"toolbar-icon-dark\"\n style=\"width: 1rem\" [alt]=\"tool.label\" />\n <i *ngIf=\"tool.icon.pi\" class=\"pi {{ tool.icon.pi }}\"></i>\n </p-button>\n <p-button icon=\"pi pi-chevron-down\" size=\"small\" [text]=\"true\"\n [pTooltip]=\"tool.modelTooltip || 'Pick the model'\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toolMenu.toggle($event)\">\n </p-button>\n <p-button *ngIf=\"tool.params.length\"\n icon=\"pi pi-sliders-h\" size=\"small\" [text]=\"true\"\n [pTooltip]=\"tool.paramsTooltip || 'Parameters'\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openToolParams.emit(tool.id)\">\n </p-button>\n </span>\n <p-menu #toolMenu styleClass=\"model-menu\" [model]=\"toolMenuItems[tool.id] || []\"\n [popup]=\"true\" appendTo=\"body\">\n <ng-template pTemplate=\"item\" let-item>\n <ng-container [ngTemplateOutlet]=\"modelMenuItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"></ng-container>\n </ng-template>\n </p-menu>\n </ng-container>\n </ng-container>\n\n <ng-container *ngIf=\"tools.help\">\n <span class=\"toolbar-separator\"></span>\n\n <!-- Help -->\n <p-button icon=\"pi pi-question-circle\" size=\"small\" [text]=\"true\"\n pTooltip=\"Help\" tooltipPosition=\"bottom\"\n (onClick)=\"showHelp()\">\n </p-button>\n </ng-container>\n\n </div>\n</p-toolbar>\n<p-dialog header=\"Plotting help\" [(visible)]=\"displayHelpDialog\" [modal]=\"true\" [style]=\"{width: '50vw'}\"\n [maximizable]=\"true\" [draggable]=\"false\" [resizable]=\"false\">\n <p>Click a file in the left-hand tree to display it. By default it opens in the\n <b>Image</b> view \u2014 a natively tiled, deeply zoomable raster (OpenSeadragon) with a\n navigator minimap and a physical-units scale bar. <b>Click the image to zoom</b> toward\n the clicked point; scroll to zoom and drag to pan. When a stack is open, a slice slider\n (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\n this toolbar into a 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:\n <b>Image</b>, <b>Heatmap</b>, <b>Contour</b>, <b>Surface</b>, <b>Volume</b>,\n <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\n with what is loaded rather than showing entries that would come up blank:\n </p>\n <ul>\n <li><b>Contour</b>, <b>Volume</b> and <b>Isosurface</b> need a grayscale image; the\n two 3D ones also need a z-stack.</li>\n <li><b>Spatial omics</b> needs a published dataset, and <b>Spatial omics 3D</b> needs\n one whose coordinates carry a z.</li>\n <li>The pixel modes (<b>Image</b>, <b>Heatmap</b>, <b>Surface</b>) are withheld for a\n dataset that brings no tissue image \u2014 seqFISH records coordinates and no section, so\n there is nothing for them to draw.</li>\n </ul>\n <ul>\n <li>\"<b>Isosurface band</b>\" slider (Isosurface): sets the intensity band the surfaces are\n drawn for, mapped onto the volume's real intensity range.</li>\n <li>\"<b>3D camera</b>\" (Surface / Scatter 3D / Isosurface): zoom <i class=\"pi pi-search\"></i>,\n pan <i class=\"pi pi-arrows-alt\"></i>, orbit <i class=\"pi pi-globe\"></i>,\n turntable <i class=\"pi pi-sync\"></i>, and reset camera <i class=\"pi pi-home\"></i>;\n scroll to zoom the 3D scene.</li>\n </ul>\n\n <h3>Image manipulation tools</h3>\n <ul>\n <li>\"<b>Region Editor</b>\" <img src=\"assets/plotting/region-editor.svg\" width=\"15\" height=\"15\">:\n opens the Region Editor as a floating, draggable dialog \u2014 a per-image table of every region\n (class, colour, area) with bulk select/label/colour/delete and GeoJSON import/export. It stays\n in sync with the image, so you can keep it open while drawing.</li>\n <li>\"<b>Intensity profile lines</b>\" <img src=\"assets/plotting/spline-chart.svg\" width=\"15\" height=\"15\">\n (<i>work in progress \u2014 not yet stable</i>): adds a coloured line ROI; a floating inset\n chart plots intensity along each line, updates live as you drag it, and re-samples at the\n current zoom resolution. Each added line gets its own colour; select a line and use the\n trash tool <i class=\"pi pi-trash\"></i> to remove it (and its trace).</li>\n <li>\"<b>Channels & Histogram</b>\" <img src=\"assets/plotting/invert.svg\" width=\"15\" height=\"15\">:\n opens a 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\n dropdown (default: inverted greys).</li>\n <li>\"<b>Smoothen</b>\" <img src=\"assets/plotting/smooth.svg\" width=\"15\" height=\"15\">\n (Image view only): the image opens showing <b>raw pixels</b> (nearest-neighbour), so\n zooming in renders crisp blocks for pixel inspection. Toggle this on for bilinear\n smoothing; the icon turns white while smoothing is active.</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>\"<b>Fit to view</b>\" / autoscale <i class=\"pi pi-window-maximize\"></i>: resets the image\n to its original dimensions. In stack mode it also resets to single-image mode.</li>\n </ul>\n\n <h3>Zoom tools</h3>\n <ul>\n <li>\"<b>Zoom box</b>\" <img src=\"assets/plotting/zoom-box.svg\" width=\"15\" height=\"15\">\n (<kbd>b</kbd>): drag a box to select an area to zoom into.</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>\"<b>Zoom out</b>\" <i class=\"pi pi-search-minus\"></i> (<kbd>-</kbd>): zoom out one step,\n stopping at the original image dimensions.</li>\n </ul>\n\n <h3>Region tools</h3>\n <p>By default no tool is selected (all greyed-out); selecting a tool highlights it. The\n vertex-editing tools (polygon, add/delete vertex, move, B\u00E9zier conversions) are available\n in the <b>Image</b> view only. On a <b>donut</b> (a region with a hole), the inner-ring\n vertices are editable too \u2014 drag, add, and delete them just like the outer ring.</p>\n <ul>\n <li>\"<b>Selection</b>\" <i class=\"pi pi-arrow-up-right\"></i> (<kbd>s</kbd>): a neutral mode\n that turns off whatever drawing tool was active (wand, vertex eraser, rectangle, etc.).\n Use it to step out of editing without picking another tool.</li>\n <li>\"<b>Rectangular region</b>\" <i class=\"pi pi-stop\"></i> (<kbd>r</kbd>): click and drag to\n create a rectangular region.</li>\n <li>\"<b>Polyline</b>\" <img src=\"assets/plotting/polyline.svg\" width=\"14\" height=\"14\">\n (<kbd>l</kbd>): draw an open (non-closed) polyline by clicking and dragging. Polylines can\n be edited and exported to GeoJSON as a <code>LineString</code> feature.</li>\n <li>\"<b>Freeform region</b>\" <i class=\"pi pi-pencil\"></i> (<kbd>f</kbd>): click and drag to\n draw a closed freeform polygon.</li>\n <li>\"<b>Brush</b>\" <img src=\"assets/plotting/paintbrush.svg\" width=\"15\" height=\"15\">: click\n and drag to paint a region with a circular brush \u2014 the painted stroke is the region.\n A <b>Size</b> slider (brush diameter, in image pixels) appears in the toolbar while the\n brush is active. Hold <kbd>Shift</kbd> while painting to <b>erase</b> (subtract) from a\n region instead. Painting over an existing region extends it; brushing across two regions\n merges them. Painting a closed ring (or shift-erasing an interior pocket) leaves the\n enclosed area empty \u2014 a <b>donut</b> (Image view).</li>\n <li>\"<b>Polygon</b>\" <img src=\"assets/plotting/polygon-vertices.svg\" width=\"15\" height=\"15\">:\n click to place each vertex; click the first point to close the polygon.</li>\n <li>\"<b>Add vertex</b>\" <img src=\"assets/plotting/vertex-add.svg\" width=\"15\" height=\"15\">:\n click an edge of the selected polygon to insert a new vertex.</li>\n <li>\"<b>Delete vertex</b>\" <img src=\"assets/plotting/vertex-delete.svg\" width=\"15\" height=\"15\">:\n click a vertex of the selected polygon to remove it.</li>\n <li>\"<b>Move region</b>\" <img src=\"assets/plotting/region-move.svg\" width=\"15\" height=\"15\">:\n drag to move the selected region.</li>\n <li>\"<b>To B\u00E9zier</b>\" <img src=\"assets/plotting/to-bezier.svg\" width=\"15\" height=\"15\"> /\n \"<b>To polygon</b>\" <img src=\"assets/plotting/to-polygon.svg\" width=\"15\" height=\"15\">:\n convert the selected region to a smooth B\u00E9zier curve, or back to a straight-edged polygon.</li>\n <li>\"<b>Wand</b>\" <img src=\"assets/plotting/wand.svg\" width=\"15\" height=\"15\"> (<kbd>w</kbd>):\n click to grow a region from pixels with similar values around the click point. Hold\n <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> for an exact-match flood fill; drag to keep extending the\n selection while moving; hold <kbd>Shift</kbd> while clicking or dragging on an existing\n region to <b>erase</b> pixels from it. Use the <b>Sens.</b> slider that appears to make the\n wand stricter (higher = smaller selection) or looser. Subsequent clicks keep extending the\n same region; clicking inside an existing region adopts it, and growing into another region\n merges them.</li>\n <li>\"<b>Vertex eraser</b>\" <i class=\"pi pi-eraser\"></i> (<kbd>e</kbd>): click or click-and-drag\n to remove polygon vertices that fall inside the cursor circle from any region, including a\n region's inner rings (donut holes). Use the <b>Radius</b> slider to set the eraser size in\n image pixels. Polygons reduced below 3 vertices (or polylines below 2) are deleted entirely;\n a hole reduced below 3 vertices is removed.</li>\n <li>\"<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\n last region action (draw, delete, edit, wand/brush 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.</li>\n <li>\"<b>Redo</b>\" <img src=\"assets/plotting/redo.svg\" width=\"15\" height=\"15\">\n (<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> or <kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Y</kbd>):\n re-applies the most recently undone action. Performing a new region action clears what can be redone.</li>\n <li>\"<b>Delete region</b>\" <i class=\"pi pi-trash\"></i> (<kbd>Del</kbd>): deletes the selected\n region. This can also be done from the Region Editor.</li>\n </ul>\n\n <h3>Region actions (right-click the image)</h3>\n <p>Right-click a region \u2014 or the current multi-selection \u2014 to open the\n \"<b>Selected region(s)</b>\" group at the top of the context menu. These act immediately on\n the selection (they're not drawing modes) and are fully undo/redo-able:</p>\n <ul>\n <li>\"<b>Select all regions</b>\" <i class=\"pi pi-check-square\"></i>: select every region on\n the image (also in the Region Editor toolbar).</li>\n <li>\"<b>Merge / group</b>\": combine 2+ selected regions into one. If the pieces don't touch,\n the result is a single multi-part region (sparse \"bits\").</li>\n <li>\"<b>Ungroup</b>\": split a multi-part region back into one region per piece.</li>\n <li>\"<b>Inverse</b>\": replace the selection with everything <i>outside</i> it \u2014 the image\n rectangle with the selected regions punched out as holes. Works at any image size.</li>\n <li>\"<b>Simplify</b>\": reduce a region's vertex count (Douglas\u2013Peucker) by an altitude\n threshold in pixels \u2014 pick <b>Light</b> (1 px), <b>Medium</b> (3 px),\n <b>Strong</b> (8 px), or <b>Custom\u2026</b> for a slider. Applies to the exterior and any\n holes (and every part of a multi-part region).</li>\n <li>The B\u00E9zier conversions and Delete also live in this group (the right-click equivalents of\n the toolbar / vertex tools above).</li>\n </ul>\n\n <h3>Segmentation & detection tools</h3>\n <p>AI-assisted segmentation and detection that run entirely in the browser. The first run\n of any model downloads it (a progress toast is shown); it's then cached.</p>\n <p>Two shapes of tool: the <b>prompted</b> ones (Segment, Point prompts, Cellpose) act on\n rectangles you draw, and generated regions inherit the color of the rectangle they came\n from<span *ngIf=\"contributedTools.length\">; the <b>no-prompt</b> ones ({{ contributedToolNames }})\n sweep the whole current view, so there is nothing to draw first</span>.</p>\n <p>Each model dropdown <i class=\"pi pi-chevron-down\"></i> lists the available checkpoints with\n a <i class=\"pi pi-info-circle\"></i> beside each one \u2014 hover it for that model's size, speed,\n training domain and accuracy, so you can pick without leaving the toolbar.</p>\n <ul>\n <li>\"<b>Segment</b>\" <img src=\"assets/plotting/sam-box-prompt.svg\" width=\"15\" height=\"15\">\n (box prompt): draw one or more <b>Rectangular regions</b> around the objects of interest,\n then click Segment. Each rectangle is replaced by the SAM mask of the object inside it.</li>\n <li>\"<b>Point prompts</b>\" <img src=\"assets/plotting/sam-point-prompt.svg\" width=\"15\" height=\"15\">: click directly on an object to\n segment it as a <b>new</b> region. Each click starts a fresh object \u2014 clicking another\n 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,\n <kbd>Enter</kbd> commits it.</li>\n <li>\"<b>Model picker</b>\" <i class=\"pi pi-chevron-down\"></i>: the dropdown attached to the\n Segment button chooses the SAM model \u2014 <b>micro-sam ViT-T</b> (default; small, fast) or\n <b>micro-sam ViT-B</b> (larger, highest accuracy) for light microscopy, or\n <b>patho-sam ViT-B</b> for histopathology (H&E / stained tissue). All are promptable\n Segment Anything models finetuned for biomedical images; they differ in the <i>image\n encoder</i> (the heavy once-per-image step) and the training domain \u2014 box and point\n prompting behave the same. The choice applies to <i>both</i> the box and point tools, and\n each tool loads its own encoder, so the first click after switching re-encodes once.</li>\n <li>\"<b>Cellpose</b>\" <img src=\"assets/plotting/cells.svg\" width=\"15\" height=\"15\">: draw rectangles, then click Cellpose to\n automatically segment every cell inside each rectangle (client-side cellpose-SAM) \u2014 one\n region per detected cell. No clicking on individual cells needed.</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 </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\n its own coordinates, over 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\n the two are exclusive so what drives the colours is never ambiguous.\n </p>\n <ul>\n <li>\"<b>Select from ROIs</b>\": turns the regions you have drawn into a selection of\n observations. Everything else then follows that selection.</li>\n <li>\"<b>Point size</b>\" / \"<b>Opacity</b>\": how the markers are drawn, not what is\n 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\n coloured by, so the two can never disagree. Each tab answers a different question, and\n the <i class=\"pi pi-question-circle\"></i> beside them explains the one on screen \u2014\n including what it must <em>not</em> be read for.\n </p>\n <ul>\n <li>\"<b>Counts</b>\": how many observations per category. A category is a label, not a\n magnitude, so a frequency is the only distribution it has.</li>\n <li>\"<b>Histogram</b>\" / \"<b>Violin</b>\" / \"<b>Box</b>\": how a continuous value is\n distributed \u2014 over everything, or split by a grouping column.</li>\n <li>\"<b>Heatmap</b>\": mean expression of the genes you pick within each group.\n Z-scored per gene by default, so a colour means \"above or below this gene's own\n average\" rather than \"highly expressed\".</li>\n <li>\"<b>Embedding</b>\": a UMAP, PCA or t-SNE over the same cells. <b>Lasso it to\n select on the map</b> \u2014 both views read the same selection. Only PCA labels its axes\n with a percentage, because only its axes explain a measurable share of the variance;\n UMAP and t-SNE coordinates are arbitrary, so distance between distant clusters\n should not be read.</li>\n <li>\"<b>Detach</b>\": moves the charts into their own window so they can sit beside the\n map instead of inside the panel. Switching tabs keeps the window where it is.</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><td><kbd>+</kbd> / <kbd>=</kbd></td><td>Zoom in</td></tr>\n <tr><td><kbd>-</kbd></td><td>Zoom out</td></tr>\n <tr><td><kbd>b</kbd></td><td>Toggle zoom box mode</td></tr>\n <tr><td><kbd>p</kbd></td><td>Toggle pan mode</td></tr>\n <tr><td><kbd>f</kbd></td><td>Toggle freeform region drawing</td></tr>\n <tr><td><kbd>l</kbd></td><td>Toggle polyline drawing</td></tr>\n <tr><td><kbd>r</kbd></td><td>Toggle rectangular region drawing</td></tr>\n <tr><td><kbd>w</kbd></td><td>Toggle wand tool</td></tr>\n <tr><td>Wand + <kbd>Shift</kbd></td><td>Erase pixels from the active region</td></tr>\n <tr><td>Wand + <kbd>Ctrl</kbd>/<kbd>Cmd</kbd></td><td>Exact-match flood fill (no smoothing)</td></tr>\n <tr><td><kbd>e</kbd></td><td>Toggle vertex eraser</td></tr>\n <tr><td><kbd>s</kbd></td><td>Selection mode (deactivates drawing tools)</td></tr>\n <tr><td>Point prompt + <kbd>Shift</kbd>/<kbd>Alt</kbd></td><td>Add an exclude point (refine the current object)</td></tr>\n <tr><td><kbd>Enter</kbd></td><td>Commit the current SAM point object</td></tr>\n <tr><td><kbd>Esc</kbd></td><td>Undo the current SAM point object</td></tr>\n <tr><td><kbd>Del</kbd> / <kbd>\u232B</kbd></td><td>Delete selected region</td></tr>\n <tr><td><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Z</kbd></td><td>Undo the last region action (up to 10)</td></tr>\n <tr><td><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> / <kbd>Ctrl</kbd>+<kbd>Y</kbd></td><td>Redo the last undone region action</td></tr>\n </table>\n\n <ng-template pTemplate=\"footer\">\n <p-button icon=\"pi pi-check\" (click)=\"displayHelpDialog=false\" label=\"Ok\" styleClass=\"p-button-text\">\n </p-button>\n </ng-template>\n</p-dialog>\n", styles: [":host{display:block}:host ::ng-deep .p-toolbar{padding:.25rem .5rem}.toolbar-text{color:var(--primary-color);font-size:.875rem}::ng-deep .toolbar-input .p-inputtext{color:var(--primary-color);border-color:var(--primary-color)}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-icon-active{filter:brightness(0) invert(1)}::ng-deep .plotmode-item{display:inline-flex;align-items:center;gap:.5rem;color:#000}::ng-deep .plotmode-item i{font-size:1rem;color:var(--primary-color)}::ng-deep .plotmode-item .plotmode-icon-img{width:1.1rem;height:1.1rem;object-fit:contain;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .p-menu.model-menu{width:auto;min-width:12.5rem}::ng-deep .model-menu-item{display:flex;align-items:center;gap:.5rem}::ng-deep .model-menu-item .p-menuitem-text{flex:1 1 auto;white-space:nowrap}::ng-deep .model-menu-item .model-info-icon{flex:0 0 auto;font-size:.875rem;opacity:.55;cursor:help;appearance:none;background:none;border:0;padding:0;margin:0;color:inherit;line-height:1}::ng-deep .model-menu-item .model-info-icon:hover{opacity:1}::ng-deep .model-menu-item .model-info-icon:focus-visible{opacity:1;outline:2px solid var(--primary-color, #2196f3);outline-offset:2px;border-radius:2px}::ng-deep .model-info-tooltip .p-tooltip-text{min-width:18rem;max-width:24rem;line-height:1.4}::ng-deep .wand-sensitivity-slider{width:8rem;align-self:center;margin:0 .25rem}.wand-sensitivity-value{min-width:2rem;text-align:right}span.colormap{width:50px;height:auto;display:inline-block}img.colormap{width:50px;height:auto}.colormap{background:no-repeat;background-size:100%;vertical-align:middle}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: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { 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: "component", type: i3.Toolbar, selector: "p-toolbar", inputs: ["style", "styleClass", "ariaLabelledBy"] }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: i5.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: "component", type: i6.Menu, selector: "p-menu", inputs: ["model", "popup", "style", "styleClass", "appendTo", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "ariaLabel", "ariaLabelledBy", "id", "tabindex"], outputs: ["onShow", "onHide", "onBlur", "onFocus"] }, { 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: i8.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "component", type: i9.Slider, selector: "p-slider", inputs: ["animate", "disabled", "min", "max", "orientation", "step", "range", "style", "styleClass", "ariaLabel", "ariaLabelledBy", "tabindex", "autofocus"], outputs: ["onChange", "onSlideEnd"] }, { kind: "component", type: i10.InputNumber, selector: "p-inputNumber", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "style", "placeholder", "size", "maxlength", "tabindex", "title", "ariaLabelledBy", "ariaLabel", "ariaRequired", "name", "required", "autocomplete", "min", "max", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "step", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "variant", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus", "disabled"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "component", type: i9$1.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: "directive", type: i12.Ripple, selector: "[pRipple]" }] }); }
|
|
6646
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: ToolbarComponent, selector: "plotting-toolbar", inputs: { imageInfo: "imageInfo", plotTypeOptions: "plotTypeOptions", selectedPlotType: "selectedPlotType", basePlotType: "basePlotType", isoRange: "isoRange", maxIndex: "maxIndex", zIndex: "zIndex", isHeatmap: "isHeatmap", is3dRegions: "is3dRegions", activeDragMode: "activeDragMode", activeSurface3dMode: "activeSurface3dMode", axesVisible: "axesVisible", wireframeActive: "wireframeActive", resolutionScale: "resolutionScale", wandSensitivity: "wandSensitivity", brushSize: "brushSize", vertexEraserRadius: "vertexEraserRadius", canUndo: "canUndo", canRedo: "canRedo", stackOptions: "stackOptions", tools: "tools", imageSmoothingEnabled: "imageSmoothingEnabled", samModels: "samModels", samModelId: "samModelId", contributedTools: "contributedTools", toolModelIds: "toolModelIds" }, outputs: { selectPlotType: "selectPlotType", addProfileLine: "addProfileLine", toggleImageSmoothing: "toggleImageSmoothing", isoRangeChange: "isoRangeChange", openChannelHistogram: "openChannelHistogram", openSpatialControls: "openSpatialControls", openRegionEditor: "openRegionEditor", selectStackOption: "selectStackOption", zScrub: "zScrub", zSlide: "zSlide", zIndexInput: "zIndexInput", reloadAndPlot: "reloadAndPlot", downloadImage: "downloadImage", autoscaleImage: "autoscaleImage", toggleDragMode: "toggleDragMode", zoomIn: "zoomIn", zoomOut: "zoomOut", toggleSurface3dMode: "toggleSurface3dMode", resetSurfaceCamera: "resetSurfaceCamera", toggleAxes: "toggleAxes", toggleWireframe: "toggleWireframe", selectResolution: "selectResolution", deleteRegion: "deleteRegion", undoRegion: "undoRegion", redoRegion: "redoRegion", segmentRegions: "segmentRegions", segmentCellpose: "segmentCellpose", runTool: "runTool", toolModelChange: "toolModelChange", openToolParams: "openToolParams", samModelChange: "samModelChange", wandSensitivityChange: "wandSensitivityChange", brushSizeChange: "brushSizeChange", vertexEraserRadiusChange: "vertexEraserRadiusChange" }, usesOnChanges: true, ngImport: i0, template: "<p-toolbar>\n <div class=\"p-toolbar-group-start flex-wrap align-items-center gap-1\">\n\n <!-- Host-provided custom toolbar buttons, shown first (e.g. app-specific\n actions the visualization view itself doesn't own). -->\n <ng-content></ng-content>\n\n <!-- Plot type: available for any image (grayscale or RGB). -->\n <ng-container *ngIf=\"imageInfo && tools.specialTools\">\n <span class=\"toolbar-separator\"></span>\n <p-dropdown [options]=\"plotTypeOptions\" optionLabel=\"label\" optionValue=\"type\"\n [(ngModel)]=\"selectedPlotType\" appendTo=\"body\"\n pTooltip=\"Select plot type\" tooltipPosition=\"bottom\"\n [style]=\"{'min-width': '11rem'}\"\n styleClass=\"plotmode-dropdown\" panelStyleClass=\"plotmode-panel\"\n (onChange)=\"selectPlotType.emit($event.value)\">\n <ng-template let-opt pTemplate=\"selectedItem\">\n <span class=\"plotmode-item\" *ngIf=\"opt\">\n <i *ngIf=\"isPiIcon(opt.icon)\" [class]=\"opt.icon\"></i>\n <img *ngIf=\"opt.icon && !isPiIcon(opt.icon)\" class=\"plotmode-icon-img\" [src]=\"opt.icon\" alt=\"\" />\n <span>{{ opt.label }}</span>\n </span>\n </ng-template>\n <ng-template let-opt pTemplate=\"item\">\n <span class=\"plotmode-item\">\n <i *ngIf=\"isPiIcon(opt.icon)\" [class]=\"opt.icon\"></i>\n <img *ngIf=\"opt.icon && !isPiIcon(opt.icon)\" class=\"plotmode-icon-img\" [src]=\"opt.icon\" alt=\"\" />\n <span>{{ opt.label }}</span>\n </span>\n </ng-template>\n </p-dropdown>\n <ng-container *ngIf=\"isIsosurfaceMode\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Iso</span>\n <p-slider [(ngModel)]=\"isoRange\" [range]=\"true\"\n [min]=\"isoValueMin\" [max]=\"isoValueMax\" [step]=\"isoValueStep\"\n (onChange)=\"isoRangeChange.emit($event.values)\"\n pTooltip=\"Isosurface band (0\u2013255), mapped onto the volume's\n actual intensity range. Surfaces are drawn for voxels whose\n intensity falls in the selected band \u2014 narrow it to isolate\n features, widen it to see the whole structure.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n styleClass=\"wand-sensitivity-slider\"></p-slider>\n <span class=\"flex align-items-center mx-1 toolbar-text wand-sensitivity-value\">\n {{ isoRange[0] }}\u2013{{ isoRange[1] }}</span>\n </ng-container>\n <!-- Intensity tools (Heatmap / Image mode): add another colored line ROI.\n Each added line gets the next bright palette colour and its own inset\n trace; select a line on the image and use the trash tool to remove it\n (and its trace). The lines are Region-based but excluded from the\n Region Editor and exports. -->\n <ng-container *ngIf=\"isIntensityCapable\">\n <span class=\"toolbar-separator\"></span>\n <p-button size=\"small\" [text]=\"true\"\n pTooltip=\"Add an intensity line (new colour). Click a line to select it,\n then use the trash tool to remove it.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"addProfileLine.emit()\">\n <img src=\"assets/plotting/spline-chart.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n </ng-container>\n </ng-container>\n <!-- Channels & Histogram: brightness/contrast, gamma, per-channel display,\n colormap/reverse/invert \u2014 opens the floating dialog. -->\n <p-button *ngIf=\"tools.specialTools\" pRipple size=\"small\" [text]=\"true\"\n pTooltip=\"Channels & Histogram (brightness/contrast, colormap)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openChannelHistogram.emit()\">\n <img src=\"assets/plotting/invert.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n\n <!-- Spatial-omics controls. Only meaningful in the spatial mode, which is\n itself only offered while a dataset is loaded \u2014 so this button appears\n exactly when there is something for it to control. -->\n <p-button *ngIf=\"tools.specialTools && isSpatialMode\" pRipple size=\"small\" [text]=\"true\"\n pTooltip=\"Spatial omics (colour by column or gene, legend, selection, distributions)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openSpatialControls.emit()\">\n <img src=\"assets/plotting/spatial-omics.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n\n <!-- Smoothen toggle: bilinear smoothing vs nearest-neighbour raw pixels (Image\n view only \u2014 it's an OpenSeadragon drawer setting). Highlighted when smoothing on. -->\n <p-button *ngIf=\"tools.specialTools && isImageView\" size=\"small\"\n [text]=\"!imageSmoothingEnabled\"\n pTooltip=\"Smoothen (turn on image smoothing)\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleImageSmoothing.emit()\">\n <img src=\"assets/plotting/smooth.svg\" style=\"width: 1rem\"\n [class]=\"imageSmoothingEnabled ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"/>\n </p-button>\n\n <!-- Stack controls -->\n <ng-container *ngIf=\"imageInfo?.isStack\">\n <span class=\"toolbar-separator\"></span>\n <!-- Image view: scrub z-slices live (the renderer swaps slices in place). -->\n <ng-container *ngIf=\"showsLiveSliceScrubber\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Slice</span>\n <p-slider [(ngModel)]=\"zIndex\" [min]=\"0\" [max]=\"maxIndex\"\n (onChange)=\"zScrub.emit($event.value)\" (onSlideEnd)=\"zSlide.emit($event.value)\"\n pTooltip=\"Slice through the stack (\u2190 / \u2192 keys)\" tooltipPosition=\"bottom\"\n [style]=\"{'width':'16rem'}\" styleClass=\"wand-sensitivity-slider\"></p-slider>\n <span class=\"flex align-items-center mx-1 toolbar-text\">{{zIndex}} of {{maxIndex}}</span>\n </ng-container>\n <!-- Heatmap view: single-image (high-def zoom) vs stack (frame slider). Plotly-only \u2014\n hidden for napari, which has no single/stack toggle or per-frame fetch, and for the\n spatial-omics modes: a 3D omics dataset publishes its reference volume as a z-stack\n image, and these modes draw observations, not one of its slices. -->\n <ng-container *ngIf=\"!showsLiveSliceScrubber && !isNapariMode && !isSpatialMode\">\n <p-button icon=\"pi pi-image\" size=\"small\"\n [text]=\"imageInfo?.showStack\"\n pTooltip=\"Single image mode: enables high-def zoom\" tooltipPosition=\"bottom\"\n (onClick)=\"selectStackOption.emit(stackOptions[0])\">\n </p-button>\n <p-button icon=\"pi pi-images\" size=\"small\"\n [text]=\"!imageInfo?.showStack\"\n pTooltip=\"Stack mode, enables slicing through image frames\" tooltipPosition=\"bottom\"\n (onClick)=\"selectStackOption.emit(stackOptions[1])\">\n </p-button>\n <ng-container *ngIf=\"!imageInfo?.showStack\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Slice</span>\n <div class=\"p-inputgroup\" style=\"width: auto\">\n <p-inputNumber styleClass=\"w-3rem toolbar-input\"\n\t\t\t\t\tpTooltip=\"View image in stack (⏎)\"\n\t\t\t\t\t[(ngModel)]=\"zIndex\" [min]=\"0\" [max]=\"maxIndex\"\n (onInput)=\"zIndexInput.emit(zIndex)\" (keydown.enter)=\"reloadAndPlot.emit()\"/>\n <p-button icon=\"pi pi-refresh\" size=\"small\" [text]=\"true\" (onClick)=\"reloadAndPlot.emit()\"\n pTooltip=\"Plot this slice\" tooltipPosition=\"bottom\">\n </p-button>\n </div>\n <span class=\"flex align-items-center mx-1 toolbar-text\">of {{maxIndex}}</span>\n </ng-container>\n </ng-container>\n </ng-container>\n\n <!-- Image actions (special tools) -->\n <ng-container *ngIf=\"tools.specialTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button icon=\"pi pi-download\" size=\"small\" [text]=\"true\"\n pTooltip=\"Download image as PNG\" tooltipPosition=\"bottom\"\n (onClick)=\"downloadImage.emit()\">\n </p-button>\n <p-button *ngIf=\"isHeatmap\" icon=\"pi pi-window-maximize\" size=\"small\" [text]=\"true\"\n pTooltip=\"Fit to view\" tooltipPosition=\"bottom\"\n (onClick)=\"autoscaleImage.emit()\">\n </p-button>\n </ng-container>\n\n <!-- Zoom tools. Plotly rubber-band drag-zoom: on OpenSeadragon the box-zoom\n covers this and a bare drag pans natively, so it's hidden in image view. -->\n <ng-container *ngIf=\"isHeatmap && tools.zoomTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button *ngIf=\"!isImageView\" icon=\"pi pi-search\" size=\"small\"\n [text]=\"activeDragMode !== 'zoom'\"\n pTooltip=\"Zoom drag\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('zoom')\">\n </p-button>\n <p-button size=\"small\"\n [text]=\"activeDragMode !== 'zoomToBox'\"\n pTooltip=\"Zoom box (<kbd>b</kbd>)\" [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('zoomToBox')\">\n <img src=\"assets/plotting/zoom-box.svg\"\n [class]=\"activeDragMode === 'zoomToBox' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n <p-button icon=\"pi pi-arrows-alt\" size=\"small\"\n [text]=\"activeDragMode !== 'pan'\"\n pTooltip=\"Pan the image (<kbd>p</kbd>)\" [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('pan')\">\n </p-button>\n <p-button icon=\"pi pi-search-plus\" size=\"small\" [text]=\"true\"\n pTooltip=\"Zoom in (<kbd>+</kbd>)\" [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"zoomIn.emit()\">\n </p-button>\n <p-button icon=\"pi pi-search-minus\" size=\"small\" [text]=\"true\"\n pTooltip=\"Zoom out (<kbd>-</kbd>)\" [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"zoomOut.emit()\">\n </p-button>\n </ng-container>\n\n <!-- Surface 3D controls -->\n <ng-container *ngIf=\"!isHeatmap\">\n <span class=\"toolbar-separator\"></span>\n <p-button icon=\"pi pi-search\" size=\"small\"\n [text]=\"activeSurface3dMode !== 'zoom'\"\n pTooltip=\"Zoom\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('zoom')\">\n </p-button>\n <p-button icon=\"pi pi-arrows-alt\" size=\"small\"\n [text]=\"activeSurface3dMode !== 'pan'\"\n pTooltip=\"Pan\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('pan')\">\n </p-button>\n <p-button icon=\"pi pi-globe\" size=\"small\"\n [text]=\"activeSurface3dMode !== 'orbit'\"\n pTooltip=\"Orbital rotation\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('orbit')\">\n </p-button>\n <p-button icon=\"pi pi-sync\" size=\"small\"\n [text]=\"activeSurface3dMode !== 'turntable'\"\n pTooltip=\"Turntable rotation\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('turntable')\">\n </p-button>\n <p-button icon=\"pi pi-home\" size=\"small\" [text]=\"true\"\n pTooltip=\"Reset camera to default\" tooltipPosition=\"bottom\"\n (onClick)=\"resetSurfaceCamera.emit()\">\n </p-button>\n <!-- 3D coordinate-axes / scale gizmo toggle (napari-js WebGPU volume/isosurface only).\n `p-button-icon-only` gives the same square sizing as the neighbouring pi-icon buttons\n (the projected <img> doesn't trigger PrimeNG's automatic icon-only sizing). -->\n <p-button size=\"small\" *ngIf=\"isNapariMode\" styleClass=\"p-button-icon-only\"\n [text]=\"!axesVisible\"\n pTooltip=\"Toggle 3D axes / scale\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleAxes.emit()\">\n <img src=\"assets/plotting/3d-coordinates.svg\"\n [class]=\"axesVisible ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n <!-- Surface wireframe toggle (napari-js WebGPU surface only). -->\n <p-button icon=\"pi pi-share-alt\" size=\"small\" *ngIf=\"isNapariSurfaceMode\"\n [text]=\"!wireframeActive\"\n pTooltip=\"Toggle wireframe\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleWireframe.emit()\">\n </p-button>\n <!-- Resolution (decimate factor) for the napari 3D types \u2014 reloads at coarser/finer sampling. -->\n <ng-container *ngIf=\"isNapari3dMode\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Res</span>\n <p-dropdown [options]=\"decimateOptions\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"resolutionScale\" appendTo=\"body\"\n pTooltip=\"Resolution (decimate factor)\" tooltipPosition=\"bottom\"\n [style]=\"{'min-width': '6rem'}\"\n (onChange)=\"selectResolution.emit($event.value)\">\n </p-dropdown>\n </ng-container>\n </ng-container>\n\n <ng-container *ngIf=\"(isHeatmap || is3dRegions) && tools.regionTools\">\n <span class=\"toolbar-separator\"></span>\n <!-- Region Editor: opens the editor as a floating dialog (formerly the\n right-panel Regions tab). Sits with the region drawing tools. -->\n <p-button size=\"small\" [text]=\"true\"\n pTooltip=\"Open the Region Editor\" tooltipPosition=\"bottom\"\n (onClick)=\"openRegionEditor.emit()\">\n <img src=\"assets/plotting/region-editor.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n <!-- Region drawing tools -->\n <p-button icon=\"pi pi-arrow-up-right\" size=\"small\"\n [text]=\"activeDragMode !== 'select'\"\n pTooltip=\"Selection mode (<kbd>s</kbd>)\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('select')\">\n </p-button>\n <p-button icon=\"pi pi-stop\" size=\"small\"\n [text]=\"activeDragMode !== 'drawrect'\"\n pTooltip=\"Draw a rectangular region (<kbd>r</kbd>)\" [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('drawrect')\">\n </p-button>\n <p-button *ngIf=\"!is3dRegions\" size=\"small\"\n [text]=\"activeDragMode !== 'drawopenpath'\"\n pTooltip=\"Draw a polyline (<kbd>l</kbd>)\" [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('drawopenpath')\">\n <img src=\"assets/plotting/polyline.svg\"\n [class]=\"activeDragMode === 'drawopenpath' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n <p-button icon=\"pi pi-pencil\" size=\"small\"\n [text]=\"activeDragMode !== 'drawclosedpath'\"\n pTooltip=\"Draw a freeform region (<kbd>f</kbd>)\" [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('drawclosedpath')\">\n </p-button>\n <!-- Brush: paint a region by dragging a disc of the given size; the Size\n slider appears while it's active. Hold Shift to erase (subtract). -->\n <!-- Brush and polyline are absent from a 3D cloud: the brush paints in IMAGE\n pixels and there is no raster to sample, and an open polyline is a\n profile tool, not a closed region a selection can test against. -->\n <p-button *ngIf=\"!is3dRegions\" size=\"small\"\n [text]=\"activeDragMode !== 'brush'\"\n pTooltip=\"Brush: click and drag to paint a region.\n <br>Hold <kbd>Shift</kbd> while painting to erase from a region.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('brush')\">\n <img src=\"assets/plotting/paintbrush.svg\"\n [class]=\"activeDragMode === 'brush' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n <ng-container *ngIf=\"activeDragMode === 'brush'\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Size</span>\n <p-slider [(ngModel)]=\"brushSize\"\n [min]=\"brushSizeMin\" [max]=\"brushSizeMax\" [step]=\"brushSizeStep\"\n (onChange)=\"brushSizeChange.emit($event.value)\"\n pTooltip=\"Brush diameter in image pixels.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n styleClass=\"wand-sensitivity-slider\"></p-slider>\n <span class=\"flex align-items-center mx-1 toolbar-text wand-sensitivity-value\">\n {{ brushSize }}</span>\n </ng-container>\n\n <!-- Vertex editing \u2014 backends with a vertex-editing overlay (OSD + napari-js image). -->\n <ng-container *ngIf=\"supportsRegionVertexTools\">\n <p-button size=\"small\"\n [text]=\"activeDragMode !== 'drawpolygon'\"\n pTooltip=\"Polygon: click to place each vertex; click the first point to close\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('drawpolygon')\">\n <img src=\"assets/plotting/polygon-vertices.svg\"\n [class]=\"activeDragMode === 'drawpolygon' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n <p-button size=\"small\"\n [text]=\"activeDragMode !== 'addpoint'\"\n pTooltip=\"Add a vertex: click an edge of the selected polygon\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('addpoint')\">\n <img src=\"assets/plotting/vertex-add.svg\"\n [class]=\"activeDragMode === 'addpoint' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n <p-button size=\"small\"\n [text]=\"activeDragMode !== 'deletepoint'\"\n pTooltip=\"Delete a vertex: click a vertex of the selected polygon\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('deletepoint')\">\n <img src=\"assets/plotting/vertex-delete.svg\"\n [class]=\"activeDragMode === 'deletepoint' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n </ng-container>\n\n <p-button size=\"small\"\n [text]=\"activeDragMode !== 'wand'\"\n pTooltip=\"Wand: click and drag to grow a region by similar pixel values (<kbd>w</kbd>).\n <br>Hold <kbd>Shift</kbd> while clicking or dragging on a region to erase pixels from it.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('wand')\">\n <img src=\"assets/plotting/wand.svg\"\n [class]=\"activeDragMode === 'wand' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n <ng-container *ngIf=\"activeDragMode === 'wand'\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Sens.</span>\n <p-slider [(ngModel)]=\"wandSensitivity\"\n [min]=\"wandSensitivityMin\" [max]=\"wandSensitivityMax\" [step]=\"wandSensitivityStep\"\n (onChange)=\"wandSensitivityChange.emit($event.value)\"\n pTooltip=\"Higher = stricter (smaller selection); lower = looser.\n Takes effect from the next click/drag tick.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n styleClass=\"wand-sensitivity-slider\"></p-slider>\n <span class=\"flex align-items-center mx-1 toolbar-text wand-sensitivity-value\">\n {{ wandSensitivity.toFixed(1) }}</span>\n </ng-container>\n <p-button icon=\"pi pi-eraser\" size=\"small\"\n [text]=\"activeDragMode !== 'eraseVertex'\"\n pTooltip=\"Vertex eraser (<kbd>e</kbd>): click or drag to remove polygon vertices inside\n the cursor circle from any region.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('eraseVertex')\">\n </p-button>\n <ng-container *ngIf=\"activeDragMode === 'eraseVertex'\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Radius</span>\n <p-slider [(ngModel)]=\"vertexEraserRadius\"\n [min]=\"vertexEraserRadiusMin\" [max]=\"vertexEraserRadiusMax\" [step]=\"vertexEraserRadiusStep\"\n (onChange)=\"vertexEraserRadiusChange.emit($event.value)\"\n pTooltip=\"Eraser radius in image pixels.\n Vertices within this distance of the cursor are removed.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n styleClass=\"wand-sensitivity-slider\"></p-slider>\n <span class=\"flex align-items-center mx-1 toolbar-text wand-sensitivity-value\">\n {{ vertexEraserRadius }}</span>\n </ng-container>\n <p-button size=\"small\" [text]=\"true\"\n [disabled]=\"!canUndo\"\n pTooltip=\"Undo the last region action (<kbd>Ctrl</kbd>+<kbd>Z</kbd>).\n Up to 10 actions can be undone.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"undoRegion.emit()\">\n <img src=\"assets/plotting/undo.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n <p-button size=\"small\" [text]=\"true\"\n [disabled]=\"!canRedo\"\n pTooltip=\"Redo the last undone region action\n (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> / <kbd>Ctrl</kbd>+<kbd>Y</kbd>).\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"redoRegion.emit()\">\n <img src=\"assets/plotting/redo.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n <p-button icon=\"pi pi-trash\" size=\"small\" [text]=\"true\"\n pTooltip=\"Delete selected region (<kbd>Del</kbd>)\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"deleteRegion.emit()\">\n </p-button>\n <!-- SAM (jit-ui#90): box-prompt segment, interactive point prompts, and a\n model picker. -->\n <span class=\"toolbar-separator\"></span>\n <!-- SAM controls grouped: box-prompt Segment, then the point-prompt tool,\n then the model-picker dropdown (the chevron) which governs BOTH tools.\n Falls back to Segment + point with no model picker when none are set. -->\n <span *ngIf=\"samModels.length; else segmentPlain\" class=\"p-buttonset segment-split\">\n <p-button size=\"small\" [text]=\"true\"\n pTooltip=\"Segment the drawn rectangles with SAM (box prompt)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"segmentRegions.emit()\">\n <img src=\"assets/plotting/sam-box-prompt.svg\"\n class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n <ng-container [ngTemplateOutlet]=\"samPointBtn\"></ng-container>\n <p-button icon=\"pi pi-chevron-down\" size=\"small\" [text]=\"true\"\n pTooltip=\"Pick the SAM model (used by both segment tools)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"samModelMenu.toggle($event)\">\n </p-button>\n </span>\n <ng-template #segmentPlain>\n <span class=\"p-buttonset segment-split\">\n <p-button size=\"small\" [text]=\"true\"\n pTooltip=\"Segment the drawn rectangles with SAM (box prompt)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"segmentRegions.emit()\">\n <img src=\"assets/plotting/sam-box-prompt.svg\"\n class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n <ng-container [ngTemplateOutlet]=\"samPointBtn\"></ng-container>\n </span>\n </ng-template>\n <!-- Point-prompt tool \u2014 shared between the models / no-models layouts so it\n always sits just left of the model dropdown. -->\n <ng-template #samPointBtn>\n <p-button size=\"small\"\n [text]=\"activeDragMode !== 'samPoint'\"\n pTooltip=\"SAM point prompts: click a fiber to segment it as a new region;\n Shift/Alt-click to refine (exclude) the current one.<br>Esc undoes it.\"\n [escape]=\"false\" tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('samPoint')\">\n <img src=\"assets/plotting/sam-point-prompt.svg\"\n [class]=\"activeDragMode === 'samPoint' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"/>\n </p-button>\n </ng-template>\n <p-menu #samModelMenu styleClass=\"model-menu\" [model]=\"samMenuItems\" [popup]=\"true\" appendTo=\"body\">\n <ng-template pTemplate=\"item\" let-item>\n <ng-container [ngTemplateOutlet]=\"modelMenuItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"></ng-container>\n </ng-template>\n </p-menu>\n\n <!-- Shared item body for all three model pickers (SAM / YOLO / retinal):\n the standard check-or-blank icon and label, plus an info icon carrying\n the model's description as a hover tooltip. This replaced a single\n \"About the SAM models\" overlay panel \u2014 per-model text sits next to the\n model it describes, and YOLO/retinal get the same affordance.\n\n p-menu puts its click handler on the wrapping `.p-menuitem-content`,\n outside this template, so `command` still fires and the whole row stays\n clickable \u2014 which is also why the info icon has to stop propagation, or\n reading about a model would switch to it. -->\n <ng-template #modelMenuItem let-item>\n <a class=\"p-menuitem-link model-menu-item\">\n <span class=\"p-menuitem-icon\" [ngClass]=\"item.icon\"></span>\n <span class=\"p-menuitem-text\">{{ item.label }}</span>\n <!-- `bottom` like every other tooltip in this toolbar, and for room:\n these menus open under buttons that can sit near the right edge,\n where a `right` tooltip has a few hundred px and squeezes its text\n into a narrow column instead of using its max-width. -->\n <!-- A focusable <button>, not a bare <i>. As an <i> the description\n was reachable only by hovering a mouse: not tabbable, no\n accessible name, nothing for a screen reader to announce \u2014 so\n keyboard and AT users could pick a checkpoint but never read what\n it was. PrimeNG's tooltip shows on focus as well as hover, so\n making it a button restores it for free.\n\n `aria-describedby` is deliberately not used: the tooltip is\n rendered into `body` on demand and has no stable id to point at.\n The description is carried on `aria-label` instead, which is\n announced whether or not the tooltip is showing. It is stripped of\n markup first, since the copy contains `<b>`/`<br>` for the visual\n tooltip and those would otherwise be read out literally. -->\n <button *ngIf=\"item.tooltip\" type=\"button\"\n class=\"pi pi-info-circle model-info-icon\"\n [attr.aria-label]=\"'About ' + item.label + ': ' + plainText(item.tooltip)\"\n [pTooltip]=\"item.tooltip\" [escape]=\"false\" tooltipPosition=\"bottom\"\n appendTo=\"body\" tooltipStyleClass=\"model-info-tooltip\"\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"$event.stopPropagation()\"\n (keydown.space)=\"$event.stopPropagation()\"></button>\n </a>\n </ng-template>\n <p-button size=\"small\" [text]=\"true\"\n pTooltip=\"Auto-segment cells inside each drawn rectangle (cellpose-SAM)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"segmentCellpose.emit()\">\n <img src=\"assets/plotting/cells.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\"/>\n </p-button>\n\n <!-- Contributed no-prompt tools (TOOLBAR_TOOLS). Unlike the SAM and\n cellpose tools these take no prompt \u2014 they sweep the whole current\n view rather than being pointed at something \u2014 so there is nothing to\n draw first. Each renders the same split button: run, pick checkpoint,\n open parameters.\n\n Nothing here names a specific tool. The library ships none; a host\n registers them, which is what lets this package be published without\n the closed model dependencies. -->\n <ng-container *ngFor=\"let tool of contributedTools; trackBy: trackToolById\">\n <span class=\"p-buttonset segment-split\">\n <p-button size=\"small\" [text]=\"true\"\n [pTooltip]=\"tool.runTooltip\"\n tooltipPosition=\"bottom\"\n (onClick)=\"runTool.emit(tool.id)\">\n <!-- `toolbar-icon-dark` is hardcoded rather than bound, unlike the\n toggle tools: these fire a run and have no active state. -->\n <img *ngIf=\"tool.icon.src\" [src]=\"tool.icon.src\" class=\"toolbar-icon-dark\"\n style=\"width: 1rem\" [alt]=\"tool.label\" />\n <i *ngIf=\"tool.icon.pi\" class=\"pi {{ tool.icon.pi }}\"></i>\n </p-button>\n <p-button icon=\"pi pi-chevron-down\" size=\"small\" [text]=\"true\"\n [pTooltip]=\"tool.modelTooltip || 'Pick the model'\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toolMenu.toggle($event)\">\n </p-button>\n <p-button *ngIf=\"tool.params.length\"\n icon=\"pi pi-sliders-h\" size=\"small\" [text]=\"true\"\n [pTooltip]=\"tool.paramsTooltip || 'Parameters'\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openToolParams.emit(tool.id)\">\n </p-button>\n </span>\n <p-menu #toolMenu styleClass=\"model-menu\" [model]=\"toolMenuItems[tool.id] || []\"\n [popup]=\"true\" appendTo=\"body\">\n <ng-template pTemplate=\"item\" let-item>\n <ng-container [ngTemplateOutlet]=\"modelMenuItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"></ng-container>\n </ng-template>\n </p-menu>\n </ng-container>\n </ng-container>\n\n <ng-container *ngIf=\"tools.help\">\n <span class=\"toolbar-separator\"></span>\n\n <!-- Help -->\n <p-button icon=\"pi pi-question-circle\" size=\"small\" [text]=\"true\"\n pTooltip=\"Help\" tooltipPosition=\"bottom\"\n (onClick)=\"showHelp()\">\n </p-button>\n </ng-container>\n\n </div>\n</p-toolbar>\n<p-dialog header=\"Plotting help\" [(visible)]=\"displayHelpDialog\" [modal]=\"true\" [style]=\"{width: '50vw'}\"\n [maximizable]=\"true\" [draggable]=\"false\" [resizable]=\"false\">\n <p>Click a file in the left-hand tree to display it. By default it opens in the\n <b>Image</b> view \u2014 a natively tiled, deeply zoomable raster (OpenSeadragon) with a\n navigator minimap and a physical-units scale bar. <b>Click the image to zoom</b> toward\n the clicked point; scroll to zoom and drag to pan. When a stack is open, a slice slider\n (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\n this toolbar into a 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:\n <b>Image</b>, <b>Heatmap</b>, <b>Contour</b>, <b>Surface</b>, <b>Volume</b>,\n <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\n with what is loaded rather than showing entries that would come up blank:\n </p>\n <ul>\n <li><b>Contour</b>, <b>Volume</b> and <b>Isosurface</b> need a grayscale image; the\n two 3D ones also need a z-stack.</li>\n <li><b>Spatial omics</b> needs a published dataset, and <b>Spatial omics 3D</b> needs\n one whose coordinates carry a z.</li>\n <li>The pixel modes (<b>Image</b>, <b>Heatmap</b>, <b>Surface</b>) are withheld for a\n dataset that brings no tissue image \u2014 seqFISH records coordinates and no section, so\n there is nothing for them to draw.</li>\n </ul>\n <ul>\n <li>\"<b>Isosurface band</b>\" slider (Isosurface): sets the intensity band the surfaces are\n drawn for, mapped onto the volume's real intensity range.</li>\n <li>\"<b>3D camera</b>\" (Surface / Scatter 3D / Isosurface): zoom <i class=\"pi pi-search\"></i>,\n pan <i class=\"pi pi-arrows-alt\"></i>, orbit <i class=\"pi pi-globe\"></i>,\n turntable <i class=\"pi pi-sync\"></i>, and reset camera <i class=\"pi pi-home\"></i>;\n scroll to zoom the 3D scene.</li>\n </ul>\n\n <h3>Image manipulation tools</h3>\n <ul>\n <li>\"<b>Region Editor</b>\" <img src=\"assets/plotting/region-editor.svg\" width=\"15\" height=\"15\">:\n opens the Region Editor as a floating, draggable dialog \u2014 a per-image table of every region\n (class, colour, area) with bulk select/label/colour/delete and GeoJSON import/export. It stays\n in sync with the image, so you can keep it open while drawing.</li>\n <li>\"<b>Intensity profile lines</b>\" <img src=\"assets/plotting/spline-chart.svg\" width=\"15\" height=\"15\">\n (<i>work in progress \u2014 not yet stable</i>): adds a coloured line ROI; a floating inset\n chart plots intensity along each line, updates live as you drag it, and re-samples at the\n current zoom resolution. Each added line gets its own colour; select a line and use the\n trash tool <i class=\"pi pi-trash\"></i> to remove it (and its trace).</li>\n <li>\"<b>Channels & Histogram</b>\" <img src=\"assets/plotting/invert.svg\" width=\"15\" height=\"15\">:\n opens a 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\n dropdown (default: inverted greys).</li>\n <li>\"<b>Smoothen</b>\" <img src=\"assets/plotting/smooth.svg\" width=\"15\" height=\"15\">\n (Image view only): the image opens showing <b>raw pixels</b> (nearest-neighbour), so\n zooming in renders crisp blocks for pixel inspection. Toggle this on for bilinear\n smoothing; the icon turns white while smoothing is active.</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>\"<b>Fit to view</b>\" / autoscale <i class=\"pi pi-window-maximize\"></i>: resets the image\n to its original dimensions. In stack mode it also resets to single-image mode.</li>\n </ul>\n\n <h3>Zoom tools</h3>\n <ul>\n <li>\"<b>Zoom box</b>\" <img src=\"assets/plotting/zoom-box.svg\" width=\"15\" height=\"15\">\n (<kbd>b</kbd>): drag a box to select an area to zoom into.</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>\"<b>Zoom out</b>\" <i class=\"pi pi-search-minus\"></i> (<kbd>-</kbd>): zoom out one step,\n stopping at the original image dimensions.</li>\n </ul>\n\n <h3>Region tools</h3>\n <p>By default no tool is selected (all greyed-out); selecting a tool highlights it. The\n vertex-editing tools (polygon, add/delete vertex, move, B\u00E9zier conversions) are available\n in the <b>Image</b> view only. On a <b>donut</b> (a region with a hole), the inner-ring\n vertices are editable too \u2014 drag, add, and delete them just like the outer ring.</p>\n <ul>\n <li>\"<b>Selection</b>\" <i class=\"pi pi-arrow-up-right\"></i> (<kbd>s</kbd>): a neutral mode\n that turns off whatever drawing tool was active (wand, vertex eraser, rectangle, etc.).\n Use it to step out of editing without picking another tool.</li>\n <li>\"<b>Rectangular region</b>\" <i class=\"pi pi-stop\"></i> (<kbd>r</kbd>): click and drag to\n create a rectangular region.</li>\n <li>\"<b>Polyline</b>\" <img src=\"assets/plotting/polyline.svg\" width=\"14\" height=\"14\">\n (<kbd>l</kbd>): draw an open (non-closed) polyline by clicking and dragging. Polylines can\n be edited and exported to GeoJSON as a <code>LineString</code> feature.</li>\n <li>\"<b>Freeform region</b>\" <i class=\"pi pi-pencil\"></i> (<kbd>f</kbd>): click and drag to\n draw a closed freeform polygon.</li>\n <li>\"<b>Brush</b>\" <img src=\"assets/plotting/paintbrush.svg\" width=\"15\" height=\"15\">: click\n and drag to paint a region with a circular brush \u2014 the painted stroke is the region.\n A <b>Size</b> slider (brush diameter, in image pixels) appears in the toolbar while the\n brush is active. Hold <kbd>Shift</kbd> while painting to <b>erase</b> (subtract) from a\n region instead. Painting over an existing region extends it; brushing across two regions\n merges them. Painting a closed ring (or shift-erasing an interior pocket) leaves the\n enclosed area empty \u2014 a <b>donut</b> (Image view).</li>\n <li>\"<b>Polygon</b>\" <img src=\"assets/plotting/polygon-vertices.svg\" width=\"15\" height=\"15\">:\n click to place each vertex; click the first point to close the polygon.</li>\n <li>\"<b>Add vertex</b>\" <img src=\"assets/plotting/vertex-add.svg\" width=\"15\" height=\"15\">:\n click an edge of the selected polygon to insert a new vertex.</li>\n <li>\"<b>Delete vertex</b>\" <img src=\"assets/plotting/vertex-delete.svg\" width=\"15\" height=\"15\">:\n click a vertex of the selected polygon to remove it.</li>\n <li>\"<b>Move region</b>\" <img src=\"assets/plotting/region-move.svg\" width=\"15\" height=\"15\">:\n drag to move the selected region.</li>\n <li>\"<b>To B\u00E9zier</b>\" <img src=\"assets/plotting/to-bezier.svg\" width=\"15\" height=\"15\"> /\n \"<b>To polygon</b>\" <img src=\"assets/plotting/to-polygon.svg\" width=\"15\" height=\"15\">:\n convert the selected region to a smooth B\u00E9zier curve, or back to a straight-edged polygon.</li>\n <li>\"<b>Wand</b>\" <img src=\"assets/plotting/wand.svg\" width=\"15\" height=\"15\"> (<kbd>w</kbd>):\n click to grow a region from pixels with similar values around the click point. Hold\n <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> for an exact-match flood fill; drag to keep extending the\n selection while moving; hold <kbd>Shift</kbd> while clicking or dragging on an existing\n region to <b>erase</b> pixels from it. Use the <b>Sens.</b> slider that appears to make the\n wand stricter (higher = smaller selection) or looser. Subsequent clicks keep extending the\n same region; clicking inside an existing region adopts it, and growing into another region\n merges them.</li>\n <li>\"<b>Vertex eraser</b>\" <i class=\"pi pi-eraser\"></i> (<kbd>e</kbd>): click or click-and-drag\n to remove polygon vertices that fall inside the cursor circle from any region, including a\n region's inner rings (donut holes). Use the <b>Radius</b> slider to set the eraser size in\n image pixels. Polygons reduced below 3 vertices (or polylines below 2) are deleted entirely;\n a hole reduced below 3 vertices is removed.</li>\n <li>\"<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\n last region action (draw, delete, edit, wand/brush 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.</li>\n <li>\"<b>Redo</b>\" <img src=\"assets/plotting/redo.svg\" width=\"15\" height=\"15\">\n (<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> or <kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Y</kbd>):\n re-applies the most recently undone action. Performing a new region action clears what can be redone.</li>\n <li>\"<b>Delete region</b>\" <i class=\"pi pi-trash\"></i> (<kbd>Del</kbd>): deletes the selected\n region. This can also be done from the Region Editor.</li>\n </ul>\n\n <h3>Region actions (right-click the image)</h3>\n <p>Right-click a region \u2014 or the current multi-selection \u2014 to open the\n \"<b>Selected region(s)</b>\" group at the top of the context menu. These act immediately on\n the selection (they're not drawing modes) and are fully undo/redo-able:</p>\n <ul>\n <li>\"<b>Select all regions</b>\" <i class=\"pi pi-check-square\"></i>: select every region on\n the image (also in the Region Editor toolbar).</li>\n <li>\"<b>Merge / group</b>\": combine 2+ selected regions into one. If the pieces don't touch,\n the result is a single multi-part region (sparse \"bits\").</li>\n <li>\"<b>Ungroup</b>\": split a multi-part region back into one region per piece.</li>\n <li>\"<b>Inverse</b>\": replace the selection with everything <i>outside</i> it \u2014 the image\n rectangle with the selected regions punched out as holes. Works at any image size.</li>\n <li>\"<b>Simplify</b>\": reduce a region's vertex count (Douglas\u2013Peucker) by an altitude\n threshold in pixels \u2014 pick <b>Light</b> (1 px), <b>Medium</b> (3 px),\n <b>Strong</b> (8 px), or <b>Custom\u2026</b> for a slider. Applies to the exterior and any\n holes (and every part of a multi-part region).</li>\n <li>The B\u00E9zier conversions and Delete also live in this group (the right-click equivalents of\n the toolbar / vertex tools above).</li>\n </ul>\n\n <h3>Segmentation & detection tools</h3>\n <p>AI-assisted segmentation and detection that run entirely in the browser. The first run\n of any model downloads it (a progress toast is shown); it's then cached.</p>\n <p>Two shapes of tool: the <b>prompted</b> ones (Segment, Point prompts, Cellpose) act on\n rectangles you draw, and generated regions inherit the color of the rectangle they came\n from<span *ngIf=\"contributedTools.length\">; the <b>no-prompt</b> ones ({{ contributedToolNames }})\n sweep the whole current view, so there is nothing to draw first</span>.</p>\n <p>Each model dropdown <i class=\"pi pi-chevron-down\"></i> lists the available checkpoints with\n a <i class=\"pi pi-info-circle\"></i> beside each one \u2014 hover it for that model's size, speed,\n training domain and accuracy, so you can pick without leaving the toolbar.</p>\n <ul>\n <li>\"<b>Segment</b>\" <img src=\"assets/plotting/sam-box-prompt.svg\" width=\"15\" height=\"15\">\n (box prompt): draw one or more <b>Rectangular regions</b> around the objects of interest,\n then click Segment. Each rectangle is replaced by the SAM mask of the object inside it.</li>\n <li>\"<b>Point prompts</b>\" <img src=\"assets/plotting/sam-point-prompt.svg\" width=\"15\" height=\"15\">: click directly on an object to\n segment it as a <b>new</b> region. Each click starts a fresh object \u2014 clicking another\n 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,\n <kbd>Enter</kbd> commits it.</li>\n <li>\"<b>Model picker</b>\" <i class=\"pi pi-chevron-down\"></i>: the dropdown attached to the\n Segment button chooses the SAM model \u2014 <b>micro-sam ViT-T</b> (default; small, fast) or\n <b>micro-sam ViT-B</b> (larger, highest accuracy) for light microscopy, or\n <b>patho-sam ViT-B</b> for histopathology (H&E / stained tissue). All are promptable\n Segment Anything models finetuned for biomedical images; they differ in the <i>image\n encoder</i> (the heavy once-per-image step) and the training domain \u2014 box and point\n prompting behave the same. The choice applies to <i>both</i> the box and point tools, and\n each tool loads its own encoder, so the first click after switching re-encodes once.</li>\n <li>\"<b>Cellpose</b>\" <img src=\"assets/plotting/cells.svg\" width=\"15\" height=\"15\">: draw rectangles, then click Cellpose to\n automatically segment every cell inside each rectangle (client-side cellpose-SAM) \u2014 one\n region per detected cell. No clicking on individual cells needed.</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 </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\n its own coordinates, over 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\n the two are exclusive so what drives the colours is never ambiguous.\n </p>\n <ul>\n <li>\"<b>Select from ROIs</b>\": turns the regions you have drawn into a selection of\n observations. Everything else then follows that selection.</li>\n <li>\"<b>Point size</b>\" / \"<b>Opacity</b>\": how the markers are drawn, not what is\n 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\n coloured by, so the two can never disagree. Each tab answers a different question, and\n the <i class=\"pi pi-question-circle\"></i> beside them explains the one on screen \u2014\n including what it must <em>not</em> be read for.\n </p>\n <ul>\n <li>\"<b>Counts</b>\": how many observations per category. A category is a label, not a\n magnitude, so a frequency is the only distribution it has.</li>\n <li>\"<b>Histogram</b>\" / \"<b>Violin</b>\" / \"<b>Box</b>\": how a continuous value is\n distributed \u2014 over everything, or split by a grouping column.</li>\n <li>\"<b>Heatmap</b>\": mean expression of the genes you pick within each group.\n Z-scored per gene by default, so a colour means \"above or below this gene's own\n average\" rather than \"highly expressed\".</li>\n <li>\"<b>Embedding</b>\": a UMAP, PCA or t-SNE over the same cells. <b>Lasso it to\n select on the map</b> \u2014 both views read the same selection. Only PCA labels its axes\n with a percentage, because only its axes explain a measurable share of the variance;\n UMAP and t-SNE coordinates are arbitrary, so distance between distant clusters\n should not be read.</li>\n <li>\"<b>Detach</b>\": moves the charts into their own window so they can sit beside the\n map instead of inside the panel. Switching tabs keeps the window where it is.</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><td><kbd>+</kbd> / <kbd>=</kbd></td><td>Zoom in</td></tr>\n <tr><td><kbd>-</kbd></td><td>Zoom out</td></tr>\n <tr><td><kbd>b</kbd></td><td>Toggle zoom box mode</td></tr>\n <tr><td><kbd>p</kbd></td><td>Toggle pan mode</td></tr>\n <tr><td><kbd>f</kbd></td><td>Toggle freeform region drawing</td></tr>\n <tr><td><kbd>l</kbd></td><td>Toggle polyline drawing</td></tr>\n <tr><td><kbd>r</kbd></td><td>Toggle rectangular region drawing</td></tr>\n <tr><td><kbd>w</kbd></td><td>Toggle wand tool</td></tr>\n <tr><td>Wand + <kbd>Shift</kbd></td><td>Erase pixels from the active region</td></tr>\n <tr><td>Wand + <kbd>Ctrl</kbd>/<kbd>Cmd</kbd></td><td>Exact-match flood fill (no smoothing)</td></tr>\n <tr><td><kbd>e</kbd></td><td>Toggle vertex eraser</td></tr>\n <tr><td><kbd>s</kbd></td><td>Selection mode (deactivates drawing tools)</td></tr>\n <tr><td>Point prompt + <kbd>Shift</kbd>/<kbd>Alt</kbd></td><td>Add an exclude point (refine the current object)</td></tr>\n <tr><td><kbd>Enter</kbd></td><td>Commit the current SAM point object</td></tr>\n <tr><td><kbd>Esc</kbd></td><td>Undo the current SAM point object</td></tr>\n <tr><td><kbd>Del</kbd> / <kbd>\u232B</kbd></td><td>Delete selected region</td></tr>\n <tr><td><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Z</kbd></td><td>Undo the last region action (up to 10)</td></tr>\n <tr><td><kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd> / <kbd>Ctrl</kbd>+<kbd>Y</kbd></td><td>Redo the last undone region action</td></tr>\n </table>\n\n <ng-template pTemplate=\"footer\">\n <p-button icon=\"pi pi-check\" (click)=\"displayHelpDialog=false\" label=\"Ok\" styleClass=\"p-button-text\">\n </p-button>\n </ng-template>\n</p-dialog>\n", styles: [":host{display:block}:host ::ng-deep .p-toolbar{padding:.25rem .5rem}.toolbar-text{color:var(--primary-color);font-size:.875rem}::ng-deep .toolbar-input .p-inputtext{color:var(--primary-color);border-color:var(--primary-color)}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-icon-active{filter:brightness(0) invert(1)}::ng-deep .plotmode-item{display:inline-flex;align-items:center;gap:.5rem;color:#000}::ng-deep .plotmode-item i{font-size:1rem;color:var(--primary-color)}::ng-deep .plotmode-item .plotmode-icon-img{width:1.1rem;height:1.1rem;object-fit:contain;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .p-menu.model-menu{width:auto;min-width:12.5rem}::ng-deep .model-menu-item{display:flex;align-items:center;gap:.5rem}::ng-deep .model-menu-item .p-menuitem-text{flex:1 1 auto;white-space:nowrap}::ng-deep .model-menu-item .model-info-icon{flex:0 0 auto;font-size:.875rem;opacity:.55;cursor:help;appearance:none;background:none;border:0;padding:0;margin:0;color:inherit;line-height:1}::ng-deep .model-menu-item .model-info-icon:hover{opacity:1}::ng-deep .model-menu-item .model-info-icon:focus-visible{opacity:1;outline:2px solid var(--primary-color, #2196f3);outline-offset:2px;border-radius:2px}::ng-deep .model-info-tooltip .p-tooltip-text{min-width:18rem;max-width:24rem;line-height:1.4}::ng-deep .wand-sensitivity-slider{width:8rem;align-self:center;margin:0 .25rem}.wand-sensitivity-value{min-width:2rem;text-align:right}span.colormap{width:50px;height:auto;display:inline-block}img.colormap{width:50px;height:auto}.colormap{background:no-repeat;background-size:100%;vertical-align:middle}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: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { 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: "component", type: i3.Toolbar, selector: "p-toolbar", inputs: ["style", "styleClass", "ariaLabelledBy"] }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: i5.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: "component", type: i6.Menu, selector: "p-menu", inputs: ["model", "popup", "style", "styleClass", "appendTo", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "ariaLabel", "ariaLabelledBy", "id", "tabindex"], outputs: ["onShow", "onHide", "onBlur", "onFocus"] }, { 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: i8.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "component", type: i9.Slider, selector: "p-slider", inputs: ["animate", "disabled", "min", "max", "orientation", "step", "range", "style", "styleClass", "ariaLabel", "ariaLabelledBy", "tabindex", "autofocus"], outputs: ["onChange", "onSlideEnd"] }, { kind: "component", type: i10.InputNumber, selector: "p-inputNumber", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "style", "placeholder", "size", "maxlength", "tabindex", "title", "ariaLabelledBy", "ariaLabel", "ariaRequired", "name", "required", "autocomplete", "min", "max", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "step", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "variant", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus", "disabled"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "component", type: i9$1.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: "directive", type: i12.Ripple, selector: "[pRipple]" }] }); }
|
|
6234
6647
|
}
|
|
6235
6648
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ToolbarComponent, decorators: [{
|
|
6236
6649
|
type: Component,
|
|
@@ -6241,6 +6654,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
|
|
|
6241
6654
|
type: Input
|
|
6242
6655
|
}], selectedPlotType: [{
|
|
6243
6656
|
type: Input
|
|
6657
|
+
}], basePlotType: [{
|
|
6658
|
+
type: Input
|
|
6244
6659
|
}], isoRange: [{
|
|
6245
6660
|
type: Input
|
|
6246
6661
|
}], maxIndex: [{
|
|
@@ -11111,21 +11526,44 @@ class VisualizerComponent {
|
|
|
11111
11526
|
get isCaching() {
|
|
11112
11527
|
return this.cacheProgress !== null && this.cacheProgress < 100;
|
|
11113
11528
|
}
|
|
11529
|
+
/**
|
|
11530
|
+
* The built-in plot type being rendered. With no contributed mode active this
|
|
11531
|
+
* is the selection itself; during one it is the mode's `baseType`. Kept with its
|
|
11532
|
+
* original `PlotType` type for existing readers — use {@link selectedPlotTypeId}
|
|
11533
|
+
* for the selector's id. Assigning selects that built-in type.
|
|
11534
|
+
*/
|
|
11535
|
+
get selectedPlotType() {
|
|
11536
|
+
return this.basePlotType;
|
|
11537
|
+
}
|
|
11538
|
+
set selectedPlotType(type) {
|
|
11539
|
+
this.selectedPlotTypeId = type;
|
|
11540
|
+
}
|
|
11541
|
+
/** Where a `mount` panel's host element is attached once the dialog renders. */
|
|
11542
|
+
set plotModePanelSlot(ref) {
|
|
11543
|
+
const host = this.plotModePanel?.host;
|
|
11544
|
+
if (ref && host && host.parentElement !== ref.nativeElement)
|
|
11545
|
+
ref.nativeElement.appendChild(host);
|
|
11546
|
+
}
|
|
11547
|
+
/** The built-in type actually rendered for {@link selectedPlotTypeId}: itself
|
|
11548
|
+
* for a built-in type, `baseType` for a contributed mode. */
|
|
11549
|
+
get basePlotType() {
|
|
11550
|
+
return this.plotModes.baseTypeOf(this.selectedPlotTypeId);
|
|
11551
|
+
}
|
|
11114
11552
|
/** True while the 3D spatial cloud is the active mode. The controls drop the ROI
|
|
11115
11553
|
* selection there: region tools are screen-space, and against an orbiting
|
|
11116
11554
|
* camera a drawn rectangle has no fixed meaning in the data. */
|
|
11117
11555
|
get isSpatial3dMode() {
|
|
11118
|
-
return isSpatialOmics3d(this.
|
|
11556
|
+
return isSpatialOmics3d(this.basePlotType);
|
|
11119
11557
|
}
|
|
11120
11558
|
/** LINE plot type shows the image + draggable line ROI + intensity inset. */
|
|
11121
11559
|
get isProfileMode() {
|
|
11122
|
-
return this.
|
|
11560
|
+
return this.basePlotType === PlotType.LINE;
|
|
11123
11561
|
}
|
|
11124
11562
|
/** True for the Image plot type, which renders as a natively pan/zoom-able
|
|
11125
11563
|
* raster — so the backend-agnostic zoom/pan toolbar tools are hidden. The
|
|
11126
11564
|
* component drives this off the plot type, not the active backend. */
|
|
11127
11565
|
get isImageView() {
|
|
11128
|
-
return this.
|
|
11566
|
+
return this.basePlotType === PlotType.IMAGE;
|
|
11129
11567
|
}
|
|
11130
11568
|
constructor(state, plotService, messageService, ngZone, cdr, session, samTool, cellSegmentTool, samPointTool, regionOps, vizConfig,
|
|
11131
11569
|
// A constructor parameter rather than a field `inject()`: this component is
|
|
@@ -11134,7 +11572,13 @@ class VisualizerComponent {
|
|
|
11134
11572
|
toolContributions,
|
|
11135
11573
|
// Optional like the rest: a host that shows only images never provides it,
|
|
11136
11574
|
// and the spatial plot types simply stay hidden.
|
|
11137
|
-
spatialData
|
|
11575
|
+
spatialData,
|
|
11576
|
+
// Contributed plot modes. Like TOOLBAR_TOOLS the token has no factory, so a
|
|
11577
|
+
// host that provides none gets null here and the selector is unchanged.
|
|
11578
|
+
plotTypeContributions,
|
|
11579
|
+
// Parent of a contributed panel component's injector. Optional only so the
|
|
11580
|
+
// specs can construct this component with `new`.
|
|
11581
|
+
injector) {
|
|
11138
11582
|
this.state = state;
|
|
11139
11583
|
this.plotService = plotService;
|
|
11140
11584
|
this.messageService = messageService;
|
|
@@ -11147,6 +11591,7 @@ class VisualizerComponent {
|
|
|
11147
11591
|
this.regionOps = regionOps;
|
|
11148
11592
|
this.vizConfig = vizConfig;
|
|
11149
11593
|
this.spatialData = spatialData;
|
|
11594
|
+
this.injector = injector;
|
|
11150
11595
|
this.vizToastKey = VIZ_TOAST_KEY;
|
|
11151
11596
|
this.vizAlertToastKey = VIZ_ALERT_TOAST_KEY;
|
|
11152
11597
|
this.isStackEvent = new EventEmitter(false);
|
|
@@ -11280,9 +11725,28 @@ class VisualizerComponent {
|
|
|
11280
11725
|
this.wireframeActive = false;
|
|
11281
11726
|
/** napari 3D decimate factor (1 = Full … 8 = ⅛; default ½); changing it re-plots. */
|
|
11282
11727
|
this.resolutionScale = NAPARI_DEFAULT_DECIMATE;
|
|
11283
|
-
/**
|
|
11728
|
+
/** The selector's entries: the built-in plot types the active backend
|
|
11729
|
+
* advertises (3D gated by capability), then any contributed modes
|
|
11730
|
+
* ({@link PLOT_TYPE_CONTRIBUTIONS}). */
|
|
11731
|
+
this.plotTypeMenu = [];
|
|
11732
|
+
/**
|
|
11733
|
+
* The built-in plot types on offer — {@link plotTypeMenu} without contributed
|
|
11734
|
+
* modes. Kept with its original type so existing readers are unaffected.
|
|
11735
|
+
*/
|
|
11284
11736
|
this.plotTypeOptions = [];
|
|
11285
|
-
|
|
11737
|
+
/**
|
|
11738
|
+
* What the selector shows: a built-in type or a contributed mode's id. Every
|
|
11739
|
+
* rendering and tool decision goes through {@link basePlotType} instead, so a
|
|
11740
|
+
* contributed mode behaves exactly like the built-in type it rides on.
|
|
11741
|
+
*/
|
|
11742
|
+
this.selectedPlotTypeId = PlotType.IMAGE;
|
|
11743
|
+
/**
|
|
11744
|
+
* The live contributed mode's side panel, as the template renders it: an
|
|
11745
|
+
* Angular component with its own injector, or a plain host element a `mount`
|
|
11746
|
+
* panel rendered into. Null while no contributed session is live.
|
|
11747
|
+
*/
|
|
11748
|
+
this.plotModePanel = null;
|
|
11749
|
+
this.destroying = false;
|
|
11286
11750
|
/** Floating intensity-profile inset (LINE mode). */
|
|
11287
11751
|
this.intensityInsetDiv = 'intensity-inset-plot';
|
|
11288
11752
|
this.profilePanelPos = { x: 20, y: 70 };
|
|
@@ -11333,6 +11797,11 @@ class VisualizerComponent {
|
|
|
11333
11797
|
/** Whether the live spatial dataset brings pixels of its own — a tissue image it
|
|
11334
11798
|
* registers onto, or a volume that is published as a z-stack image. */
|
|
11335
11799
|
this.spatialDatasetHasPixels = false;
|
|
11800
|
+
this.plotModes = new PlotModeController(plotTypeContributions, {
|
|
11801
|
+
onActivated: (active) => this.showPlotModePanel(active),
|
|
11802
|
+
onDeactivating: () => this.hidePlotModePanel(),
|
|
11803
|
+
onFailed: (contribution) => this.fallBackFromPlotMode(contribution),
|
|
11804
|
+
});
|
|
11336
11805
|
this.contributedTools = visibleToolContributions(toolContributions);
|
|
11337
11806
|
this.colormapsOptions = plotService.getColormapOptions();
|
|
11338
11807
|
this.computePlotTypeOptions();
|
|
@@ -11423,7 +11892,7 @@ class VisualizerComponent {
|
|
|
11423
11892
|
// with an image-less 2D dataset, which is why it went unhandled — before
|
|
11424
11893
|
// it, image-less meant 3D.
|
|
11425
11894
|
const target = has3d ? PlotType.SPATIAL_OMICS_3D : PlotType.SPATIAL_OMICS;
|
|
11426
|
-
if (this.
|
|
11895
|
+
if (this.selectedPlotTypeId !== target)
|
|
11427
11896
|
this.onSelectPlotType(target);
|
|
11428
11897
|
}
|
|
11429
11898
|
}
|
|
@@ -11471,7 +11940,7 @@ class VisualizerComponent {
|
|
|
11471
11940
|
}
|
|
11472
11941
|
// Pick the mode first so the image lands in the view that will show it,
|
|
11473
11942
|
// instead of rendering once into whatever mode the last dataset left active.
|
|
11474
|
-
if (this.
|
|
11943
|
+
if (this.selectedPlotTypeId !== PlotType.IMAGE)
|
|
11475
11944
|
this.onSelectPlotType(PlotType.IMAGE);
|
|
11476
11945
|
this.revokeVolumeImageUrls();
|
|
11477
11946
|
this.volumeImageUrls = built.urls;
|
|
@@ -11482,7 +11951,7 @@ class VisualizerComponent {
|
|
|
11482
11951
|
// it rather than leaving the host on an Image view with nothing in it.
|
|
11483
11952
|
console.warn('[visualizer] reference volume unavailable — falling back to the 3D cloud', err);
|
|
11484
11953
|
this.volumeImageKey = null;
|
|
11485
|
-
if (this.hasSpatial3dDataset && !isSpatialOmics3d(this.
|
|
11954
|
+
if (this.hasSpatial3dDataset && !isSpatialOmics3d(this.basePlotType)) {
|
|
11486
11955
|
this.onSelectPlotType(PlotType.SPATIAL_OMICS_3D);
|
|
11487
11956
|
}
|
|
11488
11957
|
}
|
|
@@ -11511,12 +11980,7 @@ class VisualizerComponent {
|
|
|
11511
11980
|
// though the RGB composite isn't grayscale — don't gate them out.
|
|
11512
11981
|
const m0 = this.imageInfo?.imageMeta?.[0];
|
|
11513
11982
|
const isMultichannel = (m0?.channelCount ?? 1) > 1 && (m0?.rgbChannels ?? 1) < 3;
|
|
11514
|
-
|
|
11515
|
-
.filter((d) => {
|
|
11516
|
-
// Outside test mode, only the curated set (those with a productionLabel)
|
|
11517
|
-
// is offered; test mode exposes every backend's type.
|
|
11518
|
-
if (!this.testMode && !d.productionLabel)
|
|
11519
|
-
return false;
|
|
11983
|
+
const passesGates = (d) => {
|
|
11520
11984
|
if (d.dimensions === '3d' && !caps.has(ViewerFeature.Surface3D))
|
|
11521
11985
|
return false;
|
|
11522
11986
|
// Volume and Isosurface raymarch the IMAGE STACK, and nothing else: these
|
|
@@ -11544,10 +12008,37 @@ class VisualizerComponent {
|
|
|
11544
12008
|
&& !this.spatialDatasetHasPixels)
|
|
11545
12009
|
return false;
|
|
11546
12010
|
return true;
|
|
11547
|
-
}
|
|
11548
|
-
|
|
11549
|
-
|
|
11550
|
-
|
|
12011
|
+
};
|
|
12012
|
+
// Outside test mode, only the curated set (those with a productionLabel)
|
|
12013
|
+
// is offered; test mode exposes every backend's type.
|
|
12014
|
+
const curated = (d) => this.testMode || !!d.productionLabel;
|
|
12015
|
+
const builtIn = this.plotService.getPlotTypeDescriptors()
|
|
12016
|
+
.filter((d) => curated(d) && passesGates(d));
|
|
12017
|
+
// Contributed modes come after every built-in one, under the same label rule
|
|
12018
|
+
// and their own stack/grayscale/spatial gates. A mode also needs whatever its base
|
|
12019
|
+
// view needs (pixels, a 3D-capable backend), so the base type's gates apply
|
|
12020
|
+
// too — a contributed Image-based mode goes wherever Image goes.
|
|
12021
|
+
const contributed = [];
|
|
12022
|
+
for (const d of this.plotModes.descriptors()) {
|
|
12023
|
+
if (!curated(d))
|
|
12024
|
+
continue;
|
|
12025
|
+
if (d.requiresStack && !isStack)
|
|
12026
|
+
continue;
|
|
12027
|
+
if (d.requiresGrayscale && !isGrayscale && !isMultichannel)
|
|
12028
|
+
continue;
|
|
12029
|
+
if (d.requiresSpatialData && !this.hasSpatialDataset)
|
|
12030
|
+
continue;
|
|
12031
|
+
if (d.requiresSpatial3d && !this.hasSpatial3dDataset)
|
|
12032
|
+
continue;
|
|
12033
|
+
const base = getPlotTypeDescriptor(d.baseType);
|
|
12034
|
+
if (base && passesGates(base))
|
|
12035
|
+
contributed.push(contributedPlotTypeOption(d, base));
|
|
12036
|
+
}
|
|
12037
|
+
// Default selector shows the suffix-free productionLabel; test mode keeps
|
|
12038
|
+
// the full backend-suffixed label so same-named modes stay distinguishable.
|
|
12039
|
+
const labelled = (d) => (this.testMode ? d : { ...d, label: d.productionLabel });
|
|
12040
|
+
this.plotTypeOptions = builtIn.map(labelled);
|
|
12041
|
+
this.plotTypeMenu = [...this.plotTypeOptions, ...contributed.map(labelled)];
|
|
11551
12042
|
}
|
|
11552
12043
|
ngOnChanges(changes) {
|
|
11553
12044
|
// The constructor computes the options with testMode still at its default;
|
|
@@ -11568,6 +12059,8 @@ class VisualizerComponent {
|
|
|
11568
12059
|
* `isStack`/`isGrayscale` decision is made against a file that is no longer loaded.
|
|
11569
12060
|
*/
|
|
11570
12061
|
onImageCleared() {
|
|
12062
|
+
// The image a contributed mode was drawing over is gone.
|
|
12063
|
+
this.plotModes.deactivate();
|
|
11571
12064
|
this.imageInfo = undefined;
|
|
11572
12065
|
this.loadedFileName = undefined;
|
|
11573
12066
|
this.computePlotTypeOptions();
|
|
@@ -11583,20 +12076,24 @@ class VisualizerComponent {
|
|
|
11583
12076
|
this.cdr.detectChanges();
|
|
11584
12077
|
}
|
|
11585
12078
|
reconcileSelectedPlotType() {
|
|
11586
|
-
if (this.
|
|
12079
|
+
if (this.plotTypeMenu.some((d) => d.type === this.selectedPlotTypeId))
|
|
11587
12080
|
return;
|
|
11588
12081
|
// Image is the usual fallback, but it is not always ON OFFER: with no image loaded
|
|
11589
12082
|
// the pixel modes are gone, and falling back to one would select a mode the selector
|
|
11590
12083
|
// does not list and nothing can draw. Take the first type still offered instead.
|
|
11591
|
-
const fallback = this.
|
|
12084
|
+
const fallback = this.plotTypeMenu.some((d) => d.type === PlotType.IMAGE)
|
|
11592
12085
|
? PlotType.IMAGE
|
|
11593
|
-
: this.
|
|
12086
|
+
: this.plotTypeMenu[0]?.type;
|
|
11594
12087
|
if (!fallback)
|
|
11595
12088
|
return;
|
|
11596
|
-
|
|
11597
|
-
|
|
11598
|
-
this.
|
|
11599
|
-
this.
|
|
12089
|
+
// Leaving a contributed mode (its gates stopped passing, or its provider is
|
|
12090
|
+
// gone): its session ends before anything else draws.
|
|
12091
|
+
this.plotModes.deactivate();
|
|
12092
|
+
const base = this.plotModes.baseTypeOf(fallback);
|
|
12093
|
+
this.selectedPlotTypeId = fallback;
|
|
12094
|
+
this.plotType = base;
|
|
12095
|
+
this.isHeatmap = base === PlotType.IMAGE;
|
|
12096
|
+
this.plotService.setPlotType(base);
|
|
11600
12097
|
}
|
|
11601
12098
|
ngOnInit() {
|
|
11602
12099
|
// Join the live set; the oldest member renders the shared notice outlets.
|
|
@@ -11750,7 +12247,7 @@ class VisualizerComponent {
|
|
|
11750
12247
|
if (!this.isHeatmap && imgInfo.fileName !== this.loadedFileName) {
|
|
11751
12248
|
this.isHeatmap = true;
|
|
11752
12249
|
this.plotType = PlotType.IMAGE;
|
|
11753
|
-
this.
|
|
12250
|
+
this.selectedPlotTypeId = PlotType.IMAGE;
|
|
11754
12251
|
this.plotService.setPlotType(this.plotType);
|
|
11755
12252
|
this.activeSurface3dMode = 'turntable';
|
|
11756
12253
|
}
|
|
@@ -11778,6 +12275,11 @@ class VisualizerComponent {
|
|
|
11778
12275
|
// image size — measure the plot div directly so the toolbar height is excluded
|
|
11779
12276
|
const plotDiv = document.getElementById(this.plotDivName);
|
|
11780
12277
|
const screenHeight = plotDiv?.offsetHeight || 500;
|
|
12278
|
+
// A contributed mode's session is bound to the base view about to be
|
|
12279
|
+
// torn down, so it ends here — before reset(), before anything new
|
|
12280
|
+
// draws. If the mode is still selected once this render lands, a
|
|
12281
|
+
// fresh session starts for it (see activateSelectedPlotMode).
|
|
12282
|
+
this.plotModes.deactivate();
|
|
11781
12283
|
this.plotService.reset();
|
|
11782
12284
|
this.stackLoading = imgInfo.isStack && imgInfo.showStack;
|
|
11783
12285
|
// set max index of stack — computed before updateZIndex so a
|
|
@@ -11931,6 +12433,7 @@ class VisualizerComponent {
|
|
|
11931
12433
|
releaseOverlay();
|
|
11932
12434
|
this.running = false;
|
|
11933
12435
|
applyRoi();
|
|
12436
|
+
this.activateSelectedPlotMode();
|
|
11934
12437
|
console.log(logTag);
|
|
11935
12438
|
},
|
|
11936
12439
|
sharpenFailed: (err) => {
|
|
@@ -11947,6 +12450,7 @@ class VisualizerComponent {
|
|
|
11947
12450
|
});
|
|
11948
12451
|
this.running = false;
|
|
11949
12452
|
applyRoi();
|
|
12453
|
+
this.activateSelectedPlotMode();
|
|
11950
12454
|
},
|
|
11951
12455
|
}).render(imgInfo, smallImgInfo);
|
|
11952
12456
|
}
|
|
@@ -12056,7 +12560,7 @@ class VisualizerComponent {
|
|
|
12056
12560
|
// Intercepting here fires a FIXED zoom step per wheel event, which is far
|
|
12057
12561
|
// too sensitive for a renderer that reads the scroll delta — so anything
|
|
12058
12562
|
// drawn by such a renderer keeps its own wheel. See `rendererOwnsWheel`.
|
|
12059
|
-
if (rendererOwnsWheel(this.
|
|
12563
|
+
if (rendererOwnsWheel(this.basePlotType))
|
|
12060
12564
|
return;
|
|
12061
12565
|
// 3D plot types (surface, scatter3d, isosurface) render in a Plotly scene
|
|
12062
12566
|
// that orbits/zooms natively on scroll. The 2D step-zoom doesn't apply and
|
|
@@ -12175,6 +12679,10 @@ class VisualizerComponent {
|
|
|
12175
12679
|
}
|
|
12176
12680
|
}
|
|
12177
12681
|
ngOnDestroy() {
|
|
12682
|
+
// A contributed mode's session ends first, while the viewer it drew over
|
|
12683
|
+
// still exists. The panel goes with this view, so no change detection.
|
|
12684
|
+
this.destroying = true;
|
|
12685
|
+
this.plotModes.deactivate();
|
|
12178
12686
|
// Leave the live set so the next-oldest visualizer picks up the outlets.
|
|
12179
12687
|
VisualizerComponent.liveInstances.delete(this);
|
|
12180
12688
|
this.revokeVolumeImageUrls();
|
|
@@ -12298,7 +12806,7 @@ class VisualizerComponent {
|
|
|
12298
12806
|
if (!this.isHeatmap) {
|
|
12299
12807
|
this.isHeatmap = true;
|
|
12300
12808
|
this.plotType = PlotType.HEATMAP;
|
|
12301
|
-
this.
|
|
12809
|
+
this.selectedPlotTypeId = PlotType.HEATMAP;
|
|
12302
12810
|
this.plotService.setPlotType(this.plotType);
|
|
12303
12811
|
}
|
|
12304
12812
|
this.plotService.setShowStack(showstack);
|
|
@@ -12831,7 +13339,7 @@ class VisualizerComponent {
|
|
|
12831
13339
|
}
|
|
12832
13340
|
// Bézier conversions are vertex-level edits — Image (OpenSeadragon) view
|
|
12833
13341
|
// only — gated on the selection's current form.
|
|
12834
|
-
if (this.
|
|
13342
|
+
if (this.basePlotType === PlotType.IMAGE) {
|
|
12835
13343
|
const hasStraight = selAll.some((r) => (r.bounds instanceof Polygon && !r.bounds.bezier) || r.bounds instanceof Rectangle);
|
|
12836
13344
|
const hasBezier = selAll.some((r) => r.bounds instanceof Polygon && r.bounds.bezier);
|
|
12837
13345
|
if (hasStraight) {
|
|
@@ -12945,7 +13453,7 @@ class VisualizerComponent {
|
|
|
12945
13453
|
// Vertex editing runs on the OpenSeadragon overlay, which
|
|
12946
13454
|
// backs the Image plot type. Hidden for other 2D types (Plotly), where
|
|
12947
13455
|
// these modes are no-ops.
|
|
12948
|
-
if (this.
|
|
13456
|
+
if (this.basePlotType === PlotType.IMAGE) {
|
|
12949
13457
|
items.push({
|
|
12950
13458
|
label: 'Polygon (click vertices)',
|
|
12951
13459
|
icon: 'polygon-vertices-icon',
|
|
@@ -13001,10 +13509,28 @@ class VisualizerComponent {
|
|
|
13001
13509
|
* the toolbar/context-menu logic relies on, deactivates 2D-only tools when
|
|
13002
13510
|
* moving to a 3D type, then re-plots.
|
|
13003
13511
|
*/
|
|
13004
|
-
onSelectPlotType(
|
|
13512
|
+
onSelectPlotType(selected) {
|
|
13513
|
+
// A contributed id whose provider is gone (e.g. restored from a previous
|
|
13514
|
+
// session) has nothing to activate — take the default Image view instead.
|
|
13515
|
+
if (!isBuiltinPlotType(selected) && !this.plotModes.find(selected)) {
|
|
13516
|
+
console.warn(`[visualizer] unknown plot type '${selected}' — showing '${PlotType.IMAGE}' instead.`);
|
|
13517
|
+
selected = PlotType.IMAGE;
|
|
13518
|
+
}
|
|
13519
|
+
// An explicit choice retries a mode whose earlier cleanup failed; only
|
|
13520
|
+
// automatic re-activations (re-render, image switch) fall back for that.
|
|
13521
|
+
this.plotModes.clearCleanupFailures();
|
|
13522
|
+
// Whatever was selected before, a contributed session ends before the next
|
|
13523
|
+
// mode draws. (Re-selecting the same contributed mode re-plots, so it gets a
|
|
13524
|
+
// fresh session too.)
|
|
13525
|
+
this.plotModes.deactivate();
|
|
13526
|
+
// Everything below decides how to RENDER, so it works on the built-in type
|
|
13527
|
+
// that draws `selected`: a contributed mode behaves exactly like its base.
|
|
13528
|
+
const type = this.plotModes.baseTypeOf(selected);
|
|
13005
13529
|
this.plotType = type;
|
|
13006
|
-
this.
|
|
13007
|
-
const descriptor =
|
|
13530
|
+
this.selectedPlotTypeId = selected;
|
|
13531
|
+
const descriptor = isBuiltinPlotType(selected)
|
|
13532
|
+
? this.plotTypeMenu.find((d) => d.type === type)
|
|
13533
|
+
: getPlotTypeDescriptor(type);
|
|
13008
13534
|
const is3d = descriptor?.dimensions === '3d';
|
|
13009
13535
|
this.isHeatmap = !is3d;
|
|
13010
13536
|
this.plotService.setPlotType(type);
|
|
@@ -13039,6 +13565,105 @@ class VisualizerComponent {
|
|
|
13039
13565
|
}
|
|
13040
13566
|
this.reloadAndPlot();
|
|
13041
13567
|
}
|
|
13568
|
+
// ── contributed plot modes ─────────────────────────────────────────────
|
|
13569
|
+
/**
|
|
13570
|
+
* Start the selected contributed mode's session, once its base view has
|
|
13571
|
+
* plotted. No-op for a built-in type, or when the base on screen is not the
|
|
13572
|
+
* one the mode rides on. A backend that cannot provide a viewport for it
|
|
13573
|
+
* (e.g. OSD fell back to Plotly) counts as a failed activation.
|
|
13574
|
+
*/
|
|
13575
|
+
activateSelectedPlotMode() {
|
|
13576
|
+
const contribution = this.plotModes.find(this.selectedPlotTypeId);
|
|
13577
|
+
if (!contribution || this.plotType !== contribution.descriptor.baseType)
|
|
13578
|
+
return;
|
|
13579
|
+
const viewport = this.plotService.getPlotModeViewport?.() ?? null;
|
|
13580
|
+
if (!viewport) {
|
|
13581
|
+
console.error(`[visualizer] plot-type contribution '${contribution.descriptor.type}': the backend `
|
|
13582
|
+
+ 'on screen provides no viewport to draw over — falling back.');
|
|
13583
|
+
this.fallBackFromPlotMode(contribution);
|
|
13584
|
+
return;
|
|
13585
|
+
}
|
|
13586
|
+
const ctx = {
|
|
13587
|
+
visualizer: this.plotService,
|
|
13588
|
+
viewport,
|
|
13589
|
+
imageInfo$: this.state.getImageInfo$(),
|
|
13590
|
+
};
|
|
13591
|
+
void this.plotModes.activate(contribution, ctx);
|
|
13592
|
+
}
|
|
13593
|
+
/** Render the live mode's panel. Activation can resolve outside Angular (an
|
|
13594
|
+
* OSD callback, a contribution's own promise), hence the zone re-entry. */
|
|
13595
|
+
showPlotModePanel(active) {
|
|
13596
|
+
const panel = active.contribution.panel;
|
|
13597
|
+
if (!panel)
|
|
13598
|
+
return;
|
|
13599
|
+
this.ngZone.run(() => {
|
|
13600
|
+
if ('component' in panel && panel.component) {
|
|
13601
|
+
this.plotModePanel = {
|
|
13602
|
+
title: panel.title,
|
|
13603
|
+
component: panel.component,
|
|
13604
|
+
injector: Injector.create({
|
|
13605
|
+
providers: [
|
|
13606
|
+
{ provide: PLOT_MODE_CONTEXT, useValue: active.ctx },
|
|
13607
|
+
{ provide: PLOT_MODE_SESSION, useValue: active.session },
|
|
13608
|
+
],
|
|
13609
|
+
parent: this.injector,
|
|
13610
|
+
}),
|
|
13611
|
+
host: null,
|
|
13612
|
+
};
|
|
13613
|
+
}
|
|
13614
|
+
else if (active.panelHost) {
|
|
13615
|
+
this.plotModePanel = { title: panel.title, component: null, injector: null, host: active.panelHost };
|
|
13616
|
+
}
|
|
13617
|
+
else {
|
|
13618
|
+
return;
|
|
13619
|
+
}
|
|
13620
|
+
// A component panel is created and first rendered right here; if that throws,
|
|
13621
|
+
// the mode is not usable — end it and fall back rather than leave it selected.
|
|
13622
|
+
const err = this.detectChangesSafely();
|
|
13623
|
+
if (err !== null)
|
|
13624
|
+
this.plotModes.failActive(err);
|
|
13625
|
+
});
|
|
13626
|
+
}
|
|
13627
|
+
/** Drop the panel (destroying a component panel) before the session ends. */
|
|
13628
|
+
hidePlotModePanel() {
|
|
13629
|
+
if (!this.plotModePanel)
|
|
13630
|
+
return;
|
|
13631
|
+
this.plotModePanel = null;
|
|
13632
|
+
this.detectChangesSafely();
|
|
13633
|
+
}
|
|
13634
|
+
/**
|
|
13635
|
+
* A contributed mode could not start. Its base type is already on screen —
|
|
13636
|
+
* the base plotted first — so just select it: nothing needs to re-plot.
|
|
13637
|
+
*/
|
|
13638
|
+
fallBackFromPlotMode(contribution) {
|
|
13639
|
+
if (this.destroying || this.selectedPlotTypeId !== contribution.descriptor.type)
|
|
13640
|
+
return;
|
|
13641
|
+
this.ngZone.run(() => {
|
|
13642
|
+
const base = contribution.descriptor.baseType;
|
|
13643
|
+
this.selectedPlotTypeId = base;
|
|
13644
|
+
this.plotType = base;
|
|
13645
|
+
this.messageService.add({
|
|
13646
|
+
key: this.resultToastKey,
|
|
13647
|
+
severity: 'warn',
|
|
13648
|
+
summary: `${contribution.descriptor.productionLabel ?? contribution.descriptor.label} is unavailable`,
|
|
13649
|
+
detail: 'The plot mode could not start, so the plain image is shown instead. See the browser console for details.',
|
|
13650
|
+
});
|
|
13651
|
+
this.detectChangesSafely();
|
|
13652
|
+
});
|
|
13653
|
+
}
|
|
13654
|
+
/** Run change detection; returns what it threw (logged), or null. */
|
|
13655
|
+
detectChangesSafely() {
|
|
13656
|
+
if (this.destroying)
|
|
13657
|
+
return null;
|
|
13658
|
+
try {
|
|
13659
|
+
this.cdr.detectChanges();
|
|
13660
|
+
return null;
|
|
13661
|
+
}
|
|
13662
|
+
catch (err) {
|
|
13663
|
+
console.error('[visualizer] change detection failed while updating a contributed plot mode panel.', err);
|
|
13664
|
+
return err ?? new Error('change detection failed');
|
|
13665
|
+
}
|
|
13666
|
+
}
|
|
13042
13667
|
/** Deactivate whatever tool is armed and clear every tool mode. */
|
|
13043
13668
|
deactivateActiveTool() {
|
|
13044
13669
|
this.activeDragMode = null;
|
|
@@ -13051,12 +13676,12 @@ class VisualizerComponent {
|
|
|
13051
13676
|
this.plotService.setSamPointMode(false);
|
|
13052
13677
|
this.plotService.setVertexEraserMode(false);
|
|
13053
13678
|
}
|
|
13054
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: VisualizerComponent, deps: [{ token: IMAGE_STATE_PORT }, { token: VISUALIZER }, { token: i1$2.MessageService }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }, { token: VisualizerStore }, { token: SamToolService }, { token: CellSegmentToolService }, { token: SamPointToolService }, { token: RegionOpsService }, { token: VIZ_CONFIG, optional: true }, { token: TOOLBAR_TOOLS, optional: true }, { token: SPATIAL_DATA_PORT, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
13055
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: VisualizerComponent, selector: "visualizer", inputs: { toolbarTools: "toolbarTools", testMode: "testMode" }, outputs: { isStackEvent: "isStackEvent", isGrayscaleEvent: "isGrayscaleEvent" }, host: { listeners: { "window:keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "contextMenu", first: true, predicate: ["cm"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- SAM toast: live status + encoder-download progress bar (jit-ui#90). Sticky\n while a segment runs; stays open until the bar reaches 100% and the run ends. -->\n<p-toast [key]=\"samToastKey\" position=\"bottom-right\">\n <ng-template let-message pTemplate=\"message\">\n <div style=\"flex: 1; min-width: 16rem\">\n <div class=\"font-bold mb-1\">{{ message.summary }}</div>\n <div>{{ samStatus || message.detail }}</div>\n <p-progressBar *ngIf=\"samDownloading\" [value]=\"samProgress\" styleClass=\"mt-2 sam-progress\"></p-progressBar>\n <p-progressBar *ngIf=\"samBusy && !samDownloading\" mode=\"indeterminate\" styleClass=\"mt-2\"\n [style]=\"{ height: '4px' }\"></p-progressBar>\n </div>\n </ng-template>\n</p-toast>\n\n<!-- Result/error notices from the library itself (segmentation outcome, region\n validation). Rendered here so they do not depend on the host mounting a\n keyless <p-toast> \u2014 without this the messages went nowhere in any host but\n jit-ui, and a failed run looked like a button that did nothing. -->\n<p-toast [key]=\"resultToastKey\" position=\"bottom-right\"></p-toast>\n\n<!-- Shared outlets for notices raised where there is no per-instance key to use:\n the root-provided PlotlyService and the region-editor dialog. Rendered by a\n single visualizer (ownsSharedToasts) so a message from the root service is\n not shown once per live viewer. -->\n<p-toast *ngIf=\"ownsSharedToasts\" [key]=\"vizToastKey\"></p-toast>\n<p-toast *ngIf=\"ownsSharedToasts\" [key]=\"vizAlertToastKey\" position=\"center\"></p-toast>\n\n<!-- Toolbar dock: docked across the top by default; drag the handle to detach\n it into a floating, movable window (frees the top row for the image). -->\n<div class=\"toolbar-dock\" [class.floating]=\"toolbarFloating\"\n [style.left.px]=\"toolbarFloating ? toolbarPos.x : null\"\n [style.top.px]=\"toolbarFloating ? toolbarPos.y : null\">\n <div class=\"toolbar-drag-handle\" (mousedown)=\"onToolbarDragStart($event)\"\n pTooltip=\"Drag to float the toolbar\" tooltipPosition=\"bottom\">\n <i class=\"pi pi-bars\"></i>\n </div>\n<plotting-toolbar\n [imageInfo]=\"imageInfo\"\n [plotTypeOptions]=\"plotTypeOptions\"\n [selectedPlotType]=\"selectedPlotType\"\n [isoRange]=\"isoRange\"\n [maxIndex]=\"maxIndex\"\n [zIndex]=\"zIndex\"\n [isHeatmap]=\"isHeatmap\"\n [is3dRegions]=\"isSpatial3dMode\"\n [activeDragMode]=\"activeDragMode\"\n [activeSurface3dMode]=\"activeSurface3dMode\"\n [axesVisible]=\"axesVisible\"\n [wireframeActive]=\"wireframeActive\"\n [resolutionScale]=\"resolutionScale\"\n [wandSensitivity]=\"wandSensitivity\"\n [brushSize]=\"brushSize\"\n [samModels]=\"samModels\"\n [samModelId]=\"samModelId\"\n [contributedTools]=\"contributedTools\"\n [toolModelIds]=\"toolModelIds\"\n [vertexEraserRadius]=\"vertexEraserRadius\"\n [canUndo]=\"canUndoRegion\"\n [canRedo]=\"canRedoRegion\"\n [stackOptions]=\"stackOptions\"\n [tools]=\"toolbarTools\"\n [imageSmoothingEnabled]=\"imageSmoothingEnabled\"\n (selectPlotType)=\"onSelectPlotType($event)\"\n (toggleImageSmoothing)=\"onToggleImageSmoothing()\"\n (addProfileLine)=\"addProfileLine()\"\n (isoRangeChange)=\"onIsoRangeChange($event)\"\n (openChannelHistogram)=\"openChannelHistogram()\"\n (openSpatialControls)=\"openSpatialControls()\"\n (openRegionEditor)=\"openRegionEditor()\"\n (selectStackOption)=\"selectStackOption($event)\"\n (zScrub)=\"onZScrub($event)\"\n (zSlide)=\"onZSlide($event)\"\n (zIndexInput)=\"zIndex = $event; updateZIndex()\"\n (reloadAndPlot)=\"reloadAndPlot()\"\n (downloadImage)=\"downloadImage()\"\n (autoscaleImage)=\"autoscaleImage()\"\n (toggleDragMode)=\"toggleDragMode($event)\"\n (zoomIn)=\"zoomIn()\"\n (zoomOut)=\"zoomOut()\"\n (toggleSurface3dMode)=\"toggleSurface3dMode($event)\"\n (resetSurfaceCamera)=\"resetSurfaceCamera()\"\n (toggleAxes)=\"toggleAxes()\"\n (toggleWireframe)=\"toggleWireframe()\"\n (selectResolution)=\"selectResolution($event)\"\n (deleteRegion)=\"deleteRegion()\"\n (undoRegion)=\"undoRegion()\"\n (redoRegion)=\"redoRegion()\"\n (segmentRegions)=\"segmentRegions()\"\n (segmentCellpose)=\"segmentCellpose()\"\n (samModelChange)=\"onSamModelChange($event)\"\n (runTool)=\"runTool($event)\"\n (toolModelChange)=\"onToolModelChange($event)\"\n (openToolParams)=\"openToolParams($event)\"\n (wandSensitivityChange)=\"onWandSensitivityChange($event)\"\n (brushSizeChange)=\"onBrushSizeChange($event)\"\n (vertexEraserRadiusChange)=\"onVertexEraserRadiusChange($event)\">\n <!-- Host-projected custom toolbar buttons, forwarded into the toolbar. -->\n <ng-content></ng-content>\n</plotting-toolbar>\n <p-button *ngIf=\"toolbarFloating\" class=\"toolbar-dock-btn\" icon=\"pi pi-thumbtack\" size=\"small\" [text]=\"true\"\n pTooltip=\"Dock to top\" tooltipPosition=\"bottom\" (onClick)=\"dockToolbar()\">\n </p-button>\n</div>\n\n<p-contextMenu #cm [model]=\"contextMenuItems\" appendTo=\"body\"></p-contextMenu>\n<!-- No `h-full` here: PrimeFlex's height:100% !important made #plot as tall as\n the whole host, so under the docked toolbar its bottom toolbar-height strip\n (incl. the OSD scale bar at bottom-left) was clipped by the host's\n overflow:hidden. The component stylesheet sizes it (.viz-plot { flex: 1 }). -->\n<div class=\"w-full relative viz-plot\" [id]=\"plotDivName\">\n <div *ngIf=\"imgLoading || stackLoading || isCaching\" id=\"overlay\">\n <!-- Determinate stack/volume progress takes precedence: while it's active show ONLY the\n progress bar + \"Loading frames...\", not the generic spinner/\"Loading image...\" on top of\n it (both were rendering at once for napari volume/isosurface loads). -->\n <ng-container *ngIf=\"stackLoading; else imgLoadingTpl\">\n <p-progressBar class=\"progress\" [value]=\"loadingPercentage\"></p-progressBar>\n <span class=\"text-loading\">Loading frames...</span>\n </ng-container>\n <ng-template #imgLoadingTpl>\n <!-- Cache-copy progress is shown whenever a cache download is active (isCaching), independent\n of `imgLoading`, so it's never dropped/masked mid-download. -->\n <ng-container *ngIf=\"isCaching; else spinnerLoading\">\n <!-- Stage-aware message comes from CacheProgressService updating\n imageLoadingMessage$ (\"Caching image...\" during the GCS download,\n then \"Generating preview...\" during per-slice thumbnail gen). Once the\n download hits 100% the remaining work (server preview/tile generation)\n has no determinate progress, so fall through to the spinner template\n rather than parking a full bar under \"Loading image...\". -->\n <p-progressBar class=\"progress\" [value]=\"cacheProgress\"></p-progressBar>\n <span class=\"text-loading\">{{loadingMessage}}</span>\n </ng-container>\n <ng-template #spinnerLoading>\n <div *ngIf=\"imgLoading\" class=\"spinner\"></div>\n <span *ngIf=\"imgLoading && !zoom\" class=\"text-loading\">{{loadingMessage}}</span>\n <span *ngIf=\"imgLoading && zoom\" class=\"zoom-text-loading\">{{loadingMessage}}</span>\n </ng-template>\n </ng-template>\n <p-button *ngIf=\"stackLoading || imgLoading\" class=\"cancel-loading \" label=\"Cancel\"\n (onClick)=\"cancelLoading()\">\n </p-button>\n </div>\n <!-- Translucent \"sharpening\" overlay shown after the small-tier preview is\n on screen while the large-tier render is still loading in the\n background. Lets the user see the blurry image underneath but makes\n it clear something is still loading so they don't mistake the\n low-res render for the final image. -->\n <div *ngIf=\"sharpening && !imgLoading && !stackLoading && !isCaching\" id=\"sharpening-overlay\">\n <div class=\"spinner\"></div>\n <span class=\"text-loading\">Sharpening preview...</span>\n </div>\n\n</div>\n\n<!-- Floating, draggable intensity-profile inset. Shown in LINE mode; the chart\n updates live as the line ROI on the image is moved. Kept OUTSIDE #plot and\n positioned fixed so it floats over the whole page (not clipped to the\n diagram/canvas area). -->\n<div *ngIf=\"hasProfiles\" class=\"intensity-panel\"\n [style.left.px]=\"profilePanelPos.x\" [style.top.px]=\"profilePanelPos.y\">\n <div class=\"intensity-panel-header\" (mousedown)=\"onProfilePanelDragStart($event)\">\n <span>Intensity profile</span>\n <i class=\"pi pi-arrows-alt\"></i>\n </div>\n <div [id]=\"intensityInsetDiv\" class=\"intensity-panel-plot\"></div>\n</div>\n\n<!-- Channels & Histogram: non-modal floating dialog opened from the toolbar. -->\n<channel-histogram [(visible)]=\"showChannelHistogram\"></channel-histogram>\n\n<!-- Spatial-omics controls. Renders its own empty state when no dataset (or no\n SPATIAL_DATA_PORT) is present, so it is always safe to host. -->\n<spatial-controls [(visible)]=\"showSpatialControls\" [is3d]=\"isSpatial3dMode\" ></spatial-controls>\n\n<!-- Region Editor: non-modal, draggable, resizable floating dialog opened from\n the toolbar (formerly the right-panel Regions tab). Modeless so regions can\n still be drawn/selected on the plot while it's open; stays linked to this\n plotting instance via the root RegionStore singleton. Opens over the right\n side of the view. -->\n<p-dialog header=\"Region Editor\" [(visible)]=\"showRegionEditor\"\n [modal]=\"false\" [resizable]=\"true\" [draggable]=\"true\"\n [maximizable]=\"true\" appendTo=\"body\" position=\"right\"\n [style]=\"{ width: regionEditorWidth, height: '85vh' }\"\n [contentStyle]=\"{ overflow: 'auto' }\">\n <region-editor></region-editor>\n</p-dialog>\n\n<!-- Simplify (Douglas\u2013Peucker): custom px-threshold picker for the selected\n region(s), opened from the right-click \"Simplify \u25B8 Custom\u2026\" action. -->\n<p-dialog header=\"Simplify region(s)\" [(visible)]=\"displaySimplifyDialog\" [modal]=\"true\"\n [style]=\"{width: '22rem'}\" [draggable]=\"false\" [resizable]=\"false\">\n <div class=\"flex flex-column gap-2\">\n <span>Altitude threshold: <b>{{ simplifyThreshold }} px</b></span>\n <p-slider [(ngModel)]=\"simplifyThreshold\" [min]=\"1\" [max]=\"20\" [step]=\"1\"></p-slider>\n <small>Vertices within this distance of the line between their neighbours are removed.</small>\n </div>\n <ng-template pTemplate=\"footer\">\n <p-button label=\"Cancel\" [text]=\"true\" (onClick)=\"displaySimplifyDialog = false\"></p-button>\n <p-button label=\"Apply\" icon=\"pi pi-check\" (onClick)=\"simplifyRegions(simplifyThreshold)\"></p-button>\n </ng-template>\n</p-dialog>\n\n<!-- Parameters for whichever contributed tool is open. One dialog for all of\n them: a tool declares its fields (ToolParamSpec) and this renders them, so\n the toolbar can lay every tool out the same way and a plugin needs no\n PrimeNG of its own. Defaults come from the active checkpoint, since they\n encode what it was trained for. -->\n<p-dialog *ngIf=\"openParamsTool as tool\"\n [header]=\"tool.label + ' parameters'\"\n [visible]=\"true\" (visibleChange)=\"openParamsToolId = null\"\n [modal]=\"true\"\n [style]=\"{width: '30rem'}\" [draggable]=\"false\" [resizable]=\"false\" appendTo=\"body\">\n <div class=\"grid formgrid p-fluid\">\n <ng-container *ngFor=\"let p of tool.params\">\n <div class=\"field\" [ngClass]=\"p.type === 'boolean' ? 'col-12' : 'col-6'\">\n <ng-container [ngSwitch]=\"p.type\">\n\n <ng-container *ngSwitchCase=\"'number'\">\n <label class=\"text-sm\" [pTooltip]=\"p.tooltip || ''\">{{ p.label }}</label>\n <p-inputNumber [ngModel]=\"paramsFor(tool.id)[p.id]\"\n (ngModelChange)=\"paramsFor(tool.id)[p.id] = $event\"\n [min]=\"numberParam(p).min\" [max]=\"numberParam(p).max\"\n [step]=\"numberParam(p).step\"\n [minFractionDigits]=\"numberParam(p).fractionDigits\"\n [showButtons]=\"true\"></p-inputNumber>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <label class=\"text-sm\" [pTooltip]=\"p.tooltip || ''\">{{ p.label }}</label>\n <p-dropdown [options]=\"selectParam(p).options\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"paramsFor(tool.id)[p.id]\"\n (ngModelChange)=\"paramsFor(tool.id)[p.id] = $event\"\n appendTo=\"body\"></p-dropdown>\n </ng-container>\n\n <!-- Checkbox label sits beside the box and may run to a sentence, so\n it does not get the stacked label the other types use. -->\n <div *ngSwitchCase=\"'boolean'\" class=\"flex align-items-center gap-2\">\n <p-checkbox [ngModel]=\"paramsFor(tool.id)[p.id]\"\n (ngModelChange)=\"paramsFor(tool.id)[p.id] = $event\"\n [binary]=\"true\" [inputId]=\"tool.id + '-' + p.id\"></p-checkbox>\n <label [for]=\"tool.id + '-' + p.id\" class=\"text-sm mb-0\"\n [pTooltip]=\"p.tooltip || ''\">{{ p.label }}</label>\n </div>\n\n </ng-container>\n </div>\n </ng-container>\n </div>\n <ng-template pTemplate=\"footer\">\n <p-button label=\"Reset to model defaults\" [text]=\"true\" size=\"small\"\n (onClick)=\"resetToolParams(tool.id)\"></p-button>\n <p-button label=\"Close\" size=\"small\" (onClick)=\"openParamsToolId = null\"></p-button>\n </ng-template>\n</p-dialog>\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow:hidden}.viz-plot{flex:1;min-height:0;height:auto!important}.toolbar-dock{display:flex;align-items:stretch;width:100%}.toolbar-dock plotting-toolbar{flex:1;min-width:0}.toolbar-dock.floating{position:fixed;z-index:1000;width:auto;max-width:calc(100% - 16px);background:var(--surface-card, #ffffff);border:1px solid var(--surface-border, #cccccc);border-radius:6px;box-shadow:0 4px 16px #0000004d}.toolbar-dock.floating plotting-toolbar{flex:0 1 auto}.toolbar-dock.floating .toolbar-drag-handle{border-radius:6px 0 0 6px}.toolbar-drag-handle{display:flex;align-items:center;justify-content:center;padding:0 .4rem;cursor:move;color:var(--primary-color);background:var(--surface-100, rgba(0, 0, 0, .04));border-right:1px solid var(--surface-border, #cccccc);-webkit-user-select:none;user-select:none}.toolbar-dock-btn{align-self:center}:host ::ng-deep .navigator{margin-bottom:80px!important;margin-right:12px!important}kbd{background:#e8e8e8;border:1px solid #bbb;border-radius:3px;padding:1px 5px;font-family:monospace;font-size:.85em}::ng-deep .context-menu-active>.p-menuitem-content{background-color:var(--primary-color)!important}::ng-deep .context-menu-active>.p-menuitem-content .p-menuitem-icon,::ng-deep .context-menu-active>.p-menuitem-content .p-menuitem-text{color:#fff!important}::ng-deep .context-menu-active>.p-menuitem-content .zoom-box-off-icon,::ng-deep .context-menu-active>.p-menuitem-content .wand-icon,::ng-deep .context-menu-active>.p-menuitem-content .polyline-icon,::ng-deep .context-menu-active>.p-menuitem-content .polygon-vertices-icon,::ng-deep .context-menu-active>.p-menuitem-content .vertex-add-icon,::ng-deep .context-menu-active>.p-menuitem-content .vertex-delete-icon,::ng-deep .context-menu-active>.p-menuitem-content .region-move-icon,::ng-deep .context-menu-active>.p-menuitem-content .to-bezier-icon,::ng-deep .context-menu-active>.p-menuitem-content .to-polygon-icon{filter:brightness(0) invert(1)}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.progress{position:absolute;width:50%;left:25%;top:55%}.text-loading{position:absolute;margin:0 auto;left:45%;top:60%;font-weight:700}.zoom-text-loading{position:absolute;margin:0 auto;left:35%;top:60%;font-weight:700;display:inline-block;width:350px}.cancel-loading{position:absolute;margin:0 auto;left:46%;top:70%}.spinner{position:absolute;left:45%;top:45%;height:100px;width:100px;margin:0 auto;-webkit-animation:rotation .6s infinite linear;-moz-animation:rotation .6s infinite linear;-o-animation:rotation .6s infinite linear;animation:rotation .6s infinite linear;border-left:6px solid rgba(0,174,239,.15);border-right:6px solid rgba(0,174,239,.15);border-bottom:6px solid rgba(0,174,239,.15);border-top:6px solid rgb(82,107,178);border-radius:100%}#overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#ffffff80;z-index:1000}#sharpening-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#ffffff2e;pointer-events:none;z-index:1000}#sharpening-overlay .spinner{height:60px;width:60px;border-width:4px;left:calc(50% - 30px);top:calc(50% - 60px)}#sharpening-overlay .text-loading{position:absolute;left:0;right:0;width:auto;text-align:center;top:calc(50% + 35px);font-size:.9rem;color:#526bb2}@-webkit-keyframes rotation{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(359deg)}}@-moz-keyframes rotation{0%{-moz-transform:rotate(0deg)}to{-moz-transform:rotate(359deg)}}@-o-keyframes rotation{0%{-o-transform:rotate(0deg)}to{-o-transform:rotate(359deg)}}@keyframes rotation{0%{transform:rotate(0)}to{transform:rotate(359deg)}}::ng-deep .zoom-box-off-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\" fill=\"black\" fill-rule=\"evenodd\">%0A <!-- Rectangle outline -->%0A <rect x=\"2\" y=\"2\" width=\"56\" height=\"56\"/>%0A <rect x=\"9\" y=\"9\" width=\"42\" height=\"42\"/>%0A <!-- Magnifying glass ring -->%0A <circle cx=\"70\" cy=\"70\" r=\"23\"/>%0A <circle cx=\"70\" cy=\"70\" r=\"14\"/>%0A <!-- Handle -->%0A <polygon points=\"81,81 97,97 92,100 77,85\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .wand-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<svg%0A viewBox=\"0 0 100 100\"%0A fill=\"black\"%0A fill-rule=\"evenodd\"%0A version=\"1.1\"%0A id=\"svg4\"%0A sodipodi:docname=\"wand.svg\"%0A inkscape:version=\"1.4.3 (0d15f75, 2025-12-25)\"%0A xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\"%0A xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\"%0A xmlns=\"http://www.w3.org/2000/svg\"%0A xmlns:svg=\"http://www.w3.org/2000/svg\">%0A <defs%0A id=\"defs4\" />%0A <sodipodi:namedview%0A id=\"namedview4\"%0A pagecolor=\"%23ffffff\"%0A bordercolor=\"%23000000\"%0A borderopacity=\"0.25\"%0A inkscape:showpageshadow=\"2\"%0A inkscape:pageopacity=\"0.0\"%0A inkscape:pagecheckerboard=\"0\"%0A inkscape:deskcolor=\"%23d1d1d1\"%0A inkscape:zoom=\"8.74\"%0A inkscape:cx=\"50\"%0A inkscape:cy=\"49.942792\"%0A inkscape:window-width=\"1024\"%0A inkscape:window-height=\"1186\"%0A inkscape:window-x=\"3560\"%0A inkscape:window-y=\"131\"%0A inkscape:window-maximized=\"0\"%0A inkscape:current-layer=\"svg4\" />%0A <!-- Wand handle: thin diagonal stick from lower-left to upper-right -->%0A <polygon%0A points=\"12,92 78,26 74,22 8,88 \"%0A id=\"polygon1\"%0A transform=\"matrix(1.0375192,0.05799445,0.25179496,0.91871864,-17.85351,5.6289793)\" />%0A <!-- 4-pointed star at the wand tip -->%0A <polygon%0A points=\"62,34 76,30 80,6 84,30 98,34 84,38 80,62 76,38 \"%0A id=\"polygon2\"%0A transform=\"matrix(1.3082889,0,0,1.0245178,-38.907958,-1.4056881)\" />%0A <!-- Small sparkle to the upper-left -->%0A <polygon%0A points=\"46,30 36,32 34,42 32,32 22,30 32,28 34,18 36,28 \"%0A id=\"polygon3\"%0A transform=\"translate(-9.610984,-6.7505721)\" />%0A <!-- Small sparkle to the lower-right -->%0A <polygon%0A points=\"62,80 70,82 62,84 60,92 58,84 50,82 58,80 60,72 \"%0A id=\"polygon4\"%0A transform=\"translate(0,-1.6018307)\" />%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .brush-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 640\"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d=\"M512.5 74.3L291.1 222C262 241.4 243.5 272.9 240.5 307.3C302.8 320.1 351.9 369.2 364.8 431.6C399.3 428.6 430.7 410.1 450.1 381L597.7 159.5C604.4 149.4 608 137.6 608 125.4C608 91.5 580.5 64 546.6 64C534.5 64 522.6 67.6 512.5 74.3zM320 464C320 402.1 269.9 352 208 352C146.1 352 96 402.1 96 464C96 467.9 96.2 471.8 96.6 475.6C98.4 493.1 86.4 512 68.8 512L64 512C46.3 512 32 526.3 32 544C32 561.7 46.3 576 64 576L208 576C269.9 576 320 525.9 320 464z\"/></svg>') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .polyline-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\">%0A <polyline points=\"1,14 5,3 10,11 15,2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .polygon-vertices-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Polygon (click-to-place) tool icon: a traced polyline with vertex dots. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.4740709,0,0,1.4740709,-6.0268793,-6.8086826)\">%0A <polyline points=\"23.877,16.627 14.657,10.767 5.25,21.551 12.757,29.397\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\"%0A stroke-linejoin=\"round\" stroke-linecap=\"round\"/>%0A <circle cx=\"23.877\" cy=\"16.627\" r=\"1.9\"/>%0A <circle cx=\"14.657\" cy=\"10.767\" r=\"1.9\"/>%0A <circle cx=\"5.25\" cy=\"21.551\" r=\"1.9\"/>%0A <circle cx=\"12.757\" cy=\"29.397\" r=\"1.9\"/>%0A </g>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .vertex-add-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Add-vertex tool icon: a region blob with three corner vertices and a plus glyph. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.5222969,0,0,1.5222969,-8.2143505,-7.6146399)\">%0A <path d=\"m 9.35834,12.33267 c 1.78,-2.53 4.93,-3.56 7.86,-3.98 3.98,-0.72 8.33,0.08 11.52,2.67 2.62,1.98 4.71,5.1 4.53,8.49 -0.29,2.27 -2.5,3.81 -4.66,4.01 -2.75,0.43 -5.54,0.07 -8.28,-0.26 -3.99,-0.68 -8.05,-2.16 -10.93,-5.12 -1.32,-1.63 -1.39,-4.17 -0.04,-5.81 z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\" stroke-miterlimit=\"10\"/>%0A <circle cx=\"8.294\" cy=\"15.248\" r=\"2.63\"/>%0A <circle cx=\"19.781\" cy=\"8.287\" r=\"2.63\"/>%0A <circle cx=\"25.044\" cy=\"23.748\" r=\"2.63\"/>%0A </g>%0A <path d=\"M 9.65,29.0 H 5.85 v 5.3 H 0.55 v 3.4 H 5.85 v 5.3 H 9.65 V 37.7 H 14.95 V 34.3 H 9.65 Z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .vertex-delete-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Delete-vertex tool icon: a region blob with three corner vertices and a minus glyph. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.5222969,0,0,1.5222969,-8.2143505,-7.6146399)\">%0A <path d=\"m 9.35834,12.33267 c 1.78,-2.53 4.93,-3.56 7.86,-3.98 3.98,-0.72 8.33,0.08 11.52,2.67 2.62,1.98 4.71,5.1 4.53,8.49 -0.29,2.27 -2.5,3.81 -4.66,4.01 -2.75,0.43 -5.54,0.07 -8.28,-0.26 -3.99,-0.68 -8.05,-2.16 -10.93,-5.12 -1.32,-1.63 -1.39,-4.17 -0.04,-5.81 z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\" stroke-miterlimit=\"10\"/>%0A <circle cx=\"8.294\" cy=\"15.248\" r=\"2.63\"/>%0A <circle cx=\"19.781\" cy=\"8.287\" r=\"2.63\"/>%0A <circle cx=\"25.044\" cy=\"23.748\" r=\"2.63\"/>%0A </g>%0A <path d=\"M 0.55,34.3 v 3.4 H 14.95 V 34.3 Z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .region-move-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Region-move tool icon: a region blob with a drag arrow. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <path d=\"m 17.115582,8.9648664 c 1.707597,-3.3276806 4.729468,-4.6824281 7.540287,-5.2348498 3.827703,-0.947008 7.991169,0.1052238 11.051414,3.5118228 2.523022,2.604273 4.518416,6.7079726 4.345738,11.1668036 -0.278204,2.985704 -2.398311,5.011251 -4.470451,5.274308 -2.638142,0.565574 -5.314655,0.09206 -7.943203,-0.341976 C 23.811664,22.446579 19.916808,20.499952 17.153955,16.606696 15.89724,14.462775 15.820494,11.12194 17.115585,8.9648664 Z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.1\" stroke-miterlimit=\"10\"/>%0A <path d=\"m 26.741429,13.966963 6.746922,16.786269 3.229398,-4.060758 3.583857,3.583857 2.761195,-2.761196 -3.583856,-3.571852 4.060758,-3.2354 z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .to-bezier-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Convert-to-bezier tool icon: a smooth curve through two anchor points with control handles. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <!-- smooth cubic curve between the two anchors -->%0A <path d=\"M 7,34 C 7,16 37,28 37,10\" fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.4\"%0A stroke-linecap=\"round\"/>%0A <!-- control handles -->%0A <line x1=\"7\" y1=\"34\" x2=\"7\" y2=\"22\" stroke=\"%23000000\" stroke-width=\"1.4\"/>%0A <line x1=\"37\" y1=\"10\" x2=\"37\" y2=\"22\" stroke=\"%23000000\" stroke-width=\"1.4\"/>%0A <circle cx=\"7\" cy=\"22\" r=\"2.2\"/>%0A <circle cx=\"37\" cy=\"22\" r=\"2.2\"/>%0A <!-- anchor points -->%0A <rect x=\"4\" y=\"31\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"34\" y=\"7\" width=\"6\" height=\"6\" rx=\"1\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .to-polygon-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Convert-to-polygon tool icon: endpoints joined by straight segments through a vertex. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <!-- straight-segment path through the vertices -->%0A <polyline points=\"7,34 22,12 37,10\" fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.4\"%0A stroke-linecap=\"round\" stroke-linejoin=\"round\"/>%0A <!-- vertices -->%0A <rect x=\"4\" y=\"31\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"19\" y=\"9\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"34\" y=\"7\" width=\"6\" height=\"6\" rx=\"1\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.intensity-panel{position:fixed;z-index:1100;width:280px;height:180px;background:#191919e6;border:1px solid var(--primary-color, #3399ff);border-radius:6px;box-shadow:0 4px 14px #00000073;display:flex;flex-direction:column;overflow:hidden}.intensity-panel-header{display:flex;align-items:center;justify-content:space-between;padding:.25rem .5rem;cursor:move;-webkit-user-select:none;user-select:none;background:#ffffff14;color:#eee;font-size:.75rem;font-weight:600}.intensity-panel-header i{opacity:.6;font-size:.7rem}.intensity-panel-plot{flex:1 1 auto;width:100%}:host ::ng-deep .sam-progress .p-progressbar-value{transition:none}\n"], dependencies: [{ kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: "directive", type: i1$1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { 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: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: i5.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: "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: i8.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "component", type: i9.Slider, selector: "p-slider", inputs: ["animate", "disabled", "min", "max", "orientation", "step", "range", "style", "styleClass", "ariaLabel", "ariaLabelledBy", "tabindex", "autofocus"], outputs: ["onChange", "onSlideEnd"] }, { kind: "component", type: i10.InputNumber, selector: "p-inputNumber", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "style", "placeholder", "size", "maxlength", "tabindex", "title", "ariaLabelledBy", "ariaLabel", "ariaRequired", "name", "required", "autocomplete", "min", "max", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "step", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "variant", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus", "disabled"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "component", type: i9$1.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: "component", type: i15$1.ContextMenu, selector: "p-contextMenu", inputs: ["model", "triggerEvent", "target", "global", "style", "styleClass", "appendTo", "autoZIndex", "baseZIndex", "id", "ariaLabel", "ariaLabelledBy", "pressDelay"], outputs: ["onShow", "onHide"] }, { kind: "component", type: i9$2.ProgressBar, selector: "p-progressBar", inputs: ["value", "showValue", "styleClass", "style", "unit", "mode", "color"] }, { kind: "component", type: i17$1.Toast, selector: "p-toast", inputs: ["key", "autoZIndex", "baseZIndex", "life", "style", "styleClass", "position", "preventOpenDuplicates", "preventDuplicates", "showTransformOptions", "hideTransformOptions", "showTransitionOptions", "hideTransitionOptions", "breakpoints"], outputs: ["onClose"] }, { kind: "component", type: i16.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: "component", type: ToolbarComponent, selector: "plotting-toolbar", inputs: ["imageInfo", "plotTypeOptions", "selectedPlotType", "isoRange", "maxIndex", "zIndex", "isHeatmap", "is3dRegions", "activeDragMode", "activeSurface3dMode", "axesVisible", "wireframeActive", "resolutionScale", "wandSensitivity", "brushSize", "vertexEraserRadius", "canUndo", "canRedo", "stackOptions", "tools", "imageSmoothingEnabled", "samModels", "samModelId", "contributedTools", "toolModelIds"], outputs: ["selectPlotType", "addProfileLine", "toggleImageSmoothing", "isoRangeChange", "openChannelHistogram", "openSpatialControls", "openRegionEditor", "selectStackOption", "zScrub", "zSlide", "zIndexInput", "reloadAndPlot", "downloadImage", "autoscaleImage", "toggleDragMode", "zoomIn", "zoomOut", "toggleSurface3dMode", "resetSurfaceCamera", "toggleAxes", "toggleWireframe", "selectResolution", "deleteRegion", "undoRegion", "redoRegion", "segmentRegions", "segmentCellpose", "runTool", "toolModelChange", "openToolParams", "samModelChange", "wandSensitivityChange", "brushSizeChange", "vertexEraserRadiusChange"] }, { kind: "component", type: RegionEditorComponent, selector: "region-editor" }, { kind: "component", type: ChannelHistogramComponent, selector: "channel-histogram", inputs: ["visible"], outputs: ["visibleChange"] }, { kind: "component", type: SpatialControlsComponent, selector: "spatial-controls", inputs: ["visible", "is3d"], outputs: ["visibleChange"] }] }); }
|
|
13679
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: VisualizerComponent, deps: [{ token: IMAGE_STATE_PORT }, { token: VISUALIZER }, { token: i1$2.MessageService }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }, { token: VisualizerStore }, { token: SamToolService }, { token: CellSegmentToolService }, { token: SamPointToolService }, { token: RegionOpsService }, { token: VIZ_CONFIG, optional: true }, { token: TOOLBAR_TOOLS, optional: true }, { token: SPATIAL_DATA_PORT, optional: true }, { token: PLOT_TYPE_CONTRIBUTIONS, optional: true }, { token: i0.Injector, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
13680
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: VisualizerComponent, selector: "visualizer", inputs: { toolbarTools: "toolbarTools", testMode: "testMode" }, outputs: { isStackEvent: "isStackEvent", isGrayscaleEvent: "isGrayscaleEvent" }, host: { listeners: { "window:keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "contextMenu", first: true, predicate: ["cm"], descendants: true }, { propertyName: "plotModePanelSlot", first: true, predicate: ["plotModePanelSlot"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- SAM toast: live status + encoder-download progress bar (jit-ui#90). Sticky\n while a segment runs; stays open until the bar reaches 100% and the run ends. -->\n<p-toast [key]=\"samToastKey\" position=\"bottom-right\">\n <ng-template let-message pTemplate=\"message\">\n <div style=\"flex: 1; min-width: 16rem\">\n <div class=\"font-bold mb-1\">{{ message.summary }}</div>\n <div>{{ samStatus || message.detail }}</div>\n <p-progressBar *ngIf=\"samDownloading\" [value]=\"samProgress\" styleClass=\"mt-2 sam-progress\"></p-progressBar>\n <p-progressBar *ngIf=\"samBusy && !samDownloading\" mode=\"indeterminate\" styleClass=\"mt-2\"\n [style]=\"{ height: '4px' }\"></p-progressBar>\n </div>\n </ng-template>\n</p-toast>\n\n<!-- Result/error notices from the library itself (segmentation outcome, region\n validation). Rendered here so they do not depend on the host mounting a\n keyless <p-toast> \u2014 without this the messages went nowhere in any host but\n jit-ui, and a failed run looked like a button that did nothing. -->\n<p-toast [key]=\"resultToastKey\" position=\"bottom-right\"></p-toast>\n\n<!-- Shared outlets for notices raised where there is no per-instance key to use:\n the root-provided PlotlyService and the region-editor dialog. Rendered by a\n single visualizer (ownsSharedToasts) so a message from the root service is\n not shown once per live viewer. -->\n<p-toast *ngIf=\"ownsSharedToasts\" [key]=\"vizToastKey\"></p-toast>\n<p-toast *ngIf=\"ownsSharedToasts\" [key]=\"vizAlertToastKey\" position=\"center\"></p-toast>\n\n<!-- Toolbar dock: docked across the top by default; drag the handle to detach\n it into a floating, movable window (frees the top row for the image). -->\n<div class=\"toolbar-dock\" [class.floating]=\"toolbarFloating\"\n [style.left.px]=\"toolbarFloating ? toolbarPos.x : null\"\n [style.top.px]=\"toolbarFloating ? toolbarPos.y : null\">\n <div class=\"toolbar-drag-handle\" (mousedown)=\"onToolbarDragStart($event)\"\n pTooltip=\"Drag to float the toolbar\" tooltipPosition=\"bottom\">\n <i class=\"pi pi-bars\"></i>\n </div>\n<plotting-toolbar\n [imageInfo]=\"imageInfo\"\n [plotTypeOptions]=\"plotTypeMenu\"\n [selectedPlotType]=\"selectedPlotTypeId\"\n [basePlotType]=\"basePlotType\"\n [isoRange]=\"isoRange\"\n [maxIndex]=\"maxIndex\"\n [zIndex]=\"zIndex\"\n [isHeatmap]=\"isHeatmap\"\n [is3dRegions]=\"isSpatial3dMode\"\n [activeDragMode]=\"activeDragMode\"\n [activeSurface3dMode]=\"activeSurface3dMode\"\n [axesVisible]=\"axesVisible\"\n [wireframeActive]=\"wireframeActive\"\n [resolutionScale]=\"resolutionScale\"\n [wandSensitivity]=\"wandSensitivity\"\n [brushSize]=\"brushSize\"\n [samModels]=\"samModels\"\n [samModelId]=\"samModelId\"\n [contributedTools]=\"contributedTools\"\n [toolModelIds]=\"toolModelIds\"\n [vertexEraserRadius]=\"vertexEraserRadius\"\n [canUndo]=\"canUndoRegion\"\n [canRedo]=\"canRedoRegion\"\n [stackOptions]=\"stackOptions\"\n [tools]=\"toolbarTools\"\n [imageSmoothingEnabled]=\"imageSmoothingEnabled\"\n (selectPlotType)=\"onSelectPlotType($event)\"\n (toggleImageSmoothing)=\"onToggleImageSmoothing()\"\n (addProfileLine)=\"addProfileLine()\"\n (isoRangeChange)=\"onIsoRangeChange($event)\"\n (openChannelHistogram)=\"openChannelHistogram()\"\n (openSpatialControls)=\"openSpatialControls()\"\n (openRegionEditor)=\"openRegionEditor()\"\n (selectStackOption)=\"selectStackOption($event)\"\n (zScrub)=\"onZScrub($event)\"\n (zSlide)=\"onZSlide($event)\"\n (zIndexInput)=\"zIndex = $event; updateZIndex()\"\n (reloadAndPlot)=\"reloadAndPlot()\"\n (downloadImage)=\"downloadImage()\"\n (autoscaleImage)=\"autoscaleImage()\"\n (toggleDragMode)=\"toggleDragMode($event)\"\n (zoomIn)=\"zoomIn()\"\n (zoomOut)=\"zoomOut()\"\n (toggleSurface3dMode)=\"toggleSurface3dMode($event)\"\n (resetSurfaceCamera)=\"resetSurfaceCamera()\"\n (toggleAxes)=\"toggleAxes()\"\n (toggleWireframe)=\"toggleWireframe()\"\n (selectResolution)=\"selectResolution($event)\"\n (deleteRegion)=\"deleteRegion()\"\n (undoRegion)=\"undoRegion()\"\n (redoRegion)=\"redoRegion()\"\n (segmentRegions)=\"segmentRegions()\"\n (segmentCellpose)=\"segmentCellpose()\"\n (samModelChange)=\"onSamModelChange($event)\"\n (runTool)=\"runTool($event)\"\n (toolModelChange)=\"onToolModelChange($event)\"\n (openToolParams)=\"openToolParams($event)\"\n (wandSensitivityChange)=\"onWandSensitivityChange($event)\"\n (brushSizeChange)=\"onBrushSizeChange($event)\"\n (vertexEraserRadiusChange)=\"onVertexEraserRadiusChange($event)\">\n <!-- Host-projected custom toolbar buttons, forwarded into the toolbar. -->\n <ng-content></ng-content>\n</plotting-toolbar>\n <p-button *ngIf=\"toolbarFloating\" class=\"toolbar-dock-btn\" icon=\"pi pi-thumbtack\" size=\"small\" [text]=\"true\"\n pTooltip=\"Dock to top\" tooltipPosition=\"bottom\" (onClick)=\"dockToolbar()\">\n </p-button>\n</div>\n\n<p-contextMenu #cm [model]=\"contextMenuItems\" appendTo=\"body\"></p-contextMenu>\n<!-- No `h-full` here: PrimeFlex's height:100% !important made #plot as tall as\n the whole host, so under the docked toolbar its bottom toolbar-height strip\n (incl. the OSD scale bar at bottom-left) was clipped by the host's\n overflow:hidden. The component stylesheet sizes it (.viz-plot { flex: 1 }). -->\n<div class=\"w-full relative viz-plot\" [id]=\"plotDivName\">\n <div *ngIf=\"imgLoading || stackLoading || isCaching\" id=\"overlay\">\n <!-- Determinate stack/volume progress takes precedence: while it's active show ONLY the\n progress bar + \"Loading frames...\", not the generic spinner/\"Loading image...\" on top of\n it (both were rendering at once for napari volume/isosurface loads). -->\n <ng-container *ngIf=\"stackLoading; else imgLoadingTpl\">\n <p-progressBar class=\"progress\" [value]=\"loadingPercentage\"></p-progressBar>\n <span class=\"text-loading\">Loading frames...</span>\n </ng-container>\n <ng-template #imgLoadingTpl>\n <!-- Cache-copy progress is shown whenever a cache download is active (isCaching), independent\n of `imgLoading`, so it's never dropped/masked mid-download. -->\n <ng-container *ngIf=\"isCaching; else spinnerLoading\">\n <!-- Stage-aware message comes from CacheProgressService updating\n imageLoadingMessage$ (\"Caching image...\" during the GCS download,\n then \"Generating preview...\" during per-slice thumbnail gen). Once the\n download hits 100% the remaining work (server preview/tile generation)\n has no determinate progress, so fall through to the spinner template\n rather than parking a full bar under \"Loading image...\". -->\n <p-progressBar class=\"progress\" [value]=\"cacheProgress\"></p-progressBar>\n <span class=\"text-loading\">{{loadingMessage}}</span>\n </ng-container>\n <ng-template #spinnerLoading>\n <div *ngIf=\"imgLoading\" class=\"spinner\"></div>\n <span *ngIf=\"imgLoading && !zoom\" class=\"text-loading\">{{loadingMessage}}</span>\n <span *ngIf=\"imgLoading && zoom\" class=\"zoom-text-loading\">{{loadingMessage}}</span>\n </ng-template>\n </ng-template>\n <p-button *ngIf=\"stackLoading || imgLoading\" class=\"cancel-loading \" label=\"Cancel\"\n (onClick)=\"cancelLoading()\">\n </p-button>\n </div>\n <!-- Translucent \"sharpening\" overlay shown after the small-tier preview is\n on screen while the large-tier render is still loading in the\n background. Lets the user see the blurry image underneath but makes\n it clear something is still loading so they don't mistake the\n low-res render for the final image. -->\n <div *ngIf=\"sharpening && !imgLoading && !stackLoading && !isCaching\" id=\"sharpening-overlay\">\n <div class=\"spinner\"></div>\n <span class=\"text-loading\">Sharpening preview...</span>\n </div>\n\n</div>\n\n<!-- Floating, draggable intensity-profile inset. Shown in LINE mode; the chart\n updates live as the line ROI on the image is moved. Kept OUTSIDE #plot and\n positioned fixed so it floats over the whole page (not clipped to the\n diagram/canvas area). -->\n<div *ngIf=\"hasProfiles\" class=\"intensity-panel\"\n [style.left.px]=\"profilePanelPos.x\" [style.top.px]=\"profilePanelPos.y\">\n <div class=\"intensity-panel-header\" (mousedown)=\"onProfilePanelDragStart($event)\">\n <span>Intensity profile</span>\n <i class=\"pi pi-arrows-alt\"></i>\n </div>\n <div [id]=\"intensityInsetDiv\" class=\"intensity-panel-plot\"></div>\n</div>\n\n<!-- Channels & Histogram: non-modal floating dialog opened from the toolbar. -->\n<channel-histogram [(visible)]=\"showChannelHistogram\"></channel-histogram>\n\n<!-- Spatial-omics controls. Renders its own empty state when no dataset (or no\n SPATIAL_DATA_PORT) is present, so it is always safe to host. -->\n<spatial-controls [(visible)]=\"showSpatialControls\" [is3d]=\"isSpatial3dMode\" ></spatial-controls>\n\n<!-- Side panel of the active contributed plot mode (PLOT_TYPE_CONTRIBUTIONS),\n in the same right-hand area as the spatial controls and the Region Editor.\n Exists only while the mode's session is live; there is no close button,\n because leaving the mode is what ends it. A component panel gets\n PLOT_MODE_CONTEXT / PLOT_MODE_SESSION from its injector; a `mount` panel's\n host element is attached to the slot below (see plotModePanelSlot). -->\n<p-dialog *ngIf=\"plotModePanel as panel\" [header]=\"panel.title\" [visible]=\"true\"\n [modal]=\"false\" [closable]=\"false\" [resizable]=\"true\" [draggable]=\"true\"\n appendTo=\"body\" position=\"right\" styleClass=\"plot-mode-panel\"\n [style]=\"{ width: '22rem', maxHeight: '85vh' }\"\n [contentStyle]=\"{ overflow: 'auto' }\">\n <ng-container *ngIf=\"panel.component\">\n <ng-container *ngComponentOutlet=\"panel.component; injector: panel.injector ?? undefined\"></ng-container>\n </ng-container>\n <div *ngIf=\"panel.host\" #plotModePanelSlot class=\"plot-mode-panel-slot\"></div>\n</p-dialog>\n\n<!-- Region Editor: non-modal, draggable, resizable floating dialog opened from\n the toolbar (formerly the right-panel Regions tab). Modeless so regions can\n still be drawn/selected on the plot while it's open; stays linked to this\n plotting instance via the root RegionStore singleton. Opens over the right\n side of the view. -->\n<p-dialog header=\"Region Editor\" [(visible)]=\"showRegionEditor\"\n [modal]=\"false\" [resizable]=\"true\" [draggable]=\"true\"\n [maximizable]=\"true\" appendTo=\"body\" position=\"right\"\n [style]=\"{ width: regionEditorWidth, height: '85vh' }\"\n [contentStyle]=\"{ overflow: 'auto' }\">\n <region-editor></region-editor>\n</p-dialog>\n\n<!-- Simplify (Douglas\u2013Peucker): custom px-threshold picker for the selected\n region(s), opened from the right-click \"Simplify \u25B8 Custom\u2026\" action. -->\n<p-dialog header=\"Simplify region(s)\" [(visible)]=\"displaySimplifyDialog\" [modal]=\"true\"\n [style]=\"{width: '22rem'}\" [draggable]=\"false\" [resizable]=\"false\">\n <div class=\"flex flex-column gap-2\">\n <span>Altitude threshold: <b>{{ simplifyThreshold }} px</b></span>\n <p-slider [(ngModel)]=\"simplifyThreshold\" [min]=\"1\" [max]=\"20\" [step]=\"1\"></p-slider>\n <small>Vertices within this distance of the line between their neighbours are removed.</small>\n </div>\n <ng-template pTemplate=\"footer\">\n <p-button label=\"Cancel\" [text]=\"true\" (onClick)=\"displaySimplifyDialog = false\"></p-button>\n <p-button label=\"Apply\" icon=\"pi pi-check\" (onClick)=\"simplifyRegions(simplifyThreshold)\"></p-button>\n </ng-template>\n</p-dialog>\n\n<!-- Parameters for whichever contributed tool is open. One dialog for all of\n them: a tool declares its fields (ToolParamSpec) and this renders them, so\n the toolbar can lay every tool out the same way and a plugin needs no\n PrimeNG of its own. Defaults come from the active checkpoint, since they\n encode what it was trained for. -->\n<p-dialog *ngIf=\"openParamsTool as tool\"\n [header]=\"tool.label + ' parameters'\"\n [visible]=\"true\" (visibleChange)=\"openParamsToolId = null\"\n [modal]=\"true\"\n [style]=\"{width: '30rem'}\" [draggable]=\"false\" [resizable]=\"false\" appendTo=\"body\">\n <div class=\"grid formgrid p-fluid\">\n <ng-container *ngFor=\"let p of tool.params\">\n <div class=\"field\" [ngClass]=\"p.type === 'boolean' ? 'col-12' : 'col-6'\">\n <ng-container [ngSwitch]=\"p.type\">\n\n <ng-container *ngSwitchCase=\"'number'\">\n <label class=\"text-sm\" [pTooltip]=\"p.tooltip || ''\">{{ p.label }}</label>\n <p-inputNumber [ngModel]=\"paramsFor(tool.id)[p.id]\"\n (ngModelChange)=\"paramsFor(tool.id)[p.id] = $event\"\n [min]=\"numberParam(p).min\" [max]=\"numberParam(p).max\"\n [step]=\"numberParam(p).step\"\n [minFractionDigits]=\"numberParam(p).fractionDigits\"\n [showButtons]=\"true\"></p-inputNumber>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <label class=\"text-sm\" [pTooltip]=\"p.tooltip || ''\">{{ p.label }}</label>\n <p-dropdown [options]=\"selectParam(p).options\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"paramsFor(tool.id)[p.id]\"\n (ngModelChange)=\"paramsFor(tool.id)[p.id] = $event\"\n appendTo=\"body\"></p-dropdown>\n </ng-container>\n\n <!-- Checkbox label sits beside the box and may run to a sentence, so\n it does not get the stacked label the other types use. -->\n <div *ngSwitchCase=\"'boolean'\" class=\"flex align-items-center gap-2\">\n <p-checkbox [ngModel]=\"paramsFor(tool.id)[p.id]\"\n (ngModelChange)=\"paramsFor(tool.id)[p.id] = $event\"\n [binary]=\"true\" [inputId]=\"tool.id + '-' + p.id\"></p-checkbox>\n <label [for]=\"tool.id + '-' + p.id\" class=\"text-sm mb-0\"\n [pTooltip]=\"p.tooltip || ''\">{{ p.label }}</label>\n </div>\n\n </ng-container>\n </div>\n </ng-container>\n </div>\n <ng-template pTemplate=\"footer\">\n <p-button label=\"Reset to model defaults\" [text]=\"true\" size=\"small\"\n (onClick)=\"resetToolParams(tool.id)\"></p-button>\n <p-button label=\"Close\" size=\"small\" (onClick)=\"openParamsToolId = null\"></p-button>\n </ng-template>\n</p-dialog>\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow:hidden}.viz-plot{flex:1;min-height:0;height:auto!important}.toolbar-dock{display:flex;align-items:stretch;width:100%}.toolbar-dock plotting-toolbar{flex:1;min-width:0}.toolbar-dock.floating{position:fixed;z-index:1000;width:auto;max-width:calc(100% - 16px);background:var(--surface-card, #ffffff);border:1px solid var(--surface-border, #cccccc);border-radius:6px;box-shadow:0 4px 16px #0000004d}.toolbar-dock.floating plotting-toolbar{flex:0 1 auto}.toolbar-dock.floating .toolbar-drag-handle{border-radius:6px 0 0 6px}.toolbar-drag-handle{display:flex;align-items:center;justify-content:center;padding:0 .4rem;cursor:move;color:var(--primary-color);background:var(--surface-100, rgba(0, 0, 0, .04));border-right:1px solid var(--surface-border, #cccccc);-webkit-user-select:none;user-select:none}.toolbar-dock-btn{align-self:center}:host ::ng-deep .navigator{margin-bottom:80px!important;margin-right:12px!important}kbd{background:#e8e8e8;border:1px solid #bbb;border-radius:3px;padding:1px 5px;font-family:monospace;font-size:.85em}::ng-deep .context-menu-active>.p-menuitem-content{background-color:var(--primary-color)!important}::ng-deep .context-menu-active>.p-menuitem-content .p-menuitem-icon,::ng-deep .context-menu-active>.p-menuitem-content .p-menuitem-text{color:#fff!important}::ng-deep .context-menu-active>.p-menuitem-content .zoom-box-off-icon,::ng-deep .context-menu-active>.p-menuitem-content .wand-icon,::ng-deep .context-menu-active>.p-menuitem-content .polyline-icon,::ng-deep .context-menu-active>.p-menuitem-content .polygon-vertices-icon,::ng-deep .context-menu-active>.p-menuitem-content .vertex-add-icon,::ng-deep .context-menu-active>.p-menuitem-content .vertex-delete-icon,::ng-deep .context-menu-active>.p-menuitem-content .region-move-icon,::ng-deep .context-menu-active>.p-menuitem-content .to-bezier-icon,::ng-deep .context-menu-active>.p-menuitem-content .to-polygon-icon{filter:brightness(0) invert(1)}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.progress{position:absolute;width:50%;left:25%;top:55%}.text-loading{position:absolute;margin:0 auto;left:45%;top:60%;font-weight:700}.zoom-text-loading{position:absolute;margin:0 auto;left:35%;top:60%;font-weight:700;display:inline-block;width:350px}.cancel-loading{position:absolute;margin:0 auto;left:46%;top:70%}.spinner{position:absolute;left:45%;top:45%;height:100px;width:100px;margin:0 auto;-webkit-animation:rotation .6s infinite linear;-moz-animation:rotation .6s infinite linear;-o-animation:rotation .6s infinite linear;animation:rotation .6s infinite linear;border-left:6px solid rgba(0,174,239,.15);border-right:6px solid rgba(0,174,239,.15);border-bottom:6px solid rgba(0,174,239,.15);border-top:6px solid rgb(82,107,178);border-radius:100%}#overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#ffffff80;z-index:1000}#sharpening-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#ffffff2e;pointer-events:none;z-index:1000}#sharpening-overlay .spinner{height:60px;width:60px;border-width:4px;left:calc(50% - 30px);top:calc(50% - 60px)}#sharpening-overlay .text-loading{position:absolute;left:0;right:0;width:auto;text-align:center;top:calc(50% + 35px);font-size:.9rem;color:#526bb2}@-webkit-keyframes rotation{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(359deg)}}@-moz-keyframes rotation{0%{-moz-transform:rotate(0deg)}to{-moz-transform:rotate(359deg)}}@-o-keyframes rotation{0%{-o-transform:rotate(0deg)}to{-o-transform:rotate(359deg)}}@keyframes rotation{0%{transform:rotate(0)}to{transform:rotate(359deg)}}::ng-deep .zoom-box-off-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\" fill=\"black\" fill-rule=\"evenodd\">%0A <!-- Rectangle outline -->%0A <rect x=\"2\" y=\"2\" width=\"56\" height=\"56\"/>%0A <rect x=\"9\" y=\"9\" width=\"42\" height=\"42\"/>%0A <!-- Magnifying glass ring -->%0A <circle cx=\"70\" cy=\"70\" r=\"23\"/>%0A <circle cx=\"70\" cy=\"70\" r=\"14\"/>%0A <!-- Handle -->%0A <polygon points=\"81,81 97,97 92,100 77,85\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .wand-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<svg%0A viewBox=\"0 0 100 100\"%0A fill=\"black\"%0A fill-rule=\"evenodd\"%0A version=\"1.1\"%0A id=\"svg4\"%0A sodipodi:docname=\"wand.svg\"%0A inkscape:version=\"1.4.3 (0d15f75, 2025-12-25)\"%0A xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\"%0A xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\"%0A xmlns=\"http://www.w3.org/2000/svg\"%0A xmlns:svg=\"http://www.w3.org/2000/svg\">%0A <defs%0A id=\"defs4\" />%0A <sodipodi:namedview%0A id=\"namedview4\"%0A pagecolor=\"%23ffffff\"%0A bordercolor=\"%23000000\"%0A borderopacity=\"0.25\"%0A inkscape:showpageshadow=\"2\"%0A inkscape:pageopacity=\"0.0\"%0A inkscape:pagecheckerboard=\"0\"%0A inkscape:deskcolor=\"%23d1d1d1\"%0A inkscape:zoom=\"8.74\"%0A inkscape:cx=\"50\"%0A inkscape:cy=\"49.942792\"%0A inkscape:window-width=\"1024\"%0A inkscape:window-height=\"1186\"%0A inkscape:window-x=\"3560\"%0A inkscape:window-y=\"131\"%0A inkscape:window-maximized=\"0\"%0A inkscape:current-layer=\"svg4\" />%0A <!-- Wand handle: thin diagonal stick from lower-left to upper-right -->%0A <polygon%0A points=\"12,92 78,26 74,22 8,88 \"%0A id=\"polygon1\"%0A transform=\"matrix(1.0375192,0.05799445,0.25179496,0.91871864,-17.85351,5.6289793)\" />%0A <!-- 4-pointed star at the wand tip -->%0A <polygon%0A points=\"62,34 76,30 80,6 84,30 98,34 84,38 80,62 76,38 \"%0A id=\"polygon2\"%0A transform=\"matrix(1.3082889,0,0,1.0245178,-38.907958,-1.4056881)\" />%0A <!-- Small sparkle to the upper-left -->%0A <polygon%0A points=\"46,30 36,32 34,42 32,32 22,30 32,28 34,18 36,28 \"%0A id=\"polygon3\"%0A transform=\"translate(-9.610984,-6.7505721)\" />%0A <!-- Small sparkle to the lower-right -->%0A <polygon%0A points=\"62,80 70,82 62,84 60,92 58,84 50,82 58,80 60,72 \"%0A id=\"polygon4\"%0A transform=\"translate(0,-1.6018307)\" />%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .brush-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 640\"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d=\"M512.5 74.3L291.1 222C262 241.4 243.5 272.9 240.5 307.3C302.8 320.1 351.9 369.2 364.8 431.6C399.3 428.6 430.7 410.1 450.1 381L597.7 159.5C604.4 149.4 608 137.6 608 125.4C608 91.5 580.5 64 546.6 64C534.5 64 522.6 67.6 512.5 74.3zM320 464C320 402.1 269.9 352 208 352C146.1 352 96 402.1 96 464C96 467.9 96.2 471.8 96.6 475.6C98.4 493.1 86.4 512 68.8 512L64 512C46.3 512 32 526.3 32 544C32 561.7 46.3 576 64 576L208 576C269.9 576 320 525.9 320 464z\"/></svg>') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .polyline-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\">%0A <polyline points=\"1,14 5,3 10,11 15,2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .polygon-vertices-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Polygon (click-to-place) tool icon: a traced polyline with vertex dots. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.4740709,0,0,1.4740709,-6.0268793,-6.8086826)\">%0A <polyline points=\"23.877,16.627 14.657,10.767 5.25,21.551 12.757,29.397\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\"%0A stroke-linejoin=\"round\" stroke-linecap=\"round\"/>%0A <circle cx=\"23.877\" cy=\"16.627\" r=\"1.9\"/>%0A <circle cx=\"14.657\" cy=\"10.767\" r=\"1.9\"/>%0A <circle cx=\"5.25\" cy=\"21.551\" r=\"1.9\"/>%0A <circle cx=\"12.757\" cy=\"29.397\" r=\"1.9\"/>%0A </g>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .vertex-add-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Add-vertex tool icon: a region blob with three corner vertices and a plus glyph. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.5222969,0,0,1.5222969,-8.2143505,-7.6146399)\">%0A <path d=\"m 9.35834,12.33267 c 1.78,-2.53 4.93,-3.56 7.86,-3.98 3.98,-0.72 8.33,0.08 11.52,2.67 2.62,1.98 4.71,5.1 4.53,8.49 -0.29,2.27 -2.5,3.81 -4.66,4.01 -2.75,0.43 -5.54,0.07 -8.28,-0.26 -3.99,-0.68 -8.05,-2.16 -10.93,-5.12 -1.32,-1.63 -1.39,-4.17 -0.04,-5.81 z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\" stroke-miterlimit=\"10\"/>%0A <circle cx=\"8.294\" cy=\"15.248\" r=\"2.63\"/>%0A <circle cx=\"19.781\" cy=\"8.287\" r=\"2.63\"/>%0A <circle cx=\"25.044\" cy=\"23.748\" r=\"2.63\"/>%0A </g>%0A <path d=\"M 9.65,29.0 H 5.85 v 5.3 H 0.55 v 3.4 H 5.85 v 5.3 H 9.65 V 37.7 H 14.95 V 34.3 H 9.65 Z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .vertex-delete-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Delete-vertex tool icon: a region blob with three corner vertices and a minus glyph. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.5222969,0,0,1.5222969,-8.2143505,-7.6146399)\">%0A <path d=\"m 9.35834,12.33267 c 1.78,-2.53 4.93,-3.56 7.86,-3.98 3.98,-0.72 8.33,0.08 11.52,2.67 2.62,1.98 4.71,5.1 4.53,8.49 -0.29,2.27 -2.5,3.81 -4.66,4.01 -2.75,0.43 -5.54,0.07 -8.28,-0.26 -3.99,-0.68 -8.05,-2.16 -10.93,-5.12 -1.32,-1.63 -1.39,-4.17 -0.04,-5.81 z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\" stroke-miterlimit=\"10\"/>%0A <circle cx=\"8.294\" cy=\"15.248\" r=\"2.63\"/>%0A <circle cx=\"19.781\" cy=\"8.287\" r=\"2.63\"/>%0A <circle cx=\"25.044\" cy=\"23.748\" r=\"2.63\"/>%0A </g>%0A <path d=\"M 0.55,34.3 v 3.4 H 14.95 V 34.3 Z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .region-move-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Region-move tool icon: a region blob with a drag arrow. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <path d=\"m 17.115582,8.9648664 c 1.707597,-3.3276806 4.729468,-4.6824281 7.540287,-5.2348498 3.827703,-0.947008 7.991169,0.1052238 11.051414,3.5118228 2.523022,2.604273 4.518416,6.7079726 4.345738,11.1668036 -0.278204,2.985704 -2.398311,5.011251 -4.470451,5.274308 -2.638142,0.565574 -5.314655,0.09206 -7.943203,-0.341976 C 23.811664,22.446579 19.916808,20.499952 17.153955,16.606696 15.89724,14.462775 15.820494,11.12194 17.115585,8.9648664 Z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.1\" stroke-miterlimit=\"10\"/>%0A <path d=\"m 26.741429,13.966963 6.746922,16.786269 3.229398,-4.060758 3.583857,3.583857 2.761195,-2.761196 -3.583856,-3.571852 4.060758,-3.2354 z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .to-bezier-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Convert-to-bezier tool icon: a smooth curve through two anchor points with control handles. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <!-- smooth cubic curve between the two anchors -->%0A <path d=\"M 7,34 C 7,16 37,28 37,10\" fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.4\"%0A stroke-linecap=\"round\"/>%0A <!-- control handles -->%0A <line x1=\"7\" y1=\"34\" x2=\"7\" y2=\"22\" stroke=\"%23000000\" stroke-width=\"1.4\"/>%0A <line x1=\"37\" y1=\"10\" x2=\"37\" y2=\"22\" stroke=\"%23000000\" stroke-width=\"1.4\"/>%0A <circle cx=\"7\" cy=\"22\" r=\"2.2\"/>%0A <circle cx=\"37\" cy=\"22\" r=\"2.2\"/>%0A <!-- anchor points -->%0A <rect x=\"4\" y=\"31\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"34\" y=\"7\" width=\"6\" height=\"6\" rx=\"1\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .to-polygon-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Convert-to-polygon tool icon: endpoints joined by straight segments through a vertex. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <!-- straight-segment path through the vertices -->%0A <polyline points=\"7,34 22,12 37,10\" fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.4\"%0A stroke-linecap=\"round\" stroke-linejoin=\"round\"/>%0A <!-- vertices -->%0A <rect x=\"4\" y=\"31\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"19\" y=\"9\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"34\" y=\"7\" width=\"6\" height=\"6\" rx=\"1\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.intensity-panel{position:fixed;z-index:1100;width:280px;height:180px;background:#191919e6;border:1px solid var(--primary-color, #3399ff);border-radius:6px;box-shadow:0 4px 14px #00000073;display:flex;flex-direction:column;overflow:hidden}.intensity-panel-header{display:flex;align-items:center;justify-content:space-between;padding:.25rem .5rem;cursor:move;-webkit-user-select:none;user-select:none;background:#ffffff14;color:#eee;font-size:.75rem;font-weight:600}.intensity-panel-header i{opacity:.6;font-size:.7rem}.intensity-panel-plot{flex:1 1 auto;width:100%}:host ::ng-deep .sam-progress .p-progressbar-value{transition:none}\n"], dependencies: [{ kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }, { 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: "directive", type: i1$1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1$1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { 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: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: i5.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: "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: i8.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "component", type: i9.Slider, selector: "p-slider", inputs: ["animate", "disabled", "min", "max", "orientation", "step", "range", "style", "styleClass", "ariaLabel", "ariaLabelledBy", "tabindex", "autofocus"], outputs: ["onChange", "onSlideEnd"] }, { kind: "component", type: i10.InputNumber, selector: "p-inputNumber", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "style", "placeholder", "size", "maxlength", "tabindex", "title", "ariaLabelledBy", "ariaLabel", "ariaRequired", "name", "required", "autocomplete", "min", "max", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "step", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "variant", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus", "disabled"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "component", type: i9$1.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: "component", type: i15$1.ContextMenu, selector: "p-contextMenu", inputs: ["model", "triggerEvent", "target", "global", "style", "styleClass", "appendTo", "autoZIndex", "baseZIndex", "id", "ariaLabel", "ariaLabelledBy", "pressDelay"], outputs: ["onShow", "onHide"] }, { kind: "component", type: i9$2.ProgressBar, selector: "p-progressBar", inputs: ["value", "showValue", "styleClass", "style", "unit", "mode", "color"] }, { kind: "component", type: i17$1.Toast, selector: "p-toast", inputs: ["key", "autoZIndex", "baseZIndex", "life", "style", "styleClass", "position", "preventOpenDuplicates", "preventDuplicates", "showTransformOptions", "hideTransformOptions", "showTransitionOptions", "hideTransitionOptions", "breakpoints"], outputs: ["onClose"] }, { kind: "component", type: i16.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: "component", type: ToolbarComponent, selector: "plotting-toolbar", inputs: ["imageInfo", "plotTypeOptions", "selectedPlotType", "basePlotType", "isoRange", "maxIndex", "zIndex", "isHeatmap", "is3dRegions", "activeDragMode", "activeSurface3dMode", "axesVisible", "wireframeActive", "resolutionScale", "wandSensitivity", "brushSize", "vertexEraserRadius", "canUndo", "canRedo", "stackOptions", "tools", "imageSmoothingEnabled", "samModels", "samModelId", "contributedTools", "toolModelIds"], outputs: ["selectPlotType", "addProfileLine", "toggleImageSmoothing", "isoRangeChange", "openChannelHistogram", "openSpatialControls", "openRegionEditor", "selectStackOption", "zScrub", "zSlide", "zIndexInput", "reloadAndPlot", "downloadImage", "autoscaleImage", "toggleDragMode", "zoomIn", "zoomOut", "toggleSurface3dMode", "resetSurfaceCamera", "toggleAxes", "toggleWireframe", "selectResolution", "deleteRegion", "undoRegion", "redoRegion", "segmentRegions", "segmentCellpose", "runTool", "toolModelChange", "openToolParams", "samModelChange", "wandSensitivityChange", "brushSizeChange", "vertexEraserRadiusChange"] }, { kind: "component", type: RegionEditorComponent, selector: "region-editor" }, { kind: "component", type: ChannelHistogramComponent, selector: "channel-histogram", inputs: ["visible"], outputs: ["visibleChange"] }, { kind: "component", type: SpatialControlsComponent, selector: "spatial-controls", inputs: ["visible", "is3d"], outputs: ["visibleChange"] }] }); }
|
|
13056
13681
|
}
|
|
13057
13682
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: VisualizerComponent, decorators: [{
|
|
13058
13683
|
type: Component,
|
|
13059
|
-
args: [{ selector: 'visualizer', template: "<!-- SAM toast: live status + encoder-download progress bar (jit-ui#90). Sticky\n while a segment runs; stays open until the bar reaches 100% and the run ends. -->\n<p-toast [key]=\"samToastKey\" position=\"bottom-right\">\n <ng-template let-message pTemplate=\"message\">\n <div style=\"flex: 1; min-width: 16rem\">\n <div class=\"font-bold mb-1\">{{ message.summary }}</div>\n <div>{{ samStatus || message.detail }}</div>\n <p-progressBar *ngIf=\"samDownloading\" [value]=\"samProgress\" styleClass=\"mt-2 sam-progress\"></p-progressBar>\n <p-progressBar *ngIf=\"samBusy && !samDownloading\" mode=\"indeterminate\" styleClass=\"mt-2\"\n [style]=\"{ height: '4px' }\"></p-progressBar>\n </div>\n </ng-template>\n</p-toast>\n\n<!-- Result/error notices from the library itself (segmentation outcome, region\n validation). Rendered here so they do not depend on the host mounting a\n keyless <p-toast> \u2014 without this the messages went nowhere in any host but\n jit-ui, and a failed run looked like a button that did nothing. -->\n<p-toast [key]=\"resultToastKey\" position=\"bottom-right\"></p-toast>\n\n<!-- Shared outlets for notices raised where there is no per-instance key to use:\n the root-provided PlotlyService and the region-editor dialog. Rendered by a\n single visualizer (ownsSharedToasts) so a message from the root service is\n not shown once per live viewer. -->\n<p-toast *ngIf=\"ownsSharedToasts\" [key]=\"vizToastKey\"></p-toast>\n<p-toast *ngIf=\"ownsSharedToasts\" [key]=\"vizAlertToastKey\" position=\"center\"></p-toast>\n\n<!-- Toolbar dock: docked across the top by default; drag the handle to detach\n it into a floating, movable window (frees the top row for the image). -->\n<div class=\"toolbar-dock\" [class.floating]=\"toolbarFloating\"\n [style.left.px]=\"toolbarFloating ? toolbarPos.x : null\"\n [style.top.px]=\"toolbarFloating ? toolbarPos.y : null\">\n <div class=\"toolbar-drag-handle\" (mousedown)=\"onToolbarDragStart($event)\"\n pTooltip=\"Drag to float the toolbar\" tooltipPosition=\"bottom\">\n <i class=\"pi pi-bars\"></i>\n </div>\n<plotting-toolbar\n [imageInfo]=\"imageInfo\"\n [plotTypeOptions]=\"plotTypeOptions\"\n [selectedPlotType]=\"selectedPlotType\"\n [isoRange]=\"isoRange\"\n [maxIndex]=\"maxIndex\"\n [zIndex]=\"zIndex\"\n [isHeatmap]=\"isHeatmap\"\n [is3dRegions]=\"isSpatial3dMode\"\n [activeDragMode]=\"activeDragMode\"\n [activeSurface3dMode]=\"activeSurface3dMode\"\n [axesVisible]=\"axesVisible\"\n [wireframeActive]=\"wireframeActive\"\n [resolutionScale]=\"resolutionScale\"\n [wandSensitivity]=\"wandSensitivity\"\n [brushSize]=\"brushSize\"\n [samModels]=\"samModels\"\n [samModelId]=\"samModelId\"\n [contributedTools]=\"contributedTools\"\n [toolModelIds]=\"toolModelIds\"\n [vertexEraserRadius]=\"vertexEraserRadius\"\n [canUndo]=\"canUndoRegion\"\n [canRedo]=\"canRedoRegion\"\n [stackOptions]=\"stackOptions\"\n [tools]=\"toolbarTools\"\n [imageSmoothingEnabled]=\"imageSmoothingEnabled\"\n (selectPlotType)=\"onSelectPlotType($event)\"\n (toggleImageSmoothing)=\"onToggleImageSmoothing()\"\n (addProfileLine)=\"addProfileLine()\"\n (isoRangeChange)=\"onIsoRangeChange($event)\"\n (openChannelHistogram)=\"openChannelHistogram()\"\n (openSpatialControls)=\"openSpatialControls()\"\n (openRegionEditor)=\"openRegionEditor()\"\n (selectStackOption)=\"selectStackOption($event)\"\n (zScrub)=\"onZScrub($event)\"\n (zSlide)=\"onZSlide($event)\"\n (zIndexInput)=\"zIndex = $event; updateZIndex()\"\n (reloadAndPlot)=\"reloadAndPlot()\"\n (downloadImage)=\"downloadImage()\"\n (autoscaleImage)=\"autoscaleImage()\"\n (toggleDragMode)=\"toggleDragMode($event)\"\n (zoomIn)=\"zoomIn()\"\n (zoomOut)=\"zoomOut()\"\n (toggleSurface3dMode)=\"toggleSurface3dMode($event)\"\n (resetSurfaceCamera)=\"resetSurfaceCamera()\"\n (toggleAxes)=\"toggleAxes()\"\n (toggleWireframe)=\"toggleWireframe()\"\n (selectResolution)=\"selectResolution($event)\"\n (deleteRegion)=\"deleteRegion()\"\n (undoRegion)=\"undoRegion()\"\n (redoRegion)=\"redoRegion()\"\n (segmentRegions)=\"segmentRegions()\"\n (segmentCellpose)=\"segmentCellpose()\"\n (samModelChange)=\"onSamModelChange($event)\"\n (runTool)=\"runTool($event)\"\n (toolModelChange)=\"onToolModelChange($event)\"\n (openToolParams)=\"openToolParams($event)\"\n (wandSensitivityChange)=\"onWandSensitivityChange($event)\"\n (brushSizeChange)=\"onBrushSizeChange($event)\"\n (vertexEraserRadiusChange)=\"onVertexEraserRadiusChange($event)\">\n <!-- Host-projected custom toolbar buttons, forwarded into the toolbar. -->\n <ng-content></ng-content>\n</plotting-toolbar>\n <p-button *ngIf=\"toolbarFloating\" class=\"toolbar-dock-btn\" icon=\"pi pi-thumbtack\" size=\"small\" [text]=\"true\"\n pTooltip=\"Dock to top\" tooltipPosition=\"bottom\" (onClick)=\"dockToolbar()\">\n </p-button>\n</div>\n\n<p-contextMenu #cm [model]=\"contextMenuItems\" appendTo=\"body\"></p-contextMenu>\n<!-- No `h-full` here: PrimeFlex's height:100% !important made #plot as tall as\n the whole host, so under the docked toolbar its bottom toolbar-height strip\n (incl. the OSD scale bar at bottom-left) was clipped by the host's\n overflow:hidden. The component stylesheet sizes it (.viz-plot { flex: 1 }). -->\n<div class=\"w-full relative viz-plot\" [id]=\"plotDivName\">\n <div *ngIf=\"imgLoading || stackLoading || isCaching\" id=\"overlay\">\n <!-- Determinate stack/volume progress takes precedence: while it's active show ONLY the\n progress bar + \"Loading frames...\", not the generic spinner/\"Loading image...\" on top of\n it (both were rendering at once for napari volume/isosurface loads). -->\n <ng-container *ngIf=\"stackLoading; else imgLoadingTpl\">\n <p-progressBar class=\"progress\" [value]=\"loadingPercentage\"></p-progressBar>\n <span class=\"text-loading\">Loading frames...</span>\n </ng-container>\n <ng-template #imgLoadingTpl>\n <!-- Cache-copy progress is shown whenever a cache download is active (isCaching), independent\n of `imgLoading`, so it's never dropped/masked mid-download. -->\n <ng-container *ngIf=\"isCaching; else spinnerLoading\">\n <!-- Stage-aware message comes from CacheProgressService updating\n imageLoadingMessage$ (\"Caching image...\" during the GCS download,\n then \"Generating preview...\" during per-slice thumbnail gen). Once the\n download hits 100% the remaining work (server preview/tile generation)\n has no determinate progress, so fall through to the spinner template\n rather than parking a full bar under \"Loading image...\". -->\n <p-progressBar class=\"progress\" [value]=\"cacheProgress\"></p-progressBar>\n <span class=\"text-loading\">{{loadingMessage}}</span>\n </ng-container>\n <ng-template #spinnerLoading>\n <div *ngIf=\"imgLoading\" class=\"spinner\"></div>\n <span *ngIf=\"imgLoading && !zoom\" class=\"text-loading\">{{loadingMessage}}</span>\n <span *ngIf=\"imgLoading && zoom\" class=\"zoom-text-loading\">{{loadingMessage}}</span>\n </ng-template>\n </ng-template>\n <p-button *ngIf=\"stackLoading || imgLoading\" class=\"cancel-loading \" label=\"Cancel\"\n (onClick)=\"cancelLoading()\">\n </p-button>\n </div>\n <!-- Translucent \"sharpening\" overlay shown after the small-tier preview is\n on screen while the large-tier render is still loading in the\n background. Lets the user see the blurry image underneath but makes\n it clear something is still loading so they don't mistake the\n low-res render for the final image. -->\n <div *ngIf=\"sharpening && !imgLoading && !stackLoading && !isCaching\" id=\"sharpening-overlay\">\n <div class=\"spinner\"></div>\n <span class=\"text-loading\">Sharpening preview...</span>\n </div>\n\n</div>\n\n<!-- Floating, draggable intensity-profile inset. Shown in LINE mode; the chart\n updates live as the line ROI on the image is moved. Kept OUTSIDE #plot and\n positioned fixed so it floats over the whole page (not clipped to the\n diagram/canvas area). -->\n<div *ngIf=\"hasProfiles\" class=\"intensity-panel\"\n [style.left.px]=\"profilePanelPos.x\" [style.top.px]=\"profilePanelPos.y\">\n <div class=\"intensity-panel-header\" (mousedown)=\"onProfilePanelDragStart($event)\">\n <span>Intensity profile</span>\n <i class=\"pi pi-arrows-alt\"></i>\n </div>\n <div [id]=\"intensityInsetDiv\" class=\"intensity-panel-plot\"></div>\n</div>\n\n<!-- Channels & Histogram: non-modal floating dialog opened from the toolbar. -->\n<channel-histogram [(visible)]=\"showChannelHistogram\"></channel-histogram>\n\n<!-- Spatial-omics controls. Renders its own empty state when no dataset (or no\n SPATIAL_DATA_PORT) is present, so it is always safe to host. -->\n<spatial-controls [(visible)]=\"showSpatialControls\" [is3d]=\"isSpatial3dMode\" ></spatial-controls>\n\n<!-- Region Editor: non-modal, draggable, resizable floating dialog opened from\n the toolbar (formerly the right-panel Regions tab). Modeless so regions can\n still be drawn/selected on the plot while it's open; stays linked to this\n plotting instance via the root RegionStore singleton. Opens over the right\n side of the view. -->\n<p-dialog header=\"Region Editor\" [(visible)]=\"showRegionEditor\"\n [modal]=\"false\" [resizable]=\"true\" [draggable]=\"true\"\n [maximizable]=\"true\" appendTo=\"body\" position=\"right\"\n [style]=\"{ width: regionEditorWidth, height: '85vh' }\"\n [contentStyle]=\"{ overflow: 'auto' }\">\n <region-editor></region-editor>\n</p-dialog>\n\n<!-- Simplify (Douglas\u2013Peucker): custom px-threshold picker for the selected\n region(s), opened from the right-click \"Simplify \u25B8 Custom\u2026\" action. -->\n<p-dialog header=\"Simplify region(s)\" [(visible)]=\"displaySimplifyDialog\" [modal]=\"true\"\n [style]=\"{width: '22rem'}\" [draggable]=\"false\" [resizable]=\"false\">\n <div class=\"flex flex-column gap-2\">\n <span>Altitude threshold: <b>{{ simplifyThreshold }} px</b></span>\n <p-slider [(ngModel)]=\"simplifyThreshold\" [min]=\"1\" [max]=\"20\" [step]=\"1\"></p-slider>\n <small>Vertices within this distance of the line between their neighbours are removed.</small>\n </div>\n <ng-template pTemplate=\"footer\">\n <p-button label=\"Cancel\" [text]=\"true\" (onClick)=\"displaySimplifyDialog = false\"></p-button>\n <p-button label=\"Apply\" icon=\"pi pi-check\" (onClick)=\"simplifyRegions(simplifyThreshold)\"></p-button>\n </ng-template>\n</p-dialog>\n\n<!-- Parameters for whichever contributed tool is open. One dialog for all of\n them: a tool declares its fields (ToolParamSpec) and this renders them, so\n the toolbar can lay every tool out the same way and a plugin needs no\n PrimeNG of its own. Defaults come from the active checkpoint, since they\n encode what it was trained for. -->\n<p-dialog *ngIf=\"openParamsTool as tool\"\n [header]=\"tool.label + ' parameters'\"\n [visible]=\"true\" (visibleChange)=\"openParamsToolId = null\"\n [modal]=\"true\"\n [style]=\"{width: '30rem'}\" [draggable]=\"false\" [resizable]=\"false\" appendTo=\"body\">\n <div class=\"grid formgrid p-fluid\">\n <ng-container *ngFor=\"let p of tool.params\">\n <div class=\"field\" [ngClass]=\"p.type === 'boolean' ? 'col-12' : 'col-6'\">\n <ng-container [ngSwitch]=\"p.type\">\n\n <ng-container *ngSwitchCase=\"'number'\">\n <label class=\"text-sm\" [pTooltip]=\"p.tooltip || ''\">{{ p.label }}</label>\n <p-inputNumber [ngModel]=\"paramsFor(tool.id)[p.id]\"\n (ngModelChange)=\"paramsFor(tool.id)[p.id] = $event\"\n [min]=\"numberParam(p).min\" [max]=\"numberParam(p).max\"\n [step]=\"numberParam(p).step\"\n [minFractionDigits]=\"numberParam(p).fractionDigits\"\n [showButtons]=\"true\"></p-inputNumber>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <label class=\"text-sm\" [pTooltip]=\"p.tooltip || ''\">{{ p.label }}</label>\n <p-dropdown [options]=\"selectParam(p).options\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"paramsFor(tool.id)[p.id]\"\n (ngModelChange)=\"paramsFor(tool.id)[p.id] = $event\"\n appendTo=\"body\"></p-dropdown>\n </ng-container>\n\n <!-- Checkbox label sits beside the box and may run to a sentence, so\n it does not get the stacked label the other types use. -->\n <div *ngSwitchCase=\"'boolean'\" class=\"flex align-items-center gap-2\">\n <p-checkbox [ngModel]=\"paramsFor(tool.id)[p.id]\"\n (ngModelChange)=\"paramsFor(tool.id)[p.id] = $event\"\n [binary]=\"true\" [inputId]=\"tool.id + '-' + p.id\"></p-checkbox>\n <label [for]=\"tool.id + '-' + p.id\" class=\"text-sm mb-0\"\n [pTooltip]=\"p.tooltip || ''\">{{ p.label }}</label>\n </div>\n\n </ng-container>\n </div>\n </ng-container>\n </div>\n <ng-template pTemplate=\"footer\">\n <p-button label=\"Reset to model defaults\" [text]=\"true\" size=\"small\"\n (onClick)=\"resetToolParams(tool.id)\"></p-button>\n <p-button label=\"Close\" size=\"small\" (onClick)=\"openParamsToolId = null\"></p-button>\n </ng-template>\n</p-dialog>\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow:hidden}.viz-plot{flex:1;min-height:0;height:auto!important}.toolbar-dock{display:flex;align-items:stretch;width:100%}.toolbar-dock plotting-toolbar{flex:1;min-width:0}.toolbar-dock.floating{position:fixed;z-index:1000;width:auto;max-width:calc(100% - 16px);background:var(--surface-card, #ffffff);border:1px solid var(--surface-border, #cccccc);border-radius:6px;box-shadow:0 4px 16px #0000004d}.toolbar-dock.floating plotting-toolbar{flex:0 1 auto}.toolbar-dock.floating .toolbar-drag-handle{border-radius:6px 0 0 6px}.toolbar-drag-handle{display:flex;align-items:center;justify-content:center;padding:0 .4rem;cursor:move;color:var(--primary-color);background:var(--surface-100, rgba(0, 0, 0, .04));border-right:1px solid var(--surface-border, #cccccc);-webkit-user-select:none;user-select:none}.toolbar-dock-btn{align-self:center}:host ::ng-deep .navigator{margin-bottom:80px!important;margin-right:12px!important}kbd{background:#e8e8e8;border:1px solid #bbb;border-radius:3px;padding:1px 5px;font-family:monospace;font-size:.85em}::ng-deep .context-menu-active>.p-menuitem-content{background-color:var(--primary-color)!important}::ng-deep .context-menu-active>.p-menuitem-content .p-menuitem-icon,::ng-deep .context-menu-active>.p-menuitem-content .p-menuitem-text{color:#fff!important}::ng-deep .context-menu-active>.p-menuitem-content .zoom-box-off-icon,::ng-deep .context-menu-active>.p-menuitem-content .wand-icon,::ng-deep .context-menu-active>.p-menuitem-content .polyline-icon,::ng-deep .context-menu-active>.p-menuitem-content .polygon-vertices-icon,::ng-deep .context-menu-active>.p-menuitem-content .vertex-add-icon,::ng-deep .context-menu-active>.p-menuitem-content .vertex-delete-icon,::ng-deep .context-menu-active>.p-menuitem-content .region-move-icon,::ng-deep .context-menu-active>.p-menuitem-content .to-bezier-icon,::ng-deep .context-menu-active>.p-menuitem-content .to-polygon-icon{filter:brightness(0) invert(1)}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.progress{position:absolute;width:50%;left:25%;top:55%}.text-loading{position:absolute;margin:0 auto;left:45%;top:60%;font-weight:700}.zoom-text-loading{position:absolute;margin:0 auto;left:35%;top:60%;font-weight:700;display:inline-block;width:350px}.cancel-loading{position:absolute;margin:0 auto;left:46%;top:70%}.spinner{position:absolute;left:45%;top:45%;height:100px;width:100px;margin:0 auto;-webkit-animation:rotation .6s infinite linear;-moz-animation:rotation .6s infinite linear;-o-animation:rotation .6s infinite linear;animation:rotation .6s infinite linear;border-left:6px solid rgba(0,174,239,.15);border-right:6px solid rgba(0,174,239,.15);border-bottom:6px solid rgba(0,174,239,.15);border-top:6px solid rgb(82,107,178);border-radius:100%}#overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#ffffff80;z-index:1000}#sharpening-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#ffffff2e;pointer-events:none;z-index:1000}#sharpening-overlay .spinner{height:60px;width:60px;border-width:4px;left:calc(50% - 30px);top:calc(50% - 60px)}#sharpening-overlay .text-loading{position:absolute;left:0;right:0;width:auto;text-align:center;top:calc(50% + 35px);font-size:.9rem;color:#526bb2}@-webkit-keyframes rotation{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(359deg)}}@-moz-keyframes rotation{0%{-moz-transform:rotate(0deg)}to{-moz-transform:rotate(359deg)}}@-o-keyframes rotation{0%{-o-transform:rotate(0deg)}to{-o-transform:rotate(359deg)}}@keyframes rotation{0%{transform:rotate(0)}to{transform:rotate(359deg)}}::ng-deep .zoom-box-off-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\" fill=\"black\" fill-rule=\"evenodd\">%0A <!-- Rectangle outline -->%0A <rect x=\"2\" y=\"2\" width=\"56\" height=\"56\"/>%0A <rect x=\"9\" y=\"9\" width=\"42\" height=\"42\"/>%0A <!-- Magnifying glass ring -->%0A <circle cx=\"70\" cy=\"70\" r=\"23\"/>%0A <circle cx=\"70\" cy=\"70\" r=\"14\"/>%0A <!-- Handle -->%0A <polygon points=\"81,81 97,97 92,100 77,85\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .wand-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<svg%0A viewBox=\"0 0 100 100\"%0A fill=\"black\"%0A fill-rule=\"evenodd\"%0A version=\"1.1\"%0A id=\"svg4\"%0A sodipodi:docname=\"wand.svg\"%0A inkscape:version=\"1.4.3 (0d15f75, 2025-12-25)\"%0A xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\"%0A xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\"%0A xmlns=\"http://www.w3.org/2000/svg\"%0A xmlns:svg=\"http://www.w3.org/2000/svg\">%0A <defs%0A id=\"defs4\" />%0A <sodipodi:namedview%0A id=\"namedview4\"%0A pagecolor=\"%23ffffff\"%0A bordercolor=\"%23000000\"%0A borderopacity=\"0.25\"%0A inkscape:showpageshadow=\"2\"%0A inkscape:pageopacity=\"0.0\"%0A inkscape:pagecheckerboard=\"0\"%0A inkscape:deskcolor=\"%23d1d1d1\"%0A inkscape:zoom=\"8.74\"%0A inkscape:cx=\"50\"%0A inkscape:cy=\"49.942792\"%0A inkscape:window-width=\"1024\"%0A inkscape:window-height=\"1186\"%0A inkscape:window-x=\"3560\"%0A inkscape:window-y=\"131\"%0A inkscape:window-maximized=\"0\"%0A inkscape:current-layer=\"svg4\" />%0A <!-- Wand handle: thin diagonal stick from lower-left to upper-right -->%0A <polygon%0A points=\"12,92 78,26 74,22 8,88 \"%0A id=\"polygon1\"%0A transform=\"matrix(1.0375192,0.05799445,0.25179496,0.91871864,-17.85351,5.6289793)\" />%0A <!-- 4-pointed star at the wand tip -->%0A <polygon%0A points=\"62,34 76,30 80,6 84,30 98,34 84,38 80,62 76,38 \"%0A id=\"polygon2\"%0A transform=\"matrix(1.3082889,0,0,1.0245178,-38.907958,-1.4056881)\" />%0A <!-- Small sparkle to the upper-left -->%0A <polygon%0A points=\"46,30 36,32 34,42 32,32 22,30 32,28 34,18 36,28 \"%0A id=\"polygon3\"%0A transform=\"translate(-9.610984,-6.7505721)\" />%0A <!-- Small sparkle to the lower-right -->%0A <polygon%0A points=\"62,80 70,82 62,84 60,92 58,84 50,82 58,80 60,72 \"%0A id=\"polygon4\"%0A transform=\"translate(0,-1.6018307)\" />%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .brush-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 640\"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d=\"M512.5 74.3L291.1 222C262 241.4 243.5 272.9 240.5 307.3C302.8 320.1 351.9 369.2 364.8 431.6C399.3 428.6 430.7 410.1 450.1 381L597.7 159.5C604.4 149.4 608 137.6 608 125.4C608 91.5 580.5 64 546.6 64C534.5 64 522.6 67.6 512.5 74.3zM320 464C320 402.1 269.9 352 208 352C146.1 352 96 402.1 96 464C96 467.9 96.2 471.8 96.6 475.6C98.4 493.1 86.4 512 68.8 512L64 512C46.3 512 32 526.3 32 544C32 561.7 46.3 576 64 576L208 576C269.9 576 320 525.9 320 464z\"/></svg>') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .polyline-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\">%0A <polyline points=\"1,14 5,3 10,11 15,2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .polygon-vertices-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Polygon (click-to-place) tool icon: a traced polyline with vertex dots. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.4740709,0,0,1.4740709,-6.0268793,-6.8086826)\">%0A <polyline points=\"23.877,16.627 14.657,10.767 5.25,21.551 12.757,29.397\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\"%0A stroke-linejoin=\"round\" stroke-linecap=\"round\"/>%0A <circle cx=\"23.877\" cy=\"16.627\" r=\"1.9\"/>%0A <circle cx=\"14.657\" cy=\"10.767\" r=\"1.9\"/>%0A <circle cx=\"5.25\" cy=\"21.551\" r=\"1.9\"/>%0A <circle cx=\"12.757\" cy=\"29.397\" r=\"1.9\"/>%0A </g>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .vertex-add-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Add-vertex tool icon: a region blob with three corner vertices and a plus glyph. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.5222969,0,0,1.5222969,-8.2143505,-7.6146399)\">%0A <path d=\"m 9.35834,12.33267 c 1.78,-2.53 4.93,-3.56 7.86,-3.98 3.98,-0.72 8.33,0.08 11.52,2.67 2.62,1.98 4.71,5.1 4.53,8.49 -0.29,2.27 -2.5,3.81 -4.66,4.01 -2.75,0.43 -5.54,0.07 -8.28,-0.26 -3.99,-0.68 -8.05,-2.16 -10.93,-5.12 -1.32,-1.63 -1.39,-4.17 -0.04,-5.81 z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\" stroke-miterlimit=\"10\"/>%0A <circle cx=\"8.294\" cy=\"15.248\" r=\"2.63\"/>%0A <circle cx=\"19.781\" cy=\"8.287\" r=\"2.63\"/>%0A <circle cx=\"25.044\" cy=\"23.748\" r=\"2.63\"/>%0A </g>%0A <path d=\"M 9.65,29.0 H 5.85 v 5.3 H 0.55 v 3.4 H 5.85 v 5.3 H 9.65 V 37.7 H 14.95 V 34.3 H 9.65 Z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .vertex-delete-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Delete-vertex tool icon: a region blob with three corner vertices and a minus glyph. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.5222969,0,0,1.5222969,-8.2143505,-7.6146399)\">%0A <path d=\"m 9.35834,12.33267 c 1.78,-2.53 4.93,-3.56 7.86,-3.98 3.98,-0.72 8.33,0.08 11.52,2.67 2.62,1.98 4.71,5.1 4.53,8.49 -0.29,2.27 -2.5,3.81 -4.66,4.01 -2.75,0.43 -5.54,0.07 -8.28,-0.26 -3.99,-0.68 -8.05,-2.16 -10.93,-5.12 -1.32,-1.63 -1.39,-4.17 -0.04,-5.81 z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\" stroke-miterlimit=\"10\"/>%0A <circle cx=\"8.294\" cy=\"15.248\" r=\"2.63\"/>%0A <circle cx=\"19.781\" cy=\"8.287\" r=\"2.63\"/>%0A <circle cx=\"25.044\" cy=\"23.748\" r=\"2.63\"/>%0A </g>%0A <path d=\"M 0.55,34.3 v 3.4 H 14.95 V 34.3 Z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .region-move-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Region-move tool icon: a region blob with a drag arrow. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <path d=\"m 17.115582,8.9648664 c 1.707597,-3.3276806 4.729468,-4.6824281 7.540287,-5.2348498 3.827703,-0.947008 7.991169,0.1052238 11.051414,3.5118228 2.523022,2.604273 4.518416,6.7079726 4.345738,11.1668036 -0.278204,2.985704 -2.398311,5.011251 -4.470451,5.274308 -2.638142,0.565574 -5.314655,0.09206 -7.943203,-0.341976 C 23.811664,22.446579 19.916808,20.499952 17.153955,16.606696 15.89724,14.462775 15.820494,11.12194 17.115585,8.9648664 Z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.1\" stroke-miterlimit=\"10\"/>%0A <path d=\"m 26.741429,13.966963 6.746922,16.786269 3.229398,-4.060758 3.583857,3.583857 2.761195,-2.761196 -3.583856,-3.571852 4.060758,-3.2354 z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .to-bezier-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Convert-to-bezier tool icon: a smooth curve through two anchor points with control handles. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <!-- smooth cubic curve between the two anchors -->%0A <path d=\"M 7,34 C 7,16 37,28 37,10\" fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.4\"%0A stroke-linecap=\"round\"/>%0A <!-- control handles -->%0A <line x1=\"7\" y1=\"34\" x2=\"7\" y2=\"22\" stroke=\"%23000000\" stroke-width=\"1.4\"/>%0A <line x1=\"37\" y1=\"10\" x2=\"37\" y2=\"22\" stroke=\"%23000000\" stroke-width=\"1.4\"/>%0A <circle cx=\"7\" cy=\"22\" r=\"2.2\"/>%0A <circle cx=\"37\" cy=\"22\" r=\"2.2\"/>%0A <!-- anchor points -->%0A <rect x=\"4\" y=\"31\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"34\" y=\"7\" width=\"6\" height=\"6\" rx=\"1\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .to-polygon-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Convert-to-polygon tool icon: endpoints joined by straight segments through a vertex. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <!-- straight-segment path through the vertices -->%0A <polyline points=\"7,34 22,12 37,10\" fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.4\"%0A stroke-linecap=\"round\" stroke-linejoin=\"round\"/>%0A <!-- vertices -->%0A <rect x=\"4\" y=\"31\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"19\" y=\"9\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"34\" y=\"7\" width=\"6\" height=\"6\" rx=\"1\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.intensity-panel{position:fixed;z-index:1100;width:280px;height:180px;background:#191919e6;border:1px solid var(--primary-color, #3399ff);border-radius:6px;box-shadow:0 4px 14px #00000073;display:flex;flex-direction:column;overflow:hidden}.intensity-panel-header{display:flex;align-items:center;justify-content:space-between;padding:.25rem .5rem;cursor:move;-webkit-user-select:none;user-select:none;background:#ffffff14;color:#eee;font-size:.75rem;font-weight:600}.intensity-panel-header i{opacity:.6;font-size:.7rem}.intensity-panel-plot{flex:1 1 auto;width:100%}:host ::ng-deep .sam-progress .p-progressbar-value{transition:none}\n"] }]
|
|
13684
|
+
args: [{ selector: 'visualizer', template: "<!-- SAM toast: live status + encoder-download progress bar (jit-ui#90). Sticky\n while a segment runs; stays open until the bar reaches 100% and the run ends. -->\n<p-toast [key]=\"samToastKey\" position=\"bottom-right\">\n <ng-template let-message pTemplate=\"message\">\n <div style=\"flex: 1; min-width: 16rem\">\n <div class=\"font-bold mb-1\">{{ message.summary }}</div>\n <div>{{ samStatus || message.detail }}</div>\n <p-progressBar *ngIf=\"samDownloading\" [value]=\"samProgress\" styleClass=\"mt-2 sam-progress\"></p-progressBar>\n <p-progressBar *ngIf=\"samBusy && !samDownloading\" mode=\"indeterminate\" styleClass=\"mt-2\"\n [style]=\"{ height: '4px' }\"></p-progressBar>\n </div>\n </ng-template>\n</p-toast>\n\n<!-- Result/error notices from the library itself (segmentation outcome, region\n validation). Rendered here so they do not depend on the host mounting a\n keyless <p-toast> \u2014 without this the messages went nowhere in any host but\n jit-ui, and a failed run looked like a button that did nothing. -->\n<p-toast [key]=\"resultToastKey\" position=\"bottom-right\"></p-toast>\n\n<!-- Shared outlets for notices raised where there is no per-instance key to use:\n the root-provided PlotlyService and the region-editor dialog. Rendered by a\n single visualizer (ownsSharedToasts) so a message from the root service is\n not shown once per live viewer. -->\n<p-toast *ngIf=\"ownsSharedToasts\" [key]=\"vizToastKey\"></p-toast>\n<p-toast *ngIf=\"ownsSharedToasts\" [key]=\"vizAlertToastKey\" position=\"center\"></p-toast>\n\n<!-- Toolbar dock: docked across the top by default; drag the handle to detach\n it into a floating, movable window (frees the top row for the image). -->\n<div class=\"toolbar-dock\" [class.floating]=\"toolbarFloating\"\n [style.left.px]=\"toolbarFloating ? toolbarPos.x : null\"\n [style.top.px]=\"toolbarFloating ? toolbarPos.y : null\">\n <div class=\"toolbar-drag-handle\" (mousedown)=\"onToolbarDragStart($event)\"\n pTooltip=\"Drag to float the toolbar\" tooltipPosition=\"bottom\">\n <i class=\"pi pi-bars\"></i>\n </div>\n<plotting-toolbar\n [imageInfo]=\"imageInfo\"\n [plotTypeOptions]=\"plotTypeMenu\"\n [selectedPlotType]=\"selectedPlotTypeId\"\n [basePlotType]=\"basePlotType\"\n [isoRange]=\"isoRange\"\n [maxIndex]=\"maxIndex\"\n [zIndex]=\"zIndex\"\n [isHeatmap]=\"isHeatmap\"\n [is3dRegions]=\"isSpatial3dMode\"\n [activeDragMode]=\"activeDragMode\"\n [activeSurface3dMode]=\"activeSurface3dMode\"\n [axesVisible]=\"axesVisible\"\n [wireframeActive]=\"wireframeActive\"\n [resolutionScale]=\"resolutionScale\"\n [wandSensitivity]=\"wandSensitivity\"\n [brushSize]=\"brushSize\"\n [samModels]=\"samModels\"\n [samModelId]=\"samModelId\"\n [contributedTools]=\"contributedTools\"\n [toolModelIds]=\"toolModelIds\"\n [vertexEraserRadius]=\"vertexEraserRadius\"\n [canUndo]=\"canUndoRegion\"\n [canRedo]=\"canRedoRegion\"\n [stackOptions]=\"stackOptions\"\n [tools]=\"toolbarTools\"\n [imageSmoothingEnabled]=\"imageSmoothingEnabled\"\n (selectPlotType)=\"onSelectPlotType($event)\"\n (toggleImageSmoothing)=\"onToggleImageSmoothing()\"\n (addProfileLine)=\"addProfileLine()\"\n (isoRangeChange)=\"onIsoRangeChange($event)\"\n (openChannelHistogram)=\"openChannelHistogram()\"\n (openSpatialControls)=\"openSpatialControls()\"\n (openRegionEditor)=\"openRegionEditor()\"\n (selectStackOption)=\"selectStackOption($event)\"\n (zScrub)=\"onZScrub($event)\"\n (zSlide)=\"onZSlide($event)\"\n (zIndexInput)=\"zIndex = $event; updateZIndex()\"\n (reloadAndPlot)=\"reloadAndPlot()\"\n (downloadImage)=\"downloadImage()\"\n (autoscaleImage)=\"autoscaleImage()\"\n (toggleDragMode)=\"toggleDragMode($event)\"\n (zoomIn)=\"zoomIn()\"\n (zoomOut)=\"zoomOut()\"\n (toggleSurface3dMode)=\"toggleSurface3dMode($event)\"\n (resetSurfaceCamera)=\"resetSurfaceCamera()\"\n (toggleAxes)=\"toggleAxes()\"\n (toggleWireframe)=\"toggleWireframe()\"\n (selectResolution)=\"selectResolution($event)\"\n (deleteRegion)=\"deleteRegion()\"\n (undoRegion)=\"undoRegion()\"\n (redoRegion)=\"redoRegion()\"\n (segmentRegions)=\"segmentRegions()\"\n (segmentCellpose)=\"segmentCellpose()\"\n (samModelChange)=\"onSamModelChange($event)\"\n (runTool)=\"runTool($event)\"\n (toolModelChange)=\"onToolModelChange($event)\"\n (openToolParams)=\"openToolParams($event)\"\n (wandSensitivityChange)=\"onWandSensitivityChange($event)\"\n (brushSizeChange)=\"onBrushSizeChange($event)\"\n (vertexEraserRadiusChange)=\"onVertexEraserRadiusChange($event)\">\n <!-- Host-projected custom toolbar buttons, forwarded into the toolbar. -->\n <ng-content></ng-content>\n</plotting-toolbar>\n <p-button *ngIf=\"toolbarFloating\" class=\"toolbar-dock-btn\" icon=\"pi pi-thumbtack\" size=\"small\" [text]=\"true\"\n pTooltip=\"Dock to top\" tooltipPosition=\"bottom\" (onClick)=\"dockToolbar()\">\n </p-button>\n</div>\n\n<p-contextMenu #cm [model]=\"contextMenuItems\" appendTo=\"body\"></p-contextMenu>\n<!-- No `h-full` here: PrimeFlex's height:100% !important made #plot as tall as\n the whole host, so under the docked toolbar its bottom toolbar-height strip\n (incl. the OSD scale bar at bottom-left) was clipped by the host's\n overflow:hidden. The component stylesheet sizes it (.viz-plot { flex: 1 }). -->\n<div class=\"w-full relative viz-plot\" [id]=\"plotDivName\">\n <div *ngIf=\"imgLoading || stackLoading || isCaching\" id=\"overlay\">\n <!-- Determinate stack/volume progress takes precedence: while it's active show ONLY the\n progress bar + \"Loading frames...\", not the generic spinner/\"Loading image...\" on top of\n it (both were rendering at once for napari volume/isosurface loads). -->\n <ng-container *ngIf=\"stackLoading; else imgLoadingTpl\">\n <p-progressBar class=\"progress\" [value]=\"loadingPercentage\"></p-progressBar>\n <span class=\"text-loading\">Loading frames...</span>\n </ng-container>\n <ng-template #imgLoadingTpl>\n <!-- Cache-copy progress is shown whenever a cache download is active (isCaching), independent\n of `imgLoading`, so it's never dropped/masked mid-download. -->\n <ng-container *ngIf=\"isCaching; else spinnerLoading\">\n <!-- Stage-aware message comes from CacheProgressService updating\n imageLoadingMessage$ (\"Caching image...\" during the GCS download,\n then \"Generating preview...\" during per-slice thumbnail gen). Once the\n download hits 100% the remaining work (server preview/tile generation)\n has no determinate progress, so fall through to the spinner template\n rather than parking a full bar under \"Loading image...\". -->\n <p-progressBar class=\"progress\" [value]=\"cacheProgress\"></p-progressBar>\n <span class=\"text-loading\">{{loadingMessage}}</span>\n </ng-container>\n <ng-template #spinnerLoading>\n <div *ngIf=\"imgLoading\" class=\"spinner\"></div>\n <span *ngIf=\"imgLoading && !zoom\" class=\"text-loading\">{{loadingMessage}}</span>\n <span *ngIf=\"imgLoading && zoom\" class=\"zoom-text-loading\">{{loadingMessage}}</span>\n </ng-template>\n </ng-template>\n <p-button *ngIf=\"stackLoading || imgLoading\" class=\"cancel-loading \" label=\"Cancel\"\n (onClick)=\"cancelLoading()\">\n </p-button>\n </div>\n <!-- Translucent \"sharpening\" overlay shown after the small-tier preview is\n on screen while the large-tier render is still loading in the\n background. Lets the user see the blurry image underneath but makes\n it clear something is still loading so they don't mistake the\n low-res render for the final image. -->\n <div *ngIf=\"sharpening && !imgLoading && !stackLoading && !isCaching\" id=\"sharpening-overlay\">\n <div class=\"spinner\"></div>\n <span class=\"text-loading\">Sharpening preview...</span>\n </div>\n\n</div>\n\n<!-- Floating, draggable intensity-profile inset. Shown in LINE mode; the chart\n updates live as the line ROI on the image is moved. Kept OUTSIDE #plot and\n positioned fixed so it floats over the whole page (not clipped to the\n diagram/canvas area). -->\n<div *ngIf=\"hasProfiles\" class=\"intensity-panel\"\n [style.left.px]=\"profilePanelPos.x\" [style.top.px]=\"profilePanelPos.y\">\n <div class=\"intensity-panel-header\" (mousedown)=\"onProfilePanelDragStart($event)\">\n <span>Intensity profile</span>\n <i class=\"pi pi-arrows-alt\"></i>\n </div>\n <div [id]=\"intensityInsetDiv\" class=\"intensity-panel-plot\"></div>\n</div>\n\n<!-- Channels & Histogram: non-modal floating dialog opened from the toolbar. -->\n<channel-histogram [(visible)]=\"showChannelHistogram\"></channel-histogram>\n\n<!-- Spatial-omics controls. Renders its own empty state when no dataset (or no\n SPATIAL_DATA_PORT) is present, so it is always safe to host. -->\n<spatial-controls [(visible)]=\"showSpatialControls\" [is3d]=\"isSpatial3dMode\" ></spatial-controls>\n\n<!-- Side panel of the active contributed plot mode (PLOT_TYPE_CONTRIBUTIONS),\n in the same right-hand area as the spatial controls and the Region Editor.\n Exists only while the mode's session is live; there is no close button,\n because leaving the mode is what ends it. A component panel gets\n PLOT_MODE_CONTEXT / PLOT_MODE_SESSION from its injector; a `mount` panel's\n host element is attached to the slot below (see plotModePanelSlot). -->\n<p-dialog *ngIf=\"plotModePanel as panel\" [header]=\"panel.title\" [visible]=\"true\"\n [modal]=\"false\" [closable]=\"false\" [resizable]=\"true\" [draggable]=\"true\"\n appendTo=\"body\" position=\"right\" styleClass=\"plot-mode-panel\"\n [style]=\"{ width: '22rem', maxHeight: '85vh' }\"\n [contentStyle]=\"{ overflow: 'auto' }\">\n <ng-container *ngIf=\"panel.component\">\n <ng-container *ngComponentOutlet=\"panel.component; injector: panel.injector ?? undefined\"></ng-container>\n </ng-container>\n <div *ngIf=\"panel.host\" #plotModePanelSlot class=\"plot-mode-panel-slot\"></div>\n</p-dialog>\n\n<!-- Region Editor: non-modal, draggable, resizable floating dialog opened from\n the toolbar (formerly the right-panel Regions tab). Modeless so regions can\n still be drawn/selected on the plot while it's open; stays linked to this\n plotting instance via the root RegionStore singleton. Opens over the right\n side of the view. -->\n<p-dialog header=\"Region Editor\" [(visible)]=\"showRegionEditor\"\n [modal]=\"false\" [resizable]=\"true\" [draggable]=\"true\"\n [maximizable]=\"true\" appendTo=\"body\" position=\"right\"\n [style]=\"{ width: regionEditorWidth, height: '85vh' }\"\n [contentStyle]=\"{ overflow: 'auto' }\">\n <region-editor></region-editor>\n</p-dialog>\n\n<!-- Simplify (Douglas\u2013Peucker): custom px-threshold picker for the selected\n region(s), opened from the right-click \"Simplify \u25B8 Custom\u2026\" action. -->\n<p-dialog header=\"Simplify region(s)\" [(visible)]=\"displaySimplifyDialog\" [modal]=\"true\"\n [style]=\"{width: '22rem'}\" [draggable]=\"false\" [resizable]=\"false\">\n <div class=\"flex flex-column gap-2\">\n <span>Altitude threshold: <b>{{ simplifyThreshold }} px</b></span>\n <p-slider [(ngModel)]=\"simplifyThreshold\" [min]=\"1\" [max]=\"20\" [step]=\"1\"></p-slider>\n <small>Vertices within this distance of the line between their neighbours are removed.</small>\n </div>\n <ng-template pTemplate=\"footer\">\n <p-button label=\"Cancel\" [text]=\"true\" (onClick)=\"displaySimplifyDialog = false\"></p-button>\n <p-button label=\"Apply\" icon=\"pi pi-check\" (onClick)=\"simplifyRegions(simplifyThreshold)\"></p-button>\n </ng-template>\n</p-dialog>\n\n<!-- Parameters for whichever contributed tool is open. One dialog for all of\n them: a tool declares its fields (ToolParamSpec) and this renders them, so\n the toolbar can lay every tool out the same way and a plugin needs no\n PrimeNG of its own. Defaults come from the active checkpoint, since they\n encode what it was trained for. -->\n<p-dialog *ngIf=\"openParamsTool as tool\"\n [header]=\"tool.label + ' parameters'\"\n [visible]=\"true\" (visibleChange)=\"openParamsToolId = null\"\n [modal]=\"true\"\n [style]=\"{width: '30rem'}\" [draggable]=\"false\" [resizable]=\"false\" appendTo=\"body\">\n <div class=\"grid formgrid p-fluid\">\n <ng-container *ngFor=\"let p of tool.params\">\n <div class=\"field\" [ngClass]=\"p.type === 'boolean' ? 'col-12' : 'col-6'\">\n <ng-container [ngSwitch]=\"p.type\">\n\n <ng-container *ngSwitchCase=\"'number'\">\n <label class=\"text-sm\" [pTooltip]=\"p.tooltip || ''\">{{ p.label }}</label>\n <p-inputNumber [ngModel]=\"paramsFor(tool.id)[p.id]\"\n (ngModelChange)=\"paramsFor(tool.id)[p.id] = $event\"\n [min]=\"numberParam(p).min\" [max]=\"numberParam(p).max\"\n [step]=\"numberParam(p).step\"\n [minFractionDigits]=\"numberParam(p).fractionDigits\"\n [showButtons]=\"true\"></p-inputNumber>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'select'\">\n <label class=\"text-sm\" [pTooltip]=\"p.tooltip || ''\">{{ p.label }}</label>\n <p-dropdown [options]=\"selectParam(p).options\" optionLabel=\"label\" optionValue=\"value\"\n [ngModel]=\"paramsFor(tool.id)[p.id]\"\n (ngModelChange)=\"paramsFor(tool.id)[p.id] = $event\"\n appendTo=\"body\"></p-dropdown>\n </ng-container>\n\n <!-- Checkbox label sits beside the box and may run to a sentence, so\n it does not get the stacked label the other types use. -->\n <div *ngSwitchCase=\"'boolean'\" class=\"flex align-items-center gap-2\">\n <p-checkbox [ngModel]=\"paramsFor(tool.id)[p.id]\"\n (ngModelChange)=\"paramsFor(tool.id)[p.id] = $event\"\n [binary]=\"true\" [inputId]=\"tool.id + '-' + p.id\"></p-checkbox>\n <label [for]=\"tool.id + '-' + p.id\" class=\"text-sm mb-0\"\n [pTooltip]=\"p.tooltip || ''\">{{ p.label }}</label>\n </div>\n\n </ng-container>\n </div>\n </ng-container>\n </div>\n <ng-template pTemplate=\"footer\">\n <p-button label=\"Reset to model defaults\" [text]=\"true\" size=\"small\"\n (onClick)=\"resetToolParams(tool.id)\"></p-button>\n <p-button label=\"Close\" size=\"small\" (onClick)=\"openParamsToolId = null\"></p-button>\n </ng-template>\n</p-dialog>\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow:hidden}.viz-plot{flex:1;min-height:0;height:auto!important}.toolbar-dock{display:flex;align-items:stretch;width:100%}.toolbar-dock plotting-toolbar{flex:1;min-width:0}.toolbar-dock.floating{position:fixed;z-index:1000;width:auto;max-width:calc(100% - 16px);background:var(--surface-card, #ffffff);border:1px solid var(--surface-border, #cccccc);border-radius:6px;box-shadow:0 4px 16px #0000004d}.toolbar-dock.floating plotting-toolbar{flex:0 1 auto}.toolbar-dock.floating .toolbar-drag-handle{border-radius:6px 0 0 6px}.toolbar-drag-handle{display:flex;align-items:center;justify-content:center;padding:0 .4rem;cursor:move;color:var(--primary-color);background:var(--surface-100, rgba(0, 0, 0, .04));border-right:1px solid var(--surface-border, #cccccc);-webkit-user-select:none;user-select:none}.toolbar-dock-btn{align-self:center}:host ::ng-deep .navigator{margin-bottom:80px!important;margin-right:12px!important}kbd{background:#e8e8e8;border:1px solid #bbb;border-radius:3px;padding:1px 5px;font-family:monospace;font-size:.85em}::ng-deep .context-menu-active>.p-menuitem-content{background-color:var(--primary-color)!important}::ng-deep .context-menu-active>.p-menuitem-content .p-menuitem-icon,::ng-deep .context-menu-active>.p-menuitem-content .p-menuitem-text{color:#fff!important}::ng-deep .context-menu-active>.p-menuitem-content .zoom-box-off-icon,::ng-deep .context-menu-active>.p-menuitem-content .wand-icon,::ng-deep .context-menu-active>.p-menuitem-content .polyline-icon,::ng-deep .context-menu-active>.p-menuitem-content .polygon-vertices-icon,::ng-deep .context-menu-active>.p-menuitem-content .vertex-add-icon,::ng-deep .context-menu-active>.p-menuitem-content .vertex-delete-icon,::ng-deep .context-menu-active>.p-menuitem-content .region-move-icon,::ng-deep .context-menu-active>.p-menuitem-content .to-bezier-icon,::ng-deep .context-menu-active>.p-menuitem-content .to-polygon-icon{filter:brightness(0) invert(1)}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.progress{position:absolute;width:50%;left:25%;top:55%}.text-loading{position:absolute;margin:0 auto;left:45%;top:60%;font-weight:700}.zoom-text-loading{position:absolute;margin:0 auto;left:35%;top:60%;font-weight:700;display:inline-block;width:350px}.cancel-loading{position:absolute;margin:0 auto;left:46%;top:70%}.spinner{position:absolute;left:45%;top:45%;height:100px;width:100px;margin:0 auto;-webkit-animation:rotation .6s infinite linear;-moz-animation:rotation .6s infinite linear;-o-animation:rotation .6s infinite linear;animation:rotation .6s infinite linear;border-left:6px solid rgba(0,174,239,.15);border-right:6px solid rgba(0,174,239,.15);border-bottom:6px solid rgba(0,174,239,.15);border-top:6px solid rgb(82,107,178);border-radius:100%}#overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#ffffff80;z-index:1000}#sharpening-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#ffffff2e;pointer-events:none;z-index:1000}#sharpening-overlay .spinner{height:60px;width:60px;border-width:4px;left:calc(50% - 30px);top:calc(50% - 60px)}#sharpening-overlay .text-loading{position:absolute;left:0;right:0;width:auto;text-align:center;top:calc(50% + 35px);font-size:.9rem;color:#526bb2}@-webkit-keyframes rotation{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(359deg)}}@-moz-keyframes rotation{0%{-moz-transform:rotate(0deg)}to{-moz-transform:rotate(359deg)}}@-o-keyframes rotation{0%{-o-transform:rotate(0deg)}to{-o-transform:rotate(359deg)}}@keyframes rotation{0%{transform:rotate(0)}to{transform:rotate(359deg)}}::ng-deep .zoom-box-off-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\" fill=\"black\" fill-rule=\"evenodd\">%0A <!-- Rectangle outline -->%0A <rect x=\"2\" y=\"2\" width=\"56\" height=\"56\"/>%0A <rect x=\"9\" y=\"9\" width=\"42\" height=\"42\"/>%0A <!-- Magnifying glass ring -->%0A <circle cx=\"70\" cy=\"70\" r=\"23\"/>%0A <circle cx=\"70\" cy=\"70\" r=\"14\"/>%0A <!-- Handle -->%0A <polygon points=\"81,81 97,97 92,100 77,85\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .wand-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<svg%0A viewBox=\"0 0 100 100\"%0A fill=\"black\"%0A fill-rule=\"evenodd\"%0A version=\"1.1\"%0A id=\"svg4\"%0A sodipodi:docname=\"wand.svg\"%0A inkscape:version=\"1.4.3 (0d15f75, 2025-12-25)\"%0A xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\"%0A xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\"%0A xmlns=\"http://www.w3.org/2000/svg\"%0A xmlns:svg=\"http://www.w3.org/2000/svg\">%0A <defs%0A id=\"defs4\" />%0A <sodipodi:namedview%0A id=\"namedview4\"%0A pagecolor=\"%23ffffff\"%0A bordercolor=\"%23000000\"%0A borderopacity=\"0.25\"%0A inkscape:showpageshadow=\"2\"%0A inkscape:pageopacity=\"0.0\"%0A inkscape:pagecheckerboard=\"0\"%0A inkscape:deskcolor=\"%23d1d1d1\"%0A inkscape:zoom=\"8.74\"%0A inkscape:cx=\"50\"%0A inkscape:cy=\"49.942792\"%0A inkscape:window-width=\"1024\"%0A inkscape:window-height=\"1186\"%0A inkscape:window-x=\"3560\"%0A inkscape:window-y=\"131\"%0A inkscape:window-maximized=\"0\"%0A inkscape:current-layer=\"svg4\" />%0A <!-- Wand handle: thin diagonal stick from lower-left to upper-right -->%0A <polygon%0A points=\"12,92 78,26 74,22 8,88 \"%0A id=\"polygon1\"%0A transform=\"matrix(1.0375192,0.05799445,0.25179496,0.91871864,-17.85351,5.6289793)\" />%0A <!-- 4-pointed star at the wand tip -->%0A <polygon%0A points=\"62,34 76,30 80,6 84,30 98,34 84,38 80,62 76,38 \"%0A id=\"polygon2\"%0A transform=\"matrix(1.3082889,0,0,1.0245178,-38.907958,-1.4056881)\" />%0A <!-- Small sparkle to the upper-left -->%0A <polygon%0A points=\"46,30 36,32 34,42 32,32 22,30 32,28 34,18 36,28 \"%0A id=\"polygon3\"%0A transform=\"translate(-9.610984,-6.7505721)\" />%0A <!-- Small sparkle to the lower-right -->%0A <polygon%0A points=\"62,80 70,82 62,84 60,92 58,84 50,82 58,80 60,72 \"%0A id=\"polygon4\"%0A transform=\"translate(0,-1.6018307)\" />%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .brush-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 640\"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d=\"M512.5 74.3L291.1 222C262 241.4 243.5 272.9 240.5 307.3C302.8 320.1 351.9 369.2 364.8 431.6C399.3 428.6 430.7 410.1 450.1 381L597.7 159.5C604.4 149.4 608 137.6 608 125.4C608 91.5 580.5 64 546.6 64C534.5 64 522.6 67.6 512.5 74.3zM320 464C320 402.1 269.9 352 208 352C146.1 352 96 402.1 96 464C96 467.9 96.2 471.8 96.6 475.6C98.4 493.1 86.4 512 68.8 512L64 512C46.3 512 32 526.3 32 544C32 561.7 46.3 576 64 576L208 576C269.9 576 320 525.9 320 464z\"/></svg>') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .polyline-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\">%0A <polyline points=\"1,14 5,3 10,11 15,2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .polygon-vertices-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Polygon (click-to-place) tool icon: a traced polyline with vertex dots. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.4740709,0,0,1.4740709,-6.0268793,-6.8086826)\">%0A <polyline points=\"23.877,16.627 14.657,10.767 5.25,21.551 12.757,29.397\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\"%0A stroke-linejoin=\"round\" stroke-linecap=\"round\"/>%0A <circle cx=\"23.877\" cy=\"16.627\" r=\"1.9\"/>%0A <circle cx=\"14.657\" cy=\"10.767\" r=\"1.9\"/>%0A <circle cx=\"5.25\" cy=\"21.551\" r=\"1.9\"/>%0A <circle cx=\"12.757\" cy=\"29.397\" r=\"1.9\"/>%0A </g>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .vertex-add-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Add-vertex tool icon: a region blob with three corner vertices and a plus glyph. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.5222969,0,0,1.5222969,-8.2143505,-7.6146399)\">%0A <path d=\"m 9.35834,12.33267 c 1.78,-2.53 4.93,-3.56 7.86,-3.98 3.98,-0.72 8.33,0.08 11.52,2.67 2.62,1.98 4.71,5.1 4.53,8.49 -0.29,2.27 -2.5,3.81 -4.66,4.01 -2.75,0.43 -5.54,0.07 -8.28,-0.26 -3.99,-0.68 -8.05,-2.16 -10.93,-5.12 -1.32,-1.63 -1.39,-4.17 -0.04,-5.81 z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\" stroke-miterlimit=\"10\"/>%0A <circle cx=\"8.294\" cy=\"15.248\" r=\"2.63\"/>%0A <circle cx=\"19.781\" cy=\"8.287\" r=\"2.63\"/>%0A <circle cx=\"25.044\" cy=\"23.748\" r=\"2.63\"/>%0A </g>%0A <path d=\"M 9.65,29.0 H 5.85 v 5.3 H 0.55 v 3.4 H 5.85 v 5.3 H 9.65 V 37.7 H 14.95 V 34.3 H 9.65 Z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .vertex-delete-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Delete-vertex tool icon: a region blob with three corner vertices and a minus glyph. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.5222969,0,0,1.5222969,-8.2143505,-7.6146399)\">%0A <path d=\"m 9.35834,12.33267 c 1.78,-2.53 4.93,-3.56 7.86,-3.98 3.98,-0.72 8.33,0.08 11.52,2.67 2.62,1.98 4.71,5.1 4.53,8.49 -0.29,2.27 -2.5,3.81 -4.66,4.01 -2.75,0.43 -5.54,0.07 -8.28,-0.26 -3.99,-0.68 -8.05,-2.16 -10.93,-5.12 -1.32,-1.63 -1.39,-4.17 -0.04,-5.81 z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\" stroke-miterlimit=\"10\"/>%0A <circle cx=\"8.294\" cy=\"15.248\" r=\"2.63\"/>%0A <circle cx=\"19.781\" cy=\"8.287\" r=\"2.63\"/>%0A <circle cx=\"25.044\" cy=\"23.748\" r=\"2.63\"/>%0A </g>%0A <path d=\"M 0.55,34.3 v 3.4 H 14.95 V 34.3 Z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .region-move-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Region-move tool icon: a region blob with a drag arrow. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <path d=\"m 17.115582,8.9648664 c 1.707597,-3.3276806 4.729468,-4.6824281 7.540287,-5.2348498 3.827703,-0.947008 7.991169,0.1052238 11.051414,3.5118228 2.523022,2.604273 4.518416,6.7079726 4.345738,11.1668036 -0.278204,2.985704 -2.398311,5.011251 -4.470451,5.274308 -2.638142,0.565574 -5.314655,0.09206 -7.943203,-0.341976 C 23.811664,22.446579 19.916808,20.499952 17.153955,16.606696 15.89724,14.462775 15.820494,11.12194 17.115585,8.9648664 Z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.1\" stroke-miterlimit=\"10\"/>%0A <path d=\"m 26.741429,13.966963 6.746922,16.786269 3.229398,-4.060758 3.583857,3.583857 2.761195,-2.761196 -3.583856,-3.571852 4.060758,-3.2354 z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .to-bezier-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Convert-to-bezier tool icon: a smooth curve through two anchor points with control handles. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <!-- smooth cubic curve between the two anchors -->%0A <path d=\"M 7,34 C 7,16 37,28 37,10\" fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.4\"%0A stroke-linecap=\"round\"/>%0A <!-- control handles -->%0A <line x1=\"7\" y1=\"34\" x2=\"7\" y2=\"22\" stroke=\"%23000000\" stroke-width=\"1.4\"/>%0A <line x1=\"37\" y1=\"10\" x2=\"37\" y2=\"22\" stroke=\"%23000000\" stroke-width=\"1.4\"/>%0A <circle cx=\"7\" cy=\"22\" r=\"2.2\"/>%0A <circle cx=\"37\" cy=\"22\" r=\"2.2\"/>%0A <!-- anchor points -->%0A <rect x=\"4\" y=\"31\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"34\" y=\"7\" width=\"6\" height=\"6\" rx=\"1\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .to-polygon-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Convert-to-polygon tool icon: endpoints joined by straight segments through a vertex. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <!-- straight-segment path through the vertices -->%0A <polyline points=\"7,34 22,12 37,10\" fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.4\"%0A stroke-linecap=\"round\" stroke-linejoin=\"round\"/>%0A <!-- vertices -->%0A <rect x=\"4\" y=\"31\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"19\" y=\"9\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"34\" y=\"7\" width=\"6\" height=\"6\" rx=\"1\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.intensity-panel{position:fixed;z-index:1100;width:280px;height:180px;background:#191919e6;border:1px solid var(--primary-color, #3399ff);border-radius:6px;box-shadow:0 4px 14px #00000073;display:flex;flex-direction:column;overflow:hidden}.intensity-panel-header{display:flex;align-items:center;justify-content:space-between;padding:.25rem .5rem;cursor:move;-webkit-user-select:none;user-select:none;background:#ffffff14;color:#eee;font-size:.75rem;font-weight:600}.intensity-panel-header i{opacity:.6;font-size:.7rem}.intensity-panel-plot{flex:1 1 auto;width:100%}:host ::ng-deep .sam-progress .p-progressbar-value{transition:none}\n"] }]
|
|
13060
13685
|
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
13061
13686
|
type: Inject,
|
|
13062
13687
|
args: [IMAGE_STATE_PORT]
|
|
@@ -13078,6 +13703,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
|
|
|
13078
13703
|
}, {
|
|
13079
13704
|
type: Inject,
|
|
13080
13705
|
args: [SPATIAL_DATA_PORT]
|
|
13706
|
+
}] }, { type: undefined, decorators: [{
|
|
13707
|
+
type: Optional
|
|
13708
|
+
}, {
|
|
13709
|
+
type: Inject,
|
|
13710
|
+
args: [PLOT_TYPE_CONTRIBUTIONS]
|
|
13711
|
+
}] }, { type: i0.Injector, decorators: [{
|
|
13712
|
+
type: Optional
|
|
13081
13713
|
}] }], propDecorators: { isStackEvent: [{
|
|
13082
13714
|
type: Output
|
|
13083
13715
|
}], isGrayscaleEvent: [{
|
|
@@ -13089,6 +13721,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
|
|
|
13089
13721
|
}], contextMenu: [{
|
|
13090
13722
|
type: ViewChild,
|
|
13091
13723
|
args: ['cm']
|
|
13724
|
+
}], plotModePanelSlot: [{
|
|
13725
|
+
type: ViewChild,
|
|
13726
|
+
args: ['plotModePanelSlot']
|
|
13092
13727
|
}], onKeydown: [{
|
|
13093
13728
|
type: HostListener,
|
|
13094
13729
|
args: ['window:keydown', ['$event']]
|
|
@@ -19334,6 +19969,13 @@ class OsdCoordinateTransform {
|
|
|
19334
19969
|
const rect = this.viewer.canvas.getBoundingClientRect();
|
|
19335
19970
|
return elementToImage(this.viewer, clientX - rect.left, clientY - rect.top);
|
|
19336
19971
|
}
|
|
19972
|
+
/** Data (image-pixel) coordinates -> client (mouse event) pixel. The inverse
|
|
19973
|
+
* of {@link clientToData}. */
|
|
19974
|
+
dataToClient(x, y) {
|
|
19975
|
+
const rect = this.viewer.canvas.getBoundingClientRect();
|
|
19976
|
+
const p = imageToElement(this.viewer, x, y);
|
|
19977
|
+
return { x: p.x + rect.left, y: p.y + rect.top };
|
|
19978
|
+
}
|
|
19337
19979
|
dataLengthToScreen(dataLength) {
|
|
19338
19980
|
const a = imageToElement(this.viewer, 0, 0);
|
|
19339
19981
|
const b = imageToElement(this.viewer, dataLength, 0);
|
|
@@ -20615,6 +21257,11 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
20615
21257
|
/** Visible image region (full-image pixel coords) emitted when the view
|
|
20616
21258
|
* settles, so the intensity inset can re-sample at the current zoom level. */
|
|
20617
21259
|
this.viewportChange$ = new Subject();
|
|
21260
|
+
/** The visible rect on every redraw, coalesced to one emission per animation
|
|
21261
|
+
* frame. Feeds {@link PlotModeViewport.frame$}. */
|
|
21262
|
+
this.frame$ = new Subject();
|
|
21263
|
+
this.frameRaf = null;
|
|
21264
|
+
this.plotModeViewport = null;
|
|
20618
21265
|
// Region state (regions, selection, the update event) lives in the shared
|
|
20619
21266
|
// RegionStore; the IRegionStore methods below delegate to it.
|
|
20620
21267
|
this.intensityProfile$ = new Subject();
|
|
@@ -21306,6 +21953,9 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
21306
21953
|
// Tell listeners (the intensity inset) the visible image region whenever
|
|
21307
21954
|
// the view settles, so they can re-sample at the current zoom resolution.
|
|
21308
21955
|
this.viewer.addHandler('animation-finish', () => this.emitViewportChange());
|
|
21956
|
+
// Every redraw (pan/zoom animation frames, resize) — for a contributed
|
|
21957
|
+
// plot mode's overlay, which has to move with the image, not after it.
|
|
21958
|
+
this.viewer.addHandler('update-viewport', () => this.scheduleFrame());
|
|
21309
21959
|
// Chrome compositor bug: after an OSD zoom the docked toolbar (a sibling of
|
|
21310
21960
|
// #plot in the <visualizer> host) is left laid-out-but-unpainted and
|
|
21311
21961
|
// vanishes — confirmed via DevTools (DOM intact, region simply not painted).
|
|
@@ -21483,6 +22133,10 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
21483
22133
|
}
|
|
21484
22134
|
destroyViewer() {
|
|
21485
22135
|
this.cache.cancelBackgroundLoad();
|
|
22136
|
+
if (this.frameRaf !== null) {
|
|
22137
|
+
cancelAnimationFrame(this.frameRaf);
|
|
22138
|
+
this.frameRaf = null;
|
|
22139
|
+
}
|
|
21486
22140
|
// Tear down any active tool overlays (shared singletons).
|
|
21487
22141
|
this.wandTool.setMode(false);
|
|
21488
22142
|
this.brushTool.setMode(false);
|
|
@@ -21785,9 +22439,16 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
21785
22439
|
/** Compute the current viewport's image-pixel rectangle (clamped to the image)
|
|
21786
22440
|
* and broadcast it. */
|
|
21787
22441
|
emitViewportChange() {
|
|
22442
|
+
const rect = this.visibleImageRect();
|
|
22443
|
+
if (rect)
|
|
22444
|
+
this.viewportChange$.next(rect);
|
|
22445
|
+
}
|
|
22446
|
+
/** The current viewport's image-pixel rectangle, clamped to the image, or
|
|
22447
|
+
* null when there is no laid-out viewport. */
|
|
22448
|
+
visibleImageRect() {
|
|
21788
22449
|
const vp = this.viewer?.viewport;
|
|
21789
22450
|
if (!vp || !this.descriptor)
|
|
21790
|
-
return;
|
|
22451
|
+
return null;
|
|
21791
22452
|
try {
|
|
21792
22453
|
// Route through world item 0 (osd-coords): vp.viewportToImageRectangle is
|
|
21793
22454
|
// inaccurate and warns when the world holds multiple images (per-channel
|
|
@@ -21798,12 +22459,56 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
|
|
|
21798
22459
|
const y = Math.max(0, Math.min(ih, r.y));
|
|
21799
22460
|
const width = Math.max(1, Math.min(iw - x, r.width));
|
|
21800
22461
|
const height = Math.max(1, Math.min(ih - y, r.height));
|
|
21801
|
-
|
|
22462
|
+
return { x, y, width, height };
|
|
21802
22463
|
}
|
|
21803
22464
|
catch {
|
|
21804
|
-
/* viewport not ready */
|
|
22465
|
+
return null; /* viewport not ready */
|
|
21805
22466
|
}
|
|
21806
22467
|
}
|
|
22468
|
+
/** Coalesce redraws into one {@link frame$} emission per animation frame.
|
|
22469
|
+
* Skipped entirely while nothing listens. */
|
|
22470
|
+
scheduleFrame() {
|
|
22471
|
+
if (this.frameRaf !== null || !this.frame$.observed)
|
|
22472
|
+
return;
|
|
22473
|
+
this.frameRaf = requestAnimationFrame(() => {
|
|
22474
|
+
this.frameRaf = null;
|
|
22475
|
+
const rect = this.visibleImageRect();
|
|
22476
|
+
if (rect)
|
|
22477
|
+
this.frame$.next(rect);
|
|
22478
|
+
});
|
|
22479
|
+
}
|
|
22480
|
+
/**
|
|
22481
|
+
* The viewport a contributed plot mode draws over. One stable object per
|
|
22482
|
+
* service: every method reads the CURRENT viewer, so it stays valid across a
|
|
22483
|
+
* viewer rebuild and reports `isReady() === false` while none is mounted
|
|
22484
|
+
* (conversions then return NaN rather than throwing).
|
|
22485
|
+
*/
|
|
22486
|
+
getPlotModeViewport() {
|
|
22487
|
+
if (this.plotModeViewport)
|
|
22488
|
+
return this.plotModeViewport;
|
|
22489
|
+
const nan = { x: NaN, y: NaN };
|
|
22490
|
+
const ready = () => !!this.coordTransform?.isReady();
|
|
22491
|
+
this.plotModeViewport = {
|
|
22492
|
+
getOverlayContainer: () => this.getOverlayContainer(),
|
|
22493
|
+
dataToClient: (x, y) => (ready() ? this.coordTransform.dataToClient(x, y) : nan),
|
|
22494
|
+
clientToData: (cx, cy) => (ready() ? this.coordTransform.clientToData(cx, cy) : nan),
|
|
22495
|
+
dataLengthToScreen: (len) => (ready() ? this.coordTransform.dataLengthToScreen(len) : NaN),
|
|
22496
|
+
isReady: ready,
|
|
22497
|
+
// Both start with the current visible rect when the viewport is ready: a mode
|
|
22498
|
+
// activates after the base render has gone idle, so without it an overlay
|
|
22499
|
+
// following `frame$.subscribe(redraw)` would stay blank until the next pan/zoom.
|
|
22500
|
+
frame$: this.withCurrentRect(this.frame$, ready),
|
|
22501
|
+
settled$: this.withCurrentRect(this.viewportChange$, ready),
|
|
22502
|
+
};
|
|
22503
|
+
return this.plotModeViewport;
|
|
22504
|
+
}
|
|
22505
|
+
/** `source`, preceded by the current visible rect for each subscriber (when ready). */
|
|
22506
|
+
withCurrentRect(source, ready) {
|
|
22507
|
+
return defer(() => {
|
|
22508
|
+
const rect = ready() ? this.visibleImageRect() : null;
|
|
22509
|
+
return rect ? source.pipe(startWith(rect)) : source.asObservable();
|
|
22510
|
+
});
|
|
22511
|
+
}
|
|
21807
22512
|
/** OSD targets the image display; the scalar/3D plot types belong to Plotly. */
|
|
21808
22513
|
getPlotTypeDescriptors() {
|
|
21809
22514
|
return [PLOT_TYPE_DESCRIPTORS[PlotType.HEATMAP]];
|
|
@@ -28941,6 +29646,11 @@ class RoutingVisualizerService {
|
|
|
28941
29646
|
resetSurfaceCamera() { this.renderer().resetSurfaceCamera(); }
|
|
28942
29647
|
setResolutionScale(scale) { this.renderer().setResolutionScale?.(scale); }
|
|
28943
29648
|
getResolutionScale() { return this.renderer().getResolutionScale?.() ?? 1; }
|
|
29649
|
+
/** The viewport of the backend on screen, for a contributed plot mode. Null
|
|
29650
|
+
* when that backend cannot provide one (e.g. OSD fell back to Plotly). */
|
|
29651
|
+
getPlotModeViewport() {
|
|
29652
|
+
return this.renderer().getPlotModeViewport?.() ?? null;
|
|
29653
|
+
}
|
|
28944
29654
|
getPlotTypeDescriptors() {
|
|
28945
29655
|
// Plotly enumerates the full PLOT_TYPE_DESCRIPTORS map, which already includes the
|
|
28946
29656
|
// napari-js WebGPU types (jit-ui#102) — so this single source covers them (no duplicates).
|
|
@@ -29613,5 +30323,5 @@ function provideVisualization() {
|
|
|
29613
30323
|
* Generated bundle index. Do not edit.
|
|
29614
30324
|
*/
|
|
29615
30325
|
|
|
29616
|
-
export { ALL_TOOLBAR_TOOLS, CELL_SEGMENTER, CHANNEL_HISTOGRAM_API, CellposeSegmenterService, ChannelHistogramComponent, DEFAULT_CATEGORICAL_PALETTE, DEFAULT_FALLBACK_PALETTE, DEFAULT_MUTED_OPACITY, DEFAULT_SAM_MODEL_ID, DEFAULT_SPATIAL_VIEW, HexColorPickerComponent, IMAGE_STATE_PORT, INSTANCE_SEGMENTER, ImageConverterService, LUT_COLORS, MISSING_COLOR, MultiPolygon, NAPARI_DECIMATE_OPTIONS, NAPARI_DEFAULT_DECIMATE, NO_CATEGORY, PLOT_TYPE_DESCRIPTORS, PREFERENCES_PORT, PlotType, Polygon, ProcessingImage, REGION_EDITOR_API, REGION_IO_PORT, Rectangle, Region, RegionEditorComponent, RoutingVisualizerService, SAM_MODELS, SEMANTIC_SEGMENTER, SPATIAL_DATA_PORT, SPATIAL_WIRE_VERSION, ShapeSelection, SpatialChartsComponent, SpatialControlsComponent, SpatialDataHttpService, SpatialSelectionStore, SupersededError, TILE_ACCESS_PORT, TOOLBAR_TOOLS, VISUALIZER, VIZ_CONFIG, ViewerFeature, VisualizationModule, VisualizerComponent, assertManifestVersion, benefitsFromGrouping, buildOmicsTraces, capabilitiesOf, contrastWindow, countMask, cropImage, datasetFromManifest, decodeColumn, decodeCoords, decodeFeatureVector, decodePolygons, decodeRadius, defaultPresetSet, emptySelection, encodeCategorical, encodeContinuous, findColumnMeta, getDefaultSamModelId, getOrtWasmBase, getPlotTypeDescriptor, getSamModel, hydrateBounds, isCategoricalColumn, isContinuousColumn, isEmptyPresetSet, isLittleEndian, isNapari3d, isNapariIsosurface, isNapariScatter, isNapariScatter3d, isNapariSurface, isNapariVolume, isSamModelReady, isSpatialOmics, isSpatialOmics3d, isThreeDimensional, lutFor, markerDiameters, maskToIndices, mutedFromSelection, omicsLayout, pointInRing, provideVisualization, rendererOwnsWheel, resolveCategoryColors, selectByCategory, selectInRegions, setDefaultSamModel, setOrtWasmBase, setSamModelUrls, sortToolContributions, toRgbaTuples, visibleToolContributions };
|
|
30326
|
+
export { ALL_TOOLBAR_TOOLS, CELL_SEGMENTER, CHANNEL_HISTOGRAM_API, CellposeSegmenterService, ChannelHistogramComponent, DEFAULT_CATEGORICAL_PALETTE, DEFAULT_FALLBACK_PALETTE, DEFAULT_MUTED_OPACITY, DEFAULT_SAM_MODEL_ID, DEFAULT_SPATIAL_VIEW, HexColorPickerComponent, IMAGE_STATE_PORT, INSTANCE_SEGMENTER, ImageConverterService, LUT_COLORS, MISSING_COLOR, MultiPolygon, NAPARI_DECIMATE_OPTIONS, NAPARI_DEFAULT_DECIMATE, NO_CATEGORY, PLOT_MODE_CONTEXT, PLOT_MODE_SESSION, PLOT_TYPE_CONTRIBUTIONS, PLOT_TYPE_DESCRIPTORS, PREFERENCES_PORT, PlotType, Polygon, ProcessingImage, REGION_EDITOR_API, REGION_IO_PORT, Rectangle, Region, RegionEditorComponent, RoutingVisualizerService, SAM_MODELS, SEMANTIC_SEGMENTER, SPATIAL_DATA_PORT, SPATIAL_WIRE_VERSION, ShapeSelection, SpatialChartsComponent, SpatialControlsComponent, SpatialDataHttpService, SpatialSelectionStore, SupersededError, TILE_ACCESS_PORT, TOOLBAR_TOOLS, VISUALIZER, VIZ_CONFIG, ViewerFeature, VisualizationModule, VisualizerComponent, assertManifestVersion, benefitsFromGrouping, buildOmicsTraces, capabilitiesOf, contrastWindow, contributedPlotTypeOption, countMask, cropImage, datasetFromManifest, decodeColumn, decodeCoords, decodeFeatureVector, decodePolygons, decodeRadius, defaultPresetSet, emptySelection, encodeCategorical, encodeContinuous, findColumnMeta, getDefaultSamModelId, getOrtWasmBase, getPlotTypeDescriptor, getSamModel, hydrateBounds, isBuiltinPlotType, isCategoricalColumn, isContinuousColumn, isEmptyPresetSet, isLittleEndian, isNapari3d, isNapariIsosurface, isNapariScatter, isNapariScatter3d, isNapariSurface, isNapariVolume, isSamModelReady, isSpatialOmics, isSpatialOmics3d, isThreeDimensional, lutFor, markerDiameters, maskToIndices, mutedFromSelection, omicsLayout, pointInRing, provideVisualization, rendererOwnsWheel, resolveCategoryColors, selectByCategory, selectInRegions, setDefaultSamModel, setOrtWasmBase, setSamModelUrls, sortToolContributions, toRgbaTuples, visibleToolContributions };
|
|
29617
30327
|
//# sourceMappingURL=jax-data-science-sci-image-visualizer.mjs.map
|