3dmol
Version:
JavaScript/TypeScript molecular visualization library
513 lines (449 loc) • 16 kB
text/typescript
import type { Material } from './../materials/Material';
import { LineBasicMaterial } from '../materials/LineBasicMaterial';
import { EventDispatcher } from "./EventDispatcher";
import { Vector3 } from "../math";
import { CC, Color } from "../../colors";
import { AtomSpec } from 'specs';
const BUFFERSIZE = 65535; //limited to 16bit indices
export class GeometryGroup {
id: number;
vertexArray: Float32Array | null = null;
colorArray: Float32Array | null = null;
normalArray: Float32Array | null = null;
radiusArray: Float32Array | null = null;
faceArray: Uint16Array | null = null;
lineArray: Uint16Array | null = null;
atomArray: Array<AtomSpec> = Array<AtomSpec>();
vertices: number = 0;
faceidx: number = 0;
lineidx: number = 0;
__inittedArrays = false;
useOffset: unknown;
constructor(id = 0) {
this.id = id;
}
public setColor(color: Color | number): void {
//apply constant color
var v = this.vertexArray;
var c = this.colorArray;
if (!v) throw new Error("vertex array not initialized");
if (!c) throw new Error("color array not initialized");
let col = CC.color(color);
for (var i = 0; i < v.length; i += 3) {
c[i] = col.r;
c[i + 1] = col.g;
c[i + 2] = col.b;
}
}
setColors(setcolor: (x: number, y: number, z: number) => Color | number): void {
//apply a function that takes the vertex coordinate and returns a color
var v = this.vertexArray;
var c = this.colorArray;
if (!v) throw new Error("vertex array not initialized");
if (!c) throw new Error("color array not initialized");
if (v.length != c.length) {
console.log("Cannot re-color geometry group due to mismatched lengths.");
return;
}
for (var i = 0; i < v.length; i += 3) {
var col = setcolor(v[i], v[i + 1], v[i + 2]);
if (!(col instanceof Color)) {
col = CC.color(col);
}
c[i] = col.r;
c[i + 1] = col.g;
c[i + 2] = col.b;
}
}
getNumVertices(): number {
return this.vertices;
}
getVertices() {
return this.vertexArray;
}
getCentroid() {
if (!this.vertexArray) throw new Error("vertex array not initialized");
var centroid = new Vector3();
var offset: number, x: number, y: number, z: number;
for (var i = 0; i < this.vertices; ++i) {
offset = i * 3;
x = this.vertexArray[offset];
y = this.vertexArray[offset + 1];
z = this.vertexArray[offset + 2];
centroid.x += x;
centroid.y += y;
centroid.z += z;
}
//divideScalar checks for 0 denom
centroid.divideScalar(this.vertices);
return centroid;
}
//setup normals - vertex and face array must exist
setNormals(): void {
var faces = this.faceArray;
var verts = this.vertexArray;
var norms = this.normalArray;
if (!this.vertices || !this.faceidx) return;
if (!faces) throw new Error("face array not initialized");
if (!verts) throw new Error("vertex array not initialized");
if (!norms) throw new Error("normal array not initialized");
//vertex indices
var a: number,
b: number,
c: number,
//and actual vertices
vA: Vector3,
vB: Vector3,
vC: Vector3,
norm: { normalize: () => void; x: number; y: number; z: number; };
for (var i = 0; i < faces.length / 3; ++i) {
a = faces[i * 3] * 3;
b = faces[i * 3 + 1] * 3;
c = faces[i * 3 + 2] * 3;
vA = new Vector3(verts[a], verts[a + 1], verts[a + 2]);
vB = new Vector3(verts[b], verts[b + 1], verts[b + 2]);
vC = new Vector3(verts[c], verts[c + 1], verts[c + 2]);
vA.subVectors(vA, vB);
vC.subVectors(vC, vB);
vC.cross(vA);
//face normal
norm = vC;
norm.normalize();
norms[a] += norm.x;
norms[b] += norm.x;
norms[c] += norm.x;
norms[a + 1] += norm.y;
norms[b + 1] += norm.y;
norms[c + 1] += norm.y;
norms[a + 2] += norm.z;
norms[b + 2] += norm.z;
norms[c + 2] += norm.z;
}
}
/* sets line index array from face arr
Note - assumes all faces are triangles (i.e. there will
be an extra diagonal for four-sided faces - user should
specify linearr for custom shape generation to show wireframe squares
as rectangles rather than two triangles) */
setLineIndices() {
if (!this.faceidx) return;
if (
this.lineArray &&
this.lineArray.length == this.faceidx * 2 &&
this.lineidx == this.faceidx * 2
)
return; //assume already computed
var faceArr = this.faceArray,
lineArr = (this.lineArray = new Uint16Array(this.faceidx * 2));
this.lineidx = this.faceidx * 2;
if (!faceArr) throw new Error("face array not initialized");
for (var i = 0; i < this.faceidx / 3; ++i) {
var faceoffset = i * 3;
var lineoffset = faceoffset * 2;
var a = faceArr[faceoffset],
b = faceArr[faceoffset + 1],
c = faceArr[faceoffset + 2];
lineArr[lineoffset] = a;
lineArr[lineoffset + 1] = b;
lineArr[lineoffset + 2] = a;
lineArr[lineoffset + 3] = c;
lineArr[lineoffset + 4] = b;
lineArr[lineoffset + 5] = c;
}
}
vrml(indent: string, material?: Material) {
var ret = "";
ret +=
indent +
"Shape {\n" +
indent +
" appearance Appearance {\n" +
indent +
" material Material {\n" +
indent +
" diffuseColor " +
material?.color?.r +
" " +
material?.color?.g +
" " +
material?.color?.b +
"\n";
if (material.wireframe && this.colorArray) {
//per vertex colors don't seem to work
let c = this.colorArray;
ret += indent + " emissiveColor " + c[0] + " " + c[1] + " " + c[2] + "\n";
}
if (material?.transparent) {
ret += indent + " transparency " + (1.0 - material.opacity) + "\n";
}
ret += indent + " }\n"; //material
ret += indent + " }\n"; //appearance
var oldindent = indent;
indent += " "; //inshape
if (material instanceof LineBasicMaterial || material.wireframe) {
ret +=
indent +
"geometry IndexedLineSet {\n" +
indent +
" colorPerVertex TRUE\n" +
indent +
" coord Coordinate {\n" +
indent +
" point [\n";
let x: string | number, y: string | number, z: string | number;
for (let i = 0; i < this.vertices; ++i) {
let offset = i * 3;
x = this.vertexArray?.[offset];
y = this.vertexArray?.[offset + 1];
z = this.vertexArray?.[offset + 2];
ret += indent + " " + x + " " + y + " " + z + ",\n";
}
ret += indent + " ]\n";
ret += indent + " }\n"; //end coordinate
if (this.colorArray && !material.wireframe) {
ret += indent + " color Color {\n" + indent + " color [\n";
for (let i = 0; i < this.vertices; ++i) {
let offset = i * 3;
x = this.colorArray[offset];
y = this.colorArray[offset + 1];
z = this.colorArray[offset + 2];
ret += indent + " " + x + " " + y + " " + z + ",\n";
}
ret += indent + " ]\n";
ret += indent + " }\n"; //end color
}
ret += indent + " coordIndex [\n";
if(material.wireframe && this.faceArray) {
for (let i = 0; i < this.faceidx; i += 3) {
x = this.faceArray?.[i];
y = this.faceArray?.[i + 1];
z = this.faceArray?.[i + 2];
ret += indent + " " + x + ", " + y + ", " + z + ", -1,\n";
}
} else {
for (let i = 0; i < this.vertices-1; i += 2) {
ret += indent + " " + i + ", " + (i + 1) + ", -1,\n";
}
}
ret += indent + " ]\n";
ret += indent + "}\n"; //geometry
} else {
//faces
ret +=
indent +
"geometry IndexedFaceSet {\n" +
indent +
" colorPerVertex TRUE\n" +
indent +
" normalPerVertex TRUE\n" +
indent +
" solid FALSE\n";
//vertices
ret += indent + " coord Coordinate {\n" + indent + " point [\n";
let x: string | number, y: string | number, z: string | number;
for (let i = 0; i < this.vertices; ++i) {
let offset = i * 3;
x = this.vertexArray?.[offset];
y = this.vertexArray?.[offset + 1];
z = this.vertexArray?.[offset + 2];
ret += indent + " " + x + " " + y + " " + z + ",\n";
}
ret += indent + " ]\n";
ret += indent + " }\n"; //end coordinate
//normals
ret += indent + " normal Normal {\n" + indent + " vector [\n";
for (let i = 0; i < this.vertices; ++i) {
let offset = i * 3;
x = this.normalArray?.[offset];
y = this.normalArray?.[offset + 1];
z = this.normalArray?.[offset + 2];
ret += indent + " " + x + " " + y + " " + z + ",\n";
}
ret += indent + " ]\n";
ret += indent + " }\n"; //end normal
//colors
if (this.colorArray) {
ret += indent + " color Color {\n" + indent + " color [\n";
for (let i = 0; i < this.vertices; ++i) {
let offset = i * 3;
x = this.colorArray[offset];
y = this.colorArray[offset + 1];
z = this.colorArray[offset + 2];
ret += indent + " " + x + " " + y + " " + z + ",\n";
}
ret += indent + " ]\n";
ret += indent + " }\n"; //end color
}
//faces
ret += indent + " coordIndex [\n";
for (let i = 0; i < this.faceidx; i += 3) {
x = this.faceArray?.[i];
y = this.faceArray?.[i + 1];
z = this.faceArray?.[i + 2];
ret += indent + " " + x + ", " + y + ", " + z + ", -1,\n";
}
ret += indent + " ]\n"; //end faces
ret += indent + "}\n"; //geometry
}
ret += oldindent + "}"; //shape
return ret;
}
truncateArrayBuffers(mesh = true, reallocatemem = false) {
var vertexArr = this.vertexArray,
colorArr = this.colorArray,
normalArr = this.normalArray,
faceArr = this.faceArray,
lineArr = this.lineArray,
radiusArr = this.radiusArray;
//subarray to avoid copying and reallocating memory
this.vertexArray = vertexArr?.subarray(0, this.vertices * 3) || null;
this.colorArray = colorArr?.subarray(0, this.vertices * 3) || null;
if (mesh) {
this.normalArray = normalArr?.subarray(0, this.vertices * 3) || null;
this.faceArray = faceArr?.subarray(0, this.faceidx) || null;
if (this.lineidx > 0)
//not always set so reclaim memory
this.lineArray = lineArr?.subarray(0, this.lineidx) || null;
else this.lineArray = new Uint16Array(0);
} else {
this.normalArray = new Float32Array(0);
this.faceArray = new Uint16Array(0);
this.lineArray = new Uint16Array(0);
}
if (radiusArr) {
this.radiusArray = radiusArr.subarray(0, this.vertices);
}
if (reallocatemem) {
//actually copy smaller arrays to save memory
if (this.normalArray)
this.normalArray = new Float32Array(this.normalArray);
if (this.faceArray) this.faceArray = new Uint16Array(this.faceArray);
if (this.lineArray) this.lineArray = new Uint16Array(this.lineArray);
if (this.vertexArray)
this.vertexArray = new Float32Array(this.vertexArray);
if (this.colorArray) this.colorArray = new Float32Array(this.colorArray);
if (this.radiusArray)
this.radiusArray = new Float32Array(this.radiusArray);
}
this.__inittedArrays = true;
}
}
export class Geometry extends EventDispatcher {
id: number;
name: string = "";
hasTangents: boolean = false;
dynamic: boolean = true; // the intermediate typed arrays will be deleted when set to false;
radii: boolean;
mesh: boolean;
offset: boolean;
verticesNeedUpdate: boolean = false;
elementsNeedUpdate: boolean = false;
normalsNeedUpdate: boolean = false;
colorsNeedUpdate: boolean = false;
buffersNeedUpdate: boolean = false;
imposter: boolean = false;
instanced: boolean = false;
geometryGroups: GeometryGroup[] = [];
groups: number = 0;
sphereGeometry?: Geometry;
drawnCaps?: any;
constructor(mesh = false, radii = false, offset = false) {
super();
this.id = GeometryIDCount++;
this.mesh = mesh; // Does this geometry represent a mesh (i.e. do we need Face/Line index buffers?)
this.radii = radii;
this.offset = offset; //offset buffer used for instancing
}
//Get geometry group to accomodate addVertices new vertices - create
// new group if necessary
updateGeoGroup(addVertices = 0): GeometryGroup {
var retGroup =
this.groups > 0 ? this.geometryGroups[this.groups - 1] : null;
if (
!retGroup ||
retGroup.vertices + addVertices > (retGroup?.vertexArray?.length || 0) / 3
)
retGroup = this.addGeoGroup();
return retGroup;
}
//return comma separated list of IndexedFace (or Line) sets from geometry groups
vrml(indent: string, material?: Material): string {
var ret = "";
var len = this.geometryGroups.length;
for (var g = 0; g < len; g++) {
var geoGroup = this.geometryGroups[g];
ret += geoGroup.vrml(indent, material) + ",\n";
}
return ret;
}
addGeoGroup() {
var ret = new GeometryGroup(this.geometryGroups.length);
this.geometryGroups.push(ret);
this.groups = this.geometryGroups.length;
ret.vertexArray = new Float32Array(BUFFERSIZE * 3);
ret.colorArray = new Float32Array(BUFFERSIZE * 3);
//TODO: instantiating uint arrays according to max number of vertices
// is dangerous, since there exists the possibility that there will be
// more face or line indices than vertex points - but so far that doesn't
// seem to be the case for any of the renders
if (this.mesh) {
ret.normalArray = new Float32Array(BUFFERSIZE * 3);
ret.faceArray = new Uint16Array(BUFFERSIZE * 6);
ret.lineArray = new Uint16Array(BUFFERSIZE * 6);
}
if (this.radii) {
ret.radiusArray = new Float32Array(BUFFERSIZE);
}
ret.useOffset = this.offset;
return ret;
}
setUpNormals(...args: Parameters<GeometryGroup["setNormals"]>) {
for (var g = 0; g < this.groups; g++) {
var geoGroup = this.geometryGroups[g];
geoGroup.setNormals(...args);
}
}
setColors(...setcolor: Parameters<GeometryGroup["setColors"]>): void {
var len = this.geometryGroups.length;
for (var g = 0; g < len; g++) {
var geoGroup = this.geometryGroups[g];
geoGroup.setColors(...setcolor);
}
}
setColor(...setcolor: Parameters<GeometryGroup["setColor"]>): void {
let len = this.geometryGroups.length;
for (var g = 0; g < len; g++) {
var geoGroup = this.geometryGroups[g];
geoGroup.setColor(...setcolor);
}
}
setUpWireframe(...lineIndexArgs: Parameters<GeometryGroup["setLineIndices"]>) {
let len = this.geometryGroups.length;
for (var g = 0; g < len; g++) {
var geoGroup = this.geometryGroups[g];
geoGroup.setLineIndices(...lineIndexArgs);
}
}
//After vertices, colors, etc are collected in regular or typed arrays,
// create typed arrays from regular arrays if they don't already exist,
initTypedArrays() {
for (var g = 0; g < this.groups; g++) {
var group = this.geometryGroups[g];
if (group.__inittedArrays === true) continue;
//do not actually reallocate smaller memory here because
//of the performance hit - if you know your geometry is small,
//truncate manually with the second parameter true
group.truncateArrayBuffers(this.mesh, false);
}
}
dispose() {
this.dispatchEvent({ type: "dispose" });
}
get vertices (): number {
var vertices = 0;
for (var g = 0; g < this.groups; g++)
vertices += this.geometryGroups[g].vertices;
return vertices;
}
}
export let GeometryIDCount = 0;