vae-fdm / web /static /js /mesh-renderer.js
Efradeca's picture
feat: web demo parity with desktop (web/static/js/mesh-renderer.js)
10606f9 verified
/**
* Three.js mesh renderer for FDM structures.
* Matches the desktop PyVista app visual style.
*/
import * as THREE from 'three';
import { mapScalarsToColors } from './colormap.js';
export class MeshRenderer {
constructor(scene) {
this.scene = scene;
this.targetGroup = new THREE.Group();
this.surfaceGroup = new THREE.Group();
this.supportsGroup = new THREE.Group();
this.edgesGroup = new THREE.Group();
this.cpGroup = new THREE.Group(); // mirrored control points (orange)
this.cpDragGroup = new THREE.Group(); // draggable control points (red)
scene.add(this.targetGroup);
scene.add(this.surfaceGroup);
scene.add(this.supportsGroup);
scene.add(this.edgesGroup);
scene.add(this.cpGroup);
scene.add(this.cpDragGroup);
this._edgeGeo = null;
this._surfaceGeo = null;
this._targetGeo = null;
this._topology = null;
}
init(topology) {
this._topology = topology;
const { edges, boundary, num_vertices, num_uv } = topology;
// --- Structural edges (line_width=3 in desktop, but WebGL lines are 1px) ---
// Use cylinders for thick edges
const numEdges = edges.length;
const edgePositions = new Float32Array(numEdges * 2 * 3);
const edgeColors = new Float32Array(numEdges * 2 * 3);
edgeColors.fill(0.5);
this._edgeGeo = new THREE.BufferGeometry();
this._edgeGeo.setAttribute('position', new THREE.BufferAttribute(edgePositions, 3));
this._edgeGeo.setAttribute('color', new THREE.BufferAttribute(edgeColors, 3));
// Note: WebGL ignores linewidth > 1 on most platforms.
// For thicker edges, we render a second set of slightly offset lines.
const edgeMat = new THREE.LineBasicMaterial({ vertexColors: true, linewidth: 2 });
const edgeMesh = new THREE.LineSegments(this._edgeGeo, edgeMat);
this.edgesGroup.add(edgeMesh);
// --- Predicted surface (steelblue, opacity 0.4) ---
const nu = num_uv;
const surfPos = new Float32Array(num_vertices * 3);
const indices = [];
for (let i = 0; i < nu - 1; i++) {
for (let j = 0; j < nu - 1; j++) {
const a = i * nu + j;
const b = (i + 1) * nu + j;
const c = (i + 1) * nu + j + 1;
const d = i * nu + j + 1;
indices.push(a, b, c, a, c, d);
}
}
this._surfaceGeo = new THREE.BufferGeometry();
this._surfaceGeo.setAttribute('position', new THREE.BufferAttribute(surfPos, 3));
this._surfaceGeo.setIndex(indices);
this._surfaceGeo.computeVertexNormals();
const surfMat = new THREE.MeshPhongMaterial({
color: 0x4682b4,
transparent: true,
opacity: 0.4,
side: THREE.DoubleSide,
depthWrite: false,
shininess: 30,
});
this.surfaceGroup.add(new THREE.Mesh(this._surfaceGeo, surfMat));
// --- Target wireframe (gray, opacity 0.3) ---
this._targetGeo = new THREE.BufferGeometry();
const targetPos = new Float32Array(num_vertices * 3);
this._targetGeo.setAttribute('position', new THREE.BufferAttribute(targetPos, 3));
const wireIndices = [];
for (let i = 0; i < nu; i++) {
for (let j = 0; j < nu; j++) {
const idx = i * nu + j;
if (j < nu - 1) wireIndices.push(idx, idx + 1);
if (i < nu - 1) wireIndices.push(idx, idx + nu);
}
}
this._targetGeo.setIndex(wireIndices);
const wireMat = new THREE.LineBasicMaterial({ color: 0x999999, transparent: true, opacity: 0.3 });
this.targetGroup.add(new THREE.LineSegments(this._targetGeo, wireMat));
// --- Support spheres (red, matching desktop) ---
const sphereGeo = new THREE.SphereGeometry(0.1, 10, 10);
const sphereMat = new THREE.MeshPhongMaterial({ color: 0xff3333 });
boundary.forEach(() => {
this.supportsGroup.add(new THREE.Mesh(sphereGeo, sphereMat));
});
}
update(data, colorMode) {
if (!this._topology) return;
const { edges, boundary } = this._topology;
const { target, predicted, q, forces } = data;
const scalars = colorMode === 'forces' ? forces : q;
// Edge positions
const posArr = this._edgeGeo.attributes.position.array;
for (let i = 0; i < edges.length; i++) {
const [u, v] = edges[i];
const pu = predicted[u], pv = predicted[v];
const off = i * 6;
posArr[off] = pu[0]; posArr[off + 1] = pu[1]; posArr[off + 2] = pu[2];
posArr[off + 3] = pv[0]; posArr[off + 4] = pv[1]; posArr[off + 5] = pv[2];
}
this._edgeGeo.attributes.position.needsUpdate = true;
// Edge colors from scalars
const vmin = Math.min(...scalars);
const vmax = Math.max(...scalars);
const edgeColors = mapScalarsToColors(scalars, vmin, vmax);
const colArr = this._edgeGeo.attributes.color.array;
for (let i = 0; i < edges.length; i++) {
const r = edgeColors[i * 3], g = edgeColors[i * 3 + 1], b = edgeColors[i * 3 + 2];
const off = i * 6;
colArr[off] = r; colArr[off + 1] = g; colArr[off + 2] = b;
colArr[off + 3] = r; colArr[off + 4] = g; colArr[off + 5] = b;
}
this._edgeGeo.attributes.color.needsUpdate = true;
// Surface
const surfPos = this._surfaceGeo.attributes.position.array;
for (let i = 0; i < predicted.length; i++) {
surfPos[i * 3] = predicted[i][0];
surfPos[i * 3 + 1] = predicted[i][1];
surfPos[i * 3 + 2] = predicted[i][2];
}
this._surfaceGeo.attributes.position.needsUpdate = true;
this._surfaceGeo.computeVertexNormals();
// Target
const tgtPos = this._targetGeo.attributes.position.array;
for (let i = 0; i < target.length; i++) {
tgtPos[i * 3] = target[i][0];
tgtPos[i * 3 + 1] = target[i][1];
tgtPos[i * 3 + 2] = target[i][2];
}
this._targetGeo.attributes.position.needsUpdate = true;
// Supports
const supports = this.supportsGroup.children;
boundary.forEach((vi, idx) => {
if (idx < supports.length) {
supports[idx].position.set(predicted[vi][0], predicted[vi][1], predicted[vi][2]);
}
});
return { vmin, vmax };
}
/**
* Temporarily tint the predicted-surface material to a specific color+opacity,
* e.g. for VAE diversity animation frames. Call resetSurfaceTint() to restore.
*/
setSurfaceTint(hexColor, opacity = 0.4) {
for (const child of this.surfaceGroup.children) {
if (child.material) {
if (this._surfOrig === undefined) {
this._surfOrig = {
color: child.material.color.getHex(),
opacity: child.material.opacity,
};
}
child.material.color.setHex(hexColor);
child.material.opacity = opacity;
child.material.needsUpdate = true;
}
}
}
resetSurfaceTint() {
if (this._surfOrig === undefined) return;
for (const child of this.surfaceGroup.children) {
if (child.material) {
child.material.color.setHex(this._surfOrig.color);
child.material.opacity = this._surfOrig.opacity;
child.material.needsUpdate = true;
}
}
}
/**
* Update mirrored control points (orange dots, matching desktop).
* allCp is array of [x,y,z] for ALL 16 mirrored control points.
*/
updateControlPoints(allCp) {
// Remove old
while (this.cpGroup.children.length) this.cpGroup.remove(this.cpGroup.children[0]);
const geo = new THREE.SphereGeometry(0.15, 8, 8);
const mat = new THREE.MeshPhongMaterial({ color: 0xff8c00, transparent: true, opacity: 0.5 });
for (const pt of allCp) {
const s = new THREE.Mesh(geo, mat);
s.position.set(pt[0], pt[1], pt[2]);
this.cpGroup.add(s);
}
}
}