@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/index.d.ts CHANGED
@@ -19,6 +19,7 @@ export * from './lib/contracts/image.contract';
19
19
  export * from './lib/contracts/plot-type';
20
20
  export * from './lib/contracts/toolbar-config';
21
21
  export * from './lib/contracts/toolbar-tool.contract';
22
+ export * from './lib/contracts/plot-type-contribution.contract';
22
23
  export * from './lib/contracts/capabilities.contract';
23
24
  export * from './lib/contracts/region-overlay.contract';
24
25
  export * from './lib/contracts/display-types';
@@ -0,0 +1,172 @@
1
+ /**
2
+ * How a plot mode gets into the plot-type selector without this library knowing
3
+ * what it is.
4
+ *
5
+ * A package contributes a mode by providing a {@link PlotTypeContribution} on the
6
+ * {@link PLOT_TYPE_CONTRIBUTIONS} multi-provider token, the same way
7
+ * `TOOLBAR_TOOLS` contributes toolbar tools. Nothing registers at import time:
8
+ * installing a package makes a mode available, and adding its provider turns it
9
+ * on. A plain object works, so the contributing package needs no Angular
10
+ * compiler and no decorators:
11
+ *
12
+ * { provide: PLOT_TYPE_CONTRIBUTIONS, useValue: myContribution, multi: true }
13
+ * { provide: PLOT_TYPE_CONTRIBUTIONS, useFactory: () => makeContribution(), multi: true }
14
+ *
15
+ * Provide nothing and the selector, the render path and the panel area behave
16
+ * exactly as they did before this extension point existed.
17
+ *
18
+ * WHY A CONTRIBUTED MODE RIDES ON A BUILT-IN ONE
19
+ * A contributed mode does not bring a renderer. It names a built-in
20
+ * {@link ContributedPlotTypeDescriptor.baseType} — in v1 only `PlotType.IMAGE`,
21
+ * the OpenSeadragon view — and the visualizer plots exactly as if that type had
22
+ * been selected: same backend, same toolbar, same region tools, same wheel
23
+ * handling. The contribution then draws over it through the
24
+ * {@link PlotModeViewport} and talks to the viewer through the public
25
+ * {@link IVisualizer}. That keeps every rendering decision in one place and means
26
+ * a contribution can never be half-supported by some code path that switches on
27
+ * the plot type.
28
+ *
29
+ * WHAT THE VISUALIZER GUARANTEES
30
+ * 1. Contributed descriptors are appended after the built-in ones and curated by
31
+ * the same `productionLabel` / test-mode / `requires*` rules.
32
+ * 2. Selecting one plots with `baseType`'s backend, then calls `activate(ctx)`
33
+ * once the viewport is ready.
34
+ * 3. Leaving the mode — another type, an image switch (or any re-render of the
35
+ * base view), or the visualizer being destroyed — calls `session.deactivate()`
36
+ * exactly once, before the next mode draws. If the mode is still selected
37
+ * when the new image has plotted, a fresh session is activated for it.
38
+ * 4. `panel` is shown only while a session is live.
39
+ * 5. A thrown error or rejected promise from `activate`, `deactivate`, `mount`
40
+ * or a mount's teardown is caught and logged; none escapes, so a contribution
41
+ * cannot break the viewer. Failing to start (`activate`, `mount`, or a panel
42
+ * component throwing while it is created / first rendered) falls back
43
+ * to `baseType`. Failing to clean up falls back when that same mode is being
44
+ * re-activated (base re-render, image switch) — immediately if it already is
45
+ * live again — and is only logged when the user has moved to another mode; the
46
+ * user explicitly selecting it again gets a fresh attempt.
47
+ * 6. {@link PLOT_TYPE_CONTRIBUTIONS} has no factory: no providers, no modes.
48
+ */
49
+ import { InjectionToken, Type } from '@angular/core';
50
+ import type { Observable } from 'rxjs';
51
+ import type { IImageInfo } from './image.contract';
52
+ import type { PlotType, PlotTypeDescriptor, PlotTypeId } from './plot-type';
53
+ import type { IVisualizer } from './visualizer.contract';
54
+ /** A rectangle in level-0 image pixels. */
55
+ export interface PlotModeRect {
56
+ x: number;
57
+ y: number;
58
+ width: number;
59
+ height: number;
60
+ }
61
+ /** Selector metadata for a contributed plot mode. */
62
+ export interface ContributedPlotTypeDescriptor {
63
+ /** Unique id, namespaced, e.g. 'dianne'. Must not clash with PlotType values. */
64
+ type: string;
65
+ /** Test-mode label, e.g. 'Digital Pathology - DIANNE'. */
66
+ label: string;
67
+ /** Default-selector label; omit to make the mode test-only (same rule as built-ins). */
68
+ productionLabel?: string;
69
+ /** 'pi pi-…' class or asset path, same as PlotTypeDescriptor.icon. */
70
+ icon?: string;
71
+ /** Always '2d' in v1. */
72
+ dimensions: '2d';
73
+ /** Built-in type whose backend renders the base image. v1 supports PlotType.IMAGE (OSD). */
74
+ baseType: PlotType;
75
+ /** Same gates as PlotTypeDescriptor, evaluated in addition to the base type's own. */
76
+ requiresGrayscale?: boolean;
77
+ requiresStack?: boolean;
78
+ /** Hidden until a `SpatialDataset` is published on SPATIAL_DATA_PORT (an overlay of spatial observations). */
79
+ requiresSpatialData?: boolean;
80
+ /** Hidden unless the published dataset carries `observations.z`. */
81
+ requiresSpatial3d?: boolean;
82
+ }
83
+ /** The base view's viewport, as a contributed mode sees it. */
84
+ export interface PlotModeViewport {
85
+ /** Element the mode's overlay canvas/SVG attaches to; fills the plot area. */
86
+ getOverlayContainer(): HTMLElement | null;
87
+ /** Level-0 image px -> client px, and the reverse. */
88
+ dataToClient(x: number, y: number): {
89
+ x: number;
90
+ y: number;
91
+ };
92
+ clientToData(clientX: number, clientY: number): {
93
+ x: number;
94
+ y: number;
95
+ };
96
+ /** Screen px per image px at the current zoom. */
97
+ dataLengthToScreen(len: number): number;
98
+ isReady(): boolean;
99
+ /** Visible image rect in level-0 px, emitted on every redraw frame (pan/zoom animation).
100
+ * A new subscriber first receives the current rect when the viewport is ready. */
101
+ frame$: Observable<PlotModeRect>;
102
+ /** Same rect, emitted once the viewport settles (existing getViewportChange$ semantics).
103
+ * A new subscriber first receives the current rect when the viewport is ready. */
104
+ settled$: Observable<PlotModeRect>;
105
+ }
106
+ /** Everything a contributed mode gets to work with while it is active. */
107
+ export interface PlotModeContext {
108
+ /** Full public visualizer API: regions, region overlay (tool modes), undo, etc. */
109
+ visualizer: IVisualizer;
110
+ viewport: PlotModeViewport;
111
+ /** Current image info from IMAGE_STATE_PORT (null until an image is loaded). */
112
+ imageInfo$: Observable<IImageInfo | null>;
113
+ }
114
+ /** A live activation of a contributed mode. */
115
+ export interface PlotModeSession {
116
+ /** Called when the user leaves the mode, the image changes, or the visualizer is destroyed. */
117
+ deactivate(): void;
118
+ }
119
+ /**
120
+ * The mode's side panel, shown in the visualizer's right-hand panel area while
121
+ * a session is live. Two forms:
122
+ *
123
+ * - `component`: an Angular component, rendered through `NgComponentOutlet`
124
+ * with {@link PLOT_MODE_CONTEXT} and {@link PLOT_MODE_SESSION} in its
125
+ * injector. Needs an AOT-compiled (partial-Ivy) component.
126
+ * - `mount`: framework-agnostic. The visualizer hands over an empty host
127
+ * element after `activate` resolves; the function renders into it and returns
128
+ * a teardown. The teardown is called exactly once when the mode deactivates,
129
+ * before `session.deactivate()`, and the host element is then removed.
130
+ */
131
+ export type PlotModePanel = {
132
+ title: string;
133
+ component: Type<unknown>;
134
+ } | {
135
+ title: string;
136
+ mount(host: HTMLElement, ctx: PlotModeContext, session: PlotModeSession): () => void;
137
+ };
138
+ /** A plot mode contributed to the selector. A plain object is enough. */
139
+ export interface PlotTypeContribution {
140
+ descriptor: ContributedPlotTypeDescriptor;
141
+ /** Called after the base backend has plotted the image in this mode. */
142
+ activate(ctx: PlotModeContext): PlotModeSession | Promise<PlotModeSession>;
143
+ /**
144
+ * Optional side panel, rendered in the visualizer's right-hand panel area while the
145
+ * mode is active. A component panel can inject PLOT_MODE_CONTEXT and PLOT_MODE_SESSION.
146
+ */
147
+ panel?: PlotModePanel;
148
+ }
149
+ /**
150
+ * Multi-provider token for contributed plot modes.
151
+ *
152
+ * Deliberately has no `factory`: unregistered means no contributed modes, and the
153
+ * library behaves exactly as before. The visualizer injects it `{ optional: true }`
154
+ * and treats null as empty.
155
+ */
156
+ export declare const PLOT_TYPE_CONTRIBUTIONS: InjectionToken<readonly PlotTypeContribution[]>;
157
+ /** The active mode's context, in a component panel's injector. */
158
+ export declare const PLOT_MODE_CONTEXT: InjectionToken<PlotModeContext>;
159
+ /** The active mode's session, in a component panel's injector. */
160
+ export declare const PLOT_MODE_SESSION: InjectionToken<PlotModeSession>;
161
+ /**
162
+ * One entry in the plot-type selector: a built-in {@link PlotTypeDescriptor}
163
+ * as-is, or a contributed descriptor completed with its base type's `source`.
164
+ * Every entry therefore carries the same fields the selector curates by.
165
+ */
166
+ export interface PlotTypeOption extends Omit<PlotTypeDescriptor, 'type'> {
167
+ type: PlotTypeId;
168
+ /** Set only for a contributed mode: the built-in type that renders it. */
169
+ baseType?: PlotType;
170
+ }
171
+ /** The selector entry for a contributed mode, given its base type's descriptor. */
172
+ export declare function contributedPlotTypeOption(d: ContributedPlotTypeDescriptor, base: PlotTypeDescriptor): PlotTypeOption;
@@ -42,6 +42,16 @@ export declare enum PlotType {
42
42
  * {@link SPATIAL_OMICS}: a single-plane dataset has no z to render. */
43
43
  SPATIAL_OMICS_3D = "spatial-omics-3d"
44
44
  }
45
+ /**
46
+ * A value the plot-type selector can hold: a built-in {@link PlotType}, or the
47
+ * `type` of a contributed mode (`ContributedPlotTypeDescriptor.type`, see
48
+ * `plot-type-contribution.contract.ts`). Everything that decides HOW to render
49
+ * keeps taking a {@link PlotType}; a contributed id is resolved to its
50
+ * `baseType` first, so no rendering switch ever sees one.
51
+ */
52
+ export type PlotTypeId = PlotType | string;
53
+ /** Whether an id names a built-in plot type (as opposed to a contributed one). */
54
+ export declare function isBuiltinPlotType(id: PlotTypeId | null | undefined): id is PlotType;
45
55
  /** How many spatial dimensions a plot type renders in. */
46
56
  export type PlotDimensions = '2d' | '3d';
47
57
  /**
@@ -10,6 +10,7 @@ import { IHistogram } from './channel-histogram-api.contract';
10
10
  import { ColormapNode, IWandOptions, IBrushOptions, SpatialViewState, SpatialColorBy } from './display-types';
11
11
  import { SpatialDataset, SpatialEmbedding } from './spatial-dataset.contract';
12
12
  import { SpatialSelectionMask } from '../spatial/spatial-selection';
13
+ import type { PlotModeViewport } from './plot-type-contribution.contract';
13
14
  /**
14
15
  * Backend-neutral visualization contract. Plotly is one implementation;
15
16
  * OpenSeadragon is another (for the image plot type). The abstraction renders
@@ -419,6 +420,10 @@ export interface IVisualizer extends IDataRenderer, IRegionStore, IToolControlle
419
420
  * Optional: only the routing service implements it, since the state is shared
420
421
  * rather than owned by any one backend. */
421
422
  getSpatialControls?(): ISpatialControls | null;
423
+ /** The viewport a contributed plot mode draws over, when the backend on
424
+ * screen can provide one (OpenSeadragon), else null. Optional: backends that
425
+ * no contributed mode rides on need not implement it. */
426
+ getPlotModeViewport?(): PlotModeViewport | null;
422
427
  /** Binned intensity histogram for a channel from the currently-displayed
423
428
  * pixels, or null when none are available. Feeds the Channels & Histogram
424
429
  * pane. */
@@ -10,6 +10,7 @@ import { PlotType, PlotTypeDescriptor } from '../../contracts/plot-type';
10
10
  import { IVisualizer, PixelData, IntensityProfile, IIsosurfaceControls, IIntensityControls, ISurface3dControls } from '../../contracts/visualizer.contract';
11
11
  import { ViewerCapabilities } from '../../contracts/capabilities.contract';
12
12
  import { IRegionOverlay } from '../../contracts/region-overlay.contract';
13
+ import { PlotModeViewport } from '../../contracts/plot-type-contribution.contract';
13
14
  import { BaseStoreVisualizer } from '../base-store-visualizer';
14
15
  import { SimpleSliceAccessService } from '../simple-slice-access.service';
15
16
  import { WandToolService } from '../../toolbar/wand/wand-tool.service';
@@ -221,6 +222,11 @@ export declare class OpenSeadragonVisualizerService extends BaseStoreVisualizer
221
222
  /** Visible image region (full-image pixel coords) emitted when the view
222
223
  * settles, so the intensity inset can re-sample at the current zoom level. */
223
224
  private readonly viewportChange$;
225
+ /** The visible rect on every redraw, coalesced to one emission per animation
226
+ * frame. Feeds {@link PlotModeViewport.frame$}. */
227
+ private readonly frame$;
228
+ private frameRaf;
229
+ private plotModeViewport;
224
230
  private readonly intensityProfile$;
225
231
  /**
226
232
  * Subscribe to the shared VisualizerStore colormap/reverse so OSD recolors in
@@ -410,6 +416,21 @@ export declare class OpenSeadragonVisualizerService extends BaseStoreVisualizer
410
416
  /** Compute the current viewport's image-pixel rectangle (clamped to the image)
411
417
  * and broadcast it. */
412
418
  private emitViewportChange;
419
+ /** The current viewport's image-pixel rectangle, clamped to the image, or
420
+ * null when there is no laid-out viewport. */
421
+ private visibleImageRect;
422
+ /** Coalesce redraws into one {@link frame$} emission per animation frame.
423
+ * Skipped entirely while nothing listens. */
424
+ private scheduleFrame;
425
+ /**
426
+ * The viewport a contributed plot mode draws over. One stable object per
427
+ * service: every method reads the CURRENT viewer, so it stays valid across a
428
+ * viewer rebuild and reports `isReady() === false` while none is mounted
429
+ * (conversions then return NaN rather than throwing).
430
+ */
431
+ getPlotModeViewport(): PlotModeViewport;
432
+ /** `source`, preceded by the current visible rect for each subscriber (when ready). */
433
+ private withCurrentRect;
413
434
  /** OSD targets the image display; the scalar/3D plot types belong to Plotly. */
414
435
  getPlotTypeDescriptors(): PlotTypeDescriptor[];
415
436
  getIntensityProfile$(): Observable<IntensityProfile[]>;
@@ -13,5 +13,11 @@ export declare class OsdCoordinateTransform implements ICoordinateTransform {
13
13
  x: number;
14
14
  y: number;
15
15
  };
16
+ /** Data (image-pixel) coordinates -> client (mouse event) pixel. The inverse
17
+ * of {@link clientToData}. */
18
+ dataToClient(x: number, y: number): {
19
+ x: number;
20
+ y: number;
21
+ };
16
22
  dataLengthToScreen(dataLength: number): number;
17
23
  }
@@ -0,0 +1,130 @@
1
+ import { PlotType, PlotTypeId } from '../contracts/plot-type';
2
+ import { ContributedPlotTypeDescriptor, PlotModeContext, PlotModeSession, PlotTypeContribution } from '../contracts/plot-type-contribution.contract';
3
+ /**
4
+ * Lifecycle of the contributed plot modes ({@link PlotTypeContribution}) for one
5
+ * visualizer instance. Package-internal: the visualizer component owns one and
6
+ * drives it from its render pipeline.
7
+ *
8
+ * Owns three things the component should not have to reason about:
9
+ * - which contributions are usable (validated and de-duplicated once);
10
+ * - the single live session, so `deactivate()` runs exactly once per session
11
+ * however many exits race each other (type change, image change, destroy,
12
+ * a superseded async activation);
13
+ * - isolation: nothing a contribution throws or rejects escapes this class.
14
+ * A failure is logged and reported through `onFailed`, and the component
15
+ * falls back to the mode's base type.
16
+ */
17
+ /** The live activation of a contributed mode, as the component renders it. */
18
+ export interface ActivePlotMode {
19
+ readonly contribution: PlotTypeContribution;
20
+ readonly ctx: PlotModeContext;
21
+ readonly session: PlotModeSession;
22
+ /** Host element handed to a `mount` panel, or null for a component / no panel. */
23
+ readonly panelHost: HTMLElement | null;
24
+ }
25
+ export interface PlotModeControllerHooks {
26
+ /** A session is live (and a `mount` panel, if any, has been mounted). */
27
+ onActivated(active: ActivePlotMode): void;
28
+ /** The live session is about to end. Called BEFORE any teardown, so a
29
+ * component panel can be destroyed while its session is still valid. */
30
+ onDeactivating(active: ActivePlotMode): void;
31
+ /** Activation failed (threw, rejected, never got a ready viewport, or its
32
+ * panel failed to mount). The component falls back to the base type. */
33
+ onFailed(contribution: PlotTypeContribution, error: unknown): void;
34
+ }
35
+ /** Minimal logger, injectable for tests. */
36
+ export interface PlotModeLogger {
37
+ warn(...args: unknown[]): void;
38
+ error(...args: unknown[]): void;
39
+ }
40
+ /** Base types a contributed mode can ride on in this version. */
41
+ export declare const SUPPORTED_CONTRIBUTION_BASE_TYPES: readonly PlotType[];
42
+ /**
43
+ * Validate what arrived on `PLOT_TYPE_CONTRIBUTIONS`. A malformed or clashing
44
+ * contribution is dropped with a warning rather than breaking the selector:
45
+ * - a nested array (`useValue: [a, b], multi: true`) is flattened one level;
46
+ * - `type` must be a non-empty string that is not a built-in `PlotType` value;
47
+ * - the first contribution with a given `type` wins;
48
+ * - `baseType` must be one of {@link SUPPORTED_CONTRIBUTION_BASE_TYPES};
49
+ * - `activate` must be a function.
50
+ */
51
+ export declare function normalizeContributions(raw: readonly unknown[] | null | undefined, log?: PlotModeLogger): PlotTypeContribution[];
52
+ export declare class PlotModeController {
53
+ private readonly hooks;
54
+ private readonly log;
55
+ readonly contributions: readonly PlotTypeContribution[];
56
+ private readonly byId;
57
+ private active;
58
+ private teardown;
59
+ /** Bumped by every activate/deactivate, so a pending async activation can
60
+ * tell it has been superseded. */
61
+ private generation;
62
+ private readyFrame;
63
+ /** Contributions whose last cleanup (panel teardown / `deactivate()`) threw or
64
+ * rejected. Their next automatic re-activation (base re-render, image switch)
65
+ * falls back to the base type instead; an explicit selection clears the mark. */
66
+ private readonly failedCleanup;
67
+ private readyCb;
68
+ constructor(raw: readonly unknown[] | null | undefined, hooks: PlotModeControllerHooks, log?: PlotModeLogger);
69
+ /** Descriptors of the usable contributions, in registration order. */
70
+ descriptors(): ContributedPlotTypeDescriptor[];
71
+ /** The contribution for a selector id, if it is a contributed mode. */
72
+ find(id: PlotTypeId | null | undefined): PlotTypeContribution | undefined;
73
+ /**
74
+ * The built-in type that renders `id`: a built-in type is itself, a
75
+ * contributed id is its `baseType`, and an id that is neither (a stale
76
+ * contributed id whose provider is gone) resolves to `PlotType.IMAGE`, the
77
+ * default view.
78
+ */
79
+ baseTypeOf(id: PlotTypeId | null | undefined): PlotType;
80
+ /** The live session, if any. */
81
+ get current(): ActivePlotMode | null;
82
+ /** Whether an activation is waiting on the contribution or the viewport. */
83
+ get pending(): boolean;
84
+ private pendingId;
85
+ /**
86
+ * Start a session for `contribution`. Ends any live one first. Resolves when
87
+ * the attempt has finished (successfully or not); it never rejects.
88
+ */
89
+ activate(contribution: PlotTypeContribution, ctx: PlotModeContext): Promise<void>;
90
+ /**
91
+ * End the live session, if any: the component panel is dropped (via
92
+ * `onDeactivating`), a `mount` panel's teardown runs, then
93
+ * `session.deactivate()`, then the mount host is removed. Each step is
94
+ * isolated. A pending activation is cancelled; if it resolves later its
95
+ * session is deactivated on arrival. Idempotent.
96
+ */
97
+ deactivate(): void;
98
+ /**
99
+ * Forget recorded cleanup failures. The host calls this when the USER picks a
100
+ * plot type: an explicit choice gets a fresh attempt, only automatic
101
+ * re-activations fall back.
102
+ */
103
+ clearCleanupFailures(): void;
104
+ private start;
105
+ /**
106
+ * The host could not make the live mode usable (its panel threw while being
107
+ * created or first rendered): end the session once, then fall back to its base.
108
+ * No-op when nothing is live.
109
+ */
110
+ failActive(err: unknown): void;
111
+ private fail;
112
+ private guard;
113
+ /** Like `guard`, for a contribution callback typed `void` that may still return a
114
+ * promise: a rejection is logged instead of escaping as an unhandled rejection. */
115
+ private guardLogged;
116
+ /** Run a live session's cleanup; a throw OR a rejection goes to `cleanupFailed`. */
117
+ private cleanup;
118
+ /**
119
+ * A contribution's cleanup failed. If that same contribution is live or starting
120
+ * again (a base re-render / image switch re-activated it — or, for an async
121
+ * rejection, already has), end it and fall back to its base type. Otherwise the
122
+ * user has moved on: remember it, so the next automatic re-activation falls back.
123
+ */
124
+ private cleanupFailed;
125
+ /** Call `cb(true)` once the viewport reports ready (now, or on a later
126
+ * animation frame), or `cb(false)` after {@link MAX_READY_FRAMES}. */
127
+ private whenReady;
128
+ /** Stop waiting for the viewport; the waiting activation resolves as superseded. */
129
+ private cancelReadyWait;
130
+ }
@@ -17,6 +17,7 @@ import { IChannelHistogramApi, IChannelState, IHistogram } from './contracts/cha
17
17
  import { VisualizerStore } from './store/visualizer-store.service';
18
18
  import { NapariVisualizerService } from './implementations/napari-js/napari-visualizer.service';
19
19
  import { VizConfig } from './contracts/viz-config';
20
+ import { PlotModeViewport } from './contracts/plot-type-contribution.contract';
20
21
  import * as i0 from "@angular/core";
21
22
  export declare class RoutingVisualizerService implements IVisualizer, IRegionEditorApi, IChannelHistogramApi {
22
23
  private plotly;
@@ -98,6 +99,9 @@ export declare class RoutingVisualizerService implements IVisualizer, IRegionEdi
98
99
  resetSurfaceCamera(): void;
99
100
  setResolutionScale(scale: number): void;
100
101
  getResolutionScale(): number;
102
+ /** The viewport of the backend on screen, for a contributed plot mode. Null
103
+ * when that backend cannot provide one (e.g. OSD fell back to Plotly). */
104
+ getPlotModeViewport(): PlotModeViewport | null;
101
105
  getPlotTypeDescriptors(): PlotTypeDescriptor[];
102
106
  setStackLoading(b: boolean): void;
103
107
  cancelLoading(): void;
@@ -1,9 +1,10 @@
1
1
  import { EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
2
2
  import { MenuItem } from 'primeng/api';
3
3
  import { IImageInfo } from '../contracts/image.contract';
4
- import { PlotType, PlotTypeDescriptor } from '../contracts/plot-type';
4
+ import { PlotType, PlotTypeId } from '../contracts/plot-type';
5
5
  import { ToolbarToolVisibility } from '../contracts/toolbar-config';
6
6
  import { ToolbarToolContribution } from '../contracts/toolbar-tool.contract';
7
+ import { PlotTypeOption } from '../contracts/plot-type-contribution.contract';
7
8
  import * as i0 from "@angular/core";
8
9
  /**
9
10
  * Presentational toolbar for the plotting viewport.
@@ -20,9 +21,18 @@ import * as i0 from "@angular/core";
20
21
  export declare class ToolbarComponent implements OnChanges {
21
22
  /** Current image (gates which control groups are shown). */
22
23
  imageInfo: IImageInfo | undefined;
23
- /** Plot types the active backend advertises. */
24
- plotTypeOptions: PlotTypeDescriptor[];
25
- selectedPlotType: PlotType;
24
+ /** Plot types the active backend advertises, then any contributed modes. */
25
+ plotTypeOptions: PlotTypeOption[];
26
+ /** The selector's value: a built-in type or a contributed mode's id. */
27
+ selectedPlotType: PlotTypeId;
28
+ /**
29
+ * The built-in type that renders {@link selectedPlotType} — its `baseType`
30
+ * for a contributed mode. Every tool/control decision below reads
31
+ * {@link effectivePlotType}, so a contributed mode gets exactly its base
32
+ * type's toolbar. When unbound, a built-in selection is its own base and
33
+ * anything else is treated as the default Image view.
34
+ */
35
+ basePlotType: PlotType | null;
26
36
  /** Isosurface band as a 0–255 slider position (mapped onto the volume's real
27
37
  * intensity range by the renderer). Defaults to the full range. */
28
38
  isoRange: number[];
@@ -82,7 +92,7 @@ export declare class ToolbarComponent implements OnChanges {
82
92
  contributedTools: ToolbarToolContribution[];
83
93
  /** Active checkpoint per contributed tool, keyed by tool id. */
84
94
  toolModelIds: Record<string, string>;
85
- selectPlotType: EventEmitter<PlotType>;
95
+ selectPlotType: EventEmitter<string>;
86
96
  /** Intensity (LINE) mode: add another colored line ROI + inset trace. */
87
97
  addProfileLine: EventEmitter<void>;
88
98
  /** Toggle image smoothing (bilinear) vs nearest-neighbour (crisp pixels). */
@@ -196,6 +206,8 @@ export declare class ToolbarComponent implements OnChanges {
196
206
  /** Keeps each contributed tool's `p-menu` overlay alive across CD ticks —
197
207
  * re-creating it mid-interaction swallows the click on a menu item. */
198
208
  trackToolById(_index: number, tool: ToolbarToolContribution): string;
209
+ /** The built-in type whose tools and controls apply to the current selection. */
210
+ get effectivePlotType(): PlotType;
199
211
  /** The Image plot type renders as a natively pan/zoom-able raster, so the
200
212
  * generic zoom/pan tools are hidden. */
201
213
  get isImageView(): boolean;
@@ -230,5 +242,5 @@ export declare class ToolbarComponent implements OnChanges {
230
242
  get isIntensityCapable(): boolean;
231
243
  showHelp(): void;
232
244
  static ɵfac: i0.ɵɵFactoryDeclaration<ToolbarComponent, never>;
233
- static ɵcmp: i0.ɵɵComponentDeclaration<ToolbarComponent, "plotting-toolbar", never, { "imageInfo": { "alias": "imageInfo"; "required": false; }; "plotTypeOptions": { "alias": "plotTypeOptions"; "required": false; }; "selectedPlotType": { "alias": "selectedPlotType"; "required": false; }; "isoRange": { "alias": "isoRange"; "required": false; }; "maxIndex": { "alias": "maxIndex"; "required": false; }; "zIndex": { "alias": "zIndex"; "required": false; }; "isHeatmap": { "alias": "isHeatmap"; "required": false; }; "is3dRegions": { "alias": "is3dRegions"; "required": false; }; "activeDragMode": { "alias": "activeDragMode"; "required": false; }; "activeSurface3dMode": { "alias": "activeSurface3dMode"; "required": false; }; "axesVisible": { "alias": "axesVisible"; "required": false; }; "wireframeActive": { "alias": "wireframeActive"; "required": false; }; "resolutionScale": { "alias": "resolutionScale"; "required": false; }; "wandSensitivity": { "alias": "wandSensitivity"; "required": false; }; "brushSize": { "alias": "brushSize"; "required": false; }; "vertexEraserRadius": { "alias": "vertexEraserRadius"; "required": false; }; "canUndo": { "alias": "canUndo"; "required": false; }; "canRedo": { "alias": "canRedo"; "required": false; }; "stackOptions": { "alias": "stackOptions"; "required": false; }; "tools": { "alias": "tools"; "required": false; }; "imageSmoothingEnabled": { "alias": "imageSmoothingEnabled"; "required": false; }; "samModels": { "alias": "samModels"; "required": false; }; "samModelId": { "alias": "samModelId"; "required": false; }; "contributedTools": { "alias": "contributedTools"; "required": false; }; "toolModelIds": { "alias": "toolModelIds"; "required": false; }; }, { "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"; }, never, ["*"], false, never>;
245
+ static ɵcmp: i0.ɵɵComponentDeclaration<ToolbarComponent, "plotting-toolbar", never, { "imageInfo": { "alias": "imageInfo"; "required": false; }; "plotTypeOptions": { "alias": "plotTypeOptions"; "required": false; }; "selectedPlotType": { "alias": "selectedPlotType"; "required": false; }; "basePlotType": { "alias": "basePlotType"; "required": false; }; "isoRange": { "alias": "isoRange"; "required": false; }; "maxIndex": { "alias": "maxIndex"; "required": false; }; "zIndex": { "alias": "zIndex"; "required": false; }; "isHeatmap": { "alias": "isHeatmap"; "required": false; }; "is3dRegions": { "alias": "is3dRegions"; "required": false; }; "activeDragMode": { "alias": "activeDragMode"; "required": false; }; "activeSurface3dMode": { "alias": "activeSurface3dMode"; "required": false; }; "axesVisible": { "alias": "axesVisible"; "required": false; }; "wireframeActive": { "alias": "wireframeActive"; "required": false; }; "resolutionScale": { "alias": "resolutionScale"; "required": false; }; "wandSensitivity": { "alias": "wandSensitivity"; "required": false; }; "brushSize": { "alias": "brushSize"; "required": false; }; "vertexEraserRadius": { "alias": "vertexEraserRadius"; "required": false; }; "canUndo": { "alias": "canUndo"; "required": false; }; "canRedo": { "alias": "canRedo"; "required": false; }; "stackOptions": { "alias": "stackOptions"; "required": false; }; "tools": { "alias": "tools"; "required": false; }; "imageSmoothingEnabled": { "alias": "imageSmoothingEnabled"; "required": false; }; "samModels": { "alias": "samModels"; "required": false; }; "samModelId": { "alias": "samModelId"; "required": false; }; "contributedTools": { "alias": "contributedTools"; "required": false; }; "toolModelIds": { "alias": "toolModelIds"; "required": false; }; }, { "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"; }, never, ["*"], false, never>;
234
246
  }
@@ -1,4 +1,4 @@
1
- import { ChangeDetectorRef, AfterViewInit, EventEmitter, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
1
+ import { ChangeDetectorRef, AfterViewInit, ElementRef, EventEmitter, Injector, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges, Type } from '@angular/core';
2
2
  import { Subscription } from 'rxjs';
3
3
  import { MenuItem, MessageService, TreeNode } from 'primeng/api';
4
4
  import { ContextMenu } from 'primeng/contextmenu';
@@ -7,7 +7,9 @@ import { ImageStatePort } from './contracts/ports/image-state.port';
7
7
  import { Polygon } from './models/region';
8
8
  import { RegionOpsService } from './region-ops.service';
9
9
  import { VisualizerStore } from './store/visualizer-store.service';
10
- import { PlotType, PlotTypeDescriptor } from './contracts/plot-type';
10
+ import { PlotType, PlotTypeDescriptor, PlotTypeId } from './contracts/plot-type';
11
+ import { PlotTypeContribution, PlotTypeOption } from './contracts/plot-type-contribution.contract';
12
+ import { PlotModeController } from './plot-mode/plot-mode-controller';
11
13
  import { IVisualizer } from './contracts/visualizer.contract';
12
14
  import { SamToolService } from './toolbar/segmentation/sam-tool.service';
13
15
  import { SamPointToolService } from './toolbar/segmentation/sam-point-tool.service';
@@ -30,6 +32,7 @@ export declare class VisualizerComponent implements OnInit, OnChanges, AfterView
30
32
  private regionOps;
31
33
  private vizConfig?;
32
34
  private spatialData?;
35
+ private injector?;
33
36
  /** Per-instance toast key. MessageService is a global singleton, so two live
34
37
  * `<visualizer>` instances (main viewer + pipeline-dialog preview) sharing
35
38
  * one key would each render the same message — a duplicate toast. A unique
@@ -204,9 +207,48 @@ export declare class VisualizerComponent implements OnInit, OnChanges, AfterView
204
207
  wireframeActive: boolean;
205
208
  /** napari 3D decimate factor (1 = Full … 8 = ⅛; default ½); changing it re-plots. */
206
209
  resolutionScale: number;
207
- /** Plot types the active backend advertises (3D gated by capability). */
210
+ /** The selector's entries: the built-in plot types the active backend
211
+ * advertises (3D gated by capability), then any contributed modes
212
+ * ({@link PLOT_TYPE_CONTRIBUTIONS}). */
213
+ plotTypeMenu: PlotTypeOption[];
214
+ /**
215
+ * The built-in plot types on offer — {@link plotTypeMenu} without contributed
216
+ * modes. Kept with its original type so existing readers are unaffected.
217
+ */
208
218
  plotTypeOptions: PlotTypeDescriptor[];
209
- selectedPlotType: PlotType;
219
+ /**
220
+ * What the selector shows: a built-in type or a contributed mode's id. Every
221
+ * rendering and tool decision goes through {@link basePlotType} instead, so a
222
+ * contributed mode behaves exactly like the built-in type it rides on.
223
+ */
224
+ selectedPlotTypeId: PlotTypeId;
225
+ /**
226
+ * The built-in plot type being rendered. With no contributed mode active this
227
+ * is the selection itself; during one it is the mode's `baseType`. Kept with its
228
+ * original `PlotType` type for existing readers — use {@link selectedPlotTypeId}
229
+ * for the selector's id. Assigning selects that built-in type.
230
+ */
231
+ get selectedPlotType(): PlotType;
232
+ set selectedPlotType(type: PlotType);
233
+ /** Contributed plot modes and their single live session. */
234
+ readonly plotModes: PlotModeController;
235
+ /**
236
+ * The live contributed mode's side panel, as the template renders it: an
237
+ * Angular component with its own injector, or a plain host element a `mount`
238
+ * panel rendered into. Null while no contributed session is live.
239
+ */
240
+ plotModePanel: {
241
+ title: string;
242
+ component: Type<unknown> | null;
243
+ injector: Injector | null;
244
+ host: HTMLElement | null;
245
+ } | null;
246
+ private destroying;
247
+ /** Where a `mount` panel's host element is attached once the dialog renders. */
248
+ set plotModePanelSlot(ref: ElementRef<HTMLElement> | undefined);
249
+ /** The built-in type actually rendered for {@link selectedPlotTypeId}: itself
250
+ * for a built-in type, `baseType` for a contributed mode. */
251
+ get basePlotType(): PlotType;
210
252
  /** Floating intensity-profile inset (LINE mode). */
211
253
  readonly intensityInsetDiv = "intensity-inset-plot";
212
254
  profilePanelPos: {
@@ -276,7 +318,7 @@ export declare class VisualizerComponent implements OnInit, OnChanges, AfterView
276
318
  private wheelListener?;
277
319
  /** Debounced z-slice scrubbing (see SliceScrubber — refactoring plan Step 7). */
278
320
  private readonly scrubber;
279
- constructor(state: ImageStatePort, plotService: IVisualizer, messageService: MessageService, ngZone: NgZone, cdr: ChangeDetectorRef, session: VisualizerStore, samTool: SamToolService, cellSegmentTool: CellSegmentToolService, samPointTool: SamPointToolService, regionOps: RegionOpsService, vizConfig?: VizConfig | undefined, toolContributions?: readonly ToolbarToolContribution[], spatialData?: SpatialDataPort | undefined);
321
+ constructor(state: ImageStatePort, plotService: IVisualizer, messageService: MessageService, ngZone: NgZone, cdr: ChangeDetectorRef, session: VisualizerStore, samTool: SamToolService, cellSegmentTool: CellSegmentToolService, samPointTool: SamPointToolService, regionOps: RegionOpsService, vizConfig?: VizConfig | undefined, toolContributions?: readonly ToolbarToolContribution[], spatialData?: SpatialDataPort | undefined, plotTypeContributions?: readonly PlotTypeContribution[], injector?: Injector | undefined);
280
322
  /**
281
323
  * Plot types offered in the selector for the current image:
282
324
  * - Outside **test mode**, only the curated default set (descriptors with a
@@ -544,9 +586,28 @@ export declare class VisualizerComponent implements OnInit, OnChanges, AfterView
544
586
  * the toolbar/context-menu logic relies on, deactivates 2D-only tools when
545
587
  * moving to a 3D type, then re-plots.
546
588
  */
547
- onSelectPlotType(type: PlotType): void;
589
+ onSelectPlotType(selected: PlotTypeId): void;
590
+ /**
591
+ * Start the selected contributed mode's session, once its base view has
592
+ * plotted. No-op for a built-in type, or when the base on screen is not the
593
+ * one the mode rides on. A backend that cannot provide a viewport for it
594
+ * (e.g. OSD fell back to Plotly) counts as a failed activation.
595
+ */
596
+ private activateSelectedPlotMode;
597
+ /** Render the live mode's panel. Activation can resolve outside Angular (an
598
+ * OSD callback, a contribution's own promise), hence the zone re-entry. */
599
+ private showPlotModePanel;
600
+ /** Drop the panel (destroying a component panel) before the session ends. */
601
+ private hidePlotModePanel;
602
+ /**
603
+ * A contributed mode could not start. Its base type is already on screen —
604
+ * the base plotted first — so just select it: nothing needs to re-plot.
605
+ */
606
+ private fallBackFromPlotMode;
607
+ /** Run change detection; returns what it threw (logged), or null. */
608
+ private detectChangesSafely;
548
609
  /** Deactivate whatever tool is armed and clear every tool mode. */
549
610
  private deactivateActiveTool;
550
- static ɵfac: i0.ɵɵFactoryDeclaration<VisualizerComponent, [null, null, null, null, null, null, null, null, null, null, { optional: true; }, { optional: true; }, { optional: true; }]>;
611
+ static ɵfac: i0.ɵɵFactoryDeclaration<VisualizerComponent, [null, null, null, null, null, null, null, null, null, null, { optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }, { optional: true; }]>;
551
612
  static ɵcmp: i0.ɵɵComponentDeclaration<VisualizerComponent, "visualizer", never, { "toolbarTools": { "alias": "toolbarTools"; "required": false; }; "testMode": { "alias": "testMode"; "required": false; }; }, { "isStackEvent": "isStackEvent"; "isGrayscaleEvent": "isGrayscaleEvent"; }, never, ["*"], false, never>;
552
613
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jax-data-science/sci-image-visualizer",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "engines": {
5
5
  "node": ">=20.9.0"
6
6
  },