From 3af122eb6fb10d4007572b5add4d7d60081dc575 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Tue, 9 Jun 2026 18:01:53 -0400 Subject: [PATCH 1/3] fix(sliceview): use different bounds to compute chunk positions It seems that spec.{lower,upper}*ChunkBound is in global coordinates, whereas curPositionInChunks is zeroed in the display dimensions. Due to this mismatch, clamping the chunk index produces wrong results and the viewing area stays blank. Using nonDisplay{lower,upper}ClipBound to compute chunk bounds fixes that. --- src/sliceview/base.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/sliceview/base.ts b/src/sliceview/base.ts index 7b2e5a7857..cd86bb17ba 100644 --- a/src/sliceview/base.ts +++ b/src/sliceview/base.ts @@ -184,8 +184,7 @@ function updateFixedCurPositionInChunks< ): boolean { const { curPositionInChunks, fixedPositionWithinChunk } = tsource; const { nonDisplayLowerClipBound, nonDisplayUpperClipBound } = tsource; - const { rank, chunkDataSize, lowerChunkBound, upperChunkBound } = - tsource.source.spec; + const { rank, chunkDataSize } = tsource.source.spec; if ( !getChunkPositionFromCombinedGlobalLocalPositions( curPositionInChunks, @@ -219,12 +218,19 @@ function updateFixedCurPositionInChunks< return false; } 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 - // out-of-bounds index. + // Given that clip bounds are already tested above, clamp the chunk index to + // the range implied by those clip bounds, to ensure floating-point + // imprecision does not result in an out-of-bounds index. The clip bounds + // are used because they are expressed in the same coordinate frame as `x`. + const lowerChunkLimit = Math.floor( + nonDisplayLowerClipBound[chunkDim] / chunkSize, + ); + const upperChunkLimit = Math.ceil( + nonDisplayUpperClipBound[chunkDim] / chunkSize, + ); const chunk = (curPositionInChunks[chunkDim] = Math.min( - upperChunkBound[chunkDim] - 1, - Math.max(lowerChunkBound[chunkDim], Math.floor(x / chunkSize)), + upperChunkLimit - 1, + Math.max(lowerChunkLimit, Math.floor(x / chunkSize)), )); fixedPositionWithinChunk[chunkDim] = x - chunk * chunkSize; } From 55fe839b316a2a71148f274c36ccd4be7048b706 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Wed, 10 Jun 2026 18:19:43 -0400 Subject: [PATCH 2/3] Add regression test --- src/sliceview/base.spec.ts | 61 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) 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]); + }); +}); From a3b0b75781fcb872a29fd0778ad029218301d484 Mon Sep 17 00:00:00 2001 From: Michael Innerberger Date: Thu, 30 Jul 2026 17:05:39 -0400 Subject: [PATCH 3/3] Only clamp for non-display dims --- src/sliceview/base.ts | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/sliceview/base.ts b/src/sliceview/base.ts index cd86bb17ba..1ddfb3a684 100644 --- a/src/sliceview/base.ts +++ b/src/sliceview/base.ts @@ -182,9 +182,14 @@ function updateFixedCurPositionInChunks< globalPosition: Float32Array, localPosition: Float32Array, ): boolean { - const { curPositionInChunks, fixedPositionWithinChunk } = tsource; + const { + curPositionInChunks, + fixedPositionWithinChunk, + chunkDisplayDimensionIndices, + } = tsource; const { nonDisplayLowerClipBound, nonDisplayUpperClipBound } = tsource; - const { rank, chunkDataSize } = tsource.source.spec; + const { rank, chunkDataSize, lowerChunkBound, upperChunkBound } = + tsource.source.spec; if ( !getChunkPositionFromCombinedGlobalLocalPositions( curPositionInChunks, @@ -217,20 +222,25 @@ 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 the chunk index to - // the range implied by those clip bounds, to ensure floating-point - // imprecision does not result in an out-of-bounds index. The clip bounds - // are used because they are expressed in the same coordinate frame as `x`. - const lowerChunkLimit = Math.floor( - nonDisplayLowerClipBound[chunkDim] / chunkSize, - ); - const upperChunkLimit = Math.ceil( - nonDisplayUpperClipBound[chunkDim] / chunkSize, - ); + // Given that clip bounds are already tested above, clamp chunk index to its + // bounds, to ensure floating-point imprecision does not result in an + // out-of-bounds index. const chunk = (curPositionInChunks[chunkDim] = Math.min( - upperChunkLimit - 1, - Math.max(lowerChunkLimit, Math.floor(x / chunkSize)), + upperChunkBound[chunkDim] - 1, + Math.max(lowerChunkBound[chunkDim], Math.floor(x / chunkSize)), )); fixedPositionWithinChunk[chunkDim] = x - chunk * chunkSize; }