Skip to content

Commit

Permalink
Merge pull request #131 from Exabyte-io/feature/SOF-6814
Browse files Browse the repository at this point in the history
Feature/sof 6814
  • Loading branch information
VsevolodX committed Aug 23, 2023
2 parents 96a554a + 403bed9 commit 9f8ef23
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 76 deletions.
145 changes: 74 additions & 71 deletions src/mixins/labels.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ATOM_GROUP_NAME, LABELS_GROUP_NAME } from "../enums";
// eslint-disable-next-line import/no-cycle
import { setParameters } from "../utils";
/*
* Mixin containing the logic for dealing with atom labes.
* Mixin containing the logic for dealing with atom labels.
* Dynamically draws labels over atoms.
*/
export const LabelsMixin = (superclass) =>
Expand Down Expand Up @@ -71,51 +71,38 @@ export const LabelsMixin = (superclass) =>
*/
createLabelSprite(text, name) {
const texture = this.getLabelTextTexture(text);
const spriteMaterial = new THREE.SpriteMaterial({ map: texture });
const spriteMaterial = new THREE.SpriteMaterial({
map: texture,
...this.settings.labelSpriteConfig,
});
const sprite = new THREE.Sprite(spriteMaterial);
sprite.name = name;
sprite.scale.set(0.25, 0.25, 0.25);
sprite.visible = this.areLabelsShown;
return sprite;
}

/**
* Adjusts the label sprites' positions so that the center of every sprite
* is always in the point where the atom's bound sphere is intersected
* by the line joining the atom's center with the camera.
* Adjusts label positions in 3D space so that they don't overlap with their corresponding atoms
* and always face the camera.
* @method adjustLabelsToCameraPosition
*/
adjustLabelsToCameraPosition() {
if (!this.areLabelsShown) return;

const atomGroups = this.scene
.getObjectByName(ATOM_GROUP_NAME)
.parent.children.filter((object) => object.name.includes(ATOM_GROUP_NAME));
atomGroups.forEach((group) => {
const labels = group.children.filter((child) => child instanceof THREE.Sprite);
labels.forEach((label) => {
const atomUUID = label.name.replace(/label-for-/, "");
const atom = this.scene.getObjectByProperty("uuid", atomUUID);
const clampedVectorToCamera = this.getClampedVectorToCamera(group, atom);
const newLabelPosition = atom.position.clone().add(clampedVectorToCamera);
label.position.copy(newLabelPosition);
});
if (!this.areLabelsShown || !this.settings.labelsConfig.areSpritesUsed) return;
this.labelsGroup.children.forEach((label) => {
const { atomPosition, atomName: element } = label.userData;
const offsetVector = this.getLabelOffsetVector(atomPosition, element);
label.position.addVectors(atomPosition, offsetVector);
label.visible = this.areLabelsShown;
label.lookAt(this.camera.position);
});
}

/*
* removes labels that situated in the labels array
*/
removeLabels() {
this.labelsGroup.children.forEach((label) => this.labelsGroup.remove(label));
}

/**
* Since we using a THREE.Points object for drawing label we need to know all atom names that should be drawn and in which
* places it should be drawn. This function creates vertices hashMap where key is atom name, value is array of vertices
* where this atom is situated. Since we don't know how many atom names there could be, we should to iterate through all
* array of atoms and obtain atom names as a keys to hashMap. If the name will already exists in the hash map we will just
* push the coordinates of the atom on which label should be drawn
* @returns {Object.<key, Array.<number>>}
* Creates a hash map representing the positions (vertices) for atom labels.
* The hash map uses atom names as keys and corresponding 3D positions as values.
* If an atom name already exists in the hash map, it appends the atom's coordinates to the associated entry.
*
* @returns {Object.<string, Array.<number>>} HashMap with atom names as keys and an array of vertices as values.
*/
createVerticesHashMap() {
const verticesHashMap = {};
Expand All @@ -141,18 +128,28 @@ export const LabelsMixin = (superclass) =>
return verticesHashMap;
}

/*
* function that creates label sprites as points.
/**
* Creates labels as sprites or points
* depending on the settings.labelsConfig.areSpritesUsed value
*/
createLabels() {
if (this.settings.labelsConfig.areSpritesUsed) {
this.createLabelsAsSprites();
} else {
this.createLabelsAsPoints();
}
this.render();
}

/**
* Creates label sprites as points.
* If we want to use a lot of labels and don't want to have a huge impact
* from rendering scene we should use Three.Points or Three.InstancedMesh.
* https://threejs.org/docs/#api/en/objects/Points
* https://threejs.org/docs/?q=instanced#api/en/objects/InstancedMesh
*/
createLabelSpritesAsPoints() {
if (this.labelsGroup.children.length) {
this.removeLabels();
}

createLabelsAsPoints() {
this.labelsGroup.clear();
const verticesHashMap = this.createVerticesHashMap();
Object.entries(verticesHashMap).forEach(([key, vertices]) => {
const texture = this.getLabelTextTexture(key);
Expand All @@ -168,32 +165,48 @@ export const LabelsMixin = (superclass) =>
this.labelsGroup.add(particles);
this.structureGroup.add(this.labelsGroup);
});
}

this.render();
/**
* Creates and positions label sprites based on atom vertices.
* Clears any existing labels, then uses a hash map of vertices to determine label placement.
* Each label, represented as a sprite, is positioned at an offset determined by the atom's radius.
* For performance considerations, when using many labels, consider using Three.Points or Three.InstancedMesh.
*
* https://threejs.org/docs/#api/en/objects/Points
* https://threejs.org/docs/?q=instanced#api/en/objects/InstancedMesh
*/
createLabelsAsSprites() {
this.labelsGroup.clear();
const verticesHashMap = this.createVerticesHashMap();
Object.entries(verticesHashMap).forEach(([key, vertices]) => {
for (let i = 0; i < vertices.length; i += 3) {
const atomPosition = new THREE.Vector3().fromArray(vertices, i);
const labelSprite = this.createLabelSprite(key, `label-for-${key}`);
const offsetVector = this.getLabelOffsetVector(atomPosition, key);
labelSprite.userData = { atomPosition, atomName: key };
labelSprite.position.addVectors(atomPosition, offsetVector);
this.labelsGroup.add(labelSprite);
}
});
this.structureGroup.add(this.labelsGroup);
}

/**
* Calculates a vector from the atom center to the camera clamped to the atom sphere radius.
* @param {THREE.Group} group - the instance of THREE group containing the atom mesh;
* @param {THREE.Mesh} atom - the instance of THREE mesh representing the atom;
* Computes an offset vector for a given atom position to position labels correctly.
* This method returns a vector pointing from the atom to the camera but with a
* length equal to the sphere radius.
* @param {THREE.Vector3} atomPosition - The 3D position of the atom.
* @param {String} element - The name of the atom.
* @returns {THREE.Vector3} - Offset vector for the label.
*/
getClampedVectorToCamera(group, atom) {
const { center: cellCenter } = this.getCellViewParams();
const atomRadius = atom.geometry.parameters.radius;
const atomPosition = atom.position.clone().add(group.position);
const vectorToCamera = this.isCameraOrthographic
? new THREE.Vector3(
this.camera.position.x - cellCenter[0],
this.camera.position.y - cellCenter[1],
this.camera.position.z - cellCenter[2],
)
: new THREE.Vector3(
this.camera.position.x - atomPosition.x,
this.camera.position.y - atomPosition.y,
this.camera.position.z - atomPosition.z,
);
const clampedVectorToCamera = vectorToCamera.clampLength(atomRadius, atomRadius);
return clampedVectorToCamera;
getLabelOffsetVector(atomPosition, element) {
const vectorToCamera = new THREE.Vector3().subVectors(
this.camera.position,
atomPosition,
);
const offsetLength = this.getAtomRadiusByElement(element);
return vectorToCamera.normalize().multiplyScalar(offsetLength);
}

/**
Expand All @@ -203,16 +216,6 @@ export const LabelsMixin = (superclass) =>
this.areLabelsShown = !this.areLabelsShown;
this.labelsGroup.visible = this.areLabelsShown;

const atomGroups = this.scene
.getObjectByName(ATOM_GROUP_NAME)
.parent.children.filter((object) => object.name.includes(ATOM_GROUP_NAME));
const labels = atomGroups
.map((group) => group.children.filter((child) => child instanceof THREE.Sprite))
.flat();
labels.forEach((label) => {
label.visible = this.areLabelsShown;
});

this.render();
}
};
7 changes: 6 additions & 1 deletion src/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default {
areLabelsInitiallyShown: false,
isViewAdjustable: true,
labelsConfig: {
areSpritesUsed: true,
fontFace: "Arial",
fontSize: 96,
fontWeight: "Bold",
Expand All @@ -48,7 +49,11 @@ export default {
depthFunc: THREE.NotEqualDepth,
transparent: true,
},

labelSpriteConfig: {
transparent: true,
depthFunc: THREE.LessEqualDepth,
depthTest: true,
},
boundaryConditionTypeColors: {
bc1: [0xffff00, 0xffff00],
bc2: [0x0000ff, 0x0000ff],
Expand Down
4 changes: 2 additions & 2 deletions src/wave.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,12 +322,12 @@ export class Wave extends mix(WaveBase).with(
this.drawBoundaries();
if (this.isDrawBondsEnabled) this.drawBonds();
this.render();
this.createLabelSpritesAsPoints();
this.createLabels();
this.refillChosenAtoms();
}

render() {
this.adjustLabelsToCameraPosition(this.scene, this.camera);
this.adjustLabelsToCameraPosition();
this.renderer.render(this.scene, this.camera);
if (this.renderer2) this.renderer2.render(this.scene2, this.camera2);
}
Expand Down
35 changes: 33 additions & 2 deletions tests/__tests__/mixins/labels.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,40 @@ describe("Atom labels", () => {
atoms = atomGroup.children.filter((object) => object.type === "Mesh");
});

test("Labels are created for every atom and positioned in the center of atom with the offset towards camera", async () => {
const basisAtomsNumber = wave.structure.basis.elements.length;

atoms.forEach((atom) => {
const atomName = atom.name.split("-")[0];
const atomPosition = new THREE.Vector3().setFromMatrixPosition(atom.matrixWorld);

const offsetVector = wave.getLabelOffsetVector(atomPosition, atomName);
const expectedLabelPosition = atomPosition.clone().add(offsetVector);

const labelSprite = labelGroup.children.find((label) => {
return (
label.userData.atomName === atomName &&
label.userData.atomPosition.distanceTo(atomPosition) < 0.00001 &&
label.position.distanceTo(expectedLabelPosition) < 0.00001
);
});

expect(labelSprite).toBeTruthy();
});

expect(atoms.length).toEqual(basisAtomsNumber);
});

test("Labels are created for every atom and positioned in the center of atom", async () => {
// set the flag to false to disable sprites and use points instead
wave = getWaveInstance({ labelsConfig: { areSpritesUsed: false } });
const atomGroup = wave.scene.getObjectByName(ATOM_GROUP_NAME);
labelGroup = wave.scene.getObjectByName(LABELS_GROUP_NAME);
labels = labelGroup.children;
atoms = atomGroup.children.filter((object) => object.type === "Mesh");

const basisAtomsNumber = wave.structure.basis.elements.length;
const isAllAtomsHaveLabels = atoms.every((atom) => {
const doAllAtomsHaveLabels = atoms.every((atom) => {
const atomName = atom.name.split("-")[0];
const labelName = `labels-for-${atomName}`;
const labelPointsByAtomName = labels.find(
Expand All @@ -41,7 +72,7 @@ describe("Atom labels", () => {
});

expect(atoms.length).toEqual(basisAtomsNumber);
expect(isAllAtomsHaveLabels).toBeTruthy();
expect(doAllAtomsHaveLabels).toBeTruthy();
});

test("Initial labels visibility matches the settings", async () => {
Expand Down

0 comments on commit 9f8ef23

Please sign in to comment.