@niivue/niivue 0.22.2 → 0.23.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.
- package/README.md +58 -10
- package/dist/niivue.es.js +114908 -10273
- package/dist/niivue.umd.js +406 -87
- package/package.json +5 -2
- package/src/colortables.js +73 -0
- package/src/index.html +44 -30
- package/src/indexDraw.html +119 -0
- package/src/logger.js +5 -0
- package/src/meshes.html +161 -0
- package/src/niivue-object3D.js +241 -27
- package/src/niivue-shader3D.js +6 -0
- package/src/niivue.js +1323 -575
- package/src/nvimage.js +1796 -150
- package/src/nvmesh.js +2175 -0
- package/src/shader-srcs.js +449 -4
- package/src/shader.js +11 -2
- package/src/AIL.nii.gz +0 -0
- package/src/hippo.nii.gz +0 -0
- package/src/mni152.nii.gz +0 -0
package/src/nvmesh.js
ADDED
|
@@ -0,0 +1,2175 @@
|
|
|
1
|
+
import * as gifti from "gifti-reader-js/release/current/gifti-reader";
|
|
2
|
+
import * as fflate from "fflate";
|
|
3
|
+
import { v4 as uuidv4 } from "uuid";
|
|
4
|
+
import * as cmaps from "./cmaps";
|
|
5
|
+
import { Log } from "./logger";
|
|
6
|
+
import { NiivueObject3D } from "./niivue-object3D.js"; //n.b. used by connectome
|
|
7
|
+
import { mat3, mat4, vec3, vec4 } from "gl-matrix";
|
|
8
|
+
import { colortables } from "./colortables";
|
|
9
|
+
const cmapper = new colortables();
|
|
10
|
+
const log = new Log();
|
|
11
|
+
/**
|
|
12
|
+
* @class NVMesh
|
|
13
|
+
* @type NVMesh
|
|
14
|
+
* @description
|
|
15
|
+
* a NVImage encapsulates some images data and provides methods to query and operate on images
|
|
16
|
+
* @constructor
|
|
17
|
+
* @param {array} dataBuffer an array buffer of image data to load (there are also methods that abstract this more. See loadFromUrl, and loadFromFile)
|
|
18
|
+
* @param {string} [name=''] a name for this image. Default is an empty string
|
|
19
|
+
* @param {number} [opacity=1.0] the opacity for this image. default is 1
|
|
20
|
+
* @param {boolean} [trustCalMinMax=true] whether or not to trust cal_min and cal_max from the nifti header (trusting results in faster loading)
|
|
21
|
+
* @param {number} [percentileFrac=0.02] the percentile to use for setting the robust range of the display values (smart intensity setting for images with large ranges)
|
|
22
|
+
* @param {boolean} [ignoreZeroVoxels=false] whether or not to ignore zero voxels in setting the robust range of display values
|
|
23
|
+
* @param {boolean} [visible=true] whether or not this image is to be visible
|
|
24
|
+
*/
|
|
25
|
+
export function NVMesh(
|
|
26
|
+
pts,
|
|
27
|
+
tris,
|
|
28
|
+
name = "",
|
|
29
|
+
rgba255 = [1, 0, 0, 0],
|
|
30
|
+
opacity = 1.0,
|
|
31
|
+
visible = true,
|
|
32
|
+
gl,
|
|
33
|
+
connectome = null,
|
|
34
|
+
dpg = null,
|
|
35
|
+
dps = null,
|
|
36
|
+
dpv = null
|
|
37
|
+
) {
|
|
38
|
+
this.name = name;
|
|
39
|
+
this.id = uuidv4();
|
|
40
|
+
let obj = getExtents(pts);
|
|
41
|
+
this.furthestVertexFromOrigin = obj.mxDx;
|
|
42
|
+
this.extentsMin = obj.extentsMin;
|
|
43
|
+
this.extentsMax = obj.extentsMax;
|
|
44
|
+
this.opacity = opacity > 1.0 ? 1.0 : opacity; //make sure opacity can't be initialized greater than 1 see: #107 and #117 on github
|
|
45
|
+
this.visible = visible;
|
|
46
|
+
this.indexBuffer = gl.createBuffer();
|
|
47
|
+
this.vertexBuffer = gl.createBuffer();
|
|
48
|
+
this.vao = gl.createVertexArray();
|
|
49
|
+
this.offsetPt0 = null;
|
|
50
|
+
this.hasConnectome = false;
|
|
51
|
+
this.pts = pts;
|
|
52
|
+
this.layers = [];
|
|
53
|
+
if (!rgba255) {
|
|
54
|
+
this.fiberLength = 2;
|
|
55
|
+
this.fiberDither = 0.1;
|
|
56
|
+
this.fiberColor = "Global";
|
|
57
|
+
this.fiberDecimationStride = 1; //e.g. if 2 the 50% of streamlines visible, if 3 then 1/3rd
|
|
58
|
+
this.fiberMask = []; //provide method to show/hide specific fibers
|
|
59
|
+
this.colormap = connectome;
|
|
60
|
+
this.dpg = dpg;
|
|
61
|
+
this.dps = dps;
|
|
62
|
+
this.dpv = dpv;
|
|
63
|
+
this.offsetPt0 = tris;
|
|
64
|
+
this.updateFibers(gl);
|
|
65
|
+
//define VAO
|
|
66
|
+
gl.bindVertexArray(this.vao);
|
|
67
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
|
68
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
69
|
+
//vertex position: 3 floats X,Y,Z
|
|
70
|
+
gl.enableVertexAttribArray(0);
|
|
71
|
+
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 16, 0);
|
|
72
|
+
//vertex color
|
|
73
|
+
gl.enableVertexAttribArray(1);
|
|
74
|
+
gl.vertexAttribPointer(1, 4, gl.UNSIGNED_BYTE, true, 16, 12);
|
|
75
|
+
gl.bindVertexArray(null); // https://stackoverflow.com/questions/43904396/are-we-not-allowed-to-bind-gl-array-buffer-and-vertex-attrib-array-to-0-in-webgl
|
|
76
|
+
return;
|
|
77
|
+
} //if fiber not mesh
|
|
78
|
+
if (connectome) {
|
|
79
|
+
this.hasConnectome = true;
|
|
80
|
+
var keysArray = Object.keys(connectome);
|
|
81
|
+
for (var i = 0, len = keysArray.length; i < len; i++) {
|
|
82
|
+
this[keysArray[i]] = connectome[keysArray[i]];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
this.rgba255 = rgba255;
|
|
86
|
+
this.tris = tris;
|
|
87
|
+
this.updateMesh(gl);
|
|
88
|
+
//the VAO binds the vertices and indices as well as describing the vertex layout
|
|
89
|
+
gl.bindVertexArray(this.vao);
|
|
90
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
|
91
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
92
|
+
//vertex position: 3 floats X,Y,Z
|
|
93
|
+
gl.enableVertexAttribArray(0);
|
|
94
|
+
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 28, 0);
|
|
95
|
+
//vertex surface normal vector: (also three floats)
|
|
96
|
+
gl.enableVertexAttribArray(1);
|
|
97
|
+
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 28, 12);
|
|
98
|
+
//vertex color
|
|
99
|
+
gl.enableVertexAttribArray(2);
|
|
100
|
+
gl.vertexAttribPointer(2, 4, gl.UNSIGNED_BYTE, true, 28, 24);
|
|
101
|
+
gl.bindVertexArray(null); // https://stackoverflow.com/questions/43904396/are-we-not-allowed-to-bind-gl-array-buffer-and-vertex-attrib-array-to-0-in-webgl
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
NVMesh.prototype.updateFibers = function (gl) {
|
|
105
|
+
if (!this.offsetPt0 || !this.fiberLength) return;
|
|
106
|
+
//VERTICES:
|
|
107
|
+
let pts = this.pts;
|
|
108
|
+
let offsetPt0 = this.offsetPt0;
|
|
109
|
+
let n_count = offsetPt0.length - 1;
|
|
110
|
+
let npt = pts.length / 3; //each point has three components: X,Y,Z
|
|
111
|
+
//only once: compute length of each streamline
|
|
112
|
+
if (!this.fiberLengths) {
|
|
113
|
+
this.fiberLengths = [];
|
|
114
|
+
for (let i = 0; i < n_count; i++) {
|
|
115
|
+
//for each streamline
|
|
116
|
+
let vStart3 = offsetPt0[i] * 3; //first vertex in streamline
|
|
117
|
+
let vEnd3 = (offsetPt0[i + 1] - 1) * 3; //last vertex in streamline
|
|
118
|
+
let len = 0;
|
|
119
|
+
for (let j = vStart3; j < vEnd3; j += 3) {
|
|
120
|
+
let v = vec3.fromValues(
|
|
121
|
+
pts[j + 0] - pts[j + 3],
|
|
122
|
+
pts[j + 1] - pts[j + 4],
|
|
123
|
+
pts[j + 2] - pts[j + 5]
|
|
124
|
+
);
|
|
125
|
+
len += vec3.len(v);
|
|
126
|
+
}
|
|
127
|
+
this.fiberLengths.push(len);
|
|
128
|
+
}
|
|
129
|
+
} //only once: compute length of each streamline
|
|
130
|
+
//determine fiber colors
|
|
131
|
+
//Each streamline vertex has color and position attributes
|
|
132
|
+
//Interleaved Vertex Data https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/TechniquesforWorkingwithVertexData/TechniquesforWorkingwithVertexData.html
|
|
133
|
+
var posClrF32 = new Float32Array(npt * 4); //four 32-bit components X,Y,Z,C
|
|
134
|
+
var posClrU32 = new Uint32Array(posClrF32.buffer); //typecast of our X,Y,Z,C array
|
|
135
|
+
//fill XYZ position of XYZC array
|
|
136
|
+
let i3 = 0;
|
|
137
|
+
let i4 = 0;
|
|
138
|
+
for (let i = 0; i < npt; i++) {
|
|
139
|
+
posClrF32[i4 + 0] = pts[i3 + 0];
|
|
140
|
+
posClrF32[i4 + 1] = pts[i3 + 1];
|
|
141
|
+
posClrF32[i4 + 2] = pts[i3 + 2];
|
|
142
|
+
i3 += 3;
|
|
143
|
+
i4 += 4;
|
|
144
|
+
}
|
|
145
|
+
//fill fiber Color
|
|
146
|
+
let dither = this.fiberDither;
|
|
147
|
+
let ditherHalf = dither * 0.5;
|
|
148
|
+
function direction2rgb(x1, y1, z1, x2, y2, z2, ditherFrac) {
|
|
149
|
+
//generate color based on direction between two 3D spatial positions
|
|
150
|
+
let v = vec3.fromValues(
|
|
151
|
+
Math.abs(x1 - x2),
|
|
152
|
+
Math.abs(y1 - y2),
|
|
153
|
+
Math.abs(z1 - z2)
|
|
154
|
+
);
|
|
155
|
+
vec3.normalize(v, v);
|
|
156
|
+
let r = ditherFrac - ditherHalf;
|
|
157
|
+
for (let j = 0; j < 3; j++)
|
|
158
|
+
v[j] = 255 * Math.max(Math.min(Math.abs(v[j]) + r, 1.0), 0.0);
|
|
159
|
+
return v[0] + (v[1] << 8) + (v[2] << 16);
|
|
160
|
+
} // direction2rgb()
|
|
161
|
+
//Determine color: local, global, dps0, dpv0, etc.
|
|
162
|
+
let fiberColor = this.fiberColor.toLowerCase();
|
|
163
|
+
let dps = null;
|
|
164
|
+
let dpv = null;
|
|
165
|
+
if (fiberColor.startsWith("dps") && this.dps.length > 0) {
|
|
166
|
+
let n = parseInt(fiberColor.substring(3));
|
|
167
|
+
if (n < this.dps.length && this.dps[n].vals.length === n_count)
|
|
168
|
+
dps = this.dps[n].vals;
|
|
169
|
+
}
|
|
170
|
+
if (fiberColor.startsWith("dpv") && this.dpv.length > 0) {
|
|
171
|
+
let n = parseInt(fiberColor.substring(3));
|
|
172
|
+
if (n < this.dpv.length && this.dpv[n].vals.length === npt)
|
|
173
|
+
dpv = this.dpv[n].vals;
|
|
174
|
+
}
|
|
175
|
+
if (dpv) {
|
|
176
|
+
//color per streamline
|
|
177
|
+
let lut = cmapper.colormap(this.colormap);
|
|
178
|
+
let mn = dpv[0];
|
|
179
|
+
let mx = dpv[0];
|
|
180
|
+
for (let i = 0; i < npt; i++) {
|
|
181
|
+
mn = Math.min(mn, dpv[i]);
|
|
182
|
+
mx = Math.max(mx, dpv[i]);
|
|
183
|
+
}
|
|
184
|
+
let v4 = 3; //+3: fill 4th component colors: XYZC = 0123
|
|
185
|
+
for (let i = 0; i < npt; i++) {
|
|
186
|
+
let color = (dpv[i] - mn) / (mx - mn);
|
|
187
|
+
color = Math.round(Math.max(Math.min(255, color * 255)), 1) * 4;
|
|
188
|
+
let RGBA = lut[color] + (lut[color + 1] << 8) + (lut[color + 2] << 16);
|
|
189
|
+
posClrU32[v4] = RGBA;
|
|
190
|
+
v4 += 4;
|
|
191
|
+
}
|
|
192
|
+
} else if (dps) {
|
|
193
|
+
//color per streamline
|
|
194
|
+
let lut = cmapper.colormap(this.colormap);
|
|
195
|
+
let mn = dps[0];
|
|
196
|
+
let mx = dps[0];
|
|
197
|
+
for (let i = 0; i < n_count; i++) {
|
|
198
|
+
mn = Math.min(mn, dps[i]);
|
|
199
|
+
mx = Math.max(mx, dps[i]);
|
|
200
|
+
}
|
|
201
|
+
if (mx === mn) mn -= 1; //avoid divide by zero
|
|
202
|
+
for (let i = 0; i < n_count; i++) {
|
|
203
|
+
let color = (dps[i] - mn) / (mx - mn);
|
|
204
|
+
color = Math.round(Math.max(Math.min(255, color * 255)), 1) * 4;
|
|
205
|
+
let RGBA = lut[color] + (lut[color + 1] << 8) + (lut[color + 2] << 16);
|
|
206
|
+
let vStart = offsetPt0[i]; //first vertex in streamline
|
|
207
|
+
let vEnd = offsetPt0[i + 1] - 1; //last vertex in streamline
|
|
208
|
+
let vStart4 = vStart * 4 + 3; //+3: fill 4th component colors: XYZC = 0123
|
|
209
|
+
let vEnd4 = vEnd * 4 + 3;
|
|
210
|
+
for (let j = vStart4; j <= vEnd4; j += 4) posClrU32[j] = RGBA;
|
|
211
|
+
}
|
|
212
|
+
} else if (fiberColor.includes("local")) {
|
|
213
|
+
for (let i = 0; i < n_count; i++) {
|
|
214
|
+
//for each streamline
|
|
215
|
+
let vStart = offsetPt0[i]; //first vertex in streamline
|
|
216
|
+
let vEnd = offsetPt0[i + 1] - 1; //last vertex in streamline
|
|
217
|
+
let v3 = vStart * 3; //pts have 3 components XYZ
|
|
218
|
+
let vEnd3 = vEnd * 3;
|
|
219
|
+
let ditherFrac = dither * Math.random(); //same dither amount throughout line
|
|
220
|
+
//for first point, we do not have a prior sample
|
|
221
|
+
let RGBA = direction2rgb(
|
|
222
|
+
pts[v3],
|
|
223
|
+
pts[v3 + 1],
|
|
224
|
+
pts[v3 + 2],
|
|
225
|
+
pts[v3 + 4],
|
|
226
|
+
pts[v3 + 5],
|
|
227
|
+
pts[v3 + 6],
|
|
228
|
+
ditherFrac
|
|
229
|
+
);
|
|
230
|
+
let v4 = vStart * 4 + 3; //+3: fill 4th component colors: XYZC = 0123
|
|
231
|
+
while (v3 < vEnd3) {
|
|
232
|
+
posClrU32[v4] = RGBA;
|
|
233
|
+
v4 += 4; //stride is 4 32-bit values: float32 XYZ and 32-bit rgba
|
|
234
|
+
v3 += 3; //read next vertex
|
|
235
|
+
//direction estimated based on previous and next vertex
|
|
236
|
+
RGBA = direction2rgb(
|
|
237
|
+
pts[v3 - 3],
|
|
238
|
+
pts[v3 - 2],
|
|
239
|
+
pts[v3 - 1],
|
|
240
|
+
pts[v3 + 3],
|
|
241
|
+
pts[v3 + 4],
|
|
242
|
+
pts[v3 + 5],
|
|
243
|
+
ditherFrac
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
posClrU32[v4] = posClrU32[v4 - 4];
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
//if color is local direction, else global
|
|
250
|
+
for (let i = 0; i < n_count; i++) {
|
|
251
|
+
//for each streamline
|
|
252
|
+
let vStart = offsetPt0[i]; //first vertex in streamline
|
|
253
|
+
let vEnd = offsetPt0[i + 1] - 1; //last vertex in streamline
|
|
254
|
+
let vStart3 = vStart * 3; //pts have 3 components XYZ
|
|
255
|
+
let vEnd3 = vEnd * 3;
|
|
256
|
+
let RGBA = direction2rgb(
|
|
257
|
+
pts[vStart3],
|
|
258
|
+
pts[vStart3 + 1],
|
|
259
|
+
pts[vStart3 + 2],
|
|
260
|
+
pts[vEnd3],
|
|
261
|
+
pts[vEnd3 + 1],
|
|
262
|
+
pts[vEnd3 + 2],
|
|
263
|
+
dither * Math.random()
|
|
264
|
+
);
|
|
265
|
+
let vStart4 = vStart * 4 + 3; //+3: fill 4th component colors: XYZC = 0123
|
|
266
|
+
let vEnd4 = vEnd * 4 + 3;
|
|
267
|
+
for (let j = vStart4; j <= vEnd4; j += 4) posClrU32[j] = RGBA;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
271
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Uint32Array(posClrU32), gl.STATIC_DRAW);
|
|
272
|
+
//INDICES:
|
|
273
|
+
let min_mm = this.fiberLength;
|
|
274
|
+
// https://blog.spacepatroldelta.com/a?ID=00950-d878555f-a97a-4e32-9f40-fd9a449cb4fe
|
|
275
|
+
let primitiveRestart = Math.pow(2, 32) - 1; //for gl.UNSIGNED_INT
|
|
276
|
+
let indices = [];
|
|
277
|
+
let stride = -1;
|
|
278
|
+
for (let i = 0; i < n_count; i++) {
|
|
279
|
+
//let n_pts = offsetPt0[i + 1] - offsetPt0[i]; //if streamline0 starts at point 0 and streamline1 at point 4, then streamline0 has 4 points: 0,1,2,3
|
|
280
|
+
if (this.fiberLengths[i] < min_mm) continue;
|
|
281
|
+
stride++;
|
|
282
|
+
if (stride % this.fiberDecimationStride !== 0) continue; //e.g. if stride is 2 then half culled
|
|
283
|
+
for (let j = offsetPt0[i]; j < offsetPt0[i + 1]; j++) indices.push(j);
|
|
284
|
+
indices.push(primitiveRestart);
|
|
285
|
+
}
|
|
286
|
+
this.indexCount = indices.length;
|
|
287
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
|
288
|
+
//glBufferData creates a new data store for the buffer object currently bound to target. Any pre-existing data store is deleted.
|
|
289
|
+
gl.bufferData(
|
|
290
|
+
gl.ELEMENT_ARRAY_BUFFER,
|
|
291
|
+
new Uint32Array(indices),
|
|
292
|
+
gl.STATIC_DRAW
|
|
293
|
+
);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
NVMesh.prototype.updateConnectome = function (gl) {
|
|
297
|
+
//draw nodes
|
|
298
|
+
let json = this;
|
|
299
|
+
//draw nodes
|
|
300
|
+
let tris = [];
|
|
301
|
+
let nNode = json.nodes.X.length;
|
|
302
|
+
let hasEdges = false;
|
|
303
|
+
if (nNode > 1 && json.hasOwnProperty("edges")) {
|
|
304
|
+
let nEdges = json.edges.length;
|
|
305
|
+
if ((nEdges = nNode * nNode)) hasEdges = true;
|
|
306
|
+
else console.log("Expected %d edges not %d", nNode * nNode, nEdges);
|
|
307
|
+
}
|
|
308
|
+
//draw all nodes
|
|
309
|
+
let pts = [];
|
|
310
|
+
let rgba255 = [];
|
|
311
|
+
let lut = cmapper.colormap(json.nodeColormap);
|
|
312
|
+
let lutNeg = cmapper.colormap(json.nodeColormapNegative);
|
|
313
|
+
let hasNeg = json.hasOwnProperty("nodeColormapNegative");
|
|
314
|
+
let min = json.nodeMinColor;
|
|
315
|
+
let max = json.nodeMaxColor;
|
|
316
|
+
for (let i = 0; i < nNode; i++) {
|
|
317
|
+
let radius = json.nodes.Size[i] * json.nodeScale;
|
|
318
|
+
if (radius <= 0.0) continue;
|
|
319
|
+
let color = json.nodes.Color[i];
|
|
320
|
+
let isNeg = false;
|
|
321
|
+
if (hasNeg && color < 0) {
|
|
322
|
+
isNeg = true;
|
|
323
|
+
color = -color;
|
|
324
|
+
}
|
|
325
|
+
if (min < max) {
|
|
326
|
+
if (color < min) continue;
|
|
327
|
+
color = (color - min) / (max - min);
|
|
328
|
+
} else color = 1.0;
|
|
329
|
+
color = Math.round(Math.max(Math.min(255, color * 255)), 1) * 4;
|
|
330
|
+
let rgba = [lut[color], lut[color + 1], lut[color + 2], 255];
|
|
331
|
+
if (isNeg)
|
|
332
|
+
rgba = [lutNeg[color], lutNeg[color + 1], lutNeg[color + 2], 255];
|
|
333
|
+
let pt = [json.nodes.X[i], json.nodes.Y[i], json.nodes.Z[i]];
|
|
334
|
+
NiivueObject3D.makeColoredSphere(pts, tris, rgba255, radius, pt, rgba);
|
|
335
|
+
}
|
|
336
|
+
//draw all edges
|
|
337
|
+
if (hasEdges) {
|
|
338
|
+
lut = cmapper.colormap(json.edgeColormap);
|
|
339
|
+
lutNeg = cmapper.colormap(json.edgeColormapNegative);
|
|
340
|
+
hasNeg = json.hasOwnProperty("edgeColormapNegative");
|
|
341
|
+
min = json.edgeMin;
|
|
342
|
+
max = json.edgeMax;
|
|
343
|
+
for (let i = 0; i < nNode - 1; i++) {
|
|
344
|
+
for (let j = i + 1; j < nNode; j++) {
|
|
345
|
+
let color = json.edges[i * nNode + j];
|
|
346
|
+
let isNeg = false;
|
|
347
|
+
if (hasNeg && color < 0) {
|
|
348
|
+
isNeg = true;
|
|
349
|
+
color = -color;
|
|
350
|
+
}
|
|
351
|
+
let radius = color * json.edgeScale;
|
|
352
|
+
if (radius <= 0) continue;
|
|
353
|
+
if (min < max) {
|
|
354
|
+
if (color < min) continue;
|
|
355
|
+
color = (color - min) / (max - min);
|
|
356
|
+
} else color = 1.0;
|
|
357
|
+
color = Math.round(Math.max(Math.min(255, color * 255)), 1) * 4;
|
|
358
|
+
let rgba = [lut[color], lut[color + 1], lut[color + 2], 255];
|
|
359
|
+
if (isNeg)
|
|
360
|
+
rgba = [lutNeg[color], lutNeg[color + 1], lutNeg[color + 2], 255];
|
|
361
|
+
|
|
362
|
+
let pti = [json.nodes.X[i], json.nodes.Y[i], json.nodes.Z[i]];
|
|
363
|
+
let ptj = [json.nodes.X[j], json.nodes.Y[j], json.nodes.Z[j]];
|
|
364
|
+
NiivueObject3D.makeColoredCylinder(
|
|
365
|
+
pts,
|
|
366
|
+
tris,
|
|
367
|
+
rgba255,
|
|
368
|
+
pti,
|
|
369
|
+
ptj,
|
|
370
|
+
radius,
|
|
371
|
+
rgba
|
|
372
|
+
);
|
|
373
|
+
} //for j
|
|
374
|
+
} //for i
|
|
375
|
+
} //hasEdges
|
|
376
|
+
//calculate spatial extent of connectome: user adjusting node sizes may influence size
|
|
377
|
+
let obj = getExtents(pts);
|
|
378
|
+
this.furthestVertexFromOrigin = obj.mxDx;
|
|
379
|
+
this.extentsMin = obj.extentsMin;
|
|
380
|
+
this.extentsMax = obj.extentsMax;
|
|
381
|
+
let posNormClr = this.generatePosNormClr(pts, tris, rgba255);
|
|
382
|
+
//generate webGL buffers and vao
|
|
383
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
|
384
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int32Array(tris), gl.STATIC_DRAW);
|
|
385
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
386
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(posNormClr), gl.STATIC_DRAW);
|
|
387
|
+
this.indexCount = tris.length;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
NVMesh.prototype.updateMesh = function (gl) {
|
|
391
|
+
if (this.offsetPt0) {
|
|
392
|
+
this.updateFibers(gl);
|
|
393
|
+
return; //fiber not mesh
|
|
394
|
+
}
|
|
395
|
+
if (this.hasConnectome) {
|
|
396
|
+
this.updateConnectome(gl);
|
|
397
|
+
return; //connectome not mesh
|
|
398
|
+
}
|
|
399
|
+
if (!this.pts || !this.tris || !this.rgba255) {
|
|
400
|
+
console.log("underspecified mesh");
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
let posNormClr = this.generatePosNormClr(this.pts, this.tris, this.rgba255);
|
|
404
|
+
if (this.layers && this.layers.length > 0) {
|
|
405
|
+
for (let i = 0; i < this.layers.length; i++) {
|
|
406
|
+
let layer = this.layers[i];
|
|
407
|
+
if (layer.opacity <= 0.0 || layer.cal_min >= layer.cal_max) continue;
|
|
408
|
+
let opacity = layer.opacity;
|
|
409
|
+
var u8 = new Uint8Array(posNormClr.buffer); //Each vertex has 7 components: PositionXYZ, NormalXYZ, RGBA32
|
|
410
|
+
function lerp(x, y, a) {
|
|
411
|
+
//https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/mix.xhtml
|
|
412
|
+
return x * (1 - a) + y * a;
|
|
413
|
+
}
|
|
414
|
+
if (layer.values.constructor === Uint32Array) {
|
|
415
|
+
//isRGBA!
|
|
416
|
+
let rgba8 = new Uint8Array(layer.values.buffer);
|
|
417
|
+
let k = 0;
|
|
418
|
+
for (let j = 0; j < layer.values.length; j++) {
|
|
419
|
+
let vtx = j * 28 + 24; //posNormClr is 28 bytes stride, RGBA color at offset 24,
|
|
420
|
+
u8[vtx + 0] = lerp(u8[vtx + 0], rgba8[k + 0], opacity);
|
|
421
|
+
u8[vtx + 1] = lerp(u8[vtx + 1], rgba8[k + 1], opacity);
|
|
422
|
+
u8[vtx + 2] = lerp(u8[vtx + 2], rgba8[k + 2], opacity);
|
|
423
|
+
k += 4;
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
let lut = cmapper.colormap(layer.colorMap);
|
|
428
|
+
|
|
429
|
+
let frame = Math.min(Math.max(layer.frame4D, 0), layer.nFrame4D - 1);
|
|
430
|
+
let nvtx = this.pts.length / 3;
|
|
431
|
+
let frameOffset = nvtx * frame;
|
|
432
|
+
if (layer.useNegativeCmap) {
|
|
433
|
+
layer.cal_min = Math.max(0, layer.cal_min);
|
|
434
|
+
layer.cal_max = Math.max(layer.cal_min + 0.000001, layer.cal_max);
|
|
435
|
+
}
|
|
436
|
+
let scale255 = 255.0 / (layer.cal_max - layer.cal_min);
|
|
437
|
+
//blend colors for each voxel
|
|
438
|
+
for (let j = 0; j < nvtx; j++) {
|
|
439
|
+
let v255 = Math.round(
|
|
440
|
+
(layer.values[j + frameOffset] - layer.cal_min) * scale255
|
|
441
|
+
);
|
|
442
|
+
if (v255 < 0) continue;
|
|
443
|
+
v255 = Math.min(255.0, v255) * 4;
|
|
444
|
+
let vtx = j * 28 + 24; //posNormClr is 28 bytes stride, RGBA color at offset 24,
|
|
445
|
+
u8[vtx + 0] = lerp(u8[vtx + 0], lut[v255 + 0], opacity);
|
|
446
|
+
u8[vtx + 1] = lerp(u8[vtx + 1], lut[v255 + 1], opacity);
|
|
447
|
+
u8[vtx + 2] = lerp(u8[vtx + 2], lut[v255 + 2], opacity);
|
|
448
|
+
}
|
|
449
|
+
if (layer.useNegativeCmap) {
|
|
450
|
+
let lut = cmapper.colormap(layer.colorMapNegative);
|
|
451
|
+
for (let j = 0; j < nvtx; j++) {
|
|
452
|
+
let v255 = Math.round(
|
|
453
|
+
(-layer.values[j + frameOffset] - layer.cal_min) * scale255
|
|
454
|
+
);
|
|
455
|
+
if (v255 < 0) continue;
|
|
456
|
+
v255 = Math.min(255.0, v255) * 4;
|
|
457
|
+
let vtx = j * 28 + 24; //posNormClr is 28 bytes stride, RGBA color at offset 24,
|
|
458
|
+
u8[vtx + 0] = lerp(u8[vtx + 0], lut[v255 + 0], opacity);
|
|
459
|
+
u8[vtx + 1] = lerp(u8[vtx + 1], lut[v255 + 1], opacity);
|
|
460
|
+
u8[vtx + 2] = lerp(u8[vtx + 2], lut[v255 + 2], opacity);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
//generate webGL buffers and vao
|
|
466
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
|
467
|
+
gl.bufferData(
|
|
468
|
+
gl.ELEMENT_ARRAY_BUFFER,
|
|
469
|
+
new Int32Array(this.tris),
|
|
470
|
+
gl.STATIC_DRAW
|
|
471
|
+
);
|
|
472
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
473
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(posNormClr), gl.STATIC_DRAW);
|
|
474
|
+
this.indexCount = this.tris.length;
|
|
475
|
+
this.vertexCount = this.pts.length;
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
NVMesh.prototype.setLayerProperty = function (id, key, val, gl) {
|
|
479
|
+
let layer = this.layers[id];
|
|
480
|
+
if (!layer.hasOwnProperty(key)) {
|
|
481
|
+
console.log("mesh does not have property ", key, layer);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
layer[key] = val;
|
|
485
|
+
this.updateMesh(gl); //apply the new properties...
|
|
486
|
+
};
|
|
487
|
+
NVMesh.prototype.setProperty = function (key, val, gl) {
|
|
488
|
+
if (!this.hasOwnProperty(key)) {
|
|
489
|
+
console.log("mesh does not have property ", key, this);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
this[key] = val;
|
|
493
|
+
this.updateMesh(gl); //apply the new properties...
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
function getExtents(pts) {
|
|
497
|
+
//each vertex has 3 coordinates: XYZ
|
|
498
|
+
let mxDx = 0.0;
|
|
499
|
+
let mn = vec3.fromValues(pts[0], pts[1], pts[2]);
|
|
500
|
+
let mx = vec3.fromValues(pts[0], pts[1], pts[2]);
|
|
501
|
+
for (let i = 0; i < pts.length; i += 3) {
|
|
502
|
+
let v = vec3.fromValues(pts[i], pts[i + 1], pts[i + 2]);
|
|
503
|
+
mxDx = Math.max(mxDx, vec3.len(v));
|
|
504
|
+
vec3.min(mn, mn, v);
|
|
505
|
+
vec3.max(mx, mx, v);
|
|
506
|
+
}
|
|
507
|
+
let extentsMin = [mn[0], mn[1], mn[2]];
|
|
508
|
+
let extentsMax = [mx[0], mx[1], mx[2]];
|
|
509
|
+
return { mxDx, extentsMin, extentsMax };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function generateNormals(pts, tris) {
|
|
513
|
+
//from https://github.com/rii-mango/Papaya
|
|
514
|
+
/*
|
|
515
|
+
Copyright (c) 2012-2015, RII-UTHSCSA
|
|
516
|
+
All rights reserved.
|
|
517
|
+
|
|
518
|
+
THIS PRODUCT IS NOT FOR CLINICAL USE.
|
|
519
|
+
|
|
520
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
|
521
|
+
following conditions are met:
|
|
522
|
+
|
|
523
|
+
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
|
524
|
+
disclaimer.
|
|
525
|
+
|
|
526
|
+
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
|
|
527
|
+
disclaimer in the documentation and/or other materials provided with the distribution.
|
|
528
|
+
|
|
529
|
+
- Neither the name of the RII-UTHSCSA nor the names of its contributors may be used to endorse or promote products
|
|
530
|
+
derived from this software without specific prior written permission.
|
|
531
|
+
|
|
532
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
533
|
+
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
534
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
535
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
536
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
537
|
+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
538
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
539
|
+
*/
|
|
540
|
+
var p1 = [],
|
|
541
|
+
p2 = [],
|
|
542
|
+
p3 = [],
|
|
543
|
+
normal = [],
|
|
544
|
+
nn = [],
|
|
545
|
+
ctr,
|
|
546
|
+
normalsDataLength = pts.length,
|
|
547
|
+
numIndices,
|
|
548
|
+
qx,
|
|
549
|
+
qy,
|
|
550
|
+
qz,
|
|
551
|
+
px,
|
|
552
|
+
py,
|
|
553
|
+
pz,
|
|
554
|
+
index1,
|
|
555
|
+
index2,
|
|
556
|
+
index3;
|
|
557
|
+
|
|
558
|
+
let norms = new Float32Array(normalsDataLength);
|
|
559
|
+
numIndices = tris.length;
|
|
560
|
+
for (ctr = 0; ctr < numIndices; ctr += 3) {
|
|
561
|
+
index1 = tris[ctr] * 3;
|
|
562
|
+
index2 = tris[ctr + 1] * 3;
|
|
563
|
+
index3 = tris[ctr + 2] * 3;
|
|
564
|
+
|
|
565
|
+
p1.x = pts[index1];
|
|
566
|
+
p1.y = pts[index1 + 1];
|
|
567
|
+
p1.z = pts[index1 + 2];
|
|
568
|
+
|
|
569
|
+
p2.x = pts[index2];
|
|
570
|
+
p2.y = pts[index2 + 1];
|
|
571
|
+
p2.z = pts[index2 + 2];
|
|
572
|
+
|
|
573
|
+
p3.x = pts[index3];
|
|
574
|
+
p3.y = pts[index3 + 1];
|
|
575
|
+
p3.z = pts[index3 + 2];
|
|
576
|
+
|
|
577
|
+
qx = p2.x - p1.x;
|
|
578
|
+
qy = p2.y - p1.y;
|
|
579
|
+
qz = p2.z - p1.z;
|
|
580
|
+
px = p3.x - p1.x;
|
|
581
|
+
py = p3.y - p1.y;
|
|
582
|
+
pz = p3.z - p1.z;
|
|
583
|
+
|
|
584
|
+
normal[0] = py * qz - pz * qy;
|
|
585
|
+
normal[1] = pz * qx - px * qz;
|
|
586
|
+
normal[2] = px * qy - py * qx;
|
|
587
|
+
|
|
588
|
+
norms[index1] += normal[0];
|
|
589
|
+
norms[index1 + 1] += normal[1];
|
|
590
|
+
norms[index1 + 2] += normal[2];
|
|
591
|
+
|
|
592
|
+
norms[index2] += normal[0];
|
|
593
|
+
norms[index2 + 1] += normal[1];
|
|
594
|
+
norms[index2 + 2] += normal[2];
|
|
595
|
+
|
|
596
|
+
norms[index3] += normal[0];
|
|
597
|
+
norms[index3 + 1] += normal[1];
|
|
598
|
+
norms[index3 + 2] += normal[2];
|
|
599
|
+
}
|
|
600
|
+
for (ctr = 0; ctr < normalsDataLength; ctr += 3) {
|
|
601
|
+
normal[0] = -1 * norms[ctr];
|
|
602
|
+
normal[1] = -1 * norms[ctr + 1];
|
|
603
|
+
normal[2] = -1 * norms[ctr + 2];
|
|
604
|
+
let len =
|
|
605
|
+
normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2];
|
|
606
|
+
if (len > 0) {
|
|
607
|
+
len = 1.0 / Math.sqrt(len);
|
|
608
|
+
normal[0] *= len;
|
|
609
|
+
normal[1] *= len;
|
|
610
|
+
normal[2] *= len;
|
|
611
|
+
}
|
|
612
|
+
norms[ctr] = normal[0];
|
|
613
|
+
norms[ctr + 1] = normal[1];
|
|
614
|
+
norms[ctr + 2] = normal[2];
|
|
615
|
+
}
|
|
616
|
+
return norms;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
NVMesh.prototype.generatePosNormClr = function (pts, tris, rgba255) {
|
|
620
|
+
//Each streamline vertex has color, normal and position attributes
|
|
621
|
+
//Interleaved Vertex Data https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/TechniquesforWorkingwithVertexData/TechniquesforWorkingwithVertexData.html
|
|
622
|
+
if (pts.length < 3 || rgba255.length < 4)
|
|
623
|
+
log.error("Catastrophic failure generatePosNormClr()");
|
|
624
|
+
let norms = generateNormals(pts, tris);
|
|
625
|
+
let npt = pts.length / 3;
|
|
626
|
+
let isPerVertexColors = npt === rgba255.length / 4;
|
|
627
|
+
var f32 = new Float32Array(npt * 7); //Each vertex has 7 components: PositionXYZ, NormalXYZ, RGBA32
|
|
628
|
+
var u8 = new Uint8Array(f32.buffer); //Each vertex has 7 components: PositionXYZ, NormalXYZ, RGBA32
|
|
629
|
+
let p = 0; //input position
|
|
630
|
+
let c = 0; //input color
|
|
631
|
+
let f = 0; //output float32 location (position and normals)
|
|
632
|
+
let u = 24; //output uint8 location (colors), offset 24 as after 3*position+3*normal
|
|
633
|
+
for (let i = 0; i < npt; i++) {
|
|
634
|
+
f32[f + 0] = pts[p + 0];
|
|
635
|
+
f32[f + 1] = pts[p + 1];
|
|
636
|
+
f32[f + 2] = pts[p + 2];
|
|
637
|
+
f32[f + 3] = norms[p + 0];
|
|
638
|
+
f32[f + 4] = norms[p + 1];
|
|
639
|
+
f32[f + 5] = norms[p + 2];
|
|
640
|
+
u8[u] = rgba255[c + 0];
|
|
641
|
+
u8[u + 1] = rgba255[c + 1];
|
|
642
|
+
u8[u + 2] = rgba255[c + 2];
|
|
643
|
+
u8[u + 3] = rgba255[c + 3];
|
|
644
|
+
if (isPerVertexColors) c += 4;
|
|
645
|
+
p += 3; //read 3 input components: XYZ
|
|
646
|
+
f += 7; //write 7 output components: 3*Position, 3*Normal, 1*RGBA
|
|
647
|
+
u += 28; //stride of 28 bytes
|
|
648
|
+
}
|
|
649
|
+
return f32;
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
NVMesh.readTCK = function (buffer) {
|
|
653
|
+
//https://mrtrix.readthedocs.io/en/latest/getting_started/image_data.html#tracks-file-format-tck
|
|
654
|
+
let len = buffer.byteLength;
|
|
655
|
+
if (len < 20) throw new Error("File too small to be TCK: bytes = " + len);
|
|
656
|
+
var bytes = new Uint8Array(buffer);
|
|
657
|
+
let pos = 0;
|
|
658
|
+
function readStr() {
|
|
659
|
+
while (pos < len && bytes[pos] === 10) pos++; //skip blank lines
|
|
660
|
+
let startPos = pos;
|
|
661
|
+
while (pos < len && bytes[pos] !== 10) pos++;
|
|
662
|
+
pos++; //skip EOLN
|
|
663
|
+
if (pos - startPos < 1) return "";
|
|
664
|
+
return new TextDecoder().decode(buffer.slice(startPos, pos - 1));
|
|
665
|
+
}
|
|
666
|
+
let line = readStr(); //1st line: signature 'mrtrix tracks'
|
|
667
|
+
if (!line.includes("mrtrix tracks")) {
|
|
668
|
+
console.log("Not a valid TCK file");
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
while (pos < len && !line.startsWith("END")) line = readStr();
|
|
672
|
+
var reader = new DataView(buffer);
|
|
673
|
+
//read and transform vertex positions
|
|
674
|
+
let npt = 0;
|
|
675
|
+
let offsetPt0 = [];
|
|
676
|
+
offsetPt0.push(npt); //1st streamline starts at 0
|
|
677
|
+
let pts = [];
|
|
678
|
+
while (pos + 12 < len) {
|
|
679
|
+
var ptx = reader.getFloat32(pos, true);
|
|
680
|
+
pos += 4;
|
|
681
|
+
var pty = reader.getFloat32(pos, true);
|
|
682
|
+
pos += 4;
|
|
683
|
+
var ptz = reader.getFloat32(pos, true);
|
|
684
|
+
pos += 4;
|
|
685
|
+
if (!isFinite(ptx)) {
|
|
686
|
+
//both NaN and Inifinity are not finite
|
|
687
|
+
offsetPt0.push(npt);
|
|
688
|
+
if (!isNaN(ptx))
|
|
689
|
+
//terminate if infinity
|
|
690
|
+
break;
|
|
691
|
+
} else {
|
|
692
|
+
pts.push(ptx);
|
|
693
|
+
pts.push(pty);
|
|
694
|
+
pts.push(ptz);
|
|
695
|
+
npt++;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
pts,
|
|
700
|
+
offsetPt0,
|
|
701
|
+
};
|
|
702
|
+
}; //readTCK()
|
|
703
|
+
|
|
704
|
+
NVMesh.readTRK = function (buffer) {
|
|
705
|
+
// http://trackvis.org/docs/?subsect=fileformat
|
|
706
|
+
// http://www.tractometer.org/fiberweb/
|
|
707
|
+
// https://github.com/xtk/X/tree/master/io
|
|
708
|
+
// in practice, always little endian
|
|
709
|
+
var reader = new DataView(buffer);
|
|
710
|
+
var magic = reader.getUint32(0, true); //'TRAC'
|
|
711
|
+
if (magic !== 1128354388) {
|
|
712
|
+
//e.g. TRK.gz
|
|
713
|
+
let raw;
|
|
714
|
+
if (magic === 4247762216) {
|
|
715
|
+
//zstd
|
|
716
|
+
raw = fzstd.decompress(new Uint8Array(buffer));
|
|
717
|
+
raw = new Uint8Array(raw);
|
|
718
|
+
} else raw = fflate.decompressSync(new Uint8Array(buffer));
|
|
719
|
+
buffer = raw.buffer;
|
|
720
|
+
reader = new DataView(buffer);
|
|
721
|
+
magic = reader.getUint32(0, true); //'TRAC'
|
|
722
|
+
}
|
|
723
|
+
var vers = reader.getUint32(992, true); //2
|
|
724
|
+
var hdr_sz = reader.getUint32(996, true); //1000
|
|
725
|
+
if (vers > 2 || hdr_sz !== 1000 || magic !== 1128354388)
|
|
726
|
+
throw new Error("Not a valid TRK file");
|
|
727
|
+
let dps = [];
|
|
728
|
+
let dpv = [];
|
|
729
|
+
var n_scalars = reader.getInt16(36, true);
|
|
730
|
+
if (n_scalars > 0) {
|
|
731
|
+
//data_per_vertex
|
|
732
|
+
for (let i = 0; i < n_scalars; i++) {
|
|
733
|
+
let arr = new Uint8Array(buffer.slice(38 + i * 20, 58 + i * 20));
|
|
734
|
+
var str = new TextDecoder().decode(arr).split("\0").shift();
|
|
735
|
+
dpv.push({
|
|
736
|
+
id: str.trim(),
|
|
737
|
+
vals: [],
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
var voxel_sizeX = reader.getFloat32(12, true);
|
|
742
|
+
var voxel_sizeY = reader.getFloat32(16, true);
|
|
743
|
+
var voxel_sizeZ = reader.getFloat32(20, true);
|
|
744
|
+
var zoomMat = mat4.fromValues(
|
|
745
|
+
1 / voxel_sizeX,
|
|
746
|
+
0,
|
|
747
|
+
0,
|
|
748
|
+
-0.5,
|
|
749
|
+
0,
|
|
750
|
+
1 / voxel_sizeY,
|
|
751
|
+
0,
|
|
752
|
+
-0.5,
|
|
753
|
+
0,
|
|
754
|
+
0,
|
|
755
|
+
1 / voxel_sizeZ,
|
|
756
|
+
-0.5,
|
|
757
|
+
0,
|
|
758
|
+
0,
|
|
759
|
+
0,
|
|
760
|
+
1
|
|
761
|
+
);
|
|
762
|
+
var n_properties = reader.getInt16(238, true);
|
|
763
|
+
if (n_properties > 0) {
|
|
764
|
+
for (let i = 0; i < n_properties; i++) {
|
|
765
|
+
let arr = new Uint8Array(buffer.slice(240 + i * 20, 260 + i * 20));
|
|
766
|
+
var str = new TextDecoder().decode(arr).split("\0").shift();
|
|
767
|
+
dps.push({
|
|
768
|
+
id: str.trim(),
|
|
769
|
+
vals: [],
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
var mat = mat4.create();
|
|
774
|
+
for (let i = 0; i < 16; i++) mat[i] = reader.getFloat32(440 + i * 4, true);
|
|
775
|
+
if (mat[15] === 0.0) {
|
|
776
|
+
//vox_to_ras[3][3] is 0, it means the matrix is not recorded
|
|
777
|
+
console.log("TRK vox_to_ras not set");
|
|
778
|
+
mat4.identity(mat);
|
|
779
|
+
}
|
|
780
|
+
var vox2mmMat = mat4.create();
|
|
781
|
+
mat4.mul(vox2mmMat, mat, zoomMat);
|
|
782
|
+
let i32 = null;
|
|
783
|
+
let f32 = null;
|
|
784
|
+
i32 = new Int32Array(buffer.slice(hdr_sz));
|
|
785
|
+
f32 = new Float32Array(i32.buffer);
|
|
786
|
+
|
|
787
|
+
let ntracks = i32.length;
|
|
788
|
+
//read and transform vertex positions
|
|
789
|
+
let i = 0;
|
|
790
|
+
let npt = 0;
|
|
791
|
+
let offsetPt0 = [];
|
|
792
|
+
let pts = [];
|
|
793
|
+
while (i < ntracks) {
|
|
794
|
+
let n_pts = i32[i];
|
|
795
|
+
i = i + 1; // read 1 32-bit integer for number of points in this streamline
|
|
796
|
+
offsetPt0.push(npt); //index of first vertex in this streamline
|
|
797
|
+
for (let j = 0; j < n_pts; j++) {
|
|
798
|
+
let ptx = f32[i + 0];
|
|
799
|
+
let pty = f32[i + 1];
|
|
800
|
+
let ptz = f32[i + 2];
|
|
801
|
+
i += 3; //read 3 32-bit floats for XYZ position
|
|
802
|
+
pts.push(
|
|
803
|
+
ptx * vox2mmMat[0] +
|
|
804
|
+
pty * vox2mmMat[1] +
|
|
805
|
+
ptz * vox2mmMat[2] +
|
|
806
|
+
vox2mmMat[3]
|
|
807
|
+
);
|
|
808
|
+
pts.push(
|
|
809
|
+
ptx * vox2mmMat[4] +
|
|
810
|
+
pty * vox2mmMat[5] +
|
|
811
|
+
ptz * vox2mmMat[6] +
|
|
812
|
+
vox2mmMat[7]
|
|
813
|
+
);
|
|
814
|
+
pts.push(
|
|
815
|
+
ptx * vox2mmMat[8] +
|
|
816
|
+
pty * vox2mmMat[9] +
|
|
817
|
+
ptz * vox2mmMat[10] +
|
|
818
|
+
vox2mmMat[11]
|
|
819
|
+
);
|
|
820
|
+
if (n_scalars > 0) {
|
|
821
|
+
for (let s = 0; s < n_scalars; s++) {
|
|
822
|
+
dpv[s].vals.push(f32[i]);
|
|
823
|
+
i++;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
npt++;
|
|
827
|
+
} // for j: each point in streamline
|
|
828
|
+
if (n_properties > 0) {
|
|
829
|
+
for (let j = 0; j < n_properties; j++) {
|
|
830
|
+
dps[j].vals.push(f32[i]);
|
|
831
|
+
i++;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
} //for each streamline: while i < n_count
|
|
835
|
+
offsetPt0.push(npt); //add 'first index' as if one more line was added (fence post problem)
|
|
836
|
+
return {
|
|
837
|
+
pts,
|
|
838
|
+
offsetPt0,
|
|
839
|
+
dps,
|
|
840
|
+
dpv,
|
|
841
|
+
};
|
|
842
|
+
}; //readTRK()
|
|
843
|
+
|
|
844
|
+
function readTxtVTK(buffer) {
|
|
845
|
+
var enc = new TextDecoder("utf-8");
|
|
846
|
+
var txt = enc.decode(buffer);
|
|
847
|
+
var lines = txt.split("\n");
|
|
848
|
+
var n = lines.length;
|
|
849
|
+
if (n < 7 || !lines[0].startsWith("# vtk DataFile"))
|
|
850
|
+
alert("Invalid VTK image");
|
|
851
|
+
if (!lines[2].startsWith("ASCII")) alert("Not ASCII VTK mesh");
|
|
852
|
+
let pos = 3;
|
|
853
|
+
while (lines[pos].length < 1) pos++; //skip blank lines
|
|
854
|
+
if (!lines[pos].includes("POLYDATA")) alert("Not ASCII VTK polydata");
|
|
855
|
+
pos++;
|
|
856
|
+
while (lines[pos].length < 1) pos++; //skip blank lines
|
|
857
|
+
if (!lines[pos].startsWith("POINTS")) alert("Not VTK POINTS");
|
|
858
|
+
let items = lines[pos].split(" ");
|
|
859
|
+
let nvert = parseInt(items[1]); //POINTS 10261 float
|
|
860
|
+
let nvert3 = nvert * 3;
|
|
861
|
+
var positions = new Float32Array(nvert * 3);
|
|
862
|
+
let v = 0;
|
|
863
|
+
while (v < nvert * 3) {
|
|
864
|
+
pos++;
|
|
865
|
+
let str = lines[pos].trim();
|
|
866
|
+
let pts = str.split(" ");
|
|
867
|
+
for (let i = 0; i < pts.length; i++) {
|
|
868
|
+
if (v >= nvert3) break;
|
|
869
|
+
positions[v] = parseFloat(pts[i]);
|
|
870
|
+
v++;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
let tris = [];
|
|
874
|
+
pos++;
|
|
875
|
+
while (lines[pos].length < 1) pos++; //skip blank lines
|
|
876
|
+
items = lines[pos].split(" ");
|
|
877
|
+
pos++;
|
|
878
|
+
if (items[0].includes("LINES")) {
|
|
879
|
+
let n_count = parseInt(items[1]);
|
|
880
|
+
if (n_count < 1) alert("Corrupted VTK ASCII");
|
|
881
|
+
let str = lines[pos].trim();
|
|
882
|
+
let offsetPt0 = [];
|
|
883
|
+
let pts = [];
|
|
884
|
+
if (str.startsWith("OFFSETS")) {
|
|
885
|
+
// 'new' line style https://discourse.vtk.org/t/upcoming-changes-to-vtkcellarray/2066
|
|
886
|
+
offsetPt0 = new Uint32Array(n_count);
|
|
887
|
+
pos++;
|
|
888
|
+
let c = 0;
|
|
889
|
+
while (c < n_count) {
|
|
890
|
+
str = lines[pos].trim();
|
|
891
|
+
pos++;
|
|
892
|
+
let items = str.split(" ");
|
|
893
|
+
for (let i = 0; i < items.length; i++) {
|
|
894
|
+
offsetPt0[c] = parseInt(items[i]);
|
|
895
|
+
c++;
|
|
896
|
+
if (c >= n_count) break;
|
|
897
|
+
} //for each line
|
|
898
|
+
} //while offset array not filled
|
|
899
|
+
pts = positions;
|
|
900
|
+
} else {
|
|
901
|
+
//classic line style https://www.visitusers.org/index.php?title=ASCII_VTK_Files
|
|
902
|
+
offsetPt0 = new Uint32Array(n_count + 1);
|
|
903
|
+
let npt = 0;
|
|
904
|
+
pts = [];
|
|
905
|
+
offsetPt0[0] = 0; //1st streamline starts at 0
|
|
906
|
+
let asciiInts = [];
|
|
907
|
+
let asciiIntsPos = 0;
|
|
908
|
+
function lineToInts() {
|
|
909
|
+
//VTK can save one array across multiple ASCII lines
|
|
910
|
+
str = lines[pos].trim();
|
|
911
|
+
let items = str.split(" ");
|
|
912
|
+
asciiInts = [];
|
|
913
|
+
for (let i = 0; i < items.length; i++)
|
|
914
|
+
asciiInts.push(parseInt(items[i]));
|
|
915
|
+
asciiIntsPos = 0;
|
|
916
|
+
pos++;
|
|
917
|
+
}
|
|
918
|
+
lineToInts();
|
|
919
|
+
for (let c = 0; c < n_count; c++) {
|
|
920
|
+
if (asciiIntsPos >= asciiInts.length) lineToInts();
|
|
921
|
+
let numPoints = asciiInts[asciiIntsPos++];
|
|
922
|
+
npt += numPoints;
|
|
923
|
+
offsetPt0[c + 1] = npt;
|
|
924
|
+
for (let i = 0; i < numPoints; i++) {
|
|
925
|
+
if (asciiIntsPos >= asciiInts.length) lineToInts();
|
|
926
|
+
let idx = asciiInts[asciiIntsPos++] * 3;
|
|
927
|
+
pts.push(positions[idx + 0]); //X
|
|
928
|
+
pts.push(positions[idx + 1]); //Y
|
|
929
|
+
pts.push(positions[idx + 2]); //Z
|
|
930
|
+
} //for numPoints: number of segments in streamline
|
|
931
|
+
} //for n_count: number of streamlines
|
|
932
|
+
}
|
|
933
|
+
return {
|
|
934
|
+
pts,
|
|
935
|
+
offsetPt0,
|
|
936
|
+
};
|
|
937
|
+
} else if (items[0].includes("TRIANGLE_STRIPS")) {
|
|
938
|
+
let nstrip = parseInt(items[1]);
|
|
939
|
+
for (let i = 0; i < nstrip; i++) {
|
|
940
|
+
let str = lines[pos].trim();
|
|
941
|
+
pos++;
|
|
942
|
+
let vs = str.split(" ");
|
|
943
|
+
let ntri = parseInt(vs[0]) - 2; //-2 as triangle strip is creates pts - 2 faces
|
|
944
|
+
let k = 1;
|
|
945
|
+
for (let t = 0; t < ntri; t++) {
|
|
946
|
+
if (t % 2) {
|
|
947
|
+
// preserve winding order
|
|
948
|
+
tris.push(parseInt(vs[k + 2]));
|
|
949
|
+
tris.push(parseInt(vs[k + 1]));
|
|
950
|
+
tris.push(parseInt(vs[k]));
|
|
951
|
+
} else {
|
|
952
|
+
tris.push(parseInt(vs[k]));
|
|
953
|
+
tris.push(parseInt(vs[k + 1]));
|
|
954
|
+
tris.push(parseInt(vs[k + 2]));
|
|
955
|
+
}
|
|
956
|
+
k += 1;
|
|
957
|
+
} //for each triangle
|
|
958
|
+
} //for each strip
|
|
959
|
+
} else if (items[0].includes("POLYGONS")) {
|
|
960
|
+
let npoly = parseInt(items[1]);
|
|
961
|
+
for (let i = 0; i < npoly; i++) {
|
|
962
|
+
let str = lines[pos].trim();
|
|
963
|
+
pos++;
|
|
964
|
+
let vs = str.split(" ");
|
|
965
|
+
let ntri = parseInt(vs[0]) - 2; //e.g. 3 for triangle
|
|
966
|
+
let fx = parseInt(vs[1]);
|
|
967
|
+
let fy = parseInt(vs[2]);
|
|
968
|
+
for (let t = 0; t < ntri; t++) {
|
|
969
|
+
let fz = parseInt(vs[3 + t]);
|
|
970
|
+
tris.push(fx);
|
|
971
|
+
tris.push(fy);
|
|
972
|
+
tris.push(fz);
|
|
973
|
+
fy = fz;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
} else alert("Unsupported ASCII VTK datatype " + items[0]);
|
|
977
|
+
var indices = new Int32Array(tris);
|
|
978
|
+
return {
|
|
979
|
+
positions,
|
|
980
|
+
indices,
|
|
981
|
+
};
|
|
982
|
+
} // readTxtVTK()
|
|
983
|
+
|
|
984
|
+
NVMesh.readSTC = function (buffer, n_vert) {
|
|
985
|
+
//mne STC format
|
|
986
|
+
//https://github.com/mne-tools/mne-python/blob/main/mne/source_estimate.py#L211-L365
|
|
987
|
+
//https://github.com/fahsuanlin/fhlin_toolbox/blob/400cb73cda4880d9ad7841d9dd68e4e9762976bf/codes/inverse_read_stc.m
|
|
988
|
+
let len = buffer.byteLength;
|
|
989
|
+
var reader = new DataView(buffer);
|
|
990
|
+
//first 12 bytes are header
|
|
991
|
+
let epoch_begin_latency = reader.getFloat32(0, false);
|
|
992
|
+
let sample_period = reader.getFloat32(4, false);
|
|
993
|
+
let n_vertex = reader.getInt32(8, false);
|
|
994
|
+
if (n_vertex !== n_vert) {
|
|
995
|
+
console.log("Overlay has " + n_vertex + " vertices, expected " + n_vert);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
//next 4*n_vertex bytes are vertex IDS
|
|
999
|
+
let pos = 12 + n_vertex * 4;
|
|
1000
|
+
//next 4 bytes reports number of volumes/time points
|
|
1001
|
+
let n_time = reader.getUint32(pos, false);
|
|
1002
|
+
pos += 4;
|
|
1003
|
+
let f32 = new Float32Array(n_time * n_vertex);
|
|
1004
|
+
//reading all floats with .slice() would be faster, but lets handle endian-ness
|
|
1005
|
+
for (let i = 0; i < n_time * n_vertex; i++) {
|
|
1006
|
+
f32[i] = reader.getFloat32(pos, false);
|
|
1007
|
+
pos += 4;
|
|
1008
|
+
}
|
|
1009
|
+
return f32;
|
|
1010
|
+
//this.vertexCount = this.pts.length;
|
|
1011
|
+
}; // readSTC()
|
|
1012
|
+
|
|
1013
|
+
NVMesh.readCURV = function (buffer, n_vert) {
|
|
1014
|
+
//simple format used by Freesurfer BIG-ENDIAN
|
|
1015
|
+
// https://github.com/bonilhamusclab/MRIcroS/blob/master/%2BfileUtils/%2Bpial/readPial.m
|
|
1016
|
+
// http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm
|
|
1017
|
+
const view = new DataView(buffer); //ArrayBuffer to dataview
|
|
1018
|
+
//ALWAYS big endian
|
|
1019
|
+
let sig0 = view.getUint8(0);
|
|
1020
|
+
let sig1 = view.getUint8(1);
|
|
1021
|
+
let sig2 = view.getUint8(2);
|
|
1022
|
+
let n_vertex = view.getUint32(3, false);
|
|
1023
|
+
let num_f = view.getUint32(7, false);
|
|
1024
|
+
let n_time = view.getUint32(11, false);
|
|
1025
|
+
if (sig0 !== 255 || sig1 !== 255 || sig2 !== 255)
|
|
1026
|
+
log.debug(
|
|
1027
|
+
"Unable to recognize file type: does not appear to be FreeSurfer format."
|
|
1028
|
+
);
|
|
1029
|
+
if (n_vert !== n_vertex) {
|
|
1030
|
+
console.log("CURV file has different number of vertices than mesh");
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (buffer.byteLength < 15 + 4 * n_vertex * n_time) {
|
|
1034
|
+
console.log("CURV file smaller than specified");
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
let f32 = new Float32Array(n_time * n_vertex);
|
|
1038
|
+
let pos = 15;
|
|
1039
|
+
//reading all floats with .slice() would be faster, but lets handle endian-ness
|
|
1040
|
+
for (let i = 0; i < n_time * n_vertex; i++) {
|
|
1041
|
+
f32[i] = view.getFloat32(pos, false);
|
|
1042
|
+
pos += 4;
|
|
1043
|
+
}
|
|
1044
|
+
let mn = f32[0];
|
|
1045
|
+
let mx = f32[0];
|
|
1046
|
+
for (var i = 0; i < f32.length; i++) {
|
|
1047
|
+
mn = Math.min(mn, f32[i]);
|
|
1048
|
+
mx = Math.max(mx, f32[i]);
|
|
1049
|
+
}
|
|
1050
|
+
//normalize and invert then sqrt
|
|
1051
|
+
let scale = 1.0 / (mx - mn);
|
|
1052
|
+
for (var i = 0; i < f32.length; i++)
|
|
1053
|
+
f32[i] = Math.sqrt(1.0 - (f32[i] - mn) * scale);
|
|
1054
|
+
return f32;
|
|
1055
|
+
}; // readCURV()
|
|
1056
|
+
|
|
1057
|
+
NVMesh.readANNOT = function (buffer, n_vert) {
|
|
1058
|
+
//freesurfer Annotation file provides vertex colors
|
|
1059
|
+
// https://surfer.nmr.mgh.harvard.edu/fswiki/LabelsClutsAnnotationFiles
|
|
1060
|
+
const view = new DataView(buffer); //ArrayBuffer to dataview
|
|
1061
|
+
//ALWAYS big endian
|
|
1062
|
+
let n_vertex = view.getUint32(0, false);
|
|
1063
|
+
if (n_vert !== n_vertex) {
|
|
1064
|
+
console.log("ANNOT file has different number of vertices than mesh");
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (buffer.byteLength < 4 + 8 * n_vertex) {
|
|
1068
|
+
console.log("ANNOT file smaller than specified");
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
let pos = 4;
|
|
1072
|
+
//reading all floats with .slice() would be faster, but lets handle endian-ness
|
|
1073
|
+
let rgba32 = new Uint32Array(n_vertex);
|
|
1074
|
+
for (let i = 0; i < n_vertex; i++) {
|
|
1075
|
+
let idx = view.getUint32(pos, false);
|
|
1076
|
+
pos += 4;
|
|
1077
|
+
rgba32[idx] = view.getUint32(pos, false);
|
|
1078
|
+
pos += 4;
|
|
1079
|
+
}
|
|
1080
|
+
return rgba32;
|
|
1081
|
+
}; // readANNOT()
|
|
1082
|
+
|
|
1083
|
+
NVMesh.readVTK = function (buffer) {
|
|
1084
|
+
let len = buffer.byteLength;
|
|
1085
|
+
if (len < 20)
|
|
1086
|
+
throw new Error("File too small to be VTK: bytes = " + buffer.byteLength);
|
|
1087
|
+
var bytes = new Uint8Array(buffer);
|
|
1088
|
+
let pos = 0;
|
|
1089
|
+
function readStr() {
|
|
1090
|
+
while (pos < len && bytes[pos] === 10) pos++; //skip blank lines
|
|
1091
|
+
let startPos = pos;
|
|
1092
|
+
while (pos < len && bytes[pos] !== 10) pos++;
|
|
1093
|
+
pos++; //skip EOLN
|
|
1094
|
+
if (pos - startPos < 1) return "";
|
|
1095
|
+
return new TextDecoder().decode(buffer.slice(startPos, pos - 1));
|
|
1096
|
+
}
|
|
1097
|
+
let line = readStr(); //1st line: signature
|
|
1098
|
+
if (!line.startsWith("# vtk DataFile")) alert("Invalid VTK mesh");
|
|
1099
|
+
line = readStr(); //2nd line comment
|
|
1100
|
+
line = readStr(); //3rd line ASCII/BINARY
|
|
1101
|
+
if (line.startsWith("ASCII")) return readTxtVTK(buffer); //from NiiVue
|
|
1102
|
+
else if (!line.startsWith("BINARY"))
|
|
1103
|
+
alert("Invalid VTK image, expected ASCII or BINARY", line);
|
|
1104
|
+
line = readStr(); //5th line "DATASET POLYDATA"
|
|
1105
|
+
if (!line.includes("POLYDATA")) alert("Only able to read VTK POLYDATA", line);
|
|
1106
|
+
line = readStr(); //6th line "POINTS 10261 float"
|
|
1107
|
+
if (
|
|
1108
|
+
!line.includes("POINTS") ||
|
|
1109
|
+
(!line.includes("double") && !line.includes("float"))
|
|
1110
|
+
)
|
|
1111
|
+
console.log("Only able to read VTK float or double POINTS" + line);
|
|
1112
|
+
let isFloat64 = line.includes("double");
|
|
1113
|
+
let items = line.split(" ");
|
|
1114
|
+
let nvert = parseInt(items[1]); //POINTS 10261 float
|
|
1115
|
+
let nvert3 = nvert * 3;
|
|
1116
|
+
var positions = new Float32Array(nvert3);
|
|
1117
|
+
var reader = new DataView(buffer);
|
|
1118
|
+
if (isFloat64) {
|
|
1119
|
+
for (let i = 0; i < nvert3; i++) {
|
|
1120
|
+
positions[i] = reader.getFloat64(pos, false);
|
|
1121
|
+
pos += 8;
|
|
1122
|
+
}
|
|
1123
|
+
} else {
|
|
1124
|
+
for (let i = 0; i < nvert3; i++) {
|
|
1125
|
+
positions[i] = reader.getFloat32(pos, false);
|
|
1126
|
+
pos += 4;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
line = readStr(); //Type, "LINES 11885 "
|
|
1130
|
+
items = line.split(" ");
|
|
1131
|
+
let tris = [];
|
|
1132
|
+
if (items[0].includes("LINES")) {
|
|
1133
|
+
let n_count = parseInt(items[1]);
|
|
1134
|
+
//tractogaphy data: detect if borked by DiPy
|
|
1135
|
+
let posOK = pos;
|
|
1136
|
+
line = readStr(); //borked files "OFFSETS vtktypeint64"
|
|
1137
|
+
if (line.startsWith("OFFSETS")) {
|
|
1138
|
+
//console.log("invalid VTK file created by DiPy");
|
|
1139
|
+
let isInt64 = false;
|
|
1140
|
+
if (line.includes("int64")) isInt64 = true;
|
|
1141
|
+
let offsetPt0 = new Uint32Array(n_count);
|
|
1142
|
+
if (isInt64) {
|
|
1143
|
+
let isOverflowInt32 = false;
|
|
1144
|
+
for (let c = 0; c < n_count; c++) {
|
|
1145
|
+
let idx = reader.getInt32(pos, false);
|
|
1146
|
+
if (idx !== 0) isOverflowInt32 = true;
|
|
1147
|
+
pos += 4;
|
|
1148
|
+
idx = reader.getInt32(pos, false);
|
|
1149
|
+
pos += 4;
|
|
1150
|
+
offsetPt0[c] = idx;
|
|
1151
|
+
}
|
|
1152
|
+
if (isOverflowInt32)
|
|
1153
|
+
console.log("int32 overflow: JavaScript does not support int64");
|
|
1154
|
+
} else {
|
|
1155
|
+
for (let c = 0; c < n_count; c++) {
|
|
1156
|
+
let idx = reader.getInt32(pos, false);
|
|
1157
|
+
pos += 4;
|
|
1158
|
+
offsetPt0[c] = idx;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
let pts = positions;
|
|
1162
|
+
return {
|
|
1163
|
+
pts,
|
|
1164
|
+
offsetPt0,
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
pos = posOK; //valid VTK file
|
|
1168
|
+
let npt = 0;
|
|
1169
|
+
let offsetPt0 = [];
|
|
1170
|
+
let pts = [];
|
|
1171
|
+
offsetPt0.push(npt); //1st streamline starts at 0
|
|
1172
|
+
for (let c = 0; c < n_count; c++) {
|
|
1173
|
+
let numPoints = reader.getInt32(pos, false);
|
|
1174
|
+
pos += 4;
|
|
1175
|
+
npt += numPoints;
|
|
1176
|
+
offsetPt0.push(npt);
|
|
1177
|
+
for (let i = 0; i < numPoints; i++) {
|
|
1178
|
+
let idx = reader.getInt32(pos, false) * 3;
|
|
1179
|
+
pos += 4;
|
|
1180
|
+
pts.push(positions[idx + 0]);
|
|
1181
|
+
pts.push(positions[idx + 1]);
|
|
1182
|
+
pts.push(positions[idx + 2]);
|
|
1183
|
+
} //for numPoints: number of segments in streamline
|
|
1184
|
+
} //for n_count: number of streamlines
|
|
1185
|
+
return {
|
|
1186
|
+
pts,
|
|
1187
|
+
offsetPt0,
|
|
1188
|
+
};
|
|
1189
|
+
} else if (items[0].includes("TRIANGLE_STRIPS")) {
|
|
1190
|
+
let nstrip = parseInt(items[1]);
|
|
1191
|
+
for (let i = 0; i < nstrip; i++) {
|
|
1192
|
+
let ntri = reader.getInt32(pos, false) - 2; //-2 as triangle strip is creates pts - 2 faces
|
|
1193
|
+
pos += 4;
|
|
1194
|
+
for (let t = 0; t < ntri; t++) {
|
|
1195
|
+
if (t % 2) {
|
|
1196
|
+
// preserve winding order
|
|
1197
|
+
tris.push(reader.getInt32(pos + 8, false));
|
|
1198
|
+
tris.push(reader.getInt32(pos + 4, false));
|
|
1199
|
+
tris.push(reader.getInt32(pos, false));
|
|
1200
|
+
} else {
|
|
1201
|
+
tris.push(reader.getInt32(pos, false));
|
|
1202
|
+
tris.push(reader.getInt32(pos + 4, false));
|
|
1203
|
+
tris.push(reader.getInt32(pos + 8, false));
|
|
1204
|
+
}
|
|
1205
|
+
pos += 4;
|
|
1206
|
+
} //for each triangle
|
|
1207
|
+
pos += 8;
|
|
1208
|
+
} //for each strip
|
|
1209
|
+
} else if (items[0].includes("POLYGONS")) {
|
|
1210
|
+
let npoly = parseInt(items[1]);
|
|
1211
|
+
for (let i = 0; i < npoly; i++) {
|
|
1212
|
+
let ntri = reader.getInt32(pos, false) - 2; //3 for single triangle, 4 for 2 triangles
|
|
1213
|
+
pos += 4;
|
|
1214
|
+
let fx = reader.getInt32(pos, false);
|
|
1215
|
+
pos += 4;
|
|
1216
|
+
let fy = reader.getInt32(pos, false);
|
|
1217
|
+
pos += 4;
|
|
1218
|
+
for (let t = 0; t < ntri; t++) {
|
|
1219
|
+
let fz = reader.getInt32(pos, false);
|
|
1220
|
+
pos += 4;
|
|
1221
|
+
tris.push(fx);
|
|
1222
|
+
tris.push(fy);
|
|
1223
|
+
tris.push(fz);
|
|
1224
|
+
fy = fz;
|
|
1225
|
+
} //for each triangle
|
|
1226
|
+
} //for each polygon
|
|
1227
|
+
} else alert("Unsupported ASCII VTK datatype ", items[0]);
|
|
1228
|
+
var indices = new Int32Array(tris);
|
|
1229
|
+
return {
|
|
1230
|
+
positions,
|
|
1231
|
+
indices,
|
|
1232
|
+
};
|
|
1233
|
+
}; // readVTK()
|
|
1234
|
+
|
|
1235
|
+
NVMesh.readDFS = function (buffer, n_vert = 0) {
|
|
1236
|
+
//http://brainsuite.org/formats/dfs/
|
|
1237
|
+
//Does not play with other formats: vertex positions do not use Aneterior Commissure as origin
|
|
1238
|
+
var reader = new DataView(buffer);
|
|
1239
|
+
var magic = reader.getUint32(0, true); //"DFS_"
|
|
1240
|
+
var LE = reader.getUint16(4, true); //"LE"
|
|
1241
|
+
if (magic !== 1599292996 || LE !== 17740)
|
|
1242
|
+
console.log("Not a little-endian brainsuite DFS mesh");
|
|
1243
|
+
var hdrBytes = reader.getUint32(12, true);
|
|
1244
|
+
//var mdoffset = reader.getUint32(16, true);
|
|
1245
|
+
//var pdoffset = reader.getUint32(20, true);
|
|
1246
|
+
var nface = reader.getUint32(24, true); //number of triangles
|
|
1247
|
+
var nvert = reader.getUint32(28, true);
|
|
1248
|
+
//var nStrips = reader.getUint32(32, true); //deprecated
|
|
1249
|
+
//var stripSize = reader.getUint32(36, true); //deprecated
|
|
1250
|
+
//var normals = reader.getUint32(40, true);
|
|
1251
|
+
//var uvStart = reader.getUint32(44, true);
|
|
1252
|
+
var vcoffset = reader.getUint32(48, true); //vertexColor offset
|
|
1253
|
+
//var precision = reader.getUint32(52, true);
|
|
1254
|
+
// float64 orientation[4][4]; //4x4 matrix, affine transformation to world coordinates*)
|
|
1255
|
+
let pos = hdrBytes;
|
|
1256
|
+
let indices = new Int32Array(buffer, pos, nface * 3, true);
|
|
1257
|
+
pos += nface * 3 * 4;
|
|
1258
|
+
let positions = new Float32Array(buffer, pos, nvert * 3, true);
|
|
1259
|
+
//oops, triangle winding opposite of CCW convention
|
|
1260
|
+
for (var i = 0; i < nvert * 3; i += 3) {
|
|
1261
|
+
let tmp = positions[i];
|
|
1262
|
+
positions[i] = positions[i + 1];
|
|
1263
|
+
positions[i + 1] = tmp;
|
|
1264
|
+
}
|
|
1265
|
+
var colors = null;
|
|
1266
|
+
if (vcoffset >= 0)
|
|
1267
|
+
colors = new Float32Array(buffer, vcoffset, nvert * 3, true);
|
|
1268
|
+
return {
|
|
1269
|
+
positions,
|
|
1270
|
+
indices,
|
|
1271
|
+
colors,
|
|
1272
|
+
};
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
NVMesh.readMZ3 = function (buffer, n_vert = 0) {
|
|
1276
|
+
//ToDo: mz3 always little endian: support big endian? endian https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float32Array
|
|
1277
|
+
if (buffer.byteLength < 20)
|
|
1278
|
+
//76 for raw, not sure of gzip
|
|
1279
|
+
throw new Error("File too small to be mz3: bytes = " + buffer.byteLength);
|
|
1280
|
+
var reader = new DataView(buffer);
|
|
1281
|
+
//get number of vertices and faces
|
|
1282
|
+
var magic = reader.getUint16(0, true);
|
|
1283
|
+
var _buffer = buffer;
|
|
1284
|
+
if (magic === 35615 || magic === 8075) {
|
|
1285
|
+
//gzip signature 0x1F8B in little and big endian
|
|
1286
|
+
var raw = fflate.decompressSync(new Uint8Array(buffer));
|
|
1287
|
+
reader = new DataView(raw.buffer);
|
|
1288
|
+
var magic = reader.getUint16(0, true);
|
|
1289
|
+
_buffer = raw.buffer;
|
|
1290
|
+
//throw new Error( 'Gzip MZ3 file' );
|
|
1291
|
+
}
|
|
1292
|
+
var attr = reader.getUint16(2, true);
|
|
1293
|
+
var nface = reader.getUint32(4, true);
|
|
1294
|
+
var nvert = reader.getUint32(8, true);
|
|
1295
|
+
var nskip = reader.getUint32(12, true);
|
|
1296
|
+
log.debug(
|
|
1297
|
+
"MZ3 magic %d attr %d face %d vert %d skip %d",
|
|
1298
|
+
magic,
|
|
1299
|
+
attr,
|
|
1300
|
+
nface,
|
|
1301
|
+
nvert,
|
|
1302
|
+
nskip
|
|
1303
|
+
);
|
|
1304
|
+
if (magic != 23117) throw new Error("Invalid MZ3 file");
|
|
1305
|
+
var isFace = attr & 1;
|
|
1306
|
+
var isVert = attr & 2;
|
|
1307
|
+
var isRGBA = attr & 4;
|
|
1308
|
+
var isSCALAR = attr & 8;
|
|
1309
|
+
var isDOUBLE = attr & 16;
|
|
1310
|
+
var isAOMap = attr & 32;
|
|
1311
|
+
if (attr > 63) throw new Error("Unsupported future version of MZ3 file");
|
|
1312
|
+
if (nvert < 3) throw new Error("Not a mesh MZ3 file (maybe scalar)");
|
|
1313
|
+
if (n_vert > 0 && n_vert !== nvert) {
|
|
1314
|
+
console.log(
|
|
1315
|
+
"Layer has " + nvert + "vertices, but background mesh has " + n_vert
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
var filepos = 16 + nskip;
|
|
1319
|
+
var indices = null;
|
|
1320
|
+
if (isFace) {
|
|
1321
|
+
indices = new Int32Array(_buffer, filepos, nface * 3, true);
|
|
1322
|
+
filepos += nface * 3 * 4;
|
|
1323
|
+
}
|
|
1324
|
+
var positions = null;
|
|
1325
|
+
if (isVert) {
|
|
1326
|
+
positions = new Float32Array(_buffer, filepos, nvert * 3, true);
|
|
1327
|
+
filepos += nvert * 3 * 4;
|
|
1328
|
+
}
|
|
1329
|
+
var colors = null;
|
|
1330
|
+
if (isRGBA) {
|
|
1331
|
+
colors = new Float32Array(nvert * 3);
|
|
1332
|
+
var rgba8 = new Uint8Array(_buffer, filepos, nvert * 4, true);
|
|
1333
|
+
filepos += nvert * 4;
|
|
1334
|
+
var k3 = 0;
|
|
1335
|
+
var k4 = 0;
|
|
1336
|
+
for (var i = 0; i < nvert; i++) {
|
|
1337
|
+
for (var j = 0; j < 3; j++) {
|
|
1338
|
+
//for RGBA
|
|
1339
|
+
colors[k3] = rgba8[k4] / 255;
|
|
1340
|
+
k3++;
|
|
1341
|
+
k4++;
|
|
1342
|
+
}
|
|
1343
|
+
k4++; //skip Alpha
|
|
1344
|
+
} //for i
|
|
1345
|
+
} //if isRGBA
|
|
1346
|
+
let scalars = [];
|
|
1347
|
+
if (!isRGBA && isSCALAR) {
|
|
1348
|
+
let nFrame4D = Math.floor((_buffer.byteLength - filepos) / 4 / nvert);
|
|
1349
|
+
if (nFrame4D < 1) {
|
|
1350
|
+
console.log("MZ3 corrupted");
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
scalars = new Float32Array(_buffer, filepos, nFrame4D * nvert);
|
|
1354
|
+
filepos += nvert * 4;
|
|
1355
|
+
}
|
|
1356
|
+
if (n_vert > 0) return scalars;
|
|
1357
|
+
return {
|
|
1358
|
+
positions,
|
|
1359
|
+
indices,
|
|
1360
|
+
scalars,
|
|
1361
|
+
colors,
|
|
1362
|
+
};
|
|
1363
|
+
}; // readMZ3()
|
|
1364
|
+
|
|
1365
|
+
NVMesh.readLayer = function (
|
|
1366
|
+
name,
|
|
1367
|
+
buffer,
|
|
1368
|
+
nvmesh,
|
|
1369
|
+
opacity = 0.5,
|
|
1370
|
+
colorMap = "warm",
|
|
1371
|
+
colorMapNegative = "winter",
|
|
1372
|
+
useNegativeCmap = false,
|
|
1373
|
+
cal_min = null,
|
|
1374
|
+
cal_max = null
|
|
1375
|
+
) {
|
|
1376
|
+
let layer = [];
|
|
1377
|
+
let n_vert = nvmesh.vertexCount / 3; //each vertex has XYZ component
|
|
1378
|
+
if (n_vert < 3) return;
|
|
1379
|
+
var re = /(?:\.([^.]+))?$/;
|
|
1380
|
+
let ext = re.exec(name)[1];
|
|
1381
|
+
ext = ext.toUpperCase();
|
|
1382
|
+
if (ext === "GZ") {
|
|
1383
|
+
ext = re.exec(name.slice(0, -3))[1]; //img.trk.gz -> img.trk
|
|
1384
|
+
ext = ext.toUpperCase();
|
|
1385
|
+
}
|
|
1386
|
+
if (ext === "MZ3") layer.values = this.readMZ3(buffer, n_vert);
|
|
1387
|
+
else if (ext === "ANNOT") layer.values = this.readANNOT(buffer, n_vert);
|
|
1388
|
+
else if (ext === "CRV" || ext === "CURV")
|
|
1389
|
+
layer.values = this.readCURV(buffer, n_vert);
|
|
1390
|
+
else if (ext === "GII") layer.values = this.readGII(buffer, n_vert);
|
|
1391
|
+
else if (ext === "STC") layer.values = this.readSTC(buffer, n_vert);
|
|
1392
|
+
else {
|
|
1393
|
+
console.log("Unknown layer overlay format " + name);
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
if (!layer.values) return;
|
|
1397
|
+
layer.nFrame4D = layer.values.length / n_vert;
|
|
1398
|
+
layer.frame4D = 0;
|
|
1399
|
+
//determine global min..max
|
|
1400
|
+
let mn = layer.values[0];
|
|
1401
|
+
let mx = layer.values[0];
|
|
1402
|
+
for (var i = 0; i < layer.values.length; i++) {
|
|
1403
|
+
mn = Math.min(mn, layer.values[i]);
|
|
1404
|
+
mx = Math.max(mx, layer.values[i]);
|
|
1405
|
+
}
|
|
1406
|
+
layer.global_min = mn;
|
|
1407
|
+
layer.global_max = mx;
|
|
1408
|
+
layer.cal_min = cal_min;
|
|
1409
|
+
if (!cal_min) layer.cal_min = mn;
|
|
1410
|
+
layer.cal_max = cal_max;
|
|
1411
|
+
if (!cal_max) layer.cal_max = mx;
|
|
1412
|
+
layer.opacity = opacity;
|
|
1413
|
+
layer.colorMap = colorMap;
|
|
1414
|
+
layer.colorMapNegative = colorMapNegative;
|
|
1415
|
+
layer.useNegativeCmap = useNegativeCmap;
|
|
1416
|
+
nvmesh.layers.push(layer);
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
NVMesh.readOFF = function (buffer) {
|
|
1420
|
+
//https://en.wikipedia.org/wiki/OFF_(file_format)
|
|
1421
|
+
var enc = new TextDecoder("utf-8");
|
|
1422
|
+
var txt = enc.decode(buffer);
|
|
1423
|
+
//let txt = await response.text();
|
|
1424
|
+
var lines = txt.split("\n");
|
|
1425
|
+
var n = lines.length;
|
|
1426
|
+
let pts = [];
|
|
1427
|
+
let t = [];
|
|
1428
|
+
let i = 0;
|
|
1429
|
+
if (!lines[i].startsWith("OFF")) {
|
|
1430
|
+
console.log("File does not start with OFF");
|
|
1431
|
+
} else i++;
|
|
1432
|
+
let items = lines[i].split(" ");
|
|
1433
|
+
let num_v = parseInt(items[0]);
|
|
1434
|
+
let num_f = parseInt(items[1]);
|
|
1435
|
+
i++;
|
|
1436
|
+
for (let j = 0; j < num_v; j++) {
|
|
1437
|
+
let str = lines[i];
|
|
1438
|
+
items = str.split(" ");
|
|
1439
|
+
pts.push(parseFloat(items[0]));
|
|
1440
|
+
pts.push(parseFloat(items[1]));
|
|
1441
|
+
pts.push(parseFloat(items[2]));
|
|
1442
|
+
i++;
|
|
1443
|
+
}
|
|
1444
|
+
for (let j = 0; j < num_f; j++) {
|
|
1445
|
+
let str = lines[i];
|
|
1446
|
+
items = str.split(" ");
|
|
1447
|
+
let n = parseInt(items[0]);
|
|
1448
|
+
if (n !== 3)
|
|
1449
|
+
console.log("Only able to read OFF files with triangular meshes");
|
|
1450
|
+
t.push(parseInt(items[1]));
|
|
1451
|
+
t.push(parseInt(items[2]));
|
|
1452
|
+
t.push(parseInt(items[3]));
|
|
1453
|
+
i++;
|
|
1454
|
+
}
|
|
1455
|
+
var positions = new Float32Array(pts);
|
|
1456
|
+
var indices = new Int32Array(t);
|
|
1457
|
+
return {
|
|
1458
|
+
positions,
|
|
1459
|
+
indices,
|
|
1460
|
+
};
|
|
1461
|
+
}; // readOFF()
|
|
1462
|
+
|
|
1463
|
+
NVMesh.readOBJ = function (buffer) {
|
|
1464
|
+
//WaveFront OBJ format
|
|
1465
|
+
var enc = new TextDecoder("utf-8");
|
|
1466
|
+
var txt = enc.decode(buffer);
|
|
1467
|
+
//let txt = await response.text();
|
|
1468
|
+
var lines = txt.split("\n");
|
|
1469
|
+
var n = lines.length;
|
|
1470
|
+
let pts = [];
|
|
1471
|
+
let t = [];
|
|
1472
|
+
for (let i = 0; i < n; i++) {
|
|
1473
|
+
let str = lines[i];
|
|
1474
|
+
if (str[0] === "v" && str[1] === " ") {
|
|
1475
|
+
//'v ' but not 'vt' or 'vn'
|
|
1476
|
+
let items = str.split(" ");
|
|
1477
|
+
pts.push(parseFloat(items[1]));
|
|
1478
|
+
pts.push(parseFloat(items[2]));
|
|
1479
|
+
pts.push(parseFloat(items[3]));
|
|
1480
|
+
//v 0 -0.5 -0
|
|
1481
|
+
}
|
|
1482
|
+
if (str[0] === "f") {
|
|
1483
|
+
let items = str.split(" ");
|
|
1484
|
+
let tn = items[1].split("/");
|
|
1485
|
+
t.push(parseInt(tn - 1));
|
|
1486
|
+
tn = items[2].split("/");
|
|
1487
|
+
t.push(parseInt(tn - 1));
|
|
1488
|
+
tn = items[3].split("/");
|
|
1489
|
+
t.push(parseInt(tn - 1));
|
|
1490
|
+
}
|
|
1491
|
+
} //for all lines
|
|
1492
|
+
var positions = new Float32Array(pts);
|
|
1493
|
+
var indices = new Int32Array(t);
|
|
1494
|
+
return {
|
|
1495
|
+
positions,
|
|
1496
|
+
indices,
|
|
1497
|
+
};
|
|
1498
|
+
}; // readOBJ()
|
|
1499
|
+
|
|
1500
|
+
NVMesh.readFreeSurfer = function (buffer) {
|
|
1501
|
+
const view = new DataView(buffer); //ArrayBuffer to dataview
|
|
1502
|
+
//ALWAYS big endian
|
|
1503
|
+
let sig0 = view.getUint32(0, false);
|
|
1504
|
+
let sig1 = view.getUint32(4, false);
|
|
1505
|
+
if (sig0 !== 4294966883 || sig1 !== 1919246708)
|
|
1506
|
+
log.debug(
|
|
1507
|
+
"Unable to recognize file type: does not appear to be FreeSurfer format."
|
|
1508
|
+
);
|
|
1509
|
+
let offset = 0;
|
|
1510
|
+
while (view.getUint8(offset) !== 10) offset++;
|
|
1511
|
+
offset += 2;
|
|
1512
|
+
let nv = view.getUint32(offset, false); //number of vertices
|
|
1513
|
+
offset += 4;
|
|
1514
|
+
let nf = view.getUint32(offset, false); //number of faces
|
|
1515
|
+
offset += 4;
|
|
1516
|
+
nv *= 3; //each vertex has 3 positions: XYZ
|
|
1517
|
+
var positions = new Float32Array(nv);
|
|
1518
|
+
for (let i = 0; i < nv; i++) {
|
|
1519
|
+
positions[i] = view.getFloat32(offset, false);
|
|
1520
|
+
offset += 4;
|
|
1521
|
+
}
|
|
1522
|
+
nf *= 3; //each triangle face indexes 3 triangles
|
|
1523
|
+
var indices = new Int32Array(nf);
|
|
1524
|
+
for (let i = 0; i < nf; i++) {
|
|
1525
|
+
indices[i] = view.getUint32(offset, false);
|
|
1526
|
+
offset += 4;
|
|
1527
|
+
}
|
|
1528
|
+
return {
|
|
1529
|
+
positions,
|
|
1530
|
+
indices,
|
|
1531
|
+
};
|
|
1532
|
+
}; // readFreeSurfer()
|
|
1533
|
+
|
|
1534
|
+
NVMesh.readSRF = function (buffer) {
|
|
1535
|
+
//https://support.brainvoyager.com/brainvoyager/automation-development/84-file-formats/344-users-guide-2-3-the-format-of-srf-files
|
|
1536
|
+
var bytes = new Uint8Array(buffer);
|
|
1537
|
+
if (bytes[0] === 31 && bytes[1] === 139) {
|
|
1538
|
+
// handle .srf.gz
|
|
1539
|
+
var raw = fflate.decompressSync(new Uint8Array(buffer));
|
|
1540
|
+
buffer = raw.buffer;
|
|
1541
|
+
}
|
|
1542
|
+
var reader = new DataView(buffer);
|
|
1543
|
+
let ver = reader.getFloat32(0, true);
|
|
1544
|
+
let nVert = reader.getUint32(8, true);
|
|
1545
|
+
let nTri = reader.getUint32(12, true);
|
|
1546
|
+
let oriX = reader.getFloat32(16, true);
|
|
1547
|
+
let oriY = reader.getFloat32(20, true);
|
|
1548
|
+
let oriZ = reader.getFloat32(24, true);
|
|
1549
|
+
var positions = new Float32Array(nVert * 3);
|
|
1550
|
+
//BrainVoyager does not use Talairach coordinates for XYZ!
|
|
1551
|
+
//read X component of each vertex
|
|
1552
|
+
let pos = 28;
|
|
1553
|
+
let j = 1; //BrainVoyager X is Talairach Y
|
|
1554
|
+
for (var i = 0; i < nVert; i++) {
|
|
1555
|
+
positions[j] = -reader.getFloat32(pos, true) + oriX;
|
|
1556
|
+
j += 3; //read one of 3 components: XYZ
|
|
1557
|
+
pos += 4; //read one float32
|
|
1558
|
+
}
|
|
1559
|
+
//read Y component of each vertex
|
|
1560
|
+
j = 2; //BrainVoyager Y is Talairach Z
|
|
1561
|
+
for (var i = 0; i < nVert; i++) {
|
|
1562
|
+
positions[j] = -reader.getFloat32(pos, true) + oriY;
|
|
1563
|
+
j += 3; //read one of 3 components: XYZ
|
|
1564
|
+
pos += 4; //read one float32
|
|
1565
|
+
}
|
|
1566
|
+
//read Z component of each vertex
|
|
1567
|
+
j = 0; //BrainVoyager Z is Talairach X
|
|
1568
|
+
for (var i = 0; i < nVert; i++) {
|
|
1569
|
+
positions[j] = -reader.getFloat32(pos, true) + oriZ;
|
|
1570
|
+
j += 3; //read one of 3 components: XYZ
|
|
1571
|
+
pos += 4; //read one float32
|
|
1572
|
+
}
|
|
1573
|
+
//not sure why normals are stored, does bulk up file size
|
|
1574
|
+
pos = 28 + 4 * 6 * nVert; //each vertex has 6 float32s: XYZ for position and normal
|
|
1575
|
+
//read concave and convex colors:
|
|
1576
|
+
let rVex = reader.getFloat32(pos, true);
|
|
1577
|
+
let gVex = reader.getFloat32(pos + 4, true);
|
|
1578
|
+
let bVex = reader.getFloat32(pos + 8, true);
|
|
1579
|
+
let rCave = reader.getFloat32(pos + 16, true);
|
|
1580
|
+
let gCave = reader.getFloat32(pos + 20, true);
|
|
1581
|
+
let bCave = reader.getFloat32(pos + 24, true);
|
|
1582
|
+
pos += 8 * 4; //skip 8 floats (RGBA convex/concave)
|
|
1583
|
+
//read per-vertex colors
|
|
1584
|
+
let colors = new Float32Array(nVert * 3);
|
|
1585
|
+
let colorsIdx = new Uint32Array(buffer, pos, nVert, true);
|
|
1586
|
+
j = 0; //convert RGBA -> RGB
|
|
1587
|
+
for (var i = 0; i < nVert; i++) {
|
|
1588
|
+
let c = colorsIdx[i];
|
|
1589
|
+
if (c > 1056964608) {
|
|
1590
|
+
colors[j + 0] = ((c >> 16) & 0xff) / 255;
|
|
1591
|
+
colors[j + 1] = ((c >> 8) & 0xff) / 255;
|
|
1592
|
+
colors[j + 2] = (c & 0xff) / 255;
|
|
1593
|
+
}
|
|
1594
|
+
if (c === 0) {
|
|
1595
|
+
//convex
|
|
1596
|
+
colors[j + 0] = rVex;
|
|
1597
|
+
colors[j + 1] = gVex;
|
|
1598
|
+
colors[j + 2] = bVex;
|
|
1599
|
+
}
|
|
1600
|
+
if (c === 1) {
|
|
1601
|
+
//concave
|
|
1602
|
+
colors[j + 0] = rCave;
|
|
1603
|
+
colors[j + 1] = gCave;
|
|
1604
|
+
colors[j + 2] = bCave;
|
|
1605
|
+
}
|
|
1606
|
+
j += 3;
|
|
1607
|
+
}
|
|
1608
|
+
pos += nVert * 4; // MeshColor, sequence of color indices
|
|
1609
|
+
//not sure why nearest neighbors are stored, slower and bigger files
|
|
1610
|
+
for (var i = 0; i < nVert; i++) {
|
|
1611
|
+
let nNearest = reader.getUint32(pos, true);
|
|
1612
|
+
pos += 4 + 4 * nNearest;
|
|
1613
|
+
}
|
|
1614
|
+
var indices = new Int32Array(nTri * 3);
|
|
1615
|
+
for (var i = 0; i < nTri * 3; i++) {
|
|
1616
|
+
indices[i] = reader.getInt32(pos, true);
|
|
1617
|
+
pos += 4;
|
|
1618
|
+
}
|
|
1619
|
+
if (ver !== 4) console.log("Not valid SRF");
|
|
1620
|
+
|
|
1621
|
+
return {
|
|
1622
|
+
positions,
|
|
1623
|
+
indices,
|
|
1624
|
+
colors,
|
|
1625
|
+
};
|
|
1626
|
+
}; // readSRF()
|
|
1627
|
+
|
|
1628
|
+
NVMesh.readSTL = function (buffer) {
|
|
1629
|
+
if (buffer.byteLength < 80 + 4 + 50)
|
|
1630
|
+
throw new Error("File too small to be STL: bytes = " + buffer.byteLength);
|
|
1631
|
+
var reader = new DataView(buffer);
|
|
1632
|
+
let sig = reader.getUint32(80, true);
|
|
1633
|
+
if (sig === 1768714099)
|
|
1634
|
+
throw new Error("Only able to read binary (not ASCII) STL files.");
|
|
1635
|
+
var ntri = reader.getUint32(80, true);
|
|
1636
|
+
let ntri3 = 3 * ntri;
|
|
1637
|
+
if (buffer.byteLength < 80 + 4 + ntri * 50)
|
|
1638
|
+
throw new Error("STL file too small to store triangles = ", ntri);
|
|
1639
|
+
var indices = new Int32Array(ntri3);
|
|
1640
|
+
var positions = new Float32Array(ntri3 * 3);
|
|
1641
|
+
let pos = 80 + 4 + 12;
|
|
1642
|
+
let v = 0; //vertex
|
|
1643
|
+
for (var i = 0; i < ntri; i++) {
|
|
1644
|
+
for (var j = 0; j < 9; j++) {
|
|
1645
|
+
positions[v] = reader.getFloat32(pos, true);
|
|
1646
|
+
v += 1;
|
|
1647
|
+
pos += 4;
|
|
1648
|
+
}
|
|
1649
|
+
pos += 14; //50 bytes for triangle, only 36 used for position
|
|
1650
|
+
}
|
|
1651
|
+
for (var i = 0; i < ntri3; i++) indices[i] = i;
|
|
1652
|
+
return {
|
|
1653
|
+
positions,
|
|
1654
|
+
indices,
|
|
1655
|
+
};
|
|
1656
|
+
}; // readSTL()
|
|
1657
|
+
|
|
1658
|
+
NVMesh.readGII = function (buffer, n_vert = 0) {
|
|
1659
|
+
var enc = new TextDecoder("utf-8");
|
|
1660
|
+
var xmlStr = enc.decode(buffer);
|
|
1661
|
+
let gii = gifti.parse(xmlStr);
|
|
1662
|
+
if (n_vert > 0) {
|
|
1663
|
+
//add as overlay layer
|
|
1664
|
+
if (gii.dataArrays.length < 0) {
|
|
1665
|
+
console.log("Not a valid GIfTI overlay");
|
|
1666
|
+
}
|
|
1667
|
+
let scalars = [];
|
|
1668
|
+
for (var i = 0; i < gii.dataArrays.length; i++) {
|
|
1669
|
+
let layer = gii.dataArrays[i];
|
|
1670
|
+
if (n_vert !== layer.getNumElements()) {
|
|
1671
|
+
console.log(
|
|
1672
|
+
"Number of vertices of overlay layer does not match mesh " +
|
|
1673
|
+
n_vert +
|
|
1674
|
+
" vs " +
|
|
1675
|
+
getNumElements()
|
|
1676
|
+
);
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
if (layer.isColors()) console.log("TODO: check color mesh layers");
|
|
1680
|
+
let scalarsI = new Float32Array(layer.getData());
|
|
1681
|
+
scalars.push(...scalarsI);
|
|
1682
|
+
}
|
|
1683
|
+
return scalars;
|
|
1684
|
+
}
|
|
1685
|
+
if (gii.getNumTriangles() === 0 || gii.getNumPoints() === 0) {
|
|
1686
|
+
console.log("Not a GIfTI mesh (perhaps an overlay layer)");
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
var positions = gii.getPointsDataArray().getData();
|
|
1690
|
+
var indices = gii.getTrianglesDataArray().getData();
|
|
1691
|
+
//next: ColumnMajorOrder https://github.com/rii-mango/GIFTI-Reader-JS/issues/2
|
|
1692
|
+
if (
|
|
1693
|
+
gii.getPointsDataArray().attributes.ArrayIndexingOrder ===
|
|
1694
|
+
"ColumnMajorOrder"
|
|
1695
|
+
) {
|
|
1696
|
+
//transpose points, xx..xyy..yzz..z -> xyzxyz..
|
|
1697
|
+
let ps = positions.slice();
|
|
1698
|
+
let np = ps.length / 3;
|
|
1699
|
+
let j = 0;
|
|
1700
|
+
for (var p = 0; p < np; p++)
|
|
1701
|
+
for (var i = 0; i < 3; i++) {
|
|
1702
|
+
positions[j] = ps[i * np + p];
|
|
1703
|
+
j++;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
if (
|
|
1707
|
+
gii.getTrianglesDataArray().attributes.ArrayIndexingOrder ===
|
|
1708
|
+
"ColumnMajorOrder"
|
|
1709
|
+
) {
|
|
1710
|
+
//transpose indices, xx..xyy..yzz..z -> xyzxyz..
|
|
1711
|
+
let ps = indices.slice();
|
|
1712
|
+
let np = ps.length / 3;
|
|
1713
|
+
let j = 0;
|
|
1714
|
+
for (var p = 0; p < np; p++)
|
|
1715
|
+
for (var i = 0; i < 3; i++) {
|
|
1716
|
+
indices[j] = ps[i * np + p];
|
|
1717
|
+
j++;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
return {
|
|
1721
|
+
positions,
|
|
1722
|
+
indices,
|
|
1723
|
+
};
|
|
1724
|
+
}; // readGII()
|
|
1725
|
+
|
|
1726
|
+
NVMesh.loadConnectomeFromJSON = async function (
|
|
1727
|
+
json,
|
|
1728
|
+
gl,
|
|
1729
|
+
name = "",
|
|
1730
|
+
colorMap = "",
|
|
1731
|
+
opacity = 1.0,
|
|
1732
|
+
visible = true
|
|
1733
|
+
) {
|
|
1734
|
+
if (json.hasOwnProperty("name")) name = json.name;
|
|
1735
|
+
return new NVMesh([], [], name, [], opacity, visible, gl, json);
|
|
1736
|
+
}; //loadConnectomeFromJSON()
|
|
1737
|
+
|
|
1738
|
+
NVMesh.readMesh = async function (
|
|
1739
|
+
buffer,
|
|
1740
|
+
name,
|
|
1741
|
+
gl,
|
|
1742
|
+
opacity = 1.0,
|
|
1743
|
+
rgba255 = [255, 255, 255, 255],
|
|
1744
|
+
visible = true
|
|
1745
|
+
) {
|
|
1746
|
+
let nvmesh = null;
|
|
1747
|
+
let tris = [];
|
|
1748
|
+
let pts = [];
|
|
1749
|
+
let obj = [];
|
|
1750
|
+
var re = /(?:\.([^.]+))?$/;
|
|
1751
|
+
let ext = re.exec(name)[1];
|
|
1752
|
+
ext = ext.toUpperCase();
|
|
1753
|
+
if (ext === "GZ") {
|
|
1754
|
+
ext = re.exec(name.slice(0, -3))[1]; //img.trk.gz -> img.trk
|
|
1755
|
+
ext = ext.toUpperCase();
|
|
1756
|
+
}
|
|
1757
|
+
if (ext === "TCK" || ext === "TRK" || ext === "TRX") {
|
|
1758
|
+
if (ext === "TCK") obj = this.readTCK(buffer);
|
|
1759
|
+
else if (ext === "TRX") obj = await this.readTRX(buffer);
|
|
1760
|
+
else obj = this.readTRK(buffer);
|
|
1761
|
+
//let offsetPt0 = new Int32Array(obj.offsetPt0.slice());
|
|
1762
|
+
//let pts = new Float32Array(obj.pts.slice());
|
|
1763
|
+
let offsetPt0 = new Int32Array(obj.offsetPt0.slice());
|
|
1764
|
+
let pts = new Float32Array(obj.pts.slice());
|
|
1765
|
+
if (!obj.hasOwnProperty("dpg")) obj.dpg = null;
|
|
1766
|
+
if (!obj.hasOwnProperty("dps")) obj.dps = null;
|
|
1767
|
+
if (!obj.hasOwnProperty("dpv")) obj.dpv = null;
|
|
1768
|
+
return new NVMesh(
|
|
1769
|
+
pts,
|
|
1770
|
+
offsetPt0,
|
|
1771
|
+
name,
|
|
1772
|
+
null, //colorMap,
|
|
1773
|
+
opacity, //opacity,
|
|
1774
|
+
visible, //visible,
|
|
1775
|
+
gl,
|
|
1776
|
+
"inferno",
|
|
1777
|
+
obj.dpg,
|
|
1778
|
+
obj.dps,
|
|
1779
|
+
obj.dpv
|
|
1780
|
+
);
|
|
1781
|
+
} //is fibers
|
|
1782
|
+
if (ext === "GII") {
|
|
1783
|
+
obj = this.readGII(buffer);
|
|
1784
|
+
} else if (ext === "MZ3") obj = this.readMZ3(buffer);
|
|
1785
|
+
else if (ext === "DFS") obj = this.readDFS(buffer);
|
|
1786
|
+
else if (ext === "OFF") obj = this.readOFF(buffer);
|
|
1787
|
+
else if (ext === "OBJ") obj = this.readOBJ(buffer);
|
|
1788
|
+
else if (ext === "FIB" || ext === "VTK") {
|
|
1789
|
+
obj = this.readVTK(buffer);
|
|
1790
|
+
if (obj.hasOwnProperty("offsetPt0")) {
|
|
1791
|
+
//VTK files used both for meshes and streamlines
|
|
1792
|
+
let offsetPt0 = new Int32Array(obj.offsetPt0.slice());
|
|
1793
|
+
let pts = new Float32Array(obj.pts.slice());
|
|
1794
|
+
return new NVMesh(
|
|
1795
|
+
pts,
|
|
1796
|
+
offsetPt0,
|
|
1797
|
+
name,
|
|
1798
|
+
null, //colorMap,
|
|
1799
|
+
opacity, //opacity,
|
|
1800
|
+
visible, //visible,
|
|
1801
|
+
gl,
|
|
1802
|
+
"inferno"
|
|
1803
|
+
);
|
|
1804
|
+
} //if streamlines, not mesh
|
|
1805
|
+
} else if (ext === "SRF") {
|
|
1806
|
+
obj = this.readSRF(buffer);
|
|
1807
|
+
} else if (ext === "STL") {
|
|
1808
|
+
obj = this.readSTL(buffer);
|
|
1809
|
+
} else {
|
|
1810
|
+
obj = this.readFreeSurfer(buffer);
|
|
1811
|
+
} // freesurfer hail mary
|
|
1812
|
+
pts = obj.positions.slice();
|
|
1813
|
+
tris = obj.indices.slice();
|
|
1814
|
+
if (obj.colors && obj.colors.length === pts.length) {
|
|
1815
|
+
rgba255 = [];
|
|
1816
|
+
let n = pts.length / 3;
|
|
1817
|
+
let c = 0;
|
|
1818
|
+
for (let i = 0; i < n; i++) {
|
|
1819
|
+
//convert ThreeJS unit RGB to RGBA255
|
|
1820
|
+
rgba255.push(obj.colors[c] * 255); //red
|
|
1821
|
+
rgba255.push(obj.colors[c + 1] * 255); //green
|
|
1822
|
+
rgba255.push(obj.colors[c + 2] * 255); //blue
|
|
1823
|
+
rgba255.push(255); //alpha
|
|
1824
|
+
c += 3;
|
|
1825
|
+
} //for i: each vertex
|
|
1826
|
+
} //obj includes colors
|
|
1827
|
+
let npt = pts.length / 3;
|
|
1828
|
+
let ntri = tris.length / 3;
|
|
1829
|
+
if (ntri < 1 || npt < 3) {
|
|
1830
|
+
alert("Mesh should have at least one triangle and three vertices");
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
if (tris.constructor !== Int32Array) {
|
|
1834
|
+
alert("Expected triangle indices to be of type INT32");
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
let nvm = new NVMesh(
|
|
1838
|
+
pts,
|
|
1839
|
+
tris,
|
|
1840
|
+
name,
|
|
1841
|
+
rgba255, //colorMap,
|
|
1842
|
+
opacity, //opacity,
|
|
1843
|
+
visible, //visible,
|
|
1844
|
+
gl
|
|
1845
|
+
);
|
|
1846
|
+
if (obj.hasOwnProperty("scalars") && obj.scalars.length > 0) {
|
|
1847
|
+
this.readLayer(name, buffer, nvm, opacity, "gray");
|
|
1848
|
+
nvm.updateMesh(gl);
|
|
1849
|
+
}
|
|
1850
|
+
return nvm;
|
|
1851
|
+
};
|
|
1852
|
+
|
|
1853
|
+
//https://stackoverflow.com/questions/55798396/how-do-i-make-a-nested-loop-continue-only-after-a-asynchronous-function-has-been
|
|
1854
|
+
var pow = Math.pow;
|
|
1855
|
+
function decodeFloat16(binary) {
|
|
1856
|
+
"use strict";
|
|
1857
|
+
var exponent = (binary & 0x7c00) >> 10,
|
|
1858
|
+
fraction = binary & 0x03ff;
|
|
1859
|
+
return (
|
|
1860
|
+
(binary >> 15 ? -1 : 1) *
|
|
1861
|
+
(exponent
|
|
1862
|
+
? exponent === 0x1f
|
|
1863
|
+
? fraction
|
|
1864
|
+
? NaN
|
|
1865
|
+
: Infinity
|
|
1866
|
+
: pow(2, exponent - 15) * (1 + fraction / 0x400)
|
|
1867
|
+
: 6.103515625e-5 * (fraction / 0x400))
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
NVMesh.readTRX = async function (buffer) {
|
|
1872
|
+
//Javascript does not support float16, so we convert to float32
|
|
1873
|
+
//https://stackoverflow.com/questions/5678432/decompressing-half-precision-floats-in-javascript
|
|
1874
|
+
function decodeFloat16(binary) {
|
|
1875
|
+
"use strict";
|
|
1876
|
+
var exponent = (binary & 0x7c00) >> 10,
|
|
1877
|
+
fraction = binary & 0x03ff;
|
|
1878
|
+
return (
|
|
1879
|
+
(binary >> 15 ? -1 : 1) *
|
|
1880
|
+
(exponent
|
|
1881
|
+
? exponent === 0x1f
|
|
1882
|
+
? fraction
|
|
1883
|
+
? NaN
|
|
1884
|
+
: Infinity
|
|
1885
|
+
: Math.pow(2, exponent - 15) * (1 + fraction / 0x400)
|
|
1886
|
+
: 6.103515625e-5 * (fraction / 0x400))
|
|
1887
|
+
);
|
|
1888
|
+
} // decodeFloat16()
|
|
1889
|
+
let noff = 0;
|
|
1890
|
+
let npt = 0;
|
|
1891
|
+
let pts = [];
|
|
1892
|
+
let offsetPt0 = [];
|
|
1893
|
+
let dpg = [];
|
|
1894
|
+
let dps = [];
|
|
1895
|
+
let dpv = [];
|
|
1896
|
+
let header = [];
|
|
1897
|
+
let isOverflowUint64 = false;
|
|
1898
|
+
let data = [];
|
|
1899
|
+
/*if (urlIsLocalFile) {
|
|
1900
|
+
data = fs.readFileSync(url);
|
|
1901
|
+
} else {
|
|
1902
|
+
let response = await fetch(url);
|
|
1903
|
+
if (!response.ok) throw Error(response.statusText);
|
|
1904
|
+
data = await response.arrayBuffer();
|
|
1905
|
+
}*/
|
|
1906
|
+
const decompressed = fflate.unzipSync(new Uint8Array(buffer), {
|
|
1907
|
+
filter(file) {
|
|
1908
|
+
return file.originalSize > 0;
|
|
1909
|
+
},
|
|
1910
|
+
});
|
|
1911
|
+
var keys = Object.keys(decompressed);
|
|
1912
|
+
for (var i = 0, len = keys.length; i < len; i++) {
|
|
1913
|
+
let parts = keys[i].split("/");
|
|
1914
|
+
let fname = parts.slice(-1)[0]; // my.trx/dpv/fx.float32 -> fx.float32
|
|
1915
|
+
if (fname.startsWith(".")) continue;
|
|
1916
|
+
let pname = parts.slice(-2)[0]; // my.trx/dpv/fx.float32 -> dpv
|
|
1917
|
+
let tag = fname.split(".")[0]; // "positions.3.float16 -> "positions"
|
|
1918
|
+
//todo: should tags be censored for invalide characters: https://stackoverflow.com/questions/8676011/which-characters-are-valid-invalid-in-a-json-key-name
|
|
1919
|
+
let data = decompressed[keys[i]];
|
|
1920
|
+
if (fname.includes("header.json")) {
|
|
1921
|
+
let jsonString = new TextDecoder().decode(data);
|
|
1922
|
+
header = JSON.parse(jsonString);
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
//next read arrays for all possible datatypes: int8/16/32/64 uint8/16/32/64 float16/32/64
|
|
1926
|
+
let nval = 0;
|
|
1927
|
+
let vals = [];
|
|
1928
|
+
if (fname.endsWith(".uint64") || fname.endsWith(".int64")) {
|
|
1929
|
+
//javascript does not have 64-bit integers! read lower 32-bits
|
|
1930
|
+
//note for signed int64 we only read unsigned bytes
|
|
1931
|
+
//for both signed and unsigned, generate an error if any value is out of bounds
|
|
1932
|
+
//one alternative might be to convert to 64-bit double that has a flintmax of 2^53.
|
|
1933
|
+
nval = data.length / 8; //8 bytes per 64bit input
|
|
1934
|
+
vals = new Uint32Array(nval);
|
|
1935
|
+
var u32 = new Uint32Array(data.buffer);
|
|
1936
|
+
let j = 0;
|
|
1937
|
+
for (let i = 0; i < nval; i++) {
|
|
1938
|
+
vals[i] = u32[j];
|
|
1939
|
+
if (u32[j + 1] !== 0) isOverflowUint64 = true;
|
|
1940
|
+
j += 2;
|
|
1941
|
+
}
|
|
1942
|
+
} else if (fname.endsWith(".uint32")) {
|
|
1943
|
+
vals = new Uint32Array(data.buffer);
|
|
1944
|
+
} else if (fname.endsWith(".uint16")) {
|
|
1945
|
+
vals = new Uint16Array(data.buffer);
|
|
1946
|
+
} else if (fname.endsWith(".uint8")) {
|
|
1947
|
+
vals = new Uint8Array(data.buffer);
|
|
1948
|
+
} else if (fname.endsWith(".int32")) {
|
|
1949
|
+
vals = new Int32Array(data.buffer);
|
|
1950
|
+
} else if (fname.endsWith(".int16")) {
|
|
1951
|
+
vals = new Int16Array(data.buffer);
|
|
1952
|
+
} else if (fname.endsWith(".int8")) {
|
|
1953
|
+
vals = new Int8Array(data.buffer);
|
|
1954
|
+
} else if (fname.endsWith(".float64")) {
|
|
1955
|
+
vals = new Float64Array(data.buffer);
|
|
1956
|
+
} else if (fname.endsWith(".float32")) {
|
|
1957
|
+
vals = new Float32Array(data.buffer);
|
|
1958
|
+
} else if (fname.endsWith(".float16")) {
|
|
1959
|
+
//javascript does not have 16-bit floats! Convert to 32-bits
|
|
1960
|
+
nval = data.length / 2; //2 bytes per 16bit input
|
|
1961
|
+
vals = new Float32Array(nval);
|
|
1962
|
+
var u16 = new Uint16Array(data.buffer);
|
|
1963
|
+
for (let i = 0; i < nval; i++) vals[i] = decodeFloat16(u16[i]);
|
|
1964
|
+
} else continue; //not a data array
|
|
1965
|
+
nval = vals.length;
|
|
1966
|
+
//next: read data_per_group
|
|
1967
|
+
if (pname.includes("dpg")) {
|
|
1968
|
+
dpg.push({
|
|
1969
|
+
id: tag,
|
|
1970
|
+
vals: vals.slice(),
|
|
1971
|
+
});
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1974
|
+
//next: read data_per_vertex
|
|
1975
|
+
if (pname.includes("dpv")) {
|
|
1976
|
+
dpv.push({
|
|
1977
|
+
id: tag,
|
|
1978
|
+
vals: vals.slice(),
|
|
1979
|
+
});
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
//next: read data_per_streamline
|
|
1983
|
+
if (pname.includes("dps")) {
|
|
1984
|
+
dps.push({
|
|
1985
|
+
id: tag,
|
|
1986
|
+
vals: vals.slice(),
|
|
1987
|
+
});
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
//Next: read offsets: Always uint64
|
|
1991
|
+
if (fname.startsWith("offsets.")) {
|
|
1992
|
+
//javascript does not have 64-bit integers! read lower 32-bits
|
|
1993
|
+
noff = nval; //8 bytes per 64bit input
|
|
1994
|
+
//we need to solve the fence post problem, so we can not use slice
|
|
1995
|
+
offsetPt0 = new Uint32Array(nval + 1);
|
|
1996
|
+
for (let i = 0; i < nval; i++) offsetPt0[i] = vals[i];
|
|
1997
|
+
}
|
|
1998
|
+
if (fname.startsWith("positions.3.")) {
|
|
1999
|
+
npt = nval; //4 bytes per 32bit input
|
|
2000
|
+
pts = vals.slice();
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
if (noff === 0 || npt === 0) alert("Failure reading TRX format");
|
|
2004
|
+
if (isOverflowUint64)
|
|
2005
|
+
alert("Too many vertices: JavaScript does not support 64 bit integers");
|
|
2006
|
+
offsetPt0[noff] = npt / 3; //solve fence post problem, offset for final streamline
|
|
2007
|
+
return {
|
|
2008
|
+
pts,
|
|
2009
|
+
offsetPt0,
|
|
2010
|
+
dpg,
|
|
2011
|
+
dps,
|
|
2012
|
+
dpv,
|
|
2013
|
+
header,
|
|
2014
|
+
};
|
|
2015
|
+
}; // readTRX()
|
|
2016
|
+
|
|
2017
|
+
/**
|
|
2018
|
+
* factory function to load and return a new NVMesh instance from a given URL
|
|
2019
|
+
* @param {string} url the resolvable URL pointing to a nifti image to load
|
|
2020
|
+
* @param {string} [name=''] a name for this image. Default is an empty string
|
|
2021
|
+
* @param {string} [colorMap='gray'] a color map to use. default is gray
|
|
2022
|
+
* @param {number} [opacity=1.0] the opacity for this image. default is 1
|
|
2023
|
+
* @param {boolean} [visible=true] whether or not this image is to be visible
|
|
2024
|
+
* @returns {NVMesh} returns a NVImage intance
|
|
2025
|
+
* @example
|
|
2026
|
+
* myImage = NVMesh.loadFromUrl('./someURL/mesh.gii') // must be served from a server (local or remote)
|
|
2027
|
+
*/
|
|
2028
|
+
NVMesh.loadFromUrl = async function ({
|
|
2029
|
+
url = "",
|
|
2030
|
+
gl = null,
|
|
2031
|
+
name = "",
|
|
2032
|
+
opacity = 1.0,
|
|
2033
|
+
rgba255 = [255, 255, 255, 255],
|
|
2034
|
+
visible = true,
|
|
2035
|
+
layers = [],
|
|
2036
|
+
} = {}) {
|
|
2037
|
+
let urlParts = url.split("/"); // split url parts at slash
|
|
2038
|
+
name = urlParts.slice(-1)[0]; // name will be last part of url (e.g. some/url/image.nii.gz --> image.nii.gz)
|
|
2039
|
+
if (url === "") throw Error("url must not be empty");
|
|
2040
|
+
if (gl === null) throw Error("gl context is null");
|
|
2041
|
+
//TRX format is special (its a zip archive of multiple files)
|
|
2042
|
+
let response = await fetch(url);
|
|
2043
|
+
if (!response.ok) throw Error(response.statusText);
|
|
2044
|
+
let tris = [];
|
|
2045
|
+
var pts = [];
|
|
2046
|
+
let buffer = await response.arrayBuffer();
|
|
2047
|
+
let nvmesh = await this.readMesh(buffer, name, gl, opacity, rgba255, visible);
|
|
2048
|
+
if (!layers || layers.length < 1) return nvmesh;
|
|
2049
|
+
for (let i = 0; i < layers.length; i++) {
|
|
2050
|
+
response = await fetch(layers[i].url);
|
|
2051
|
+
if (!response.ok) throw Error(response.statusText);
|
|
2052
|
+
buffer = await response.arrayBuffer();
|
|
2053
|
+
urlParts = layers[i].url.split("/");
|
|
2054
|
+
let opacity = 0.5;
|
|
2055
|
+
if (layers[i].hasOwnProperty("opacity")) opacity = layers[i].opacity;
|
|
2056
|
+
let colorMap = "warm";
|
|
2057
|
+
if (layers[i].hasOwnProperty("colorMap")) colorMap = layers[i].colorMap;
|
|
2058
|
+
let colorMapNegative = "winter";
|
|
2059
|
+
if (layers[i].hasOwnProperty("colorMapNegative"))
|
|
2060
|
+
colorMapNegative = layers[i].colorMapNegative;
|
|
2061
|
+
let useNegativeCmap = false;
|
|
2062
|
+
if (layers[i].hasOwnProperty("useNegativeCmap"))
|
|
2063
|
+
useNegativeCmap = layers[i].useNegativeCmap;
|
|
2064
|
+
let cal_min = null;
|
|
2065
|
+
if (layers[i].hasOwnProperty("cal_min")) cal_min = layers[i].cal_min;
|
|
2066
|
+
let cal_max = null;
|
|
2067
|
+
if (layers[i].hasOwnProperty("cal_max")) cal_max = layers[i].cal_max;
|
|
2068
|
+
|
|
2069
|
+
this.readLayer(
|
|
2070
|
+
urlParts.slice(-1)[0],
|
|
2071
|
+
buffer,
|
|
2072
|
+
nvmesh,
|
|
2073
|
+
opacity,
|
|
2074
|
+
colorMap,
|
|
2075
|
+
colorMapNegative,
|
|
2076
|
+
useNegativeCmap,
|
|
2077
|
+
cal_min,
|
|
2078
|
+
cal_max
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
nvmesh.updateMesh(gl); //apply the new properties...
|
|
2082
|
+
return nvmesh;
|
|
2083
|
+
};
|
|
2084
|
+
|
|
2085
|
+
// not included in public docs
|
|
2086
|
+
// loading Nifti files
|
|
2087
|
+
NVMesh.readFileAsync = function (file) {
|
|
2088
|
+
return new Promise((resolve, reject) => {
|
|
2089
|
+
let reader = new FileReader();
|
|
2090
|
+
|
|
2091
|
+
reader.onload = () => {
|
|
2092
|
+
resolve(reader.result);
|
|
2093
|
+
};
|
|
2094
|
+
|
|
2095
|
+
reader.onerror = reject;
|
|
2096
|
+
|
|
2097
|
+
reader.readAsArrayBuffer(file);
|
|
2098
|
+
});
|
|
2099
|
+
};
|
|
2100
|
+
|
|
2101
|
+
/**
|
|
2102
|
+
* factory function to load and return a new NVImage instance from a file in the browser
|
|
2103
|
+
* @param {string} file the file object
|
|
2104
|
+
* @param {string} [name=''] a name for this image. Default is an empty string
|
|
2105
|
+
* @param {number} [opacity=1.0] the opacity for this image. default is 1
|
|
2106
|
+
* @param {boolean} [trustCalMinMax=true] whether or not to trust cal_min and cal_max from the nifti header (trusting results in faster loading)
|
|
2107
|
+
* @param {number} [percentileFrac=0.02] the percentile to use for setting the robust range of the display values (smart intensity setting for images with large ranges)
|
|
2108
|
+
* @param {boolean} [ignoreZeroVoxels=false] whether or not to ignore zero voxels in setting the robust range of display values
|
|
2109
|
+
* @param {boolean} [visible=true] whether or not this image is to be visible
|
|
2110
|
+
* @returns {NVImage} returns a NVImage intance
|
|
2111
|
+
* @example
|
|
2112
|
+
* myImage = NVImage.loadFromFile(SomeFileObject) // files can be from dialogs or drag and drop
|
|
2113
|
+
*/
|
|
2114
|
+
NVMesh.loadFromFile = async function ({
|
|
2115
|
+
file,
|
|
2116
|
+
gl,
|
|
2117
|
+
name = "",
|
|
2118
|
+
opacity = 1.0,
|
|
2119
|
+
rgba255 = [255, 255, 255, 255],
|
|
2120
|
+
visible = true,
|
|
2121
|
+
layers = [],
|
|
2122
|
+
} = {}) {
|
|
2123
|
+
let buffer = await this.readFileAsync(file);
|
|
2124
|
+
return await this.readMesh(
|
|
2125
|
+
buffer,
|
|
2126
|
+
name,
|
|
2127
|
+
gl,
|
|
2128
|
+
opacity,
|
|
2129
|
+
rgba255,
|
|
2130
|
+
visible,
|
|
2131
|
+
layers
|
|
2132
|
+
);
|
|
2133
|
+
};
|
|
2134
|
+
|
|
2135
|
+
NVMesh.loadFromBase64 = async function ({
|
|
2136
|
+
base64 = null,
|
|
2137
|
+
gl = null,
|
|
2138
|
+
name = "",
|
|
2139
|
+
opacity = 1.0,
|
|
2140
|
+
rgba255 = [255, 255, 255, 255],
|
|
2141
|
+
visible = true,
|
|
2142
|
+
layers = [],
|
|
2143
|
+
} = {}) {
|
|
2144
|
+
//https://stackoverflow.com/questions/21797299/convert-base64-string-to-arraybuffer
|
|
2145
|
+
function base64ToArrayBuffer(base64) {
|
|
2146
|
+
var binary_string = window.atob(base64);
|
|
2147
|
+
var len = binary_string.length;
|
|
2148
|
+
var bytes = new Uint8Array(len);
|
|
2149
|
+
for (var i = 0; i < len; i++) {
|
|
2150
|
+
bytes[i] = binary_string.charCodeAt(i);
|
|
2151
|
+
}
|
|
2152
|
+
return bytes.buffer;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
let buffer = base64ToArrayBuffer(base64);
|
|
2156
|
+
return await this.readMesh(
|
|
2157
|
+
buffer,
|
|
2158
|
+
name,
|
|
2159
|
+
gl,
|
|
2160
|
+
opacity,
|
|
2161
|
+
rgba255,
|
|
2162
|
+
visible,
|
|
2163
|
+
layers
|
|
2164
|
+
);
|
|
2165
|
+
};
|
|
2166
|
+
|
|
2167
|
+
String.prototype.getBytes = function () {
|
|
2168
|
+
//CR??? What does this do?
|
|
2169
|
+
let bytes = [];
|
|
2170
|
+
for (var i = 0; i < this.length; i++) {
|
|
2171
|
+
bytes.push(this.charCodeAt(i));
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
return bytes;
|
|
2175
|
+
};
|