UNPKG

@chinhui/niivue

Version:

minimal webgl2 nifti image viewer

1,444 lines (1,409 loc) 103 kB
//import * as gifti from "gifti-reader-js/release/current/gifti-reader"; import * as fflate from "fflate"; import { v4 as uuidv4 } from "uuid"; import * as cmaps from "./cmaps"; import { Log } from "./logger"; import { NiivueObject3D } from "./niivue-object3D.js"; //n.b. used by connectome import { mat3, mat4, vec3, vec4 } from "gl-matrix"; import { colortables } from "./colortables"; const cmapper = new colortables(); const log = new Log(); /** * @class NVMesh * @type NVMesh * @description * a NVImage encapsulates some images data and provides methods to query and operate on images * @constructor * @param {array} dataBuffer an array buffer of image data to load (there are also methods that abstract this more. See loadFromUrl, and loadFromFile) * @param {string} [name=''] a name for this image. Default is an empty string * @param {number} [opacity=1.0] the opacity for this image. default is 1 * @param {boolean} [trustCalMinMax=true] whether or not to trust cal_min and cal_max from the nifti header (trusting results in faster loading) * @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) * @param {boolean} [ignoreZeroVoxels=false] whether or not to ignore zero voxels in setting the robust range of display values * @param {boolean} [visible=true] whether or not this image is to be visible */ export function NVMesh( pts, tris, name = "", rgba255 = [1, 0, 0, 0], opacity = 1.0, visible = true, gl, connectome = null, dpg = null, dps = null, dpv = null ) { this.name = name; this.id = uuidv4(); let obj = getExtents(pts); this.furthestVertexFromOrigin = obj.mxDx; this.extentsMin = obj.extentsMin; this.extentsMax = obj.extentsMax; this.opacity = opacity > 1.0 ? 1.0 : opacity; //make sure opacity can't be initialized greater than 1 see: #107 and #117 on github this.visible = visible; this.indexBuffer = gl.createBuffer(); this.vertexBuffer = gl.createBuffer(); this.vao = gl.createVertexArray(); this.offsetPt0 = null; this.hasConnectome = false; this.pts = pts; this.layers = []; if (!rgba255) { this.fiberLength = 2; this.fiberDither = 0.1; this.fiberColor = "Global"; this.fiberDecimationStride = 1; //e.g. if 2 the 50% of streamlines visible, if 3 then 1/3rd this.fiberMask = []; //provide method to show/hide specific fibers this.colormap = connectome; this.dpg = dpg; this.dps = dps; this.dpv = dpv; this.offsetPt0 = tris; this.updateFibers(gl); //define VAO gl.bindVertexArray(this.vao); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); //vertex position: 3 floats X,Y,Z gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 16, 0); //vertex color gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 4, gl.UNSIGNED_BYTE, true, 16, 12); 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 return; } //if fiber not mesh if (connectome) { this.hasConnectome = true; var keysArray = Object.keys(connectome); for (var i = 0, len = keysArray.length; i < len; i++) { this[keysArray[i]] = connectome[keysArray[i]]; } } this.rgba255 = rgba255; this.tris = tris; this.updateMesh(gl); //the VAO binds the vertices and indices as well as describing the vertex layout gl.bindVertexArray(this.vao); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); //vertex position: 3 floats X,Y,Z gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 28, 0); //vertex surface normal vector: (also three floats) gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 28, 12); //vertex color gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 4, gl.UNSIGNED_BYTE, true, 28, 24); 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 } NVMesh.prototype.updateFibers = function (gl) { if (!this.offsetPt0 || !this.fiberLength) return; //VERTICES: let pts = this.pts; let offsetPt0 = this.offsetPt0; let n_count = offsetPt0.length - 1; let npt = pts.length / 3; //each point has three components: X,Y,Z //only once: compute length of each streamline if (!this.fiberLengths) { this.fiberLengths = []; for (let i = 0; i < n_count; i++) { //for each streamline let vStart3 = offsetPt0[i] * 3; //first vertex in streamline let vEnd3 = (offsetPt0[i + 1] - 1) * 3; //last vertex in streamline let len = 0; for (let j = vStart3; j < vEnd3; j += 3) { let v = vec3.fromValues( pts[j + 0] - pts[j + 3], pts[j + 1] - pts[j + 4], pts[j + 2] - pts[j + 5] ); len += vec3.len(v); } this.fiberLengths.push(len); } } //only once: compute length of each streamline //determine fiber colors //Each streamline vertex has color and position attributes //Interleaved Vertex Data https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/TechniquesforWorkingwithVertexData/TechniquesforWorkingwithVertexData.html var posClrF32 = new Float32Array(npt * 4); //four 32-bit components X,Y,Z,C var posClrU32 = new Uint32Array(posClrF32.buffer); //typecast of our X,Y,Z,C array //fill XYZ position of XYZC array let i3 = 0; let i4 = 0; for (let i = 0; i < npt; i++) { posClrF32[i4 + 0] = pts[i3 + 0]; posClrF32[i4 + 1] = pts[i3 + 1]; posClrF32[i4 + 2] = pts[i3 + 2]; i3 += 3; i4 += 4; } //fill fiber Color let dither = this.fiberDither; let ditherHalf = dither * 0.5; function direction2rgb(x1, y1, z1, x2, y2, z2, ditherFrac) { //generate color based on direction between two 3D spatial positions let v = vec3.fromValues( Math.abs(x1 - x2), Math.abs(y1 - y2), Math.abs(z1 - z2) ); vec3.normalize(v, v); let r = ditherFrac - ditherHalf; for (let j = 0; j < 3; j++) v[j] = 255 * Math.max(Math.min(Math.abs(v[j]) + r, 1.0), 0.0); return v[0] + (v[1] << 8) + (v[2] << 16); } // direction2rgb() //Determine color: local, global, dps0, dpv0, etc. let fiberColor = this.fiberColor.toLowerCase(); let dps = null; let dpv = null; if (fiberColor.startsWith("dps") && this.dps.length > 0) { let n = parseInt(fiberColor.substring(3)); if (n < this.dps.length && this.dps[n].vals.length === n_count) dps = this.dps[n].vals; } if (fiberColor.startsWith("dpv") && this.dpv.length > 0) { let n = parseInt(fiberColor.substring(3)); if (n < this.dpv.length && this.dpv[n].vals.length === npt) dpv = this.dpv[n].vals; } if (dpv) { //color per streamline let lut = cmapper.colormap(this.colormap); let mn = dpv[0]; let mx = dpv[0]; for (let i = 0; i < npt; i++) { mn = Math.min(mn, dpv[i]); mx = Math.max(mx, dpv[i]); } let v4 = 3; //+3: fill 4th component colors: XYZC = 0123 for (let i = 0; i < npt; i++) { let color = (dpv[i] - mn) / (mx - mn); color = Math.round(Math.max(Math.min(255, color * 255)), 1) * 4; let RGBA = lut[color] + (lut[color + 1] << 8) + (lut[color + 2] << 16); posClrU32[v4] = RGBA; v4 += 4; } } else if (dps) { //color per streamline let lut = cmapper.colormap(this.colormap); let mn = dps[0]; let mx = dps[0]; for (let i = 0; i < n_count; i++) { mn = Math.min(mn, dps[i]); mx = Math.max(mx, dps[i]); } if (mx === mn) mn -= 1; //avoid divide by zero for (let i = 0; i < n_count; i++) { let color = (dps[i] - mn) / (mx - mn); color = Math.round(Math.max(Math.min(255, color * 255)), 1) * 4; let RGBA = lut[color] + (lut[color + 1] << 8) + (lut[color + 2] << 16); let vStart = offsetPt0[i]; //first vertex in streamline let vEnd = offsetPt0[i + 1] - 1; //last vertex in streamline let vStart4 = vStart * 4 + 3; //+3: fill 4th component colors: XYZC = 0123 let vEnd4 = vEnd * 4 + 3; for (let j = vStart4; j <= vEnd4; j += 4) posClrU32[j] = RGBA; } } else if (fiberColor.includes("local")) { for (let i = 0; i < n_count; i++) { //for each streamline let vStart = offsetPt0[i]; //first vertex in streamline let vEnd = offsetPt0[i + 1] - 1; //last vertex in streamline let v3 = vStart * 3; //pts have 3 components XYZ let vEnd3 = vEnd * 3; let ditherFrac = dither * Math.random(); //same dither amount throughout line //for first point, we do not have a prior sample let RGBA = direction2rgb( pts[v3], pts[v3 + 1], pts[v3 + 2], pts[v3 + 4], pts[v3 + 5], pts[v3 + 6], ditherFrac ); let v4 = vStart * 4 + 3; //+3: fill 4th component colors: XYZC = 0123 while (v3 < vEnd3) { posClrU32[v4] = RGBA; v4 += 4; //stride is 4 32-bit values: float32 XYZ and 32-bit rgba v3 += 3; //read next vertex //direction estimated based on previous and next vertex RGBA = direction2rgb( pts[v3 - 3], pts[v3 - 2], pts[v3 - 1], pts[v3 + 3], pts[v3 + 4], pts[v3 + 5], ditherFrac ); } posClrU32[v4] = posClrU32[v4 - 4]; } } else { //if color is local direction, else global for (let i = 0; i < n_count; i++) { //for each streamline let vStart = offsetPt0[i]; //first vertex in streamline let vEnd = offsetPt0[i + 1] - 1; //last vertex in streamline let vStart3 = vStart * 3; //pts have 3 components XYZ let vEnd3 = vEnd * 3; let RGBA = direction2rgb( pts[vStart3], pts[vStart3 + 1], pts[vStart3 + 2], pts[vEnd3], pts[vEnd3 + 1], pts[vEnd3 + 2], dither * Math.random() ); let vStart4 = vStart * 4 + 3; //+3: fill 4th component colors: XYZC = 0123 let vEnd4 = vEnd * 4 + 3; for (let j = vStart4; j <= vEnd4; j += 4) posClrU32[j] = RGBA; } } gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Uint32Array(posClrU32), gl.STATIC_DRAW); //INDICES: let min_mm = this.fiberLength; // https://blog.spacepatroldelta.com/a?ID=00950-d878555f-a97a-4e32-9f40-fd9a449cb4fe let primitiveRestart = Math.pow(2, 32) - 1; //for gl.UNSIGNED_INT let indices = []; let stride = -1; for (let i = 0; i < n_count; i++) { //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 if (this.fiberLengths[i] < min_mm) continue; stride++; if (stride % this.fiberDecimationStride !== 0) continue; //e.g. if stride is 2 then half culled for (let j = offsetPt0[i]; j < offsetPt0[i + 1]; j++) indices.push(j); indices.push(primitiveRestart); } this.indexCount = indices.length; gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); //glBufferData creates a new data store for the buffer object currently bound to target​. Any pre-existing data store is deleted. gl.bufferData( gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW ); }; NVMesh.prototype.updateConnectome = function (gl) { //draw nodes let json = this; //draw nodes let tris = []; let nNode = json.nodes.X.length; let hasEdges = false; if (nNode > 1 && json.hasOwnProperty("edges")) { let nEdges = json.edges.length; if ((nEdges = nNode * nNode)) hasEdges = true; else console.log("Expected %d edges not %d", nNode * nNode, nEdges); } //draw all nodes let pts = []; let rgba255 = []; let lut = cmapper.colormap(json.nodeColormap); let lutNeg = cmapper.colormap(json.nodeColormapNegative); let hasNeg = json.hasOwnProperty("nodeColormapNegative"); let min = json.nodeMinColor; let max = json.nodeMaxColor; for (let i = 0; i < nNode; i++) { let radius = json.nodes.Size[i] * json.nodeScale; if (radius <= 0.0) continue; let color = json.nodes.Color[i]; let isNeg = false; if (hasNeg && color < 0) { isNeg = true; color = -color; } if (min < max) { if (color < min) continue; color = (color - min) / (max - min); } else color = 1.0; color = Math.round(Math.max(Math.min(255, color * 255)), 1) * 4; let rgba = [lut[color], lut[color + 1], lut[color + 2], 255]; if (isNeg) rgba = [lutNeg[color], lutNeg[color + 1], lutNeg[color + 2], 255]; let pt = [json.nodes.X[i], json.nodes.Y[i], json.nodes.Z[i]]; NiivueObject3D.makeColoredSphere(pts, tris, rgba255, radius, pt, rgba); } //draw all edges if (hasEdges) { lut = cmapper.colormap(json.edgeColormap); lutNeg = cmapper.colormap(json.edgeColormapNegative); hasNeg = json.hasOwnProperty("edgeColormapNegative"); min = json.edgeMin; max = json.edgeMax; for (let i = 0; i < nNode - 1; i++) { for (let j = i + 1; j < nNode; j++) { let color = json.edges[i * nNode + j]; let isNeg = false; if (hasNeg && color < 0) { isNeg = true; color = -color; } let radius = color * json.edgeScale; if (radius <= 0) continue; if (min < max) { if (color < min) continue; color = (color - min) / (max - min); } else color = 1.0; color = Math.round(Math.max(Math.min(255, color * 255)), 1) * 4; let rgba = [lut[color], lut[color + 1], lut[color + 2], 255]; if (isNeg) rgba = [lutNeg[color], lutNeg[color + 1], lutNeg[color + 2], 255]; let pti = [json.nodes.X[i], json.nodes.Y[i], json.nodes.Z[i]]; let ptj = [json.nodes.X[j], json.nodes.Y[j], json.nodes.Z[j]]; NiivueObject3D.makeColoredCylinder( pts, tris, rgba255, pti, ptj, radius, rgba ); } //for j } //for i } //hasEdges //calculate spatial extent of connectome: user adjusting node sizes may influence size let obj = getExtents(pts); this.furthestVertexFromOrigin = obj.mxDx; this.extentsMin = obj.extentsMin; this.extentsMax = obj.extentsMax; let posNormClr = this.generatePosNormClr(pts, tris, rgba255); //generate webGL buffers and vao gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Int32Array(tris), gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(posNormClr), gl.STATIC_DRAW); this.indexCount = tris.length; }; NVMesh.prototype.updateMesh = function (gl) { if (this.offsetPt0) { this.updateFibers(gl); return; //fiber not mesh } if (this.hasConnectome) { this.updateConnectome(gl); return; //connectome not mesh } if (!this.pts || !this.tris || !this.rgba255) { console.log("underspecified mesh"); return; } let posNormClr = this.generatePosNormClr(this.pts, this.tris, this.rgba255); if (this.layers && this.layers.length > 0) { for (let i = 0; i < this.layers.length; i++) { let layer = this.layers[i]; if (layer.opacity <= 0.0 || layer.cal_min >= layer.cal_max) continue; let opacity = layer.opacity; var u8 = new Uint8Array(posNormClr.buffer); //Each vertex has 7 components: PositionXYZ, NormalXYZ, RGBA32 function lerp(x, y, a) { //https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/mix.xhtml return x * (1 - a) + y * a; } if (layer.values.constructor === Uint32Array) { //isRGBA! let rgba8 = new Uint8Array(layer.values.buffer); let k = 0; for (let j = 0; j < layer.values.length; j++) { let vtx = j * 28 + 24; //posNormClr is 28 bytes stride, RGBA color at offset 24, u8[vtx + 0] = lerp(u8[vtx + 0], rgba8[k + 0], opacity); u8[vtx + 1] = lerp(u8[vtx + 1], rgba8[k + 1], opacity); u8[vtx + 2] = lerp(u8[vtx + 2], rgba8[k + 2], opacity); k += 4; } continue; } let lut = cmapper.colormap(layer.colorMap); let frame = Math.min(Math.max(layer.frame4D, 0), layer.nFrame4D - 1); let nvtx = this.pts.length / 3; let frameOffset = nvtx * frame; if (layer.useNegativeCmap) { layer.cal_min = Math.max(0, layer.cal_min); layer.cal_max = Math.max(layer.cal_min + 0.000001, layer.cal_max); } let scale255 = 255.0 / (layer.cal_max - layer.cal_min); //blend colors for each voxel for (let j = 0; j < nvtx; j++) { let v255 = Math.round( (layer.values[j + frameOffset] - layer.cal_min) * scale255 ); if (v255 < 0) continue; v255 = Math.min(255.0, v255) * 4; let vtx = j * 28 + 24; //posNormClr is 28 bytes stride, RGBA color at offset 24, u8[vtx + 0] = lerp(u8[vtx + 0], lut[v255 + 0], opacity); u8[vtx + 1] = lerp(u8[vtx + 1], lut[v255 + 1], opacity); u8[vtx + 2] = lerp(u8[vtx + 2], lut[v255 + 2], opacity); } if (layer.useNegativeCmap) { let lut = cmapper.colormap(layer.colorMapNegative); for (let j = 0; j < nvtx; j++) { let v255 = Math.round( (-layer.values[j + frameOffset] - layer.cal_min) * scale255 ); if (v255 < 0) continue; v255 = Math.min(255.0, v255) * 4; let vtx = j * 28 + 24; //posNormClr is 28 bytes stride, RGBA color at offset 24, u8[vtx + 0] = lerp(u8[vtx + 0], lut[v255 + 0], opacity); u8[vtx + 1] = lerp(u8[vtx + 1], lut[v255 + 1], opacity); u8[vtx + 2] = lerp(u8[vtx + 2], lut[v255 + 2], opacity); } } } } //generate webGL buffers and vao gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); gl.bufferData( gl.ELEMENT_ARRAY_BUFFER, new Int32Array(this.tris), gl.STATIC_DRAW ); gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(posNormClr), gl.STATIC_DRAW); this.indexCount = this.tris.length; this.vertexCount = this.pts.length; }; NVMesh.prototype.setLayerProperty = function (id, key, val, gl) { let layer = this.layers[id]; if (!layer.hasOwnProperty(key)) { console.log("mesh does not have property ", key, layer); return; } layer[key] = val; this.updateMesh(gl); //apply the new properties... }; NVMesh.prototype.setProperty = function (key, val, gl) { if (!this.hasOwnProperty(key)) { console.log("mesh does not have property ", key, this); return; } this[key] = val; this.updateMesh(gl); //apply the new properties... }; function getExtents(pts) { //each vertex has 3 coordinates: XYZ let mxDx = 0.0; let mn = vec3.fromValues(pts[0], pts[1], pts[2]); let mx = vec3.fromValues(pts[0], pts[1], pts[2]); for (let i = 0; i < pts.length; i += 3) { let v = vec3.fromValues(pts[i], pts[i + 1], pts[i + 2]); mxDx = Math.max(mxDx, vec3.len(v)); vec3.min(mn, mn, v); vec3.max(mx, mx, v); } let extentsMin = [mn[0], mn[1], mn[2]]; let extentsMax = [mx[0], mx[1], mx[2]]; return { mxDx, extentsMin, extentsMax }; } function generateNormals(pts, tris) { //from https://github.com/rii-mango/Papaya /* Copyright (c) 2012-2015, RII-UTHSCSA All rights reserved. THIS PRODUCT IS NOT FOR CLINICAL USE. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of the RII-UTHSCSA nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ var p1 = [], p2 = [], p3 = [], normal = [], nn = [], ctr, normalsDataLength = pts.length, numIndices, qx, qy, qz, px, py, pz, index1, index2, index3; let norms = new Float32Array(normalsDataLength); numIndices = tris.length; for (ctr = 0; ctr < numIndices; ctr += 3) { index1 = tris[ctr] * 3; index2 = tris[ctr + 1] * 3; index3 = tris[ctr + 2] * 3; p1.x = pts[index1]; p1.y = pts[index1 + 1]; p1.z = pts[index1 + 2]; p2.x = pts[index2]; p2.y = pts[index2 + 1]; p2.z = pts[index2 + 2]; p3.x = pts[index3]; p3.y = pts[index3 + 1]; p3.z = pts[index3 + 2]; qx = p2.x - p1.x; qy = p2.y - p1.y; qz = p2.z - p1.z; px = p3.x - p1.x; py = p3.y - p1.y; pz = p3.z - p1.z; normal[0] = py * qz - pz * qy; normal[1] = pz * qx - px * qz; normal[2] = px * qy - py * qx; norms[index1] += normal[0]; norms[index1 + 1] += normal[1]; norms[index1 + 2] += normal[2]; norms[index2] += normal[0]; norms[index2 + 1] += normal[1]; norms[index2 + 2] += normal[2]; norms[index3] += normal[0]; norms[index3 + 1] += normal[1]; norms[index3 + 2] += normal[2]; } for (ctr = 0; ctr < normalsDataLength; ctr += 3) { normal[0] = -1 * norms[ctr]; normal[1] = -1 * norms[ctr + 1]; normal[2] = -1 * norms[ctr + 2]; let len = normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]; if (len > 0) { len = 1.0 / Math.sqrt(len); normal[0] *= len; normal[1] *= len; normal[2] *= len; } norms[ctr] = normal[0]; norms[ctr + 1] = normal[1]; norms[ctr + 2] = normal[2]; } return norms; } NVMesh.prototype.generatePosNormClr = function (pts, tris, rgba255) { //Each streamline vertex has color, normal and position attributes //Interleaved Vertex Data https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/TechniquesforWorkingwithVertexData/TechniquesforWorkingwithVertexData.html if (pts.length < 3 || rgba255.length < 4) log.error("Catastrophic failure generatePosNormClr()"); let norms = generateNormals(pts, tris); let npt = pts.length / 3; let isPerVertexColors = npt === rgba255.length / 4; var f32 = new Float32Array(npt * 7); //Each vertex has 7 components: PositionXYZ, NormalXYZ, RGBA32 var u8 = new Uint8Array(f32.buffer); //Each vertex has 7 components: PositionXYZ, NormalXYZ, RGBA32 let p = 0; //input position let c = 0; //input color let f = 0; //output float32 location (position and normals) let u = 24; //output uint8 location (colors), offset 24 as after 3*position+3*normal for (let i = 0; i < npt; i++) { f32[f + 0] = pts[p + 0]; f32[f + 1] = pts[p + 1]; f32[f + 2] = pts[p + 2]; f32[f + 3] = norms[p + 0]; f32[f + 4] = norms[p + 1]; f32[f + 5] = norms[p + 2]; u8[u] = rgba255[c + 0]; u8[u + 1] = rgba255[c + 1]; u8[u + 2] = rgba255[c + 2]; u8[u + 3] = rgba255[c + 3]; if (isPerVertexColors) c += 4; p += 3; //read 3 input components: XYZ f += 7; //write 7 output components: 3*Position, 3*Normal, 1*RGBA u += 28; //stride of 28 bytes } return f32; }; NVMesh.readTRACT = function (buffer) { let len = buffer.byteLength; if (len < 20) throw new Error("File too small to be niml.tract: bytes = " + len); var reader = new DataView(buffer); var bytes = new Uint8Array(buffer); let pos = 0; function readStr() { //read until right angle bracket ">" while (pos < len && bytes[pos] !== 60) pos++; //start with "<" let startPos = pos; while (pos < len && bytes[pos] !== 62) pos++; pos++; //skip EOLN if (pos - startPos < 1) return ""; return new TextDecoder().decode(buffer.slice(startPos, pos - 1)).trim(); } function readNumericTag(TagName) { //Tag 'Dim1' will return 3 for Dim1="3" let pos = line.indexOf(TagName); if (pos < 0) return 0; let spos = line.indexOf('"', pos) + 1; let epos = line.indexOf('"', spos); let str = line.slice(spos, epos); return parseInt(str); } let line = readStr(); //1st line: signature '<network' let n_tracts = readNumericTag("N_tracts="); if (!line.startsWith("<network") || n_tracts < 1) console.log("This is not a valid niml.tract file " + line); let npt = 0; let offsetPt0 = []; offsetPt0.push(npt); //1st streamline starts at 0 let pts = []; let dps = []; dps.push({ id: "tract", vals: [], }); for (let t = 0; t < n_tracts; t++) { line = readStr(); //<tracts ... let new_tracts = readNumericTag("ni_dimen="); let bundleTag = readNumericTag("Bundle_Tag="); let isLittleEndian = line.includes("binary.lsbfirst"); //console.log(new_tracts, pos, isLittleEndian); for (let i = 0; i < new_tracts; i++) { let id = reader.getUint32(pos, isLittleEndian); pos += 4; let new_pts = reader.getUint32(pos, isLittleEndian) / 3; pos += 4; //console.log('offset', pos, 'new', new_pts,'id', id); for (let j = 0; j < new_pts; j++) { pts.push(reader.getFloat32(pos, isLittleEndian)); pos += 4; pts.push(-reader.getFloat32(pos, isLittleEndian)); pos += 4; pts.push(reader.getFloat32(pos, isLittleEndian)); pos += 4; } npt += new_pts; offsetPt0.push(npt); dps[0].vals.push(bundleTag); //each streamline associated with tract } line = readStr(); //</tracts> } return { pts, offsetPt0, dps, }; }; // readTRACT() NVMesh.readTCK = function (buffer) { //https://mrtrix.readthedocs.io/en/latest/getting_started/image_data.html#tracks-file-format-tck let len = buffer.byteLength; if (len < 20) throw new Error("File too small to be TCK: bytes = " + len); var bytes = new Uint8Array(buffer); let pos = 0; function readStr() { while (pos < len && bytes[pos] === 10) pos++; //skip blank lines let startPos = pos; while (pos < len && bytes[pos] !== 10) pos++; pos++; //skip EOLN if (pos - startPos < 1) return ""; return new TextDecoder().decode(buffer.slice(startPos, pos - 1)); } let line = readStr(); //1st line: signature 'mrtrix tracks' if (!line.includes("mrtrix tracks")) { console.log("Not a valid TCK file"); return; } while (pos < len && !line.startsWith("END")) line = readStr(); var reader = new DataView(buffer); //read and transform vertex positions let npt = 0; let offsetPt0 = []; offsetPt0.push(npt); //1st streamline starts at 0 let pts = []; while (pos + 12 < len) { var ptx = reader.getFloat32(pos, true); pos += 4; var pty = reader.getFloat32(pos, true); pos += 4; var ptz = reader.getFloat32(pos, true); pos += 4; if (!isFinite(ptx)) { //both NaN and Inifinity are not finite offsetPt0.push(npt); if (!isNaN(ptx)) //terminate if infinity break; } else { pts.push(ptx); pts.push(pty); pts.push(ptz); npt++; } } return { pts, offsetPt0, }; }; //readTCK() NVMesh.readTRK = function (buffer) { // http://trackvis.org/docs/?subsect=fileformat // http://www.tractometer.org/fiberweb/ // https://github.com/xtk/X/tree/master/io // in practice, always little endian var reader = new DataView(buffer); var magic = reader.getUint32(0, true); //'TRAC' if (magic !== 1128354388) { //e.g. TRK.gz let raw; if (magic === 4247762216) { //zstd raw = fzstd.decompress(new Uint8Array(buffer)); raw = new Uint8Array(raw); } else raw = fflate.decompressSync(new Uint8Array(buffer)); buffer = raw.buffer; reader = new DataView(buffer); magic = reader.getUint32(0, true); //'TRAC' } var vers = reader.getUint32(992, true); //2 var hdr_sz = reader.getUint32(996, true); //1000 if (vers > 2 || hdr_sz !== 1000 || magic !== 1128354388) throw new Error("Not a valid TRK file"); let dps = []; let dpv = []; var n_scalars = reader.getInt16(36, true); if (n_scalars > 0) { //data_per_vertex for (let i = 0; i < n_scalars; i++) { let arr = new Uint8Array(buffer.slice(38 + i * 20, 58 + i * 20)); var str = new TextDecoder().decode(arr).split("\0").shift(); dpv.push({ id: str.trim(), vals: [], }); } } var voxel_sizeX = reader.getFloat32(12, true); var voxel_sizeY = reader.getFloat32(16, true); var voxel_sizeZ = reader.getFloat32(20, true); var zoomMat = mat4.fromValues( 1 / voxel_sizeX, 0, 0, -0.5, 0, 1 / voxel_sizeY, 0, -0.5, 0, 0, 1 / voxel_sizeZ, -0.5, 0, 0, 0, 1 ); var n_properties = reader.getInt16(238, true); if (n_properties > 0) { for (let i = 0; i < n_properties; i++) { let arr = new Uint8Array(buffer.slice(240 + i * 20, 260 + i * 20)); var str = new TextDecoder().decode(arr).split("\0").shift(); dps.push({ id: str.trim(), vals: [], }); } } var mat = mat4.create(); for (let i = 0; i < 16; i++) mat[i] = reader.getFloat32(440 + i * 4, true); if (mat[15] === 0.0) { //vox_to_ras[3][3] is 0, it means the matrix is not recorded console.log("TRK vox_to_ras not set"); mat4.identity(mat); } var vox2mmMat = mat4.create(); mat4.mul(vox2mmMat, mat, zoomMat); let i32 = null; let f32 = null; i32 = new Int32Array(buffer.slice(hdr_sz)); f32 = new Float32Array(i32.buffer); let ntracks = i32.length; //read and transform vertex positions let i = 0; let npt = 0; let offsetPt0 = []; let pts = []; while (i < ntracks) { let n_pts = i32[i]; i = i + 1; // read 1 32-bit integer for number of points in this streamline offsetPt0.push(npt); //index of first vertex in this streamline for (let j = 0; j < n_pts; j++) { let ptx = f32[i + 0]; let pty = f32[i + 1]; let ptz = f32[i + 2]; i += 3; //read 3 32-bit floats for XYZ position pts.push( ptx * vox2mmMat[0] + pty * vox2mmMat[1] + ptz * vox2mmMat[2] + vox2mmMat[3] ); pts.push( ptx * vox2mmMat[4] + pty * vox2mmMat[5] + ptz * vox2mmMat[6] + vox2mmMat[7] ); pts.push( ptx * vox2mmMat[8] + pty * vox2mmMat[9] + ptz * vox2mmMat[10] + vox2mmMat[11] ); if (n_scalars > 0) { for (let s = 0; s < n_scalars; s++) { dpv[s].vals.push(f32[i]); i++; } } npt++; } // for j: each point in streamline if (n_properties > 0) { for (let j = 0; j < n_properties; j++) { dps[j].vals.push(f32[i]); i++; } } } //for each streamline: while i < n_count offsetPt0.push(npt); //add 'first index' as if one more line was added (fence post problem) return { pts, offsetPt0, dps, dpv, }; }; //readTRK() function readTxtVTK(buffer) { var enc = new TextDecoder("utf-8"); var txt = enc.decode(buffer); var lines = txt.split("\n"); var n = lines.length; if (n < 7 || !lines[0].startsWith("# vtk DataFile")) alert("Invalid VTK image"); if (!lines[2].startsWith("ASCII")) alert("Not ASCII VTK mesh"); let pos = 3; while (lines[pos].length < 1) pos++; //skip blank lines if (!lines[pos].includes("POLYDATA")) alert("Not ASCII VTK polydata"); pos++; while (lines[pos].length < 1) pos++; //skip blank lines if (!lines[pos].startsWith("POINTS")) alert("Not VTK POINTS"); let items = lines[pos].split(" "); let nvert = parseInt(items[1]); //POINTS 10261 float let nvert3 = nvert * 3; var positions = new Float32Array(nvert * 3); let v = 0; while (v < nvert * 3) { pos++; let str = lines[pos].trim(); let pts = str.split(" "); for (let i = 0; i < pts.length; i++) { if (v >= nvert3) break; positions[v] = parseFloat(pts[i]); v++; } } let tris = []; pos++; while (lines[pos].length < 1) pos++; //skip blank lines items = lines[pos].split(" "); pos++; if (items[0].includes("LINES")) { let n_count = parseInt(items[1]); if (n_count < 1) alert("Corrupted VTK ASCII"); let str = lines[pos].trim(); let offsetPt0 = []; let pts = []; if (str.startsWith("OFFSETS")) { // 'new' line style https://discourse.vtk.org/t/upcoming-changes-to-vtkcellarray/2066 offsetPt0 = new Uint32Array(n_count); pos++; let c = 0; while (c < n_count) { str = lines[pos].trim(); pos++; let items = str.split(" "); for (let i = 0; i < items.length; i++) { offsetPt0[c] = parseInt(items[i]); c++; if (c >= n_count) break; } //for each line } //while offset array not filled pts = positions; } else { //classic line style https://www.visitusers.org/index.php?title=ASCII_VTK_Files offsetPt0 = new Uint32Array(n_count + 1); let npt = 0; pts = []; offsetPt0[0] = 0; //1st streamline starts at 0 let asciiInts = []; let asciiIntsPos = 0; function lineToInts() { //VTK can save one array across multiple ASCII lines str = lines[pos].trim(); let items = str.split(" "); asciiInts = []; for (let i = 0; i < items.length; i++) asciiInts.push(parseInt(items[i])); asciiIntsPos = 0; pos++; } lineToInts(); for (let c = 0; c < n_count; c++) { if (asciiIntsPos >= asciiInts.length) lineToInts(); let numPoints = asciiInts[asciiIntsPos++]; npt += numPoints; offsetPt0[c + 1] = npt; for (let i = 0; i < numPoints; i++) { if (asciiIntsPos >= asciiInts.length) lineToInts(); let idx = asciiInts[asciiIntsPos++] * 3; pts.push(positions[idx + 0]); //X pts.push(positions[idx + 1]); //Y pts.push(positions[idx + 2]); //Z } //for numPoints: number of segments in streamline } //for n_count: number of streamlines } return { pts, offsetPt0, }; } else if (items[0].includes("TRIANGLE_STRIPS")) { let nstrip = parseInt(items[1]); for (let i = 0; i < nstrip; i++) { let str = lines[pos].trim(); pos++; let vs = str.split(" "); let ntri = parseInt(vs[0]) - 2; //-2 as triangle strip is creates pts - 2 faces let k = 1; for (let t = 0; t < ntri; t++) { if (t % 2) { // preserve winding order tris.push(parseInt(vs[k + 2])); tris.push(parseInt(vs[k + 1])); tris.push(parseInt(vs[k])); } else { tris.push(parseInt(vs[k])); tris.push(parseInt(vs[k + 1])); tris.push(parseInt(vs[k + 2])); } k += 1; } //for each triangle } //for each strip } else if (items[0].includes("POLYGONS")) { let npoly = parseInt(items[1]); for (let i = 0; i < npoly; i++) { let str = lines[pos].trim(); pos++; let vs = str.split(" "); let ntri = parseInt(vs[0]) - 2; //e.g. 3 for triangle let fx = parseInt(vs[1]); let fy = parseInt(vs[2]); for (let t = 0; t < ntri; t++) { let fz = parseInt(vs[3 + t]); tris.push(fx); tris.push(fy); tris.push(fz); fy = fz; } } } else alert("Unsupported ASCII VTK datatype " + items[0]); var indices = new Int32Array(tris); return { positions, indices, }; } // readTxtVTK() NVMesh.readSMP = function (buffer, n_vert) { //https://support.brainvoyager.com/brainvoyager/automation-development/84-file-formats/40-the-format-of-smp-files let len = buffer.byteLength; var reader = new DataView(buffer); let vers = reader.getUint16(0, true); if (vers > 5) { //assume gzip var raw = fflate.decompressSync(new Uint8Array(buffer)); reader = new DataView(raw.buffer); vers = reader.getUint16(0, true); buffer = raw.buffer; } if (vers > 5) console.log("Unsupported or invalud BrainVoyager SMP version " + vers); let nvert = reader.getUint32(2, true); if (nvert !== n_vert) console.log( "SMP file has " + nvert + " vertices, background mesh has " + n_vert ); let nMaps = reader.getUint16(6, true); function readStr() { let startPos = pos; while (pos < len && reader.getUint8(pos) !== 0) { pos++; } pos++; //skip null termination return new TextDecoder().decode(buffer.slice(startPos, pos - 1)); } // readStr: read variable length string let scalars = new Float32Array(nvert * nMaps); let maps = []; //read Name of SRF let pos = 9; let filenameSRF = readStr(); for (let i = 0; i < nMaps; i++) { let m = []; m.mapType = reader.getUint32(pos, true); pos += 4; //Read additional values only if a lag map if (vers >= 3 && m.mapType === 3) { m.nLags = reader.getUint32(pos, true); pos += 4; m.mnLag = reader.getUint32(pos, true); pos += 4; m.mxLag = reader.getUint32(pos, true); pos += 4; m.ccOverlay = reader.getUint32(pos, true); pos += 4; } m.clusterSize = reader.getUint32(pos, true); pos += 4; m.clusterCheck = reader.getUint8(pos); pos += 1; m.critThresh = reader.getFloat32(pos, true); pos += 4; m.maxThresh = reader.getFloat32(pos, true); pos += 4; if (vers >= 4) { m.includeValuesGreaterThreshMax = reader.getUint32(pos, true); pos += 4; } m.df1 = reader.getUint32(pos, true); pos += 4; m.df2 = reader.getUint32(pos, true); pos += 4; if (vers >= 5) { m.posNegFlag = reader.getUint32(pos, true); pos += 4; } else m.posNegFlag = 3; m.cortexBonferroni = reader.getUint32(pos, true); pos += 4; m.posMinRGB = [0, 0, 0]; m.posMaxRGB = [0, 0, 0]; m.negMinRGB = [0, 0, 0]; m.negMaxRGB = [0, 0, 0]; if (vers >= 2) { m.posMinRGB[0] = reader.getUint8(pos); pos++; m.posMinRGB[1] = reader.getUint8(pos); pos++; m.posMinRGB[2] = reader.getUint8(pos); pos++; m.posMaxRGB[0] = reader.getUint8(pos); pos++; m.posMaxRGB[1] = reader.getUint8(pos); pos++; m.posMaxRGB[2] = reader.getUint8(pos); pos++; if (vers >= 4) { m.negMinRGB[0] = reader.getUint8(pos); pos++; m.negMinRGB[1] = reader.getUint8(pos); pos++; m.negMinRGB[2] = reader.getUint8(pos); pos++; m.negMaxRGB[0] = reader.getUint8(pos); pos++; m.negMaxRGB[1] = reader.getUint8(pos); pos++; m.negMaxRGB[2] = reader.getUint8(pos); pos++; } //vers >= 4 m.enableSMPColor = reader.getUint8(pos); pos++; if (vers >= 4) m.lut = readStr(); m.colorAlpha = reader.getFloat32(pos, true); pos += 4; } //vers >= 2 m.name = readStr(); let scalarsNew = new Float32Array(buffer, pos, nvert, true); scalars.set(scalarsNew, i * nvert); pos += nvert * 4; maps.push(m); } // for i to nMaps return scalars; }; //readSMP() NVMesh.readSTC = function (buffer, n_vert) { //mne STC format //https://github.com/mne-tools/mne-python/blob/main/mne/source_estimate.py#L211-L365 //https://github.com/fahsuanlin/fhlin_toolbox/blob/400cb73cda4880d9ad7841d9dd68e4e9762976bf/codes/inverse_read_stc.m let len = buffer.byteLength; var reader = new DataView(buffer); //first 12 bytes are header let epoch_begin_latency = reader.getFloat32(0, false); let sample_period = reader.getFloat32(4, false); let n_vertex = reader.getInt32(8, false); if (n_vertex !== n_vert) { console.log("Overlay has " + n_vertex + " vertices, expected " + n_vert); return; } //next 4*n_vertex bytes are vertex IDS let pos = 12 + n_vertex * 4; //next 4 bytes reports number of volumes/time points let n_time = reader.getUint32(pos, false); pos += 4; let f32 = new Float32Array(n_time * n_vertex); //reading all floats with .slice() would be faster, but lets handle endian-ness for (let i = 0; i < n_time * n_vertex; i++) { f32[i] = reader.getFloat32(pos, false); pos += 4; } return f32; }; // readSTC() NVMesh.readCURV = function (buffer, n_vert) { //simple format used by Freesurfer BIG-ENDIAN // https://github.com/bonilhamusclab/MRIcroS/blob/master/%2BfileUtils/%2Bpial/readPial.m // http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm const view = new DataView(buffer); //ArrayBuffer to dataview //ALWAYS big endian let sig0 = view.getUint8(0); let sig1 = view.getUint8(1); let sig2 = view.getUint8(2); let n_vertex = view.getUint32(3, false); let num_f = view.getUint32(7, false); let n_time = view.getUint32(11, false); if (sig0 !== 255 || sig1 !== 255 || sig2 !== 255) log.debug( "Unable to recognize file type: does not appear to be FreeSurfer format." ); if (n_vert !== n_vertex) { console.log("CURV file has different number of vertices than mesh"); return; } if (buffer.byteLength < 15 + 4 * n_vertex * n_time) { console.log("CURV file smaller than specified"); return; } let f32 = new Float32Array(n_time * n_vertex); let pos = 15; //reading all floats with .slice() would be faster, but lets handle endian-ness for (let i = 0; i < n_time * n_vertex; i++) { f32[i] = view.getFloat32(pos, false); pos += 4; } let mn = f32[0]; let mx = f32[0]; for (var i = 0; i < f32.length; i++) { mn = Math.min(mn, f32[i]); mx = Math.max(mx, f32[i]); } //normalize and invert then sqrt let scale = 1.0 / (mx - mn); for (var i = 0; i < f32.length; i++) f32[i] = Math.sqrt(1.0 - (f32[i] - mn) * scale); return f32; }; // readCURV() NVMesh.readANNOT = function (buffer, n_vert) { //freesurfer Annotation file provides vertex colors // https://surfer.nmr.mgh.harvard.edu/fswiki/LabelsClutsAnnotationFiles const view = new DataView(buffer); //ArrayBuffer to dataview //ALWAYS big endian let n_vertex = view.getUint32(0, false); if (n_vert !== n_vertex) { console.log("ANNOT file has different number of vertices than mesh"); return; } if (buffer.byteLength < 4 + 8 * n_vertex) { console.log("ANNOT file smaller than specified"); return; } let pos = 4; //reading all floats with .slice() would be faster, but lets handle endian-ness let rgba32 = new Uint32Array(n_vertex); for (let i = 0; i < n_vertex; i++) { let idx = view.getUint32(pos, false); pos += 4; rgba32[idx] = view.getUint32(pos, false); pos += 4; } return rgba32; }; // readANNOT() NVMesh.readASC = function (buffer) { //SUMA ASCII format https://afni.nimh.nih.gov/pub/dist/doc/htmldoc/demos/Bootcamp/CD.html#cd //http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm let len = buffer.byteLength; var bytes = new Uint8Array(buffer); let pos = 0; function readStr() { while (pos < len && bytes[pos] === 10) pos++; //skip blank lines let startPos = pos; while (pos < len && bytes[pos] !== 10) pos++; pos++; //skip EOLN if (pos - startPos < 1) return ""; return new TextDecoder().decode(buffer.slice(startPos, pos - 1)); } let line = readStr(); //1st line: '#!ascii version of lh.pial' if (!line.startsWith("#!ascii")) console.log("Invalid ASC mesh"); line = readStr(); //1st line: signature let items = line.split(" "); let nvert = parseInt(items[0]); //173404 346804 let ntri = parseInt(items[1]); var positions = new Float32Array(nvert * 3); let j = 0; for (let i = 0; i < nvert; i++) { line = readStr(); //1st line: signature items = line.trim().split(/\s+/); positions[j] = parseFloat(items[0]); positions[j + 1] = parseFloat(items[1]); positions[j + 2] = parseFloat(items[2]); j += 3; } var indices = new Int32Array(ntri * 3); j = 0; for (let i = 0; i < ntri; i++) { line = readStr(); //1st line: signature items = line.trim().split(/\s+/); indices[j] = parseInt(items[0]); indices[j + 1] = parseInt(items[1]); indices[j + 2] = parseInt(items[2]); j += 3; } return { positions, indices, }; }; // readASC() NVMesh.readVTK = function (buffer) { let len = buffer.byteLength; if (len < 20) throw new Error("File too small to be VTK: bytes = " + buffer.byteLength); var bytes = new Uint8Array(buffer); let pos = 0; function readStr() { while (pos < len && bytes[pos] === 10) pos++; //skip blank lines let startPos = pos; while (pos < len && bytes[pos] !== 10) pos++; pos++; //skip EOLN if (pos - startPos < 1) return ""; return new TextDecoder().decode(buffer.slice(startPos, pos - 1)); } let line = readStr(); //1st line: signature if (!line.startsWith("# vtk DataFile")) alert("Invalid VTK mesh"); line = readStr(); //2nd line comment line = readStr(); //3rd line ASCII/BINARY if (line.startsWith("ASCII")) return readTxtVTK(buffer); //from NiiVue else if (!line.startsWith("BINARY")) alert("Invalid VTK image, expected ASCII or BINARY", line); line = readStr(); //5th line "DATASET POLYDATA" if (!line.includes("POLYDATA")) alert("Only able to read VTK POLYDATA", line); line = readStr(); //6th line "POINTS 10261 float" if ( !line.includes("POINTS") || (!line.includes("double") && !line.includes("float")) ) console.log("Only able to read VTK float or double POINTS" + line); let isFloat64 = line.includes("double"); let items = line.split(" "); let nvert = parseInt(items[1]); //POINTS 10261 float let nvert3 = nvert * 3; var positions = new Float32Array(nvert3); var reader = new DataView(buffer); if (isFloat64) { for (let i = 0; i < nvert3; i++) { positions[i] = reader.getFloat64(pos, false); pos += 8; } } else { for (let i = 0; i < nvert3; i++) { positions[i] = reader.getFloat32(pos, false); pos += 4; } } line = readStr(); //Type, "LINES 11885 " items = line.split(" "); let tris = []; if (items[0].includes("LINES")) { let n_count = parseInt(items[1]); //tractogaphy data: detect if borked by DiPy let posOK = pos; line = readStr(); //borked files "OFFSETS vtktypeint64" if (line.startsWith("OFFSETS")) { //console.log("invalid VTK file created by DiPy"); let isInt64 = false; if (line.includes("int64")) isInt64 = true; let offsetPt0 = new Uint32Array(n_count); if (isInt64) { let isOverflowInt32 = false; for (let c = 0; c < n_count; c++) { let idx = reader.getInt32(pos, false); if (idx !== 0) isOverflowInt32 = true; pos += 4; idx = reader.getInt32(pos, false); pos += 4; offsetPt0[c] = idx; } if (isOverflowInt32) console.log("int32 overflow: JavaScript does not support int64"); } else { for (let c = 0; c < n_count; c++) { let idx = reader.getInt32(pos, false); pos += 4; offsetPt0[c] = idx; } } let pts = positions; return { pts, offsetPt0, }; } pos = posOK; //valid VTK file let npt = 0; let offsetPt0 = []; let pts = []; offsetPt0.push(npt); //1st streamline starts at 0 for (let c = 0; c < n_count; c++) { let numPoints = reader.getInt32(pos, false); pos += 4; npt += numPoints; offsetPt0.push(npt); for (let i = 0; i < numPoints; i++) { let idx = reader.getInt32(pos, false) * 3; pos += 4; pts.push(positions[idx + 0]); pts.push(positions[idx + 1]); pts.push(positions[idx + 2]); } //for numPoints: number of segments in streamline } //for n_count: number of streamlines return { pts, offsetPt0, }; } else if (items[0].includes("TRIANGLE_STRIPS")) { let nstrip = parseInt(items[1]); for (let i = 0; i < nstrip; i++) { let ntri = reader.getInt32(pos, false) - 2; //-2 as triangle strip is creates pts - 2 faces pos += 4; for (let t = 0; t < ntri; t++) { if (t % 2) { // preserve winding order tris.push(reader.getInt32(pos + 8, false)); tris.push(reader.getInt32(pos + 4, false)); tris.push(reader.getInt32(pos, false)); } else { tris.push(reader.getInt32(pos, false)); tris.push(reader.getInt32(pos + 4, false)); tris.push(reader.getInt32(pos + 8, false)); } pos += 4; } //for each triangle pos += 8; } //for each strip } else if (items[0].includes("POLYGONS")) { let npoly = parseInt(items[1]); for (let i = 0; i < npoly; i++) { let ntri = reader.getInt32(pos, false) - 2; //3 for single triangle, 4 for 2 tria