@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/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
+ };