@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.
- package/LICENSE.txt +9 -0
- package/README.md +180 -0
- package/dist/BufferManager.d.ts +86 -0
- package/dist/BufferManager.d.ts.map +1 -0
- package/dist/BufferManager.js +146 -0
- package/dist/BufferManager.js.map +1 -0
- package/dist/ClipPlanes.d.ts +180 -0
- package/dist/ClipPlanes.d.ts.map +1 -0
- package/dist/ClipPlanes.js +513 -0
- package/dist/ClipPlanes.js.map +1 -0
- package/dist/OMEZarrNVImage.d.ts +545 -0
- package/dist/OMEZarrNVImage.d.ts.map +1 -0
- package/dist/OMEZarrNVImage.js +1799 -0
- package/dist/OMEZarrNVImage.js.map +1 -0
- package/dist/RegionCoalescer.d.ts +75 -0
- package/dist/RegionCoalescer.d.ts.map +1 -0
- package/dist/RegionCoalescer.js +151 -0
- package/dist/RegionCoalescer.js.map +1 -0
- package/dist/ResolutionSelector.d.ts +88 -0
- package/dist/ResolutionSelector.d.ts.map +1 -0
- package/dist/ResolutionSelector.js +224 -0
- package/dist/ResolutionSelector.js.map +1 -0
- package/dist/ViewportBounds.d.ts +50 -0
- package/dist/ViewportBounds.d.ts.map +1 -0
- package/dist/ViewportBounds.js +325 -0
- package/dist/ViewportBounds.js.map +1 -0
- package/dist/events.d.ts +122 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +12 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +273 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +126 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/affine.d.ts +72 -0
- package/dist/utils/affine.d.ts.map +1 -0
- package/dist/utils/affine.js +173 -0
- package/dist/utils/affine.js.map +1 -0
- package/dist/utils/coordinates.d.ts +80 -0
- package/dist/utils/coordinates.d.ts.map +1 -0
- package/dist/utils/coordinates.js +207 -0
- package/dist/utils/coordinates.js.map +1 -0
- package/package.json +61 -0
- package/src/BufferManager.ts +176 -0
- package/src/ClipPlanes.ts +640 -0
- package/src/OMEZarrNVImage.ts +2286 -0
- package/src/RegionCoalescer.ts +217 -0
- package/src/ResolutionSelector.ts +325 -0
- package/src/ViewportBounds.ts +369 -0
- package/src/events.ts +146 -0
- package/src/index.ts +153 -0
- package/src/types.ts +429 -0
- package/src/utils/affine.ts +218 -0
- 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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"}
|