diff --git a/src/sliceview/base.spec.ts b/src/sliceview/base.spec.ts index 9bec5c3011..62fbc8a0c9 100644 --- a/src/sliceview/base.spec.ts +++ b/src/sliceview/base.spec.ts @@ -15,8 +15,11 @@ */ import { describe, it, expect } from "vitest"; +import type { ProjectionParameters } from "#src/projection_parameters.js"; +import type { TransformedSource } from "#src/sliceview/base.js"; import { estimateSliceAreaPerChunk, + forEachVisibleVolumetricChunk, getNearIsotropicBlockSize, } from "#src/sliceview/base.js"; import { ChunkLayout } from "#src/sliceview/chunk_layout.js"; @@ -200,3 +203,61 @@ describe("estimateSliceAreaPerChunk", () => { } }); }); + +describe("forEachVisibleVolumetricChunk", () => { + it("does not clamp zeroed display-dim positions to the chunk origin", () => { + // `xy` slice view of a rank-3 volume whose origin is *not* at (0, 0, 0) + const tsource = { + source: { + spec: { + rank: 3, + chunkDataSize: Uint32Array.of(1024, 1024, 1), + // Stack origin at voxel (64056, 33042, 20) -> nonzero chunk bounds. + lowerChunkBound: Float32Array.of(62, 32, 20), + upperChunkBound: Float32Array.of(64, 34, 21), + }, + }, + layerRank: 3, + // The following data is prepared as would be done by + // `getVolumetricTransformedSources`: display-dim rows (x, y) are zeroed and + // z maps identically from global z with no translation. + fixedLayerToChunkTransform: Float32Array.of( + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + ), + // Display dims (x, y) get infinite clip bounds; only z is finite. + nonDisplayLowerClipBound: Float32Array.of(-Infinity, -Infinity, 20), + nonDisplayUpperClipBound: Float32Array.of(Infinity, Infinity, 21), + chunkLayout: new ChunkLayout(vec3.fromValues(1, 1, 1), mat4.create(), 3), + lowerChunkDisplayBound: vec3.fromValues(62, 32, 20), + upperChunkDisplayBound: vec3.fromValues(64, 34, 21), + chunkDisplayDimensionIndices: [0, 1, 2], + curPositionInChunks: new Float32Array(3), + // Sentinel so an early return (source excluded) can't masquerade as a pass. + fixedPositionWithinChunk: Uint32Array.of(999, 999, 999), + } as unknown as TransformedSource; + + forEachVisibleVolumetricChunk( + { + // x/y are irrelevant (transform rows zeroed); z is at slice 20. + globalPosition: Float32Array.of(70000, 40000, 20), + viewProjectionMat: mat4.create(), + } as unknown as ProjectionParameters, + new Float32Array(0), + tsource, + () => {}, + ); + + expect(Array.from(tsource.fixedPositionWithinChunk)).toEqual([0, 0, 0]); + }); +}); diff --git a/src/sliceview/base.ts b/src/sliceview/base.ts index 7b2e5a7857..1ddfb3a684 100644 --- a/src/sliceview/base.ts +++ b/src/sliceview/base.ts @@ -182,7 +182,11 @@ function updateFixedCurPositionInChunks< globalPosition: Float32Array, localPosition: Float32Array, ): boolean { - const { curPositionInChunks, fixedPositionWithinChunk } = tsource; + const { + curPositionInChunks, + fixedPositionWithinChunk, + chunkDisplayDimensionIndices, + } = tsource; const { nonDisplayLowerClipBound, nonDisplayUpperClipBound } = tsource; const { rank, chunkDataSize, lowerChunkBound, upperChunkBound } = tsource.source.spec; @@ -218,6 +222,18 @@ function updateFixedCurPositionInChunks< } return false; } + if (chunkDisplayDimensionIndices.includes(chunkDim)) { + // This function computes only the *fixed* (non-display) part of the + // position. The rows of `fixedLayerToChunkTransform` corresponding to + // display dimensions are zeroed, so `x` is a placeholder 0 rather than a + // real coordinate; the actual chunk index is filled in by the caller's + // iteration over the display subspace. Clamping the placeholder to + // `lowerChunkBound` would, for a source with a non-zero + // `spec.lowerVoxelBound`, force `chunk` past `x` and leave a negative + // `fixedPositionWithinChunk`. + fixedPositionWithinChunk[chunkDim] = 0; + continue; + } const chunkSize = chunkDataSize[chunkDim]; // Given that clip bounds are already tested above, clamp chunk index to its // bounds, to ensure floating-point imprecision does not result in an