/** * MaskVolume — True 3D Volumetric Mask Storage * * Stores annotation masks in a single contiguous Uint8Array with the memory * layout [z][y][x][channel] (slice-major order). * * Design goals: * - Contiguous memory for optimal cache locality and minimal GC pressure. * - Multi-channel support (binary mask, confidence, labels, etc.). * - O(1) voxel access with full bounds checking. * - Slice extraction to ImageData for Canvas rendering (all 3 axes). * - Multi-channel color mapping with 4 render modes. * * Memory comparison (512 × 512 × 100, 1 channel): * Old ImageData approach ≈ 4.4 GB → MaskVolume ≈ 25 MB (~99 % reduction) * * @example * ```ts * const vol = new MaskVolume(512, 512, 100); // 1 channel (default) * vol.setVoxel(256, 256, 50, 255); // paint centre voxel * * // Grayscale slice (backward compatible) * const slice = vol.getSliceImageData(50, 'z'); * * // Colored slice * const colored = vol.getSliceImageData(50, 'z', { * mode: RenderMode.COLORED_SINGLE, * channel: 0, * opacity: 0.6, * }); * ``` */ import type { Dimensions, RGBAColor, ChannelColorMap, SliceRenderOptions } from './types'; export declare class MaskVolume { /** Contiguous backing buffer — layout [z][y][x][channel]. */ private data; /** Volume size in voxels. */ private readonly dims; /** Number of value channels per voxel (≥ 1). */ private readonly numChannels; /** * Number of bytes in one complete z-slice. * Equal to width × height × channels. */ private readonly bytesPerSlice; /** Per-channel color map used for colored rendering modes. */ private colorMap; /** * Monotonic data-mutation counter. * * Incremented by every method that mutates voxel data (not color). * Consumers (e.g. the contour cache in RenderingUtils) key cached * derivations on this value so an edit automatically invalidates them — * it is impossible to forget an invalidation point because every write * bumps the version. Color-map changes do **not** bump it (colors are * applied at fill time, never cached). */ private version; /** * Create a new MaskVolume. * * All voxels are initialised to **0**. * * @param width Number of voxels along the X axis (≥ 1). * @param height Number of voxels along the Y axis (≥ 1). * @param depth Number of voxels along the Z axis (≥ 1). * @param channels Number of channels per voxel (default 1). * @param customColorMap Optional color map to override defaults. * * @throws {RangeError} If any dimension or the channel count is < 1. */ constructor(width: number, height: number, depth: number, channels?: number, customColorMap?: ChannelColorMap); /** * Map 3D coordinates + channel to a flat 1D index. * * **Memory layout (slice-major):** * ``` * index = (z × height × width × channels) * + (y × width × channels) * + (x × channels) * + channel * ``` * * @param x Voxel column (0 … width − 1). * @param y Voxel row (0 … height − 1). * @param z Slice index (0 … depth − 1). * @param channel Channel index (0 … channels − 1). * @returns Flat 1D index into `this.data`. * * @throws {RangeError} If any coordinate or channel is out of bounds. */ private getIndex; /** * Read a single voxel value. * * @param x Voxel column (0 … width − 1). * @param y Voxel row (0 … height − 1). * @param z Slice index (0 … depth − 1). * @param channel Channel index (default 0). * @returns The stored value (0 – 255). * * @throws {RangeError} If any coordinate is out of bounds. */ getVoxel(x: number, y: number, z: number, channel?: number): number; /** * Write a single voxel value. * * Values are clamped to the Uint8 range [0, 255] by the typed array. * * @param x Voxel column (0 … width − 1). * @param y Voxel row (0 … height − 1). * @param z Slice index (0 … depth − 1). * @param value New voxel value (0 – 255). * @param channel Channel index (default 0). * * @throws {RangeError} If any coordinate is out of bounds. */ setVoxel(x: number, y: number, z: number, value: number, channel?: number): void; /** * Return the current data-mutation version. * * Increments on every voxel write. Use as a cache key so derived data * (e.g. extracted contours) is invalidated whenever the volume changes. */ getVersion(): number; /** * Update the color for a specific label/channel in the color map. * * For label-based volumes (1-channel), the channel parameter refers to * the label value (0-8), not the storage channel index. * * @param channel Label/channel index to update (0-8). * @param color New RGBA color. * * @throws {RangeError} If channel is outside valid label range [0, 8]. */ setChannelColor(channel: number, color: RGBAColor): void; /** * Get the current color for a channel. * * Falls back to the background color (transparent) if no color is defined. * * @param channel Channel index. * @returns A copy of the RGBA color. */ getChannelColor(channel: number): RGBAColor; /** * Reset the color map for one or all labels to defaults. * * @param channel Optional specific label to reset. If omitted, resets all. */ resetChannelColors(channel?: number): void; /** * Get a shallow copy of the entire color map. * * @returns A copy of the color map (safe to mutate). */ getColorMap(): ChannelColorMap; /** * Extract a 2D slice as an ImageData with color mapping. * * Slice dimensions depend on the axis: * * | Axis | Plane | Width | Height | * |------|----------|--------|--------| * | `z` | Axial | width | height | * | `y` | Coronal | width | depth | * | `x` | Sagittal | depth | height | * * @param sliceIndex Index along the specified axis. * @param axis `'x'` (sagittal), `'y'` (coronal), or `'z'` (axial). * @param options Rendering options (mode, channel, colors, etc.). * @returns ImageData ready for `ctx.putImageData()`. * * @throws {RangeError} If sliceIndex is out of bounds for the given axis. * * @example * ```ts * // Grayscale (default, backward compatible) * const gs = vol.getSliceImageData(50, 'z'); * * // Colored single-channel * const cs = vol.getSliceImageData(50, 'z', { * mode: RenderMode.COLORED_SINGLE, * channel: 0, * }); * * // Multi-channel priority * const mc = vol.getSliceImageData(50, 'z', { * mode: RenderMode.COLORED_MULTI, * visibleChannels: [false, true, true, true], * }); * ``` */ getSliceImageData(sliceIndex: number, axis?: 'x' | 'y' | 'z', options?: SliceRenderOptions): ImageData; /** * Write a 2D ImageData back into the volume for the given axis/slice. * * When the volume has **4 channels**, all RGBA components are stored * (ch0=R, ch1=G, ch2=B, ch3=A) for lossless round-trip. * Otherwise, the **R channel** of each pixel is used as grayscale. * * @param sliceIndex Index along the specified axis. * @param imageData Source ImageData (expected dimensions must match the axis). * @param axis `'x'`, `'y'`, or `'z'` (default `'z'`). * @param channel Target channel (default 0, ignored when numChannels >= 4). * * @throws {RangeError} If sliceIndex is out of bounds. * @throws {Error} If imageData dimensions don't match. * * @example * ```ts * const imgData = ctx.getImageData(0, 0, 512, 512); * vol.setSliceFromImageData(50, imgData, 'z'); * ``` */ setSliceFromImageData(sliceIndex: number, imageData: ImageData, axis?: 'x' | 'y' | 'z', channel?: number): void; /** /** * Store label data from canvas RGBA ImageData into a 1-channel volume. * * For each pixel: matches the RGB color against MASK_CHANNEL_COLORS to * determine which channel label to store. If no match is found, falls * back to `activeChannel`. Transparent pixels (alpha === 0) are stored as 0. * * This is the write counterpart to `renderLabelSliceInto`. * * @param sliceIndex Index along the specified axis. * @param imageData Canvas ImageData (RGBA) to convert. * @param axis `'x'`, `'y'`, or `'z'`. * @param activeChannel Fallback label for pixels whose RGB doesn't match any channel (1-8). * @param channelVisible Optional map of visible channels (true=visible, false=hidden). * If provided, data for hidden channels will be preserved * when the canvas pixel is transparent. */ setSliceLabelsFromImageData(sliceIndex: number, imageData: ImageData, axis?: 'x' | 'y' | 'z', activeChannel?: number, channelVisible?: Record): void; /** * Build a Map from RGB packed integer to channel label for reverse lookup. * Uses this instance's colorMap (channels 1-8, skips 0 = transparent). * * Instance method ensures custom per-layer colors are correctly reverse-mapped. */ private buildRgbToChannelMap; /** * Render a 1-channel label slice into a pre-allocated RGBA ImageData buffer. * * Each voxel stores a label value (0-8). Label 0 = transparent. * Labels 1-8 are mapped to colors via MASK_CHANNEL_COLORS, filtered * by the channelVisible array. * * @param sliceIndex Index along the specified axis. * @param axis `'x'`, `'y'`, or `'z'`. * @param target Pre-allocated ImageData buffer (must match slice dimensions). * @param channelVisible Array where index N indicates if channel N is visible. * If undefined, all channels are visible. * @param opacity Opacity multiplier 0.0–1.0 (default 1.0). */ renderLabelSliceInto(sliceIndex: number, axis: "x" | "y" | "z" | undefined, target: ImageData, channelVisible?: Record, opacity?: number): void; /** * Return a copy of the volume dimensions. * * @returns `{ width, height, depth }` — a fresh object (safe to mutate). */ getDimensions(): Dimensions; /** * Return the number of channels per voxel. * * @returns Channel count (≥ 1). */ getChannels(): number; /** * Return the number of bytes in one complete z-slice. * * Equal to `width × height × channels`. Useful for direct buffer * index arithmetic in performance-critical code paths. * * @returns Bytes per z-slice. */ getBytesPerSlice(): number; /** * Return total memory used by the backing buffer, in bytes. * * @returns `width × height × depth × channels` bytes. * * @example * ```ts * const vol = new MaskVolume(512, 512, 100); * vol.getMemoryUsage(); // 26_214_400 (~25 MB) * ``` */ getMemoryUsage(): number; /** * Return a **reference** to the raw backing buffer. * * Use this for direct read access (e.g. serialisation, GPU upload). * Mutating the returned array mutates the volume. * * @returns The underlying Uint8Array. * * @example * ```ts * // Serialise for network transfer * const bytes = vol.getRawData(); * socket.send(bytes.buffer); * ``` */ getRawData(): Uint8Array; /** * Replace the entire backing buffer. * * The provided array **must** have exactly the same length as the * current buffer (`width × height × depth × channels`). * * @param newData Replacement data. * @throws {Error} If the length does not match. * * @example * ```ts * // Restore from a saved snapshot * const saved = new Uint8Array(savedBuffer); * vol.setRawData(saved); * ``` */ setRawData(newData: Uint8Array): void; /** * Create an independent deep copy of this volume. * * The clone has the same dimensions, channels, data, and color map * but shares no references with the original. * * @returns A new MaskVolume with identical content. * * @example * ```ts * // Undo support: snapshot before an operation * const snapshot = vol.clone(); * performEdit(vol); * // Rollback: vol.setRawData(snapshot.getRawData()); * ``` */ clone(): MaskVolume; /** * Zero every voxel in the entire volume (all channels). * * @example * ```ts * vol.clear(); * vol.getVoxel(256, 256, 50); // 0 * ``` */ clear(): void; /** * Check if the volume contains any non-zero voxel data. * * Returns true if at least one voxel has a non-zero value, * false if all voxels are zero (empty mask). * * This is useful for determining if a layer has actual mask * annotations before saving or exporting. * * @returns True if any voxel is non-zero, false if all zeros. * * @example * ```ts * const vol = new MaskVolume(512, 512, 100); * vol.hasData(); // false (all zeros) * * vol.setVoxel(256, 256, 50, 255); * vol.hasData(); // true * ``` */ hasData(): boolean; /** * Zero every voxel in a single slice along the given axis. * * If `channel` is provided, only that channel is cleared; * otherwise **all** channels in the slice are cleared. * * @param sliceIndex Index along the specified axis. * @param axis `'x'`, `'y'`, or `'z'` (default `'z'`). * @param channel Optional channel to clear (default: all). * * @throws {RangeError} If sliceIndex is out of bounds. * * @example * ```ts * // Erase a single axial slice * vol.clearSlice(50, 'z'); * * // Erase only channel 1 of slice 50 * vol.clearSlice(50, 'z', 1); * ``` */ clearSlice(sliceIndex: number, axis?: 'x' | 'y' | 'z', channel?: number): void; /** * Extract a 2D slice as a flat Uint8Array (one byte per voxel per channel). * * For a 1-channel volume the result is `sliceWidth × sliceHeight` bytes, * each byte being the label value at that pixel. This is suitable for * sending to a backend that expects raw label data (e.g. NIfTI slice * replacement). * * @param sliceIndex Index along the specified axis. * @param axis `'x'`, `'y'`, or `'z'` (default `'z'`). * @returns A **copy** of the slice data plus its dimensions. * * @throws {RangeError} If sliceIndex is out of bounds. * * @example * ```ts * const { data, width, height } = vol.getSliceUint8(50, 'z'); * // data.length === width * height * channels * ``` */ getSliceUint8(sliceIndex: number, axis?: 'x' | 'y' | 'z'): { data: Uint8Array; width: number; height: number; }; /** * Write a 2D slice from a flat Uint8Array back into the volume. * * This is the inverse of {@link getSliceUint8}. The `data` array must * have exactly `sliceWidth × sliceHeight × channels` bytes. * * @param sliceIndex Index along the specified axis. * @param data Flat source array (same layout as returned by getSliceUint8). * @param axis `'x'`, `'y'`, or `'z'` (default `'z'`). * * @throws {RangeError} If sliceIndex is out of bounds. */ setSliceUint8(sliceIndex: number, data: Uint8Array, axis?: 'x' | 'y' | 'z'): void; /** * Get the 2D slice dimensions for a given axis. * * @returns `[width, height]` of the slice. */ private getSliceDimensions; /** * Validate that a slice index is within bounds for the given axis. * * @throws {RangeError} If out of bounds. */ private validateSliceIndex; /** * Render a single channel as grayscale (original behaviour). * * Non-zero voxels are fully opaque (A = 255), zero voxels are * transparent (A = 0). */ private renderGrayscale; /** * Render a single channel with its predefined color. * * Alpha is modulated by the voxel intensity and the opacity multiplier. */ private renderColoredSingle; /** * Render all visible channels with distinct colors. * * The **highest-index** non-zero visible channel wins (priority-based). */ private renderColoredMulti; /** * Render all visible channels with additive blending. * * Each channel contributes its color weighted by intensity × opacity. * RGB values are clamped to 255. */ private renderBlended; }