@aics/vole-core 3.12.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.
- package/LICENSE.txt +26 -0
- package/README.md +119 -0
- package/es/Atlas2DSlice.js +224 -0
- package/es/Channel.js +264 -0
- package/es/FileSaver.js +31 -0
- package/es/FusedChannelData.js +192 -0
- package/es/Histogram.js +250 -0
- package/es/ImageInfo.js +127 -0
- package/es/Light.js +74 -0
- package/es/Lut.js +500 -0
- package/es/MarchingCubes.js +507 -0
- package/es/MeshVolume.js +334 -0
- package/es/NaiveSurfaceNets.js +251 -0
- package/es/PathTracedVolume.js +482 -0
- package/es/RayMarchedAtlasVolume.js +250 -0
- package/es/RenderToBuffer.js +31 -0
- package/es/ThreeJsPanel.js +633 -0
- package/es/Timing.js +28 -0
- package/es/TrackballControls.js +538 -0
- package/es/View3d.js +848 -0
- package/es/Volume.js +352 -0
- package/es/VolumeCache.js +161 -0
- package/es/VolumeDims.js +16 -0
- package/es/VolumeDrawable.js +702 -0
- package/es/VolumeMaker.js +101 -0
- package/es/VolumeRenderImpl.js +1 -0
- package/es/VolumeRenderSettings.js +203 -0
- package/es/constants/basicShaders.js +29 -0
- package/es/constants/colors.js +59 -0
- package/es/constants/denoiseShader.js +43 -0
- package/es/constants/lights.js +42 -0
- package/es/constants/materials.js +85 -0
- package/es/constants/pathtraceOutputShader.js +13 -0
- package/es/constants/scaleBarSVG.js +21 -0
- package/es/constants/time.js +34 -0
- package/es/constants/volumePTshader.js +153 -0
- package/es/constants/volumeRayMarchShader.js +123 -0
- package/es/constants/volumeSliceShader.js +115 -0
- package/es/index.js +21 -0
- package/es/loaders/IVolumeLoader.js +131 -0
- package/es/loaders/JsonImageInfoLoader.js +255 -0
- package/es/loaders/OmeZarrLoader.js +495 -0
- package/es/loaders/OpenCellLoader.js +65 -0
- package/es/loaders/RawArrayLoader.js +89 -0
- package/es/loaders/TiffLoader.js +219 -0
- package/es/loaders/VolumeLoadError.js +44 -0
- package/es/loaders/VolumeLoaderUtils.js +221 -0
- package/es/loaders/index.js +40 -0
- package/es/loaders/zarr_utils/ChunkPrefetchIterator.js +143 -0
- package/es/loaders/zarr_utils/WrappedStore.js +51 -0
- package/es/loaders/zarr_utils/types.js +24 -0
- package/es/loaders/zarr_utils/utils.js +225 -0
- package/es/loaders/zarr_utils/validation.js +49 -0
- package/es/test/ChunkPrefetchIterator.test.js +208 -0
- package/es/test/RequestQueue.test.js +442 -0
- package/es/test/SubscribableRequestQueue.test.js +244 -0
- package/es/test/VolumeCache.test.js +118 -0
- package/es/test/VolumeRenderSettings.test.js +71 -0
- package/es/test/lut.test.js +671 -0
- package/es/test/num_utils.test.js +140 -0
- package/es/test/volume.test.js +98 -0
- package/es/test/zarr_utils.test.js +358 -0
- package/es/types/Atlas2DSlice.d.ts +41 -0
- package/es/types/Channel.d.ts +44 -0
- package/es/types/FileSaver.d.ts +6 -0
- package/es/types/FusedChannelData.d.ts +26 -0
- package/es/types/Histogram.d.ts +57 -0
- package/es/types/ImageInfo.d.ts +87 -0
- package/es/types/Light.d.ts +27 -0
- package/es/types/Lut.d.ts +67 -0
- package/es/types/MarchingCubes.d.ts +53 -0
- package/es/types/MeshVolume.d.ts +40 -0
- package/es/types/NaiveSurfaceNets.d.ts +11 -0
- package/es/types/PathTracedVolume.d.ts +65 -0
- package/es/types/RayMarchedAtlasVolume.d.ts +41 -0
- package/es/types/RenderToBuffer.d.ts +17 -0
- package/es/types/ThreeJsPanel.d.ts +107 -0
- package/es/types/Timing.d.ts +11 -0
- package/es/types/TrackballControls.d.ts +51 -0
- package/es/types/View3d.d.ts +357 -0
- package/es/types/Volume.d.ts +152 -0
- package/es/types/VolumeCache.d.ts +43 -0
- package/es/types/VolumeDims.d.ts +28 -0
- package/es/types/VolumeDrawable.d.ts +108 -0
- package/es/types/VolumeMaker.d.ts +49 -0
- package/es/types/VolumeRenderImpl.d.ts +22 -0
- package/es/types/VolumeRenderSettings.d.ts +98 -0
- package/es/types/constants/basicShaders.d.ts +4 -0
- package/es/types/constants/colors.d.ts +2 -0
- package/es/types/constants/denoiseShader.d.ts +40 -0
- package/es/types/constants/lights.d.ts +38 -0
- package/es/types/constants/materials.d.ts +20 -0
- package/es/types/constants/pathtraceOutputShader.d.ts +11 -0
- package/es/types/constants/scaleBarSVG.d.ts +2 -0
- package/es/types/constants/time.d.ts +19 -0
- package/es/types/constants/volumePTshader.d.ts +137 -0
- package/es/types/constants/volumeRayMarchShader.d.ts +117 -0
- package/es/types/constants/volumeSliceShader.d.ts +109 -0
- package/es/types/glsl.d.js +0 -0
- package/es/types/index.d.ts +28 -0
- package/es/types/loaders/IVolumeLoader.d.ts +113 -0
- package/es/types/loaders/JsonImageInfoLoader.d.ts +80 -0
- package/es/types/loaders/OmeZarrLoader.d.ts +87 -0
- package/es/types/loaders/OpenCellLoader.d.ts +9 -0
- package/es/types/loaders/RawArrayLoader.d.ts +33 -0
- package/es/types/loaders/TiffLoader.d.ts +45 -0
- package/es/types/loaders/VolumeLoadError.d.ts +18 -0
- package/es/types/loaders/VolumeLoaderUtils.d.ts +38 -0
- package/es/types/loaders/index.d.ts +22 -0
- package/es/types/loaders/zarr_utils/ChunkPrefetchIterator.d.ts +22 -0
- package/es/types/loaders/zarr_utils/WrappedStore.d.ts +24 -0
- package/es/types/loaders/zarr_utils/types.d.ts +94 -0
- package/es/types/loaders/zarr_utils/utils.d.ts +23 -0
- package/es/types/loaders/zarr_utils/validation.d.ts +7 -0
- package/es/types/test/ChunkPrefetchIterator.test.d.ts +1 -0
- package/es/types/test/RequestQueue.test.d.ts +1 -0
- package/es/types/test/SubscribableRequestQueue.test.d.ts +1 -0
- package/es/types/test/VolumeCache.test.d.ts +1 -0
- package/es/types/test/VolumeRenderSettings.test.d.ts +1 -0
- package/es/types/test/lut.test.d.ts +1 -0
- package/es/types/test/num_utils.test.d.ts +1 -0
- package/es/types/test/volume.test.d.ts +1 -0
- package/es/types/test/zarr_utils.test.d.ts +1 -0
- package/es/types/types.d.ts +115 -0
- package/es/types/utils/RequestQueue.d.ts +112 -0
- package/es/types/utils/SubscribableRequestQueue.d.ts +52 -0
- package/es/types/utils/num_utils.d.ts +43 -0
- package/es/types/workers/VolumeLoaderContext.d.ts +106 -0
- package/es/types/workers/types.d.ts +101 -0
- package/es/types/workers/util.d.ts +3 -0
- package/es/types.js +75 -0
- package/es/typings.d.js +0 -0
- package/es/utils/RequestQueue.js +267 -0
- package/es/utils/SubscribableRequestQueue.js +187 -0
- package/es/utils/num_utils.js +231 -0
- package/es/workers/FetchTiffWorker.js +153 -0
- package/es/workers/VolumeLoadWorker.js +129 -0
- package/es/workers/VolumeLoaderContext.js +271 -0
- package/es/workers/types.js +41 -0
- package/es/workers/util.js +8 -0
- package/package.json +83 -0
package/es/MeshVolume.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { Object3D, Vector3, Color, Mesh, Group, Material, MeshPhongMaterial, Plane, DoubleSide } from "three";
|
|
2
|
+
import { STLExporter } from "three/examples/jsm/exporters/STLExporter.js";
|
|
3
|
+
import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
|
|
4
|
+
import { defaultMaterialSettings } from "./constants/materials.js";
|
|
5
|
+
import FileSaver from "./FileSaver.js";
|
|
6
|
+
import NaiveSurfaceNets from "./NaiveSurfaceNets.js";
|
|
7
|
+
import MarchingCubes from "./MarchingCubes.js";
|
|
8
|
+
import { MESH_LAYER } from "./ThreeJsPanel.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* this cutoff is chosen to have a small buffer of values before the object is treated
|
|
12
|
+
* as transparent for gpu blending and depth testing.
|
|
13
|
+
*/
|
|
14
|
+
const ALPHA_THRESHOLD = 0.95;
|
|
15
|
+
export default class MeshVolume {
|
|
16
|
+
constructor(volume) {
|
|
17
|
+
// need?
|
|
18
|
+
this.volume = volume;
|
|
19
|
+
this.meshRoot = new Object3D(); //create an empty container
|
|
20
|
+
this.meshRoot.name = "Mesh Surface Group";
|
|
21
|
+
|
|
22
|
+
// handle transform ordering for giving the meshroot a rotation about a pivot point
|
|
23
|
+
this.meshPivot = new Group();
|
|
24
|
+
this.meshPivot.name = "MeshContainerNode";
|
|
25
|
+
this.meshPivot.add(this.meshRoot);
|
|
26
|
+
this.meshrep = [];
|
|
27
|
+
this.channelColors = [];
|
|
28
|
+
this.channelOpacities = [];
|
|
29
|
+
this.scale = new Vector3(1, 1, 1);
|
|
30
|
+
this.bounds = {
|
|
31
|
+
bmin: new Vector3(-0.5, -0.5, -0.5),
|
|
32
|
+
bmax: new Vector3(0.5, 0.5, 0.5)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
cleanup() {
|
|
36
|
+
for (let i = 0; i < this.volume.imageInfo.numChannels; ++i) {
|
|
37
|
+
this.destroyIsosurface(i);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
setVisible(isVisible) {
|
|
41
|
+
this.meshRoot.visible = isVisible;
|
|
42
|
+
}
|
|
43
|
+
doRender() {
|
|
44
|
+
// no op
|
|
45
|
+
}
|
|
46
|
+
get3dObject() {
|
|
47
|
+
return this.meshPivot;
|
|
48
|
+
}
|
|
49
|
+
setScale(scale, position = new Vector3(0, 0, 0)) {
|
|
50
|
+
this.scale = scale;
|
|
51
|
+
this.meshRoot.scale.copy(scale).multiplyScalar(0.5);
|
|
52
|
+
this.meshRoot.position.copy(position);
|
|
53
|
+
}
|
|
54
|
+
setFlipAxes(flipX, flipY, flipZ) {
|
|
55
|
+
this.meshRoot.scale.copy(new Vector3(0.5 * this.scale.x * flipX, 0.5 * this.scale.y * flipY, 0.5 * this.scale.z * flipZ));
|
|
56
|
+
}
|
|
57
|
+
setTranslation(vec3xyz) {
|
|
58
|
+
this.meshPivot.position.copy(vec3xyz);
|
|
59
|
+
}
|
|
60
|
+
setRotation(eulerXYZ) {
|
|
61
|
+
this.meshPivot.rotation.copy(eulerXYZ);
|
|
62
|
+
this.updateClipFromBounds();
|
|
63
|
+
}
|
|
64
|
+
setResolution(_x, _y) {
|
|
65
|
+
// no op
|
|
66
|
+
}
|
|
67
|
+
setOrthoThickness(_value) {
|
|
68
|
+
// no op
|
|
69
|
+
}
|
|
70
|
+
setAxisClip(axis, minval, maxval, _isOrthoAxis) {
|
|
71
|
+
this.bounds.bmax[axis] = maxval;
|
|
72
|
+
this.bounds.bmin[axis] = minval;
|
|
73
|
+
this.updateClipFromBounds();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//////////////////////////////
|
|
77
|
+
|
|
78
|
+
updateMeshColors(channelColors) {
|
|
79
|
+
// stash values here for later changes
|
|
80
|
+
this.channelColors = channelColors;
|
|
81
|
+
|
|
82
|
+
// update existing meshes
|
|
83
|
+
for (let i = 0; i < this.volume.imageInfo.numChannels; ++i) {
|
|
84
|
+
const meshrep = this.meshrep[i];
|
|
85
|
+
if (meshrep) {
|
|
86
|
+
const rgb = channelColors[i];
|
|
87
|
+
const c = rgb[0] << 16 | rgb[1] << 8 | rgb[2];
|
|
88
|
+
meshrep.traverse(function (child) {
|
|
89
|
+
if (child instanceof Mesh) {
|
|
90
|
+
child.material.color = new Color(c);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
createMaterialForChannel(rgb, alpha, _transp) {
|
|
97
|
+
const col = rgb[0] << 16 | rgb[1] << 8 | rgb[2];
|
|
98
|
+
const material = new MeshPhongMaterial({
|
|
99
|
+
color: new Color(col),
|
|
100
|
+
shininess: defaultMaterialSettings.shininess,
|
|
101
|
+
specular: new Color(defaultMaterialSettings.specularColor),
|
|
102
|
+
opacity: alpha,
|
|
103
|
+
transparent: alpha < ALPHA_THRESHOLD,
|
|
104
|
+
side: DoubleSide
|
|
105
|
+
});
|
|
106
|
+
return material;
|
|
107
|
+
}
|
|
108
|
+
createMeshForChannel(channelIndex, colorrgb, isovalue, alpha, transp) {
|
|
109
|
+
// note that if isovalue out of range, this will return an empty array.
|
|
110
|
+
const geometries = this.generateIsosurfaceGeometry(channelIndex, isovalue);
|
|
111
|
+
const material = this.createMaterialForChannel(colorrgb, alpha, transp);
|
|
112
|
+
const theObject = new Group();
|
|
113
|
+
theObject.name = "Channel" + channelIndex;
|
|
114
|
+
theObject.userData = {
|
|
115
|
+
isovalue: isovalue
|
|
116
|
+
};
|
|
117
|
+
// proper scaling will be done in parent object
|
|
118
|
+
for (let i = 0; i < geometries.length; ++i) {
|
|
119
|
+
const mesh = new Mesh(geometries[i], material);
|
|
120
|
+
mesh.layers.set(MESH_LAYER);
|
|
121
|
+
theObject.add(mesh);
|
|
122
|
+
}
|
|
123
|
+
return theObject;
|
|
124
|
+
}
|
|
125
|
+
updateIsovalue(channel, value) {
|
|
126
|
+
const meshrep = this.meshrep[channel];
|
|
127
|
+
if (!meshrep) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (meshrep.userData.isovalue === value) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// find the current isosurface opacity and color.
|
|
135
|
+
const opacity = this.channelOpacities[channel];
|
|
136
|
+
const color = this.channelColors[channel];
|
|
137
|
+
this.destroyIsosurface(channel);
|
|
138
|
+
const newmeshrep = this.createMeshForChannel(channel, color, value, opacity, false);
|
|
139
|
+
this.meshrep[channel] = newmeshrep;
|
|
140
|
+
this.meshRoot.add(newmeshrep);
|
|
141
|
+
}
|
|
142
|
+
getIsovalue(channel) {
|
|
143
|
+
const meshrep = this.meshrep[channel];
|
|
144
|
+
return meshrep?.userData.isovalue;
|
|
145
|
+
}
|
|
146
|
+
getOpacity(channel) {
|
|
147
|
+
const meshrep = this.meshrep[channel];
|
|
148
|
+
let opacity = undefined;
|
|
149
|
+
meshrep?.traverse(obj => {
|
|
150
|
+
if (obj instanceof Mesh) {
|
|
151
|
+
opacity = obj.material.opacity;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return opacity;
|
|
155
|
+
}
|
|
156
|
+
updateClipRegion(xmin, xmax, ymin, ymax, zmin, zmax) {
|
|
157
|
+
// incoming values expected to be between 0 and 1.
|
|
158
|
+
// I shift them here to be between -0.5 and 0.5
|
|
159
|
+
this.bounds = {
|
|
160
|
+
bmin: new Vector3(xmin - 0.5, ymin - 0.5, zmin - 0.5),
|
|
161
|
+
bmax: new Vector3(xmax - 0.5, ymax - 0.5, zmax - 0.5)
|
|
162
|
+
};
|
|
163
|
+
this.updateClipFromBounds();
|
|
164
|
+
}
|
|
165
|
+
updateClipFromBounds() {
|
|
166
|
+
const xmin = this.bounds.bmin.x;
|
|
167
|
+
const ymin = this.bounds.bmin.y;
|
|
168
|
+
const zmin = this.bounds.bmin.z;
|
|
169
|
+
const xmax = this.bounds.bmax.x;
|
|
170
|
+
const ymax = this.bounds.bmax.y;
|
|
171
|
+
const zmax = this.bounds.bmax.z;
|
|
172
|
+
const euler = this.meshPivot.rotation;
|
|
173
|
+
for (let channel = 0; channel < this.meshrep.length; ++channel) {
|
|
174
|
+
const meshrep = this.meshrep[channel];
|
|
175
|
+
if (!meshrep) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const planes = [];
|
|
179
|
+
// up to 6 planes.
|
|
180
|
+
if (xmin > -0.5) {
|
|
181
|
+
planes.push(new Plane(new Vector3(1, 0, 0).applyEuler(euler), this.meshRoot.position.x + -xmin * this.scale.x));
|
|
182
|
+
}
|
|
183
|
+
if (ymin > -0.5) {
|
|
184
|
+
planes.push(new Plane(new Vector3(0, 1, 0).applyEuler(euler), this.meshRoot.position.y + -ymin * this.scale.y));
|
|
185
|
+
}
|
|
186
|
+
if (zmin > -0.5) {
|
|
187
|
+
planes.push(new Plane(new Vector3(0, 0, 1).applyEuler(euler), this.meshRoot.position.z + -zmin * this.scale.z));
|
|
188
|
+
}
|
|
189
|
+
if (xmax < 0.5) {
|
|
190
|
+
planes.push(new Plane(new Vector3(-1, 0, 0).applyEuler(euler), this.meshRoot.position.x + xmax * this.scale.x));
|
|
191
|
+
}
|
|
192
|
+
if (ymax < 0.5) {
|
|
193
|
+
planes.push(new Plane(new Vector3(0, -1, 0).applyEuler(euler), this.meshRoot.position.y + ymax * this.scale.y));
|
|
194
|
+
}
|
|
195
|
+
if (zmax < 0.5) {
|
|
196
|
+
planes.push(new Plane(new Vector3(0, 0, -1).applyEuler(euler), this.meshRoot.position.z + zmax * this.scale.z));
|
|
197
|
+
}
|
|
198
|
+
meshrep.traverse(function (child) {
|
|
199
|
+
if (child instanceof Mesh) {
|
|
200
|
+
child.material.clippingPlanes = planes;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
updateOpacity(channel, value) {
|
|
206
|
+
this.channelOpacities[channel] = value;
|
|
207
|
+
const meshrep = this.meshrep[channel];
|
|
208
|
+
if (!meshrep) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
meshrep.traverse(function (child) {
|
|
212
|
+
if (child instanceof Mesh) {
|
|
213
|
+
child.material.opacity = value;
|
|
214
|
+
child.material.transparent = value < ALPHA_THRESHOLD;
|
|
215
|
+
//child.material.depthWrite = !child.material.transparent;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
hasIsosurface(channel) {
|
|
220
|
+
return !!this.meshrep[channel];
|
|
221
|
+
}
|
|
222
|
+
createIsosurface(channel, color,
|
|
223
|
+
// 127 is half of the intensity range 0..255
|
|
224
|
+
value = 127,
|
|
225
|
+
// 1.0 indicates full opacity, non-transparent
|
|
226
|
+
alpha = 1.0, transp = alpha < ALPHA_THRESHOLD) {
|
|
227
|
+
if (!this.meshrep[channel]) {
|
|
228
|
+
const meshrep = this.createMeshForChannel(channel, color, value, alpha, transp);
|
|
229
|
+
this.meshrep[channel] = meshrep;
|
|
230
|
+
this.channelOpacities[channel] = alpha;
|
|
231
|
+
this.channelColors[channel] = color;
|
|
232
|
+
// note we are not removing any prior mesh reps for this channel
|
|
233
|
+
this.meshRoot.add(meshrep);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
destroyIsosurface(channel) {
|
|
237
|
+
const meshrep = this.meshrep[channel];
|
|
238
|
+
if (meshrep) {
|
|
239
|
+
this.meshRoot.remove(meshrep);
|
|
240
|
+
meshrep.traverse(function (child) {
|
|
241
|
+
if (child instanceof Mesh) {
|
|
242
|
+
child.material.dispose();
|
|
243
|
+
child.geometry.dispose();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
this.meshrep[channel] = null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
saveChannelIsosurface(channelIndex, type, namePrefix) {
|
|
250
|
+
const meshrep = this.meshrep[channelIndex];
|
|
251
|
+
if (!meshrep) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (type === "STL") {
|
|
255
|
+
this.exportSTL(meshrep, namePrefix + "_" + this.volume.channelNames[channelIndex]);
|
|
256
|
+
} else if (type === "GLTF") {
|
|
257
|
+
// temporarily set other meshreps to invisible
|
|
258
|
+
const prevviz = [];
|
|
259
|
+
for (let i = 0; i < this.meshrep.length; ++i) {
|
|
260
|
+
const meshrepi = this.meshrep[i];
|
|
261
|
+
if (meshrepi) {
|
|
262
|
+
prevviz[i] = meshrepi.visible;
|
|
263
|
+
meshrepi.visible = i === channelIndex;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
this.exportGLTF(this.meshRoot, namePrefix + "_" + this.volume.channelNames[channelIndex]);
|
|
267
|
+
for (let i = 0; i < this.meshrep.length; ++i) {
|
|
268
|
+
const meshrepi = this.meshrep[i];
|
|
269
|
+
if (meshrepi) {
|
|
270
|
+
meshrepi.visible = prevviz[i];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
exportSTL(input, fname) {
|
|
276
|
+
const ex = new STLExporter();
|
|
277
|
+
const output = ex.parse(input, {
|
|
278
|
+
binary: true
|
|
279
|
+
});
|
|
280
|
+
// STLExporter's typing shows that it returns string
|
|
281
|
+
// but this is not the case when binary=true.
|
|
282
|
+
FileSaver.saveBinary(output.buffer, fname + ".stl");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// takes a scene or object or array of scenes or objects or both!
|
|
286
|
+
exportGLTF(input, fname) {
|
|
287
|
+
const gltfExporter = new GLTFExporter();
|
|
288
|
+
const options = {
|
|
289
|
+
// transforms as translate rotate scale?
|
|
290
|
+
trs: false,
|
|
291
|
+
onlyVisible: true,
|
|
292
|
+
truncateDrawRange: true,
|
|
293
|
+
binary: true,
|
|
294
|
+
forceIndices: false,
|
|
295
|
+
forcePowerOfTwoTextures: true
|
|
296
|
+
};
|
|
297
|
+
gltfExporter.parse(input, function (result) {
|
|
298
|
+
if (result instanceof ArrayBuffer) {
|
|
299
|
+
FileSaver.saveArrayBuffer(result, fname + ".glb");
|
|
300
|
+
} else {
|
|
301
|
+
const output = JSON.stringify(result, null, 2);
|
|
302
|
+
FileSaver.saveString(output, fname + ".gltf");
|
|
303
|
+
}
|
|
304
|
+
}, function (error) {
|
|
305
|
+
console.error(error);
|
|
306
|
+
}, options);
|
|
307
|
+
}
|
|
308
|
+
generateIsosurfaceGeometry(channelIndex, isovalue) {
|
|
309
|
+
if (!this.volume) {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
const volumeData = this.volume.channels[channelIndex].volumeData;
|
|
313
|
+
const marchingcubes = true;
|
|
314
|
+
const regionSizeArr = this.volume.imageInfo.subregionSize.toArray();
|
|
315
|
+
if (marchingcubes) {
|
|
316
|
+
const effect = new MarchingCubes(regionSizeArr, new Material(), false, false, true, volumeData);
|
|
317
|
+
effect.position.copy(this.meshRoot.position);
|
|
318
|
+
effect.scale.set(0.5 * this.scale.x, 0.5 * this.scale.y, 0.5 * this.scale.z);
|
|
319
|
+
effect.isovalue = isovalue;
|
|
320
|
+
const geometries = effect.generateGeometry();
|
|
321
|
+
// TODO: weld vertices and recompute normals if MarchingCubes results in excessive coincident verts
|
|
322
|
+
// for (var i = 0; i < geometries.length; ++i) {
|
|
323
|
+
// var g = new THREE.Geometry().fromBufferGeometry(geometries[i]);
|
|
324
|
+
// g.mergeVertices();
|
|
325
|
+
// geometries[i] = new THREE.BufferGeometry().fromGeometry(g);
|
|
326
|
+
// geometries[i].computeVertexNormals();
|
|
327
|
+
// }
|
|
328
|
+
return geometries || [];
|
|
329
|
+
} else {
|
|
330
|
+
const result = NaiveSurfaceNets.surfaceNets(volumeData, regionSizeArr, isovalue);
|
|
331
|
+
return NaiveSurfaceNets.constructTHREEGeometry(result);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// The MIT License (MIT)
|
|
2
|
+
//
|
|
3
|
+
// Copyright (c) 2012-2013 Mikola Lysenko
|
|
4
|
+
//
|
|
5
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
// in the Software without restriction, including without limitation the rights
|
|
8
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
// furnished to do so, subject to the following conditions:
|
|
11
|
+
//
|
|
12
|
+
// The above copyright notice and this permission notice shall be included in
|
|
13
|
+
// all copies or substantial portions of the Software.
|
|
14
|
+
//
|
|
15
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
// THE SOFTWARE.
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* SurfaceNets in JavaScript
|
|
25
|
+
*
|
|
26
|
+
* Written by Mikola Lysenko (C) 2012
|
|
27
|
+
*
|
|
28
|
+
* MIT License
|
|
29
|
+
*
|
|
30
|
+
* Based on: S.F. Gibson, "Constrained Elastic Surface Nets". (1998) MERL Tech Report.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// MODIFIED 2018 BY DANIELT@ALLENINSTITUTE.ORG TO ACCEPT AN ISOVALUE AND RESCALE VERTEX POSITIONS
|
|
34
|
+
import { BufferAttribute, BufferGeometry } from "three";
|
|
35
|
+
var SurfaceNets = function () {
|
|
36
|
+
"use strict";
|
|
37
|
+
|
|
38
|
+
//Precompute edge table, like Paul Bourke does.
|
|
39
|
+
// This saves a bit of time when computing the centroid of each boundary cell
|
|
40
|
+
var cube_edges = new Int32Array(24),
|
|
41
|
+
edge_table = new Int32Array(256);
|
|
42
|
+
(function () {
|
|
43
|
+
//Initialize the cube_edges table
|
|
44
|
+
// This is just the vertex number of each cube
|
|
45
|
+
var k = 0;
|
|
46
|
+
for (var i = 0; i < 8; ++i) {
|
|
47
|
+
for (var j = 1; j <= 4; j <<= 1) {
|
|
48
|
+
var p = i ^ j;
|
|
49
|
+
if (i <= p) {
|
|
50
|
+
cube_edges[k++] = i;
|
|
51
|
+
cube_edges[k++] = p;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//Initialize the intersection table.
|
|
57
|
+
// This is a 2^(cube configuration) -> 2^(edge configuration) map
|
|
58
|
+
// There is one entry for each possible cube configuration, and the output is a 12-bit vector enumerating all edges crossing the 0-level.
|
|
59
|
+
for (var i = 0; i < 256; ++i) {
|
|
60
|
+
var em = 0;
|
|
61
|
+
for (var j = 0; j < 24; j += 2) {
|
|
62
|
+
var a = !!(i & 1 << cube_edges[j]),
|
|
63
|
+
b = !!(i & 1 << cube_edges[j + 1]);
|
|
64
|
+
em |= a !== b ? 1 << (j >> 1) : 0;
|
|
65
|
+
}
|
|
66
|
+
edge_table[i] = em;
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
|
|
70
|
+
//Internal buffer, this may get resized at run time
|
|
71
|
+
var buffer = new Int32Array(4096);
|
|
72
|
+
return function (data, dims, isovalue) {
|
|
73
|
+
var vertices = [],
|
|
74
|
+
faces = [],
|
|
75
|
+
n = 0,
|
|
76
|
+
x = new Int32Array(3),
|
|
77
|
+
R = new Int32Array([1, dims[0] + 1, (dims[0] + 1) * (dims[1] + 1)]),
|
|
78
|
+
grid = new Float32Array(8),
|
|
79
|
+
buf_no = 1;
|
|
80
|
+
|
|
81
|
+
//Resize buffer if necessary
|
|
82
|
+
if (R[2] * 2 > buffer.length) {
|
|
83
|
+
buffer = new Int32Array(R[2] * 2);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//March over the voxel grid
|
|
87
|
+
for (x[2] = 0; x[2] < dims[2] - 1; ++x[2], n += dims[0], buf_no ^= 1, R[2] = -R[2]) {
|
|
88
|
+
//m is the pointer into the buffer we are going to use.
|
|
89
|
+
//This is slightly obtuse because javascript does not have good support for packed data structures, so we must use typed arrays :(
|
|
90
|
+
//The contents of the buffer will be the indices of the vertices on the previous x/y slice of the volume
|
|
91
|
+
var m = 1 + (dims[0] + 1) * (1 + buf_no * (dims[1] + 1));
|
|
92
|
+
for (x[1] = 0; x[1] < dims[1] - 1; ++x[1], ++n, m += 2) for (x[0] = 0; x[0] < dims[0] - 1; ++x[0], ++n, ++m) {
|
|
93
|
+
//Read in 8 field values around this vertex and store them in an array
|
|
94
|
+
//Also calculate 8-bit mask, like in marching cubes, so we can speed up sign checks later
|
|
95
|
+
var mask = 0,
|
|
96
|
+
g = 0,
|
|
97
|
+
idx = n;
|
|
98
|
+
for (var k = 0; k < 2; ++k, idx += dims[0] * (dims[1] - 2)) for (var j = 0; j < 2; ++j, idx += dims[0] - 2) for (var i = 0; i < 2; ++i, ++g, ++idx) {
|
|
99
|
+
var p = data[idx] - isovalue;
|
|
100
|
+
grid[g] = p;
|
|
101
|
+
mask |= p < 0 ? 1 << g : 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//Check for early termination if cell does not intersect boundary
|
|
105
|
+
if (mask === 0 || mask === 0xff) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//Sum up edge intersections
|
|
110
|
+
var edge_mask = edge_table[mask],
|
|
111
|
+
v = [0.0, 0.0, 0.0],
|
|
112
|
+
e_count = 0;
|
|
113
|
+
|
|
114
|
+
//For every edge of the cube...
|
|
115
|
+
for (var i = 0; i < 12; ++i) {
|
|
116
|
+
//Use edge mask to check if it is crossed
|
|
117
|
+
if (!(edge_mask & 1 << i)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//If it did, increment number of edge crossings
|
|
122
|
+
++e_count;
|
|
123
|
+
|
|
124
|
+
//Now find the point of intersection
|
|
125
|
+
var e0 = cube_edges[i << 1],
|
|
126
|
+
//Unpack vertices
|
|
127
|
+
e1 = cube_edges[(i << 1) + 1],
|
|
128
|
+
g0 = grid[e0],
|
|
129
|
+
//Unpack grid values
|
|
130
|
+
g1 = grid[e1],
|
|
131
|
+
t = g0 - g1; //Compute point of intersection
|
|
132
|
+
if (Math.abs(t) > 1e-6) {
|
|
133
|
+
t = g0 / t;
|
|
134
|
+
} else {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
//Interpolate vertices and add up intersections (this can be done without multiplying)
|
|
139
|
+
for (var j = 0, k = 1; j < 3; ++j, k <<= 1) {
|
|
140
|
+
var a = e0 & k,
|
|
141
|
+
b = e1 & k;
|
|
142
|
+
if (a !== b) {
|
|
143
|
+
v[j] += a ? 1.0 - t : t;
|
|
144
|
+
} else {
|
|
145
|
+
v[j] += a ? 1.0 : 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//Now we just average the edge intersections and add them to coordinate
|
|
151
|
+
var s = 1.0 / e_count;
|
|
152
|
+
for (var i = 0; i < 3; ++i) {
|
|
153
|
+
v[i] = x[i] + s * v[i];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
//Add vertex to buffer, store pointer to vertex index in buffer
|
|
157
|
+
buffer[m] = vertices.length;
|
|
158
|
+
vertices.push([2.0 * v[0] / dims[0] - 1.0, 2.0 * v[1] / dims[1] - 1.0, 2.0 * v[2] / dims[2] - 1.0]);
|
|
159
|
+
|
|
160
|
+
//Now we need to add faces together, to do this we just loop over 3 basis components
|
|
161
|
+
for (var i = 0; i < 3; ++i) {
|
|
162
|
+
//The first three entries of the edge_mask count the crossings along the edge
|
|
163
|
+
if (!(edge_mask & 1 << i)) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// i = axes we are point along. iu, iv = orthogonal axes
|
|
168
|
+
var iu = (i + 1) % 3,
|
|
169
|
+
iv = (i + 2) % 3;
|
|
170
|
+
|
|
171
|
+
//If we are on a boundary, skip it
|
|
172
|
+
if (x[iu] === 0 || x[iv] === 0) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
//Otherwise, look up adjacent edges in buffer
|
|
177
|
+
var du = R[iu],
|
|
178
|
+
dv = R[iv];
|
|
179
|
+
|
|
180
|
+
//Remember to flip orientation depending on the sign of the corner.
|
|
181
|
+
if (mask & 1) {
|
|
182
|
+
faces.push([buffer[m], buffer[m - du], buffer[m - du - dv], buffer[m - dv]]);
|
|
183
|
+
} else {
|
|
184
|
+
faces.push([buffer[m], buffer[m - dv], buffer[m - du - dv], buffer[m - du]]);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//All done! Return the result
|
|
191
|
+
return {
|
|
192
|
+
vertices: vertices,
|
|
193
|
+
faces: faces
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
}();
|
|
197
|
+
|
|
198
|
+
// returns array of indexed BufferGeometry
|
|
199
|
+
function ConstructTHREEGeometry(surfaceNetResult) {
|
|
200
|
+
var result = surfaceNetResult;
|
|
201
|
+
var varray = new Float32Array(result.vertices.length * 3);
|
|
202
|
+
for (var i = 0; i < result.vertices.length; ++i) {
|
|
203
|
+
var v = result.vertices[i];
|
|
204
|
+
varray[i * 3 + 0] = v[0];
|
|
205
|
+
varray[i * 3 + 1] = v[1];
|
|
206
|
+
varray[i * 3 + 2] = v[2];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// count triangles; split quad faces
|
|
210
|
+
let n_triangles = 0;
|
|
211
|
+
for (var i = 0; i < result.faces.length; ++i) {
|
|
212
|
+
var f = result.faces[i];
|
|
213
|
+
if (f.length === 3) {
|
|
214
|
+
n_triangles += 1;
|
|
215
|
+
} else if (f.length === 4) {
|
|
216
|
+
n_triangles += 2;
|
|
217
|
+
} else {
|
|
218
|
+
//Polygon needs to be subdivided...
|
|
219
|
+
console.log("unhandled poly with " + f.length + " vertices");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
var inds = new Uint32Array(n_triangles * 3);
|
|
223
|
+
let j = 0;
|
|
224
|
+
for (var i = 0; i < result.faces.length; ++i) {
|
|
225
|
+
var f = result.faces[i];
|
|
226
|
+
// note what appears to be inverted winding order.
|
|
227
|
+
// I believe this is related to isosurface < or > testing but have not checked.
|
|
228
|
+
if (f.length === 3) {
|
|
229
|
+
inds[j++] = f[2];
|
|
230
|
+
inds[j++] = f[1];
|
|
231
|
+
inds[j++] = f[0];
|
|
232
|
+
} else if (f.length === 4) {
|
|
233
|
+
// two triangles: (2,1,0) and (3,2,0)
|
|
234
|
+
inds[j++] = f[2];
|
|
235
|
+
inds[j++] = f[1];
|
|
236
|
+
inds[j++] = f[0];
|
|
237
|
+
inds[j++] = f[3];
|
|
238
|
+
inds[j++] = f[2];
|
|
239
|
+
inds[j++] = f[0];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
var geo = new BufferGeometry();
|
|
243
|
+
geo.setAttribute("position", new BufferAttribute(varray, 3, false));
|
|
244
|
+
geo.setIndex(new BufferAttribute(inds, 1));
|
|
245
|
+
geo.computeVertexNormals();
|
|
246
|
+
return [geo];
|
|
247
|
+
}
|
|
248
|
+
export default {
|
|
249
|
+
surfaceNets: SurfaceNets,
|
|
250
|
+
constructTHREEGeometry: ConstructTHREEGeometry
|
|
251
|
+
};
|