@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,224 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
2
+ // SPDX-License-Identifier: MIT
3
+ import { clipPlanesToPixelRegion } from "./ClipPlanes.js";
4
+ /**
5
+ * Select the appropriate resolution level based on pixel budget and clip planes.
6
+ *
7
+ * The selection process:
8
+ * 1. Starts from the highest resolution (level 0)
9
+ * 2. Finds the highest resolution that fits within maxPixels
10
+ * 3. Considers the clipped region size, not full volume
11
+ *
12
+ * @param multiscales - The OME-Zarr multiscales data
13
+ * @param maxPixels - Maximum number of pixels to use
14
+ * @param clipPlanes - Current clip planes in world space
15
+ * @param volumeBounds - Full volume bounds in world space
16
+ * @param viewportBounds - Optional viewport bounds (for viewport-aware mode)
17
+ * @returns The selected resolution level and buffer dimensions
18
+ */
19
+ export function selectResolution(multiscales, maxPixels, clipPlanes, volumeBounds, viewportBounds) {
20
+ const images = multiscales.images;
21
+ // Try each resolution from highest to lowest
22
+ for (let i = 0; i < images.length; i++) {
23
+ const image = images[i];
24
+ const region = clipPlanesToPixelRegion(clipPlanes, volumeBounds, image, viewportBounds);
25
+ const alignedRegion = alignRegionToChunks(region, image);
26
+ const dimensions = [
27
+ alignedRegion.end[0] - alignedRegion.start[0],
28
+ alignedRegion.end[1] - alignedRegion.start[1],
29
+ alignedRegion.end[2] - alignedRegion.start[2],
30
+ ];
31
+ const pixelCount = dimensions[0] * dimensions[1] * dimensions[2];
32
+ if (pixelCount <= maxPixels) {
33
+ return {
34
+ levelIndex: i,
35
+ dimensions,
36
+ pixelCount,
37
+ };
38
+ }
39
+ }
40
+ // Fall back to lowest resolution
41
+ const lowestImage = images[images.length - 1];
42
+ const region = clipPlanesToPixelRegion(clipPlanes, volumeBounds, lowestImage, viewportBounds);
43
+ const alignedRegion = alignRegionToChunks(region, lowestImage);
44
+ const dimensions = [
45
+ alignedRegion.end[0] - alignedRegion.start[0],
46
+ alignedRegion.end[1] - alignedRegion.start[1],
47
+ alignedRegion.end[2] - alignedRegion.start[2],
48
+ ];
49
+ return {
50
+ levelIndex: images.length - 1,
51
+ dimensions,
52
+ pixelCount: dimensions[0] * dimensions[1] * dimensions[2],
53
+ };
54
+ }
55
+ /**
56
+ * Get the chunk shape for a volume.
57
+ *
58
+ * @param ngffImage - The NgffImage to get chunk shape from
59
+ * @returns Chunk shape as [z, y, x]
60
+ */
61
+ export function getChunkShape(ngffImage) {
62
+ const chunks = ngffImage.data.chunks;
63
+ const dims = ngffImage.dims;
64
+ // Find z, y, x indices in dims
65
+ const zIdx = dims.indexOf("z");
66
+ const yIdx = dims.indexOf("y");
67
+ const xIdx = dims.indexOf("x");
68
+ if (zIdx === -1 || yIdx === -1 || xIdx === -1) {
69
+ // Fallback: assume last 3 dimensions are z, y, x
70
+ const n = chunks.length;
71
+ return [chunks[n - 3] || 1, chunks[n - 2] || 1, chunks[n - 1] || 1];
72
+ }
73
+ return [chunks[zIdx], chunks[yIdx], chunks[xIdx]];
74
+ }
75
+ /**
76
+ * Get the shape of a volume as [z, y, x].
77
+ *
78
+ * @param ngffImage - The NgffImage
79
+ * @returns Shape as [z, y, x]
80
+ */
81
+ export function getVolumeShape(ngffImage) {
82
+ const shape = ngffImage.data.shape;
83
+ const dims = ngffImage.dims;
84
+ // Find z, y, x indices in dims
85
+ const zIdx = dims.indexOf("z");
86
+ const yIdx = dims.indexOf("y");
87
+ const xIdx = dims.indexOf("x");
88
+ if (zIdx === -1 || yIdx === -1 || xIdx === -1) {
89
+ // Fallback: assume last 3 dimensions are z, y, x
90
+ const n = shape.length;
91
+ return [shape[n - 3] || 1, shape[n - 2] || 1, shape[n - 1] || 1];
92
+ }
93
+ return [shape[zIdx], shape[yIdx], shape[xIdx]];
94
+ }
95
+ /**
96
+ * Align a pixel region to chunk boundaries.
97
+ * Expands the region to include complete chunks.
98
+ *
99
+ * @param region - The pixel region to align
100
+ * @param ngffImage - The NgffImage (for chunk shape)
101
+ * @returns Aligned region
102
+ */
103
+ export function alignRegionToChunks(region, ngffImage) {
104
+ const chunkShape = getChunkShape(ngffImage);
105
+ const volumeShape = getVolumeShape(ngffImage);
106
+ // Align start down to chunk boundary
107
+ const alignedStart = [
108
+ Math.floor(region.start[0] / chunkShape[0]) * chunkShape[0],
109
+ Math.floor(region.start[1] / chunkShape[1]) * chunkShape[1],
110
+ Math.floor(region.start[2] / chunkShape[2]) * chunkShape[2],
111
+ ];
112
+ // Align end up to chunk boundary (but don't exceed volume size)
113
+ const alignedEnd = [
114
+ Math.min(Math.ceil(region.end[0] / chunkShape[0]) * chunkShape[0], volumeShape[0]),
115
+ Math.min(Math.ceil(region.end[1] / chunkShape[1]) * chunkShape[1], volumeShape[1]),
116
+ Math.min(Math.ceil(region.end[2] / chunkShape[2]) * chunkShape[2], volumeShape[2]),
117
+ ];
118
+ return {
119
+ start: alignedStart,
120
+ end: alignedEnd,
121
+ };
122
+ }
123
+ /**
124
+ * Calculate the middle resolution level index.
125
+ *
126
+ * @param multiscales - The OME-Zarr multiscales data
127
+ * @returns Middle resolution level index
128
+ */
129
+ export function getMiddleResolutionIndex(multiscales) {
130
+ return Math.floor(multiscales.images.length / 2);
131
+ }
132
+ /**
133
+ * Calculate upsample factor from one resolution level to another.
134
+ *
135
+ * @param multiscales - The OME-Zarr multiscales data
136
+ * @param fromLevel - Source resolution level
137
+ * @param toLevel - Target resolution level (should be higher resolution, i.e., lower index)
138
+ * @returns Upsample factor for each dimension [z, y, x]
139
+ */
140
+ export function calculateUpsampleFactor(multiscales, fromLevel, toLevel) {
141
+ const fromImage = multiscales.images[fromLevel];
142
+ const toImage = multiscales.images[toLevel];
143
+ const fromShape = getVolumeShape(fromImage);
144
+ const toShape = getVolumeShape(toImage);
145
+ return [
146
+ toShape[0] / fromShape[0],
147
+ toShape[1] / fromShape[1],
148
+ toShape[2] / fromShape[2],
149
+ ];
150
+ }
151
+ /**
152
+ * Get dimensions for the full volume at a given resolution level.
153
+ */
154
+ export function getFullVolumeDimensions(multiscales, levelIndex) {
155
+ return getVolumeShape(multiscales.images[levelIndex]);
156
+ }
157
+ /**
158
+ * Select the appropriate resolution level for a 2D slice view.
159
+ *
160
+ * Unlike `selectResolution` which counts all 3D pixels (z*y*x), this function
161
+ * counts only the 2D in-plane pixels (e.g., x*y for axial), ignoring the
162
+ * orthogonal axis. This allows much higher resolution for 2D slice views.
163
+ *
164
+ * The slab dimensions returned include one chunk of thickness in the
165
+ * orthogonal direction (needed for zarr fetching efficiency). The pixel
166
+ * budget accounts for the full 3D slab volume (in-plane area multiplied
167
+ * by the chunk thickness along the orthogonal axis).
168
+ *
169
+ * @param multiscales - The OME-Zarr multiscales data
170
+ * @param maxPixels - Maximum number of voxels for the slab (in-plane area × chunk depth)
171
+ * @param clipPlanes - Current clip planes in world space
172
+ * @param volumeBounds - Full volume bounds in world space
173
+ * @param orthogonalAxis - The axis perpendicular to the slice plane (0=Z, 1=Y, 2=X)
174
+ * @param viewportBounds - Optional viewport bounds (for viewport-aware mode)
175
+ * @returns The selected resolution level and slab dimensions
176
+ */
177
+ export function select2DResolution(multiscales, maxPixels, clipPlanes, volumeBounds, orthogonalAxis, viewportBounds) {
178
+ const images = multiscales.images;
179
+ // Try each resolution from highest to lowest
180
+ for (let i = 0; i < images.length; i++) {
181
+ const image = images[i];
182
+ const region = clipPlanesToPixelRegion(clipPlanes, volumeBounds, image, viewportBounds);
183
+ const alignedRegion = alignRegionToChunks(region, image);
184
+ const dimensions = [
185
+ alignedRegion.end[0] - alignedRegion.start[0],
186
+ alignedRegion.end[1] - alignedRegion.start[1],
187
+ alignedRegion.end[2] - alignedRegion.start[2],
188
+ ];
189
+ // Count the full slab volume: in-plane area × chunk depth along the
190
+ // orthogonal axis. The slab is always one chunk thick, so we use the
191
+ // chunk shape rather than the full volume extent in that axis.
192
+ const chunkShape = getChunkShape(image);
193
+ const slabDepth = chunkShape[orthogonalAxis];
194
+ const inPlaneAxes = [0, 1, 2].filter((a) => a !== orthogonalAxis);
195
+ const slabVoxelCount = dimensions[inPlaneAxes[0]] *
196
+ dimensions[inPlaneAxes[1]] * slabDepth;
197
+ if (slabVoxelCount <= maxPixels) {
198
+ return {
199
+ levelIndex: i,
200
+ dimensions,
201
+ pixelCount: slabVoxelCount,
202
+ };
203
+ }
204
+ }
205
+ // Fall back to lowest resolution
206
+ const lowestImage = images[images.length - 1];
207
+ const region = clipPlanesToPixelRegion(clipPlanes, volumeBounds, lowestImage, viewportBounds);
208
+ const alignedRegion = alignRegionToChunks(region, lowestImage);
209
+ const dimensions = [
210
+ alignedRegion.end[0] - alignedRegion.start[0],
211
+ alignedRegion.end[1] - alignedRegion.start[1],
212
+ alignedRegion.end[2] - alignedRegion.start[2],
213
+ ];
214
+ const chunkShape = getChunkShape(lowestImage);
215
+ const slabDepth = chunkShape[orthogonalAxis];
216
+ const inPlaneAxes = [0, 1, 2].filter((a) => a !== orthogonalAxis);
217
+ return {
218
+ levelIndex: images.length - 1,
219
+ dimensions,
220
+ pixelCount: dimensions[inPlaneAxes[0]] * dimensions[inPlaneAxes[1]] *
221
+ slabDepth,
222
+ };
223
+ }
224
+ //# sourceMappingURL=ResolutionSelector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResolutionSelector.js","sourceRoot":"","sources":["../src/ResolutionSelector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import type { Niivue } from "@niivue/niivue";
2
+ import { SLICE_TYPE } from "@niivue/niivue";
3
+ import type { VolumeBounds } from "./types.js";
4
+ /**
5
+ * Intersect two axis-aligned bounding boxes.
6
+ * Returns the overlapping region, clamped so max >= min.
7
+ */
8
+ export declare function intersectBounds(a: VolumeBounds, b: VolumeBounds): VolumeBounds;
9
+ /**
10
+ * Compute the world-space axis-aligned bounding box of the visible region
11
+ * in a 3D render view.
12
+ *
13
+ * NiiVue uses an orthographic projection whose extents depend on
14
+ * `volScaleMultiplier`, `renderAzimuth`, and `renderElevation`.
15
+ * We build the 8 corners of the ortho frustum, rotate them by the inverse
16
+ * of the view rotation, and take the AABB.
17
+ *
18
+ * @param nv - The Niivue instance
19
+ * @param volumeBounds - Full volume bounds to intersect with
20
+ * @returns Viewport bounds in world space, intersected with volumeBounds
21
+ */
22
+ export declare function computeViewportBounds3D(nv: Niivue, volumeBounds: VolumeBounds): VolumeBounds;
23
+ /**
24
+ * Compute the world-space bounding box of the visible region in a 2D slice
25
+ * view, accounting for pan and zoom.
26
+ *
27
+ * NiiVue's 2D slice renderer applies pan (`pan2Dxyzmm[0..2]`) and zoom
28
+ * (`pan2Dxyzmm[3]`) to the base field of view. We replicate this math to
29
+ * determine what mm range is visible on screen.
30
+ *
31
+ * @param nv - The Niivue instance
32
+ * @param sliceType - Current 2D slice type (AXIAL, CORONAL, SAGITTAL)
33
+ * @param volumeBounds - Full volume bounds to intersect with
34
+ * @param normalizationScale - If the NVImage affine was normalized (multiplied
35
+ * by 1/maxVoxelSize to avoid NiiVue precision issues), pass that scale here
36
+ * so we can convert the NiiVue mm-space FOV back to physical world
37
+ * coordinates. Pass 1.0 if no normalization was applied.
38
+ * @returns Viewport bounds in world space, intersected with volumeBounds
39
+ */
40
+ export declare function computeViewportBounds2D(nv: Niivue, sliceType: SLICE_TYPE, volumeBounds: VolumeBounds, normalizationScale?: number): VolumeBounds;
41
+ /**
42
+ * Check if two VolumeBounds are approximately equal.
43
+ *
44
+ * @param a - First bounds
45
+ * @param b - Second bounds
46
+ * @param tolerance - Relative tolerance (default: 0.01 = 1%)
47
+ * @returns True if bounds are within tolerance
48
+ */
49
+ export declare function boundsApproxEqual(a: VolumeBounds, b: VolumeBounds, tolerance?: number): boolean;
50
+ //# sourceMappingURL=ViewportBounds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ViewportBounds.d.ts","sourceRoot":"","sources":["../src/ViewportBounds.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,CAAC,EAAE,YAAY,EACf,CAAC,EAAE,YAAY,GACd,YAAY,CAwBd;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,MAAM,EACV,YAAY,EAAE,YAAY,GACzB,YAAY,CA2Gd;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,UAAU,EACrB,YAAY,EAAE,YAAY,EAC1B,kBAAkB,GAAE,MAAY,GAC/B,YAAY,CA6Jd;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,YAAY,EACf,CAAC,EAAE,YAAY,EACf,SAAS,GAAE,MAAa,GACvB,OAAO,CAUT"}
@@ -0,0 +1,325 @@
1
+ // SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
2
+ // SPDX-License-Identifier: MIT
3
+ import { SLICE_TYPE } from "@niivue/niivue";
4
+ /**
5
+ * Intersect two axis-aligned bounding boxes.
6
+ * Returns the overlapping region, clamped so max >= min.
7
+ */
8
+ export function intersectBounds(a, b) {
9
+ const min = [
10
+ Math.max(a.min[0], b.min[0]),
11
+ Math.max(a.min[1], b.min[1]),
12
+ Math.max(a.min[2], b.min[2]),
13
+ ];
14
+ const max = [
15
+ Math.min(a.max[0], b.max[0]),
16
+ Math.min(a.max[1], b.max[1]),
17
+ Math.min(a.max[2], b.max[2]),
18
+ ];
19
+ // Ensure valid bounds (max >= min)
20
+ return {
21
+ min: [
22
+ Math.min(min[0], max[0]),
23
+ Math.min(min[1], max[1]),
24
+ Math.min(min[2], max[2]),
25
+ ],
26
+ max: [
27
+ Math.max(min[0], max[0]),
28
+ Math.max(min[1], max[1]),
29
+ Math.max(min[2], max[2]),
30
+ ],
31
+ };
32
+ }
33
+ /**
34
+ * Compute the world-space axis-aligned bounding box of the visible region
35
+ * in a 3D render view.
36
+ *
37
+ * NiiVue uses an orthographic projection whose extents depend on
38
+ * `volScaleMultiplier`, `renderAzimuth`, and `renderElevation`.
39
+ * We build the 8 corners of the ortho frustum, rotate them by the inverse
40
+ * of the view rotation, and take the AABB.
41
+ *
42
+ * @param nv - The Niivue instance
43
+ * @param volumeBounds - Full volume bounds to intersect with
44
+ * @returns Viewport bounds in world space, intersected with volumeBounds
45
+ */
46
+ export function computeViewportBounds3D(nv, volumeBounds) {
47
+ // Get scene extents: [min, max, range]
48
+ const extents = nv.sceneExtentsMinMax(true);
49
+ const mn = extents[0]; // vec3
50
+ const mx = extents[1]; // vec3
51
+ const range = extents[2]; // vec3
52
+ // Pivot = center of scene
53
+ const pivotX = (mn[0] + mx[0]) * 0.5;
54
+ const pivotY = (mn[1] + mx[1]) * 0.5;
55
+ const pivotZ = (mn[2] + mx[2]) * 0.5;
56
+ // furthestFromPivot = half-diagonal of bounding box
57
+ const furthest = Math.sqrt(range[0] * range[0] + range[1] * range[1] + range[2] * range[2]) * 0.5;
58
+ // NiiVue's orthographic scale (matches calculateMvpMatrix)
59
+ const scale = (0.8 * furthest) / (nv.scene.volScaleMultiplier || 1);
60
+ // Canvas aspect ratio
61
+ const canvas = nv.canvas;
62
+ const canvasW = canvas?.width ?? 1;
63
+ const canvasH = canvas?.height ?? 1;
64
+ const whratio = canvasW / canvasH;
65
+ // Ortho extents in view space (before rotation)
66
+ let halfW, halfH;
67
+ if (whratio < 1) {
68
+ // Portrait
69
+ halfW = scale;
70
+ halfH = scale / whratio;
71
+ }
72
+ else {
73
+ // Landscape
74
+ halfW = scale * whratio;
75
+ halfH = scale;
76
+ }
77
+ // For viewport-aware resolution, we need the world-space extent that is
78
+ // visible on screen. Rather than rotating a full 3D frustum box (whose depth
79
+ // dominates the AABB after rotation), we compute the extent differently:
80
+ //
81
+ // Project the view-space axes onto world space and accumulate the half-extents
82
+ // contributed by each view axis. The view X axis (with extent halfW) and view
83
+ // Y axis (with extent halfH) determine what is visible on screen. The depth
84
+ // axis does NOT constrain visibility — we see through the full depth of the
85
+ // volume — so we include the full volume extent along the view Z axis.
86
+ // Build the inverse of NiiVue's view rotation.
87
+ // NiiVue applies: rotateX(270 - elevation) then rotateZ(azimuth - 180)
88
+ // Also mirrors X (modelMatrix[0] = -1).
89
+ // We need the inverse rotation to go from view space back to world space.
90
+ const azimuth = nv.scene.renderAzimuth ?? 0;
91
+ const elevation = nv.scene.renderElevation ?? 0;
92
+ const azRad = ((azimuth - 180) * Math.PI) / 180;
93
+ const elRad = ((270 - elevation) * Math.PI) / 180;
94
+ const cosAz = Math.cos(azRad);
95
+ const sinAz = Math.sin(azRad);
96
+ const cosEl = Math.cos(elRad);
97
+ const sinEl = Math.sin(elRad);
98
+ // Compute inverse rotation of unit view-space axes to world space.
99
+ // Inverse: un-mirror X, then rotateZ(-azRad), then rotateX(-elRad).
100
+ //
101
+ // View X axis (1, 0, 0) after un-mirror: (-1, 0, 0)
102
+ const viewXinWorld = [
103
+ -cosAz, // wx
104
+ sinAz * cosEl, // wy
105
+ -sinAz * sinEl, // wz
106
+ ];
107
+ // View Y axis (0, 1, 0) — no mirror effect
108
+ const viewYinWorld = [
109
+ sinAz, // wx
110
+ cosAz * cosEl, // wy
111
+ -cosAz * sinEl, // wz
112
+ ];
113
+ // View Z axis (0, 0, 1) — no mirror effect
114
+ const viewZinWorld = [
115
+ 0, // wx
116
+ sinEl, // wy
117
+ cosEl, // wz
118
+ ];
119
+ // For each world axis, the visible half-extent is:
120
+ // halfW * |viewXinWorld[axis]| + halfH * |viewYinWorld[axis]|
121
+ // + furthest * |viewZinWorld[axis]| (full volume depth along view Z)
122
+ const worldHalfExtent = [0, 0, 0];
123
+ for (let axis = 0; axis < 3; axis++) {
124
+ worldHalfExtent[axis] = halfW * Math.abs(viewXinWorld[axis]) +
125
+ halfH * Math.abs(viewYinWorld[axis]) +
126
+ furthest * Math.abs(viewZinWorld[axis]);
127
+ }
128
+ const frustumBounds = {
129
+ min: [
130
+ pivotX - worldHalfExtent[0],
131
+ pivotY - worldHalfExtent[1],
132
+ pivotZ - worldHalfExtent[2],
133
+ ],
134
+ max: [
135
+ pivotX + worldHalfExtent[0],
136
+ pivotY + worldHalfExtent[1],
137
+ pivotZ + worldHalfExtent[2],
138
+ ],
139
+ };
140
+ return intersectBounds(frustumBounds, volumeBounds);
141
+ }
142
+ /**
143
+ * Compute the world-space bounding box of the visible region in a 2D slice
144
+ * view, accounting for pan and zoom.
145
+ *
146
+ * NiiVue's 2D slice renderer applies pan (`pan2Dxyzmm[0..2]`) and zoom
147
+ * (`pan2Dxyzmm[3]`) to the base field of view. We replicate this math to
148
+ * determine what mm range is visible on screen.
149
+ *
150
+ * @param nv - The Niivue instance
151
+ * @param sliceType - Current 2D slice type (AXIAL, CORONAL, SAGITTAL)
152
+ * @param volumeBounds - Full volume bounds to intersect with
153
+ * @param normalizationScale - If the NVImage affine was normalized (multiplied
154
+ * by 1/maxVoxelSize to avoid NiiVue precision issues), pass that scale here
155
+ * so we can convert the NiiVue mm-space FOV back to physical world
156
+ * coordinates. Pass 1.0 if no normalization was applied.
157
+ * @returns Viewport bounds in world space, intersected with volumeBounds
158
+ */
159
+ export function computeViewportBounds2D(nv, sliceType, volumeBounds, normalizationScale = 1.0) {
160
+ // Compute the base field of view from the FULL volume bounds (in normalized
161
+ // mm space), then swizzle to screen axes for this slice orientation.
162
+ //
163
+ // IMPORTANT: We intentionally do NOT use nv.screenFieldOfViewExtendedMM()
164
+ // because that returns the extents of the *currently loaded* NVImage (the
165
+ // slab). After a viewport-aware reload shrinks the slab, the next call to
166
+ // screenFieldOfViewExtendedMM() would return a smaller FOV, creating a
167
+ // feedback loop that progressively shrinks the slab to nothing.
168
+ //
169
+ // Instead, we derive the base FOV from the constant full-volume bounds,
170
+ // scaled to normalized mm space (matching the slab NVImage's affine).
171
+ // We then apply the same swizzle that NiiVue uses, giving us a stable
172
+ // base FOV that doesn't depend on the current slab geometry.
173
+ const normMin = [
174
+ volumeBounds.min[0] * normalizationScale,
175
+ volumeBounds.min[1] * normalizationScale,
176
+ volumeBounds.min[2] * normalizationScale,
177
+ ];
178
+ const normMax = [
179
+ volumeBounds.max[0] * normalizationScale,
180
+ volumeBounds.max[1] * normalizationScale,
181
+ volumeBounds.max[2] * normalizationScale,
182
+ ];
183
+ // Swizzle to screen axes (same mapping as NiiVue's swizzleVec3MM):
184
+ // AXIAL: screen X = mm X, screen Y = mm Y
185
+ // CORONAL: screen X = mm X, screen Y = mm Z
186
+ // SAGITTAL: screen X = mm Y, screen Y = mm Z
187
+ let mnMM0, mxMM0, mnMM1, mxMM1;
188
+ switch (sliceType) {
189
+ case SLICE_TYPE.CORONAL:
190
+ mnMM0 = normMin[0];
191
+ mxMM0 = normMax[0]; // screen X = mm X
192
+ mnMM1 = normMin[2];
193
+ mxMM1 = normMax[2]; // screen Y = mm Z
194
+ break;
195
+ case SLICE_TYPE.SAGITTAL:
196
+ mnMM0 = normMin[1];
197
+ mxMM0 = normMax[1]; // screen X = mm Y
198
+ mnMM1 = normMin[2];
199
+ mxMM1 = normMax[2]; // screen Y = mm Z
200
+ break;
201
+ default: // AXIAL
202
+ mnMM0 = normMin[0];
203
+ mxMM0 = normMax[0]; // screen X = mm X
204
+ mnMM1 = normMin[1];
205
+ mxMM1 = normMax[1]; // screen Y = mm Y
206
+ break;
207
+ }
208
+ // Account for canvas aspect ratio stretching (matches draw2DMain logic)
209
+ // NiiVue stretches the FOV to fill the canvas while preserving aspect ratio
210
+ const canvas = nv.canvas;
211
+ if (canvas) {
212
+ const canvasW = canvas.width || 1;
213
+ const canvasH = canvas.height || 1;
214
+ const fovW = Math.abs(mxMM0 - mnMM0);
215
+ const fovH = Math.abs(mxMM1 - mnMM1);
216
+ if (fovW > 0 && fovH > 0) {
217
+ const canvasAspect = canvasW / canvasH;
218
+ const fovAspect = fovW / fovH;
219
+ if (canvasAspect > fovAspect) {
220
+ // Canvas is wider than FOV: expand X
221
+ const midX = (mnMM0 + mxMM0) * 0.5;
222
+ const newHalfW = (fovH * canvasAspect) * 0.5;
223
+ mnMM0 = midX - newHalfW;
224
+ mxMM0 = midX + newHalfW;
225
+ }
226
+ else {
227
+ // Canvas is taller than FOV: expand Y
228
+ const midY = (mnMM1 + mxMM1) * 0.5;
229
+ const newHalfH = (fovW / canvasAspect) * 0.5;
230
+ mnMM1 = midY - newHalfH;
231
+ mxMM1 = midY + newHalfH;
232
+ }
233
+ }
234
+ }
235
+ // Apply pan and zoom (matching NiiVue's draw2DMain logic)
236
+ const pan = nv.scene.pan2Dxyzmm; // vec4: [panX, panY, panZ, zoom]
237
+ // Swizzle the pan vector to match the current orientation
238
+ const panSwizzled = nv.swizzleVec3MM([pan[0], pan[1], pan[2]], sliceType);
239
+ const zoom = pan[3] || 1;
240
+ // Apply pan: shift visible window
241
+ mnMM0 -= panSwizzled[0];
242
+ mxMM0 -= panSwizzled[0];
243
+ mnMM1 -= panSwizzled[1];
244
+ mxMM1 -= panSwizzled[1];
245
+ // Apply zoom: divide by zoom factor (zoom > 1 = zoomed in = smaller FOV)
246
+ mnMM0 /= zoom;
247
+ mxMM0 /= zoom;
248
+ mnMM1 /= zoom;
249
+ mxMM1 /= zoom;
250
+ // Convert from NiiVue's mm space back to physical world coordinates.
251
+ // If the slab affine was normalized (multiplied by normalizationScale),
252
+ // NiiVue's mm values are world * normalizationScale. Dividing by the
253
+ // normalization scale recovers physical coordinates.
254
+ if (normalizationScale !== 1.0 && normalizationScale > 0) {
255
+ const invNorm = 1.0 / normalizationScale;
256
+ mnMM0 *= invNorm;
257
+ mxMM0 *= invNorm;
258
+ mnMM1 *= invNorm;
259
+ mxMM1 *= invNorm;
260
+ }
261
+ // Now un-swizzle back to RAS world coordinates.
262
+ // The swizzle maps depend on the slice orientation:
263
+ // AXIAL: screen X = R/L (world X), screen Y = A/P (world Y)
264
+ // CORONAL: screen X = R/L (world X), screen Y = S/I (world Z)
265
+ // SAGITTAL: screen X = A/P (world Y), screen Y = S/I (world Z)
266
+ //
267
+ // The orthogonal axis (depth) is left as full volume extent.
268
+ const result = {
269
+ min: [...volumeBounds.min],
270
+ max: [...volumeBounds.max],
271
+ };
272
+ // Ensure min < max for each swizzled axis
273
+ const visMin0 = Math.min(mnMM0, mxMM0);
274
+ const visMax0 = Math.max(mnMM0, mxMM0);
275
+ const visMin1 = Math.min(mnMM1, mxMM1);
276
+ const visMax1 = Math.max(mnMM1, mxMM1);
277
+ switch (sliceType) {
278
+ case SLICE_TYPE.AXIAL:
279
+ // screen X = world X (R/L), screen Y = world Y (A/P)
280
+ result.min[0] = visMin0;
281
+ result.max[0] = visMax0;
282
+ result.min[1] = visMin1;
283
+ result.max[1] = visMax1;
284
+ // Z (S/I) = full extent (orthogonal axis)
285
+ break;
286
+ case SLICE_TYPE.CORONAL:
287
+ // screen X = world X (R/L), screen Y = world Z (S/I)
288
+ result.min[0] = visMin0;
289
+ result.max[0] = visMax0;
290
+ result.min[2] = visMin1;
291
+ result.max[2] = visMax1;
292
+ // Y (A/P) = full extent (orthogonal axis)
293
+ break;
294
+ case SLICE_TYPE.SAGITTAL:
295
+ // screen X = world Y (A/P), screen Y = world Z (S/I)
296
+ result.min[1] = visMin0;
297
+ result.max[1] = visMax0;
298
+ result.min[2] = visMin1;
299
+ result.max[2] = visMax1;
300
+ // X (R/L) = full extent (orthogonal axis)
301
+ break;
302
+ }
303
+ return intersectBounds(result, volumeBounds);
304
+ }
305
+ /**
306
+ * Check if two VolumeBounds are approximately equal.
307
+ *
308
+ * @param a - First bounds
309
+ * @param b - Second bounds
310
+ * @param tolerance - Relative tolerance (default: 0.01 = 1%)
311
+ * @returns True if bounds are within tolerance
312
+ */
313
+ export function boundsApproxEqual(a, b, tolerance = 0.01) {
314
+ for (let i = 0; i < 3; i++) {
315
+ const rangeA = a.max[i] - a.min[i];
316
+ const rangeB = b.max[i] - b.min[i];
317
+ const maxRange = Math.max(Math.abs(rangeA), Math.abs(rangeB), 1e-10);
318
+ if (Math.abs(a.min[i] - b.min[i]) / maxRange > tolerance)
319
+ return false;
320
+ if (Math.abs(a.max[i] - b.max[i]) / maxRange > tolerance)
321
+ return false;
322
+ }
323
+ return true;
324
+ }
325
+ //# sourceMappingURL=ViewportBounds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ViewportBounds.js","sourceRoot":"","sources":["../src/ViewportBounds.ts"],"names":[],"mappings":"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"}