From 7f7e6d46db8512c6deaaf8c4509e7e9645d006ee Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sun, 21 Jun 2026 15:56:17 -0700 Subject: [PATCH 1/2] update: bonds optimization (antigravity) --- dist/mixins/bonds.d.ts | 14 ++-- dist/mixins/bonds.js | 104 ++++++++++++++++++----------- dist/mixins/cell.js | 4 +- src/mixins/bonds.ts | 145 +++++++++++++++++++++++++---------------- src/mixins/cell.ts | 8 ++- 5 files changed, 171 insertions(+), 104 deletions(-) diff --git a/dist/mixins/bonds.d.ts b/dist/mixins/bonds.d.ts index df51c48b..dce50e87 100644 --- a/dist/mixins/bonds.d.ts +++ b/dist/mixins/bonds.d.ts @@ -45,11 +45,11 @@ export declare const BondsMixin: (superclass: any) => { */ getElementsAndCoordinatesArrayWithEdgeNeighbors(maxBondLength: number): ElementAndCoordinateAsArray[]; /** - * Create the half bond objects between elements. + * Create the instanced mesh for all bonds, including repetitions. * k-d tree algorithm is used to optimize the time to find the element's neighbors. * See https://en.wikipedia.org/wiki/K-d_tree for more information. */ - createBondsGroup(): THREE.Group; + createBondsGroup(): THREE.InstancedMesh | THREE.Group; /** * Draw bonds. Bonds are created synchronously if the asynchronous callback (createBondsAsync) to draw bonds * in background has not returned yet. This may happen if the structure is large and draw bonds is toggled quickly. @@ -57,10 +57,14 @@ export declare const BondsMixin: (superclass: any) => { */ drawBonds(): void; /** - * Returns a bond as cylinder geometry object. - * @return {THREE.Mesh} + * Returns bond data properties (position, quaternion, height, color). */ - getBondObject(element1: string, index1: number, coordinate1: number[], element2: string, index2: number, coordinate2: number[]): THREE.Mesh; + getBondData(element1: string, index1: number, coordinate1: number[], element2: string, index2: number, coordinate2: number[]): { + position: THREE.Vector3; + quaternion: THREE.Quaternion; + height: number; + color: any; + }; }; [x: string]: any; }; diff --git a/dist/mixins/bonds.js b/dist/mixins/bonds.js index 604bb156..34ca4f67 100644 --- a/dist/mixins/bonds.js +++ b/dist/mixins/bonds.js @@ -84,43 +84,50 @@ export const BondsMixin = (superclass) => class extends superclass { */ // TODO: move to made basis bonded getElementsAndCoordinatesArrayWithEdgeNeighbors(maxBondLength) { - const newBasis = this.basis.clone(); - const basisCloneInCrystalCoordinates = this.basis.clone(); - newBasis.toCrystal(); - basisCloneInCrystalCoordinates.toCrystal(); + const elementsAndCoordinatesArray1 = this.basis.elementsAndCoordinatesArray; const planes = this.getCellPlanes(this.cell); - basisCloneInCrystalCoordinates.elements.forEach((element, index) => { - const coord = basisCloneInCrystalCoordinates.getCoordinateValueByIndex(index); - if (planes.find((plane) => plane.distanceToPoint(new THREE.Vector3(...coord)) <= maxBondLength)) { + const { cell } = this; + const vecA = new THREE.Vector3(cell.ax, cell.ay, cell.az); + const vecB = new THREE.Vector3(cell.bx, cell.by, cell.bz); + const vecC = new THREE.Vector3(cell.cx, cell.cy, cell.cz); + const result = [...elementsAndCoordinatesArray1]; + elementsAndCoordinatesArray1.forEach(([element, coord]) => { + const cartesianCoord = new THREE.Vector3(...coord); + let nearEdge = false; + for (let i = 0; i < planes.length; i++) { + if (Math.abs(planes[i].distanceToPoint(cartesianCoord)) <= maxBondLength) { + nearEdge = true; + break; + } + } + if (nearEdge) { [-1, 0, 1].forEach((shiftI) => { [-1, 0, 1].forEach((shiftJ) => { [-1, 0, 1].forEach((shiftK) => { if (shiftI === 0 && shiftJ === 0 && shiftK === 0) return; - newBasis.addAtom({ - element: element.value, - coordinate: [ - coord[0] + shiftI, - coord[1] + shiftJ, - coord[2] + shiftK, - ], - }); + const shiftedCoord = cartesianCoord + .clone() + .addScaledVector(vecA, shiftI) + .addScaledVector(vecB, shiftJ) + .addScaledVector(vecC, shiftK); + result.push([ + element, + [shiftedCoord.x, shiftedCoord.y, shiftedCoord.z], + ]); }); }); }); } }); - newBasis.toCartesian(); - return newBasis.elementsAndCoordinatesArray; + return result; } /** - * Create the half bond objects between elements. + * Create the instanced mesh for all bonds, including repetitions. * k-d tree algorithm is used to optimize the time to find the element's neighbors. * See https://en.wikipedia.org/wiki/K-d_tree for more information. */ - // TODO: move to made basis bonded - refactor to return bonds array and use the array in createBondsGroup createBondsGroup() { - const bondsGroup = new THREE.Group(); const bondsData = this.getBondsDataForUniqueElementPairs(); const maxBondLength = this.getMaxBondLength(bondsData); const elementsAndCoordinatesArray1 = this.basis.elementsAndCoordinatesArray; @@ -128,6 +135,7 @@ export const BondsMixin = (superclass) => class extends superclass { const tree = createKDTree( // eslint-disable-next-line no-unused-vars elementsAndCoordinatesArray2.map(([element, coordinate]) => coordinate)); + const baseBondsData = []; elementsAndCoordinatesArray1.forEach(([element1, coordinate1], index1) => { // iterate over all elements in maxBondLength radius of this element. O(3n^(2/3)) tree.rnn(coordinate1, maxBondLength, (index2) => { @@ -135,11 +143,38 @@ export const BondsMixin = (superclass) => class extends superclass { if (index2 === index1 || !this.areElementsBonded(element1, coordinate1, element2, coordinate2, bondsData)) return; - const bond = this.getBondObject(element1, index1, coordinate1, element2, index2, coordinate2); - bondsGroup.add(bond); + const bondData = this.getBondData(element1, index1, coordinate1, element2, index2, coordinate2); + baseBondsData.push(bondData); }); }); - return bondsGroup; + const { coordinates: repetitionCoords } = this.getRepetitionInfo(); + const totalRepetitions = 1 + repetitionCoords.length; + const totalInstances = baseBondsData.length * totalRepetitions; + if (totalInstances === 0) { + return new THREE.Group(); + } + const geometry = new THREE.CylinderGeometry(0.1, 0.1, 1, 8, 1); + geometry.translate(0, 0.5, 0); // shift so scaling operates from the base + const material = new THREE.MeshBasicMaterial(); + const instancedMesh = new THREE.InstancedMesh(geometry, material, totalInstances); + const matrix = new THREE.Matrix4(); + const colorObj = new THREE.Color(); + let instanceIndex = 0; + const allShifts = [[0, 0, 0], ...repetitionCoords]; + allShifts.forEach((shiftArr) => { + const shiftVec = new THREE.Vector3(...shiftArr); + baseBondsData.forEach((bond) => { + const finalPos = bond.position.clone().add(shiftVec); + matrix.compose(finalPos, bond.quaternion, new THREE.Vector3(1, bond.height, 1)); + instancedMesh.setMatrixAt(instanceIndex, matrix); + instancedMesh.setColorAt(instanceIndex, colorObj.set(bond.color)); + instanceIndex += 1; + }); + }); + instancedMesh.instanceMatrix.needsUpdate = true; + if (instancedMesh.instanceColor) + instancedMesh.instanceColor.needsUpdate = true; + return instancedMesh; } /** * Draw bonds. Bonds are created synchronously if the asynchronous callback (createBondsAsync) to draw bonds @@ -152,13 +187,12 @@ export const BondsMixin = (superclass) => class extends superclass { this.bondsGroup = this.createBondsGroup(); this.areBondsCreated = true; } - this.repeatObject3DAtRepetitionCoordinates(this.bondsGroup); + this.structureGroup.add(this.bondsGroup); } /** - * Returns a bond as cylinder geometry object. - * @return {THREE.Mesh} + * Returns bond data properties (position, quaternion, height, color). */ - getBondObject(element1, index1, coordinate1, element2, index2, coordinate2) { + getBondData(element1, index1, coordinate1, element2, index2, coordinate2) { const vector1 = new THREE.Vector3(...coordinate1); const vector2 = new THREE.Vector3(...coordinate2); const direction = new THREE.Vector3().subVectors(vector2, vector1); @@ -167,17 +201,11 @@ export const BondsMixin = (superclass) => class extends superclass { // create quaternion to rotate the cylinder const quaternion = new THREE.Quaternion(); quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction); - const geometry = new THREE.CylinderGeometry(0.1, 0.1, height, 8, 1); - // Move the cylinder by height / 2 as the cylinder position points to the center. - geometry.translate(0, height / 2, 0); - const material = new THREE.MeshBasicMaterial({ + return { + position: vector1, + quaternion, + height, color: this.getAtomColorByElement(element1), - }); - const bond = new THREE.Mesh(geometry, material); - // rotate the cylinder - bond.applyQuaternion(quaternion); - bond.position.set(vector1.x, vector1.y, vector1.z); - bond.name = `${element1}-${index1}:${element2}-${index2}`; - return bond; + }; } }; diff --git a/dist/mixins/cell.js b/dist/mixins/cell.js index 98366562..aa6db433 100644 --- a/dist/mixins/cell.js +++ b/dist/mixins/cell.js @@ -131,9 +131,7 @@ export const CellMixin = (superclass) => class extends superclass { [0, 2, 4], [4, 6, 5], ].map((face) => { - const slide1 = new THREE.Vector3().subVectors(vertices[face[0]], vertices[face[1]]); - const slide2 = new THREE.Vector3().subVectors(vertices[face[0]], vertices[face[2]]); - return new THREE.Plane(new THREE.Vector3().crossVectors(slide1, slide2)); + return new THREE.Plane().setFromCoplanarPoints(vertices[face[0]], vertices[face[1]], vertices[face[2]]); }); } /** diff --git a/src/mixins/bonds.ts b/src/mixins/bonds.ts index b18b1050..89b01b3d 100644 --- a/src/mixins/bonds.ts +++ b/src/mixins/bonds.ts @@ -116,54 +116,58 @@ export const BondsMixin = (superclass: any) => getElementsAndCoordinatesArrayWithEdgeNeighbors( maxBondLength: number, ): ElementAndCoordinateAsArray[] { - const newBasis = this.basis.clone(); - const basisCloneInCrystalCoordinates = this.basis.clone(); + const elementsAndCoordinatesArray1 = this.basis.elementsAndCoordinatesArray; + const planes = this.getCellPlanes(this.cell); - newBasis.toCrystal(); - basisCloneInCrystalCoordinates.toCrystal(); + const { cell } = this; + const vecA = new THREE.Vector3(cell.ax, cell.ay, cell.az); + const vecB = new THREE.Vector3(cell.bx, cell.by, cell.bz); + const vecC = new THREE.Vector3(cell.cx, cell.cy, cell.cz); - const planes = this.getCellPlanes(this.cell); + const result: ElementAndCoordinateAsArray[] = [...elementsAndCoordinatesArray1]; - basisCloneInCrystalCoordinates.elements.forEach( - (element: AtomicElementSchema, index: number) => { - const coord = basisCloneInCrystalCoordinates.getCoordinateValueByIndex(index); - if ( - planes.find( - (plane: THREE.Plane) => - plane.distanceToPoint(new THREE.Vector3(...coord)) <= maxBondLength, - ) - ) { - [-1, 0, 1].forEach((shiftI) => { - [-1, 0, 1].forEach((shiftJ) => { - [-1, 0, 1].forEach((shiftK) => { - if (shiftI === 0 && shiftJ === 0 && shiftK === 0) return; - newBasis.addAtom({ - element: element.value, - coordinate: [ - coord[0] + shiftI, - coord[1] + shiftJ, - coord[2] + shiftK, - ], - }); - }); + elementsAndCoordinatesArray1.forEach(([element, coord]: [any, any]) => { + const cartesianCoord = new THREE.Vector3(...(coord as [number, number, number])); + + let nearEdge = false; + for (let i = 0; i < planes.length; i++) { + if (Math.abs(planes[i].distanceToPoint(cartesianCoord)) <= maxBondLength) { + nearEdge = true; + break; + } + } + + if (nearEdge) { + [-1, 0, 1].forEach((shiftI) => { + [-1, 0, 1].forEach((shiftJ) => { + [-1, 0, 1].forEach((shiftK) => { + if (shiftI === 0 && shiftJ === 0 && shiftK === 0) return; + + const shiftedCoord = cartesianCoord + .clone() + .addScaledVector(vecA, shiftI) + .addScaledVector(vecB, shiftJ) + .addScaledVector(vecC, shiftK); + + result.push([ + element, + [shiftedCoord.x, shiftedCoord.y, shiftedCoord.z], + ]); }); }); - } - }, - ); + }); + } + }); - newBasis.toCartesian(); - return newBasis.elementsAndCoordinatesArray; + return result; } /** - * Create the half bond objects between elements. + * Create the instanced mesh for all bonds, including repetitions. * k-d tree algorithm is used to optimize the time to find the element's neighbors. * See https://en.wikipedia.org/wiki/K-d_tree for more information. */ - // TODO: move to made basis bonded - refactor to return bonds array and use the array in createBondsGroup - createBondsGroup(): THREE.Group { - const bondsGroup = new THREE.Group(); + createBondsGroup(): THREE.InstancedMesh | THREE.Group { const bondsData = this.getBondsDataForUniqueElementPairs(); const maxBondLength = this.getMaxBondLength(bondsData); @@ -178,6 +182,8 @@ export const BondsMixin = (superclass: any) => ), ); + const baseBondsData: any[] = []; + elementsAndCoordinatesArray1.forEach( ([element1, coordinate1]: ElementAndCoordinateAsArray, index1: number) => { // iterate over all elements in maxBondLength radius of this element. O(3n^(2/3)) @@ -194,7 +200,7 @@ export const BondsMixin = (superclass: any) => ) ) return; - const bond = this.getBondObject( + const bondData = this.getBondData( element1, index1, coordinate1, @@ -202,11 +208,47 @@ export const BondsMixin = (superclass: any) => index2, coordinate2, ); - bondsGroup.add(bond); + baseBondsData.push(bondData); }); }, ); - return bondsGroup; + + const { coordinates: repetitionCoords } = this.getRepetitionInfo(); + const totalRepetitions = 1 + repetitionCoords.length; + const totalInstances = baseBondsData.length * totalRepetitions; + + if (totalInstances === 0) { + return new THREE.Group(); + } + + const geometry = new THREE.CylinderGeometry(0.1, 0.1, 1, 8, 1); + geometry.translate(0, 0.5, 0); // shift so scaling operates from the base + + const material = new THREE.MeshBasicMaterial(); + const instancedMesh = new THREE.InstancedMesh(geometry, material, totalInstances); + + const matrix = new THREE.Matrix4(); + const colorObj = new THREE.Color(); + + let instanceIndex = 0; + const allShifts = [[0, 0, 0], ...repetitionCoords]; + + allShifts.forEach((shiftArr) => { + const shiftVec = new THREE.Vector3(...shiftArr); + + baseBondsData.forEach((bond) => { + const finalPos = bond.position.clone().add(shiftVec); + matrix.compose(finalPos, bond.quaternion, new THREE.Vector3(1, bond.height, 1)); + instancedMesh.setMatrixAt(instanceIndex, matrix); + instancedMesh.setColorAt(instanceIndex, colorObj.set(bond.color)); + instanceIndex += 1; + }); + }); + + instancedMesh.instanceMatrix.needsUpdate = true; + if (instancedMesh.instanceColor) instancedMesh.instanceColor.needsUpdate = true; + + return instancedMesh; } /** @@ -220,21 +262,20 @@ export const BondsMixin = (superclass: any) => this.bondsGroup = this.createBondsGroup(); this.areBondsCreated = true; } - this.repeatObject3DAtRepetitionCoordinates(this.bondsGroup); + this.structureGroup.add(this.bondsGroup); } /** - * Returns a bond as cylinder geometry object. - * @return {THREE.Mesh} + * Returns bond data properties (position, quaternion, height, color). */ - getBondObject( + getBondData( element1: string, index1: number, coordinate1: number[], element2: string, index2: number, coordinate2: number[], - ): THREE.Mesh { + ) { const vector1 = new THREE.Vector3(...coordinate1); const vector2 = new THREE.Vector3(...coordinate2); const direction = new THREE.Vector3().subVectors(vector2, vector1); @@ -243,17 +284,11 @@ export const BondsMixin = (superclass: any) => // create quaternion to rotate the cylinder const quaternion = new THREE.Quaternion(); quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction); - const geometry = new THREE.CylinderGeometry(0.1, 0.1, height, 8, 1); - // Move the cylinder by height / 2 as the cylinder position points to the center. - geometry.translate(0, height / 2, 0); - const material = new THREE.MeshBasicMaterial({ + return { + position: vector1, + quaternion, + height, color: this.getAtomColorByElement(element1), - }); - const bond = new THREE.Mesh(geometry, material); - // rotate the cylinder - bond.applyQuaternion(quaternion); - bond.position.set(vector1.x, vector1.y, vector1.z); - bond.name = `${element1}-${index1}:${element2}-${index2}`; - return bond; + }; } }; diff --git a/src/mixins/cell.ts b/src/mixins/cell.ts index 82697242..6e303605 100644 --- a/src/mixins/cell.ts +++ b/src/mixins/cell.ts @@ -180,9 +180,11 @@ export const CellMixin = (superclass: any) => [0, 2, 4], [4, 6, 5], ].map((face) => { - const slide1 = new THREE.Vector3().subVectors(vertices[face[0]], vertices[face[1]]); - const slide2 = new THREE.Vector3().subVectors(vertices[face[0]], vertices[face[2]]); - return new THREE.Plane(new THREE.Vector3().crossVectors(slide1, slide2)); + return new THREE.Plane().setFromCoplanarPoints( + vertices[face[0]], + vertices[face[1]], + vertices[face[2]], + ); }); } From c792ef1abe7d7d1f255a826d00325c7b301eff24 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sun, 21 Jun 2026 16:04:38 -0700 Subject: [PATCH 2/2] update: isolate to a function --- dist/mixins/bonds.d.ts | 4 ++++ dist/mixins/bonds.js | 6 ++++++ src/mixins/bonds.ts | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/dist/mixins/bonds.d.ts b/dist/mixins/bonds.d.ts index dce50e87..5a42d5eb 100644 --- a/dist/mixins/bonds.d.ts +++ b/dist/mixins/bonds.d.ts @@ -50,6 +50,10 @@ export declare const BondsMixin: (superclass: any) => { * See https://en.wikipedia.org/wiki/K-d_tree for more information. */ createBondsGroup(): THREE.InstancedMesh | THREE.Group; + /** + * Creates an InstancedMesh containing all bonds, accounting for repetitions. + */ + createInstancedMeshForBonds(baseBondsData: any[]): THREE.InstancedMesh | THREE.Group; /** * Draw bonds. Bonds are created synchronously if the asynchronous callback (createBondsAsync) to draw bonds * in background has not returned yet. This may happen if the structure is large and draw bonds is toggled quickly. diff --git a/dist/mixins/bonds.js b/dist/mixins/bonds.js index 34ca4f67..43da0c21 100644 --- a/dist/mixins/bonds.js +++ b/dist/mixins/bonds.js @@ -147,6 +147,12 @@ export const BondsMixin = (superclass) => class extends superclass { baseBondsData.push(bondData); }); }); + return this.createInstancedMeshForBonds(baseBondsData); + } + /** + * Creates an InstancedMesh containing all bonds, accounting for repetitions. + */ + createInstancedMeshForBonds(baseBondsData) { const { coordinates: repetitionCoords } = this.getRepetitionInfo(); const totalRepetitions = 1 + repetitionCoords.length; const totalInstances = baseBondsData.length * totalRepetitions; diff --git a/src/mixins/bonds.ts b/src/mixins/bonds.ts index 89b01b3d..5f1283ba 100644 --- a/src/mixins/bonds.ts +++ b/src/mixins/bonds.ts @@ -213,6 +213,13 @@ export const BondsMixin = (superclass: any) => }, ); + return this.createInstancedMeshForBonds(baseBondsData); + } + + /** + * Creates an InstancedMesh containing all bonds, accounting for repetitions. + */ + createInstancedMeshForBonds(baseBondsData: any[]): THREE.InstancedMesh | THREE.Group { const { coordinates: repetitionCoords } = this.getRepetitionInfo(); const totalRepetitions = 1 + repetitionCoords.length; const totalInstances = baseBondsData.length * totalRepetitions;