@aics/vole-core 3.15.2 → 3.15.4

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.
@@ -7,7 +7,7 @@ const fuseShaderSrcF = "precision highp float;\nprecision highp int;\nprecision
7
7
  /* babel-plugin-inline-import './constants/shaders/fuseI.frag' */
8
8
  const fuseShaderSrcI = "precision highp float;\nprecision highp int;\nprecision highp sampler2D;\nprecision highp sampler3D;\n\n// the lut texture is a 256x1 rgba texture for each channel\nuniform sampler2D lutSampler;\n\nuniform vec2 lutMinMax;\n\n// src texture is the raw volume intensity data\nuniform isampler2D srcTexture;\n\nvoid main()\n{\n ivec2 vUv = ivec2(int(gl_FragCoord.x), int(gl_FragCoord.y));\n int intensity = texelFetch(srcTexture, vUv, 0).r;\n float ilookup = float(float(intensity) - lutMinMax.x) / float(lutMinMax.y - lutMinMax.x);\n // apply lut to intensity:\n vec4 pix = texture(lutSampler, vec2(ilookup, 0.5));\n gl_FragColor = vec4(pix.xyz*pix.w, pix.w);\n}\n";
9
9
  /* babel-plugin-inline-import './constants/shaders/colorizeUI.frag' */
10
- const colorizeSrcUI = "precision highp float;\nprecision highp int;\nprecision highp usampler2D;\nprecision highp sampler3D;\n\nuniform sampler2D featureData;\n/** Min and max feature values that define the endpoints of the color map. Values\n * outside the range will be clamped to the nearest endpoint.\n */\nuniform float featureColorRampMin;\nuniform float featureColorRampMax;\nuniform sampler2D colorRamp;\nuniform usampler2D inRangeIds;\nuniform usampler2D outlierData;\n\n/**\n * LUT mapping from the segmentation ID (raw pixel value) to the\n * global ID (index in data buffers like `featureData` and `outlierData`).\n * \n * For a given segmentation ID `segId`, the global ID is given by:\n * `segIdToGlobalId[segId - segIdOffset] - 1`.\n*/\nuniform usampler2D segIdToGlobalId;\nuniform uint segIdOffset;\n\nuniform vec3 outlineColor;\n\n/** MUST be synchronized with the DrawMode enum in ColorizeCanvas! */\nconst uint DRAW_MODE_HIDE = 0u;\nconst uint DRAW_MODE_COLOR = 1u;\nconst uint BACKGROUND_ID = 0u;\nconst uint MISSING_DATA_ID = 0xFFFFFFFFu;\n\nuniform vec3 outlierColor;\nuniform uint outlierDrawMode;\nuniform vec3 outOfRangeColor;\nuniform uint outOfRangeDrawMode;\n\nuniform uint highlightedId;\n\nuniform bool hideOutOfRange;\n\n// src texture is the raw volume intensity data\nuniform usampler2D srcTexture;\n\nvec4 getFloatFromTex(sampler2D tex, int index) {\n int width = textureSize(tex, 0).x;\n ivec2 featurePos = ivec2(index % width, index / width);\n return texelFetch(tex, featurePos, 0);\n}\nuvec4 getUintFromTex(usampler2D tex, int index) {\n int width = textureSize(tex, 0).x;\n ivec2 featurePos = ivec2(index % width, index / width);\n return texelFetch(tex, featurePos, 0);\n}\nuint getId(ivec2 uv) {\n uint rawId = texelFetch(srcTexture, uv, 0).r;\n if (rawId == 0u) {\n return BACKGROUND_ID;\n }\n uvec4 c = getUintFromTex(segIdToGlobalId, int(rawId - segIdOffset));\n // Note: IDs are offset by `1` to reserve `0` for segmentations that don't\n // have associated data. `1` MUST be subtracted from the ID when accessing\n // data buffers.\n uint globalId = c.r;\n if (globalId == 0u) {\n return MISSING_DATA_ID;\n }\n return globalId;\n}\nvec4 getColorRamp(float val) {\n float width = float(textureSize(colorRamp, 0).x);\n float range = (width - 1.0) / width;\n float adjustedVal = (0.5 / width) + (val * range);\n return texture(colorRamp, vec2(adjustedVal, 0.5));\n}\nvec4 getColorFromDrawMode(uint drawMode, vec3 defaultColor) {\n const uint DRAW_MODE_HIDE = 0u;\n vec3 backgroundColor = vec3(0.0, 0.0, 0.0);\n if (drawMode == DRAW_MODE_HIDE) {\n return vec4(backgroundColor, 0.0);\n } else {\n return vec4(defaultColor, 1.0);\n }\n}\n\nfloat getFeatureVal(uint id) {\n // Data buffer starts at 0, non-background segmentation IDs start at 1\n return getFloatFromTex(featureData, int(id) - 1).r;\n}\nuint getOutlierVal(uint id) {\n // Data buffer starts at 0, non-background segmentation IDs start at 1\n return getUintFromTex(outlierData, int(id) - 1).r;\n}\nbool getIsInRange(uint id) {\n return getUintFromTex(inRangeIds, int(id) - 1).r == 1u;\n}\nbool getIsOutlier(float featureVal, uint outlierVal) {\n return isinf(featureVal) || outlierVal != 0u;\n}\n\nvec4 getObjectColor(ivec2 sUv, float opacity) {\n // Get the segmentation id at this pixel\n uint id = getId(sUv);\n\n // A segmentation id of 0 represents background\n if (id == BACKGROUND_ID) {\n return vec4(0, 0, 0, 0);\n }\n\n // color the highlighted object. Note, `highlightedId` is a 0-based index\n // (global ID w/o offset), while `id` is a 1-based index.\n if (id - 1u == highlightedId) {\n return vec4(outlineColor, 1.0);\n }\n\n float featureVal = getFeatureVal(id);\n uint outlierVal = getOutlierVal(id);\n float normFeatureVal = (featureVal - featureColorRampMin) / (featureColorRampMax - featureColorRampMin);\n\n // Use the selected draw mode to handle out of range and outlier values;\n // otherwise color with the color ramp as usual.\n bool isInRange = getIsInRange(id);\n bool isOutlier = getIsOutlier(featureVal, outlierVal);\n bool isMissingData = (id == MISSING_DATA_ID);\n\n // Features outside the filtered/thresholded range will all be treated the same (use `outOfRangeDrawColor`).\n // Features inside the range can either be outliers or standard values, and are colored accordingly.\n vec4 color;\n if (isMissingData) { \n // TODO: Add color controls for missing data\n color = getColorFromDrawMode(outlierDrawMode, outlierColor);\n } else if (isInRange) {\n if (isOutlier) {\n color = getColorFromDrawMode(outlierDrawMode, outlierColor);\n } else {\n color = getColorRamp(normFeatureVal);\n }\n } else {\n color = getColorFromDrawMode(outOfRangeDrawMode, outOfRangeColor);\n }\n color.a *= opacity;\n return color;\n}\n\nvoid main() {\n ivec2 vUv = ivec2(int(gl_FragCoord.x), int(gl_FragCoord.y));\n gl_FragColor = getObjectColor(vUv, 1.0);\n}\n";
10
+ const colorizeSrcUI = "precision highp float;\nprecision highp int;\nprecision highp usampler2D;\nprecision highp sampler3D;\n\nuniform sampler2D featureData;\n/** Min and max feature values that define the endpoints of the color map. Values\n * outside the range will be clamped to the nearest endpoint.\n */\nuniform float featureColorRampMin;\nuniform float featureColorRampMax;\nuniform sampler2D colorRamp;\nuniform usampler2D inRangeIds;\nuniform usampler2D outlierData;\n\n/**\n * LUT mapping from the segmentation ID (raw pixel value) to the\n * global ID (index in data buffers like `featureData` and `outlierData`).\n * \n * For a given segmentation ID `segId`, the global ID is given by:\n * `segIdToGlobalId[segId - segIdOffset] - 1`.\n*/\nuniform usampler2D segIdToGlobalId;\nuniform uint segIdOffset;\n\nuniform vec3 outlineColor;\n\n/** MUST be synchronized with the DrawMode enum in ColorizeCanvas! */\nconst uint DRAW_MODE_HIDE = 0u;\nconst uint DRAW_MODE_COLOR = 1u;\nconst uint BACKGROUND_ID = 0u;\nconst uint MISSING_DATA_ID = 0xFFFFFFFFu;\n\nuniform vec3 outlierColor;\nuniform uint outlierDrawMode;\nuniform vec3 outOfRangeColor;\nuniform uint outOfRangeDrawMode;\n\nuniform uint highlightedId;\n\nuniform bool useRepeatingCategoricalColors;\n\n// src texture is the raw volume intensity data\nuniform usampler2D srcTexture;\n\nvec4 getFloatFromTex(sampler2D tex, int index) {\n int width = textureSize(tex, 0).x;\n ivec2 featurePos = ivec2(index % width, index / width);\n return texelFetch(tex, featurePos, 0);\n}\nuvec4 getUintFromTex(usampler2D tex, int index) {\n int width = textureSize(tex, 0).x;\n ivec2 featurePos = ivec2(index % width, index / width);\n return texelFetch(tex, featurePos, 0);\n}\nuint getId(ivec2 uv) {\n uint rawId = texelFetch(srcTexture, uv, 0).r;\n if (rawId == 0u) {\n return BACKGROUND_ID;\n }\n uvec4 c = getUintFromTex(segIdToGlobalId, int(rawId - segIdOffset));\n // Note: IDs are offset by `1` to reserve `0` for segmentations that don't\n // have associated data. `1` MUST be subtracted from the ID when accessing\n // data buffers.\n uint globalId = c.r;\n if (globalId == 0u) {\n return MISSING_DATA_ID;\n }\n return globalId;\n}\n\nvec4 getColorRamp(float val) {\n float width = float(textureSize(colorRamp, 0).x);\n float range = (width - 1.0) / width;\n float adjustedVal = (0.5 / width) + (val * range);\n return texture(colorRamp, vec2(adjustedVal, 0.5));\n}\n\nvec4 getCategoricalColor(float featureValue) {\n float width = float(textureSize(colorRamp, 0).x);\n float modValue = mod(featureValue, width);\n // The categorical texture uses no interpolation, so when sampling, `modValue`\n // is rounded to the nearest integer.\n return getColorRamp(modValue / (width - 1.0));\n}\n\nvec4 getColorFromDrawMode(uint drawMode, vec3 defaultColor) {\n const uint DRAW_MODE_HIDE = 0u;\n vec3 backgroundColor = vec3(0.0, 0.0, 0.0);\n if (drawMode == DRAW_MODE_HIDE) {\n return vec4(backgroundColor, 0.0);\n } else {\n return vec4(defaultColor, 1.0);\n }\n}\n\nfloat getFeatureVal(uint id) {\n // Data buffer starts at 0, non-background segmentation IDs start at 1\n return getFloatFromTex(featureData, int(id) - 1).r;\n}\nuint getOutlierVal(uint id) {\n // Data buffer starts at 0, non-background segmentation IDs start at 1\n return getUintFromTex(outlierData, int(id) - 1).r;\n}\nbool getIsInRange(uint id) {\n return getUintFromTex(inRangeIds, int(id) - 1).r == 1u;\n}\nbool getIsOutlier(float featureVal, uint outlierVal) {\n return isinf(featureVal) || outlierVal != 0u;\n}\n\nvec4 getObjectColor(ivec2 sUv, float opacity) {\n // Get the segmentation id at this pixel\n uint id = getId(sUv);\n\n // A segmentation id of 0 represents background\n if (id == BACKGROUND_ID) {\n return vec4(0, 0, 0, 0);\n }\n\n // color the highlighted object. Note, `highlightedId` is a 0-based index\n // (global ID w/o offset), while `id` is a 1-based index.\n if (id - 1u == highlightedId) {\n return vec4(outlineColor, 1.0);\n }\n\n float featureVal = getFeatureVal(id);\n uint outlierVal = getOutlierVal(id);\n float normFeatureVal = (featureVal - featureColorRampMin) / (featureColorRampMax - featureColorRampMin);\n\n // Use the selected draw mode to handle out of range and outlier values;\n // otherwise color with the color ramp as usual.\n bool isInRange = getIsInRange(id);\n bool isOutlier = getIsOutlier(featureVal, outlierVal);\n bool isMissingData = (id == MISSING_DATA_ID);\n\n // Features outside the filtered/thresholded range will all be treated the same (use `outOfRangeDrawColor`).\n // Features inside the range can either be outliers or standard values, and are colored accordingly.\n vec4 color;\n if (isMissingData) { \n // TODO: Add color controls for missing data\n color = getColorFromDrawMode(outlierDrawMode, outlierColor);\n } else if (isInRange) {\n if (isOutlier) {\n color = getColorFromDrawMode(outlierDrawMode, outlierColor);\n } else if (useRepeatingCategoricalColors) {\n color = getCategoricalColor(featureVal);\n } else {\n color = getColorRamp(normFeatureVal);\n }\n } else {\n color = getColorFromDrawMode(outOfRangeDrawMode, outOfRangeColor);\n }\n color.a *= opacity;\n return color;\n}\n\nvoid main() {\n ivec2 vUv = ivec2(int(gl_FragCoord.x), int(gl_FragCoord.y));\n gl_FragColor = getObjectColor(vUv, 1.0);\n}\n";
11
11
  // This is the owner of the fused RGBA volume texture atlas, and the mask texture atlas.
12
12
  // This module is responsible for updating the fused texture, given the read-only volume channel data.
13
13
  export default class FusedChannelData {
@@ -107,6 +107,9 @@ export default class FusedChannelData {
107
107
  colorRamp: {
108
108
  value: null
109
109
  },
110
+ useRepeatingCategoricalColors: {
111
+ value: false
112
+ },
110
113
  outlineColor: {
111
114
  value: new Color(0xffffff)
112
115
  },
@@ -219,6 +222,7 @@ export default class FusedChannelData {
219
222
  mat.uniforms.featureColorRampMin.value = feature.featureMin;
220
223
  mat.uniforms.featureColorRampMax.value = feature.featureMax;
221
224
  mat.uniforms.colorRamp.value = feature.featureValueToColor;
225
+ mat.uniforms.useRepeatingCategoricalColors.value = feature.useRepeatingColor;
222
226
  mat.uniforms.outlineColor.value = feature.outlineColor;
223
227
  mat.uniforms.outlierColor.value = feature.outlierColor;
224
228
  mat.uniforms.outOfRangeColor.value = feature.outOfRangeColor;
package/es/Line3d.js ADDED
@@ -0,0 +1,167 @@
1
+ import { Group, Vector3 } from "three";
2
+ import { LineMaterial } from "three/addons/lines/LineMaterial";
3
+ import { MESH_LAYER, OVERLAY_LAYER } from "./ThreeJsPanel";
4
+ import { LineSegments2 } from "three/addons/lines/LineSegments2";
5
+ import { LineSegmentsGeometry } from "three/addons/lines/LineSegmentsGeometry";
6
+ const DEFAULT_VERTEX_BUFFER_SIZE = 1020;
7
+
8
+ /**
9
+ * Simple wrapper for a 3D line segments object, with controls for vertex data,
10
+ * color, width, and segments visible.
11
+ */
12
+ export default class Line3d {
13
+ constructor() {
14
+ this.bufferSize = DEFAULT_VERTEX_BUFFER_SIZE;
15
+ const geometry = new LineSegmentsGeometry();
16
+ geometry.setPositions(new Float32Array(this.bufferSize));
17
+ const material = new LineMaterial({
18
+ color: "#f00",
19
+ linewidth: 2,
20
+ worldUnits: false
21
+ });
22
+ this.lineMesh = new LineSegments2(geometry, material);
23
+ this.lineMesh.layers.set(MESH_LAYER);
24
+ this.lineMesh.frustumCulled = false;
25
+ this.meshPivot = new Group();
26
+ this.meshPivot.add(this.lineMesh);
27
+ this.meshPivot.layers.set(MESH_LAYER);
28
+ this.scale = new Vector3(1, 1, 1);
29
+ this.flipAxes = new Vector3(1, 1, 1);
30
+ }
31
+
32
+ // IDrawableObject interface methods
33
+
34
+ cleanup() {
35
+ this.lineMesh.geometry.dispose();
36
+ this.lineMesh.material.dispose();
37
+ }
38
+ setVisible(visible) {
39
+ this.lineMesh.visible = visible;
40
+ }
41
+ doRender() {
42
+ // no op
43
+ }
44
+ get3dObject() {
45
+ return this.meshPivot;
46
+ }
47
+ setTranslation(translation) {
48
+ this.meshPivot.position.copy(translation);
49
+ }
50
+ setScale(scale) {
51
+ this.scale.copy(scale);
52
+ this.meshPivot.scale.copy(scale).multiply(this.flipAxes);
53
+ }
54
+ setRotation(eulerXYZ) {
55
+ this.meshPivot.rotation.copy(eulerXYZ);
56
+ }
57
+ setFlipAxes(flipX, flipY, flipZ) {
58
+ this.flipAxes.set(flipX, flipY, flipZ);
59
+ this.meshPivot.scale.copy(this.scale).multiply(this.flipAxes);
60
+ }
61
+ setOrthoThickness(_thickness) {
62
+ // no op
63
+ }
64
+ setResolution(_x, _y) {
65
+ // no op
66
+ }
67
+ setAxisClip(_axis, _minval, _maxval, _isOrthoAxis) {
68
+ // no op
69
+ }
70
+ updateClipRegion(_xmin, _xmax, _ymin, _ymax, _zmin, _zmax) {
71
+ // no op
72
+ }
73
+
74
+ // Line-specific functions
75
+
76
+ /**
77
+ * Sets the color of the line material.
78
+ * @param color Base line color.
79
+ * @param useVertexColors If true, _the line will multiply the base color with
80
+ * the per-vertex colors defined in the geometry (see `setLineVertexData`). Default is false.
81
+ */
82
+ setColor(color, useVertexColors = false) {
83
+ this.lineMesh.material.color.set(color);
84
+ this.lineMesh.material.vertexColors = useVertexColors;
85
+ this.lineMesh.material.needsUpdate = true;
86
+ }
87
+
88
+ /**
89
+ * Sets the opacity of the line material.
90
+ *
91
+ * Note that transparent lines may have unexpected colors when intersecting
92
+ * with or overlapping the volume. Consider setting them to render as an
93
+ * overlay instead using `setOverlay()`.
94
+ * @param opacity Opacity value in the range `[0, 1]`. 0 is fully transparent,
95
+ * 1 is fully opaque.
96
+ */
97
+ setOpacity(opacity) {
98
+ const isTransparent = opacity < 1.0;
99
+ this.lineMesh.material.opacity = opacity;
100
+ this.lineMesh.material.transparent = isTransparent;
101
+ this.lineMesh.material.needsUpdate = true;
102
+ }
103
+
104
+ /**
105
+ * Sets whether a line should be rendered as an overlay (rendered on top of
106
+ * the volume) instead of a mesh (with depth, intersects with volume).
107
+ * @param renderAsOverlay If true, the line will be rendered on top of the
108
+ * volume, ignoring depth.
109
+ */
110
+ setRenderAsOverlay(renderAsOverlay) {
111
+ this.lineMesh.layers.set(renderAsOverlay ? OVERLAY_LAYER : MESH_LAYER);
112
+ this.lineMesh.material.depthTest = !renderAsOverlay;
113
+ this.lineMesh.material.depthTest = !renderAsOverlay;
114
+ this.lineMesh.material.needsUpdate = true;
115
+ }
116
+
117
+ /**
118
+ * Sets the width of the line in pixels.
119
+ * @param widthPx Width in pixels.
120
+ * @param useWorldUnits Whether to use world units for the line width. By
121
+ * default (false), the width is in screen pixels.
122
+ */
123
+ setLineWidth(widthPx, useWorldUnits = false) {
124
+ this.lineMesh.material.linewidth = widthPx;
125
+ this.lineMesh.material.worldUnits = useWorldUnits;
126
+ this.lineMesh.material.needsUpdate = true;
127
+ }
128
+
129
+ /**
130
+ * Sets the vertex data (position and RGB colors) for the line segments.
131
+ * @param positions A Float32Array of 3D coordinates, where each pair of
132
+ * coordinates is one line segment. Length must be a multiple of 6 (pairs of
133
+ * two 3-dimensional coordinates).
134
+ * @param colors A Float32Array of RGB values in the [0, 1] range, where each
135
+ * triplet corresponds to a vertex color.
136
+ * @throws {Error} If `positions` length is not a multiple of 6 or if `colors`
137
+ * length is not a multiple of 3.
138
+ */
139
+ setLineVertexData(positions, colors) {
140
+ if (positions.length % 6 !== 0) {
141
+ throw new Error(`positions length of ${positions.length} is not a multiple of 6 (pairs of two 3-dimensional coordinates)`);
142
+ }
143
+ if (colors !== undefined && colors.length % 3 !== 0) {
144
+ throw new Error(`colors length of ${colors.length} is not a multiple of 3 (triplets of RGB values)`);
145
+ }
146
+ const newBufferSize = Math.max(positions.length, colors ? colors.length : 0);
147
+ // If buffer size is too small, dispose of the old geometry and create a new
148
+ // one with the larger size.
149
+ if (newBufferSize > this.bufferSize) {
150
+ this.lineMesh.geometry.dispose();
151
+ this.lineMesh.geometry = new LineSegmentsGeometry();
152
+ this.bufferSize = newBufferSize;
153
+ }
154
+ this.lineMesh.geometry.setPositions(positions);
155
+ if (colors) {
156
+ this.lineMesh.geometry.setColors(colors);
157
+ }
158
+ }
159
+
160
+ /** Number of line segments that should be visible. */
161
+ setNumSegmentsVisible(segments) {
162
+ if (this.lineMesh.geometry) {
163
+ const count = segments;
164
+ this.lineMesh.geometry.instanceCount = Math.max(0, count);
165
+ }
166
+ }
167
+ }
@@ -9,6 +9,7 @@ import RenderToBuffer from "./RenderToBuffer.js";
9
9
  import { copyImageFragShader } from "./constants/basicShaders.js";
10
10
  export const VOLUME_LAYER = 0;
11
11
  export const MESH_LAYER = 1;
12
+ export const OVERLAY_LAYER = 2;
12
13
  const DEFAULT_PERSPECTIVE_CAMERA_DISTANCE = 5.0;
13
14
  const DEFAULT_PERSPECTIVE_CAMERA_NEAR = 0.1;
14
15
  const DEFAULT_PERSPECTIVE_CAMERA_FAR = 20.0;
@@ -554,9 +555,14 @@ export class ThreeJsPanel {
554
555
  this.meshRenderToBuffer.render(this.renderer);
555
556
 
556
557
  // Step 3: Render volumes, which can now depth test against the meshes.
558
+ this.renderer.autoClear = false;
557
559
  this.camera.layers.set(VOLUME_LAYER);
558
560
  this.renderer.setRenderTarget(null);
559
- this.renderer.autoClear = false;
561
+ this.renderer.render(this.scene, this.camera);
562
+
563
+ // Step 4: Render lines and other objects that must render over volumes and meshes.
564
+ this.camera.layers.set(OVERLAY_LAYER);
565
+ this.renderer.setRenderTarget(null);
560
566
  this.renderer.render(this.scene, this.camera);
561
567
  this.renderer.autoClear = true;
562
568
 
@@ -588,6 +594,7 @@ export class ThreeJsPanel {
588
594
  const delta = this.timer.lastFrameMs / 1000.0;
589
595
  this.controls.update(delta);
590
596
  this.render();
597
+ this.onRenderCallback?.();
591
598
  }
592
599
  startRenderLoop() {
593
600
  this.inRenderLoop = true;
@@ -604,6 +611,9 @@ export class ThreeJsPanel {
604
611
  }
605
612
  this.timer.end();
606
613
  }
614
+ setOnRenderCallback(callback) {
615
+ this.onRenderCallback = callback ?? undefined;
616
+ }
607
617
  removeControlHandlers() {
608
618
  if (this.controlStartHandler) {
609
619
  this.controls.removeEventListener("start", this.controlStartHandler);
@@ -648,7 +658,7 @@ export class ThreeJsPanel {
648
658
  // read from the instance buffer
649
659
  const pixel = new Float32Array(4).fill(-1);
650
660
  this.renderer.readRenderTargetPixels(pickBuffer, sx, sy, 1, 1, pixel);
651
- // For future reference, Simularium stores the following:
661
+ // For future reference, Simularium stores the following:
652
662
  // (typeId), (instanceId), fragViewPos.z, fragPosDepth;
653
663
 
654
664
  if (pixel[3] === -1 || pixel[3] === 0) {
package/es/View3d.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AmbientLight, Vector3, Object3D, SpotLight, DirectionalLight, Euler, Scene, Color } from "three";
1
+ import { AmbientLight, Vector3, Object3D, SpotLight, DirectionalLight, Euler, Scene, Color, Matrix4 } from "three";
2
2
  import { Pane } from "tweakpane";
3
3
  import { MESH_LAYER, ThreeJsPanel } from "./ThreeJsPanel.js";
4
4
  import lightSettings from "./constants/lights.js";
@@ -107,6 +107,18 @@ export class View3d {
107
107
  this.redraw();
108
108
  }
109
109
 
110
+ /**
111
+ * Returns the view projection matrix, which transforms from world coordinates
112
+ * to clip space coordinates.
113
+ *
114
+ * 3D coordinates within the camera's view frustum will be transformed to a
115
+ * [-1, 1] range in the X and Y axes, and a [0, 1] range in the Z axis.
116
+ */
117
+ getViewProjectionMatrix() {
118
+ const camera = this.canvas3d.camera;
119
+ return new Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
120
+ }
121
+
110
122
  /**
111
123
  * Force a redraw.
112
124
  * @param synchronous If true, the redraw will be done synchronously. If false (default), the
@@ -120,6 +132,13 @@ export class View3d {
120
132
  this.canvas3d.redraw();
121
133
  }
122
134
  }
135
+
136
+ /**
137
+ * Sets a listener that will be called after the 3D canvas renders.
138
+ */
139
+ setOnRenderCallback(callback) {
140
+ this.canvas3d.setOnRenderCallback(callback);
141
+ }
123
142
  unsetImage() {
124
143
  if (this.image) {
125
144
  this.canvas3d.removeControlHandlers();
@@ -824,6 +843,39 @@ export class View3d {
824
843
  }
825
844
  }
826
845
 
846
+ /**
847
+ * Adds a Line3d object as a child of the Volume, if a volume has been added
848
+ * to the view and the line is not already a child of it. Line positions will
849
+ * be in the normalized coordinate space of the Volume, where the origin
850
+ * (0,0,0) is at the center of the Volume and the extent is from -0.5 to 0.5
851
+ * in each axis.
852
+ */
853
+ addLineObject(line) {
854
+ if (this.image) {
855
+ this.image.addLineObject(line);
856
+ this.redraw();
857
+ }
858
+ }
859
+
860
+ /** Returns whether a Line3d object exists as a child of the volume. */
861
+ hasLineObject(line) {
862
+ if (this.image) {
863
+ return this.image.hasLineObject(line);
864
+ }
865
+ return false;
866
+ }
867
+
868
+ /**
869
+ * Removes a Line3d object from the Volume, if it exists. Note that the
870
+ * object's resources are not freed automatically (e.g. via `line.cleanup()`).
871
+ */
872
+ removeLineObject(line) {
873
+ if (this.image) {
874
+ this.image.removeLineObject(line);
875
+ this.redraw();
876
+ }
877
+ }
878
+
827
879
  /**
828
880
  * @description Enable or disable picking on a volume. If enabled, the channelIndex is used to determine which channel to pick.
829
881
  * @param volume the image to enable picking on
package/es/Volume.js CHANGED
@@ -14,13 +14,30 @@ export default class Volume {
14
14
  * Used to intelligently issue load requests whenever required by a state change. Modify with `updateRequiredData`.
15
15
  */
16
16
 
17
- /** The maximum of the measurements of 3 axes in physical units (pixels*physicalSize) */
17
+ /**
18
+ * The maximum of the measurements of 3 axes in physical units.
19
+ *
20
+ * Equivalent to `Math.max(physicalSize.x, physicalSize.y, physicalSize.z)`.
21
+ */
18
22
 
19
- /** The physical size of a voxel in the original level 0 volume */
23
+ /**
24
+ * The size of a voxel in the original level 0 volume in real-world (physical)
25
+ * units (e.g. μm).
26
+ */
20
27
 
21
- /** The physical dims of the whole volume (not accounting for subregion) */
28
+ /**
29
+ * The dimensions of the whole volume (not accounting for subregion) in
30
+ * real-world (physical) units. Equivalent to `imageInfo.originalSize *
31
+ * physicalPixelSize`.
32
+ */
22
33
 
23
- /** Normalized physical size of the whole volume (not accounting for subregion) */
34
+ /**
35
+ * Physical size of the whole volume (not accounting for subregion) normalized
36
+ * along the largest axis, so that all dimensions are `<= 1`.
37
+ *
38
+ * Example: If the physical size is `[100, 50, 25]` μm, then the
39
+ * normPhysicalSize will be `[1, 0.5, 0.25]`.
40
+ */
24
41
 
25
42
  constructor(imageInfo = defaultImageInfo(), loadSpec = new LoadSpec(), loader) {
26
43
  this.loaded = false;
@@ -1,4 +1,4 @@
1
- import { Vector3, Object3D, Euler, Vector2, Box3 } from "three";
1
+ import { Vector3, Object3D, Euler, Vector2, Box3, Group } from "three";
2
2
  import MeshVolume from "./MeshVolume.js";
3
3
  import RayMarchedAtlasVolume from "./RayMarchedAtlasVolume.js";
4
4
  import PathTracedVolume from "./PathTracedVolume.js";
@@ -23,6 +23,19 @@ export const colorObjectToArray = ({
23
23
 
24
24
  // A renderable multichannel volume image with 8-bits per channel intensity values.
25
25
  export default class VolumeDrawable {
26
+ // TODO: Move responsibility for drawable objects that are otherwise unrelated
27
+ // to the volume out of VolumeDrawable. Consider making a parent group that
28
+ // can contain all other drawable objects AND the VolumeDrawable and its owned
29
+ // objects (MeshVolume).
30
+ /**
31
+ * Group for all child objects of the volume. The group is scaled and
32
+ * transformed to the normalized volume coordinate space, where the volume's
33
+ * center is at (0, 0, 0) and the bounds have a range of [-0.5, 0.5] along each
34
+ * axis.
35
+ */
36
+
37
+ /** Set of drawable objects that are children of the volume's transform. */
38
+
26
39
  constructor(volume, options) {
27
40
  // THE VOLUME DATA
28
41
  this.volume = volume;
@@ -49,7 +62,9 @@ export default class VolumeDrawable {
49
62
  });
50
63
  this.sceneRoot = new Object3D(); //create an empty container
51
64
 
52
- this.meshVolume = new MeshVolume(this.volume);
65
+ this.childObjectsGroup = new Group();
66
+ this.childObjects = new Set();
67
+ this.sceneRoot.add(this.childObjectsGroup);
53
68
  options.renderMode = options.renderMode || RenderMode.RAYMARCH;
54
69
  switch (options.renderMode) {
55
70
  case RenderMode.PATHTRACE:
@@ -67,8 +82,10 @@ export default class VolumeDrawable {
67
82
  }
68
83
 
69
84
  // draw meshes first, and volume last, for blending and depth test reasons with raymarch
85
+ this.meshVolume = new MeshVolume(this.volume);
70
86
  if (options.renderMode === RenderMode.RAYMARCH || options.renderMode === RenderMode.SLICE) {
71
- this.sceneRoot.add(this.meshVolume.get3dObject());
87
+ this.childObjectsGroup.add(this.meshVolume.get3dObject());
88
+ this.childObjects.add(this.meshVolume);
72
89
  }
73
90
  this.sceneRoot.add(this.volumeRendering.get3dObject());
74
91
  // draw meshes last (as overlay) for pathtrace? (or not at all?)
@@ -216,7 +233,8 @@ export default class VolumeDrawable {
216
233
  normRegionSize
217
234
  } = this.volume;
218
235
  const scale = normPhysicalSize.clone().multiply(normRegionSize).multiply(this.settings.scale);
219
- this.meshVolume.setScale(scale, this.volume.getContentCenter().multiply(this.settings.scale));
236
+ this.childObjectsGroup.scale.copy(scale);
237
+ this.childObjectsGroup.position.copy(this.volume.getContentCenter().multiply(this.settings.scale));
220
238
  // TODO only `RayMarchedAtlasVolume` handles scale properly. Get the others on board too!
221
239
  this.volumeRendering.updateVolumeDimensions();
222
240
  this.volumeRendering.updateSettings(this.settings, SettingsFlags.TRANSFORM);
@@ -234,7 +252,9 @@ export default class VolumeDrawable {
234
252
  setResolution(x, y) {
235
253
  const resolution = new Vector2(x, y);
236
254
  if (!this.settings.resolution.equals(resolution)) {
237
- this.meshVolume.setResolution(x, y);
255
+ for (const object of this.childObjects) {
256
+ object.setResolution(x, y);
257
+ }
238
258
  this.settings.resolution = resolution;
239
259
  this.volumeRendering.updateSettings(this.settings, SettingsFlags.SAMPLING);
240
260
  this.pickRendering?.updateSettings(this.settings, SettingsFlags.SAMPLING);
@@ -259,7 +279,9 @@ export default class VolumeDrawable {
259
279
 
260
280
  // Configure mesh volume when in an orthographic axis alignment
261
281
  if (axis !== Axis.NONE && this.renderMode !== RenderMode.PATHTRACE) {
262
- this.meshVolume.setAxisClip(axis, minval, maxval, !!isOrthoAxis);
282
+ for (const object of this.childObjects) {
283
+ object.setAxisClip(axis, minval, maxval, !!isOrthoAxis);
284
+ }
263
285
  }
264
286
  this.volumeRendering.updateSettings(this.settings, SettingsFlags.ROI | SettingsFlags.VIEW);
265
287
  this.pickRendering?.updateSettings(this.settings, SettingsFlags.ROI | SettingsFlags.VIEW);
@@ -323,7 +345,9 @@ export default class VolumeDrawable {
323
345
  if (this.renderMode === RenderMode.PATHTRACE) {
324
346
  return;
325
347
  }
326
- this.meshVolume.setOrthoThickness(value);
348
+ for (const object of this.childObjects) {
349
+ object.setOrthoThickness(value);
350
+ }
327
351
  // No settings update because ortho thickness is calculated in the renderers
328
352
  }
329
353
 
@@ -345,7 +369,9 @@ export default class VolumeDrawable {
345
369
  const flipAxes = new Vector3(flipX, flipY, flipZ);
346
370
  if (!this.settings.flipAxes.equals(flipAxes)) {
347
371
  this.settings.flipAxes = flipAxes;
348
- this.meshVolume.setFlipAxes(flipX, flipY, flipZ);
372
+ for (const object of this.childObjects) {
373
+ object.setFlipAxes(flipX, flipY, flipZ);
374
+ }
349
375
  this.volumeRendering.updateSettings(this.settings, SettingsFlags.TRANSFORM);
350
376
  this.pickRendering?.updateSettings(this.settings, SettingsFlags.TRANSFORM);
351
377
  }
@@ -367,7 +393,9 @@ export default class VolumeDrawable {
367
393
  // TODO confirm sequence
368
394
  this.volumeRendering.doRender(renderer, camera, depthTexture);
369
395
  if (this.renderMode !== RenderMode.PATHTRACE) {
370
- this.meshVolume.doRender();
396
+ for (const object of this.childObjects) {
397
+ object.doRender();
398
+ }
371
399
  }
372
400
  }
373
401
  enablePicking(enabled, channelIndex) {
@@ -441,7 +469,9 @@ export default class VolumeDrawable {
441
469
  this.updateScale();
442
470
  }
443
471
  cleanup() {
444
- this.meshVolume.cleanup();
472
+ for (const object of this.childObjects) {
473
+ object.cleanup();
474
+ }
445
475
  this.volumeRendering.cleanup();
446
476
  this.pickRendering?.cleanup();
447
477
  }
@@ -635,7 +665,9 @@ export default class VolumeDrawable {
635
665
  updateClipRegion(xmin, xmax, ymin, ymax, zmin, zmax) {
636
666
  this.settings.bounds.bmin = new Vector3(xmin - 0.5, ymin - 0.5, zmin - 0.5);
637
667
  this.settings.bounds.bmax = new Vector3(xmax - 0.5, ymax - 0.5, zmax - 0.5);
638
- this.meshVolume.updateClipRegion(xmin, xmax, ymin, ymax, zmin, zmax);
668
+ for (const object of this.childObjects) {
669
+ object.updateClipRegion(xmin, xmax, ymin, ymax, zmin, zmax);
670
+ }
639
671
  this.volumeRendering.updateSettings(this.settings, SettingsFlags.ROI);
640
672
  this.pickRendering?.updateSettings(this.settings, SettingsFlags.ROI);
641
673
  }
@@ -717,6 +749,37 @@ export default class VolumeDrawable {
717
749
  this.settings.scale.copy(xyz);
718
750
  this.updateScale();
719
751
  }
752
+
753
+ /**
754
+ * Adds a Line3d object as a child of the Volume, if it does not already
755
+ * exist. Line objects will be in the normalized coordinate space of the
756
+ * Volume, where the origin (0,0,0) is at the center of the Volume and the
757
+ * extent is from -0.5 to 0.5 in each axis.
758
+ */
759
+ addLineObject(line) {
760
+ if (!this.childObjects.has(line)) {
761
+ this.childObjects.add(line);
762
+ this.childObjectsGroup.add(line.get3dObject());
763
+ line.setResolution(this.settings.resolution.x, this.settings.resolution.y);
764
+ line.setFlipAxes(this.settings.flipAxes.x, this.settings.flipAxes.y, this.settings.flipAxes.z);
765
+ }
766
+ }
767
+
768
+ /** Returns whether a line object exists as a child of the volume. */
769
+ hasLineObject(line) {
770
+ return this.childObjects.has(line);
771
+ }
772
+
773
+ /**
774
+ * Removes a Line3d object from the Volume, if it exists. Note that the
775
+ * object's resources are not freed automatically (e.g. via `line.cleanup()`).
776
+ */
777
+ removeLineObject(line) {
778
+ if (this.childObjects.has(line)) {
779
+ this.childObjects.delete(line);
780
+ this.childObjectsGroup.remove(line.get3dObject());
781
+ }
782
+ }
720
783
  setupGui(pane) {
721
784
  pane.addInput(this.settings, "translation").on("change", ({
722
785
  value
package/es/index.js CHANGED
@@ -18,4 +18,5 @@ import { TiffLoader } from "./loaders/TiffLoader.js";
18
18
  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
- export { Histogram, Lut, 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 };
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 };
@@ -0,0 +1,70 @@
1
+ import { Color, Euler, Group, Vector3 } from "three";
2
+ import { IDrawableObject } from "./types";
3
+ /**
4
+ * Simple wrapper for a 3D line segments object, with controls for vertex data,
5
+ * color, width, and segments visible.
6
+ */
7
+ export default class Line3d implements IDrawableObject {
8
+ private meshPivot;
9
+ private scale;
10
+ private flipAxes;
11
+ private lineMesh;
12
+ private bufferSize;
13
+ 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
+ /**
27
+ * Sets the color of the line material.
28
+ * @param color Base line color.
29
+ * @param useVertexColors If true, _the line will multiply the base color with
30
+ * the per-vertex colors defined in the geometry (see `setLineVertexData`). Default is false.
31
+ */
32
+ setColor(color: Color, useVertexColors?: boolean): void;
33
+ /**
34
+ * Sets the opacity of the line material.
35
+ *
36
+ * Note that transparent lines may have unexpected colors when intersecting
37
+ * with or overlapping the volume. Consider setting them to render as an
38
+ * overlay instead using `setOverlay()`.
39
+ * @param opacity Opacity value in the range `[0, 1]`. 0 is fully transparent,
40
+ * 1 is fully opaque.
41
+ */
42
+ setOpacity(opacity: number): void;
43
+ /**
44
+ * Sets whether a line should be rendered as an overlay (rendered on top of
45
+ * the volume) instead of a mesh (with depth, intersects with volume).
46
+ * @param renderAsOverlay If true, the line will be rendered on top of the
47
+ * volume, ignoring depth.
48
+ */
49
+ setRenderAsOverlay(renderAsOverlay: boolean): void;
50
+ /**
51
+ * Sets the width of the line in pixels.
52
+ * @param widthPx Width in pixels.
53
+ * @param useWorldUnits Whether to use world units for the line width. By
54
+ * default (false), the width is in screen pixels.
55
+ */
56
+ setLineWidth(widthPx: number, useWorldUnits?: boolean): void;
57
+ /**
58
+ * Sets the vertex data (position and RGB colors) for the line segments.
59
+ * @param positions A Float32Array of 3D coordinates, where each pair of
60
+ * coordinates is one line segment. Length must be a multiple of 6 (pairs of
61
+ * two 3-dimensional coordinates).
62
+ * @param colors A Float32Array of RGB values in the [0, 1] range, where each
63
+ * triplet corresponds to a vertex color.
64
+ * @throws {Error} If `positions` length is not a multiple of 6 or if `colors`
65
+ * length is not a multiple of 3.
66
+ */
67
+ setLineVertexData(positions: Float32Array, colors?: Float32Array): void;
68
+ /** Number of line segments that should be visible. */
69
+ setNumSegmentsVisible(segments: number): void;
70
+ }
@@ -1,6 +1,7 @@
1
1
  import { BufferGeometry, Euler, Object3D, Vector3, Group, Material } from "three";
2
2
  import Volume from "./Volume.js";
3
- export default class MeshVolume {
3
+ import type { IDrawableObject } from "./types.js";
4
+ export default class MeshVolume implements IDrawableObject {
4
5
  private volume;
5
6
  private meshRoot;
6
7
  private meshPivot;
@@ -3,6 +3,7 @@ import TrackballControls from "./TrackballControls.js";
3
3
  import { ViewportCorner } from "./types.js";
4
4
  export declare const VOLUME_LAYER = 0;
5
5
  export declare const MESH_LAYER = 1;
6
+ export declare const OVERLAY_LAYER = 2;
6
7
  export type CameraState = {
7
8
  position: [number, number, number];
8
9
  up: [number, number, number];
@@ -53,6 +54,7 @@ export declare class ThreeJsPanel {
53
54
  private timestepIndicatorElement;
54
55
  showTimestepIndicator: boolean;
55
56
  private dataurlcallback?;
57
+ private onRenderCallback?;
56
58
  constructor(parentElement: HTMLElement | undefined, _useWebGL2: boolean);
57
59
  updateCameraFocus(fov: number, _focalDistance: number, _apertureSize: number): void;
58
60
  resetPerspectiveCamera(): void;
@@ -102,6 +104,7 @@ export declare class ThreeJsPanel {
102
104
  onAnimationLoop(): void;
103
105
  startRenderLoop(): void;
104
106
  stopRenderLoop(): void;
107
+ setOnRenderCallback(callback: (() => void) | null): void;
105
108
  removeControlHandlers(): void;
106
109
  setControlHandlers(onstart: EventListener<Event, "start", TrackballControls>, onchange: EventListener<Event, "change", TrackballControls>, onend: EventListener<Event, "end", TrackballControls>): void;
107
110
  hitTest(offsetX: number, offsetY: number, pickBuffer: WebGLRenderTarget | undefined): number;
@@ -1,3 +1,4 @@
1
+ import { Matrix4 } from "three";
1
2
  import { CameraState } from "./ThreeJsPanel.js";
2
3
  import VolumeDrawable from "./VolumeDrawable.js";
3
4
  import { Light } from "./Light.js";
@@ -5,6 +6,7 @@ import Volume from "./Volume.js";
5
6
  import { type ColorizeFeature, type VolumeChannelDisplayOptions, type VolumeDisplayOptions, ViewportCorner, RenderMode } from "./types.js";
6
7
  import { PerChannelCallback } from "./loaders/IVolumeLoader.js";
7
8
  import VolumeLoaderContext from "./workers/VolumeLoaderContext.js";
9
+ import Line3d from "./Line3d.js";
8
10
  export declare const RENDERMODE_RAYMARCH = RenderMode.RAYMARCH;
9
11
  export declare const RENDERMODE_PATHTRACE = RenderMode.PATHTRACE;
10
12
  export interface View3dOptions {
@@ -52,6 +54,14 @@ export declare class View3d {
52
54
  getCanvasDOMElement(): HTMLCanvasElement;
53
55
  getCameraState(): CameraState;
54
56
  setCameraState(transform: Partial<CameraState>): void;
57
+ /**
58
+ * Returns the view projection matrix, which transforms from world coordinates
59
+ * to clip space coordinates.
60
+ *
61
+ * 3D coordinates within the camera's view frustum will be transformed to a
62
+ * [-1, 1] range in the X and Y axes, and a [0, 1] range in the Z axis.
63
+ */
64
+ getViewProjectionMatrix(): Matrix4;
55
65
  /**
56
66
  * Force a redraw.
57
67
  * @param synchronous If true, the redraw will be done synchronously. If false (default), the
@@ -59,6 +69,10 @@ export declare class View3d {
59
69
  * whenever possible for the best performance.
60
70
  */
61
71
  redraw(synchronous?: boolean): void;
72
+ /**
73
+ * Sets a listener that will be called after the 3D canvas renders.
74
+ */
75
+ setOnRenderCallback(callback: (() => void) | null): void;
62
76
  unsetImage(): VolumeDrawable | undefined;
63
77
  /**
64
78
  * Add a new volume image to the viewer. (The viewer currently only supports a single image at a time - adding repeatedly, without removing in between, is a potential resource leak)
@@ -371,6 +385,21 @@ export declare class View3d {
371
385
  * @param id the selected id
372
386
  */
373
387
  setSelectedID(volume: Volume, channel: number, id: number): void;
388
+ /**
389
+ * Adds a Line3d object as a child of the Volume, if a volume has been added
390
+ * to the view and the line is not already a child of it. Line positions will
391
+ * be in the normalized coordinate space of the Volume, where the origin
392
+ * (0,0,0) is at the center of the Volume and the extent is from -0.5 to 0.5
393
+ * in each axis.
394
+ */
395
+ addLineObject(line: Line3d): void;
396
+ /** Returns whether a Line3d object exists as a child of the volume. */
397
+ hasLineObject(line: Line3d): boolean;
398
+ /**
399
+ * Removes a Line3d object from the Volume, if it exists. Note that the
400
+ * object's resources are not freed automatically (e.g. via `line.cleanup()`).
401
+ */
402
+ removeLineObject(line: Line3d): void;
374
403
  /**
375
404
  * @description Enable or disable picking on a volume. If enabled, the channelIndex is used to determine which channel to pick.
376
405
  * @param volume the image to enable picking on
@@ -30,13 +30,30 @@ export default class Volume {
30
30
  numChannels: number;
31
31
  channelNames: string[];
32
32
  channelColorsDefault: [number, number, number][];
33
- /** The maximum of the measurements of 3 axes in physical units (pixels*physicalSize) */
33
+ /**
34
+ * The maximum of the measurements of 3 axes in physical units.
35
+ *
36
+ * Equivalent to `Math.max(physicalSize.x, physicalSize.y, physicalSize.z)`.
37
+ */
34
38
  physicalScale: number;
35
- /** The physical size of a voxel in the original level 0 volume */
39
+ /**
40
+ * The size of a voxel in the original level 0 volume in real-world (physical)
41
+ * units (e.g. μm).
42
+ */
36
43
  physicalPixelSize: Vector3;
37
- /** The physical dims of the whole volume (not accounting for subregion) */
44
+ /**
45
+ * The dimensions of the whole volume (not accounting for subregion) in
46
+ * real-world (physical) units. Equivalent to `imageInfo.originalSize *
47
+ * physicalPixelSize`.
48
+ */
38
49
  physicalSize: Vector3;
39
- /** Normalized physical size of the whole volume (not accounting for subregion) */
50
+ /**
51
+ * Physical size of the whole volume (not accounting for subregion) normalized
52
+ * along the largest axis, so that all dimensions are `<= 1`.
53
+ *
54
+ * Example: If the physical size is `[100, 50, 25]` μm, then the
55
+ * normPhysicalSize will be `[1, 0.5, 0.25]`.
56
+ */
40
57
  normPhysicalSize: Vector3;
41
58
  normRegionSize: Vector3;
42
59
  normRegionOffset: Vector3;
@@ -6,6 +6,7 @@ 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";
9
10
  type ColorArray = [number, number, number];
10
11
  type ColorObject = {
11
12
  r: number;
@@ -27,6 +28,15 @@ export default class VolumeDrawable {
27
28
  private fusion;
28
29
  sceneRoot: Object3D;
29
30
  private meshVolume;
31
+ /**
32
+ * Group for all child objects of the volume. The group is scaled and
33
+ * transformed to the normalized volume coordinate space, where the volume's
34
+ * center is at (0, 0, 0) and the bounds have a range of [-0.5, 0.5] along each
35
+ * axis.
36
+ */
37
+ private childObjectsGroup;
38
+ /** Set of drawable objects that are children of the volume's transform. */
39
+ private childObjects;
30
40
  private volumeRendering;
31
41
  private pickRendering?;
32
42
  private renderMode;
@@ -107,6 +117,20 @@ export default class VolumeDrawable {
107
117
  setTranslation(xyz: Vector3): void;
108
118
  setRotation(eulerXYZ: Euler): void;
109
119
  setScale(xyz: Vector3): void;
120
+ /**
121
+ * Adds a Line3d object as a child of the Volume, if it does not already
122
+ * exist. Line objects will be in the normalized coordinate space of the
123
+ * Volume, where the origin (0,0,0) is at the center of the Volume and the
124
+ * extent is from -0.5 to 0.5 in each axis.
125
+ */
126
+ addLineObject(line: Line3d): void;
127
+ /** Returns whether a line object exists as a child of the volume. */
128
+ hasLineObject(line: Line3d): boolean;
129
+ /**
130
+ * Removes a Line3d object from the Volume, if it exists. Note that the
131
+ * object's resources are not freed automatically (e.g. via `line.cleanup()`).
132
+ */
133
+ removeLineObject(line: Line3d): void;
110
134
  setupGui(pane: Pane): void;
111
135
  setZSlice(slice: number): boolean;
112
136
  get showBoundingBox(): boolean;
@@ -19,10 +19,11 @@ import VolumeLoaderContext from "./workers/VolumeLoaderContext.js";
19
19
  import { VolumeLoadError, VolumeLoadErrorType } from "./loaders/VolumeLoadError.js";
20
20
  import { type CameraState } from "./ThreeJsPanel.js";
21
21
  import { Light, AREA_LIGHT, SKY_LIGHT } from "./Light.js";
22
+ import Line3d from "./Line3d.js";
22
23
  export type { ImageInfo } from "./ImageInfo.js";
23
24
  export type { ControlPoint } from "./Lut.js";
24
25
  export type { CreateLoaderOptions } from "./loaders/index.js";
25
26
  export type { IVolumeLoader, PerChannelCallback, ThreadableVolumeLoader } from "./loaders/IVolumeLoader.js";
26
27
  export type { ZarrLoaderFetchOptions } from "./loaders/OmeZarrLoader.js";
27
28
  export type { WorkerLoader } from "./workers/VolumeLoaderContext.js";
28
- export { Histogram, Lut, 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, };
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, };
@@ -1,4 +1,4 @@
1
- import { Camera, Color, DataTexture, OrthographicCamera, PerspectiveCamera, Vector3 } from "three";
1
+ import { Camera, Color, DataTexture, Euler, Group, OrthographicCamera, PerspectiveCamera, Vector3 } from "three";
2
2
  export interface Bounds {
3
3
  bmin: Vector3;
4
4
  bmax: Vector3;
@@ -30,6 +30,13 @@ export declare const ARRAY_CONSTRUCTORS: {
30
30
  export interface ColorizeFeature {
31
31
  idsToFeatureValue: DataTexture;
32
32
  featureValueToColor: DataTexture;
33
+ /**
34
+ * Ignore the feature min and max, and treat the color ramp texture as a
35
+ * direct lookup for feature values. Feature values that are greater than
36
+ * the length of the color ramp will be wrapped around to the start
37
+ * (e.g. `value % colorRamp.length`).
38
+ */
39
+ useRepeatingColor: boolean;
33
40
  /**
34
41
  * Maps from a frame number to an info object used to look up the global ID
35
42
  * from a given segmentation ID (raw pixel value) on that frame. The info
@@ -64,6 +71,20 @@ export interface ColorizeFeature {
64
71
  outOfRangeDrawMode: number;
65
72
  hideOutOfRange: boolean;
66
73
  }
74
+ export interface IDrawableObject {
75
+ cleanup(): void;
76
+ setVisible(visible: boolean): void;
77
+ doRender(): void;
78
+ get3dObject(): Group;
79
+ setTranslation(translation: Vector3): void;
80
+ setScale(scale: Vector3): void;
81
+ setRotation(eulerXYZ: Euler): void;
82
+ setFlipAxes(flipX: number, flipY: number, flipZ: number): void;
83
+ setOrthoThickness(thickness: number): void;
84
+ setResolution(x: number, y: number): void;
85
+ setAxisClip(axis: "x" | "y" | "z", minval: number, maxval: number, _isOrthoAxis: boolean): void;
86
+ updateClipRegion(xmin: number, xmax: number, ymin: number, ymax: number, zmin: number, zmax: number): void;
87
+ }
67
88
  export interface FuseChannel {
68
89
  chIndex: number;
69
90
  lut: Uint8Array;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aics/vole-core",
3
- "version": "3.15.2",
3
+ "version": "3.15.4",
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",
@@ -78,6 +78,6 @@
78
78
  "vitest": "^3.0.8",
79
79
  "webpack": "^5.69.1",
80
80
  "webpack-cli": "^4.9.2",
81
- "webpack-dev-server": "^4.7.4"
81
+ "webpack-dev-server": "^5.2.1"
82
82
  }
83
83
  }