crystalView = {
const [Nx, Ny, Nz] = [3, 3, 3];
const isoLevel = 0.3;
const a = 2;
const radius = 0.3;
const cameraFov = 45, cameraAspect = 1, cameraNear = 0.1, cameraFar = 1000;
const backgroundColor = 0xffffff;
const scene = new three.Scene();
const camera = new three.PerspectiveCamera(cameraFov, cameraAspect, cameraNear, cameraFar);
camera.position.set(3, 3, 3);
const renderer = new three.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(width, width);
renderer.setPixelRatio(devicePixelRatio);
renderer.setClearColor(backgroundColor, 1);
renderer.localClippingEnabled = true;
const container = html`<div style="background:white; width:${width}px; height:${width}px;"></div>`;
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enablePan = false;
controls.enableZoom = true;
controls.minPolarAngle = 0;
controls.maxPolarAngle = Math.PI / 2;
controls.enableDamping = true;
scene.add(new three.AmbientLight(0xffffff, 0.5));
const dl = new three.DirectionalLight(0xffffff, 0.8);
dl.position.set(5, 5, 5);
scene.add(dl);
const [lattice, basisName] = latticeKey.split(" | ");
const entry = lattices[lattice].bases[basisName];
const params = lattices[lattice].parameters.map(p => lattices[lattice].defaults[p]);
const vectors = (entry.vectors || lattices[lattice].vectors)(params);
const basis = entry.basis;
const species = entry.species;
const scaled = vectors.map(v => new three.Vector3(...v).multiplyScalar(a));
const scaledLengths = scaled.map(v => v.length());
const shift = scaled.reduce((acc, vec) => acc.addScaledVector(vec, -0.5), new three.Vector3());
// ─── Planes ───
const planes = [];
for (let i = 0; i < 3; i++) {
const normal = scaled[(i + 1) % 3].clone().cross(scaled[(i + 2) % 3]).normalize();
const half = scaled[i].clone().multiplyScalar(0.5);
planes.push(new three.Plane().setFromNormalAndCoplanarPoint(normal, half));
planes.push(new three.Plane().setFromNormalAndCoplanarPoint(normal.clone().negate(), half.clone().negate()));
}
planes.forEach(p => { if (p.distanceToPoint(new three.Vector3(0, 0, 0)) < 0) p.negate(); });
const exteriorPlanes = planes;
const interiorPlanes = planes.map(p => p.clone().negate());
// ─── Materials ───
const speciesMats = {};
species.forEach(sp => {
if (!speciesMats[sp]) {
const col = new three.Color(colorMap[sp]?.color ?? 0x008080);
speciesMats[sp] = {
outside: new three.MeshStandardMaterial({
color: col,
transparent: true,
opacity: 0.2,
side: three.DoubleSide,
clippingPlanes: interiorPlanes,
clipIntersection: true,
depthWrite: false
}),
inside: new three.MeshStandardMaterial({
color: col,
transparent: false,
opacity: 0.6,
side: three.DoubleSide,
clippingPlanes: exteriorPlanes,
clipIntersection: false,
depthWrite: true
})
};
}
});
// ─── Groups ───
const atomsGroup = new three.Group();
const cellEdgesGroup = new three.Group();
const supercellEdgesGroup = new three.Group();
scene.add(atomsGroup, cellEdgesGroup, supercellEdgesGroup);
// ─── Atoms ───
const atoms = [];
for (let ix = 0; ix <= Nx; ix++) {
for (let iy = 0; iy <= Ny; iy++) {
for (let iz = 0; iz <= Nz; iz++) {
basis.forEach(([u, v, w], i) => {
if ((ix < Nx || u === 0) && (iy < Ny || v === 0) && (iz < Nz || w === 0)) {
const fx = ix + u, fy = iy + v, fz = iz + w;
const pos = new three.Vector3()
.addScaledVector(scaled[0], fx)
.addScaledVector(scaled[1], fy)
.addScaledVector(scaled[2], fz)
.add(shift);
atoms.push(pos);
const sp = species[i];
const mats = speciesMats[sp];
const Z = colorMap[sp]?.Z || 20;
const r = radius * (0.5 + 0.5 * (1 - Math.exp(-Z / 20)));
const geo = new three.SphereGeometry(r, 32, 32);
const outMesh = new three.Mesh(geo, mats.outside);
outMesh.position.copy(pos);
outMesh.renderOrder = 0;
atomsGroup.add(outMesh);
const inMesh = new three.Mesh(geo, mats.inside);
inMesh.position.copy(pos);
inMesh.renderOrder = 1;
atomsGroup.add(inMesh);
}
});
}
}
}
// ─── Electron Density (only for Diamond) ───
if (showDensity) { // (lattice === "FCC" && basisName === "Diamond (C)") {
const Ngrid = 40;
// 1) Build density grid in fractional (u,v,w)
const grid = Array.from({ length: Ngrid + 1 }, (_, i) =>
Array.from({ length: Ngrid + 1 }, (_, j) =>
Array.from({ length: Ngrid + 1 }, (_, k) => {
const u = i / Ngrid, v = j / Ngrid, w = k / Ngrid;
const pos = new three.Vector3()
.addScaledVector(scaled[0], u)
.addScaledVector(scaled[1], v)
.addScaledVector(scaled[2], w)
.add(shift);
let rho = 0;
atoms.forEach(atomPos => {
const d2 = pos.distanceToSquared(atomPos);
rho += Math.exp(-5 * d2);
});
return rho;
})
)
);
const gradientGrid = Array.from({ length: Ngrid + 1 }, (_, i) =>
Array.from({ length: Ngrid + 1 }, (_, j) =>
Array.from({ length: Ngrid + 1 }, (_, k) => new three.Vector3())
)
);
for (let i = 1; i < Ngrid; i++) {
for (let j = 1; j < Ngrid; j++) {
for (let k = 1; k < Ngrid; k++) {
const dx = (grid[i+1][j][k] - grid[i-1][j][k]) * 0.5;
const dy = (grid[i][j+1][k] - grid[i][j-1][k]) * 0.5;
const dz = (grid[i][j][k+1] - grid[i][j][k-1]) * 0.5;
gradientGrid[i][j][k].set(dx, dy, dz).normalize();
}
}
}
// 2) Interpolate in fractional space (linear interpolation)
function interpolateVerts(p1, p2, i, j, k) {
const [dx1, dy1, dz1] = cubeVerts[p1];
const [dx2, dy2, dz2] = cubeVerts[p2];
const idx1 = i + dx1, idy1 = j + dy1, idz1 = k + dz1;
const idx2 = i + dx2, idy2 = j + dy2, idz2 = k + dz2;
const v1 = grid[idx1][idy1][idz1];
const v2 = grid[idx2][idy2][idz2];
const mu = (v2 === v1) ? 0 : (isoLevel - v1) / (v2 - v1);
// Interpolate position
const u = (idx1 + mu * (idx2 - idx1)) / Ngrid;
const v = (idy1 + mu * (idy2 - idy1)) / Ngrid;
const w = (idz1 + mu * (idz2 - idz1)) / Ngrid;
const pos = new three.Vector3()
.addScaledVector(scaled[0], u)
.addScaledVector(scaled[1], v)
.addScaledVector(scaled[2], w)
.add(shift);
// Interpolate normal (gradient vector)
const g1 = gradientGrid[idx1][idy1][idz1];
const g2 = gradientGrid[idx2][idy2][idz2];
const normal = g1.clone().lerp(g2, mu).normalize();
return { pos, normal };
}
// 3) Run Marching Cubes
const vertices = [];
const normals = [];
const faces = [];
for (let i = 0; i < Ngrid; i++) {
for (let j = 0; j < Ngrid; j++) {
for (let k = 0; k < Ngrid; k++) {
let cubeindex = 0;
for (let n = 0; n < 8; n++) {
const [dx, dy, dz] = cubeVerts[n];
const ii = (i + dx);
const jj = (j + dy);
const kk = (k + dz);
if (grid[ii][jj][kk] < isoLevel) cubeindex |= 1 << n;
}
const edges = edgeTable[cubeindex];
if (!edges) continue;
const vertList = [];
for (let e = 0; e < 12; e++) {
if (edges & (1 << e)) {
const [p1, p2] = edgeConnections[e];
vertList[e] = interpolateVerts(p1, p2, i, j, k);
}
}
for (let t = 0; triTable[cubeindex][t] !== -1; t += 3) {
const a = vertList[triTable[cubeindex][t]];
const b = vertList[triTable[cubeindex][t+1]];
const c = vertList[triTable[cubeindex][t+2]];
if (!a || !b || !c) continue;
const idx = vertices.length / 3;
// Push positions
vertices.push(
a.pos.x, a.pos.y, a.pos.z,
b.pos.x, b.pos.y, b.pos.z,
c.pos.x, c.pos.y, c.pos.z
);
// Push normals
normals.push(
a.normal.x, a.normal.y, a.normal.z,
b.normal.x, b.normal.y, b.normal.z,
c.normal.x, c.normal.y, c.normal.z
);
// Push face indices
faces.push(idx, idx+1, idx+2);
}
}
}
}
// 4) Build isosurface mesh
const isoGeometry = new three.BufferGeometry();
isoGeometry.setAttribute('position', new three.Float32BufferAttribute(vertices, 3));
isoGeometry.setAttribute('normal', new three.Float32BufferAttribute(normals, 3));
isoGeometry.setIndex(faces);
// merge nearby vertices to stitch faces together
if (three.BufferGeometryUtils) {
three.BufferGeometryUtils.mergeVertices(isoGeometry, 1e-6);
}
isoGeometry.computeVertexNormals();
const isoMaterial = new three.MeshStandardMaterial({
color: 0x00ffff,
side: three.DoubleSide,
transparent: true,
opacity: 0.5,
clippingPlanes: exteriorPlanes,
clipIntersection: false,
depthWrite: false
});
const isoMesh = new three.Mesh(isoGeometry, isoMaterial);
isoMesh.renderOrder = 1;
scene.add(isoMesh);
}
// ─── Helper to Create Corners ───
function createCorners(scaleVectors, signs) {
const corners = [];
signs.forEach(s1 => signs.forEach(s2 => signs.forEach(s3 => {
corners.push(
new three.Vector3()
.addScaledVector(scaleVectors[0], s1 / 2)
.addScaledVector(scaleVectors[1], s2 / 2)
.addScaledVector(scaleVectors[2], s3 / 2)
);
})));
return corners;
}
const corners = createCorners(scaled, [-1, 1]);
const superCorners = [
new three.Vector3().addScaledVector(scaled[0], -0.5).addScaledVector(scaled[1], -0.5).addScaledVector(scaled[2], -0.5),
new three.Vector3().addScaledVector(scaled[0], Nx-0.5).addScaledVector(scaled[1], -0.5).addScaledVector(scaled[2], -0.5),
new three.Vector3().addScaledVector(scaled[0], -0.5).addScaledVector(scaled[1], Ny-0.5).addScaledVector(scaled[2], -0.5),
new three.Vector3().addScaledVector(scaled[0], Nx-0.5).addScaledVector(scaled[1], Ny-0.5).addScaledVector(scaled[2], -0.5),
new three.Vector3().addScaledVector(scaled[0], -0.5).addScaledVector(scaled[1], -0.5).addScaledVector(scaled[2], Nz-0.5),
new three.Vector3().addScaledVector(scaled[0], Nx-0.5).addScaledVector(scaled[1], -0.5).addScaledVector(scaled[2], Nz-0.5),
new three.Vector3().addScaledVector(scaled[0], -0.5).addScaledVector(scaled[1], Ny-0.5).addScaledVector(scaled[2], Nz-0.5),
new three.Vector3().addScaledVector(scaled[0], Nx-0.5).addScaledVector(scaled[1], Ny-0.5).addScaledVector(scaled[2], Nz-0.5)
];
// ─── Draw Edges ───
const edgeMat = new three.LineBasicMaterial({ color: 0x222222, depthTest: true });
corners.forEach((c1, i) =>
corners.slice(i + 1).forEach(c2 => {
const d = c1.distanceTo(c2);
if (
Math.abs(d - scaledLengths[0]) < 1e-6 ||
Math.abs(d - scaledLengths[1]) < 1e-6 ||
Math.abs(d - scaledLengths[2]) < 1e-6
) {
const line = new three.Line(
new three.BufferGeometry().setFromPoints([c1, c2]),
edgeMat
);
line.renderOrder = 2;
cellEdgesGroup.add(line);
}
})
);
const superMat = new three.LineBasicMaterial({ color: 0xcccccc, depthTest: false });
superCorners.forEach((c1, i) =>
superCorners.slice(i + 1).forEach(c2 => {
const d = c1.distanceTo(c2);
if (
Math.abs(d - scaledLengths[0] * Nx) < 1e-6 ||
Math.abs(d - scaledLengths[1] * Ny) < 1e-6 ||
Math.abs(d - scaledLengths[2] * Nz) < 1e-6
) {
const superLine = new three.Line(
new three.BufferGeometry().setFromPoints([c1, c2]),
superMat
);
superLine.renderOrder = 1;
supercellEdgesGroup.add(superLine);
}
})
);
// ─── Animate ───
function animate() {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
return container;
}