Skip to content

Commit

Permalink
Render atom legends in legend scene (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
superstar54 authored Nov 26, 2024
1 parent a746da8 commit 7fe5f9a
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 58 deletions.
2 changes: 2 additions & 0 deletions src/atoms/AtomsViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ class AtomsViewer {
this.weas.eventHandlers.dispatchViewerUpdated({ colorType: newValue });
// update the bondManager settings
this.atomManager.init();
this.guiManager.updateLegend();
this.bondManager.init();
this.polyhedraManager.init();
}
Expand Down Expand Up @@ -483,6 +484,7 @@ class AtomsViewer {
this.VFManager.drawVectorFields();
this.highlightManager.drawHighlightAtoms();
this.ALManager.drawAtomLabels();
this.guiManager.updateLegend();
this.ready = true;
this.weas.tjs.render();
}
Expand Down
219 changes: 161 additions & 58 deletions src/atoms/plugins/AtomsLegend.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import * as THREE from "three";

export default class AtomsLegend {
constructor(viewer, guiConfig) {
this.viewer = viewer;
this.guiConfig = guiConfig;
this.legendContainer = null;
this.legendSprites = [];

if (this.guiConfig.legend && this.guiConfig.legend.enabled) {
this.addLegend();
Expand All @@ -13,62 +15,87 @@ export default class AtomsLegend {
// Remove existing legend if any
this.removeLegend();

// Create legend container
this.legendContainer = document.createElement("div");
this.legendContainer.id = "legend-container";
this.legendContainer.style.position = "absolute";
this.legendContainer.style.backgroundColor = "rgba(255, 255, 255, 0.8)";
this.legendContainer.style.padding = "10px";
// bottom margin to avoid overlapping with other GUI elements
this.legendContainer.style.marginBottom = "30px";
this.legendContainer.style.borderRadius = "5px";
this.legendContainer.style.zIndex = "1000";

// Positioning based on configuration
this.setLegendPosition(this.legendContainer);

// Add entries for each unique element
// Starting coordinates for legend entries in normalized device coordinates (NDC)
let xStart, yStart;
const xOffset = 2; // Adjust spacing
const yOffset = -2; // Adjust spacing

// Determine position based on configuration
const position = this.guiConfig.legend.position || "top-right";
switch (position) {
case "top-left":
xStart = -0.9;
yStart = 0.9;
break;
case "top-right":
xStart = 0.7;
yStart = 0.9;
break;
case "bottom-left":
xStart = -0.9;
yStart = -0.7;
break;
case "bottom-right":
xStart = -5;
yStart = -2;
break;
default:
xStart = 0.7;
yStart = 0.9;
break;
}

// Create legend entries
let entryIndex = 0;
// max radius of settings
const maxRadius = this.viewer.atomManager.getMaxRadius();
const minRadius = this.viewer.atomManager.getMinRadius();
console.log("maxRadius: ", maxRadius);
console.log("minRadius: ", minRadius);

Object.entries(this.viewer.atomManager.settings).forEach(([symbol, setting]) => {
const legendEntry = document.createElement("div");
legendEntry.style.display = "flex";
legendEntry.style.alignItems = "center";
legendEntry.style.marginBottom = "5px";

// Sphere representation
const sphereCanvas = document.createElement("canvas");
sphereCanvas.width = 20;
sphereCanvas.height = 20;
const context = sphereCanvas.getContext("2d");
if (typeof setting.color === "string") {
context.fillStyle = setting.color;
} else {
context.fillStyle = `#${setting.color.getHexString()}`;
}
const radius = Math.min(setting.radius * 10, 8); // Cap radius to maintain a spherical look
context.beginPath();
context.arc(10, 10, radius, 0, Math.PI * 2);
context.fill();

legendEntry.appendChild(sphereCanvas);

// Symbol label
const elementLabel = document.createElement("span");
elementLabel.textContent = ` ${symbol}`;
elementLabel.style.marginLeft = "5px";
elementLabel.style.fontSize = "14px";
legendEntry.appendChild(elementLabel);

this.legendContainer.appendChild(legendEntry);
});
const message = `${symbol}`;

// Create text sprite
const textSprite = createTextSprite(message, {
fontsize: 96, // Adjust as needed
fontface: "Arial",
textColor: { r: 0, g: 0, b: 0, a: 1.0 },
backgroundColor: { r: 255, g: 255, b: 255, a: 0.0 },
scale: 1.5,
});

// Create circle sprite
const color = typeof setting.color === "string" ? new THREE.Color(setting.color) : setting.color;

// Append legend to viewer container
this.viewer.tjs.containerElement.appendChild(this.legendContainer);
const scale = Math.min(2.0, Math.max(1, setting.radius));
console.log("scale: ", scale);
const circleSprite = createCircleSprite(color, { scale: scale });

// Position sprites
const yPosition = yStart - entryIndex * yOffset;
circleSprite.position.set(xStart, yPosition, -1);
textSprite.position.set(xStart + xOffset, yPosition, -1);

// Add sprites to the camera
this.viewer.tjs.legendScene.add(circleSprite);
this.viewer.tjs.legendScene.add(textSprite);

this.legendSprites.push(circleSprite, textSprite);

entryIndex += 1;
});
}

removeLegend() {
if (this.legendContainer) {
this.legendContainer.remove();
this.legendContainer = null;
if (this.legendSprites.length > 0) {
this.legendSprites.forEach((sprite) => {
this.viewer.tjs.legendScene.remove(sprite);
sprite.material.map.dispose();
sprite.material.dispose();
// No geometry to dispose for sprites
});
this.legendSprites = [];
}
}

Expand All @@ -79,12 +106,88 @@ export default class AtomsLegend {
this.removeLegend();
}
}
}

setLegendPosition(legendContainer) {
const position = this.guiConfig.legend.position || "top-right";
legendContainer.style.top = position.includes("top") ? "10px" : "";
legendContainer.style.bottom = position.includes("bottom") ? "10px" : "";
legendContainer.style.left = position.includes("left") ? "10px" : "";
legendContainer.style.right = position.includes("right") ? "10px" : "";
function createCircleSprite(color, parameters = {}) {
const radius = parameters.radius || 32;

const canvas = document.createElement("canvas");
const size = radius * 2;
canvas.width = size;
canvas.height = size;

const context = canvas.getContext("2d");
context.beginPath();
context.arc(radius, radius, radius - 2, 0, 2 * Math.PI, false);
context.fillStyle = `#${color.getHexString()}`;
context.fill();

const texture = new THREE.CanvasTexture(canvas);
texture.minFilter = THREE.LinearFilter;
texture.generateMipmaps = false;

const spriteMaterial = new THREE.SpriteMaterial({ map: texture });
const sprite = new THREE.Sprite(spriteMaterial);

// Scale the sprite
const scaleFactor = parameters.scale || 0.1; // Adjust based on desired size
sprite.scale.set(scaleFactor, scaleFactor, 1);

return sprite;
}

function createTextSprite(message, parameters = {}) {
const fontface = parameters.fontface || "Arial";
const fontsize = parameters.fontsize || 96;
const borderThickness = parameters.borderThickness || 0;
const borderColor = parameters.borderColor || { r: 0, g: 0, b: 0, a: 1.0 };
const backgroundColor = parameters.backgroundColor || { r: 255, g: 255, b: 255, a: 0.0 };
const textColor = parameters.textColor || { r: 0, g: 0, b: 0, a: 1.0 };

const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
context.font = `${fontsize}px ${fontface}`;

// Measure the text size
const metrics = context.measureText(message);
const textWidth = metrics.width;
const textHeight = fontsize;

// Adjust canvas size based on text
canvas.width = textWidth + borderThickness * 2;
canvas.height = textHeight + borderThickness * 2;

// Background
context.fillStyle = `rgba(${backgroundColor.r},${backgroundColor.g},${backgroundColor.b},${backgroundColor.a})`;
context.fillRect(0, 0, canvas.width, canvas.height);

// Border
if (borderThickness > 0) {
context.strokeStyle = `rgba(${borderColor.r},${borderColor.g},${borderColor.b},${borderColor.a})`;
context.lineWidth = borderThickness;
context.strokeRect(0, 0, canvas.width, canvas.height);
}

// Text
context.fillStyle = `rgba(${textColor.r},${textColor.g},${textColor.b},${textColor.a})`;
context.font = `${fontsize}px ${fontface}`;
context.textBaseline = "top";
context.fillText(message, borderThickness, borderThickness);

// Create texture
const texture = new THREE.CanvasTexture(canvas);
texture.minFilter = THREE.LinearFilter;
texture.generateMipmaps = false;

// Create sprite material
const spriteMaterial = new THREE.SpriteMaterial({ map: texture });

// Create sprite
const sprite = new THREE.Sprite(spriteMaterial);

// Scale the sprite
const scaleFactor = parameters.scale || 1;
sprite.scale.set((scaleFactor * canvas.width) / canvas.height, scaleFactor, 1);

return sprite;
}
10 changes: 10 additions & 0 deletions src/atoms/plugins/atom.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ export class AtomManager {
this.settings[symbol] = setting;
}

getMaxRadius() {
/* Get the maximum radius of the atoms */
return Math.max(...Object.values(this.settings).map((setting) => setting.radius));
}

getMinRadius() {
/* Get the minimum radius of the atoms */
return Math.min(...Object.values(this.settings).map((setting) => setting.radius));
}

clearMeshes() {
/* Remove highlighted atom meshes from the selectedAtomsMesh group */
Object.values(this.meshes).forEach((mesh) => {
Expand Down
39 changes: 39 additions & 0 deletions src/core/blendjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,21 @@ export class BlendJS {
this.coordScene.add(directionalLight);
}

createLegendScene() {
this.legendScene = new THREE.Scene();

const legendSceneRatio = 0.3;
this.legendSceneView = {
left: 0.8,
bottom: 0,
width: this.sceneView.width * legendSceneRatio,
height: this.sceneView.height * legendSceneRatio,
};

this.legendCamera = new THREE.OrthographicCamera(this.orthographicCamera.left, this.orthographicCamera.right, this.orthographicCamera.top, this.orthographicCamera.bottom, 1, 2000);
this.legendCamera.position.set(0, 0, 100);
}

get cameraType() {
return this._cameraType;
}
Expand Down Expand Up @@ -168,6 +183,7 @@ export class BlendJS {
this.containerElement.addEventListener("wheel", this.render.bind(this));
this.containerElement.addEventListener("atomsUpdated", this.render.bind(this));
this.createCoordScene();
this.createLegendScene();
}

addObject(name, geometry, material) {
Expand Down Expand Up @@ -222,13 +238,24 @@ export class BlendJS {
this.camera.updateProjectionMatrix();
this.coordCamera.updateProjectionMatrix();

// Update legendCamera
// Compute the aspect ratio for the legendCamera based on the legendSceneView dimensions
const legendWidth = clientWidth * this.legendSceneView.width;
const legendHeight = clientHeight * this.legendSceneView.height;
const legendAspect = legendWidth / legendHeight;
const frustumHeightLegend = this.legendCamera.top - this.legendCamera.bottom;
this.legendCamera.left = (-frustumHeightLegend * legendAspect) / 2;
this.legendCamera.right = (frustumHeightLegend * legendAspect) / 2;
this.legendCamera.updateProjectionMatrix();

// Resize all renderers
Object.values(this.renderers).forEach((rndr) => {
rndr.renderer.setSize(clientWidth, clientHeight);
});
this.viewerRect = this.containerElement.getBoundingClientRect();
this.render();
}

//
updateCameraAndControls({ lookAt = null, direction = [0, 0, 1], distance = null, zoom = 1, fov = 50 }) {
/*
Expand Down Expand Up @@ -358,6 +385,18 @@ export class BlendJS {
this.coordSceneView.height,
this.renderers["MainRenderer"].renderer,
);
// this.legendCamera.position.copy(this.camera.position);
this.renderSceneInfo(
this.legendScene,
this.legendCamera,
this.legendSceneView.left,
this.legendSceneView.bottom,
this.legendSceneView.width,
this.legendSceneView.height,
this.renderers["MainRenderer"].renderer,
);
// legend

this.controls.update();
}

Expand Down
Binary file modified tests/e2e/gui.spec.js-snapshots/ModelStyle-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/e2e/gui.spec.js-snapshots/Species-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 7fe5f9a

Please sign in to comment.