@fideus-labs/fidnii 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE.txt +9 -0
  2. package/README.md +180 -0
  3. package/dist/BufferManager.d.ts +86 -0
  4. package/dist/BufferManager.d.ts.map +1 -0
  5. package/dist/BufferManager.js +146 -0
  6. package/dist/BufferManager.js.map +1 -0
  7. package/dist/ClipPlanes.d.ts +180 -0
  8. package/dist/ClipPlanes.d.ts.map +1 -0
  9. package/dist/ClipPlanes.js +513 -0
  10. package/dist/ClipPlanes.js.map +1 -0
  11. package/dist/OMEZarrNVImage.d.ts +545 -0
  12. package/dist/OMEZarrNVImage.d.ts.map +1 -0
  13. package/dist/OMEZarrNVImage.js +1799 -0
  14. package/dist/OMEZarrNVImage.js.map +1 -0
  15. package/dist/RegionCoalescer.d.ts +75 -0
  16. package/dist/RegionCoalescer.d.ts.map +1 -0
  17. package/dist/RegionCoalescer.js +151 -0
  18. package/dist/RegionCoalescer.js.map +1 -0
  19. package/dist/ResolutionSelector.d.ts +88 -0
  20. package/dist/ResolutionSelector.d.ts.map +1 -0
  21. package/dist/ResolutionSelector.js +224 -0
  22. package/dist/ResolutionSelector.js.map +1 -0
  23. package/dist/ViewportBounds.d.ts +50 -0
  24. package/dist/ViewportBounds.d.ts.map +1 -0
  25. package/dist/ViewportBounds.js +325 -0
  26. package/dist/ViewportBounds.js.map +1 -0
  27. package/dist/events.d.ts +122 -0
  28. package/dist/events.d.ts.map +1 -0
  29. package/dist/events.js +12 -0
  30. package/dist/events.js.map +1 -0
  31. package/dist/index.d.ts +48 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +59 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/types.d.ts +273 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +126 -0
  38. package/dist/types.js.map +1 -0
  39. package/dist/utils/affine.d.ts +72 -0
  40. package/dist/utils/affine.d.ts.map +1 -0
  41. package/dist/utils/affine.js +173 -0
  42. package/dist/utils/affine.js.map +1 -0
  43. package/dist/utils/coordinates.d.ts +80 -0
  44. package/dist/utils/coordinates.d.ts.map +1 -0
  45. package/dist/utils/coordinates.js +207 -0
  46. package/dist/utils/coordinates.js.map +1 -0
  47. package/package.json +61 -0
  48. package/src/BufferManager.ts +176 -0
  49. package/src/ClipPlanes.ts +640 -0
  50. package/src/OMEZarrNVImage.ts +2286 -0
  51. package/src/RegionCoalescer.ts +217 -0
  52. package/src/ResolutionSelector.ts +325 -0
  53. package/src/ViewportBounds.ts +369 -0
  54. package/src/events.ts +146 -0
  55. package/src/index.ts +153 -0
  56. package/src/types.ts +429 -0
  57. package/src/utils/affine.ts +218 -0
  58. package/src/utils/coordinates.ts +271 -0
@@ -0,0 +1,217 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import * as zarr from "zarrita";
5
+ import type { NgffImage } from "@fideus-labs/ngff-zarr";
6
+ import { zarrGet } from "@fideus-labs/ngff-zarr/browser";
7
+ import type {
8
+ ChunkCache,
9
+ PixelRegion,
10
+ RegionFetchResult,
11
+ TypedArray,
12
+ } from "./types.js";
13
+
14
+ /**
15
+ * Represents a pending request that may have multiple consumers waiting for the result.
16
+ */
17
+ interface PendingRequest {
18
+ /** The promise that resolves when the request completes */
19
+ promise: Promise<RegionFetchResult>;
20
+ /** Function to resolve the promise with the result */
21
+ resolve: (data: RegionFetchResult) => void;
22
+ /** Function to reject the promise with an error */
23
+ reject: (error: Error) => void;
24
+ /** Set of requester IDs waiting for this result */
25
+ requesters: Set<string>;
26
+ }
27
+
28
+ /**
29
+ * RegionCoalescer handles fetching sub-regions from OME-Zarr images with:
30
+ *
31
+ * 1. Request deduplication - Multiple async triggers (zoom, crop changes, etc.)
32
+ * requesting the same region receive the same promise
33
+ * 2. Parallel fetching - Uses fizarrita's worker-pool-accelerated zarrGet
34
+ * for concurrent, worker-offloaded chunk fetches
35
+ * 3. Requester tracking - Tracks who is waiting for each request
36
+ * 4. Chunk caching - Optional decoded-chunk cache to avoid redundant
37
+ * decompression on repeated or overlapping reads
38
+ *
39
+ * This design supports future scenarios where multiple UI events may trigger
40
+ * overlapping region requests simultaneously.
41
+ */
42
+ export class RegionCoalescer {
43
+ private readonly pending: Map<string, PendingRequest> = new Map();
44
+
45
+ /** Optional decoded-chunk cache forwarded to fizarrita's getWorker. */
46
+ private readonly _cache: ChunkCache | undefined;
47
+
48
+ /**
49
+ * @param cache - Optional decoded-chunk cache. When provided, `zarrGet`
50
+ * caches decoded chunks to avoid redundant decompression on repeated
51
+ * or overlapping reads.
52
+ */
53
+ constructor(cache?: ChunkCache) {
54
+ this._cache = cache;
55
+ }
56
+
57
+ /**
58
+ * Generate a unique key for a request based on image path, level index, and region.
59
+ */
60
+ private makeKey(
61
+ imagePath: string,
62
+ levelIndex: number,
63
+ region: PixelRegion,
64
+ ): string {
65
+ const start = region.start.join(",");
66
+ const end = region.end.join(",");
67
+ return `${imagePath}:${levelIndex}:${start}:${end}`;
68
+ }
69
+
70
+ /**
71
+ * Request a region, coalescing with any in-flight request for the same data.
72
+ *
73
+ * @param ngffImage - The NgffImage to fetch from
74
+ * @param levelIndex - The resolution level index
75
+ * @param region - The pixel region to fetch
76
+ * @param requesterId - ID of the requester (e.g., 'zoom', 'crop-change', 'progressive-load')
77
+ * @returns The fetched region data
78
+ */
79
+ async fetchRegion(
80
+ ngffImage: NgffImage,
81
+ levelIndex: number,
82
+ region: PixelRegion,
83
+ requesterId: string = "default",
84
+ ): Promise<RegionFetchResult> {
85
+ const key = this.makeKey(ngffImage.data.path, levelIndex, region);
86
+
87
+ // Check if there's already a pending request for this data
88
+ const existing = this.pending.get(key);
89
+ if (existing) {
90
+ // Add this requester to the waiters and return the existing promise
91
+ existing.requesters.add(requesterId);
92
+ return existing.promise;
93
+ }
94
+
95
+ // Create a new pending request
96
+ let resolvePromise!: (data: RegionFetchResult) => void;
97
+ let rejectPromise!: (error: Error) => void;
98
+
99
+ const promise = new Promise<RegionFetchResult>((resolve, reject) => {
100
+ resolvePromise = resolve;
101
+ rejectPromise = reject;
102
+ });
103
+
104
+ const pendingRequest: PendingRequest = {
105
+ promise,
106
+ resolve: resolvePromise,
107
+ reject: rejectPromise,
108
+ requesters: new Set([requesterId]),
109
+ };
110
+
111
+ this.pending.set(key, pendingRequest);
112
+
113
+ // Fetch using fizarrita's worker-accelerated zarrGet
114
+ try {
115
+ const selection = [
116
+ zarr.slice(region.start[0], region.end[0]),
117
+ zarr.slice(region.start[1], region.end[1]),
118
+ zarr.slice(region.start[2], region.end[2]),
119
+ ];
120
+ // Pass the chunk cache to fizarrita's getWorker via zarrGet.
121
+ // The `cache` option is available in @fideus-labs/fizarrita >=1.2.0.
122
+ const zarrOpts = this._cache
123
+ ? { cache: this._cache } as Record<string, unknown>
124
+ : undefined;
125
+ const result = await zarrGet(ngffImage.data, selection, zarrOpts);
126
+
127
+ const fetchResult: RegionFetchResult = {
128
+ data: result.data as TypedArray,
129
+ shape: result.shape,
130
+ stride: result.stride,
131
+ };
132
+
133
+ pendingRequest.resolve(fetchResult);
134
+ return fetchResult;
135
+ } catch (error) {
136
+ const err = error instanceof Error ? error : new Error(String(error));
137
+ pendingRequest.reject(err);
138
+ throw err;
139
+ } finally {
140
+ this.pending.delete(key);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Fetch multiple regions in parallel, with deduplication.
146
+ * Useful for fetching multiple chunks for a single view update.
147
+ *
148
+ * @param ngffImage - The NgffImage to fetch from
149
+ * @param levelIndex - The resolution level index
150
+ * @param regions - Array of pixel regions to fetch
151
+ * @param requesterId - ID of the requester
152
+ * @returns Array of fetched region data
153
+ */
154
+ async fetchRegions(
155
+ ngffImage: NgffImage,
156
+ levelIndex: number,
157
+ regions: PixelRegion[],
158
+ requesterId: string = "default",
159
+ ): Promise<RegionFetchResult[]> {
160
+ return Promise.all(
161
+ regions.map((region) =>
162
+ this.fetchRegion(ngffImage, levelIndex, region, requesterId)
163
+ ),
164
+ );
165
+ }
166
+
167
+ /**
168
+ * Check if there's a pending request for the given parameters.
169
+ */
170
+ hasPending(
171
+ ngffImage: NgffImage,
172
+ levelIndex: number,
173
+ region: PixelRegion,
174
+ ): boolean {
175
+ const key = this.makeKey(ngffImage.data.path, levelIndex, region);
176
+ return this.pending.has(key);
177
+ }
178
+
179
+ /**
180
+ * Get the set of requesters waiting for a pending request.
181
+ * Returns undefined if no pending request exists.
182
+ */
183
+ getPendingRequesters(
184
+ ngffImage: NgffImage,
185
+ levelIndex: number,
186
+ region: PixelRegion,
187
+ ): Set<string> | undefined {
188
+ const key = this.makeKey(ngffImage.data.path, levelIndex, region);
189
+ return this.pending.get(key)?.requesters;
190
+ }
191
+
192
+ /**
193
+ * Wait for all pending fetches to complete.
194
+ */
195
+ async onIdle(): Promise<void> {
196
+ // Wait for all in-flight requests to settle
197
+ const promises = Array.from(this.pending.values()).map((p) =>
198
+ p.promise.catch(() => {})
199
+ );
200
+ await Promise.all(promises);
201
+ }
202
+
203
+ /**
204
+ * Get the number of pending requests (unique region requests).
205
+ */
206
+ get pendingCount(): number {
207
+ return this.pending.size;
208
+ }
209
+
210
+ /**
211
+ * Clear all pending requests.
212
+ * Note: Does not resolve or reject pending promises.
213
+ */
214
+ clear(): void {
215
+ this.pending.clear();
216
+ }
217
+ }
@@ -0,0 +1,325 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import type { Multiscales, NgffImage } from "@fideus-labs/ngff-zarr";
5
+ import type {
6
+ ClipPlanes,
7
+ PixelRegion,
8
+ ResolutionSelection,
9
+ VolumeBounds,
10
+ } from "./types.js";
11
+ import { clipPlanesToPixelRegion } from "./ClipPlanes.js";
12
+
13
+ /**
14
+ * Orthogonal axis index in [z, y, x] order.
15
+ * 0 = Z (axial view), 1 = Y (coronal view), 2 = X (sagittal view)
16
+ */
17
+ export type OrthogonalAxis = 0 | 1 | 2;
18
+
19
+ /**
20
+ * Select the appropriate resolution level based on pixel budget and clip planes.
21
+ *
22
+ * The selection process:
23
+ * 1. Starts from the highest resolution (level 0)
24
+ * 2. Finds the highest resolution that fits within maxPixels
25
+ * 3. Considers the clipped region size, not full volume
26
+ *
27
+ * @param multiscales - The OME-Zarr multiscales data
28
+ * @param maxPixels - Maximum number of pixels to use
29
+ * @param clipPlanes - Current clip planes in world space
30
+ * @param volumeBounds - Full volume bounds in world space
31
+ * @param viewportBounds - Optional viewport bounds (for viewport-aware mode)
32
+ * @returns The selected resolution level and buffer dimensions
33
+ */
34
+ export function selectResolution(
35
+ multiscales: Multiscales,
36
+ maxPixels: number,
37
+ clipPlanes: ClipPlanes,
38
+ volumeBounds: VolumeBounds,
39
+ viewportBounds?: VolumeBounds,
40
+ ): ResolutionSelection {
41
+ const images = multiscales.images;
42
+
43
+ // Try each resolution from highest to lowest
44
+ for (let i = 0; i < images.length; i++) {
45
+ const image = images[i];
46
+ const region = clipPlanesToPixelRegion(
47
+ clipPlanes,
48
+ volumeBounds,
49
+ image,
50
+ viewportBounds,
51
+ );
52
+ const alignedRegion = alignRegionToChunks(region, image);
53
+
54
+ const dimensions: [number, number, number] = [
55
+ alignedRegion.end[0] - alignedRegion.start[0],
56
+ alignedRegion.end[1] - alignedRegion.start[1],
57
+ alignedRegion.end[2] - alignedRegion.start[2],
58
+ ];
59
+
60
+ const pixelCount = dimensions[0] * dimensions[1] * dimensions[2];
61
+
62
+ if (pixelCount <= maxPixels) {
63
+ return {
64
+ levelIndex: i,
65
+ dimensions,
66
+ pixelCount,
67
+ };
68
+ }
69
+ }
70
+
71
+ // Fall back to lowest resolution
72
+ const lowestImage = images[images.length - 1];
73
+ const region = clipPlanesToPixelRegion(
74
+ clipPlanes,
75
+ volumeBounds,
76
+ lowestImage,
77
+ viewportBounds,
78
+ );
79
+ const alignedRegion = alignRegionToChunks(region, lowestImage);
80
+
81
+ const dimensions: [number, number, number] = [
82
+ alignedRegion.end[0] - alignedRegion.start[0],
83
+ alignedRegion.end[1] - alignedRegion.start[1],
84
+ alignedRegion.end[2] - alignedRegion.start[2],
85
+ ];
86
+
87
+ return {
88
+ levelIndex: images.length - 1,
89
+ dimensions,
90
+ pixelCount: dimensions[0] * dimensions[1] * dimensions[2],
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Get the chunk shape for a volume.
96
+ *
97
+ * @param ngffImage - The NgffImage to get chunk shape from
98
+ * @returns Chunk shape as [z, y, x]
99
+ */
100
+ export function getChunkShape(ngffImage: NgffImage): [number, number, number] {
101
+ const chunks = ngffImage.data.chunks;
102
+ const dims = ngffImage.dims;
103
+
104
+ // Find z, y, x indices in dims
105
+ const zIdx = dims.indexOf("z");
106
+ const yIdx = dims.indexOf("y");
107
+ const xIdx = dims.indexOf("x");
108
+
109
+ if (zIdx === -1 || yIdx === -1 || xIdx === -1) {
110
+ // Fallback: assume last 3 dimensions are z, y, x
111
+ const n = chunks.length;
112
+ return [chunks[n - 3] || 1, chunks[n - 2] || 1, chunks[n - 1] || 1];
113
+ }
114
+
115
+ return [chunks[zIdx], chunks[yIdx], chunks[xIdx]];
116
+ }
117
+
118
+ /**
119
+ * Get the shape of a volume as [z, y, x].
120
+ *
121
+ * @param ngffImage - The NgffImage
122
+ * @returns Shape as [z, y, x]
123
+ */
124
+ export function getVolumeShape(ngffImage: NgffImage): [number, number, number] {
125
+ const shape = ngffImage.data.shape;
126
+ const dims = ngffImage.dims;
127
+
128
+ // Find z, y, x indices in dims
129
+ const zIdx = dims.indexOf("z");
130
+ const yIdx = dims.indexOf("y");
131
+ const xIdx = dims.indexOf("x");
132
+
133
+ if (zIdx === -1 || yIdx === -1 || xIdx === -1) {
134
+ // Fallback: assume last 3 dimensions are z, y, x
135
+ const n = shape.length;
136
+ return [shape[n - 3] || 1, shape[n - 2] || 1, shape[n - 1] || 1];
137
+ }
138
+
139
+ return [shape[zIdx], shape[yIdx], shape[xIdx]];
140
+ }
141
+
142
+ /**
143
+ * Align a pixel region to chunk boundaries.
144
+ * Expands the region to include complete chunks.
145
+ *
146
+ * @param region - The pixel region to align
147
+ * @param ngffImage - The NgffImage (for chunk shape)
148
+ * @returns Aligned region
149
+ */
150
+ export function alignRegionToChunks(
151
+ region: PixelRegion,
152
+ ngffImage: NgffImage,
153
+ ): PixelRegion {
154
+ const chunkShape = getChunkShape(ngffImage);
155
+ const volumeShape = getVolumeShape(ngffImage);
156
+
157
+ // Align start down to chunk boundary
158
+ const alignedStart: [number, number, number] = [
159
+ Math.floor(region.start[0] / chunkShape[0]) * chunkShape[0],
160
+ Math.floor(region.start[1] / chunkShape[1]) * chunkShape[1],
161
+ Math.floor(region.start[2] / chunkShape[2]) * chunkShape[2],
162
+ ];
163
+
164
+ // Align end up to chunk boundary (but don't exceed volume size)
165
+ const alignedEnd: [number, number, number] = [
166
+ Math.min(
167
+ Math.ceil(region.end[0] / chunkShape[0]) * chunkShape[0],
168
+ volumeShape[0],
169
+ ),
170
+ Math.min(
171
+ Math.ceil(region.end[1] / chunkShape[1]) * chunkShape[1],
172
+ volumeShape[1],
173
+ ),
174
+ Math.min(
175
+ Math.ceil(region.end[2] / chunkShape[2]) * chunkShape[2],
176
+ volumeShape[2],
177
+ ),
178
+ ];
179
+
180
+ return {
181
+ start: alignedStart,
182
+ end: alignedEnd,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Calculate the middle resolution level index.
188
+ *
189
+ * @param multiscales - The OME-Zarr multiscales data
190
+ * @returns Middle resolution level index
191
+ */
192
+ export function getMiddleResolutionIndex(multiscales: Multiscales): number {
193
+ return Math.floor(multiscales.images.length / 2);
194
+ }
195
+
196
+ /**
197
+ * Calculate upsample factor from one resolution level to another.
198
+ *
199
+ * @param multiscales - The OME-Zarr multiscales data
200
+ * @param fromLevel - Source resolution level
201
+ * @param toLevel - Target resolution level (should be higher resolution, i.e., lower index)
202
+ * @returns Upsample factor for each dimension [z, y, x]
203
+ */
204
+ export function calculateUpsampleFactor(
205
+ multiscales: Multiscales,
206
+ fromLevel: number,
207
+ toLevel: number,
208
+ ): [number, number, number] {
209
+ const fromImage = multiscales.images[fromLevel];
210
+ const toImage = multiscales.images[toLevel];
211
+
212
+ const fromShape = getVolumeShape(fromImage);
213
+ const toShape = getVolumeShape(toImage);
214
+
215
+ return [
216
+ toShape[0] / fromShape[0],
217
+ toShape[1] / fromShape[1],
218
+ toShape[2] / fromShape[2],
219
+ ];
220
+ }
221
+
222
+ /**
223
+ * Get dimensions for the full volume at a given resolution level.
224
+ */
225
+ export function getFullVolumeDimensions(
226
+ multiscales: Multiscales,
227
+ levelIndex: number,
228
+ ): [number, number, number] {
229
+ return getVolumeShape(multiscales.images[levelIndex]);
230
+ }
231
+
232
+ /**
233
+ * Select the appropriate resolution level for a 2D slice view.
234
+ *
235
+ * Unlike `selectResolution` which counts all 3D pixels (z*y*x), this function
236
+ * counts only the 2D in-plane pixels (e.g., x*y for axial), ignoring the
237
+ * orthogonal axis. This allows much higher resolution for 2D slice views.
238
+ *
239
+ * The slab dimensions returned include one chunk of thickness in the
240
+ * orthogonal direction (needed for zarr fetching efficiency). The pixel
241
+ * budget accounts for the full 3D slab volume (in-plane area multiplied
242
+ * by the chunk thickness along the orthogonal axis).
243
+ *
244
+ * @param multiscales - The OME-Zarr multiscales data
245
+ * @param maxPixels - Maximum number of voxels for the slab (in-plane area × chunk depth)
246
+ * @param clipPlanes - Current clip planes in world space
247
+ * @param volumeBounds - Full volume bounds in world space
248
+ * @param orthogonalAxis - The axis perpendicular to the slice plane (0=Z, 1=Y, 2=X)
249
+ * @param viewportBounds - Optional viewport bounds (for viewport-aware mode)
250
+ * @returns The selected resolution level and slab dimensions
251
+ */
252
+ export function select2DResolution(
253
+ multiscales: Multiscales,
254
+ maxPixels: number,
255
+ clipPlanes: ClipPlanes,
256
+ volumeBounds: VolumeBounds,
257
+ orthogonalAxis: OrthogonalAxis,
258
+ viewportBounds?: VolumeBounds,
259
+ ): ResolutionSelection {
260
+ const images = multiscales.images;
261
+
262
+ // Try each resolution from highest to lowest
263
+ for (let i = 0; i < images.length; i++) {
264
+ const image = images[i];
265
+ const region = clipPlanesToPixelRegion(
266
+ clipPlanes,
267
+ volumeBounds,
268
+ image,
269
+ viewportBounds,
270
+ );
271
+ const alignedRegion = alignRegionToChunks(region, image);
272
+
273
+ const dimensions: [number, number, number] = [
274
+ alignedRegion.end[0] - alignedRegion.start[0],
275
+ alignedRegion.end[1] - alignedRegion.start[1],
276
+ alignedRegion.end[2] - alignedRegion.start[2],
277
+ ];
278
+
279
+ // Count the full slab volume: in-plane area × chunk depth along the
280
+ // orthogonal axis. The slab is always one chunk thick, so we use the
281
+ // chunk shape rather than the full volume extent in that axis.
282
+ const chunkShape = getChunkShape(image);
283
+ const slabDepth = chunkShape[orthogonalAxis];
284
+ const inPlaneAxes = ([0, 1, 2] as const).filter((a) =>
285
+ a !== orthogonalAxis
286
+ );
287
+ const slabVoxelCount = dimensions[inPlaneAxes[0]] *
288
+ dimensions[inPlaneAxes[1]] * slabDepth;
289
+
290
+ if (slabVoxelCount <= maxPixels) {
291
+ return {
292
+ levelIndex: i,
293
+ dimensions,
294
+ pixelCount: slabVoxelCount,
295
+ };
296
+ }
297
+ }
298
+
299
+ // Fall back to lowest resolution
300
+ const lowestImage = images[images.length - 1];
301
+ const region = clipPlanesToPixelRegion(
302
+ clipPlanes,
303
+ volumeBounds,
304
+ lowestImage,
305
+ viewportBounds,
306
+ );
307
+ const alignedRegion = alignRegionToChunks(region, lowestImage);
308
+
309
+ const dimensions: [number, number, number] = [
310
+ alignedRegion.end[0] - alignedRegion.start[0],
311
+ alignedRegion.end[1] - alignedRegion.start[1],
312
+ alignedRegion.end[2] - alignedRegion.start[2],
313
+ ];
314
+
315
+ const chunkShape = getChunkShape(lowestImage);
316
+ const slabDepth = chunkShape[orthogonalAxis];
317
+ const inPlaneAxes = ([0, 1, 2] as const).filter((a) => a !== orthogonalAxis);
318
+
319
+ return {
320
+ levelIndex: images.length - 1,
321
+ dimensions,
322
+ pixelCount: dimensions[inPlaneAxes[0]] * dimensions[inPlaneAxes[1]] *
323
+ slabDepth,
324
+ };
325
+ }