@aics/vole-core 4.3.1 → 4.4.0

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/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
@@ -0,0 +1,325 @@
1
+ import { InstancedMesh, CylinderGeometry, ConeGeometry, Object3D, Vector3, MeshBasicMaterial, Color, DynamicDrawUsage, Matrix4 } from "three";
2
+ import BaseDrawableMeshObject from "./BaseDrawableMeshObject";
3
+ import { MESH_NO_PICK_OCCLUSION_LAYER } from "./ThreeJsPanel";
4
+
5
+ // Unscaled arrowhead dimensions.
6
+ const SHAFT_BASE_RADIUS = 0.5;
7
+ const HEAD_BASE_RADIUS = 1.5;
8
+ const HEAD_BASE_HEIGHT = 4;
9
+
10
+ /** Default arrow shaft thickness, in world units. */
11
+ const DEFAULT_DIAMETER = 0.002;
12
+ const DEFAULT_INSTANCE_COUNT = 256;
13
+
14
+ /**
15
+ * A drawable vector arrow field, which uses instanced meshes for performance.
16
+ */
17
+ export default class VectorArrows3d extends BaseDrawableMeshObject {
18
+ /**
19
+ * Scale of this object in world coordinates, when unscaled. Used to
20
+ * compensate for parent transforms in order to keep arrow meshes from being
21
+ * distorted.
22
+ */
23
+
24
+ // Temporary calculation objects. Optimization taken from three.js examples.
25
+
26
+ constructor() {
27
+ super();
28
+ this.worldScale = new Vector3(1, 1, 1);
29
+ this.meshPivot.layers.set(MESH_NO_PICK_OCCLUSION_LAYER);
30
+ this.maxInstanceCount = DEFAULT_INSTANCE_COUNT;
31
+ const {
32
+ headMesh: headMesh,
33
+ shaftMesh: shaftMesh
34
+ } = this.initInstancedMeshes(DEFAULT_INSTANCE_COUNT);
35
+ this.headInstancedMesh = headMesh;
36
+ this.shaftInstancedMesh = shaftMesh;
37
+ this.headInstancedMesh.count = 0;
38
+ this.shaftInstancedMesh.count = 0;
39
+ this.positions = null;
40
+ this.deltas = null;
41
+ this.colors = null;
42
+ this.diameter = new Float32Array([DEFAULT_DIAMETER]);
43
+ this.tempDst = new Vector3();
44
+ this.tempScale = new Vector3();
45
+ this.tempMatrix = new Object3D();
46
+ this.onParentTransformUpdated();
47
+ }
48
+
49
+ /**
50
+ * Returns (unscaled) buffer geometry for the head and shaft parts of the
51
+ * arrow.
52
+ * @returns
53
+ * - `head`: BufferGeometry for the arrowhead, a cone pointing along the +Z
54
+ * axis, with the pivot at the tip of the cone. Height and radius are based
55
+ * on constant values (`HEAD_BASE_HEIGHT` and `HEAD_BASE_RADIUS`).
56
+ * - `shaft`: BufferGeometry for the cylindrical arrow shaft. The cylinder
57
+ * points along the +Z axis, with the pivot at the base of the cylinder.
58
+ * Height is 1 and diameter is 1.
59
+ *
60
+ * ```txt
61
+ * ^ +Z axis ^
62
+ * _____ x
63
+ * | | / \
64
+ * | | / \
65
+ * --x-- /-----\
66
+ *
67
+ * x = pivot (0,0,0)
68
+ * ```
69
+ */
70
+ static generateGeometry() {
71
+ // TODO: Currently the shape of the arrow head is fixed. Allow configuring
72
+ // this in the future?
73
+ const cylinderGeometry = new CylinderGeometry(SHAFT_BASE_RADIUS, SHAFT_BASE_RADIUS, 2 * SHAFT_BASE_RADIUS,
74
+ // height
75
+ 8,
76
+ // radial segments
77
+ 1,
78
+ // height segments
79
+ false // capped ends
80
+ );
81
+ const coneRadius = HEAD_BASE_RADIUS;
82
+ const coneHeight = HEAD_BASE_HEIGHT;
83
+ const coneGeometry = new ConeGeometry(coneRadius, coneHeight, 12);
84
+
85
+ // Rotate both to point along +Z axis
86
+ const rotateToPositiveZ = new Matrix4().makeRotationX(Math.PI / 2);
87
+ // Change cone pivot to be at the tip.
88
+ const coneTranslation = new Matrix4().makeTranslation(0, 0, -coneHeight / 2);
89
+ coneGeometry.applyMatrix4(coneTranslation.multiply(rotateToPositiveZ));
90
+ // Change cylinder pivot to be at the base.
91
+ const cylinderTranslation = new Matrix4().makeTranslation(0, 0, 0.5);
92
+ cylinderGeometry.applyMatrix4(cylinderTranslation.multiply(rotateToPositiveZ));
93
+ return {
94
+ head: coneGeometry,
95
+ shaft: cylinderGeometry
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Create new instanced meshes with the specified instance count, and adds
101
+ * them to the mesh pivot and internal meshes array for future cleanup.
102
+ *
103
+ * If calling outside of the constructor, be sure to call `cleanup()` first.
104
+ */
105
+ initInstancedMeshes(instanceCount) {
106
+ this.cleanup();
107
+ this.meshPivot.clear();
108
+ const basicMaterial = new MeshBasicMaterial({
109
+ color: "#fff"
110
+ });
111
+ const {
112
+ head: headGeometry,
113
+ shaft: shaftGeometry
114
+ } = VectorArrows3d.generateGeometry();
115
+ const headMesh = new InstancedMesh(headGeometry, basicMaterial, instanceCount);
116
+ const shaftMesh = new InstancedMesh(shaftGeometry, basicMaterial, instanceCount);
117
+ headMesh.layers.set(MESH_NO_PICK_OCCLUSION_LAYER);
118
+ shaftMesh.layers.set(MESH_NO_PICK_OCCLUSION_LAYER);
119
+ headMesh.frustumCulled = false;
120
+ shaftMesh.frustumCulled = false;
121
+ headMesh.instanceMatrix.setUsage(DynamicDrawUsage);
122
+ shaftMesh.instanceMatrix.setUsage(DynamicDrawUsage);
123
+ this.addChildMesh(headMesh);
124
+ this.addChildMesh(shaftMesh);
125
+ return {
126
+ headMesh,
127
+ shaftMesh
128
+ };
129
+ }
130
+ increaseInstanceCountMax(instanceCount) {
131
+ // Max instance count is set when instanced meshes are created. If we need
132
+ // to increase the max, we need to recreate the instanced meshes.
133
+ let newInstanceCount = this.maxInstanceCount;
134
+ while (newInstanceCount < instanceCount) {
135
+ newInstanceCount *= 2;
136
+ }
137
+ // Delete existing meshes
138
+ this.cleanup();
139
+ const {
140
+ headMesh,
141
+ shaftMesh
142
+ } = this.initInstancedMeshes(newInstanceCount);
143
+ this.headInstancedMesh = headMesh;
144
+ this.shaftInstancedMesh = shaftMesh;
145
+ this.maxInstanceCount = newInstanceCount;
146
+ }
147
+ setScale(scale) {
148
+ if (scale !== this.scale) {
149
+ this.onParentTransformUpdated();
150
+ this.scale.copy(scale);
151
+ if (this.positions && this.deltas) {
152
+ // Update arrows
153
+ this.setArrowData(this.positions, this.deltas);
154
+ }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Called when scaling of parent transforms has been updated or whenever
160
+ * vector data is updated.
161
+ */
162
+ onParentTransformUpdated() {
163
+ // Measure world scale by temporarily resetting mesh pivot scale
164
+ this.meshPivot.scale.set(1, 1, 1);
165
+ let newWorldScale = new Vector3();
166
+ newWorldScale = this.meshPivot.getWorldScale(newWorldScale);
167
+
168
+ // Scale is inverted on mesh pivot to cancel out parent transforms (though
169
+ // translation and rotation are still affected by any parent transforms).
170
+ // This allows arrows meshes to be scaled 1:1 with world space, regardless
171
+ // of parent transforms, and prevents distortion or skewing of the mesh.
172
+ // Parent scaling is applied to arrow positions and deltas (see
173
+ // `updateAllArrowTransforms`), rather than the meshes themselves.
174
+ const invertScale = new Vector3(1, 1, 1).divide(newWorldScale);
175
+ this.meshPivot.scale.copy(invertScale);
176
+ if (!newWorldScale.equals(this.worldScale)) {
177
+ this.worldScale.copy(newWorldScale);
178
+ if (this.positions && this.deltas) {
179
+ this.setArrowData(this.positions, this.deltas);
180
+ }
181
+ }
182
+ }
183
+ updateSingleArrowTransform(index, src, delta, diameter) {
184
+ // Update the arrow shaft
185
+ const headHeight = HEAD_BASE_HEIGHT * diameter;
186
+ const length = delta.length();
187
+ const shaftHeight = Math.max(length - headHeight, 0);
188
+ if (shaftHeight < 1e-6) {
189
+ // If the shaft height is too small, scale to 0.
190
+ this.tempScale.set(0, 0, 0);
191
+ } else {
192
+ this.tempScale.set(diameter, diameter, shaftHeight);
193
+ }
194
+ this.tempMatrix.scale.copy(this.tempScale);
195
+ this.tempMatrix.position.copy(src);
196
+ this.tempDst.copy(src).add(delta);
197
+ this.tempMatrix.lookAt(this.tempDst);
198
+ this.tempMatrix.updateMatrix();
199
+ this.shaftInstancedMesh.setMatrixAt(index, this.tempMatrix.matrix);
200
+ if (length < headHeight) {
201
+ // If head is longer than the total length, shrink the head to match
202
+ // length. TODO: Is it okay to do this automatically?
203
+ const newDiameter = length / HEAD_BASE_HEIGHT;
204
+ this.tempScale.set(newDiameter, newDiameter, newDiameter);
205
+ } else {
206
+ this.tempScale.set(diameter, diameter, diameter);
207
+ }
208
+ this.tempMatrix.scale.copy(this.tempScale);
209
+ this.tempMatrix.position.copy(this.tempDst);
210
+ this.tempDst.add(delta);
211
+ this.tempMatrix.lookAt(this.tempDst);
212
+ this.tempMatrix.updateMatrix();
213
+ this.headInstancedMesh.setMatrixAt(index, this.tempMatrix.matrix);
214
+ }
215
+ updateAllArrowTransforms() {
216
+ if (!this.positions || !this.deltas) {
217
+ return;
218
+ }
219
+ const count = this.positions.length / 3;
220
+ const combinedScale = new Vector3().copy(this.scale).multiply(this.flipAxes).multiply(this.worldScale);
221
+ const tempSrc = new Vector3();
222
+ const tempDelta = new Vector3();
223
+ let tempDiameter;
224
+ for (let i = 0; i < count; i++) {
225
+ // Points and deltas scaled to volume space.
226
+ tempSrc.fromArray(this.positions, i * 3).multiply(combinedScale);
227
+ tempDelta.fromArray(this.deltas, i * 3).multiply(combinedScale);
228
+ tempDiameter = this.diameter[i % this.diameter.length] ?? DEFAULT_DIAMETER;
229
+ this.updateSingleArrowTransform(i, tempSrc, tempDelta, tempDiameter);
230
+ }
231
+ this.headInstancedMesh.instanceMatrix.needsUpdate = true;
232
+ this.shaftInstancedMesh.instanceMatrix.needsUpdate = true;
233
+ }
234
+ applyColors() {
235
+ if (!this.colors) {
236
+ return;
237
+ }
238
+ const colorCount = Math.round(this.colors.length / 3);
239
+ const color = new Color();
240
+ for (let i = 0; i < this.headInstancedMesh.count; i++) {
241
+ // Wrap colors if there are fewer colors than arrows
242
+ const colorIndex = i % colorCount;
243
+ color.fromArray(this.colors, colorIndex * 3);
244
+ this.headInstancedMesh.setColorAt(i, color);
245
+ this.shaftInstancedMesh.setColorAt(i, color);
246
+ }
247
+ if (this.headInstancedMesh.instanceColor) {
248
+ this.headInstancedMesh.instanceColor.needsUpdate = true;
249
+ }
250
+ if (this.shaftInstancedMesh.instanceColor) {
251
+ this.shaftInstancedMesh.instanceColor.needsUpdate = true;
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Sets the colors for the arrows as either a single Color or an array of RGB values.
257
+ * If there are more arrows than colors, colors will be repeated in order.
258
+ * @param colors Color object or numeric array of RGB values in the [0, 1] range.
259
+ * @throws {Error} If colors array length is not a multiple of 3.
260
+ */
261
+ setColors(colors) {
262
+ if (colors instanceof Color) {
263
+ this.colors = new Float32Array(3);
264
+ colors.toArray(this.colors);
265
+ } else {
266
+ if (colors.length % 3 !== 0) {
267
+ throw new Error("VectorArrows.setColors: colors array length must be a multiple of 3.");
268
+ }
269
+ this.colors = new Float32Array(colors);
270
+ }
271
+ this.applyColors();
272
+ }
273
+
274
+ /**
275
+ * Sets all arrows to a uniform diameter (default is `0.002`). To set
276
+ * per-arrow diameter, pass an array of values into `setArrowData` instead.
277
+ * @param diameter Diameter value to set for all arrows.
278
+ */
279
+ setDiameter(diameter) {
280
+ this.diameter = new Float32Array([diameter]);
281
+ this.updateAllArrowTransforms();
282
+ }
283
+
284
+ /**
285
+ * Sets the per-arrow data. The number of rendered arrows is equal to
286
+ * `positions.length / 3`.
287
+ * @param positions Float32Array, where every three values is the XYZ position
288
+ * of the base of an arrow.
289
+ * @param deltas Float32Array, where every three values is the XYZ delta
290
+ * vector for each arrow.
291
+ * @param diameters Optional Float32Array of diameter thickness values for
292
+ * each arrow's shaft. If provided, overrides a single diameter value set by
293
+ * `setDiameter`. If fewer diameter values are provided than arrows, the
294
+ * values will be repeated in order.
295
+ * @throws {Error} If positions and deltas arrays have different lengths or if
296
+ * their length is not a multiple of 3.
297
+ */
298
+ setArrowData(positions, deltas, diameters) {
299
+ if (positions.length !== deltas.length) {
300
+ throw new Error("VectorArrows.setArrowData: positions and deltas arrays must have the same length");
301
+ }
302
+ if (positions.length % 3 !== 0) {
303
+ throw new Error("VectorArrows.setArrowData: positions and deltas arrays length must be a multiple of 3");
304
+ }
305
+ this.positions = positions;
306
+ this.deltas = deltas;
307
+ if (diameters) {
308
+ this.diameter = diameters;
309
+ }
310
+
311
+ // Update instance count, add more instances as needed.
312
+ const count = positions.length / 3;
313
+ const didInstanceCountIncrease = this.headInstancedMesh.count < count;
314
+ if (this.maxInstanceCount < count) {
315
+ this.increaseInstanceCountMax(count);
316
+ }
317
+ this.headInstancedMesh.count = count;
318
+ this.shaftInstancedMesh.count = count;
319
+ this.updateAllArrowTransforms();
320
+ if (didInstanceCountIncrease) {
321
+ // Apply colors to new arrows as needed
322
+ this.applyColors();
323
+ }
324
+ }
325
+ }
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 };
@@ -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";
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
+ }
@@ -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.
@@ -0,0 +1,92 @@
1
+ import { Vector3, Color } from "three";
2
+ import { IDrawableObject } from "./types";
3
+ import BaseDrawableMeshObject from "./BaseDrawableMeshObject";
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
@@ -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, };
@@ -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;
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.0",
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",