@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/ClipPlanes.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) Fideus Labs LLC
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
import type { Multiscales, NgffImage } from "@fideus-labs/ngff-zarr"
|
|
4
|
+
import type { Multiscales, NgffImage } from "@fideus-labs/ngff-zarr"
|
|
5
|
+
|
|
6
|
+
import { getChunkShape, getVolumeShape } from "./ResolutionSelector.js"
|
|
5
7
|
import type {
|
|
6
8
|
ChunkAlignedRegion,
|
|
7
9
|
ClipPlane,
|
|
8
10
|
ClipPlanes,
|
|
9
11
|
PixelRegion,
|
|
10
12
|
VolumeBounds,
|
|
11
|
-
} from "./types.js"
|
|
12
|
-
import { pixelToWorld, worldToPixel } from "./utils/coordinates.js"
|
|
13
|
-
import { getChunkShape, getVolumeShape } from "./ResolutionSelector.js";
|
|
13
|
+
} from "./types.js"
|
|
14
|
+
import { pixelToWorld, worldToPixel } from "./utils/coordinates.js"
|
|
14
15
|
|
|
15
16
|
/** Maximum number of clip planes supported by NiiVue */
|
|
16
|
-
export const MAX_CLIP_PLANES = 6
|
|
17
|
+
export const MAX_CLIP_PLANES = 6
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Normalize a 3D vector to unit length.
|
|
@@ -25,11 +26,11 @@ export const MAX_CLIP_PLANES = 6;
|
|
|
25
26
|
export function normalizeVector(
|
|
26
27
|
v: [number, number, number],
|
|
27
28
|
): [number, number, number] {
|
|
28
|
-
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
|
|
29
|
+
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
|
|
29
30
|
if (length === 0) {
|
|
30
|
-
throw new Error("Cannot normalize zero-length vector")
|
|
31
|
+
throw new Error("Cannot normalize zero-length vector")
|
|
31
32
|
}
|
|
32
|
-
return [v[0] / length, v[1] / length, v[2] / length]
|
|
33
|
+
return [v[0] / length, v[1] / length, v[2] / length]
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -47,7 +48,7 @@ export function createClipPlane(
|
|
|
47
48
|
return {
|
|
48
49
|
point: [...point],
|
|
49
50
|
normal: normalizeVector(normal),
|
|
50
|
-
}
|
|
51
|
+
}
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/**
|
|
@@ -57,7 +58,7 @@ export function createClipPlane(
|
|
|
57
58
|
* @returns Empty ClipPlanes array
|
|
58
59
|
*/
|
|
59
60
|
export function createDefaultClipPlanes(_multiscales: Multiscales): ClipPlanes {
|
|
60
|
-
return []
|
|
61
|
+
return []
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
|
@@ -70,14 +71,14 @@ export function getVolumeBoundsFromMultiscales(
|
|
|
70
71
|
multiscales: Multiscales,
|
|
71
72
|
): VolumeBounds {
|
|
72
73
|
// Use highest resolution for most accurate bounds
|
|
73
|
-
const image = multiscales.images[0]
|
|
74
|
-
const shape = getVolumeShape(image)
|
|
74
|
+
const image = multiscales.images[0]
|
|
75
|
+
const shape = getVolumeShape(image)
|
|
75
76
|
|
|
76
|
-
const minPixel: [number, number, number] = [0, 0, 0]
|
|
77
|
-
const maxPixel: [number, number, number] = [shape[0], shape[1], shape[2]]
|
|
77
|
+
const minPixel: [number, number, number] = [0, 0, 0]
|
|
78
|
+
const maxPixel: [number, number, number] = [shape[0], shape[1], shape[2]]
|
|
78
79
|
|
|
79
|
-
const minWorld = pixelToWorld(minPixel, image)
|
|
80
|
-
const maxWorld = pixelToWorld(maxPixel, image)
|
|
80
|
+
const minWorld = pixelToWorld(minPixel, image)
|
|
81
|
+
const maxWorld = pixelToWorld(maxPixel, image)
|
|
81
82
|
|
|
82
83
|
return {
|
|
83
84
|
min: [
|
|
@@ -90,7 +91,7 @@ export function getVolumeBoundsFromMultiscales(
|
|
|
90
91
|
Math.max(minWorld[1], maxWorld[1]),
|
|
91
92
|
Math.max(minWorld[2], maxWorld[2]),
|
|
92
93
|
],
|
|
93
|
-
}
|
|
94
|
+
}
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
/**
|
|
@@ -103,22 +104,23 @@ export function getVolumeBoundsFromMultiscales(
|
|
|
103
104
|
* @param normal - Unit normal vector [x, y, z]
|
|
104
105
|
* @returns Object with azimuth and elevation in degrees
|
|
105
106
|
*/
|
|
106
|
-
export function normalToAzimuthElevation(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
export function normalToAzimuthElevation(normal: [number, number, number]): {
|
|
108
|
+
azimuth: number
|
|
109
|
+
elevation: number
|
|
110
|
+
} {
|
|
111
|
+
const [x, y, z] = normal
|
|
110
112
|
|
|
111
113
|
// Elevation: angle from XY plane (arcsin of z component)
|
|
112
114
|
// Clamp to [-1, 1] to handle floating point errors
|
|
113
|
-
const elevation = Math.asin(Math.max(-1, Math.min(1, z))) * (180 / Math.PI)
|
|
115
|
+
const elevation = Math.asin(Math.max(-1, Math.min(1, z))) * (180 / Math.PI)
|
|
114
116
|
|
|
115
117
|
// Azimuth: angle in XY plane from +Y axis
|
|
116
118
|
// atan2(x, y) gives angle from +Y, which matches NiiVue's azimuth=0 = posterior (+Y)
|
|
117
|
-
let azimuth = Math.atan2(x, y) * (180 / Math.PI)
|
|
119
|
+
let azimuth = Math.atan2(x, y) * (180 / Math.PI)
|
|
118
120
|
// Normalize to [0, 360)
|
|
119
|
-
azimuth = ((azimuth % 360) + 360) % 360
|
|
121
|
+
azimuth = ((azimuth % 360) + 360) % 360
|
|
120
122
|
|
|
121
|
-
return { azimuth, elevation }
|
|
123
|
+
return { azimuth, elevation }
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
/**
|
|
@@ -132,15 +134,15 @@ export function azimuthElevationToNormal(
|
|
|
132
134
|
azimuth: number,
|
|
133
135
|
elevation: number,
|
|
134
136
|
): [number, number, number] {
|
|
135
|
-
const azRad = (azimuth * Math.PI) / 180
|
|
136
|
-
const elRad = (elevation * Math.PI) / 180
|
|
137
|
+
const azRad = (azimuth * Math.PI) / 180
|
|
138
|
+
const elRad = (elevation * Math.PI) / 180
|
|
137
139
|
|
|
138
|
-
const cosEl = Math.cos(elRad)
|
|
139
|
-
const x = cosEl * Math.sin(azRad)
|
|
140
|
-
const y = cosEl * Math.cos(azRad)
|
|
141
|
-
const z = Math.sin(elRad)
|
|
140
|
+
const cosEl = Math.cos(elRad)
|
|
141
|
+
const x = cosEl * Math.sin(azRad)
|
|
142
|
+
const y = cosEl * Math.cos(azRad)
|
|
143
|
+
const z = Math.sin(elRad)
|
|
142
144
|
|
|
143
|
-
return [x, y, z]
|
|
145
|
+
return [x, y, z]
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
/**
|
|
@@ -159,41 +161,43 @@ export function calculateNiivueDepth(
|
|
|
159
161
|
plane: ClipPlane,
|
|
160
162
|
volumeBounds: VolumeBounds,
|
|
161
163
|
): number {
|
|
162
|
-
const { min, max } = volumeBounds
|
|
164
|
+
const { min, max } = volumeBounds
|
|
163
165
|
|
|
164
166
|
// Volume center
|
|
165
167
|
const center: [number, number, number] = [
|
|
166
168
|
(min[0] + max[0]) / 2,
|
|
167
169
|
(min[1] + max[1]) / 2,
|
|
168
170
|
(min[2] + max[2]) / 2,
|
|
169
|
-
]
|
|
171
|
+
]
|
|
170
172
|
|
|
171
173
|
// Volume extent
|
|
172
174
|
const extent: [number, number, number] = [
|
|
173
175
|
max[0] - min[0],
|
|
174
176
|
max[1] - min[1],
|
|
175
177
|
max[2] - min[2],
|
|
176
|
-
]
|
|
178
|
+
]
|
|
177
179
|
|
|
178
180
|
// Signed distance from center to plane along normal
|
|
179
|
-
const { point, normal } = plane
|
|
180
|
-
const signedDistance =
|
|
181
|
+
const { point, normal } = plane
|
|
182
|
+
const signedDistance =
|
|
183
|
+
normal[0] * (point[0] - center[0]) +
|
|
181
184
|
normal[1] * (point[1] - center[1]) +
|
|
182
|
-
normal[2] * (point[2] - center[2])
|
|
185
|
+
normal[2] * (point[2] - center[2])
|
|
183
186
|
|
|
184
187
|
// Full extent along normal direction (using absolute value of each component)
|
|
185
188
|
// This is the "width" of the bounding box when projected onto the normal direction
|
|
186
|
-
const extentAlongNormal =
|
|
189
|
+
const extentAlongNormal =
|
|
190
|
+
Math.abs(normal[0]) * extent[0] +
|
|
187
191
|
Math.abs(normal[1]) * extent[1] +
|
|
188
|
-
Math.abs(normal[2]) * extent[2]
|
|
192
|
+
Math.abs(normal[2]) * extent[2]
|
|
189
193
|
|
|
190
194
|
// Avoid division by zero
|
|
191
195
|
if (extentAlongNormal === 0) {
|
|
192
|
-
return 0
|
|
196
|
+
return 0
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
// Normalize to NiiVue's coordinate system where volume spans -0.5 to 0.5 from center
|
|
196
|
-
return signedDistance / extentAlongNormal
|
|
200
|
+
return signedDistance / extentAlongNormal
|
|
197
201
|
}
|
|
198
202
|
|
|
199
203
|
/**
|
|
@@ -221,7 +225,7 @@ export function clipPlaneToNiivue(
|
|
|
221
225
|
plane: ClipPlane,
|
|
222
226
|
volumeBounds: VolumeBounds,
|
|
223
227
|
): [number, number, number] {
|
|
224
|
-
const depth = calculateNiivueDepth(plane, volumeBounds)
|
|
228
|
+
const depth = calculateNiivueDepth(plane, volumeBounds)
|
|
225
229
|
|
|
226
230
|
// Negate the normal for azimuth/elevation calculation.
|
|
227
231
|
// After NiiVue adds 180° to azimuth, the shader will see our original normal.
|
|
@@ -229,14 +233,14 @@ export function clipPlaneToNiivue(
|
|
|
229
233
|
-plane.normal[0],
|
|
230
234
|
-plane.normal[1],
|
|
231
235
|
-plane.normal[2],
|
|
232
|
-
]
|
|
233
|
-
const { azimuth, elevation } = normalToAzimuthElevation(negatedNormal)
|
|
236
|
+
]
|
|
237
|
+
const { azimuth, elevation } = normalToAzimuthElevation(negatedNormal)
|
|
234
238
|
|
|
235
239
|
// Also negate the depth to be consistent with the flipped normal.
|
|
236
240
|
// The plane equation dot(n, p-center) + d = 0 changes sign when n is negated.
|
|
237
|
-
const negatedDepth = -depth
|
|
241
|
+
const negatedDepth = -depth
|
|
238
242
|
|
|
239
|
-
return [negatedDepth, azimuth, elevation]
|
|
243
|
+
return [negatedDepth, azimuth, elevation]
|
|
240
244
|
}
|
|
241
245
|
|
|
242
246
|
/**
|
|
@@ -250,7 +254,7 @@ export function clipPlanesToNiivue(
|
|
|
250
254
|
clipPlanes: ClipPlanes,
|
|
251
255
|
volumeBounds: VolumeBounds,
|
|
252
256
|
): number[][] {
|
|
253
|
-
return clipPlanes.map((plane) => clipPlaneToNiivue(plane, volumeBounds))
|
|
257
|
+
return clipPlanes.map((plane) => clipPlaneToNiivue(plane, volumeBounds))
|
|
254
258
|
}
|
|
255
259
|
|
|
256
260
|
/**
|
|
@@ -267,12 +271,12 @@ export function pointToPlaneDistance(
|
|
|
267
271
|
testPoint: [number, number, number],
|
|
268
272
|
plane: ClipPlane,
|
|
269
273
|
): number {
|
|
270
|
-
const { point, normal } = plane
|
|
274
|
+
const { point, normal } = plane
|
|
271
275
|
return (
|
|
272
276
|
normal[0] * (testPoint[0] - point[0]) +
|
|
273
277
|
normal[1] * (testPoint[1] - point[1]) +
|
|
274
278
|
normal[2] * (testPoint[2] - point[2])
|
|
275
|
-
)
|
|
279
|
+
)
|
|
276
280
|
}
|
|
277
281
|
|
|
278
282
|
/**
|
|
@@ -288,10 +292,10 @@ export function isInsideClipPlanes(
|
|
|
288
292
|
): boolean {
|
|
289
293
|
for (const plane of clipPlanes) {
|
|
290
294
|
if (pointToPlaneDistance(worldCoord, plane) < 0) {
|
|
291
|
-
return false
|
|
295
|
+
return false
|
|
292
296
|
}
|
|
293
297
|
}
|
|
294
|
-
return true
|
|
298
|
+
return true
|
|
295
299
|
}
|
|
296
300
|
|
|
297
301
|
/**
|
|
@@ -315,58 +319,62 @@ export function clipPlanesToBoundingBox(
|
|
|
315
319
|
return {
|
|
316
320
|
min: [...volumeBounds.min],
|
|
317
321
|
max: [...volumeBounds.max],
|
|
318
|
-
}
|
|
322
|
+
}
|
|
319
323
|
}
|
|
320
324
|
|
|
321
325
|
// Start with full volume bounds
|
|
322
|
-
let minX = volumeBounds.min[0]
|
|
323
|
-
let maxX = volumeBounds.max[0]
|
|
324
|
-
let minY = volumeBounds.min[1]
|
|
325
|
-
let maxY = volumeBounds.max[1]
|
|
326
|
-
let minZ = volumeBounds.min[2]
|
|
327
|
-
let maxZ = volumeBounds.max[2]
|
|
326
|
+
let minX = volumeBounds.min[0]
|
|
327
|
+
let maxX = volumeBounds.max[0]
|
|
328
|
+
let minY = volumeBounds.min[1]
|
|
329
|
+
let maxY = volumeBounds.max[1]
|
|
330
|
+
let minZ = volumeBounds.min[2]
|
|
331
|
+
let maxZ = volumeBounds.max[2]
|
|
328
332
|
|
|
329
333
|
// For each clip plane, constrain the bounding box
|
|
330
334
|
// This is an approximation: we check the 8 corners and constrain based on
|
|
331
335
|
// which corners are clipped. For axis-aligned planes, this is exact.
|
|
332
336
|
// For oblique planes, it's a conservative approximation.
|
|
333
337
|
for (const plane of clipPlanes) {
|
|
334
|
-
const { point, normal } = plane
|
|
338
|
+
const { point, normal } = plane
|
|
335
339
|
|
|
336
340
|
// For axis-aligned normals, we can compute exact bounds
|
|
337
|
-
const absNx = Math.abs(normal[0])
|
|
338
|
-
const absNy = Math.abs(normal[1])
|
|
339
|
-
const absNz = Math.abs(normal[2])
|
|
341
|
+
const absNx = Math.abs(normal[0])
|
|
342
|
+
const absNy = Math.abs(normal[1])
|
|
343
|
+
const absNz = Math.abs(normal[2])
|
|
340
344
|
|
|
341
345
|
// Check if plane is approximately axis-aligned
|
|
342
|
-
const tolerance = 0.001
|
|
346
|
+
const tolerance = 0.001
|
|
343
347
|
|
|
344
348
|
if (absNx > 1 - tolerance && absNy < tolerance && absNz < tolerance) {
|
|
345
349
|
// X-aligned plane
|
|
346
350
|
if (normal[0] > 0) {
|
|
347
351
|
// Normal points +X, clips -X side
|
|
348
|
-
minX = Math.max(minX, point[0])
|
|
352
|
+
minX = Math.max(minX, point[0])
|
|
349
353
|
} else {
|
|
350
354
|
// Normal points -X, clips +X side
|
|
351
|
-
maxX = Math.min(maxX, point[0])
|
|
355
|
+
maxX = Math.min(maxX, point[0])
|
|
352
356
|
}
|
|
353
357
|
} else if (
|
|
354
|
-
absNy > 1 - tolerance &&
|
|
358
|
+
absNy > 1 - tolerance &&
|
|
359
|
+
absNx < tolerance &&
|
|
360
|
+
absNz < tolerance
|
|
355
361
|
) {
|
|
356
362
|
// Y-aligned plane
|
|
357
363
|
if (normal[1] > 0) {
|
|
358
|
-
minY = Math.max(minY, point[1])
|
|
364
|
+
minY = Math.max(minY, point[1])
|
|
359
365
|
} else {
|
|
360
|
-
maxY = Math.min(maxY, point[1])
|
|
366
|
+
maxY = Math.min(maxY, point[1])
|
|
361
367
|
}
|
|
362
368
|
} else if (
|
|
363
|
-
absNz > 1 - tolerance &&
|
|
369
|
+
absNz > 1 - tolerance &&
|
|
370
|
+
absNx < tolerance &&
|
|
371
|
+
absNy < tolerance
|
|
364
372
|
) {
|
|
365
373
|
// Z-aligned plane
|
|
366
374
|
if (normal[2] > 0) {
|
|
367
|
-
minZ = Math.max(minZ, point[2])
|
|
375
|
+
minZ = Math.max(minZ, point[2])
|
|
368
376
|
} else {
|
|
369
|
-
maxZ = Math.min(maxZ, point[2])
|
|
377
|
+
maxZ = Math.min(maxZ, point[2])
|
|
370
378
|
}
|
|
371
379
|
} else {
|
|
372
380
|
// Oblique plane - use conservative approximation
|
|
@@ -377,38 +385,30 @@ export function clipPlanesToBoundingBox(
|
|
|
377
385
|
// Project the plane point onto each axis and use as potential bound
|
|
378
386
|
// Only constrain if the plane actually intersects that face
|
|
379
387
|
if (normal[0] > tolerance) {
|
|
380
|
-
minX = Math.max(minX, Math.min(point[0], maxX))
|
|
388
|
+
minX = Math.max(minX, Math.min(point[0], maxX))
|
|
381
389
|
} else if (normal[0] < -tolerance) {
|
|
382
|
-
maxX = Math.min(maxX, Math.max(point[0], minX))
|
|
390
|
+
maxX = Math.min(maxX, Math.max(point[0], minX))
|
|
383
391
|
}
|
|
384
392
|
|
|
385
393
|
if (normal[1] > tolerance) {
|
|
386
|
-
minY = Math.max(minY, Math.min(point[1], maxY))
|
|
394
|
+
minY = Math.max(minY, Math.min(point[1], maxY))
|
|
387
395
|
} else if (normal[1] < -tolerance) {
|
|
388
|
-
maxY = Math.min(maxY, Math.max(point[1], minY))
|
|
396
|
+
maxY = Math.min(maxY, Math.max(point[1], minY))
|
|
389
397
|
}
|
|
390
398
|
|
|
391
399
|
if (normal[2] > tolerance) {
|
|
392
|
-
minZ = Math.max(minZ, Math.min(point[2], maxZ))
|
|
400
|
+
minZ = Math.max(minZ, Math.min(point[2], maxZ))
|
|
393
401
|
} else if (normal[2] < -tolerance) {
|
|
394
|
-
maxZ = Math.min(maxZ, Math.max(point[2], minZ))
|
|
402
|
+
maxZ = Math.min(maxZ, Math.max(point[2], minZ))
|
|
395
403
|
}
|
|
396
404
|
}
|
|
397
405
|
}
|
|
398
406
|
|
|
399
407
|
// Ensure valid bounds (min <= max)
|
|
400
408
|
return {
|
|
401
|
-
min: [
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
Math.min(minZ, maxZ),
|
|
405
|
-
],
|
|
406
|
-
max: [
|
|
407
|
-
Math.max(minX, maxX),
|
|
408
|
-
Math.max(minY, maxY),
|
|
409
|
-
Math.max(minZ, maxZ),
|
|
410
|
-
],
|
|
411
|
-
};
|
|
409
|
+
min: [Math.min(minX, maxX), Math.min(minY, maxY), Math.min(minZ, maxZ)],
|
|
410
|
+
max: [Math.max(minX, maxX), Math.max(minY, maxY), Math.max(minZ, maxZ)],
|
|
411
|
+
}
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
/**
|
|
@@ -431,7 +431,7 @@ export function clipPlanesToPixelRegion(
|
|
|
431
431
|
ngffImage: NgffImage,
|
|
432
432
|
viewportBounds?: VolumeBounds,
|
|
433
433
|
): PixelRegion {
|
|
434
|
-
let bounds = clipPlanesToBoundingBox(clipPlanes, volumeBounds)
|
|
434
|
+
let bounds = clipPlanesToBoundingBox(clipPlanes, volumeBounds)
|
|
435
435
|
|
|
436
436
|
// If viewport bounds are provided, intersect with them
|
|
437
437
|
if (viewportBounds) {
|
|
@@ -446,46 +446,46 @@ export function clipPlanesToPixelRegion(
|
|
|
446
446
|
Math.min(bounds.max[1], viewportBounds.max[1]),
|
|
447
447
|
Math.min(bounds.max[2], viewportBounds.max[2]),
|
|
448
448
|
],
|
|
449
|
-
}
|
|
449
|
+
}
|
|
450
450
|
// Ensure valid bounds
|
|
451
451
|
for (let i = 0; i < 3; i++) {
|
|
452
452
|
if (bounds.min[i] > bounds.max[i]) {
|
|
453
|
-
bounds.min[i] = bounds.max[i] = (bounds.min[i] + bounds.max[i]) * 0.5
|
|
453
|
+
bounds.min[i] = bounds.max[i] = (bounds.min[i] + bounds.max[i]) * 0.5
|
|
454
454
|
}
|
|
455
455
|
}
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
-
const shape = getVolumeShape(ngffImage)
|
|
458
|
+
const shape = getVolumeShape(ngffImage)
|
|
459
459
|
|
|
460
460
|
// Convert world corners to pixel coordinates
|
|
461
461
|
const minWorld: [number, number, number] = [
|
|
462
462
|
bounds.min[0],
|
|
463
463
|
bounds.min[1],
|
|
464
464
|
bounds.min[2],
|
|
465
|
-
]
|
|
465
|
+
]
|
|
466
466
|
const maxWorld: [number, number, number] = [
|
|
467
467
|
bounds.max[0],
|
|
468
468
|
bounds.max[1],
|
|
469
469
|
bounds.max[2],
|
|
470
|
-
]
|
|
470
|
+
]
|
|
471
471
|
|
|
472
|
-
const minPixel = worldToPixel(minWorld, ngffImage)
|
|
473
|
-
const maxPixel = worldToPixel(maxWorld, ngffImage)
|
|
472
|
+
const minPixel = worldToPixel(minWorld, ngffImage)
|
|
473
|
+
const maxPixel = worldToPixel(maxWorld, ngffImage)
|
|
474
474
|
|
|
475
475
|
// Ensure proper ordering and clamp to valid range
|
|
476
476
|
const start: [number, number, number] = [
|
|
477
477
|
Math.max(0, Math.floor(Math.min(minPixel[0], maxPixel[0]))),
|
|
478
478
|
Math.max(0, Math.floor(Math.min(minPixel[1], maxPixel[1]))),
|
|
479
479
|
Math.max(0, Math.floor(Math.min(minPixel[2], maxPixel[2]))),
|
|
480
|
-
]
|
|
480
|
+
]
|
|
481
481
|
|
|
482
482
|
const end: [number, number, number] = [
|
|
483
483
|
Math.min(shape[0], Math.ceil(Math.max(minPixel[0], maxPixel[0]))),
|
|
484
484
|
Math.min(shape[1], Math.ceil(Math.max(minPixel[1], maxPixel[1]))),
|
|
485
485
|
Math.min(shape[2], Math.ceil(Math.max(minPixel[2], maxPixel[2]))),
|
|
486
|
-
]
|
|
486
|
+
]
|
|
487
487
|
|
|
488
|
-
return { start, end }
|
|
488
|
+
return { start, end }
|
|
489
489
|
}
|
|
490
490
|
|
|
491
491
|
/**
|
|
@@ -502,15 +502,15 @@ export function alignToChunks(
|
|
|
502
502
|
region: PixelRegion,
|
|
503
503
|
ngffImage: NgffImage,
|
|
504
504
|
): ChunkAlignedRegion {
|
|
505
|
-
const chunkShape = getChunkShape(ngffImage)
|
|
506
|
-
const volumeShape = getVolumeShape(ngffImage)
|
|
505
|
+
const chunkShape = getChunkShape(ngffImage)
|
|
506
|
+
const volumeShape = getVolumeShape(ngffImage)
|
|
507
507
|
|
|
508
508
|
// Align start down to chunk boundary
|
|
509
509
|
const chunkAlignedStart: [number, number, number] = [
|
|
510
510
|
Math.floor(region.start[0] / chunkShape[0]) * chunkShape[0],
|
|
511
511
|
Math.floor(region.start[1] / chunkShape[1]) * chunkShape[1],
|
|
512
512
|
Math.floor(region.start[2] / chunkShape[2]) * chunkShape[2],
|
|
513
|
-
]
|
|
513
|
+
]
|
|
514
514
|
|
|
515
515
|
// Align end up to chunk boundary (but don't exceed volume size)
|
|
516
516
|
const chunkAlignedEnd: [number, number, number] = [
|
|
@@ -526,15 +526,16 @@ export function alignToChunks(
|
|
|
526
526
|
Math.ceil(region.end[2] / chunkShape[2]) * chunkShape[2],
|
|
527
527
|
volumeShape[2],
|
|
528
528
|
),
|
|
529
|
-
]
|
|
529
|
+
]
|
|
530
530
|
|
|
531
531
|
// Check if alignment changed the region
|
|
532
|
-
const needsClipping =
|
|
532
|
+
const needsClipping =
|
|
533
|
+
chunkAlignedStart[0] !== region.start[0] ||
|
|
533
534
|
chunkAlignedStart[1] !== region.start[1] ||
|
|
534
535
|
chunkAlignedStart[2] !== region.start[2] ||
|
|
535
536
|
chunkAlignedEnd[0] !== region.end[0] ||
|
|
536
537
|
chunkAlignedEnd[1] !== region.end[1] ||
|
|
537
|
-
chunkAlignedEnd[2] !== region.end[2]
|
|
538
|
+
chunkAlignedEnd[2] !== region.end[2]
|
|
538
539
|
|
|
539
540
|
return {
|
|
540
541
|
start: region.start,
|
|
@@ -542,7 +543,7 @@ export function alignToChunks(
|
|
|
542
543
|
chunkAlignedStart,
|
|
543
544
|
chunkAlignedEnd,
|
|
544
545
|
needsClipping,
|
|
545
|
-
}
|
|
546
|
+
}
|
|
546
547
|
}
|
|
547
548
|
|
|
548
549
|
/**
|
|
@@ -564,35 +565,35 @@ export function createAxisAlignedClipPlane(
|
|
|
564
565
|
direction: "positive" | "negative",
|
|
565
566
|
volumeBounds: VolumeBounds,
|
|
566
567
|
): ClipPlane {
|
|
567
|
-
const { min, max } = volumeBounds
|
|
568
|
+
const { min, max } = volumeBounds
|
|
568
569
|
const center: [number, number, number] = [
|
|
569
570
|
(min[0] + max[0]) / 2,
|
|
570
571
|
(min[1] + max[1]) / 2,
|
|
571
572
|
(min[2] + max[2]) / 2,
|
|
572
|
-
]
|
|
573
|
+
]
|
|
573
574
|
|
|
574
|
-
let point: [number, number, number]
|
|
575
|
-
let normal: [number, number, number]
|
|
575
|
+
let point: [number, number, number]
|
|
576
|
+
let normal: [number, number, number]
|
|
576
577
|
|
|
577
578
|
// Normal points toward the visible region
|
|
578
|
-
const sign = direction === "positive" ? 1 : -1
|
|
579
|
+
const sign = direction === "positive" ? 1 : -1
|
|
579
580
|
|
|
580
581
|
switch (axis) {
|
|
581
582
|
case "x":
|
|
582
|
-
point = [position, center[1], center[2]]
|
|
583
|
-
normal = [sign, 0, 0]
|
|
584
|
-
break
|
|
583
|
+
point = [position, center[1], center[2]]
|
|
584
|
+
normal = [sign, 0, 0]
|
|
585
|
+
break
|
|
585
586
|
case "y":
|
|
586
|
-
point = [center[0], position, center[2]]
|
|
587
|
-
normal = [0, sign, 0]
|
|
588
|
-
break
|
|
587
|
+
point = [center[0], position, center[2]]
|
|
588
|
+
normal = [0, sign, 0]
|
|
589
|
+
break
|
|
589
590
|
case "z":
|
|
590
|
-
point = [center[0], center[1], position]
|
|
591
|
-
normal = [0, 0, sign]
|
|
592
|
-
break
|
|
591
|
+
point = [center[0], center[1], position]
|
|
592
|
+
normal = [0, 0, sign]
|
|
593
|
+
break
|
|
593
594
|
}
|
|
594
595
|
|
|
595
|
-
return { point, normal }
|
|
596
|
+
return { point, normal }
|
|
596
597
|
}
|
|
597
598
|
|
|
598
599
|
/**
|
|
@@ -605,36 +606,36 @@ export function validateClipPlanes(clipPlanes: ClipPlanes): void {
|
|
|
605
606
|
if (clipPlanes.length > MAX_CLIP_PLANES) {
|
|
606
607
|
throw new Error(
|
|
607
608
|
`Too many clip planes: ${clipPlanes.length} exceeds maximum of ${MAX_CLIP_PLANES}`,
|
|
608
|
-
)
|
|
609
|
+
)
|
|
609
610
|
}
|
|
610
611
|
|
|
611
612
|
for (let i = 0; i < clipPlanes.length; i++) {
|
|
612
|
-
const plane = clipPlanes[i]
|
|
613
|
+
const plane = clipPlanes[i]
|
|
613
614
|
|
|
614
615
|
// Check point is valid
|
|
615
616
|
if (
|
|
616
617
|
!Array.isArray(plane.point) ||
|
|
617
618
|
plane.point.length !== 3 ||
|
|
618
|
-
plane.point.some((v) => typeof v !== "number" || !isFinite(v))
|
|
619
|
+
plane.point.some((v) => typeof v !== "number" || !Number.isFinite(v))
|
|
619
620
|
) {
|
|
620
|
-
throw new Error(`Invalid point in clip plane ${i}`)
|
|
621
|
+
throw new Error(`Invalid point in clip plane ${i}`)
|
|
621
622
|
}
|
|
622
623
|
|
|
623
624
|
// Check normal is valid
|
|
624
625
|
if (
|
|
625
626
|
!Array.isArray(plane.normal) ||
|
|
626
627
|
plane.normal.length !== 3 ||
|
|
627
|
-
plane.normal.some((v) => typeof v !== "number" || !isFinite(v))
|
|
628
|
+
plane.normal.some((v) => typeof v !== "number" || !Number.isFinite(v))
|
|
628
629
|
) {
|
|
629
|
-
throw new Error(`Invalid normal in clip plane ${i}`)
|
|
630
|
+
throw new Error(`Invalid normal in clip plane ${i}`)
|
|
630
631
|
}
|
|
631
632
|
|
|
632
633
|
// Check normal is not zero
|
|
633
634
|
const length = Math.sqrt(
|
|
634
635
|
plane.normal[0] ** 2 + plane.normal[1] ** 2 + plane.normal[2] ** 2,
|
|
635
|
-
)
|
|
636
|
+
)
|
|
636
637
|
if (length < 0.0001) {
|
|
637
|
-
throw new Error(`Zero-length normal in clip plane ${i}`)
|
|
638
|
+
throw new Error(`Zero-length normal in clip plane ${i}`)
|
|
638
639
|
}
|
|
639
640
|
}
|
|
640
641
|
}
|