@aics/vole-core 4.3.1 → 4.4.1

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.
@@ -0,0 +1,72 @@
1
+ import { Group, Mesh, Vector3 } from "three";
2
+ /**
3
+ * Abstract base class for drawable 3D mesh objects.
4
+ *
5
+ * Provides default implementations for some methods in the IDrawableObject
6
+ * interface, including handling for visibility, transformations, and cleanup.
7
+ *
8
+ * As a default, subclasses should call `this.addChildMesh(mesh)` to register
9
+ * any meshes that should be managed by the base class.
10
+ */
11
+ export default class BaseDrawableMeshObject {
12
+ /**
13
+ * Pivot group that all child meshes are parented to. Transformations are
14
+ * applied to this group.
15
+ */
16
+
17
+ constructor() {
18
+ this.meshPivot = new Group();
19
+ this.scale = new Vector3(1, 1, 1);
20
+ this.flipAxes = new Vector3(1, 1, 1);
21
+ }
22
+ addChildMesh(mesh) {
23
+ this.meshPivot.add(mesh);
24
+ }
25
+ removeChildMesh(mesh) {
26
+ this.meshPivot.remove(mesh);
27
+ }
28
+ cleanup() {
29
+ this.meshPivot.traverse(obj => {
30
+ if (obj instanceof Mesh) {
31
+ obj.geometry.dispose();
32
+ obj.material.dispose();
33
+ }
34
+ });
35
+ this.meshPivot.clear();
36
+ }
37
+ doRender() {
38
+ // no op
39
+ }
40
+ setVisible(visible) {
41
+ this.meshPivot.visible = visible;
42
+ }
43
+ get3dObject() {
44
+ return this.meshPivot;
45
+ }
46
+ setTranslation(translation) {
47
+ this.meshPivot.position.copy(translation);
48
+ }
49
+ setScale(scale) {
50
+ this.scale.copy(scale);
51
+ this.meshPivot.scale.copy(scale).multiply(this.flipAxes);
52
+ }
53
+ setRotation(eulerXYZ) {
54
+ this.meshPivot.rotation.copy(eulerXYZ);
55
+ }
56
+ setFlipAxes(flipX, flipY, flipZ) {
57
+ this.flipAxes.set(flipX, flipY, flipZ);
58
+ this.meshPivot.scale.copy(this.scale).multiply(this.flipAxes);
59
+ }
60
+ setOrthoThickness(_thickness) {
61
+ // no op
62
+ }
63
+ setResolution(_x, _y) {
64
+ // no op
65
+ }
66
+ setAxisClip(_axis, _minval, _maxval, _isOrthoAxis) {
67
+ // no op
68
+ }
69
+ updateClipRegion(_xmin, _xmax, _ymin, _ymax, _zmin, _zmax) {
70
+ // no op
71
+ }
72
+ }
package/es/Channel.js CHANGED
@@ -157,7 +157,7 @@ export default class Channel {
157
157
  };
158
158
  this.rebuildDataTexture(this.imgData.data, w, h);
159
159
  this.loaded = true;
160
- this.histogram = new Histogram(bitsArray);
160
+ this.histogram = new Histogram(bitsArray, rawMin, rawMax);
161
161
  this.frame = frame;
162
162
 
163
163
  // reuse old lut but auto-remap it to new data range
@@ -205,7 +205,7 @@ export default class Channel {
205
205
  this.loaded = true;
206
206
  // update from current histogram?
207
207
  this.setRawDataRange(rawMin, rawMax);
208
- this.histogram = new Histogram(this.volumeData);
208
+ this.histogram = new Histogram(this.volumeData, rawMin, rawMax);
209
209
  }
210
210
 
211
211
  // given this.volumeData, let's unpack it into a flat textureatlas and fill up this.imgData.
package/es/Histogram.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isFloatTypeArray } from "./types.js";
2
+ import { getDataRange } from "./utils/num_utils.js";
2
3
  const NBINS = 256;
3
4
  /**
4
5
  * Builds a histogram with 256 bins from a data array. Assume data is 8 bit single channel grayscale.
@@ -18,7 +19,7 @@ export default class Histogram {
18
19
 
19
20
  /** Index of the last bin (other than 0) with at least 1 value. */
20
21
 
21
- constructor(data) {
22
+ constructor(data, dataMin = undefined, dataMax = undefined) {
22
23
  this.dataMinBin = 0;
23
24
  this.dataMaxBin = 0;
24
25
  this.maxBin = 0;
@@ -28,7 +29,7 @@ export default class Histogram {
28
29
  this.binSize = 0;
29
30
 
30
31
  // build up the histogram
31
- const hinfo = Histogram.calculateHistogram(data, NBINS);
32
+ const hinfo = Histogram.calculateHistogram(data, NBINS, dataMin, dataMax);
32
33
  this.bins = hinfo.bins;
33
34
  this.min = hinfo.min;
34
35
  this.max = hinfo.max;
@@ -233,24 +234,19 @@ export default class Histogram {
233
234
  }
234
235
  return [b, e];
235
236
  }
236
- static calculateHistogram(arr, numBins = 1) {
237
+ static calculateHistogram(arr, numBins = 1, dataMin = undefined, dataMax = undefined) {
237
238
  if (numBins < 1) {
238
239
  numBins = 1;
239
240
  }
240
241
 
241
- // calculate min and max of arr
242
- // TODO See convertChannel, which will also compute min and max!
243
- // We could save a whole extra loop over the data, or have convertChannel compute the whole histogram.
244
- // need to be careful about computing over chunks or whole ready-to-display volume
242
+ // ASSUMPTION: we will trust the min and max if provided.
243
+ let min = dataMin !== undefined ? dataMin : arr[0];
244
+ let max = dataMax !== undefined ? dataMax : arr[0];
245
245
 
246
- let min = arr[0];
247
- let max = arr[0];
248
- for (let i = 1; i < arr.length; i++) {
249
- if (arr[i] < min) {
250
- min = arr[i];
251
- } else if (arr[i] > max) {
252
- max = arr[i];
253
- }
246
+ // Find min and max in the array if the user did not provide them.
247
+ // Note that this is a completely separate walk through the data array which could be expensive.
248
+ if (dataMin === undefined || dataMax === undefined) {
249
+ [min, max] = getDataRange(arr);
254
250
  }
255
251
  const bins = new Uint32Array(numBins).fill(0);
256
252
 
package/es/Line3d.js CHANGED
@@ -1,16 +1,17 @@
1
- import { Group, Vector3 } from "three";
2
1
  import { LineMaterial } from "three/addons/lines/LineMaterial.js";
3
2
  import { LineSegments2 } from "three/addons/lines/LineSegments2.js";
4
3
  import { LineSegmentsGeometry } from "three/addons/lines/LineSegmentsGeometry.js";
5
4
  import { MESH_NO_PICK_OCCLUSION_LAYER, OVERLAY_LAYER } from "./ThreeJsPanel.js";
5
+ import BaseDrawableMeshObject from "./BaseDrawableMeshObject.js";
6
6
  const DEFAULT_VERTEX_BUFFER_SIZE = 1020;
7
7
 
8
8
  /**
9
9
  * Simple wrapper for a 3D line segments object, with controls for vertex data,
10
10
  * color, width, and segments visible.
11
11
  */
12
- export default class Line3d {
12
+ export default class Line3d extends BaseDrawableMeshObject {
13
13
  constructor() {
14
+ super();
14
15
  this.bufferSize = DEFAULT_VERTEX_BUFFER_SIZE;
15
16
  const geometry = new LineSegmentsGeometry();
16
17
  geometry.setPositions(new Float32Array(this.bufferSize));
@@ -27,55 +28,10 @@ export default class Line3d {
27
28
  // artifacts can occur where contours are drawn around lines. This layer
28
29
  // (MESH_NO_PICK_OCCLUSION_LAYER) does not occlude/interact with the pick
29
30
  // buffer but still writes depth information for the volume.
31
+ this.meshPivot.layers.set(MESH_NO_PICK_OCCLUSION_LAYER);
30
32
  this.lineMesh.layers.set(MESH_NO_PICK_OCCLUSION_LAYER);
31
33
  this.lineMesh.frustumCulled = false;
32
- this.meshPivot = new Group();
33
- this.meshPivot.add(this.lineMesh);
34
- this.meshPivot.layers.set(MESH_NO_PICK_OCCLUSION_LAYER);
35
- this.scale = new Vector3(1, 1, 1);
36
- this.flipAxes = new Vector3(1, 1, 1);
37
- }
38
-
39
- // IDrawableObject interface methods
40
-
41
- cleanup() {
42
- this.lineMesh.geometry.dispose();
43
- this.lineMesh.material.dispose();
44
- }
45
- setVisible(visible) {
46
- this.lineMesh.visible = visible;
47
- }
48
- doRender() {
49
- // no op
50
- }
51
- get3dObject() {
52
- return this.meshPivot;
53
- }
54
- setTranslation(translation) {
55
- this.meshPivot.position.copy(translation);
56
- }
57
- setScale(scale) {
58
- this.scale.copy(scale);
59
- this.meshPivot.scale.copy(scale).multiply(this.flipAxes);
60
- }
61
- setRotation(eulerXYZ) {
62
- this.meshPivot.rotation.copy(eulerXYZ);
63
- }
64
- setFlipAxes(flipX, flipY, flipZ) {
65
- this.flipAxes.set(flipX, flipY, flipZ);
66
- this.meshPivot.scale.copy(this.scale).multiply(this.flipAxes);
67
- }
68
- setOrthoThickness(_thickness) {
69
- // no op
70
- }
71
- setResolution(_x, _y) {
72
- // no op
73
- }
74
- setAxisClip(_axis, _minval, _maxval, _isOrthoAxis) {
75
- // no op
76
- }
77
- updateClipRegion(_xmin, _xmax, _ymin, _ymax, _zmin, _zmax) {
78
- // no op
34
+ this.addChildMesh(this.lineMesh);
79
35
  }
80
36
 
81
37
  // Line-specific functions
package/es/MeshVolume.js CHANGED
@@ -250,11 +250,12 @@ export default class MeshVolume {
250
250
  this.meshrep[channel] = null;
251
251
  }
252
252
  }
253
- saveChannelIsosurface(channelIndex, type, namePrefix) {
253
+ saveChannelIsosurface(channelIndex, type, name) {
254
254
  const meshrep = this.meshrep[channelIndex];
255
255
  if (!meshrep) {
256
256
  return;
257
257
  }
258
+ const namePrefix = name !== undefined ? `${name}_` : "";
258
259
  if (type === "STL") {
259
260
  this.exportSTL(meshrep, namePrefix + "_" + this.volume.channelNames[channelIndex]);
260
261
  } else if (type === "GLTF") {
@@ -0,0 +1,324 @@
1
+ import BaseDrawableMeshObject from "./BaseDrawableMeshObject.js";
2
+ import { MESH_NO_PICK_OCCLUSION_LAYER } from "./ThreeJsPanel.js";
3
+ import { InstancedMesh, CylinderGeometry, ConeGeometry, Object3D, Vector3, MeshBasicMaterial, Color, DynamicDrawUsage, Matrix4 } from "three";
4
+ // Unscaled arrowhead dimensions.
5
+ const SHAFT_BASE_RADIUS = 0.5;
6
+ const HEAD_BASE_RADIUS = 1.5;
7
+ const HEAD_BASE_HEIGHT = 4;
8
+
9
+ /** Default arrow shaft thickness, in world units. */
10
+ const DEFAULT_DIAMETER = 0.002;
11
+ const DEFAULT_INSTANCE_COUNT = 256;
12
+
13
+ /**
14
+ * A drawable vector arrow field, which uses instanced meshes for performance.
15
+ */
16
+ export default class VectorArrows3d extends BaseDrawableMeshObject {
17
+ /**
18
+ * Scale of this object in world coordinates, when unscaled. Used to
19
+ * compensate for parent transforms in order to keep arrow meshes from being
20
+ * distorted.
21
+ */
22
+
23
+ // Temporary calculation objects. Optimization taken from three.js examples.
24
+
25
+ constructor() {
26
+ super();
27
+ this.worldScale = new Vector3(1, 1, 1);
28
+ this.meshPivot.layers.set(MESH_NO_PICK_OCCLUSION_LAYER);
29
+ this.maxInstanceCount = DEFAULT_INSTANCE_COUNT;
30
+ const {
31
+ headMesh: headMesh,
32
+ shaftMesh: shaftMesh
33
+ } = this.initInstancedMeshes(DEFAULT_INSTANCE_COUNT);
34
+ this.headInstancedMesh = headMesh;
35
+ this.shaftInstancedMesh = shaftMesh;
36
+ this.headInstancedMesh.count = 0;
37
+ this.shaftInstancedMesh.count = 0;
38
+ this.positions = null;
39
+ this.deltas = null;
40
+ this.colors = null;
41
+ this.diameter = new Float32Array([DEFAULT_DIAMETER]);
42
+ this.tempDst = new Vector3();
43
+ this.tempScale = new Vector3();
44
+ this.tempMatrix = new Object3D();
45
+ this.onParentTransformUpdated();
46
+ }
47
+
48
+ /**
49
+ * Returns (unscaled) buffer geometry for the head and shaft parts of the
50
+ * arrow.
51
+ * @returns
52
+ * - `head`: BufferGeometry for the arrowhead, a cone pointing along the +Z
53
+ * axis, with the pivot at the tip of the cone. Height and radius are based
54
+ * on constant values (`HEAD_BASE_HEIGHT` and `HEAD_BASE_RADIUS`).
55
+ * - `shaft`: BufferGeometry for the cylindrical arrow shaft. The cylinder
56
+ * points along the +Z axis, with the pivot at the base of the cylinder.
57
+ * Height is 1 and diameter is 1.
58
+ *
59
+ * ```txt
60
+ * ^ +Z axis ^
61
+ * _____ x
62
+ * | | / \
63
+ * | | / \
64
+ * --x-- /-----\
65
+ *
66
+ * x = pivot (0,0,0)
67
+ * ```
68
+ */
69
+ static generateGeometry() {
70
+ // TODO: Currently the shape of the arrow head is fixed. Allow configuring
71
+ // this in the future?
72
+ const cylinderGeometry = new CylinderGeometry(SHAFT_BASE_RADIUS, SHAFT_BASE_RADIUS, 2 * SHAFT_BASE_RADIUS,
73
+ // height
74
+ 8,
75
+ // radial segments
76
+ 1,
77
+ // height segments
78
+ false // capped ends
79
+ );
80
+ const coneRadius = HEAD_BASE_RADIUS;
81
+ const coneHeight = HEAD_BASE_HEIGHT;
82
+ const coneGeometry = new ConeGeometry(coneRadius, coneHeight, 12);
83
+
84
+ // Rotate both to point along +Z axis
85
+ const rotateToPositiveZ = new Matrix4().makeRotationX(Math.PI / 2);
86
+ // Change cone pivot to be at the tip.
87
+ const coneTranslation = new Matrix4().makeTranslation(0, 0, -coneHeight / 2);
88
+ coneGeometry.applyMatrix4(coneTranslation.multiply(rotateToPositiveZ));
89
+ // Change cylinder pivot to be at the base.
90
+ const cylinderTranslation = new Matrix4().makeTranslation(0, 0, 0.5);
91
+ cylinderGeometry.applyMatrix4(cylinderTranslation.multiply(rotateToPositiveZ));
92
+ return {
93
+ head: coneGeometry,
94
+ shaft: cylinderGeometry
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Create new instanced meshes with the specified instance count, and adds
100
+ * them to the mesh pivot and internal meshes array for future cleanup.
101
+ *
102
+ * If calling outside of the constructor, be sure to call `cleanup()` first.
103
+ */
104
+ initInstancedMeshes(instanceCount) {
105
+ this.cleanup();
106
+ this.meshPivot.clear();
107
+ const basicMaterial = new MeshBasicMaterial({
108
+ color: "#fff"
109
+ });
110
+ const {
111
+ head: headGeometry,
112
+ shaft: shaftGeometry
113
+ } = VectorArrows3d.generateGeometry();
114
+ const headMesh = new InstancedMesh(headGeometry, basicMaterial, instanceCount);
115
+ const shaftMesh = new InstancedMesh(shaftGeometry, basicMaterial, instanceCount);
116
+ headMesh.layers.set(MESH_NO_PICK_OCCLUSION_LAYER);
117
+ shaftMesh.layers.set(MESH_NO_PICK_OCCLUSION_LAYER);
118
+ headMesh.frustumCulled = false;
119
+ shaftMesh.frustumCulled = false;
120
+ headMesh.instanceMatrix.setUsage(DynamicDrawUsage);
121
+ shaftMesh.instanceMatrix.setUsage(DynamicDrawUsage);
122
+ this.addChildMesh(headMesh);
123
+ this.addChildMesh(shaftMesh);
124
+ return {
125
+ headMesh,
126
+ shaftMesh
127
+ };
128
+ }
129
+ increaseInstanceCountMax(instanceCount) {
130
+ // Max instance count is set when instanced meshes are created. If we need
131
+ // to increase the max, we need to recreate the instanced meshes.
132
+ let newInstanceCount = this.maxInstanceCount;
133
+ while (newInstanceCount < instanceCount) {
134
+ newInstanceCount *= 2;
135
+ }
136
+ // Delete existing meshes
137
+ this.cleanup();
138
+ const {
139
+ headMesh,
140
+ shaftMesh
141
+ } = this.initInstancedMeshes(newInstanceCount);
142
+ this.headInstancedMesh = headMesh;
143
+ this.shaftInstancedMesh = shaftMesh;
144
+ this.maxInstanceCount = newInstanceCount;
145
+ }
146
+ setScale(scale) {
147
+ if (scale !== this.scale) {
148
+ this.onParentTransformUpdated();
149
+ this.scale.copy(scale);
150
+ if (this.positions && this.deltas) {
151
+ // Update arrows
152
+ this.setArrowData(this.positions, this.deltas);
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Called when scaling of parent transforms has been updated or whenever
159
+ * vector data is updated.
160
+ */
161
+ onParentTransformUpdated() {
162
+ // Measure world scale by temporarily resetting mesh pivot scale
163
+ this.meshPivot.scale.set(1, 1, 1);
164
+ let newWorldScale = new Vector3();
165
+ newWorldScale = this.meshPivot.getWorldScale(newWorldScale);
166
+
167
+ // Scale is inverted on mesh pivot to cancel out parent transforms (though
168
+ // translation and rotation are still affected by any parent transforms).
169
+ // This allows arrows meshes to be scaled 1:1 with world space, regardless
170
+ // of parent transforms, and prevents distortion or skewing of the mesh.
171
+ // Parent scaling is applied to arrow positions and deltas (see
172
+ // `updateAllArrowTransforms`), rather than the meshes themselves.
173
+ const invertScale = new Vector3(1, 1, 1).divide(newWorldScale);
174
+ this.meshPivot.scale.copy(invertScale);
175
+ if (!newWorldScale.equals(this.worldScale)) {
176
+ this.worldScale.copy(newWorldScale);
177
+ if (this.positions && this.deltas) {
178
+ this.setArrowData(this.positions, this.deltas);
179
+ }
180
+ }
181
+ }
182
+ updateSingleArrowTransform(index, src, delta, diameter) {
183
+ // Update the arrow shaft
184
+ const headHeight = HEAD_BASE_HEIGHT * diameter;
185
+ const length = delta.length();
186
+ const shaftHeight = Math.max(length - headHeight, 0);
187
+ if (shaftHeight < 1e-6) {
188
+ // If the shaft height is too small, scale to 0.
189
+ this.tempScale.set(0, 0, 0);
190
+ } else {
191
+ this.tempScale.set(diameter, diameter, shaftHeight);
192
+ }
193
+ this.tempMatrix.scale.copy(this.tempScale);
194
+ this.tempMatrix.position.copy(src);
195
+ this.tempDst.copy(src).add(delta);
196
+ this.tempMatrix.lookAt(this.tempDst);
197
+ this.tempMatrix.updateMatrix();
198
+ this.shaftInstancedMesh.setMatrixAt(index, this.tempMatrix.matrix);
199
+ if (length < headHeight) {
200
+ // If head is longer than the total length, shrink the head to match
201
+ // length. TODO: Is it okay to do this automatically?
202
+ const newDiameter = length / HEAD_BASE_HEIGHT;
203
+ this.tempScale.set(newDiameter, newDiameter, newDiameter);
204
+ } else {
205
+ this.tempScale.set(diameter, diameter, diameter);
206
+ }
207
+ this.tempMatrix.scale.copy(this.tempScale);
208
+ this.tempMatrix.position.copy(this.tempDst);
209
+ this.tempDst.add(delta);
210
+ this.tempMatrix.lookAt(this.tempDst);
211
+ this.tempMatrix.updateMatrix();
212
+ this.headInstancedMesh.setMatrixAt(index, this.tempMatrix.matrix);
213
+ }
214
+ updateAllArrowTransforms() {
215
+ if (!this.positions || !this.deltas) {
216
+ return;
217
+ }
218
+ const count = this.positions.length / 3;
219
+ const combinedScale = new Vector3().copy(this.scale).multiply(this.flipAxes).multiply(this.worldScale);
220
+ const tempSrc = new Vector3();
221
+ const tempDelta = new Vector3();
222
+ let tempDiameter;
223
+ for (let i = 0; i < count; i++) {
224
+ // Points and deltas scaled to volume space.
225
+ tempSrc.fromArray(this.positions, i * 3).multiply(combinedScale);
226
+ tempDelta.fromArray(this.deltas, i * 3).multiply(combinedScale);
227
+ tempDiameter = this.diameter[i % this.diameter.length] ?? DEFAULT_DIAMETER;
228
+ this.updateSingleArrowTransform(i, tempSrc, tempDelta, tempDiameter);
229
+ }
230
+ this.headInstancedMesh.instanceMatrix.needsUpdate = true;
231
+ this.shaftInstancedMesh.instanceMatrix.needsUpdate = true;
232
+ }
233
+ applyColors() {
234
+ if (!this.colors) {
235
+ return;
236
+ }
237
+ const colorCount = Math.round(this.colors.length / 3);
238
+ const color = new Color();
239
+ for (let i = 0; i < this.headInstancedMesh.count; i++) {
240
+ // Wrap colors if there are fewer colors than arrows
241
+ const colorIndex = i % colorCount;
242
+ color.fromArray(this.colors, colorIndex * 3);
243
+ this.headInstancedMesh.setColorAt(i, color);
244
+ this.shaftInstancedMesh.setColorAt(i, color);
245
+ }
246
+ if (this.headInstancedMesh.instanceColor) {
247
+ this.headInstancedMesh.instanceColor.needsUpdate = true;
248
+ }
249
+ if (this.shaftInstancedMesh.instanceColor) {
250
+ this.shaftInstancedMesh.instanceColor.needsUpdate = true;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Sets the colors for the arrows as either a single Color or an array of RGB values.
256
+ * If there are more arrows than colors, colors will be repeated in order.
257
+ * @param colors Color object or numeric array of RGB values in the [0, 1] range.
258
+ * @throws {Error} If colors array length is not a multiple of 3.
259
+ */
260
+ setColors(colors) {
261
+ if (colors instanceof Color) {
262
+ this.colors = new Float32Array(3);
263
+ colors.toArray(this.colors);
264
+ } else {
265
+ if (colors.length % 3 !== 0) {
266
+ throw new Error("VectorArrows.setColors: colors array length must be a multiple of 3.");
267
+ }
268
+ this.colors = new Float32Array(colors);
269
+ }
270
+ this.applyColors();
271
+ }
272
+
273
+ /**
274
+ * Sets all arrows to a uniform diameter (default is `0.002`). To set
275
+ * per-arrow diameter, pass an array of values into `setArrowData` instead.
276
+ * @param diameter Diameter value to set for all arrows.
277
+ */
278
+ setDiameter(diameter) {
279
+ this.diameter = new Float32Array([diameter]);
280
+ this.updateAllArrowTransforms();
281
+ }
282
+
283
+ /**
284
+ * Sets the per-arrow data. The number of rendered arrows is equal to
285
+ * `positions.length / 3`.
286
+ * @param positions Float32Array, where every three values is the XYZ position
287
+ * of the base of an arrow.
288
+ * @param deltas Float32Array, where every three values is the XYZ delta
289
+ * vector for each arrow.
290
+ * @param diameters Optional Float32Array of diameter thickness values for
291
+ * each arrow's shaft. If provided, overrides a single diameter value set by
292
+ * `setDiameter`. If fewer diameter values are provided than arrows, the
293
+ * values will be repeated in order.
294
+ * @throws {Error} If positions and deltas arrays have different lengths or if
295
+ * their length is not a multiple of 3.
296
+ */
297
+ setArrowData(positions, deltas, diameters) {
298
+ if (positions.length !== deltas.length) {
299
+ throw new Error("VectorArrows.setArrowData: positions and deltas arrays must have the same length");
300
+ }
301
+ if (positions.length % 3 !== 0) {
302
+ throw new Error("VectorArrows.setArrowData: positions and deltas arrays length must be a multiple of 3");
303
+ }
304
+ this.positions = positions;
305
+ this.deltas = deltas;
306
+ if (diameters) {
307
+ this.diameter = diameters;
308
+ }
309
+
310
+ // Update instance count, add more instances as needed.
311
+ const count = positions.length / 3;
312
+ const didInstanceCountIncrease = this.headInstancedMesh.count < count;
313
+ if (this.maxInstanceCount < count) {
314
+ this.increaseInstanceCountMax(count);
315
+ }
316
+ this.headInstancedMesh.count = count;
317
+ this.shaftInstancedMesh.count = count;
318
+ this.updateAllArrowTransforms();
319
+ if (didInstanceCountIncrease) {
320
+ // Apply colors to new arrows as needed
321
+ this.applyColors();
322
+ }
323
+ }
324
+ }
package/es/View3d.js CHANGED
@@ -852,29 +852,54 @@ export class View3d {
852
852
  * be in the normalized coordinate space of the Volume, where the origin
853
853
  * (0,0,0) is at the center of the Volume and the extent is from -0.5 to 0.5
854
854
  * in each axis.
855
+ * @deprecated Will be removed in the next major release. Use `addDrawableObject` instead.
855
856
  */
856
857
  addLineObject(line) {
857
- if (this.image) {
858
- this.image.addLineObject(line);
859
- this.redraw();
860
- }
858
+ return this.addDrawableObject(line);
861
859
  }
862
860
 
863
- /** Returns whether a Line3d object exists as a child of the volume. */
861
+ /**
862
+ * Returns whether a Line3d object exists as a child of the volume.
863
+ * @deprecated Will be removed in the next major release. Use `hasDrawableObject` instead.
864
+ */
864
865
  hasLineObject(line) {
865
- if (this.image) {
866
- return this.image.hasLineObject(line);
867
- }
868
- return false;
866
+ return this.hasDrawableObject(line);
869
867
  }
870
868
 
871
869
  /**
872
870
  * Removes a Line3d object from the Volume, if it exists. Note that the
873
871
  * object's resources are not freed automatically (e.g. via `line.cleanup()`).
872
+ * @deprecated Will be removed in the next major release. Use `removeDrawableObject` instead.
874
873
  */
875
874
  removeLineObject(line) {
875
+ return this.removeDrawableObject(line);
876
+ }
877
+
878
+ /**
879
+ * Adds a drawable object as a child of the Volume, if it does not already
880
+ * exist. Objects will be in the normalized coordinate space of the Volume,
881
+ * where the origin (0,0,0) is at the center of the Volume and the extent is
882
+ * from -0.5 to 0.5 in each axis.
883
+ */
884
+ addDrawableObject(object) {
885
+ if (this.image) {
886
+ this.image.addDrawableObject(object);
887
+ this.redraw();
888
+ }
889
+ }
890
+
891
+ /** Returns whether a drawable object exists as a child of the volume. */
892
+ hasDrawableObject(object) {
893
+ return this.image ? this.image.hasDrawableObject(object) : false;
894
+ }
895
+
896
+ /**
897
+ * Removes a drawable object from the Volume, if it exists. Note that the
898
+ * object's resources are not freed automatically (e.g. via `object.cleanup()`).
899
+ */
900
+ removeDrawableObject(object) {
876
901
  if (this.image) {
877
- this.image.removeLineObject(line);
902
+ this.image.removeDrawableObject(object);
878
903
  this.redraw();
879
904
  }
880
905
  }
@@ -240,6 +240,8 @@ export default class VolumeDrawable {
240
240
  const scale = normPhysicalSize.clone().multiply(normRegionSize).multiply(this.settings.scale);
241
241
  this.childObjectsGroup.scale.copy(scale);
242
242
  this.childObjectsGroup.position.copy(this.volume.getContentCenter().multiply(this.settings.scale));
243
+ this.childObjects.forEach(obj => obj.onParentTransformUpdated?.());
244
+
243
245
  // TODO only `RayMarchedAtlasVolume` handles scale properly. Get the others on board too!
244
246
  this.volumeRendering.updateVolumeDimensions();
245
247
  this.volumeRendering.updateSettings(this.settings, SettingsFlags.TRANSFORM);
@@ -773,33 +775,31 @@ export default class VolumeDrawable {
773
775
  }
774
776
 
775
777
  /**
776
- * Adds a Line3d object as a child of the Volume, if it does not already
777
- * exist. Line objects will be in the normalized coordinate space of the
778
- * Volume, where the origin (0,0,0) is at the center of the Volume and the
779
- * extent is from -0.5 to 0.5 in each axis.
778
+ * Adds a drawable object as a child of the Volume, if it does not already
779
+ * exist. Objects will be in the normalized coordinate space of the Volume,
780
+ * where the origin (0,0,0) is at the center of the Volume and the extent is
781
+ * from -0.5 to 0.5 in each axis.
780
782
  */
781
- addLineObject(line) {
782
- if (!this.childObjects.has(line)) {
783
- this.childObjects.add(line);
784
- this.childObjectsGroup.add(line.get3dObject());
785
- line.setResolution(this.settings.resolution.x, this.settings.resolution.y);
786
- line.setFlipAxes(this.settings.flipAxes.x, this.settings.flipAxes.y, this.settings.flipAxes.z);
783
+ addDrawableObject(object) {
784
+ if (!this.childObjects.has(object)) {
785
+ this.childObjectsGroup.add(object.get3dObject());
786
+ this.childObjects.add(object);
787
+ this.updateScale();
787
788
  }
788
789
  }
789
790
 
790
- /** Returns whether a line object exists as a child of the volume. */
791
- hasLineObject(line) {
792
- return this.childObjects.has(line);
791
+ /** Returns whether a drawable object exists as a child of the volume. */
792
+ hasDrawableObject(object) {
793
+ return this.childObjects.has(object);
793
794
  }
794
795
 
795
- /**
796
- * Removes a Line3d object from the Volume, if it exists. Note that the
797
- * object's resources are not freed automatically (e.g. via `line.cleanup()`).
796
+ /** Removes a drawable object from the Volume, if it exists. Note that the
797
+ * object's resources are not freed automatically (e.g. via `object.cleanup()`).
798
798
  */
799
- removeLineObject(line) {
800
- if (this.childObjects.has(line)) {
801
- this.childObjects.delete(line);
802
- this.childObjectsGroup.remove(line.get3dObject());
799
+ removeDrawableObject(object) {
800
+ if (this.childObjects.has(object)) {
801
+ this.childObjects.delete(object);
802
+ this.childObjectsGroup.remove(object.get3dObject());
803
803
  }
804
804
  }
805
805
  setupGui(pane) {
package/es/index.js CHANGED
@@ -19,4 +19,5 @@ import VolumeLoaderContext from "./workers/VolumeLoaderContext.js";
19
19
  import { VolumeLoadError, VolumeLoadErrorType } from "./loaders/VolumeLoadError.js";
20
20
  import { Light, AREA_LIGHT, SKY_LIGHT } from "./Light.js";
21
21
  import Line3d from "./Line3d.js";
22
- export { Histogram, Lut, Line3d, remapControlPoints, View3d, Volume, VolumeDrawable, LoadSpec, VolumeMaker, VolumeCache, RequestQueue, SubscribableRequestQueue, PrefetchDirection, OMEZarrLoader, JsonImageInfoLoader, RawArrayLoader, TiffLoader, VolumeLoaderContext, VolumeLoadError, VolumeLoadErrorType, VolumeFileFormat, createVolumeLoader, Channel, Light, ViewportCorner, AREA_LIGHT, RENDERMODE_PATHTRACE, RENDERMODE_RAYMARCH, SKY_LIGHT };
22
+ import VectorArrows3d from "./VectorArrows3d.js";
23
+ export { Histogram, Lut, Line3d, VectorArrows3d, remapControlPoints, View3d, Volume, VolumeDrawable, LoadSpec, VolumeMaker, VolumeCache, RequestQueue, SubscribableRequestQueue, PrefetchDirection, OMEZarrLoader, JsonImageInfoLoader, RawArrayLoader, TiffLoader, VolumeLoaderContext, VolumeLoadError, VolumeLoadErrorType, VolumeFileFormat, createVolumeLoader, Channel, Light, ViewportCorner, AREA_LIGHT, RENDERMODE_PATHTRACE, RENDERMODE_RAYMARCH, SKY_LIGHT };
@@ -3,6 +3,7 @@ import * as zarr from "zarrita";
3
3
  const {
4
4
  slice
5
5
  } = zarr;
6
+ import { getDataRange } from "../utils/num_utils.js";
6
7
  import SubscribableRequestQueue from "../utils/SubscribableRequestQueue.js";
7
8
  import { ThreadableVolumeLoader } from "./IVolumeLoader.js";
8
9
  import { composeSubregion, computePackedAtlasDims, convertSubregionToPixels, pickLevelToLoad, unitNameToSymbol } from "./VolumeLoaderUtils.js";
@@ -16,18 +17,7 @@ const CHUNK_REQUEST_CANCEL_REASON = "chunk request cancelled";
16
17
  // returns the converted data and the original min and max values
17
18
  function convertChannel(channelData, dtype) {
18
19
  // get min and max
19
- // TODO FIXME Histogram will also compute min and max!
20
- let min = channelData[0];
21
- let max = channelData[0];
22
- for (let i = 0; i < channelData.length; i++) {
23
- const val = channelData[i];
24
- if (val < min) {
25
- min = val;
26
- }
27
- if (val > max) {
28
- max = val;
29
- }
30
- }
20
+ const [min, max] = getDataRange(channelData);
31
21
  if (dtype === "float64") {
32
22
  // convert to float32
33
23
  const f32 = new Float32Array(channelData.length);
@@ -315,7 +305,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
315
305
  return dims;
316
306
  });
317
307
  const imgdata = {
318
- name: source0.omeroMetadata?.name || "Volume",
308
+ name: source0.omeroMetadata?.name,
319
309
  atlasTileDims: [atlasTileDims.x, atlasTileDims.y],
320
310
  subregionSize: [pxSizeLv.x, pxSizeLv.y, pxSizeLv.z],
321
311
  subregionOffset: [0, 0, 0],
@@ -468,7 +458,6 @@ class OMEZarrLoader extends ThreadableVolumeLoader {
468
458
  const level = this.sources[sourceIdx].scaleLevels[multiscaleLevel];
469
459
  const sliceSpec = this.orderByDimension(unorderedSpec, sourceIdx);
470
460
  const reportChunk = (coords, sub) => reportChunkBase(sourceIdx, coords, sub);
471
- console.log(level);
472
461
  const result = await zarr.get(level, sliceSpec, {
473
462
  opts: {
474
463
  subscriber,
@@ -21,6 +21,7 @@ function getOME(xml) {
21
21
  }
22
22
  }
23
23
  class OMEDims {
24
+ name = undefined;
24
25
  sizex = 0;
25
26
  sizey = 0;
26
27
  sizez = 1;
@@ -63,14 +64,15 @@ function getAttributeOrError(el, attr) {
63
64
  function getOMEDims(imageEl) {
64
65
  const dims = new OMEDims();
65
66
  const pixelsEl = imageEl.getElementsByTagName("Pixels")[0];
67
+ dims.name = imageEl.getAttribute("Name") ?? "";
66
68
  dims.sizex = Number(getAttributeOrError(pixelsEl, "SizeX"));
67
69
  dims.sizey = Number(getAttributeOrError(pixelsEl, "SizeY"));
68
70
  dims.sizez = Number(pixelsEl.getAttribute("SizeZ"));
69
71
  dims.sizec = Number(pixelsEl.getAttribute("SizeC"));
70
72
  dims.sizet = Number(pixelsEl.getAttribute("SizeT"));
71
- dims.unit = pixelsEl.getAttribute("PhysicalSizeXUnit") || "";
72
- dims.pixeltype = pixelsEl.getAttribute("Type") || "";
73
- dims.dimensionorder = pixelsEl.getAttribute("DimensionOrder") || "XYZCT";
73
+ dims.unit = pixelsEl.getAttribute("PhysicalSizeXUnit") ?? "";
74
+ dims.pixeltype = pixelsEl.getAttribute("Type") ?? "";
75
+ dims.dimensionorder = pixelsEl.getAttribute("DimensionOrder") ?? "XYZCT";
74
76
  dims.pixelsizex = Number(pixelsEl.getAttribute("PhysicalSizeX"));
75
77
  dims.pixelsizey = Number(pixelsEl.getAttribute("PhysicalSizeY"));
76
78
  dims.pixelsizez = Number(pixelsEl.getAttribute("PhysicalSizeZ"));
@@ -178,7 +180,7 @@ class TiffLoader extends ThreadableVolumeLoader {
178
180
  // load tiff and check metadata
179
181
  const numChannelsPerSource = this.url.length > 1 ? Array(this.url.length).fill(1) : [dims.sizec];
180
182
  const imgdata = {
181
- name: "TEST",
183
+ name: dims.name,
182
184
  atlasTileDims: [atlasDims.x, atlasDims.y],
183
185
  subregionSize: [tilesizex, tilesizey, dims.sizez],
184
186
  subregionOffset: [0, 0, 0],
@@ -188,7 +190,7 @@ class TiffLoader extends ThreadableVolumeLoader {
188
190
  multiscaleLevelDims: [{
189
191
  shape: [dims.sizet, dims.sizec, dims.sizez, tilesizey, tilesizex],
190
192
  spacing: [1, 1, dims.pixelsizez, dims.pixelsizey * dims.sizey / tilesizey, dims.pixelsizex * dims.sizex / tilesizex],
191
- spaceUnit: dims.unit || "",
193
+ spaceUnit: dims.unit ?? "",
192
194
  timeUnit: "",
193
195
  dataType: getDtype(dims.pixeltype)
194
196
  }],
@@ -78,6 +78,7 @@ export function remapAxesToTCZYX(axes) {
78
78
  export function orderByDimension(valsTCZYX, orderTCZYX) {
79
79
  const specLen = getDimensionCount(orderTCZYX);
80
80
  const result = Array(specLen);
81
+ let curIdx = 0;
81
82
  orderTCZYX.forEach((val, idx) => {
82
83
  if (val >= 0) {
83
84
  if (val >= specLen) {
@@ -85,7 +86,7 @@ export function orderByDimension(valsTCZYX, orderTCZYX) {
85
86
  type: VolumeLoadErrorType.INVALID_METADATA
86
87
  });
87
88
  }
88
- result[val] = valsTCZYX[idx];
89
+ result[curIdx++] = valsTCZYX[idx];
89
90
  }
90
91
  });
91
92
  return result;
@@ -0,0 +1,35 @@
1
+ import { Euler, Group, Mesh, Vector3 } from "three";
2
+ import { IDrawableObject } from "./types.js";
3
+ /**
4
+ * Abstract base class for drawable 3D mesh objects.
5
+ *
6
+ * Provides default implementations for some methods in the IDrawableObject
7
+ * interface, including handling for visibility, transformations, and cleanup.
8
+ *
9
+ * As a default, subclasses should call `this.addChildMesh(mesh)` to register
10
+ * any meshes that should be managed by the base class.
11
+ */
12
+ export default abstract class BaseDrawableMeshObject implements IDrawableObject {
13
+ /**
14
+ * Pivot group that all child meshes are parented to. Transformations are
15
+ * applied to this group.
16
+ */
17
+ protected readonly meshPivot: Group;
18
+ protected scale: Vector3;
19
+ protected flipAxes: Vector3;
20
+ constructor();
21
+ protected addChildMesh(mesh: Mesh): void;
22
+ protected removeChildMesh(mesh: Mesh): void;
23
+ cleanup(): void;
24
+ doRender(): void;
25
+ setVisible(visible: boolean): void;
26
+ get3dObject(): Group;
27
+ setTranslation(translation: Vector3): void;
28
+ setScale(scale: Vector3): void;
29
+ setRotation(eulerXYZ: Euler): void;
30
+ setFlipAxes(flipX: number, flipY: number, flipZ: number): void;
31
+ setOrthoThickness(_thickness: number): void;
32
+ setResolution(_x: number, _y: number): void;
33
+ setAxisClip(_axis: "x" | "y" | "z", _minval: number, _maxval: number, _isOrthoAxis: boolean): void;
34
+ updateClipRegion(_xmin: number, _xmax: number, _ymin: number, _ymax: number, _zmin: number, _zmax: number): void;
35
+ }
@@ -18,7 +18,7 @@ export default class Histogram {
18
18
  private dataMaxBin;
19
19
  private pixelCount;
20
20
  maxBin: number;
21
- constructor(data: TypedArray<NumberType>);
21
+ constructor(data: TypedArray<NumberType>, dataMin?: number | undefined, dataMax?: number | undefined);
22
22
  private static findBin;
23
23
  /**
24
24
  * Returns the integer bin index for the given value. If a value is outside
@@ -1,7 +1,7 @@
1
1
  import { type VolumeDims } from "./VolumeDims.js";
2
2
  import { Vector3, Vector2 } from "three";
3
3
  export type ImageInfo = Readonly<{
4
- name: string;
4
+ name: string | undefined;
5
5
  /**
6
6
  * XY dimensions of the texture atlas used by `RayMarchedAtlasVolume` and
7
7
  * `Atlas2DSlice`, in number of z-slice tiles (not pixels). Chosen by the
@@ -1,28 +1,14 @@
1
- import { Color, Euler, Group, Vector3 } from "three";
1
+ import { Color } from "three";
2
2
  import { IDrawableObject } from "./types.js";
3
+ import BaseDrawableMeshObject from "./BaseDrawableMeshObject.js";
3
4
  /**
4
5
  * Simple wrapper for a 3D line segments object, with controls for vertex data,
5
6
  * color, width, and segments visible.
6
7
  */
7
- export default class Line3d implements IDrawableObject {
8
- private meshPivot;
9
- private scale;
10
- private flipAxes;
8
+ export default class Line3d extends BaseDrawableMeshObject implements IDrawableObject {
11
9
  private lineMesh;
12
10
  private bufferSize;
13
11
  constructor();
14
- cleanup(): void;
15
- setVisible(visible: boolean): void;
16
- doRender(): void;
17
- get3dObject(): Group;
18
- setTranslation(translation: Vector3): void;
19
- setScale(scale: Vector3): void;
20
- setRotation(eulerXYZ: Euler): void;
21
- setFlipAxes(flipX: number, flipY: number, flipZ: number): void;
22
- setOrthoThickness(_thickness: number): void;
23
- setResolution(_x: number, _y: number): void;
24
- setAxisClip(_axis: "x" | "y" | "z", _minval: number, _maxval: number, _isOrthoAxis: boolean): void;
25
- updateClipRegion(_xmin: number, _xmax: number, _ymin: number, _ymax: number, _zmin: number, _zmax: number): void;
26
12
  /**
27
13
  * Sets the color of the line material.
28
14
  * @param color Base line color.
@@ -34,7 +34,7 @@ export default class MeshVolume implements IDrawableObject {
34
34
  hasIsosurface(channel: number): boolean;
35
35
  createIsosurface(channel: number, color: [number, number, number], value?: number, alpha?: number, transp?: boolean): void;
36
36
  destroyIsosurface(channel: number): void;
37
- saveChannelIsosurface(channelIndex: number, type: string, namePrefix: string): void;
37
+ saveChannelIsosurface(channelIndex: number, type: string, name?: string): void;
38
38
  exportSTL(input: Object3D, fname: string): void;
39
39
  exportGLTF(input: Object3D, fname: string): void;
40
40
  generateIsosurfaceGeometry(channelIndex: number, isovalue: number): BufferGeometry[];
@@ -0,0 +1,92 @@
1
+ import BaseDrawableMeshObject from "./BaseDrawableMeshObject.js";
2
+ import { Vector3, Color } from "three";
3
+ import { IDrawableObject } from "./types.js";
4
+ /**
5
+ * A drawable vector arrow field, which uses instanced meshes for performance.
6
+ */
7
+ export default class VectorArrows3d extends BaseDrawableMeshObject implements IDrawableObject {
8
+ /**
9
+ * Scale of this object in world coordinates, when unscaled. Used to
10
+ * compensate for parent transforms in order to keep arrow meshes from being
11
+ * distorted.
12
+ */
13
+ protected worldScale: Vector3;
14
+ private headInstancedMesh;
15
+ private shaftInstancedMesh;
16
+ private maxInstanceCount;
17
+ private positions;
18
+ private deltas;
19
+ private colors;
20
+ private diameter;
21
+ private tempDst;
22
+ private tempScale;
23
+ private tempMatrix;
24
+ constructor();
25
+ /**
26
+ * Returns (unscaled) buffer geometry for the head and shaft parts of the
27
+ * arrow.
28
+ * @returns
29
+ * - `head`: BufferGeometry for the arrowhead, a cone pointing along the +Z
30
+ * axis, with the pivot at the tip of the cone. Height and radius are based
31
+ * on constant values (`HEAD_BASE_HEIGHT` and `HEAD_BASE_RADIUS`).
32
+ * - `shaft`: BufferGeometry for the cylindrical arrow shaft. The cylinder
33
+ * points along the +Z axis, with the pivot at the base of the cylinder.
34
+ * Height is 1 and diameter is 1.
35
+ *
36
+ * ```txt
37
+ * ^ +Z axis ^
38
+ * _____ x
39
+ * | | / \
40
+ * | | / \
41
+ * --x-- /-----\
42
+ *
43
+ * x = pivot (0,0,0)
44
+ * ```
45
+ */
46
+ private static generateGeometry;
47
+ /**
48
+ * Create new instanced meshes with the specified instance count, and adds
49
+ * them to the mesh pivot and internal meshes array for future cleanup.
50
+ *
51
+ * If calling outside of the constructor, be sure to call `cleanup()` first.
52
+ */
53
+ private initInstancedMeshes;
54
+ private increaseInstanceCountMax;
55
+ setScale(scale: Vector3): void;
56
+ /**
57
+ * Called when scaling of parent transforms has been updated or whenever
58
+ * vector data is updated.
59
+ */
60
+ onParentTransformUpdated(): void;
61
+ private updateSingleArrowTransform;
62
+ private updateAllArrowTransforms;
63
+ private applyColors;
64
+ /**
65
+ * Sets the colors for the arrows as either a single Color or an array of RGB values.
66
+ * If there are more arrows than colors, colors will be repeated in order.
67
+ * @param colors Color object or numeric array of RGB values in the [0, 1] range.
68
+ * @throws {Error} If colors array length is not a multiple of 3.
69
+ */
70
+ setColors(colors: Float32Array | Color): void;
71
+ /**
72
+ * Sets all arrows to a uniform diameter (default is `0.002`). To set
73
+ * per-arrow diameter, pass an array of values into `setArrowData` instead.
74
+ * @param diameter Diameter value to set for all arrows.
75
+ */
76
+ setDiameter(diameter: number): void;
77
+ /**
78
+ * Sets the per-arrow data. The number of rendered arrows is equal to
79
+ * `positions.length / 3`.
80
+ * @param positions Float32Array, where every three values is the XYZ position
81
+ * of the base of an arrow.
82
+ * @param deltas Float32Array, where every three values is the XYZ delta
83
+ * vector for each arrow.
84
+ * @param diameters Optional Float32Array of diameter thickness values for
85
+ * each arrow's shaft. If provided, overrides a single diameter value set by
86
+ * `setDiameter`. If fewer diameter values are provided than arrows, the
87
+ * values will be repeated in order.
88
+ * @throws {Error} If positions and deltas arrays have different lengths or if
89
+ * their length is not a multiple of 3.
90
+ */
91
+ setArrowData(positions: Float32Array, deltas: Float32Array, diameters?: Float32Array): void;
92
+ }
@@ -3,7 +3,7 @@ import { CameraState } from "./ThreeJsPanel.js";
3
3
  import VolumeDrawable from "./VolumeDrawable.js";
4
4
  import { Light } from "./Light.js";
5
5
  import Volume from "./Volume.js";
6
- import { type ColorizeFeature, type VolumeChannelDisplayOptions, type VolumeDisplayOptions, ViewportCorner, RenderMode } from "./types.js";
6
+ import { type ColorizeFeature, type VolumeChannelDisplayOptions, type VolumeDisplayOptions, ViewportCorner, RenderMode, IDrawableObject } from "./types.js";
7
7
  import { PerChannelCallback } from "./loaders/IVolumeLoader.js";
8
8
  import Line3d from "./Line3d.js";
9
9
  export declare const RENDERMODE_RAYMARCH = RenderMode.RAYMARCH;
@@ -389,15 +389,34 @@ export declare class View3d {
389
389
  * be in the normalized coordinate space of the Volume, where the origin
390
390
  * (0,0,0) is at the center of the Volume and the extent is from -0.5 to 0.5
391
391
  * in each axis.
392
+ * @deprecated Will be removed in the next major release. Use `addDrawableObject` instead.
392
393
  */
393
394
  addLineObject(line: Line3d): void;
394
- /** Returns whether a Line3d object exists as a child of the volume. */
395
+ /**
396
+ * Returns whether a Line3d object exists as a child of the volume.
397
+ * @deprecated Will be removed in the next major release. Use `hasDrawableObject` instead.
398
+ */
395
399
  hasLineObject(line: Line3d): boolean;
396
400
  /**
397
401
  * Removes a Line3d object from the Volume, if it exists. Note that the
398
402
  * object's resources are not freed automatically (e.g. via `line.cleanup()`).
403
+ * @deprecated Will be removed in the next major release. Use `removeDrawableObject` instead.
399
404
  */
400
405
  removeLineObject(line: Line3d): void;
406
+ /**
407
+ * Adds a drawable object as a child of the Volume, if it does not already
408
+ * exist. Objects will be in the normalized coordinate space of the Volume,
409
+ * where the origin (0,0,0) is at the center of the Volume and the extent is
410
+ * from -0.5 to 0.5 in each axis.
411
+ */
412
+ addDrawableObject(object: IDrawableObject): void;
413
+ /** Returns whether a drawable object exists as a child of the volume. */
414
+ hasDrawableObject(object: IDrawableObject): boolean;
415
+ /**
416
+ * Removes a drawable object from the Volume, if it exists. Note that the
417
+ * object's resources are not freed automatically (e.g. via `object.cleanup()`).
418
+ */
419
+ removeDrawableObject(object: IDrawableObject): void;
401
420
  /**
402
421
  * @description Enable or disable picking on a volume. If enabled, the channelIndex is used to determine which channel to pick.
403
422
  * @param volume the image to enable picking on
@@ -25,7 +25,7 @@ export default class Volume {
25
25
  loadSpecRequired: Required<LoadSpec>;
26
26
  channelLoadCallback?: PerChannelCallback;
27
27
  imageMetadata: Record<string, unknown>;
28
- name: string;
28
+ name: string | undefined;
29
29
  channels: Channel[];
30
30
  numChannels: number;
31
31
  channelNames: string[];
@@ -1,12 +1,11 @@
1
1
  import { Vector3, Object3D, Euler, DepthTexture, OrthographicCamera, PerspectiveCamera, WebGLRenderer, WebGLRenderTarget, Texture } from "three";
2
2
  import { Pane } from "tweakpane";
3
3
  import Volume from "./Volume.js";
4
- import type { VolumeDisplayOptions, VolumeChannelDisplayOptions, ColorizeFeature } from "./types.js";
4
+ import type { VolumeDisplayOptions, VolumeChannelDisplayOptions, ColorizeFeature, IDrawableObject } from "./types.js";
5
5
  import { RenderMode } from "./types.js";
6
6
  import { Light } from "./Light.js";
7
7
  import Channel from "./Channel.js";
8
8
  import { Axis } from "./VolumeRenderSettings.js";
9
- import Line3d from "./Line3d.js";
10
9
  type ColorArray = [number, number, number];
11
10
  type ColorObject = {
12
11
  r: number;
@@ -120,19 +119,18 @@ export default class VolumeDrawable {
120
119
  setRotation(eulerXYZ: Euler): void;
121
120
  setScale(xyz: Vector3): void;
122
121
  /**
123
- * Adds a Line3d object as a child of the Volume, if it does not already
124
- * exist. Line objects will be in the normalized coordinate space of the
125
- * Volume, where the origin (0,0,0) is at the center of the Volume and the
126
- * extent is from -0.5 to 0.5 in each axis.
122
+ * Adds a drawable object as a child of the Volume, if it does not already
123
+ * exist. Objects will be in the normalized coordinate space of the Volume,
124
+ * where the origin (0,0,0) is at the center of the Volume and the extent is
125
+ * from -0.5 to 0.5 in each axis.
127
126
  */
128
- addLineObject(line: Line3d): void;
129
- /** Returns whether a line object exists as a child of the volume. */
130
- hasLineObject(line: Line3d): boolean;
131
- /**
132
- * Removes a Line3d object from the Volume, if it exists. Note that the
133
- * object's resources are not freed automatically (e.g. via `line.cleanup()`).
127
+ addDrawableObject(object: IDrawableObject): void;
128
+ /** Returns whether a drawable object exists as a child of the volume. */
129
+ hasDrawableObject(object: IDrawableObject): boolean;
130
+ /** Removes a drawable object from the Volume, if it exists. Note that the
131
+ * object's resources are not freed automatically (e.g. via `object.cleanup()`).
134
132
  */
135
- removeLineObject(line: Line3d): void;
133
+ removeDrawableObject(object: IDrawableObject): void;
136
134
  setupGui(pane: Pane): void;
137
135
  setZSlice(slice: number): boolean;
138
136
  get showBoundingBox(): boolean;
@@ -20,10 +20,11 @@ import { VolumeLoadError, VolumeLoadErrorType } from "./loaders/VolumeLoadError.
20
20
  import { type CameraState } from "./ThreeJsPanel.js";
21
21
  import { Light, AREA_LIGHT, SKY_LIGHT } from "./Light.js";
22
22
  import Line3d from "./Line3d.js";
23
+ import VectorArrows3d from "./VectorArrows3d.js";
23
24
  export type { ImageInfo } from "./ImageInfo.js";
24
25
  export type { ControlPoint } from "./Lut.js";
25
26
  export type { CreateLoaderOptions } from "./loaders/index.js";
26
27
  export type { IVolumeLoader, PerChannelCallback, ThreadableVolumeLoader } from "./loaders/IVolumeLoader.js";
27
28
  export type { ZarrLoaderFetchOptions } from "./loaders/OmeZarrLoader.js";
28
29
  export type { WorkerLoader } from "./workers/VolumeLoaderContext.js";
29
- export { Histogram, Lut, Line3d, remapControlPoints, View3d, Volume, VolumeDrawable, LoadSpec, VolumeMaker, VolumeCache, RequestQueue, SubscribableRequestQueue, PrefetchDirection, OMEZarrLoader, JsonImageInfoLoader, RawArrayLoader, type RawArrayData, type RawArrayInfo, type RawArrayLoaderOptions, TiffLoader, VolumeLoaderContext, VolumeLoadError, VolumeLoadErrorType, VolumeFileFormat, createVolumeLoader, Channel, Light, ViewportCorner, AREA_LIGHT, RENDERMODE_PATHTRACE, RENDERMODE_RAYMARCH, SKY_LIGHT, type CameraState, type ColorizeFeature, type NumberType, };
30
+ export { Histogram, Lut, Line3d, VectorArrows3d, remapControlPoints, View3d, Volume, VolumeDrawable, LoadSpec, VolumeMaker, VolumeCache, RequestQueue, SubscribableRequestQueue, PrefetchDirection, OMEZarrLoader, JsonImageInfoLoader, RawArrayLoader, type RawArrayData, type RawArrayInfo, type RawArrayLoaderOptions, TiffLoader, VolumeLoaderContext, VolumeLoadError, VolumeLoadErrorType, VolumeFileFormat, createVolumeLoader, Channel, Light, ViewportCorner, AREA_LIGHT, RENDERMODE_PATHTRACE, RENDERMODE_RAYMARCH, SKY_LIGHT, type CameraState, type ColorizeFeature, type NumberType, };
@@ -3,6 +3,7 @@ import { type ImageInfo } from "../ImageInfo.js";
3
3
  import type { VolumeDims } from "../VolumeDims.js";
4
4
  import { TypedArray, NumberType } from "../types.js";
5
5
  declare class OMEDims {
6
+ name: string | undefined;
6
7
  sizex: number;
7
8
  sizey: number;
8
9
  sizez: number;
@@ -80,6 +80,10 @@ export interface IDrawableObject {
80
80
  get3dObject(): Group;
81
81
  setTranslation(translation: Vector3): void;
82
82
  setScale(scale: Vector3): void;
83
+ /**
84
+ * Optional. Should be called when parent transforms are updated.
85
+ */
86
+ onParentTransformUpdated?(): void;
83
87
  setRotation(eulerXYZ: Euler): void;
84
88
  setFlipAxes(flipX: number, flipY: number, flipZ: number): void;
85
89
  setOrthoThickness(thickness: number): void;
@@ -224,8 +224,12 @@ export function getDataRange(data) {
224
224
  let min = data[0];
225
225
  let max = data[0];
226
226
  for (let i = 1; i < data.length; i++) {
227
- min = Math.min(min, data[i]);
228
- max = Math.max(max, data[i]);
227
+ const value = data[i];
228
+ if (value < min) {
229
+ min = value;
230
+ } else if (value > max) {
231
+ max = value;
232
+ }
229
233
  }
230
234
  return [min, max];
231
235
  }
@@ -1,6 +1,7 @@
1
1
  import { fromUrl } from "geotiff";
2
2
  import { serializeError } from "serialize-error";
3
3
  import { VolumeLoadError, VolumeLoadErrorType } from "../loaders/VolumeLoadError.js";
4
+ import { getDataRange } from "../utils/num_utils.js";
4
5
  // from TIFF
5
6
  const SAMPLEFORMAT_UINT = 1;
6
7
  const SAMPLEFORMAT_INT = 2;
@@ -121,17 +122,7 @@ async function loadTiffChannel(e) {
121
122
  // all slices collected, now resample to 8 bits full data range
122
123
  const src = castToArray(buffer, bytesPerPixel, sampleFormat);
123
124
  const dtype = getDtype(sampleFormat, bytesPerPixel);
124
- let chmin = src[0];
125
- let chmax = src[0];
126
- for (let j = 0; j < src.length; ++j) {
127
- const val = src[j];
128
- if (val < chmin) {
129
- chmin = val;
130
- }
131
- if (val > chmax) {
132
- chmax = val;
133
- }
134
- }
125
+ const [chmin, chmax] = getDataRange(src);
135
126
  return {
136
127
  data: src,
137
128
  channel: channelIndex,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aics/vole-core",
3
- "version": "4.3.1",
3
+ "version": "4.4.1",
4
4
  "description": "volume renderer for 3d, 4d, or 5d imaging data with OME-Zarr support",
5
5
  "main": "es/index.js",
6
6
  "type": "module",
@@ -17,6 +17,7 @@
17
17
  "build": "npm run transpileES && npm run build-types",
18
18
  "build-types": "tsc -p tsconfig.types.json",
19
19
  "build-demo": "vite build public/ --config vite.config.ts --outDir ./demo",
20
+ "checks": "npm run lint & npm run typeCheck & npx vitest run",
20
21
  "clean": "rimraf es/",
21
22
  "format": "prettier --write src/**/*.ts",
22
23
  "gh-build": "vite build public/ --config vite.config.ts --outDir ./vole-core",