@peterspackman/occjs
Version:
JavaScript/WebAssembly bindings for OCC - a quantum chemistry and crystallography library
1,058 lines (921 loc) • 54.4 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Elastic Tensor Analyzer - Simple</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#059669',
secondary: '#0f766e',
}
}
}
}
</script>
</head>
<body class="bg-gray-50 font-sans">
<div class="max-w-6xl mx-auto p-6">
<div class="bg-white rounded-lg shadow-lg p-8">
<h1 class="text-3xl font-bold text-gray-800 text-center mb-8">Elastic Tensor Analyzer</h1>
<div class="mb-8">
<label for="tensorInput" class="block text-sm font-semibold text-gray-700 mb-3">
Elastic Stiffness Matrix (6×6, in GPa):
</label>
<textarea
id="tensorInput"
class="w-full h-32 p-3 border-2 border-gray-300 rounded-lg font-mono text-sm resize-y focus:border-primary focus:outline-none transition-colors"
placeholder="Enter 6x6 elastic stiffness matrix (Voigt notation)...
Full matrix (6 values per row) or upper triangular format supported."></textarea>
<button
onclick="analyzeTensor()"
class="mt-4 bg-primary hover:bg-secondary text-white px-6 py-3 rounded-lg font-semibold transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed">
Analyze Tensor
</button>
</div>
<div id="loading" class="text-center text-gray-600 italic hidden">
<div class="flex items-center justify-center">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mr-3"></div>
Loading OCC module...
</div>
</div>
<div id="error" class="bg-red-50 border-l-4 border-red-500 text-red-700 p-4 rounded hidden"></div>
<div id="results" class="hidden">
<!-- Tab Navigation -->
<div class="border-b border-gray-200 mb-6">
<nav class="-mb-px flex space-x-8">
<button id="tab-properties" onclick="switchTab('properties')" class="tab-button active py-2 px-1 border-b-2 border-primary font-medium text-sm text-primary">
Average Properties
</button>
<button id="tab-plots" onclick="switchTab('plots')" class="tab-button py-2 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300">
Directional Plots
</button>
<button id="tab-3d" onclick="switchTab('3d')" class="tab-button py-2 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300">
3D Visualization
</button>
<button id="tab-extrema" onclick="switchTab('extrema')" class="tab-button py-2 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300">
Extrema & Eigenvalues
</button>
</nav>
</div>
<!-- Properties Tab -->
<div id="properties-content" class="tab-content">
<div class="mb-8">
<div class="overflow-x-auto">
<table class="w-full border-collapse bg-white rounded-lg overflow-hidden shadow">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Property</th>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Voigt</th>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Reuss</th>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Hill</th>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Unit</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 text-sm text-gray-900">Bulk Modulus</td>
<td id="bulk-voigt" class="px-6 py-4 text-sm font-semibold text-primary">-</td>
<td id="bulk-reuss" class="px-6 py-4 text-sm font-semibold text-primary">-</td>
<td id="bulk-hill" class="px-6 py-4 text-sm font-semibold text-primary">-</td>
<td class="px-6 py-4 text-sm text-gray-500">GPa</td>
</tr>
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 text-sm text-gray-900">Shear Modulus</td>
<td id="shear-voigt" class="px-6 py-4 text-sm font-semibold text-primary">-</td>
<td id="shear-reuss" class="px-6 py-4 text-sm font-semibold text-primary">-</td>
<td id="shear-hill" class="px-6 py-4 text-sm font-semibold text-primary">-</td>
<td class="px-6 py-4 text-sm text-gray-500">GPa</td>
</tr>
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 text-sm text-gray-900">Young's Modulus</td>
<td id="youngs-voigt" class="px-6 py-4 text-sm font-semibold text-primary">-</td>
<td id="youngs-reuss" class="px-6 py-4 text-sm font-semibold text-primary">-</td>
<td id="youngs-hill" class="px-6 py-4 text-sm font-semibold text-primary">-</td>
<td class="px-6 py-4 text-sm text-gray-500">GPa</td>
</tr>
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 text-sm text-gray-900">Poisson's Ratio</td>
<td id="poisson-voigt" class="px-6 py-4 text-sm font-semibold text-primary">-</td>
<td id="poisson-reuss" class="px-6 py-4 text-sm font-semibold text-primary">-</td>
<td id="poisson-hill" class="px-6 py-4 text-sm font-semibold text-primary">-</td>
<td class="px-6 py-4 text-sm text-gray-500">-</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Plots Tab -->
<div id="plots-content" class="tab-content hidden">
<div class="mb-6">
<label for="plotProperty" class="block text-sm font-semibold text-gray-700 mb-2">
Select Property to Visualize:
</label>
<select id="plotProperty" onchange="updatePlots()" class="w-full p-2 border-2 border-gray-300 rounded-lg focus:border-primary focus:outline-none">
<option value="youngs">Young's Modulus</option>
<option value="linear_compressibility">Linear Compressibility</option>
<option value="shear">Shear Modulus</option>
<option value="poisson">Poisson's Ratio</option>
</select>
</div>
<div class="mb-6">
<label for="plotPlane" class="block text-sm font-semibold text-gray-700 mb-2">
Select Plane:
</label>
<select id="plotPlane" onchange="updatePlots()" class="w-full p-2 border-2 border-gray-300 rounded-lg focus:border-primary focus:outline-none">
<option value="xy">XY Plane (Z = 0)</option>
<option value="xz">XZ Plane (Y = 0)</option>
<option value="yz">YZ Plane (X = 0)</option>
</select>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="bg-white p-4 rounded-lg shadow">
<h4 class="text-lg font-semibold text-gray-800 mb-4 text-center">Directional Curve</h4>
<div class="relative h-80">
<canvas id="directionalChart"></canvas>
</div>
</div>
<div class="bg-white p-4 rounded-lg shadow">
<h4 class="text-lg font-semibold text-gray-800 mb-4 text-center">Polar Representation</h4>
<div class="relative h-80">
<canvas id="polarChart"></canvas>
</div>
</div>
</div>
</div>
<!-- 3D Visualization Tab -->
<div id="3d-content" class="tab-content hidden">
<div class="mb-6">
<label for="plot3dProperty" class="block text-sm font-semibold text-gray-700 mb-2">
Select Property for 3D Visualization:
</label>
<select id="plot3dProperty" onchange="update3DPlot()" class="w-full p-2 border-2 border-gray-300 rounded-lg focus:border-primary focus:outline-none">
<option value="youngs">Young's Modulus</option>
<option value="linear_compressibility">Linear Compressibility</option>
<option value="shear">Shear Modulus (Max)</option>
<option value="poisson">Poisson's Ratio (Max)</option>
</select>
</div>
<div class="mb-6">
<div class="flex items-center gap-4">
<label class="block text-sm font-semibold text-gray-700">
Display Options:
</label>
<label class="flex items-center">
<input type="checkbox" id="showWireframe" checked onchange="update3DPlot()" class="mr-2">
<span class="text-sm text-gray-700">Wireframe</span>
</label>
<label class="flex items-center">
<input type="checkbox" id="showColorBar" checked onchange="update3DPlot()" class="mr-2">
<span class="text-sm text-gray-700">Color Scale</span>
</label>
</div>
</div>
<div class="bg-white p-4 rounded-lg shadow">
<h4 id="3d-title" class="text-lg font-semibold text-gray-800 mb-4 text-center">3D Elastic Surface</h4>
<div id="threejs-container" class="w-full h-96 bg-gray-50 rounded-lg"></div>
<div class="mt-4 text-sm text-gray-600 text-center">
<p>Use mouse to rotate, zoom, and pan the 3D visualization</p>
</div>
</div>
</div>
<!-- Extrema Tab -->
<div id="extrema-content" class="tab-content hidden">
<div class="mb-8">
<h3 class="text-xl font-semibold text-gray-800 mb-4 pb-2 border-b-2 border-primary">Eigenvalues of Stiffness Matrix</h3>
<div id="eigenvalues" class="flex flex-wrap gap-3"></div>
</div>
<div class="mb-8">
<h3 class="text-xl font-semibold text-gray-800 mb-4 pb-2 border-b-2 border-primary">Directional Extrema</h3>
<div class="overflow-x-auto">
<table class="w-full border-collapse bg-white rounded-lg overflow-hidden shadow">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Property</th>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Minimum</th>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Maximum</th>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Anisotropy</th>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Unit</th>
</tr>
</thead>
<tbody id="extrema-table" class="divide-y divide-gray-200">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
let Module = null;
let currentElasticTensor = null;
let directionalChart = null;
let polarChart = null;
// Three.js variables
let scene, camera, renderer, controls;
let elasticSurface = null;
let colorScale = null;
// Load OCC module on page load
async function loadOCC() {
const loading = document.getElementById('loading');
const error = document.getElementById('error');
loading.classList.remove('hidden');
error.classList.add('hidden');
try {
const { loadOCC: initOCC } = await import('../dist/index.browser.js');
Module = await initOCC({
wasmPath: '../dist/occjs.wasm',
dataPath: '../dist/occjs.data'
});
console.log('OCC module loaded successfully');
loading.classList.add('hidden');
} catch (err) {
console.error('Failed to load OCC module:', err);
error.textContent = 'Failed to load OCC module: ' + err.message;
error.classList.remove('hidden');
loading.classList.add('hidden');
}
}
// Tab switching functionality
function switchTab(tabName) {
// Hide all tab contents
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.add('hidden');
});
// Remove active class from all tabs
document.querySelectorAll('.tab-button').forEach(button => {
button.classList.remove('active', 'border-primary', 'text-primary');
button.classList.add('border-transparent', 'text-gray-500');
});
// Show selected tab content
document.getElementById(tabName + '-content').classList.remove('hidden');
// Add active class to selected tab
const activeTab = document.getElementById('tab-' + tabName);
activeTab.classList.add('active', 'border-primary', 'text-primary');
activeTab.classList.remove('border-transparent', 'text-gray-500');
// Update plots if switching to plots tab
if (tabName === 'plots' && currentElasticTensor) {
updatePlots();
}
// Initialize/update 3D plot if switching to 3D tab
if (tabName === '3d' && currentElasticTensor) {
init3DScene();
update3DPlot();
}
}
function parseTensorInput(input) {
const lines = input.trim().split('\n').filter(line => line.trim());
if (lines.length !== 6) {
throw new Error('Elastic tensor must have exactly 6 rows');
}
const matrix = [];
// Parse each line and detect format automatically
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const values = line.trim().split(/\s+/).filter(val => val !== '').map(parseFloat);
if (values.some(isNaN)) {
throw new Error('All values must be valid numbers');
}
matrix.push(values);
}
// Check if it's upper triangular (6, 5, 4, 3, 2, 1 values per row)
const isUpperTriangular = matrix.every((row, i) => row.length === 6 - i);
// Check if it's full matrix (6 values per row)
const isFullMatrix = matrix.every(row => row.length === 6);
if (!isUpperTriangular && !isFullMatrix) {
throw new Error('Matrix must be either full 6x6 format (6 values per row) or upper triangular format (6,5,4,3,2,1 values per row)');
}
// Convert upper triangular to full matrix
if (isUpperTriangular) {
const fullMatrix = Array(6).fill().map(() => Array(6).fill(0));
for (let i = 0; i < 6; i++) {
for (let j = 0; j < matrix[i].length; j++) {
const value = matrix[i][j];
const colIndex = i + j; // Column index in full matrix
fullMatrix[i][colIndex] = value;
fullMatrix[colIndex][i] = value; // Symmetric
}
}
return fullMatrix;
}
return matrix;
}
function analyzeTensor() {
const error = document.getElementById('error');
const results = document.getElementById('results');
const tensorInput = document.getElementById('tensorInput').value;
error.classList.add('hidden');
results.classList.add('hidden');
if (!Module) {
error.textContent = 'OCC module not loaded yet. Please wait...';
error.classList.remove('hidden');
return;
}
if (!tensorInput.trim()) {
error.textContent = 'Please enter a valid elastic tensor';
error.classList.remove('hidden');
return;
}
try {
const matrix = parseTensorInput(tensorInput);
console.log('Available Module methods:', Object.keys(Module));
console.log('Mat6 available:', !!Module.Mat6);
console.log('Mat available:', !!Module.Mat);
console.log('ElasticTensor available:', !!Module.ElasticTensor);
// Create 6x6 matrix from the input - try Mat6 first, fallback to Mat
let mat6;
if (Module.Mat6 && Module.Mat6.create) {
mat6 = Module.Mat6.create(6, 6);
} else {
console.warn('Mat6 not available, using Mat.create(6, 6)');
mat6 = Module.Mat.create(6, 6);
}
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 6; j++) {
mat6.set(i, j, matrix[i][j]);
}
}
console.log('Created matrix:', mat6);
console.log('ElasticTensor constructor:', Module.ElasticTensor);
// Create ElasticTensor
const elasticTensor = new Module.ElasticTensor(mat6);
// Store tensor for plotting
currentElasticTensor = elasticTensor;
// Debug: Print matrix values to verify they were set correctly
console.log('Matrix verification:');
for (let i = 0; i < 6; i++) {
const row = [];
for (let j = 0; j < 6; j++) {
row.push(mat6.get(i, j).toFixed(3));
}
console.log(`Row ${i}:`, row.join(' '));
}
// Debug: Test specific elastic tensor methods
console.log('Testing elastic tensor methods:');
try {
const voigtC = elasticTensor.voigtC;
console.log('Voigt C matrix rows:', voigtC.rows(), 'cols:', voigtC.cols());
console.log('Sample values from Voigt C:');
console.log('C[0,0]:', voigtC.get(0, 0));
console.log('C[0,1]:', voigtC.get(0, 1));
console.log('C[1,1]:', voigtC.get(1, 1));
} catch (e) {
console.error('Error accessing Voigt C matrix:', e);
}
// Calculate and display results
displayResults(elasticTensor, matrix);
} catch (err) {
error.textContent = 'Error analyzing tensor: ' + err.message;
error.classList.remove('hidden');
}
}
function displayResults(elasticTensor, matrix) {
// Average properties
const schemes = [Module.AveragingScheme.VOIGT, Module.AveragingScheme.REUSS, Module.AveragingScheme.HILL];
const schemeNames = ['voigt', 'reuss', 'hill'];
console.log('=== Calculating average properties ===');
for (let i = 0; i < schemes.length; i++) {
const scheme = schemes[i];
const name = schemeNames[i];
try {
const bulk = elasticTensor.averageBulkModulus(scheme);
const shear = elasticTensor.averageShearModulus(scheme);
const youngs = elasticTensor.averageYoungsModulus(scheme);
const poisson = elasticTensor.averagePoissonRatio(scheme);
console.log(`${name.toUpperCase()} averages:`);
console.log(` Bulk: ${bulk.toFixed(3)} GPa`);
console.log(` Shear: ${shear.toFixed(3)} GPa`);
console.log(` Young's: ${youngs.toFixed(3)} GPa`);
console.log(` Poisson: ${poisson.toFixed(5)}`);
document.getElementById(`bulk-${name}`).textContent = bulk.toFixed(3);
document.getElementById(`shear-${name}`).textContent = shear.toFixed(3);
document.getElementById(`youngs-${name}`).textContent = youngs.toFixed(3);
document.getElementById(`poisson-${name}`).textContent = poisson.toFixed(5);
} catch (e) {
console.error(`Error calculating ${name} averages:`, e);
document.getElementById(`bulk-${name}`).textContent = 'Error';
document.getElementById(`shear-${name}`).textContent = 'Error';
document.getElementById(`youngs-${name}`).textContent = 'Error';
document.getElementById(`poisson-${name}`).textContent = 'Error';
}
}
// Eigenvalues
displayEigenvalues(matrix);
// Directional extrema
displayExtrema(elasticTensor);
document.getElementById('results').classList.remove('hidden');
}
function displayEigenvalues(matrix) {
// For simplicity, just show the diagonal elements
// A full eigenvalue decomposition would require additional math libraries
const eigenvaluesDiv = document.getElementById('eigenvalues');
eigenvaluesDiv.innerHTML = '';
// Simple approximation: show diagonal elements
const diagonals = [matrix[0][0], matrix[1][1], matrix[2][2], matrix[3][3], matrix[4][4], matrix[5][5]];
diagonals.sort((a, b) => a - b);
diagonals.forEach((val, i) => {
const div = document.createElement('div');
div.className = 'bg-green-50 border border-primary px-3 py-2 rounded-lg text-sm font-medium text-primary';
div.innerHTML = `λ<sub>${i+1}</sub> = ${val.toFixed(1)} GPa`;
eigenvaluesDiv.appendChild(div);
});
}
function displayExtrema(elasticTensor) {
const table = document.getElementById('extrema-table');
table.innerHTML = '';
// Test directions for finding extrema
const directions = [
Module.Vec3.create(1, 0, 0), // x-axis
Module.Vec3.create(0, 1, 0), // y-axis
Module.Vec3.create(0, 0, 1), // z-axis
// Normalize diagonal directions manually
Module.Vec3.create(1/Math.sqrt(2), 1/Math.sqrt(2), 0), // xy diagonal
Module.Vec3.create(1/Math.sqrt(2), 0, 1/Math.sqrt(2)), // xz diagonal
Module.Vec3.create(0, 1/Math.sqrt(2), 1/Math.sqrt(2)), // yz diagonal
Module.Vec3.create(1/Math.sqrt(3), 1/Math.sqrt(3), 1/Math.sqrt(3)) // xyz diagonal
];
// Young's modulus extrema
let youngsMin = Infinity, youngsMax = -Infinity;
directions.forEach(dir => {
const value = elasticTensor.youngsModulus(dir);
youngsMin = Math.min(youngsMin, value);
youngsMax = Math.max(youngsMax, value);
});
// Linear compressibility extrema
let compMin = Infinity, compMax = -Infinity;
directions.forEach(dir => {
const value = elasticTensor.linearCompressibility(dir);
compMin = Math.min(compMin, value);
compMax = Math.max(compMax, value);
});
// Add rows to table
addExtremaRow(table, "Young's Modulus", youngsMin, youngsMax, "GPa");
addExtremaRow(table, "Linear Compressibility", compMin, compMax, "TPa⁻¹");
}
function addExtremaRow(table, property, min, max, unit) {
const row = table.insertRow();
row.className = 'hover:bg-gray-50';
row.innerHTML = `
<td class="px-6 py-4 text-sm text-gray-900">${property}</td>
<td class="px-6 py-4 text-sm font-semibold text-primary">${min.toFixed(3)}</td>
<td class="px-6 py-4 text-sm font-semibold text-primary">${max.toFixed(3)}</td>
<td class="px-6 py-4 text-sm font-semibold text-primary">${(max/min).toFixed(3)}</td>
<td class="px-6 py-4 text-sm text-gray-500">${unit}</td>
`;
}
// Plotting functions
function generateDirectionalData(elasticTensor, property, plane = 'xy', numPoints = 180) {
const data = [];
// Generate numPoints + 1 to ensure we complete the full circle (0° to 360°)
for (let i = 0; i <= numPoints; i++) {
const theta = (i / numPoints) * 2 * Math.PI;
// Create direction vector based on selected plane
let direction;
if (plane === 'xy') {
direction = Module.Vec3.create(Math.cos(theta), Math.sin(theta), 0);
} else if (plane === 'xz') {
direction = Module.Vec3.create(Math.cos(theta), 0, Math.sin(theta));
} else if (plane === 'yz') {
direction = Module.Vec3.create(0, Math.cos(theta), Math.sin(theta));
}
let valueMin = 0, valueMax = 0;
try {
if (property === 'youngs') {
valueMin = valueMax = elasticTensor.youngsModulus(direction);
} else if (property === 'linear_compressibility') {
valueMin = valueMax = elasticTensor.linearCompressibility(direction);
} else if (property === 'shear') {
// For shear modulus, get min/max in the given direction
const minMax = elasticTensor.shearModulusMinMax(direction);
valueMin = minMax.min;
valueMax = minMax.max;
} else if (property === 'poisson') {
// For Poisson's ratio, get min/max in the given direction
const minMax = elasticTensor.poissonRatioMinMax(direction);
valueMin = minMax.min;
valueMax = minMax.max;
}
} catch (e) {
console.warn('Error calculating property value:', e);
valueMin = valueMax = 0;
}
const angleDeg = theta * 180 / Math.PI;
data.push({
angle: angleDeg,
angleRad: theta,
valueMin: valueMin,
valueMax: valueMax,
value: valueMax, // Use max for single value compatibility
// Polar coordinates (for polar plot)
polarXMin: Math.cos(theta) * valueMin,
polarYMin: Math.sin(theta) * valueMin,
polarXMax: Math.cos(theta) * valueMax,
polarYMax: Math.sin(theta) * valueMax
});
}
console.log(`Generated ${data.length} points for ${property} in ${plane} plane`);
console.log('Sample values:', data.slice(0, 5).map(d => ({ angle: d.angle.toFixed(1), value: d.value.toFixed(3) })));
return data;
}
function updatePlots() {
if (!currentElasticTensor) {
console.log('No elastic tensor available for plotting');
return;
}
const property = document.getElementById('plotProperty').value;
const plane = document.getElementById('plotPlane').value;
const data = generateDirectionalData(currentElasticTensor, property, plane);
if (data.length === 0) {
console.error('No data generated for plotting');
return;
}
// Destroy existing charts
if (directionalChart) {
directionalChart.destroy();
}
if (polarChart) {
polarChart.destroy();
}
// Create directional curve chart (angle vs value)
const directionalCtx = document.getElementById('directionalChart').getContext('2d');
// Determine if we need to show min/max curves
const needsMinMax = property === 'shear' || property === 'poisson';
const datasets = [];
if (needsMinMax) {
// Add maximum curve
datasets.push({
label: `${getPropertyTitle(property)} (Maximum)`,
data: data.map(d => d.valueMax),
backgroundColor: 'rgba(5, 150, 105, 0.1)',
borderColor: 'rgba(5, 150, 105, 1)',
borderWidth: 2,
fill: false,
pointRadius: 0,
tension: 0.1
});
// Add minimum curve
datasets.push({
label: `${getPropertyTitle(property)} (Minimum)`,
data: data.map(d => d.valueMin),
backgroundColor: 'rgba(220, 38, 127, 0.1)',
borderColor: 'rgba(220, 38, 127, 1)',
borderWidth: 2,
fill: false,
pointRadius: 0,
tension: 0.1
});
} else {
// Single curve for Young's modulus and linear compressibility
datasets.push({
label: getPropertyTitle(property),
data: data.map(d => d.value),
backgroundColor: 'rgba(5, 150, 105, 0.1)',
borderColor: 'rgba(5, 150, 105, 1)',
borderWidth: 2,
fill: true,
pointRadius: 0,
tension: 0.1
});
}
directionalChart = new Chart(directionalCtx, {
type: 'line',
data: {
labels: data.map(d => d.angle.toFixed(0) + '°'),
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: needsMinMax,
position: 'top'
},
title: {
display: true,
text: `${getPropertyTitle(property)} vs Angle (${plane.toUpperCase()} plane)`
}
},
scales: {
x: {
title: {
display: true,
text: 'Angle (degrees)'
},
min: 0,
max: 360,
ticks: {
stepSize: 45
}
},
y: {
title: {
display: true,
text: getPropertyUnit(property)
},
beginAtZero: property !== 'poisson' // Poisson's ratio can be negative
}
}
}
});
// Create polar plot (showing directional variation as polar coordinates)
const polarCtx = document.getElementById('polarChart').getContext('2d');
const polarDatasets = [];
if (needsMinMax) {
// Add maximum polar curve
polarDatasets.push({
label: 'Maximum',
data: data.map(d => ({ x: d.polarXMax, y: d.polarYMax })),
borderColor: 'rgba(5, 150, 105, 1)',
borderWidth: 2,
fill: false,
pointRadius: 0,
tension: 0
});
// Add minimum polar curve
polarDatasets.push({
label: 'Minimum',
data: data.map(d => ({ x: d.polarXMin, y: d.polarYMin })),
borderColor: 'rgba(220, 38, 127, 1)',
borderWidth: 2,
fill: false,
pointRadius: 0,
tension: 0
});
} else {
// Single polar curve
polarDatasets.push({
label: 'Polar Representation',
data: data.map(d => ({ x: d.polarXMax, y: d.polarYMax })),
borderColor: 'rgba(5, 150, 105, 1)',
borderWidth: 2,
fill: false,
pointRadius: 0,
tension: 0
});
}
polarChart = new Chart(polarCtx, {
type: 'line',
data: {
datasets: polarDatasets
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: needsMinMax,
position: 'top'
},
title: {
display: true,
text: `Polar Plot - ${getPropertyTitle(property)} (${plane.toUpperCase()} plane)`
}
},
scales: {
x: {
type: 'linear',
position: 'center',
title: {
display: true,
text: plane === 'xy' ? 'X' : plane === 'xz' ? 'X' : 'Y'
}
},
y: {
type: 'linear',
position: 'center',
title: {
display: true,
text: plane === 'xy' ? 'Y' : plane === 'xz' ? 'Z' : 'Z'
}
}
},
elements: {
point: {
radius: 0
}
}
}
});
console.log(`Charts updated for ${property} in ${plane} plane`);
}
function getPropertyTitle(property) {
const titles = {
'youngs': "Young's Modulus",
'linear_compressibility': 'Linear Compressibility',
'shear': 'Shear Modulus',
'poisson': "Poisson's Ratio"
};
return titles[property] || property;
}
function getPropertyUnit(property) {
const units = {
'youngs': 'GPa',
'linear_compressibility': 'TPa⁻¹',
'shear': 'GPa',
'poisson': 'dimensionless'
};
return units[property] || '';
}
// Three.js initialization
function init3DScene() {
const container = document.getElementById('threejs-container');
// Clear existing scene
if (renderer) {
container.removeChild(renderer.domElement);
}
// Scene setup
scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
// Camera setup
camera = new THREE.PerspectiveCamera(50, container.clientWidth / container.clientHeight, 0.1, 100);
camera.position.set(2, 2, 4);
// Renderer setup
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
container.appendChild(renderer.domElement);
// Controls setup
controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// Add ground plane exactly like the contact shadows example
const PLANE_WIDTH = 10;
const PLANE_HEIGHT = 10;
const planeGeometry = new THREE.PlaneGeometry(PLANE_WIDTH, PLANE_HEIGHT).rotateX(-Math.PI / 2);
const planeMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: false,
});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.position.y = -2;
scene.add(plane);
// Add brighter lighting for more vibrant colors
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);
// Axes helper
const axesHelper = new THREE.AxesHelper(1.5);
scene.add(axesHelper);
// Animation loop
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// Handle window resize
window.addEventListener('resize', () => {
const newWidth = container.clientWidth;
const newHeight = container.clientHeight;
camera.aspect = newWidth / newHeight;
camera.updateProjectionMatrix();
renderer.setSize(newWidth, newHeight);
});
}
// Generate 3D surface data
function generate3DSurfaceData(elasticTensor, property, resolution = 32) {
const vertices = [];
const colors = [];
let minValue = Infinity;
let maxValue = -Infinity;
// First pass: collect all values to find min/max
const allData = [];
for (let i = 0; i <= resolution; i++) {
for (let j = 0; j <= resolution; j++) {
const theta = (i / resolution) * Math.PI; // 0 to π
const phi = (j / resolution) * 2 * Math.PI; // 0 to 2π
// Convert to Cartesian direction
const x = Math.sin(theta) * Math.cos(phi);
const y = Math.sin(theta) * Math.sin(phi);
const z = Math.cos(theta);
const direction = Module.Vec3.create(x, y, z);
let value = 0;
try {
if (property === 'youngs') {
value = elasticTensor.youngsModulus(direction);
} else if (property === 'linear_compressibility') {
value = elasticTensor.linearCompressibility(direction);
} else if (property === 'shear') {
const minMax = elasticTensor.shearModulusMinMax(direction);
value = minMax.max;
} else if (property === 'poisson') {
const minMax = elasticTensor.poissonRatioMinMax(direction);
value = minMax.max;
}
} catch (e) {
console.warn('Error calculating property value:', e);
value = 0;
}
minValue = Math.min(minValue, value);
maxValue = Math.max(maxValue, value);
allData.push({ theta, phi, value, direction: [x, y, z] });
}
}
// Second pass: generate vertices and colors
for (let i = 0; i <= resolution; i++) {
for (let j = 0; j <= resolution; j++) {
const idx = i * (resolution + 1) + j;
const point = allData[idx];
// Normalize value
const normalizedValue = (point.value - minValue) / (maxValue - minValue);
// Scale the radius based on the property value
const baseRadius = 1.0;
const radius = baseRadius + (normalizedValue * 0.5);
const vertex = new THREE.Vector3(
point.direction[0] * radius,
point.direction[1] * radius,
point.direction[2] * radius
);
vertices.push(vertex);
// Color based on value
const color = new THREE.Color();
color.setHSL(0.7 * (1 - normalizedValue), 0.8, 0.6);
colors.push(color);
}
}
return {
vertices,
colors,
minValue,
maxValue,
resolution,
property
};
}
// Create 3D surface mesh using icosahedron geometry
function create3DSurface(elasticTensor, property, detail = 9) {
// Create an icosahedron geometry with high detail for uniform vertex distribution
const geometry = new THREE.IcosahedronGeometry(1, detail);
// Get the position attribute
const positionAttribute = geometry.getAttribute('position');
const positions = positionAttribute.array;
const colors = [];
let minValue = Infinity;
let maxValue = -Infinity;
// First pass: calculate all values to find min/max
const values = [];
for (let i = 0; i < positions.length; i += 3) {
const x = positions[i];
const y = positions[i + 1];
const z = positions[i + 2];
// Normalize the direction vector
const length = Math.sqrt(x * x + y * y + z * z);
const direction = Module.Vec3.create(x / length, y / length, z / length);
let value = 0;
try {
if (property === 'youngs') {
value = elasticTensor.youngsModulus(direction);
} else if (property === 'linear_compressibility') {
value = elasticTensor.linearCompressibility(direction);
} else if (property === 'shear') {
const minMax = elasticTensor.shearModulusMinMax(direction);
value = minMax.max;
} else if (property === 'poisson') {
const minMax = elasticTensor.poissonRatioMinMax(direction);
value = minMax.max;
}
} catch (e) {
console.warn('Error calculating property value:', e);
value = 0;
}
values.push(value);
minValue = Math.min(minValue, value);
maxValue = Math.max(maxValue, value);
}
// Second pass: modify positions and set colors
for (let i = 0; i < positions.length; i += 3) {
const x = positions[i];
const y = positions[i + 1];
const z = positions[i + 2];
// Normalize the direction vector
const length = Math.sqrt(x * x + y * y + z * z);
const normalizedX = x / length;
const normalizedY = y / length;
const normalizedZ = z / length;
const value = values[i / 3];
const normalizedValue = (value - minValue) / (maxValue - minValue);
// Scale the radius based on the property value
const baseRadius = 1.0;
const radius = baseRadius + (normalizedValue * 0.5);
// Update