@fideus-labs/fidnii 0.1.0 → 0.2.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/dist/BufferManager.d.ts.map +1 -1
- package/dist/BufferManager.js +2 -5
- package/dist/BufferManager.js.map +1 -1
- package/dist/ClipPlanes.d.ts.map +1 -1
- package/dist/ClipPlanes.js +11 -15
- package/dist/ClipPlanes.js.map +1 -1
- package/dist/OMEZarrNVImage.d.ts +33 -3
- package/dist/OMEZarrNVImage.d.ts.map +1 -1
- package/dist/OMEZarrNVImage.js +129 -50
- package/dist/OMEZarrNVImage.js.map +1 -1
- package/dist/RegionCoalescer.d.ts.map +1 -1
- package/dist/RegionCoalescer.js +1 -1
- package/dist/RegionCoalescer.js.map +1 -1
- package/dist/ResolutionSelector.d.ts.map +1 -1
- package/dist/ResolutionSelector.js +2 -4
- package/dist/ResolutionSelector.js.map +1 -1
- package/dist/ViewportBounds.d.ts.map +1 -1
- package/dist/ViewportBounds.js +7 -5
- package/dist/ViewportBounds.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -16
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/affine.d.ts +1 -1
- package/dist/utils/affine.d.ts.map +1 -1
- package/dist/utils/affine.js.map +1 -1
- package/dist/utils/coordinates.d.ts +1 -1
- package/dist/utils/coordinates.d.ts.map +1 -1
- package/dist/utils/coordinates.js.map +1 -1
- package/package.json +1 -1
- package/src/BufferManager.ts +45 -45
- package/src/ClipPlanes.ts +131 -130
- package/src/OMEZarrNVImage.ts +685 -606
- package/src/RegionCoalescer.ts +48 -47
- package/src/ResolutionSelector.ts +66 -67
- package/src/ViewportBounds.ts +120 -118
- package/src/events.ts +36 -35
- package/src/index.ts +59 -69
- package/src/types.ts +95 -94
- package/src/utils/affine.ts +65 -65
- package/src/utils/coordinates.ts +70 -70
package/src/ViewportBounds.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
import type { Niivue } from "@niivue/niivue"
|
|
5
|
-
import { SLICE_TYPE } from "@niivue/niivue"
|
|
6
|
-
|
|
4
|
+
import type { Niivue } from "@niivue/niivue"
|
|
5
|
+
import { SLICE_TYPE } from "@niivue/niivue"
|
|
6
|
+
|
|
7
|
+
import type { VolumeBounds } from "./types.js"
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Intersect two axis-aligned bounding boxes.
|
|
@@ -17,12 +18,12 @@ export function intersectBounds(
|
|
|
17
18
|
Math.max(a.min[0], b.min[0]),
|
|
18
19
|
Math.max(a.min[1], b.min[1]),
|
|
19
20
|
Math.max(a.min[2], b.min[2]),
|
|
20
|
-
]
|
|
21
|
+
]
|
|
21
22
|
const max: [number, number, number] = [
|
|
22
23
|
Math.min(a.max[0], b.max[0]),
|
|
23
24
|
Math.min(a.max[1], b.max[1]),
|
|
24
25
|
Math.min(a.max[2], b.max[2]),
|
|
25
|
-
]
|
|
26
|
+
]
|
|
26
27
|
// Ensure valid bounds (max >= min)
|
|
27
28
|
return {
|
|
28
29
|
min: [
|
|
@@ -35,7 +36,7 @@ export function intersectBounds(
|
|
|
35
36
|
Math.max(min[1], max[1]),
|
|
36
37
|
Math.max(min[2], max[2]),
|
|
37
38
|
],
|
|
38
|
-
}
|
|
39
|
+
}
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -56,40 +57,40 @@ export function computeViewportBounds3D(
|
|
|
56
57
|
volumeBounds: VolumeBounds,
|
|
57
58
|
): VolumeBounds {
|
|
58
59
|
// Get scene extents: [min, max, range]
|
|
59
|
-
const extents = nv.sceneExtentsMinMax(true)
|
|
60
|
-
const mn = extents[0]
|
|
61
|
-
const mx = extents[1]
|
|
62
|
-
const range = extents[2]
|
|
60
|
+
const extents = nv.sceneExtentsMinMax(true)
|
|
61
|
+
const mn = extents[0] // vec3
|
|
62
|
+
const mx = extents[1] // vec3
|
|
63
|
+
const range = extents[2] // vec3
|
|
63
64
|
|
|
64
65
|
// Pivot = center of scene
|
|
65
|
-
const pivotX = (mn[0] + mx[0]) * 0.5
|
|
66
|
-
const pivotY = (mn[1] + mx[1]) * 0.5
|
|
67
|
-
const pivotZ = (mn[2] + mx[2]) * 0.5
|
|
66
|
+
const pivotX = (mn[0] + mx[0]) * 0.5
|
|
67
|
+
const pivotY = (mn[1] + mx[1]) * 0.5
|
|
68
|
+
const pivotZ = (mn[2] + mx[2]) * 0.5
|
|
68
69
|
|
|
69
70
|
// furthestFromPivot = half-diagonal of bounding box
|
|
70
|
-
const furthest =
|
|
71
|
-
range[0] * range[0] + range[1] * range[1] + range[2] * range[2]
|
|
72
|
-
|
|
71
|
+
const furthest =
|
|
72
|
+
Math.sqrt(range[0] * range[0] + range[1] * range[1] + range[2] * range[2]) *
|
|
73
|
+
0.5
|
|
73
74
|
|
|
74
75
|
// NiiVue's orthographic scale (matches calculateMvpMatrix)
|
|
75
|
-
const scale = (0.8 * furthest) / (nv.scene.volScaleMultiplier || 1)
|
|
76
|
+
const scale = (0.8 * furthest) / (nv.scene.volScaleMultiplier || 1)
|
|
76
77
|
|
|
77
78
|
// Canvas aspect ratio
|
|
78
|
-
const canvas = nv.canvas
|
|
79
|
-
const canvasW = canvas?.width ?? 1
|
|
80
|
-
const canvasH = canvas?.height ?? 1
|
|
81
|
-
const whratio = canvasW / canvasH
|
|
79
|
+
const canvas = nv.canvas
|
|
80
|
+
const canvasW = canvas?.width ?? 1
|
|
81
|
+
const canvasH = canvas?.height ?? 1
|
|
82
|
+
const whratio = canvasW / canvasH
|
|
82
83
|
|
|
83
84
|
// Ortho extents in view space (before rotation)
|
|
84
|
-
let halfW: number, halfH: number
|
|
85
|
+
let halfW: number, halfH: number
|
|
85
86
|
if (whratio < 1) {
|
|
86
87
|
// Portrait
|
|
87
|
-
halfW = scale
|
|
88
|
-
halfH = scale / whratio
|
|
88
|
+
halfW = scale
|
|
89
|
+
halfH = scale / whratio
|
|
89
90
|
} else {
|
|
90
91
|
// Landscape
|
|
91
|
-
halfW = scale * whratio
|
|
92
|
-
halfH = scale
|
|
92
|
+
halfW = scale * whratio
|
|
93
|
+
halfH = scale
|
|
93
94
|
}
|
|
94
95
|
// For viewport-aware resolution, we need the world-space extent that is
|
|
95
96
|
// visible on screen. Rather than rotating a full 3D frustum box (whose depth
|
|
@@ -105,15 +106,15 @@ export function computeViewportBounds3D(
|
|
|
105
106
|
// NiiVue applies: rotateX(270 - elevation) then rotateZ(azimuth - 180)
|
|
106
107
|
// Also mirrors X (modelMatrix[0] = -1).
|
|
107
108
|
// We need the inverse rotation to go from view space back to world space.
|
|
108
|
-
const azimuth = nv.scene.renderAzimuth ?? 0
|
|
109
|
-
const elevation = nv.scene.renderElevation ?? 0
|
|
110
|
-
const azRad = ((azimuth - 180) * Math.PI) / 180
|
|
111
|
-
const elRad = ((270 - elevation) * Math.PI) / 180
|
|
109
|
+
const azimuth = nv.scene.renderAzimuth ?? 0
|
|
110
|
+
const elevation = nv.scene.renderElevation ?? 0
|
|
111
|
+
const azRad = ((azimuth - 180) * Math.PI) / 180
|
|
112
|
+
const elRad = ((270 - elevation) * Math.PI) / 180
|
|
112
113
|
|
|
113
|
-
const cosAz = Math.cos(azRad)
|
|
114
|
-
const sinAz = Math.sin(azRad)
|
|
115
|
-
const cosEl = Math.cos(elRad)
|
|
116
|
-
const sinEl = Math.sin(elRad)
|
|
114
|
+
const cosAz = Math.cos(azRad)
|
|
115
|
+
const sinAz = Math.sin(azRad)
|
|
116
|
+
const cosEl = Math.cos(elRad)
|
|
117
|
+
const sinEl = Math.sin(elRad)
|
|
117
118
|
|
|
118
119
|
// Compute inverse rotation of unit view-space axes to world space.
|
|
119
120
|
// Inverse: un-mirror X, then rotateZ(-azRad), then rotateX(-elRad).
|
|
@@ -123,28 +124,29 @@ export function computeViewportBounds3D(
|
|
|
123
124
|
-cosAz, // wx
|
|
124
125
|
sinAz * cosEl, // wy
|
|
125
126
|
-sinAz * sinEl, // wz
|
|
126
|
-
]
|
|
127
|
+
]
|
|
127
128
|
// View Y axis (0, 1, 0) — no mirror effect
|
|
128
129
|
const viewYinWorld: [number, number, number] = [
|
|
129
130
|
sinAz, // wx
|
|
130
131
|
cosAz * cosEl, // wy
|
|
131
132
|
-cosAz * sinEl, // wz
|
|
132
|
-
]
|
|
133
|
+
]
|
|
133
134
|
// View Z axis (0, 0, 1) — no mirror effect
|
|
134
135
|
const viewZinWorld: [number, number, number] = [
|
|
135
136
|
0, // wx
|
|
136
137
|
sinEl, // wy
|
|
137
138
|
cosEl, // wz
|
|
138
|
-
]
|
|
139
|
+
]
|
|
139
140
|
|
|
140
141
|
// For each world axis, the visible half-extent is:
|
|
141
142
|
// halfW * |viewXinWorld[axis]| + halfH * |viewYinWorld[axis]|
|
|
142
143
|
// + furthest * |viewZinWorld[axis]| (full volume depth along view Z)
|
|
143
|
-
const worldHalfExtent: [number, number, number] = [0, 0, 0]
|
|
144
|
+
const worldHalfExtent: [number, number, number] = [0, 0, 0]
|
|
144
145
|
for (let axis = 0; axis < 3; axis++) {
|
|
145
|
-
worldHalfExtent[axis] =
|
|
146
|
+
worldHalfExtent[axis] =
|
|
147
|
+
halfW * Math.abs(viewXinWorld[axis]) +
|
|
146
148
|
halfH * Math.abs(viewYinWorld[axis]) +
|
|
147
|
-
furthest * Math.abs(viewZinWorld[axis])
|
|
149
|
+
furthest * Math.abs(viewZinWorld[axis])
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
const frustumBounds: VolumeBounds = {
|
|
@@ -158,9 +160,9 @@ export function computeViewportBounds3D(
|
|
|
158
160
|
pivotY + worldHalfExtent[1],
|
|
159
161
|
pivotZ + worldHalfExtent[2],
|
|
160
162
|
],
|
|
161
|
-
}
|
|
163
|
+
}
|
|
162
164
|
|
|
163
|
-
return intersectBounds(frustumBounds, volumeBounds)
|
|
165
|
+
return intersectBounds(frustumBounds, volumeBounds)
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
/**
|
|
@@ -203,97 +205,97 @@ export function computeViewportBounds2D(
|
|
|
203
205
|
volumeBounds.min[0] * normalizationScale,
|
|
204
206
|
volumeBounds.min[1] * normalizationScale,
|
|
205
207
|
volumeBounds.min[2] * normalizationScale,
|
|
206
|
-
]
|
|
208
|
+
]
|
|
207
209
|
const normMax: [number, number, number] = [
|
|
208
210
|
volumeBounds.max[0] * normalizationScale,
|
|
209
211
|
volumeBounds.max[1] * normalizationScale,
|
|
210
212
|
volumeBounds.max[2] * normalizationScale,
|
|
211
|
-
]
|
|
213
|
+
]
|
|
212
214
|
|
|
213
215
|
// Swizzle to screen axes (same mapping as NiiVue's swizzleVec3MM):
|
|
214
216
|
// AXIAL: screen X = mm X, screen Y = mm Y
|
|
215
217
|
// CORONAL: screen X = mm X, screen Y = mm Z
|
|
216
218
|
// SAGITTAL: screen X = mm Y, screen Y = mm Z
|
|
217
|
-
let mnMM0: number, mxMM0: number, mnMM1: number, mxMM1: number
|
|
219
|
+
let mnMM0: number, mxMM0: number, mnMM1: number, mxMM1: number
|
|
218
220
|
switch (sliceType) {
|
|
219
221
|
case SLICE_TYPE.CORONAL:
|
|
220
|
-
mnMM0 = normMin[0]
|
|
221
|
-
mxMM0 = normMax[0]
|
|
222
|
-
mnMM1 = normMin[2]
|
|
223
|
-
mxMM1 = normMax[2]
|
|
224
|
-
break
|
|
222
|
+
mnMM0 = normMin[0]
|
|
223
|
+
mxMM0 = normMax[0] // screen X = mm X
|
|
224
|
+
mnMM1 = normMin[2]
|
|
225
|
+
mxMM1 = normMax[2] // screen Y = mm Z
|
|
226
|
+
break
|
|
225
227
|
case SLICE_TYPE.SAGITTAL:
|
|
226
|
-
mnMM0 = normMin[1]
|
|
227
|
-
mxMM0 = normMax[1]
|
|
228
|
-
mnMM1 = normMin[2]
|
|
229
|
-
mxMM1 = normMax[2]
|
|
230
|
-
break
|
|
228
|
+
mnMM0 = normMin[1]
|
|
229
|
+
mxMM0 = normMax[1] // screen X = mm Y
|
|
230
|
+
mnMM1 = normMin[2]
|
|
231
|
+
mxMM1 = normMax[2] // screen Y = mm Z
|
|
232
|
+
break
|
|
231
233
|
default: // AXIAL
|
|
232
|
-
mnMM0 = normMin[0]
|
|
233
|
-
mxMM0 = normMax[0]
|
|
234
|
-
mnMM1 = normMin[1]
|
|
235
|
-
mxMM1 = normMax[1]
|
|
236
|
-
break
|
|
234
|
+
mnMM0 = normMin[0]
|
|
235
|
+
mxMM0 = normMax[0] // screen X = mm X
|
|
236
|
+
mnMM1 = normMin[1]
|
|
237
|
+
mxMM1 = normMax[1] // screen Y = mm Y
|
|
238
|
+
break
|
|
237
239
|
}
|
|
238
240
|
|
|
239
241
|
// Account for canvas aspect ratio stretching (matches draw2DMain logic)
|
|
240
242
|
// NiiVue stretches the FOV to fill the canvas while preserving aspect ratio
|
|
241
|
-
const canvas = nv.canvas
|
|
243
|
+
const canvas = nv.canvas
|
|
242
244
|
if (canvas) {
|
|
243
|
-
const canvasW = canvas.width || 1
|
|
244
|
-
const canvasH = canvas.height || 1
|
|
245
|
-
const fovW = Math.abs(mxMM0 - mnMM0)
|
|
246
|
-
const fovH = Math.abs(mxMM1 - mnMM1)
|
|
245
|
+
const canvasW = canvas.width || 1
|
|
246
|
+
const canvasH = canvas.height || 1
|
|
247
|
+
const fovW = Math.abs(mxMM0 - mnMM0)
|
|
248
|
+
const fovH = Math.abs(mxMM1 - mnMM1)
|
|
247
249
|
if (fovW > 0 && fovH > 0) {
|
|
248
|
-
const canvasAspect = canvasW / canvasH
|
|
249
|
-
const fovAspect = fovW / fovH
|
|
250
|
+
const canvasAspect = canvasW / canvasH
|
|
251
|
+
const fovAspect = fovW / fovH
|
|
250
252
|
if (canvasAspect > fovAspect) {
|
|
251
253
|
// Canvas is wider than FOV: expand X
|
|
252
|
-
const midX = (mnMM0 + mxMM0) * 0.5
|
|
253
|
-
const newHalfW =
|
|
254
|
-
mnMM0 = midX - newHalfW
|
|
255
|
-
mxMM0 = midX + newHalfW
|
|
254
|
+
const midX = (mnMM0 + mxMM0) * 0.5
|
|
255
|
+
const newHalfW = fovH * canvasAspect * 0.5
|
|
256
|
+
mnMM0 = midX - newHalfW
|
|
257
|
+
mxMM0 = midX + newHalfW
|
|
256
258
|
} else {
|
|
257
259
|
// Canvas is taller than FOV: expand Y
|
|
258
|
-
const midY = (mnMM1 + mxMM1) * 0.5
|
|
259
|
-
const newHalfH = (fovW / canvasAspect) * 0.5
|
|
260
|
-
mnMM1 = midY - newHalfH
|
|
261
|
-
mxMM1 = midY + newHalfH
|
|
260
|
+
const midY = (mnMM1 + mxMM1) * 0.5
|
|
261
|
+
const newHalfH = (fovW / canvasAspect) * 0.5
|
|
262
|
+
mnMM1 = midY - newHalfH
|
|
263
|
+
mxMM1 = midY + newHalfH
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
// Apply pan and zoom (matching NiiVue's draw2DMain logic)
|
|
267
|
-
const pan = nv.scene.pan2Dxyzmm
|
|
269
|
+
const pan = nv.scene.pan2Dxyzmm // vec4: [panX, panY, panZ, zoom]
|
|
268
270
|
// Swizzle the pan vector to match the current orientation
|
|
269
271
|
const panSwizzled = nv.swizzleVec3MM(
|
|
270
272
|
[pan[0], pan[1], pan[2]] as unknown as import("gl-matrix").vec3,
|
|
271
273
|
sliceType,
|
|
272
|
-
)
|
|
273
|
-
const zoom = pan[3] || 1
|
|
274
|
+
)
|
|
275
|
+
const zoom = pan[3] || 1
|
|
274
276
|
|
|
275
277
|
// Apply pan: shift visible window
|
|
276
|
-
mnMM0 -= panSwizzled[0]
|
|
277
|
-
mxMM0 -= panSwizzled[0]
|
|
278
|
-
mnMM1 -= panSwizzled[1]
|
|
279
|
-
mxMM1 -= panSwizzled[1]
|
|
278
|
+
mnMM0 -= panSwizzled[0]
|
|
279
|
+
mxMM0 -= panSwizzled[0]
|
|
280
|
+
mnMM1 -= panSwizzled[1]
|
|
281
|
+
mxMM1 -= panSwizzled[1]
|
|
280
282
|
|
|
281
283
|
// Apply zoom: divide by zoom factor (zoom > 1 = zoomed in = smaller FOV)
|
|
282
|
-
mnMM0 /= zoom
|
|
283
|
-
mxMM0 /= zoom
|
|
284
|
-
mnMM1 /= zoom
|
|
285
|
-
mxMM1 /= zoom
|
|
284
|
+
mnMM0 /= zoom
|
|
285
|
+
mxMM0 /= zoom
|
|
286
|
+
mnMM1 /= zoom
|
|
287
|
+
mxMM1 /= zoom
|
|
286
288
|
|
|
287
289
|
// Convert from NiiVue's mm space back to physical world coordinates.
|
|
288
290
|
// If the slab affine was normalized (multiplied by normalizationScale),
|
|
289
291
|
// NiiVue's mm values are world * normalizationScale. Dividing by the
|
|
290
292
|
// normalization scale recovers physical coordinates.
|
|
291
293
|
if (normalizationScale !== 1.0 && normalizationScale > 0) {
|
|
292
|
-
const invNorm = 1.0 / normalizationScale
|
|
293
|
-
mnMM0 *= invNorm
|
|
294
|
-
mxMM0 *= invNorm
|
|
295
|
-
mnMM1 *= invNorm
|
|
296
|
-
mxMM1 *= invNorm
|
|
294
|
+
const invNorm = 1.0 / normalizationScale
|
|
295
|
+
mnMM0 *= invNorm
|
|
296
|
+
mxMM0 *= invNorm
|
|
297
|
+
mnMM1 *= invNorm
|
|
298
|
+
mxMM1 *= invNorm
|
|
297
299
|
}
|
|
298
300
|
|
|
299
301
|
// Now un-swizzle back to RAS world coordinates.
|
|
@@ -306,42 +308,42 @@ export function computeViewportBounds2D(
|
|
|
306
308
|
const result: VolumeBounds = {
|
|
307
309
|
min: [...volumeBounds.min],
|
|
308
310
|
max: [...volumeBounds.max],
|
|
309
|
-
}
|
|
311
|
+
}
|
|
310
312
|
|
|
311
313
|
// Ensure min < max for each swizzled axis
|
|
312
|
-
const visMin0 = Math.min(mnMM0, mxMM0)
|
|
313
|
-
const visMax0 = Math.max(mnMM0, mxMM0)
|
|
314
|
-
const visMin1 = Math.min(mnMM1, mxMM1)
|
|
315
|
-
const visMax1 = Math.max(mnMM1, mxMM1)
|
|
314
|
+
const visMin0 = Math.min(mnMM0, mxMM0)
|
|
315
|
+
const visMax0 = Math.max(mnMM0, mxMM0)
|
|
316
|
+
const visMin1 = Math.min(mnMM1, mxMM1)
|
|
317
|
+
const visMax1 = Math.max(mnMM1, mxMM1)
|
|
316
318
|
|
|
317
319
|
switch (sliceType) {
|
|
318
320
|
case SLICE_TYPE.AXIAL:
|
|
319
321
|
// screen X = world X (R/L), screen Y = world Y (A/P)
|
|
320
|
-
result.min[0] = visMin0
|
|
321
|
-
result.max[0] = visMax0
|
|
322
|
-
result.min[1] = visMin1
|
|
323
|
-
result.max[1] = visMax1
|
|
322
|
+
result.min[0] = visMin0
|
|
323
|
+
result.max[0] = visMax0
|
|
324
|
+
result.min[1] = visMin1
|
|
325
|
+
result.max[1] = visMax1
|
|
324
326
|
// Z (S/I) = full extent (orthogonal axis)
|
|
325
|
-
break
|
|
327
|
+
break
|
|
326
328
|
case SLICE_TYPE.CORONAL:
|
|
327
329
|
// screen X = world X (R/L), screen Y = world Z (S/I)
|
|
328
|
-
result.min[0] = visMin0
|
|
329
|
-
result.max[0] = visMax0
|
|
330
|
-
result.min[2] = visMin1
|
|
331
|
-
result.max[2] = visMax1
|
|
330
|
+
result.min[0] = visMin0
|
|
331
|
+
result.max[0] = visMax0
|
|
332
|
+
result.min[2] = visMin1
|
|
333
|
+
result.max[2] = visMax1
|
|
332
334
|
// Y (A/P) = full extent (orthogonal axis)
|
|
333
|
-
break
|
|
335
|
+
break
|
|
334
336
|
case SLICE_TYPE.SAGITTAL:
|
|
335
337
|
// screen X = world Y (A/P), screen Y = world Z (S/I)
|
|
336
|
-
result.min[1] = visMin0
|
|
337
|
-
result.max[1] = visMax0
|
|
338
|
-
result.min[2] = visMin1
|
|
339
|
-
result.max[2] = visMax1
|
|
338
|
+
result.min[1] = visMin0
|
|
339
|
+
result.max[1] = visMax0
|
|
340
|
+
result.min[2] = visMin1
|
|
341
|
+
result.max[2] = visMax1
|
|
340
342
|
// X (R/L) = full extent (orthogonal axis)
|
|
341
|
-
break
|
|
343
|
+
break
|
|
342
344
|
}
|
|
343
345
|
|
|
344
|
-
return intersectBounds(result, volumeBounds)
|
|
346
|
+
return intersectBounds(result, volumeBounds)
|
|
345
347
|
}
|
|
346
348
|
|
|
347
349
|
/**
|
|
@@ -358,12 +360,12 @@ export function boundsApproxEqual(
|
|
|
358
360
|
tolerance: number = 0.01,
|
|
359
361
|
): boolean {
|
|
360
362
|
for (let i = 0; i < 3; i++) {
|
|
361
|
-
const rangeA = a.max[i] - a.min[i]
|
|
362
|
-
const rangeB = b.max[i] - b.min[i]
|
|
363
|
-
const maxRange = Math.max(Math.abs(rangeA), Math.abs(rangeB), 1e-10)
|
|
363
|
+
const rangeA = a.max[i] - a.min[i]
|
|
364
|
+
const rangeB = b.max[i] - b.min[i]
|
|
365
|
+
const maxRange = Math.max(Math.abs(rangeA), Math.abs(rangeB), 1e-10)
|
|
364
366
|
|
|
365
|
-
if (Math.abs(a.min[i] - b.min[i]) / maxRange > tolerance) return false
|
|
366
|
-
if (Math.abs(a.max[i] - b.max[i]) / maxRange > tolerance) return false
|
|
367
|
+
if (Math.abs(a.min[i] - b.min[i]) / maxRange > tolerance) return false
|
|
368
|
+
if (Math.abs(a.max[i] - b.max[i]) / maxRange > tolerance) return false
|
|
367
369
|
}
|
|
368
|
-
return true
|
|
370
|
+
return true
|
|
369
371
|
}
|
package/src/events.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
import type { SLICE_TYPE } from "@niivue/niivue"
|
|
5
|
-
|
|
4
|
+
import type { SLICE_TYPE } from "@niivue/niivue"
|
|
5
|
+
|
|
6
|
+
import type { ClipPlanes } from "./types.js"
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Identifies what triggered a volume population.
|
|
@@ -12,7 +13,7 @@ export type PopulateTrigger =
|
|
|
12
13
|
| "initial" // First load or reload with new settings
|
|
13
14
|
| "clipPlanesChanged" // Clip planes were modified
|
|
14
15
|
| "sliceChanged" // Slice position changed (slab reload)
|
|
15
|
-
| "viewportChanged"
|
|
16
|
+
| "viewportChanged" // Viewport pan/zoom/rotation changed
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Type-safe event map for OMEZarrNVImage events.
|
|
@@ -43,15 +44,15 @@ export type PopulateTrigger =
|
|
|
43
44
|
export interface OMEZarrNVImageEventMap {
|
|
44
45
|
/** Fired when loading starts for a resolution level */
|
|
45
46
|
loadingStart: {
|
|
46
|
-
levelIndex: number
|
|
47
|
-
trigger: PopulateTrigger
|
|
48
|
-
}
|
|
47
|
+
levelIndex: number
|
|
48
|
+
trigger: PopulateTrigger
|
|
49
|
+
}
|
|
49
50
|
|
|
50
51
|
/** Fired when loading completes for a resolution level */
|
|
51
52
|
loadingComplete: {
|
|
52
|
-
levelIndex: number
|
|
53
|
-
trigger: PopulateTrigger
|
|
54
|
-
}
|
|
53
|
+
levelIndex: number
|
|
54
|
+
trigger: PopulateTrigger
|
|
55
|
+
}
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* Fired when resolution level changes.
|
|
@@ -59,27 +60,27 @@ export interface OMEZarrNVImageEventMap {
|
|
|
59
60
|
* cause a resolution change.
|
|
60
61
|
*/
|
|
61
62
|
resolutionChange: {
|
|
62
|
-
currentLevel: number
|
|
63
|
-
targetLevel: number
|
|
64
|
-
previousLevel: number
|
|
65
|
-
trigger: PopulateTrigger
|
|
66
|
-
}
|
|
63
|
+
currentLevel: number
|
|
64
|
+
targetLevel: number
|
|
65
|
+
previousLevel: number
|
|
66
|
+
trigger: PopulateTrigger
|
|
67
|
+
}
|
|
67
68
|
|
|
68
69
|
/**
|
|
69
70
|
* Fired when clip planes are updated (after debounce).
|
|
70
71
|
* This is emitted after the debounce delay, not on every slider movement.
|
|
71
72
|
*/
|
|
72
|
-
clipPlanesChange: { clipPlanes: ClipPlanes }
|
|
73
|
+
clipPlanesChange: { clipPlanes: ClipPlanes }
|
|
73
74
|
|
|
74
75
|
/**
|
|
75
76
|
* Fired when populateVolume() completes and no more requests are queued.
|
|
76
77
|
* This is the final event after all loading is done.
|
|
77
78
|
*/
|
|
78
79
|
populateComplete: {
|
|
79
|
-
currentLevel: number
|
|
80
|
-
targetLevel: number
|
|
81
|
-
trigger: PopulateTrigger
|
|
82
|
-
}
|
|
80
|
+
currentLevel: number
|
|
81
|
+
targetLevel: number
|
|
82
|
+
trigger: PopulateTrigger
|
|
83
|
+
}
|
|
83
84
|
|
|
84
85
|
/**
|
|
85
86
|
* Fired when a queued load request is replaced by a newer one.
|
|
@@ -87,30 +88,30 @@ export interface OMEZarrNVImageEventMap {
|
|
|
87
88
|
* the first request is queued.
|
|
88
89
|
*/
|
|
89
90
|
loadingSkipped: {
|
|
90
|
-
reason: "queued-replaced"
|
|
91
|
-
trigger: PopulateTrigger
|
|
92
|
-
}
|
|
91
|
+
reason: "queued-replaced"
|
|
92
|
+
trigger: PopulateTrigger
|
|
93
|
+
}
|
|
93
94
|
|
|
94
95
|
/**
|
|
95
96
|
* Fired when a slab (2D slice buffer) finishes loading.
|
|
96
97
|
* This event is specific to slab-based loading for 2D slice views.
|
|
97
98
|
*/
|
|
98
99
|
slabLoadingComplete: {
|
|
99
|
-
sliceType: SLICE_TYPE
|
|
100
|
-
levelIndex: number
|
|
101
|
-
slabStart: number
|
|
102
|
-
slabEnd: number
|
|
103
|
-
trigger: PopulateTrigger
|
|
104
|
-
}
|
|
100
|
+
sliceType: SLICE_TYPE
|
|
101
|
+
levelIndex: number
|
|
102
|
+
slabStart: number
|
|
103
|
+
slabEnd: number
|
|
104
|
+
trigger: PopulateTrigger
|
|
105
|
+
}
|
|
105
106
|
|
|
106
107
|
/**
|
|
107
108
|
* Fired when a slab starts loading.
|
|
108
109
|
*/
|
|
109
110
|
slabLoadingStart: {
|
|
110
|
-
sliceType: SLICE_TYPE
|
|
111
|
-
levelIndex: number
|
|
112
|
-
trigger: PopulateTrigger
|
|
113
|
-
}
|
|
111
|
+
sliceType: SLICE_TYPE
|
|
112
|
+
levelIndex: number
|
|
113
|
+
trigger: PopulateTrigger
|
|
114
|
+
}
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
/**
|
|
@@ -121,7 +122,7 @@ export class OMEZarrNVImageEvent<
|
|
|
121
122
|
K extends keyof OMEZarrNVImageEventMap,
|
|
122
123
|
> extends CustomEvent<OMEZarrNVImageEventMap[K]> {
|
|
123
124
|
constructor(type: K, detail: OMEZarrNVImageEventMap[K]) {
|
|
124
|
-
super(type, { detail })
|
|
125
|
+
super(type, { detail })
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
|
|
@@ -131,7 +132,7 @@ export class OMEZarrNVImageEvent<
|
|
|
131
132
|
*/
|
|
132
133
|
export type OMEZarrNVImageEventListener<
|
|
133
134
|
K extends keyof OMEZarrNVImageEventMap,
|
|
134
|
-
> = (event: OMEZarrNVImageEvent<K>) => void | Promise<void
|
|
135
|
+
> = (event: OMEZarrNVImageEvent<K>) => void | Promise<void>
|
|
135
136
|
|
|
136
137
|
/**
|
|
137
138
|
* Options for addEventListener/removeEventListener.
|
|
@@ -143,4 +144,4 @@ export type OMEZarrNVImageEventListener<
|
|
|
143
144
|
*/
|
|
144
145
|
export type OMEZarrNVImageEventListenerOptions =
|
|
145
146
|
| boolean
|
|
146
|
-
| AddEventListenerOptions
|
|
147
|
+
| AddEventListenerOptions
|