Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
<body height="100%" width="100%">
<div id="vol-e" style="width: 1200px; height: 900px"></div>
<select name="Test data" id="testData">
<option value="cellpainting" selected>CellPainting</option>
<option value="cellpainting">CellPainting</option>
<option value="testpick">Test picking</option>
<option value="timeSeries">Time Series Json</option>
<option value="omeTiff">OME TIFF</option>
<option value="zarrEMT">Zarr EMT</option>
<option value="zarrIDR1">OME Zarr (IDR 1)</option>
<option value="zarrIDR2">OME Zarr (IDR 2)</option>
<option value="zarrVariance">OME Zarr (Variance)</option>
<option value="zarrVariance" selected>OME Zarr (Variance)</option>
<option value="zarrNucmorph0">OME Zarr (NucMorph)</option>
<option value="zarrFlyBrain">OME Zarr (Fly brain)</option>
<option value="zarrHumanOrganAtlas">OME Zarr (Human Organ Atlas)</option>
Expand All @@ -31,6 +31,7 @@
<button id="Y">Y</button>
<button id="Z">Z</button>
<button id="3D">3D</button>
<button id="tripleBtn">Triple</button>
<button id="rotBtn">Turntable</button>
<button id="axisBtn">Axis</button>
<button id="showScaleBar">ScaleBar</button>
Expand Down
4 changes: 4 additions & 0 deletions public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,10 @@ function main() {
d3Btn?.addEventListener("click", () => {
view3D.setCameraMode("3D");
});
const tripleBtn = document.getElementById("tripleBtn");
tripleBtn?.addEventListener("click", () => {
view3D.setCameraMode("TRIPLE");
});
const rotBtn = document.getElementById("rotBtn");
rotBtn?.addEventListener("click", () => {
myState.isTurntable = !myState.isTurntable;
Expand Down
144 changes: 125 additions & 19 deletions src/Atlas2DSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,24 @@ import {
import { Channel, Volume } from "./index.js";
import { sliceFragmentShaderSrc, sliceShaderUniforms, sliceVertexShaderSrc } from "./constants/volumeSliceShader.js";
import type { VolumeRenderImpl } from "./VolumeRenderImpl.js";
import { SettingsFlags, VolumeRenderSettings } from "./VolumeRenderSettings.js";
import { Axis, SettingsFlags, VolumeRenderSettings } from "./VolumeRenderSettings.js";
import FusedChannelData from "./FusedChannelData.js";
import type { FuseChannel } from "./types.js";

const BOUNDING_BOX_DEFAULT_COLOR = new Color(0xffff00);

/** Maps the slice-along Axis to the integer expected by the viewAxis shader uniform. */
function axisToShaderInt(axis: Axis): number {
switch (axis) {
case Axis.X:
return 1;
case Axis.Y:
return 2;
default:
return 0;
}
}

/**
* Creates a plane that renders a 2D XY slice of volume atlas data.
*/
Expand All @@ -37,7 +49,13 @@ export default class Atlas2DSlice implements VolumeRenderImpl {
private boxHelper: Box3Helper;
private uniforms: ReturnType<typeof sliceShaderUniforms>;
private channelData!: FusedChannelData;
/** When false, `channelData` is shared from another renderer and must not be cleaned up by this instance. */
private ownsChannelData = true;
private sliceUpdateWaiting = false;
/** The axis being sliced along: Z = XY view, X = YZ view, Y = XZ view. */
private viewAxisValue: Axis = Axis.Z;
/** When true, always request the full volume (all Z slices) even for viewAxis 0. */
private requireFullVolume = false;

/**
* Creates a new Atlas2DSlice.
Expand Down Expand Up @@ -70,18 +88,40 @@ export default class Atlas2DSlice implements VolumeRenderImpl {

/**
* Syncs `this.settings.zSlice` with the corresponding shader uniform, or defers syncing until the slice is loaded.
* For non-XY views, zSlice represents the coordinate along the slicing axis (X for YZ, Y for XZ).
* @returns a boolean indicating whether the slice is out of bounds of the volume entirely.
*/
private updateSlice(): boolean {
const slice = Math.floor(this.settings.zSlice);
const sizez = this.volume.imageInfo.volumeSize.z;
if (slice < 0 || slice >= sizez) {
const volSize = this.volume.imageInfo.volumeSize;

// Determine the axis-appropriate size and subregion bounds
let axisSize: number;
let regionMin: number;
let regionMax: number;
switch (this.viewAxisValue) {
case Axis.X: // YZ view: slicing along X
axisSize = volSize.x;
regionMin = this.volume.imageInfo.subregionOffset.x;
regionMax = regionMin + this.volume.imageInfo.subregionSize.x;
break;
case Axis.Y: // XZ view: slicing along Y
axisSize = volSize.y;
regionMin = this.volume.imageInfo.subregionOffset.y;
regionMax = regionMin + this.volume.imageInfo.subregionSize.y;
break;
default: // XY view: slicing along Z
axisSize = volSize.z;
regionMin = this.volume.imageInfo.subregionOffset.z;
regionMax = regionMin + this.volume.imageInfo.subregionSize.z;
break;
}

if (slice < 0 || slice >= axisSize) {
return false;
}

const regionMinZ = this.volume.imageInfo.subregionOffset.z;
const regionMaxZ = regionMinZ + this.volume.imageInfo.subregionSize.z;
if (slice < regionMinZ || slice >= regionMaxZ) {
if (slice < regionMin || slice >= regionMax) {
// If the slice is outside the current loaded subregion, defer until the subregion is updated
this.sliceUpdateWaiting = true;
} else {
Expand All @@ -96,9 +136,24 @@ export default class Atlas2DSlice implements VolumeRenderImpl {
const volumeScale = this.volume.normPhysicalSize.clone().multiply(this.settings.scale);
const regionScale = volumeScale.clone().multiply(this.volume.normRegionSize);
const volumeOffset = this.volume.getContentCenter().clone().multiply(this.settings.scale);
this.geometryMesh.position.copy(volumeOffset);
// set scale
this.geometryMesh.scale.copy(regionScale);

// For non-XY views, remap scale/position so the face appears correctly in the XY plane.
// The PlaneGeometry is always in XY, so we set mesh X/Y scale to the target face dimensions.
switch (this.viewAxisValue) {
case Axis.X: // YZ face: volume Z → mesh X, volume Y → mesh Y (Y vertical to align with XY)
this.geometryMesh.position.set(volumeOffset.z, volumeOffset.y, 0);
this.geometryMesh.scale.set(regionScale.z, regionScale.y, 1);
break;
case Axis.Y: // XZ face: volume X → mesh X, volume Z → mesh Y
this.geometryMesh.position.set(volumeOffset.x, volumeOffset.z, 0);
this.geometryMesh.scale.set(regionScale.x, regionScale.z, 1);
break;
default: // XY face (standard)
this.geometryMesh.position.copy(volumeOffset);
this.geometryMesh.scale.copy(regionScale);
break;
}

this.setUniform("volumeScale", regionScale);
this.boxHelper.box.set(volumeScale.clone().multiplyScalar(-0.5), volumeScale.clone().multiplyScalar(0.5));

Expand All @@ -109,14 +164,17 @@ export default class Atlas2DSlice implements VolumeRenderImpl {
this.setUniform("ATLAS_DIMS", atlasTileDims);
this.setUniform("textureRes", atlasSize);
this.setUniform("SLICES", volumeSize.z);
this.setUniform("volumeSize", volumeSize);
if (this.sliceUpdateWaiting) {
this.updateSlice();
}

// (re)create channel data
if (!this.channelData || this.channelData.width !== atlasSize.x || this.channelData.height !== atlasSize.y) {
this.channelData?.cleanup();
this.channelData = new FusedChannelData(atlasSize.x, atlasSize.y);
// (re)create channel data (skip if sharing from another renderer)
if (this.ownsChannelData) {
if (!this.channelData || this.channelData.width !== atlasSize.x || this.channelData.height !== atlasSize.y) {
this.channelData?.cleanup();
this.channelData = new FusedChannelData(atlasSize.x, atlasSize.y);
}
}
}

Expand Down Expand Up @@ -184,11 +242,18 @@ export default class Atlas2DSlice implements VolumeRenderImpl {

const sliceInBounds = this.updateSlice();
if (sliceInBounds) {
const sliceLowerBound = Math.floor(this.settings.zSlice) / this.volume.imageInfo.volumeSize.z;
const sliceUpperBound = (Math.floor(this.settings.zSlice) + 1) / this.volume.imageInfo.volumeSize.z;
this.volume.updateRequiredData({
subregion: new Box3(new Vector3(0, 0, sliceLowerBound), new Vector3(1, 1, sliceUpperBound)),
});
if (this.viewAxisValue !== Axis.Z || this.requireFullVolume) {
// For non-XY views (or triple-mode), we need the full volume loaded
this.volume.updateRequiredData({
subregion: new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1)),
});
} else {
const sliceLowerBound = Math.floor(this.settings.zSlice) / this.volume.imageInfo.volumeSize.z;
const sliceUpperBound = (Math.floor(this.settings.zSlice) + 1) / this.volume.imageInfo.volumeSize.z;
this.volume.updateRequiredData({
subregion: new Box3(new Vector3(0, 0, sliceLowerBound), new Vector3(1, 1, sliceUpperBound)),
});
}
}
}

Expand Down Expand Up @@ -236,7 +301,26 @@ export default class Atlas2DSlice implements VolumeRenderImpl {
this.geometry.dispose();
this.geometryMesh.material.dispose();

this.channelData.cleanup();
if (this.ownsChannelData) {
this.channelData?.cleanup();
}
}

/** Returns the FusedChannelData owned by this renderer. */
public getChannelData(): FusedChannelData {
return this.channelData;
}

/**
* Sets a shared FusedChannelData from another renderer.
* When set, this renderer will not create or maintain its own fused data.
*/
public setSharedChannelData(data: FusedChannelData): void {
if (this.ownsChannelData && this.channelData) {
this.channelData.cleanup();
}
this.channelData = data;
this.ownsChannelData = false;
}

public viewpointMoved(): void {
Expand Down Expand Up @@ -268,6 +352,10 @@ export default class Atlas2DSlice implements VolumeRenderImpl {

// channelcolors is array of {rgbColor, lut} and channeldata is volume.channels
public updateActiveChannels(channelcolors: FuseChannel[], channeldata: Channel[]): void {
// Skip fusion if sharing another renderer's fused data
if (!this.ownsChannelData) {
return;
}
this.channelData.fuse(channelcolors, channeldata);

// update to fused texture
Expand All @@ -288,4 +376,22 @@ export default class Atlas2DSlice implements VolumeRenderImpl {
public setRenderUpdateListener(_listener?: ((iteration: number) => void) | undefined) {
return;
}

/**
* Sets the view axis for this slice renderer.
* @param axis The axis to slice along: Axis.Z = XY view, Axis.X = YZ view, Axis.Y = XZ view.
*/
public setViewAxis(axis: Axis): void {
this.viewAxisValue = axis;
this.setUniform("viewAxis", axisToShaderInt(axis));
}

/**
* When set to true, this renderer will always request the full volume (all Z slices)
* instead of only the single slice needed for XY view. Required for triple-slice mode
* so that YZ/XZ renderers sharing this renderer's fused texture can sample all slices.
*/
public setRequireFullVolume(require: boolean): void {
this.requireFullVolume = require;
}
}
Loading
Loading