From 241a5f8ffb4271dc3b89eea6705c571f276a61fb Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 5 May 2026 11:52:57 -0700 Subject: [PATCH 01/12] remove three.js type from `LoadSpec` --- src/loaders/IVolumeLoader.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/loaders/IVolumeLoader.ts b/src/loaders/IVolumeLoader.ts index 2ef8f60a..40b2e7aa 100644 --- a/src/loaders/IVolumeLoader.ts +++ b/src/loaders/IVolumeLoader.ts @@ -1,5 +1,3 @@ -import { Box3, Vector3 } from "three"; - import Volume from "../Volume.js"; import type { VolumeDims } from "../VolumeDims.js"; import { CImageInfo, type ImageInfo } from "../ImageInfo.js"; @@ -20,7 +18,7 @@ export class LoadSpec { */ multiscaleLevel?: number; /** Subregion of volume to load. If not specified, the entire volume is loaded. Specify as floats between 0-1. */ - subregion = new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1)); + subregion: { min: [number, number, number]; max: [number, number, number] } = { min: [0, 0, 0], max: [1, 1, 1] }; channels?: number[]; /** Treat multiscaleLevel literally and don't use other constraints to change it. * By default we will try to load the best level based on the maxAtlasEdge and scaleLevelBias, @@ -31,7 +29,9 @@ export class LoadSpec { export function loadSpecToString(spec: LoadSpec): string { const { min, max } = spec.subregion; - return `${spec.multiscaleLevel}:${spec.time}:x(${min.x},${max.x}):y(${min.y},${max.y}):z(${min.z},${max.z})`; + const [xmin, ymin, zmin] = min; + const [xmax, ymax, zmax] = max; + return `${spec.multiscaleLevel}:${spec.time}:x(${xmin},${xmax}):y(${ymin},${ymax}):z(${zmin},${zmax})`; } export type LoadedVolumeInfo = { From 4e982f32e96bce6877dcb4b6edeb1dbddc296a47 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 5 May 2026 12:21:18 -0700 Subject: [PATCH 02/12] fix new errors in `Volume` --- src/Volume.ts | 36 ++++++++++++++++++-------------- src/loaders/IVolumeLoader.ts | 18 ++++++++++++++-- src/loaders/VolumeLoaderUtils.ts | 5 +++-- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/Volume.ts b/src/Volume.ts index 204eef1b..82cffb02 100644 --- a/src/Volume.ts +++ b/src/Volume.ts @@ -4,7 +4,13 @@ import Channel from "./Channel.js"; import Histogram from "./Histogram.js"; import { Lut } from "./Lut.js"; import { getColorByChannelIndex } from "./constants/colors.js"; -import { type IVolumeLoader, LoadSpec, type PerChannelCallback } from "./loaders/IVolumeLoader.js"; +import { + cloneLoadSpec, + type IVolumeLoader, + LoadSpec, + loadSpecSubregionAsBox3, + type PerChannelCallback, +} from "./loaders/IVolumeLoader.js"; import { MAX_ATLAS_EDGE, pickLevelToLoadUnscaled } from "./loaders/VolumeLoaderUtils.js"; import type { NumberType, TypedArray } from "./types.js"; import { type ImageInfo, CImageInfo, defaultImageInfo } from "./ImageInfo.js"; @@ -83,13 +89,9 @@ export default class Volume { scaleLevelBias: 0, maxAtlasEdge: MAX_ATLAS_EDGE, channels: Array.from({ length: this.imageInfo.numChannels }, (_val, idx) => idx), - ...loadSpec, - }; - this.loadSpecRequired = { - ...this.loadSpec, - channels: this.loadSpec.channels.slice(), - subregion: this.loadSpec.subregion.clone(), + ...cloneLoadSpec(loadSpec), }; + this.loadSpecRequired = cloneLoadSpec(this.loadSpec); this.loader = loader; // imageMetadata to be filled in by Volume Loaders this.imageMetadata = {}; @@ -166,11 +168,14 @@ export default class Volume { /** Returns `true` iff differences between `loadSpec` and `loadSpecRequired` indicate new data *must* be loaded. */ private mustLoadNewData(): boolean { + const { loadSpec, loadSpecRequired } = this; + const currentRegion = loadSpecSubregionAsBox3(loadSpec); + const requiredRegion = loadSpecSubregionAsBox3(loadSpecRequired); return ( - this.loadSpec.useExplicitLevel !== this.loadSpecRequired.useExplicitLevel || // explicit vs automatic level changed - this.loadSpec.time !== this.loadSpecRequired.time || // time point changed - !this.loadSpec.subregion.containsBox(this.loadSpecRequired.subregion) || // new subregion not contained in old - this.loadSpecRequired.channels.some((channel) => !this.loadSpec.channels.includes(channel)) // new channel(s) + loadSpec.useExplicitLevel !== loadSpecRequired.useExplicitLevel || // explicit vs automatic level changed + loadSpec.time !== loadSpecRequired.time || // time point changed + !currentRegion.containsBox(requiredRegion) || // new subregion not contained in old + (loadSpecRequired.channels ?? []).some((channel) => !(loadSpec.channels ?? []).includes(channel)) // new channel(s) ); } @@ -184,8 +189,10 @@ export default class Volume { * imposed by `multiscaleLevel`. */ private mayLoadNewScaleLevel(): boolean { + const currentRegion = loadSpecSubregionAsBox3(this.loadSpec); + const requiredRegion = loadSpecSubregionAsBox3(this.loadSpecRequired); return ( - !this.loadSpec.subregion.equals(this.loadSpecRequired.subregion) || + !currentRegion.equals(requiredRegion) || this.loadSpecRequired.maxAtlasEdge !== this.loadSpec.maxAtlasEdge || this.loadSpecRequired.multiscaleLevel !== this.loadSpec.multiscaleLevel || this.loadSpecRequired.scaleLevelBias !== this.loadSpec.scaleLevelBias @@ -229,10 +236,7 @@ export default class Volume { */ private async loadNewData(onChannelLoaded?: PerChannelCallback): Promise { this.setUnloaded(); - this.loadSpec = { - ...this.loadSpecRequired, - subregion: this.loadSpecRequired.subregion.clone(), - }; + this.loadSpec = cloneLoadSpec(this.loadSpecRequired); try { await this.loader?.loadVolumeData(this, undefined, onChannelLoaded); diff --git a/src/loaders/IVolumeLoader.ts b/src/loaders/IVolumeLoader.ts index 40b2e7aa..fb1b5e71 100644 --- a/src/loaders/IVolumeLoader.ts +++ b/src/loaders/IVolumeLoader.ts @@ -1,10 +1,12 @@ +import { Vector3, Box3 } from "three"; + import Volume from "../Volume.js"; import type { VolumeDims } from "../VolumeDims.js"; import { CImageInfo, type ImageInfo } from "../ImageInfo.js"; -import { TypedArray, NumberType } from "../types.js"; +import type { TypedArray, NumberType } from "../types.js"; import { createDefaultMetadata } from "./VolumeLoaderUtils.js"; import { PrefetchDirection } from "./zarr_utils/types.js"; -import { ZarrLoaderFetchOptions } from "./OmeZarrLoader.js"; +import type { ZarrLoaderFetchOptions } from "./OmeZarrLoader.js"; export class LoadSpec { time = 0; @@ -34,6 +36,18 @@ export function loadSpecToString(spec: LoadSpec): string { return `${spec.multiscaleLevel}:${spec.time}:x(${xmin},${xmax}):y(${ymin},${ymax}):z(${zmin},${zmax})`; } +export function cloneLoadSpec(spec: S): S { + return { + ...spec, + channels: spec.channels?.slice() ?? [], + subregion: { min: [...spec.subregion.min], max: [...spec.subregion.max] }, + }; +} + +export function loadSpecSubregionAsBox3({ subregion }: LoadSpec): Box3 { + return new Box3(new Vector3(...subregion.min), new Vector3(...subregion.max)); +} + export type LoadedVolumeInfo = { imageInfo: ImageInfo; loadSpec: LoadSpec; diff --git a/src/loaders/VolumeLoaderUtils.ts b/src/loaders/VolumeLoaderUtils.ts index b375041b..4e63371a 100644 --- a/src/loaders/VolumeLoaderUtils.ts +++ b/src/loaders/VolumeLoaderUtils.ts @@ -1,7 +1,7 @@ import { Box3, Vector2, Vector3 } from "three"; import { CImageInfo, type ImageInfo } from "../ImageInfo.js"; -import { LoadSpec } from "./IVolumeLoader.js"; +import { LoadSpec, loadSpecSubregionAsBox3 } from "./IVolumeLoader.js"; export const MAX_ATLAS_EDGE = 4096; @@ -175,7 +175,8 @@ export function pickLevelToLoadUnscaled(loadSpec: LoadSpec, spatialDimsZYX: ZYX[ * `LoadSpec`'s `subregion` property. */ export function pickLevelToLoad(loadSpec: LoadSpec, spatialDimsZYX: ZYX[]): number { - const scaledDims = scaleMultipleDimsToSubregion(loadSpec.subregion, spatialDimsZYX); + const region = loadSpecSubregionAsBox3(loadSpec); + const scaledDims = scaleMultipleDimsToSubregion(region, spatialDimsZYX); return pickLevelToLoadUnscaled(loadSpec, scaledDims); } From cd5e2ab8284d6d40e486317deb3ac22b867f4fed Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 5 May 2026 12:24:05 -0700 Subject: [PATCH 03/12] create & use new `LoadSpec` defaults creator --- src/Volume.ts | 8 +++----- src/loaders/IVolumeLoader.ts | 12 +++++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Volume.ts b/src/Volume.ts index 82cffb02..496a767a 100644 --- a/src/Volume.ts +++ b/src/Volume.ts @@ -6,12 +6,13 @@ import { Lut } from "./Lut.js"; import { getColorByChannelIndex } from "./constants/colors.js"; import { cloneLoadSpec, + defaultLoadSpec, type IVolumeLoader, LoadSpec, loadSpecSubregionAsBox3, type PerChannelCallback, } from "./loaders/IVolumeLoader.js"; -import { MAX_ATLAS_EDGE, pickLevelToLoadUnscaled } from "./loaders/VolumeLoaderUtils.js"; +import { pickLevelToLoadUnscaled } from "./loaders/VolumeLoaderUtils.js"; import type { NumberType, TypedArray } from "./types.js"; import { type ImageInfo, CImageInfo, defaultImageInfo } from "./ImageInfo.js"; import type { VolumeDims } from "./VolumeDims.js"; @@ -84,10 +85,7 @@ export default class Volume { // TODO: use getter? this.name = imageInfo.name; this.loadSpec = { - // Fill in defaults for optional properties - multiscaleLevel: 0, - scaleLevelBias: 0, - maxAtlasEdge: MAX_ATLAS_EDGE, + ...defaultLoadSpec(), channels: Array.from({ length: this.imageInfo.numChannels }, (_val, idx) => idx), ...cloneLoadSpec(loadSpec), }; diff --git a/src/loaders/IVolumeLoader.ts b/src/loaders/IVolumeLoader.ts index fb1b5e71..bdbf597e 100644 --- a/src/loaders/IVolumeLoader.ts +++ b/src/loaders/IVolumeLoader.ts @@ -4,7 +4,7 @@ import Volume from "../Volume.js"; import type { VolumeDims } from "../VolumeDims.js"; import { CImageInfo, type ImageInfo } from "../ImageInfo.js"; import type { TypedArray, NumberType } from "../types.js"; -import { createDefaultMetadata } from "./VolumeLoaderUtils.js"; +import { createDefaultMetadata, MAX_ATLAS_EDGE } from "./VolumeLoaderUtils.js"; import { PrefetchDirection } from "./zarr_utils/types.js"; import type { ZarrLoaderFetchOptions } from "./OmeZarrLoader.js"; @@ -36,6 +36,16 @@ export function loadSpecToString(spec: LoadSpec): string { return `${spec.multiscaleLevel}:${spec.time}:x(${xmin},${xmax}):y(${ymin},${ymax}):z(${zmin},${zmax})`; } +export function defaultLoadSpec(): Required { + return { + ...new LoadSpec(), + maxAtlasEdge: MAX_ATLAS_EDGE, + scaleLevelBias: 0, + multiscaleLevel: 0, + channels: [0], + }; +} + export function cloneLoadSpec(spec: S): S { return { ...spec, From 424bd97624acc09808aac0abbff155b75ff8241e Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 5 May 2026 12:24:43 -0700 Subject: [PATCH 04/12] clean up an import --- src/Volume.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Volume.ts b/src/Volume.ts index 496a767a..f6a96d80 100644 --- a/src/Volume.ts +++ b/src/Volume.ts @@ -4,14 +4,8 @@ import Channel from "./Channel.js"; import Histogram from "./Histogram.js"; import { Lut } from "./Lut.js"; import { getColorByChannelIndex } from "./constants/colors.js"; -import { - cloneLoadSpec, - defaultLoadSpec, - type IVolumeLoader, - LoadSpec, - loadSpecSubregionAsBox3, - type PerChannelCallback, -} from "./loaders/IVolumeLoader.js"; +import type { IVolumeLoader, PerChannelCallback } from "./loaders/IVolumeLoader.js"; +import { cloneLoadSpec, defaultLoadSpec, LoadSpec, loadSpecSubregionAsBox3 } from "./loaders/IVolumeLoader.js"; import { pickLevelToLoadUnscaled } from "./loaders/VolumeLoaderUtils.js"; import type { NumberType, TypedArray } from "./types.js"; import { type ImageInfo, CImageInfo, defaultImageInfo } from "./ImageInfo.js"; From 7113eba8cb262535d4f0c7f651dc0bcc816d5d2b Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 5 May 2026 12:26:57 -0700 Subject: [PATCH 05/12] fix errors in all loaders except `OMEZarrLoader` --- src/loaders/JsonImageInfoLoader.ts | 6 ++---- src/loaders/RawArrayLoader.ts | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/loaders/JsonImageInfoLoader.ts b/src/loaders/JsonImageInfoLoader.ts index 8fe1115c..244831c6 100644 --- a/src/loaders/JsonImageInfoLoader.ts +++ b/src/loaders/JsonImageInfoLoader.ts @@ -1,5 +1,3 @@ -import { Box3, Vector3 } from "three"; - import { ThreadableVolumeLoader, type LoadSpec, @@ -206,10 +204,10 @@ class JsonImageInfoLoader extends ThreadableVolumeLoader { images = images.map((element) => ({ ...element, name: urlPrefix + element.name })); // Update `image`'s `loadSpec` before loading - const adjustedLoadSpec = { + const adjustedLoadSpec: LoadSpec = { ...loadSpec, // `subregion` and `multiscaleLevel` are unused by this loader - subregion: new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1)), + subregion: { min: [0, 0, 0], max: [1, 1, 1] }, multiscaleLevel: 0, // include all channels in any loaded images channels: images.flatMap(({ channels }) => channels), diff --git a/src/loaders/RawArrayLoader.ts b/src/loaders/RawArrayLoader.ts index 64e065a7..4b6635df 100644 --- a/src/loaders/RawArrayLoader.ts +++ b/src/loaders/RawArrayLoader.ts @@ -1,5 +1,3 @@ -import { Box3, Vector3 } from "three"; - import { ThreadableVolumeLoader, type LoadSpec, @@ -140,10 +138,10 @@ class RawArrayLoader extends ThreadableVolumeLoader { ): Promise { const requestedChannels = loadSpec.channels; - const adjustedLoadSpec = { + const adjustedLoadSpec: LoadSpec = { ...loadSpec, // `subregion` and `multiscaleLevel` are unused by this loader - subregion: new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1)), + subregion: { min: [0, 0, 0], max: [1, 1, 1] }, multiscaleLevel: 0, }; onUpdateMetadata(undefined, adjustedLoadSpec); From e402f14288e7c9a4014faf32dd9c8980d1ab3938 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 5 May 2026 12:37:44 -0700 Subject: [PATCH 06/12] break out vanilla region type --- src/Volume.ts | 17 +++++++++-------- src/loaders/IVolumeLoader.ts | 20 +++++++++++++++----- src/loaders/VolumeLoaderUtils.ts | 4 ++-- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/Volume.ts b/src/Volume.ts index f6a96d80..16a861ce 100644 --- a/src/Volume.ts +++ b/src/Volume.ts @@ -5,7 +5,7 @@ import Histogram from "./Histogram.js"; import { Lut } from "./Lut.js"; import { getColorByChannelIndex } from "./constants/colors.js"; import type { IVolumeLoader, PerChannelCallback } from "./loaders/IVolumeLoader.js"; -import { cloneLoadSpec, defaultLoadSpec, LoadSpec, loadSpecSubregionAsBox3 } from "./loaders/IVolumeLoader.js"; +import { cloneLoadSpec, defaultLoadSpec, LoadSpec, regionToBox3 } from "./loaders/IVolumeLoader.js"; import { pickLevelToLoadUnscaled } from "./loaders/VolumeLoaderUtils.js"; import type { NumberType, TypedArray } from "./types.js"; import { type ImageInfo, CImageInfo, defaultImageInfo } from "./ImageInfo.js"; @@ -161,8 +161,8 @@ export default class Volume { /** Returns `true` iff differences between `loadSpec` and `loadSpecRequired` indicate new data *must* be loaded. */ private mustLoadNewData(): boolean { const { loadSpec, loadSpecRequired } = this; - const currentRegion = loadSpecSubregionAsBox3(loadSpec); - const requiredRegion = loadSpecSubregionAsBox3(loadSpecRequired); + const currentRegion = regionToBox3(loadSpec.subregion); + const requiredRegion = regionToBox3(loadSpecRequired.subregion); return ( loadSpec.useExplicitLevel !== loadSpecRequired.useExplicitLevel || // explicit vs automatic level changed loadSpec.time !== loadSpecRequired.time || // time point changed @@ -181,13 +181,14 @@ export default class Volume { * imposed by `multiscaleLevel`. */ private mayLoadNewScaleLevel(): boolean { - const currentRegion = loadSpecSubregionAsBox3(this.loadSpec); - const requiredRegion = loadSpecSubregionAsBox3(this.loadSpecRequired); + const { loadSpec, loadSpecRequired } = this; + const currentRegion = regionToBox3(loadSpec.subregion); + const requiredRegion = regionToBox3(loadSpecRequired.subregion); return ( !currentRegion.equals(requiredRegion) || - this.loadSpecRequired.maxAtlasEdge !== this.loadSpec.maxAtlasEdge || - this.loadSpecRequired.multiscaleLevel !== this.loadSpec.multiscaleLevel || - this.loadSpecRequired.scaleLevelBias !== this.loadSpec.scaleLevelBias + loadSpecRequired.maxAtlasEdge !== loadSpec.maxAtlasEdge || + loadSpecRequired.multiscaleLevel !== loadSpec.multiscaleLevel || + loadSpecRequired.scaleLevelBias !== loadSpec.scaleLevelBias ); } diff --git a/src/loaders/IVolumeLoader.ts b/src/loaders/IVolumeLoader.ts index bdbf597e..243d9b71 100644 --- a/src/loaders/IVolumeLoader.ts +++ b/src/loaders/IVolumeLoader.ts @@ -8,6 +8,20 @@ import { createDefaultMetadata, MAX_ATLAS_EDGE } from "./VolumeLoaderUtils.js"; import { PrefetchDirection } from "./zarr_utils/types.js"; import type { ZarrLoaderFetchOptions } from "./OmeZarrLoader.js"; +/** A vanilla JS variant of `three`'s `Box3` type, to avoid using `three` types in our public interface */ +export type Region = { + min: [number, number, number]; + max: [number, number, number]; +}; + +export function regionToBox3(region: Region): Box3 { + return new Box3(new Vector3(...region.min), new Vector3(...region.max)); +} + +export function box3ToRegion(box: Box3): Region { + return { min: box.min.toArray(), max: box.max.toArray() }; +} + export class LoadSpec { time = 0; /** The max size of a volume atlas that may be produced by a load. Used to pick the appropriate multiscale level. */ @@ -20,7 +34,7 @@ export class LoadSpec { */ multiscaleLevel?: number; /** Subregion of volume to load. If not specified, the entire volume is loaded. Specify as floats between 0-1. */ - subregion: { min: [number, number, number]; max: [number, number, number] } = { min: [0, 0, 0], max: [1, 1, 1] }; + subregion: Region = { min: [0, 0, 0], max: [1, 1, 1] }; channels?: number[]; /** Treat multiscaleLevel literally and don't use other constraints to change it. * By default we will try to load the best level based on the maxAtlasEdge and scaleLevelBias, @@ -54,10 +68,6 @@ export function cloneLoadSpec(spec: S): S { }; } -export function loadSpecSubregionAsBox3({ subregion }: LoadSpec): Box3 { - return new Box3(new Vector3(...subregion.min), new Vector3(...subregion.max)); -} - export type LoadedVolumeInfo = { imageInfo: ImageInfo; loadSpec: LoadSpec; diff --git a/src/loaders/VolumeLoaderUtils.ts b/src/loaders/VolumeLoaderUtils.ts index 4e63371a..0bf819f9 100644 --- a/src/loaders/VolumeLoaderUtils.ts +++ b/src/loaders/VolumeLoaderUtils.ts @@ -1,7 +1,7 @@ import { Box3, Vector2, Vector3 } from "three"; import { CImageInfo, type ImageInfo } from "../ImageInfo.js"; -import { LoadSpec, loadSpecSubregionAsBox3 } from "./IVolumeLoader.js"; +import { LoadSpec, regionToBox3 } from "./IVolumeLoader.js"; export const MAX_ATLAS_EDGE = 4096; @@ -175,7 +175,7 @@ export function pickLevelToLoadUnscaled(loadSpec: LoadSpec, spatialDimsZYX: ZYX[ * `LoadSpec`'s `subregion` property. */ export function pickLevelToLoad(loadSpec: LoadSpec, spatialDimsZYX: ZYX[]): number { - const region = loadSpecSubregionAsBox3(loadSpec); + const region = regionToBox3(loadSpec.subregion); const scaledDims = scaleMultipleDimsToSubregion(region, spatialDimsZYX); return pickLevelToLoadUnscaled(loadSpec, scaledDims); } From 958f49a347711c9ae6b7b5d841fe4959e7ce80bc Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 5 May 2026 12:45:11 -0700 Subject: [PATCH 07/12] fix up `OMEZarrLoader` --- src/loaders/OmeZarrLoader.ts | 17 ++++++++++------- src/loaders/VolumeLoaderUtils.ts | 18 ++++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/loaders/OmeZarrLoader.ts b/src/loaders/OmeZarrLoader.ts index b11bd96e..6bd3d96a 100644 --- a/src/loaders/OmeZarrLoader.ts +++ b/src/loaders/OmeZarrLoader.ts @@ -1,4 +1,4 @@ -import { Box3, Vector3 } from "three"; +import { Vector3 } from "three"; import * as zarr from "zarrita"; const { slice } = zarr; @@ -8,11 +8,14 @@ import type { VolumeDims } from "../VolumeDims.js"; import VolumeCache from "../VolumeCache.js"; import { getDataRange } from "../utils/num_utils.js"; import SubscribableRequestQueue from "../utils/SubscribableRequestQueue.js"; +// TODO cleanup import import { ThreadableVolumeLoader, LoadSpec, type RawChannelDataCallback, type LoadedVolumeInfo, + type Region, + box3ToRegion, } from "./IVolumeLoader.js"; import { composeSubregion, @@ -100,7 +103,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader { // TODO: this property should definitely be owned by `Volume` if this loader is ever used by multiple volumes. // This may cause errors or incorrect results otherwise! - private maxExtent?: Box3; + private maxExtent?: Region; private syncChannels = false; @@ -293,7 +296,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader { loadDims(loadSpec: LoadSpec): Promise { const [spaceUnit, timeUnit] = this.getUnitSymbols(); // Compute subregion size so we can factor that in - const maxExtent = this.maxExtent ?? new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1)); + const maxExtent = this.maxExtent ?? { min: [0, 0, 0], max: [1, 1, 1] }; const subregion = composeSubregion(loadSpec.subregion, maxExtent); const regionSize = subregion.getSize(new Vector3()); const regionArr = [1, 1, regionSize.z, regionSize.y, regionSize.x]; @@ -350,7 +353,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader { } if (!this.maxExtent) { - this.maxExtent = loadSpec.subregion.clone(); + this.maxExtent = { min: [...loadSpec.subregion.min], max: [...loadSpec.subregion.max] }; } // from source 0: @@ -423,7 +426,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader { // which the volume contains. The volume contains the full extent of the subset recognized by this loader. const fullExtentLoadSpec: LoadSpec = { ...loadSpec, - subregion: new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1)), + subregion: { min: [0, 0, 0], max: [1, 1, 1] }, }; return Promise.resolve({ imageInfo: imgdata, loadSpec: fullExtentLoadSpec }); @@ -496,8 +499,8 @@ class OMEZarrLoader extends ThreadableVolumeLoader { private updateImageInfoForLoad(imageInfo: ImageInfo, loadSpec: LoadSpec): ImageInfo { // Apply `this.maxExtent` to subregion, if it exists - const maxExtent = this.maxExtent ?? new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1)); - const subregion = composeSubregion(loadSpec.subregion, maxExtent); + const maxExtent = this.maxExtent ?? { min: [0, 0, 0], max: [1, 1, 1] }; + const subregion = box3ToRegion(composeSubregion(loadSpec.subregion, maxExtent)); // Pick the level to load based on the subregion size const multiscaleLevel = pickLevelToLoad({ ...loadSpec, subregion }, this.getLevelShapesZYX()); diff --git a/src/loaders/VolumeLoaderUtils.ts b/src/loaders/VolumeLoaderUtils.ts index 0bf819f9..c68344cd 100644 --- a/src/loaders/VolumeLoaderUtils.ts +++ b/src/loaders/VolumeLoaderUtils.ts @@ -1,7 +1,7 @@ import { Box3, Vector2, Vector3 } from "three"; import { CImageInfo, type ImageInfo } from "../ImageInfo.js"; -import { LoadSpec, regionToBox3 } from "./IVolumeLoader.js"; +import { LoadSpec, type Region, regionToBox3 } from "./IVolumeLoader.js"; export const MAX_ATLAS_EDGE = 4096; @@ -181,9 +181,9 @@ export function pickLevelToLoad(loadSpec: LoadSpec, spatialDimsZYX: ZYX[]): numb } /** Given the size of a volume in pixels, convert a `Box3` in the 0-1 range to pixels */ -export function convertSubregionToPixels(region: Box3, size: Vector3): Box3 { - const min = region.min.clone().multiply(size).floor(); - const max = region.max.clone().multiply(size).ceil(); +export function convertSubregionToPixels(region: Region, size: Vector3): Box3 { + const min = new Vector3(...region.min).multiply(size).floor(); + const max = new Vector3(...region.max).multiply(size).ceil(); // ensure it's always valid to specify the same number at both ends and get a single slice if (min.x === max.x && min.x < size.x) { @@ -203,10 +203,12 @@ export function convertSubregionToPixels(region: Box3, size: Vector3): Box3 { * Return the subset of `container` specified by `region`, assuming that `region` contains fractional values (between 0 * and 1). i.e. if `container`'s range on the X axis is 0-4 and `region`'s is 0.25-0.5, the result will have range 1-2. */ -export function composeSubregion(region: Box3, container: Box3): Box3 { - const size = container.getSize(new Vector3()); - const min = region.min.clone().multiply(size).add(container.min); - const max = region.max.clone().multiply(size).add(container.min); +export function composeSubregion(region: Region, container: Region): Box3 { + const regionBox = regionToBox3(region); + const containerBox = regionToBox3(container); + const size = containerBox.getSize(new Vector3()); + const min = regionBox.min.clone().multiply(size).add(containerBox.min); + const max = regionBox.max.clone().multiply(size).add(containerBox.min); return new Box3(min, max); } From 32dbbd729da8ac80668e80a4800541b5e2254c29 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 5 May 2026 12:45:39 -0700 Subject: [PATCH 08/12] cleanup import --- src/loaders/OmeZarrLoader.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/loaders/OmeZarrLoader.ts b/src/loaders/OmeZarrLoader.ts index 6bd3d96a..46a351cb 100644 --- a/src/loaders/OmeZarrLoader.ts +++ b/src/loaders/OmeZarrLoader.ts @@ -8,15 +8,8 @@ import type { VolumeDims } from "../VolumeDims.js"; import VolumeCache from "../VolumeCache.js"; import { getDataRange } from "../utils/num_utils.js"; import SubscribableRequestQueue from "../utils/SubscribableRequestQueue.js"; -// TODO cleanup import -import { - ThreadableVolumeLoader, - LoadSpec, - type RawChannelDataCallback, - type LoadedVolumeInfo, - type Region, - box3ToRegion, -} from "./IVolumeLoader.js"; +import type { RawChannelDataCallback, LoadedVolumeInfo, Region } from "./IVolumeLoader.js"; +import { ThreadableVolumeLoader, LoadSpec, box3ToRegion } from "./IVolumeLoader.js"; import { composeSubregion, computePackedAtlasDims, From e7dbe5c4460cc29201a47f5d323d40edddcca3b1 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 5 May 2026 12:55:17 -0700 Subject: [PATCH 09/12] remove now-unnecessary util method (and module!) --- src/workers/VolumeLoadWorker.ts | 7 +++---- src/workers/VolumeLoaderContext.ts | 7 ++----- src/workers/util.ts | 11 ----------- 3 files changed, 5 insertions(+), 20 deletions(-) delete mode 100644 src/workers/util.ts diff --git a/src/workers/VolumeLoadWorker.ts b/src/workers/VolumeLoadWorker.ts index cdcd0d76..687d12ee 100644 --- a/src/workers/VolumeLoadWorker.ts +++ b/src/workers/VolumeLoadWorker.ts @@ -14,7 +14,6 @@ import type { WorkerResponsePayload, } from "./types.js"; import { WorkerEventType, WorkerMsgType, WorkerResponseResult } from "./types.js"; -import { rebuildLoadSpec } from "./util.js"; type LoaderEntry = { loader: ThreadableVolumeLoader; copyOnLoad: boolean }; @@ -74,12 +73,12 @@ const messageHandlers: { [T in WorkerMsgType]: MessageHandler } = { [WorkerMsgType.CREATE_VOLUME]: async (loadSpec, loaderId) => { const { loader } = getLoader(loaderId); - return await loader.createImageInfo(rebuildLoadSpec(loadSpec)); + return await loader.createImageInfo(loadSpec); }, [WorkerMsgType.LOAD_DIMS]: async (loadSpec, loaderId) => { const { loader } = getLoader(loaderId); - return await loader.loadDims(rebuildLoadSpec(loadSpec)); + return await loader.loadDims(loadSpec); }, [WorkerMsgType.LOAD_VOLUME_DATA]: ({ imageInfo, loadSpec, loadId }, loaderId) => { @@ -87,7 +86,7 @@ const messageHandlers: { [T in WorkerMsgType]: MessageHandler } = { return loader.loadRawChannelData( imageInfo, - rebuildLoadSpec(loadSpec), + loadSpec, (imageInfo, loadSpec) => { const message: WorkerResponse = { responseResult: WorkerResponseResult.EVENT, diff --git a/src/workers/VolumeLoaderContext.ts b/src/workers/VolumeLoaderContext.ts index 124dd971..090fb21c 100644 --- a/src/workers/VolumeLoaderContext.ts +++ b/src/workers/VolumeLoaderContext.ts @@ -23,7 +23,6 @@ import type { } from "./types.js"; import type { ZarrLoaderFetchOptions } from "../loaders/OmeZarrLoader.js"; import { WorkerMsgType, WorkerResponseResult, WorkerEventType } from "./types.js"; -import { rebuildLoadSpec } from "./util.js"; type StoredPromise = { type: T; @@ -299,7 +298,7 @@ class WorkerLoader extends ThreadableVolumeLoader { loadSpec, this.getLoaderId() ); - return { imageInfo, loadSpec: rebuildLoadSpec(adjustedLoadSpec) }; + return { imageInfo, loadSpec: adjustedLoadSpec }; } loadRawChannelData( @@ -333,9 +332,7 @@ class WorkerLoader extends ThreadableVolumeLoader { return; } - const imageInfo = e.imageInfo; - const loadSpec = e.loadSpec && rebuildLoadSpec(e.loadSpec); - this.currentMetadataUpdateCallback?.(imageInfo, loadSpec); + this.currentMetadataUpdateCallback?.(e.imageInfo, e.loadSpec); } } diff --git a/src/workers/util.ts b/src/workers/util.ts deleted file mode 100644 index ddc1453e..00000000 --- a/src/workers/util.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Box3, Vector3 } from "three"; - -import { LoadSpec } from "../loaders/IVolumeLoader.js"; - -/** Recreates a `LoadSpec` that has just been sent to/from a worker to restore three.js object prototypes */ -export function rebuildLoadSpec(spec: LoadSpec): LoadSpec { - return { - ...spec, - subregion: new Box3(new Vector3().copy(spec.subregion.min), new Vector3().copy(spec.subregion.max)), - }; -} From 31b1970fed6936c859c3d76aa16fb9a3f5620bb7 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 5 May 2026 12:55:29 -0700 Subject: [PATCH 10/12] fix remaining errors --- src/Atlas2DSlice.ts | 2 +- src/VolumeDrawable.ts | 4 ++-- src/test/RequestQueue.test.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Atlas2DSlice.ts b/src/Atlas2DSlice.ts index a69d18f1..21c9c301 100644 --- a/src/Atlas2DSlice.ts +++ b/src/Atlas2DSlice.ts @@ -187,7 +187,7 @@ export default class Atlas2DSlice implements VolumeRenderImpl { 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)), + subregion: { min: [0, 0, sliceLowerBound], max: [1, 1, sliceUpperBound] }, }); } } diff --git a/src/VolumeDrawable.ts b/src/VolumeDrawable.ts index 32af925a..bcaa644a 100644 --- a/src/VolumeDrawable.ts +++ b/src/VolumeDrawable.ts @@ -845,7 +845,7 @@ export default class VolumeDrawable { switch (newRenderMode) { case RenderMode.PATHTRACE: this.volumeRendering = new PathTracedVolume(this.volume, this.settings); - this.volume.updateRequiredData({ subregion: new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1)) }); + this.volume.updateRequiredData({ subregion: { min: [0, 0, 0], max: [1, 1, 1] } }); this.volumeRendering.setRenderUpdateListener(this.renderUpdateListener); break; case RenderMode.SLICE: @@ -855,7 +855,7 @@ export default class VolumeDrawable { case RenderMode.RAYMARCH: default: this.volumeRendering = new RayMarchedAtlasVolume(this.volume, this.settings); - this.volume.updateRequiredData({ subregion: new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1)) }); + this.volume.updateRequiredData({ subregion: { min: [0, 0, 0], max: [1, 1, 1] } }); break; } if (this.pickRendering) { diff --git a/src/test/RequestQueue.test.ts b/src/test/RequestQueue.test.ts index fb3445c6..2734df24 100644 --- a/src/test/RequestQueue.test.ts +++ b/src/test/RequestQueue.test.ts @@ -4,7 +4,7 @@ import { Vector3 } from "three"; import type { TypedArray } from "zarrita"; import RequestQueue, { type Request } from "../utils/RequestQueue.js"; -import { LoadSpec, loadSpecToString } from "../loaders/IVolumeLoader.js"; +import { LoadSpec, loadSpecToString, regionToBox3 } from "../loaders/IVolumeLoader.js"; /** * Returns a promise that resolves once the timeout (give in ms) is completed. @@ -404,7 +404,7 @@ describe("test RequestQueue", () => { }); async function mockLoader(loadSpec: Required, maxDelayMs = 10.0): Promise> { - const { x, y, z } = loadSpec.subregion.getSize(new Vector3()); + const { x, y, z } = regionToBox3(loadSpec.subregion).getSize(new Vector3()); const data = new Uint8Array(x * y * z); const delayMs = Math.random() * maxDelayMs; @@ -426,8 +426,8 @@ describe("test RequestQueue", () => { const requests: Request[] = []; for (let i = startingFrame; i < startingFrame + frames; i++) { const loadSpec = new LoadSpec(); - loadSpec.subregion.min.set(0, 0, i); - loadSpec.subregion.max.set(xDim, yDim, i + 1); + loadSpec.subregion.min = [0, 0, i]; + loadSpec.subregion.max = [xDim, yDim, i + 1]; requests.push({ key: loadSpecToString(loadSpec), From c5fd64f45b58b45887934682c2515263842e38c3 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 5 May 2026 13:06:56 -0700 Subject: [PATCH 11/12] remove now-unused import --- src/VolumeDrawable.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/VolumeDrawable.ts b/src/VolumeDrawable.ts index bcaa644a..ff45ef76 100644 --- a/src/VolumeDrawable.ts +++ b/src/VolumeDrawable.ts @@ -3,7 +3,6 @@ import { Object3D, Euler, Vector2, - Box3, DepthTexture, OrthographicCamera, PerspectiveCamera, From 2e2fde5405b48d7e9bd6ddc074b84831979ca447 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 12 May 2026 13:56:57 -0700 Subject: [PATCH 12/12] reorder imports --- src/loaders/IVolumeLoader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loaders/IVolumeLoader.ts b/src/loaders/IVolumeLoader.ts index 243d9b71..f838c201 100644 --- a/src/loaders/IVolumeLoader.ts +++ b/src/loaders/IVolumeLoader.ts @@ -1,4 +1,4 @@ -import { Vector3, Box3 } from "three"; +import { Box3, Vector3 } from "three"; import Volume from "../Volume.js"; import type { VolumeDims } from "../VolumeDims.js";