@chinhui/niivue
Version:
minimal webgl2 nifti image viewer
1,444 lines (1,409 loc) • 103 kB
JavaScript
//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