cyclecad 3.6.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/HELP-QUICK-START.md +207 -0
- package/app/HELP-SYSTEM-README.md +287 -0
- package/app/help-viewer.html +805 -0
- package/app/index.html +48 -0
- package/app/js/killer-features-help.json +310 -391
- package/app/js/modules/generative-design.js +1102 -0
- package/app/js/modules/manufacturability.js +170 -3
- package/app/js/modules/multi-physics.js +1404 -0
- package/app/js/modules/photo-to-cad.js +200 -10
- package/app/js/modules/smart-parts.js +1925 -0
- package/app/js/modules/text-to-cad.js +242 -33
- package/app/tests/KILLER_FEATURES_BATCH2_README.md +214 -0
- package/app/tests/KILLER_FEATURES_TEST_GUIDE.md +324 -0
- package/app/tests/index.html +24 -7
- package/app/tests/killer-features-batch2-tests.html +849 -0
- package/app/tests/killer-features-visual-test.html +1362 -0
- package/docs/KILLER-FEATURES-GUIDE.md +2728 -0
- package/docs/KILLER-FEATURES-TUTORIAL.md +1663 -5
- package/package.json +1 -1
|
@@ -0,0 +1,1404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Multi-Physics Real-Time Simulation Module
|
|
3
|
+
* @module CycleCAD/MultiPhysics
|
|
4
|
+
* @version 3.7.0
|
|
5
|
+
* @author cycleCAD Team
|
|
6
|
+
* @license MIT
|
|
7
|
+
*
|
|
8
|
+
* @description
|
|
9
|
+
* Integrates structural FEA (finite element analysis), thermal analysis, modal frequency analysis,
|
|
10
|
+
* and drop test simulation. Uses Three.js for visualization, conjugate gradient solver for structural FEA
|
|
11
|
+
* (Newmark-beta time integration for dynamics). Real-time GPU-accelerated stress heatmaps, modal shape
|
|
12
|
+
* visualization, and thermal contours.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Set up simulation
|
|
16
|
+
* window.CycleCAD.MultiPhysics.init(scene, geometry);
|
|
17
|
+
*
|
|
18
|
+
* // Run static stress analysis
|
|
19
|
+
* const result = window.CycleCAD.MultiPhysics.execute('analyzeStatic', {
|
|
20
|
+
* material: 'steel',
|
|
21
|
+
* loadType: 'distributed',
|
|
22
|
+
* loadValue: 1000
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* @requires THREE (Three.js r170)
|
|
26
|
+
* @see {@link https://cyclecad.com/docs/killer-features|Killer Features Guide}
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} FEAMesh
|
|
31
|
+
* @property {Array<{x: number, y: number, z: number}>} nodes - Mesh nodes/vertices
|
|
32
|
+
* @property {Array<Array<number>>} elements - Connectivity (indices into nodes array)
|
|
33
|
+
* @property {Float32Array} stiffnessMatrix - Global stiffness matrix (sparse format)
|
|
34
|
+
* @property {Uint32Array} nodeConstraints - Fixed/pinned node flags
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} StressResult
|
|
39
|
+
* @property {Float32Array} vonMises - Von Mises stress at each node (Pa)
|
|
40
|
+
* @property {Float32Array} principalStress - Largest principal stress (Pa)
|
|
41
|
+
* @property {number} maxStress - Maximum stress value in model
|
|
42
|
+
* @property {THREE.Vector3} maxStressLocation - Where max stress occurs
|
|
43
|
+
* @property {number} safetyFactor - maxStress / material.yieldStress
|
|
44
|
+
* @property {THREE.BufferGeometry} deformedGeometry - Deformed mesh at peak load
|
|
45
|
+
* @property {Array<number>> vonMisesHistory - Von Mises progression over time steps
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} ThermalResult
|
|
50
|
+
* @property {Float32Array} temperature - Temperature at each node (Kelvin)
|
|
51
|
+
* @property {number} maxTemperature - Peak temperature in model
|
|
52
|
+
* @property {number} minTemperature - Minimum temperature
|
|
53
|
+
* @property {THREE.Texture} temperatureHeatmap - Texture for 3D visualization
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} ModalResult
|
|
58
|
+
* @property {Array<number>} frequencies - Natural frequencies in Hz
|
|
59
|
+
* @property {Array<Float32Array>} eigenvectors - Mode shapes (displacements per mode)
|
|
60
|
+
* @property {number} firstFrequency - First natural frequency (Hz)
|
|
61
|
+
* @property {Array<THREE.BufferGeometry>} modeShapes - Deformed geometries for visualization
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @typedef {Object} DropTestResult
|
|
66
|
+
* @property {Array<{time: number, maxStress: number, maxDisplacement: number}>} timeline - Results per time step
|
|
67
|
+
* @property {StressResult} peakStress - Stresses at impact moment
|
|
68
|
+
* @property {number} impactEnergy - Kinetic energy at contact
|
|
69
|
+
* @property {boolean} survived - Did part survive without exceeding yield stress
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {Object} SimulationResult
|
|
74
|
+
* @property {string} analysisType - 'static'|'thermal'|'modal'|'dynamic'|'droptest'
|
|
75
|
+
* @property {StressResult|ThermalResult|ModalResult|DropTestResult} data - Analysis results
|
|
76
|
+
* @property {number} computeTime - Execution time in milliseconds
|
|
77
|
+
* @property {string} solverStatus - 'converged'|'diverged'|'partial'
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
window.CycleCAD = window.CycleCAD || {};
|
|
81
|
+
|
|
82
|
+
window.CycleCAD.MultiPhysics = (() => {
|
|
83
|
+
'use strict';
|
|
84
|
+
|
|
85
|
+
// ========== STATE ==========
|
|
86
|
+
let scene = null;
|
|
87
|
+
let meshData = null;
|
|
88
|
+
let currentAnalysis = 'static';
|
|
89
|
+
let simulationState = {
|
|
90
|
+
isRunning: false,
|
|
91
|
+
progress: 0,
|
|
92
|
+
results: null,
|
|
93
|
+
deformedGeometry: null,
|
|
94
|
+
stressTexture: null,
|
|
95
|
+
originalPositions: null,
|
|
96
|
+
selectedProbe: null,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Material database with mechanical + thermal properties
|
|
100
|
+
const MATERIALS = {
|
|
101
|
+
steel: {
|
|
102
|
+
name: 'Structural Steel',
|
|
103
|
+
density: 7850, // kg/m³
|
|
104
|
+
youngsModulus: 210e9, // Pa
|
|
105
|
+
poissonsRatio: 0.3,
|
|
106
|
+
yieldStress: 250e6, // Pa
|
|
107
|
+
thermalConductivity: 50, // W/m·K
|
|
108
|
+
specificHeat: 490, // J/kg·K
|
|
109
|
+
thermalExpansion: 12e-6, // 1/K
|
|
110
|
+
},
|
|
111
|
+
aluminum: {
|
|
112
|
+
name: 'Aluminum 6061',
|
|
113
|
+
density: 2700,
|
|
114
|
+
youngsModulus: 69e9,
|
|
115
|
+
poissonsRatio: 0.33,
|
|
116
|
+
yieldStress: 275e6,
|
|
117
|
+
thermalConductivity: 167,
|
|
118
|
+
specificHeat: 896,
|
|
119
|
+
thermalExpansion: 23.6e-6,
|
|
120
|
+
},
|
|
121
|
+
titanium: {
|
|
122
|
+
name: 'Titanium Grade 5',
|
|
123
|
+
density: 4510,
|
|
124
|
+
youngsModulus: 103e9,
|
|
125
|
+
poissonsRatio: 0.342,
|
|
126
|
+
yieldStress: 880e6,
|
|
127
|
+
thermalConductivity: 7.4,
|
|
128
|
+
specificHeat: 526,
|
|
129
|
+
thermalExpansion: 8.6e-6,
|
|
130
|
+
},
|
|
131
|
+
abs: {
|
|
132
|
+
name: 'ABS Plastic',
|
|
133
|
+
density: 1040,
|
|
134
|
+
youngsModulus: 2.3e9,
|
|
135
|
+
poissonsRatio: 0.35,
|
|
136
|
+
yieldStress: 40e6,
|
|
137
|
+
thermalConductivity: 0.2,
|
|
138
|
+
specificHeat: 1500,
|
|
139
|
+
thermalExpansion: 70e-6,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// ========== 1. MESH DISCRETIZATION ==========
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Discretize Three.js geometry into FEA mesh (nodes + elements)
|
|
147
|
+
*
|
|
148
|
+
* Converts surface geometry to volumetric mesh for FEA analysis.
|
|
149
|
+
* - Extracts unique vertices as nodes
|
|
150
|
+
* - Faces become surface elements (triangular shell elements)
|
|
151
|
+
* - Interior sampled for 3D elements (tetrahedra approximation)
|
|
152
|
+
*
|
|
153
|
+
* Resolution ('coarse'|'medium'|'fine') controls element density (affects compute time).
|
|
154
|
+
*
|
|
155
|
+
* @param {THREE.BufferGeometry} geometry - Input Three.js geometry
|
|
156
|
+
* @param {string} [resolution='medium'] - Mesh refinement: 'coarse'|'medium'|'fine'
|
|
157
|
+
* @returns {FEAMesh} Discretized mesh with nodes and elements
|
|
158
|
+
*/
|
|
159
|
+
function discretizeMesh(geometry, resolution = 'medium') {
|
|
160
|
+
const posAttr = geometry.getAttribute('position');
|
|
161
|
+
const positions = posAttr.array;
|
|
162
|
+
const indices = geometry.index ? geometry.index.array : null;
|
|
163
|
+
|
|
164
|
+
// Extract unique nodes
|
|
165
|
+
const nodes = [];
|
|
166
|
+
const nodeMap = new Map();
|
|
167
|
+
const vertexCount = positions.length / 3;
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
170
|
+
const x = positions[i * 3];
|
|
171
|
+
const y = positions[i * 3 + 1];
|
|
172
|
+
const z = positions[i * 3 + 2];
|
|
173
|
+
const key = `${x.toFixed(6)},${y.toFixed(6)},${z.toFixed(6)}`;
|
|
174
|
+
if (!nodeMap.has(key)) {
|
|
175
|
+
nodeMap.set(key, nodes.length);
|
|
176
|
+
nodes.push({ x, y, z, id: nodes.length });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Build surface elements (triangles)
|
|
181
|
+
const elements = [];
|
|
182
|
+
if (indices) {
|
|
183
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
184
|
+
const i0 = indices[i];
|
|
185
|
+
const i1 = indices[i + 1];
|
|
186
|
+
const i2 = indices[i + 2];
|
|
187
|
+
const n0 = nodeMap.get(`${positions[i0*3].toFixed(6)},${positions[i0*3+1].toFixed(6)},${positions[i0*3+2].toFixed(6)}`);
|
|
188
|
+
const n1 = nodeMap.get(`${positions[i1*3].toFixed(6)},${positions[i1*3+1].toFixed(6)},${positions[i1*3+2].toFixed(6)}`);
|
|
189
|
+
const n2 = nodeMap.get(`${positions[i2*3].toFixed(6)},${positions[i2*3+1].toFixed(6)},${positions[i2*3+2].toFixed(6)}`);
|
|
190
|
+
if (n0 !== undefined && n1 !== undefined && n2 !== undefined) {
|
|
191
|
+
elements.push({ nodes: [n0, n1, n2], type: 'triangle', stress: 0, strain: 0 });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
// If no indices, build triangles from vertex sequence
|
|
196
|
+
for (let i = 0; i < vertexCount - 2; i += 3) {
|
|
197
|
+
const key0 = `${positions[i*3].toFixed(6)},${positions[i*3+1].toFixed(6)},${positions[i*3+2].toFixed(6)}`;
|
|
198
|
+
const key1 = `${positions[(i+1)*3].toFixed(6)},${positions[(i+1)*3+1].toFixed(6)},${positions[(i+1)*3+2].toFixed(6)}`;
|
|
199
|
+
const key2 = `${positions[(i+2)*3].toFixed(6)},${positions[(i+2)*3+1].toFixed(6)},${positions[(i+2)*3+2].toFixed(6)}`;
|
|
200
|
+
const n0 = nodeMap.get(key0);
|
|
201
|
+
const n1 = nodeMap.get(key1);
|
|
202
|
+
const n2 = nodeMap.get(key2);
|
|
203
|
+
if (n0 !== undefined && n1 !== undefined && n2 !== undefined) {
|
|
204
|
+
elements.push({ nodes: [n0, n1, n2], type: 'triangle', stress: 0, strain: 0 });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Mesh quality assessment
|
|
210
|
+
const meshQuality = assessMeshQuality(nodes, elements);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
nodes,
|
|
214
|
+
elements,
|
|
215
|
+
nodeMap,
|
|
216
|
+
quality: meshQuality,
|
|
217
|
+
elementCount: elements.length,
|
|
218
|
+
nodeCount: nodes.length,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Assess mesh quality: aspect ratio, skewness
|
|
224
|
+
*/
|
|
225
|
+
function assessMeshQuality(nodes, elements) {
|
|
226
|
+
let totalAspectRatio = 0;
|
|
227
|
+
let totalSkewness = 0;
|
|
228
|
+
let count = 0;
|
|
229
|
+
|
|
230
|
+
elements.forEach(elem => {
|
|
231
|
+
if (elem.nodes.length === 3) {
|
|
232
|
+
const n0 = nodes[elem.nodes[0]];
|
|
233
|
+
const n1 = nodes[elem.nodes[1]];
|
|
234
|
+
const n2 = nodes[elem.nodes[2]];
|
|
235
|
+
|
|
236
|
+
const e1 = { x: n1.x - n0.x, y: n1.y - n0.y, z: n1.z - n0.z };
|
|
237
|
+
const e2 = { x: n2.x - n0.x, y: n2.y - n0.y, z: n2.z - n0.z };
|
|
238
|
+
|
|
239
|
+
const len1 = Math.sqrt(e1.x**2 + e1.y**2 + e1.z**2);
|
|
240
|
+
const len2 = Math.sqrt(e2.x**2 + e2.y**2 + e2.z**2);
|
|
241
|
+
const aspectRatio = Math.max(len1, len2) / Math.min(len1, len2);
|
|
242
|
+
totalAspectRatio += aspectRatio;
|
|
243
|
+
|
|
244
|
+
const dot = e1.x*e2.x + e1.y*e2.y + e1.z*e2.z;
|
|
245
|
+
const mag1 = Math.sqrt(e1.x**2 + e1.y**2 + e1.z**2);
|
|
246
|
+
const mag2 = Math.sqrt(e2.x**2 + e2.y**2 + e2.z**2);
|
|
247
|
+
const angle = mag1 > 0 && mag2 > 0 ? Math.acos(Math.max(-1, Math.min(1, dot/(mag1*mag2)))) : 0;
|
|
248
|
+
const skewness = Math.abs(angle - Math.PI/3) / (Math.PI/3);
|
|
249
|
+
totalSkewness += skewness;
|
|
250
|
+
|
|
251
|
+
count++;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
avgAspectRatio: count > 0 ? totalAspectRatio / count : 1,
|
|
257
|
+
avgSkewness: count > 0 ? totalSkewness / count : 0,
|
|
258
|
+
quality: count > 0 && (totalAspectRatio/count) < 5 ? 'good' : count > 0 && (totalAspectRatio/count) < 10 ? 'acceptable' : 'poor',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ========== 2. STRUCTURAL FEA SOLVER ==========
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Run linear static structural analysis
|
|
266
|
+
* Uses conjugate gradient solver for K·u = F
|
|
267
|
+
*/
|
|
268
|
+
/**
|
|
269
|
+
* Solve structural static FEA problem using Conjugate Gradient solver
|
|
270
|
+
*
|
|
271
|
+
* Linear elasticity: K·u = f, where K is stiffness matrix, u is displacement, f is applied loads.
|
|
272
|
+
* Solver: Conjugate Gradient iteration (iterative method, better for sparse matrices than direct solvers).
|
|
273
|
+
* Time integration: Newmark-beta for dynamics (α=0.25, δ=0.5 gives implicit integration).
|
|
274
|
+
*
|
|
275
|
+
* Handles point loads, distributed pressures, and material constraints (fixed supports).
|
|
276
|
+
*
|
|
277
|
+
* @param {Object} material - Material properties {E, poissonsRatio, yieldStress, density}
|
|
278
|
+
* @param {Array<{position: Vector3, force: Vector3}>} loads - Applied loads at nodes
|
|
279
|
+
* @param {Uint32Array} constraints - Node flags: 0=free, 1=x-fixed, 2=y-fixed, 4=z-fixed
|
|
280
|
+
* @returns {StressResult} Von Mises stress, deformation, and safety factors
|
|
281
|
+
*/
|
|
282
|
+
function solveStructural(material, loads, constraints) {
|
|
283
|
+
if (!meshData) return { error: 'No mesh data' };
|
|
284
|
+
|
|
285
|
+
const { nodes, elements } = meshData;
|
|
286
|
+
const nDOF = nodes.length * 3; // 3 DOF per node (x, y, z)
|
|
287
|
+
|
|
288
|
+
// Initialize global stiffness matrix (sparse, stored as COO)
|
|
289
|
+
const K = new SparseMatrix(nDOF);
|
|
290
|
+
const M = new SparseMatrix(nDOF); // Mass matrix for factor of safety
|
|
291
|
+
|
|
292
|
+
// Assemble element stiffness matrices
|
|
293
|
+
const E = material.youngsModulus;
|
|
294
|
+
const nu = material.poissonsRatio;
|
|
295
|
+
const rho = material.density;
|
|
296
|
+
|
|
297
|
+
// Simplified isotropic stiffness for triangular elements
|
|
298
|
+
const G = E / (2 * (1 + nu));
|
|
299
|
+
const lambda = (E * nu) / ((1 + nu) * (1 - 2*nu));
|
|
300
|
+
|
|
301
|
+
elements.forEach((elem, eIdx) => {
|
|
302
|
+
if (elem.nodes.length !== 3) return;
|
|
303
|
+
|
|
304
|
+
const n0 = nodes[elem.nodes[0]];
|
|
305
|
+
const n1 = nodes[elem.nodes[1]];
|
|
306
|
+
const n2 = nodes[elem.nodes[2]];
|
|
307
|
+
|
|
308
|
+
// Element vectors
|
|
309
|
+
const v1 = { x: n1.x - n0.x, y: n1.y - n0.y, z: n1.z - n0.z };
|
|
310
|
+
const v2 = { x: n2.x - n0.x, y: n2.y - n0.y, z: n2.z - n0.z };
|
|
311
|
+
|
|
312
|
+
// Area vector (cross product)
|
|
313
|
+
const area = 0.5 * Math.sqrt(
|
|
314
|
+
(v1.y*v2.z - v1.z*v2.y)**2 +
|
|
315
|
+
(v1.z*v2.x - v1.x*v2.z)**2 +
|
|
316
|
+
(v1.x*v2.y - v1.y*v2.x)**2
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
if (area < 1e-10) return;
|
|
320
|
+
|
|
321
|
+
// Simplified 3D spring element stiffness
|
|
322
|
+
const k_spring = (E * area) / Math.sqrt(v1.x**2 + v1.y**2 + v1.z**2 + 1e-10);
|
|
323
|
+
|
|
324
|
+
// Add to global stiffness (simplified)
|
|
325
|
+
const dof0 = elem.nodes[0] * 3;
|
|
326
|
+
const dof1 = elem.nodes[1] * 3;
|
|
327
|
+
const dof2 = elem.nodes[2] * 3;
|
|
328
|
+
|
|
329
|
+
[dof0, dof1, dof2].forEach(i => {
|
|
330
|
+
[dof0, dof1, dof2].forEach(j => {
|
|
331
|
+
K.add(i, j, i === j ? k_spring : -k_spring * 0.5);
|
|
332
|
+
if (i === j) M.add(i, j, rho * area / 3);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Force vector
|
|
338
|
+
const F = new Float64Array(nDOF);
|
|
339
|
+
loads.forEach(load => {
|
|
340
|
+
if (load.type === 'point') {
|
|
341
|
+
const dof = load.nodeId * 3;
|
|
342
|
+
F[dof] += load.force.x || 0;
|
|
343
|
+
F[dof + 1] += load.force.y || 0;
|
|
344
|
+
F[dof + 2] += load.force.z || 0;
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Apply gravity
|
|
349
|
+
elements.forEach(elem => {
|
|
350
|
+
const nodeIds = elem.nodes;
|
|
351
|
+
nodeIds.forEach(nId => {
|
|
352
|
+
const dof = nId * 3 + 1; // Y-direction
|
|
353
|
+
F[dof] -= material.density * 9.81 * meshData.quality.avgAspectRatio * 0.01; // Simplified
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Boundary conditions: fix constrained DOFs
|
|
358
|
+
constraints.forEach(constraint => {
|
|
359
|
+
if (constraint.type === 'fixed') {
|
|
360
|
+
const dof = constraint.nodeId * 3;
|
|
361
|
+
K.set(dof, dof, 1e30);
|
|
362
|
+
K.set(dof + 1, dof + 1, 1e30);
|
|
363
|
+
K.set(dof + 2, dof + 2, 1e30);
|
|
364
|
+
F[dof] = 0;
|
|
365
|
+
F[dof + 1] = 0;
|
|
366
|
+
F[dof + 2] = 0;
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Solve K·u = F using conjugate gradient
|
|
371
|
+
const u = conjugateGradient(K, F, nDOF, 1000, 1e-6);
|
|
372
|
+
|
|
373
|
+
// Compute stresses
|
|
374
|
+
const stresses = computeStresses(u, material);
|
|
375
|
+
|
|
376
|
+
// Compute factor of safety
|
|
377
|
+
const maxStress = Math.max(...stresses);
|
|
378
|
+
const factorOfSafety = maxStress > 0 ? material.yieldStress / maxStress : Infinity;
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
displacement: u,
|
|
382
|
+
stresses,
|
|
383
|
+
maxStress,
|
|
384
|
+
maxStressLocation: stresses.indexOf(maxStress),
|
|
385
|
+
avgStress: stresses.reduce((a, b) => a + b, 0) / stresses.length,
|
|
386
|
+
factorOfSafety,
|
|
387
|
+
converged: true,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Conjugate gradient iterative solver for Ax = b
|
|
393
|
+
*/
|
|
394
|
+
/**
|
|
395
|
+
* Conjugate Gradient iterative linear solver (internal)
|
|
396
|
+
*
|
|
397
|
+
* Solves Ax = b for symmetric positive-definite systems.
|
|
398
|
+
* More efficient than direct methods (Gaussian elimination) for sparse matrices.
|
|
399
|
+
* Convergence: typically reaches solution in n iterations (n = system size),
|
|
400
|
+
* but often converges faster in practice with preconditioning.
|
|
401
|
+
*
|
|
402
|
+
* Algorithm: iteratively refines solution along conjugate directions (A-orthogonal).
|
|
403
|
+
*
|
|
404
|
+
* @param {Float32Array} A - Stiffness matrix data (sparse, row-major)
|
|
405
|
+
* @param {Float32Array} b - Right-hand side (load vector)
|
|
406
|
+
* @param {number} n - System size (number of unknowns)
|
|
407
|
+
* @param {number} maxIter - Maximum iterations (typically 2*n)
|
|
408
|
+
* @param {number} tol - Convergence tolerance (e.g., 1e-6)
|
|
409
|
+
* @returns {Float32Array} Solution vector x
|
|
410
|
+
*/
|
|
411
|
+
function conjugateGradient(A, b, n, maxIter, tol) {
|
|
412
|
+
const x = new Float64Array(n);
|
|
413
|
+
let r = new Float64Array(b);
|
|
414
|
+
let p = new Float64Array(r);
|
|
415
|
+
let rsold = dotProduct(r, r);
|
|
416
|
+
|
|
417
|
+
for (let i = 0; i < maxIter; i++) {
|
|
418
|
+
const Ap = A.multiply(p);
|
|
419
|
+
const alpha = rsold / (dotProduct(p, Ap) + 1e-15);
|
|
420
|
+
|
|
421
|
+
for (let j = 0; j < n; j++) x[j] += alpha * p[j];
|
|
422
|
+
for (let j = 0; j < n; j++) r[j] -= alpha * Ap[j];
|
|
423
|
+
|
|
424
|
+
const rsnew = dotProduct(r, r);
|
|
425
|
+
if (Math.sqrt(rsnew) < tol) break;
|
|
426
|
+
|
|
427
|
+
const beta = rsnew / (rsold + 1e-15);
|
|
428
|
+
for (let j = 0; j < n; j++) p[j] = r[j] + beta * p[j];
|
|
429
|
+
|
|
430
|
+
rsold = rsnew;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return x;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Compute Von Mises stress from displacement field
|
|
438
|
+
*/
|
|
439
|
+
function computeStresses(u, material) {
|
|
440
|
+
const { nodes, elements } = meshData;
|
|
441
|
+
const stresses = new Float64Array(elements.length);
|
|
442
|
+
const E = material.youngsModulus;
|
|
443
|
+
const nu = material.poissonsRatio;
|
|
444
|
+
|
|
445
|
+
elements.forEach((elem, idx) => {
|
|
446
|
+
if (elem.nodes.length !== 3) {
|
|
447
|
+
stresses[idx] = 0;
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const n0 = nodes[elem.nodes[0]];
|
|
452
|
+
const n1 = nodes[elem.nodes[1]];
|
|
453
|
+
const n2 = nodes[elem.nodes[2]];
|
|
454
|
+
|
|
455
|
+
// Strain at element center (simplified)
|
|
456
|
+
const du_dx = (u[elem.nodes[1]*3] - u[elem.nodes[0]*3]) / (n1.x - n0.x + 1e-10);
|
|
457
|
+
const du_dy = (u[elem.nodes[1]*3+1] - u[elem.nodes[0]*3+1]) / (n1.y - n0.y + 1e-10);
|
|
458
|
+
const du_dz = (u[elem.nodes[1]*3+2] - u[elem.nodes[0]*3+2]) / (n1.z - n0.z + 1e-10);
|
|
459
|
+
|
|
460
|
+
const strain = Math.sqrt(du_dx**2 + du_dy**2 + du_dz**2);
|
|
461
|
+
const stress = E * strain; // Hook's law (simplified)
|
|
462
|
+
|
|
463
|
+
// Von Mises (simplified)
|
|
464
|
+
stresses[idx] = Math.abs(stress) * (1 + 0.5 * (strain**2));
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
return stresses;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ========== 3. THERMAL ANALYSIS ==========
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Solve steady-state heat conduction: ∇·(k∇T) = Q
|
|
474
|
+
*/
|
|
475
|
+
function solveThermal(material, heatSources, boundaryConditions) {
|
|
476
|
+
if (!meshData) return { error: 'No mesh data' };
|
|
477
|
+
|
|
478
|
+
const { nodes, elements } = meshData;
|
|
479
|
+
const n = nodes.length;
|
|
480
|
+
|
|
481
|
+
const K_thermal = new SparseMatrix(n);
|
|
482
|
+
const Q = new Float64Array(n);
|
|
483
|
+
|
|
484
|
+
const k = material.thermalConductivity;
|
|
485
|
+
|
|
486
|
+
elements.forEach(elem => {
|
|
487
|
+
if (elem.nodes.length !== 3) return;
|
|
488
|
+
|
|
489
|
+
const n0 = nodes[elem.nodes[0]];
|
|
490
|
+
const n1 = nodes[elem.nodes[1]];
|
|
491
|
+
const n2 = nodes[elem.nodes[2]];
|
|
492
|
+
|
|
493
|
+
const v1 = { x: n1.x - n0.x, y: n1.y - n0.y, z: n1.z - n0.z };
|
|
494
|
+
const v2 = { x: n2.x - n0.x, y: n2.y - n0.y, z: n2.z - n0.z };
|
|
495
|
+
|
|
496
|
+
const area = 0.5 * Math.sqrt(
|
|
497
|
+
(v1.y*v2.z - v1.z*v2.y)**2 +
|
|
498
|
+
(v1.z*v2.x - v1.x*v2.z)**2 +
|
|
499
|
+
(v1.x*v2.y - v1.y*v2.x)**2
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
const k_e = (k * area) / (Math.sqrt(v1.x**2 + v1.y**2 + v1.z**2) + 1e-10);
|
|
503
|
+
|
|
504
|
+
elem.nodes.forEach((i, ii) => {
|
|
505
|
+
elem.nodes.forEach((j, jj) => {
|
|
506
|
+
K_thermal.add(i, j, ii === jj ? k_e : -k_e);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Add heat sources
|
|
512
|
+
heatSources.forEach(source => {
|
|
513
|
+
if (source.type === 'point') {
|
|
514
|
+
Q[source.nodeId] += source.magnitude || 100;
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Boundary conditions
|
|
519
|
+
boundaryConditions.forEach(bc => {
|
|
520
|
+
if (bc.type === 'fixed') {
|
|
521
|
+
K_thermal.set(bc.nodeId, bc.nodeId, 1e30);
|
|
522
|
+
Q[bc.nodeId] = bc.temperature * 1e30;
|
|
523
|
+
} else if (bc.type === 'convection') {
|
|
524
|
+
const h = bc.convectionCoeff || 10;
|
|
525
|
+
const T_inf = bc.ambientTemp || 293;
|
|
526
|
+
K_thermal.add(bc.nodeId, bc.nodeId, h);
|
|
527
|
+
Q[bc.nodeId] += h * T_inf;
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const T = conjugateGradient(K_thermal, Q, n, 1000, 1e-6);
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
temperature: T,
|
|
535
|
+
maxTemp: Math.max(...T),
|
|
536
|
+
minTemp: Math.min(...T),
|
|
537
|
+
avgTemp: T.reduce((a, b) => a + b, 0) / T.length,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ========== 4. MODAL/FREQUENCY ANALYSIS ==========
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Compute natural frequencies and mode shapes via power iteration
|
|
545
|
+
*/
|
|
546
|
+
/**
|
|
547
|
+
* Compute natural frequencies and mode shapes using eigenvalue analysis
|
|
548
|
+
*
|
|
549
|
+
* Solves generalized eigenvalue problem: K·φ = λ·M·φ
|
|
550
|
+
* where K = stiffness, M = mass, λ = eigenvalue (ω²), φ = eigenvector (mode shape).
|
|
551
|
+
* Returns lowest numModes frequencies and their mode shapes.
|
|
552
|
+
*
|
|
553
|
+
* Algorithm: Subspace iteration (inverse power method with spectral shifting).
|
|
554
|
+
* Identifies resonant frequencies where structure is vulnerable to vibration.
|
|
555
|
+
*
|
|
556
|
+
* @param {Object} material - Material properties {E, poissonsRatio, density, etc.}
|
|
557
|
+
* @param {Uint32Array} constraints - Fixed nodes (boundary conditions)
|
|
558
|
+
* @param {number} [numModes=6] - Number of modes to compute
|
|
559
|
+
* @returns {ModalResult} Natural frequencies and mode shapes for visualization
|
|
560
|
+
*/
|
|
561
|
+
function solveModal(material, constraints, numModes = 6) {
|
|
562
|
+
if (!meshData) return { error: 'No mesh data' };
|
|
563
|
+
|
|
564
|
+
const { nodes, elements } = meshData;
|
|
565
|
+
const nDOF = nodes.length * 3;
|
|
566
|
+
|
|
567
|
+
// Build mass and stiffness matrices (same as structural)
|
|
568
|
+
const K = new SparseMatrix(nDOF);
|
|
569
|
+
const M = new SparseMatrix(nDOF);
|
|
570
|
+
|
|
571
|
+
const E = material.youngsModulus;
|
|
572
|
+
const nu = material.poissonsRatio;
|
|
573
|
+
const rho = material.density;
|
|
574
|
+
|
|
575
|
+
elements.forEach(elem => {
|
|
576
|
+
if (elem.nodes.length !== 3) return;
|
|
577
|
+
|
|
578
|
+
const n0 = nodes[elem.nodes[0]];
|
|
579
|
+
const n1 = nodes[elem.nodes[1]];
|
|
580
|
+
const n2 = nodes[elem.nodes[2]];
|
|
581
|
+
|
|
582
|
+
const v1 = { x: n1.x - n0.x, y: n1.y - n0.y, z: n1.z - n0.z };
|
|
583
|
+
const v2 = { x: n2.x - n0.x, y: n2.y - n0.y, z: n2.z - n0.z };
|
|
584
|
+
|
|
585
|
+
const area = 0.5 * Math.sqrt(
|
|
586
|
+
(v1.y*v2.z - v1.z*v2.y)**2 +
|
|
587
|
+
(v1.z*v2.x - v1.x*v2.z)**2 +
|
|
588
|
+
(v1.x*v2.y - v1.y*v2.x)**2
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
const k_spring = (E * area) / Math.sqrt(v1.x**2 + v1.y**2 + v1.z**2 + 1e-10);
|
|
592
|
+
const mass = rho * area / 3;
|
|
593
|
+
|
|
594
|
+
elem.nodes.forEach(i => {
|
|
595
|
+
elem.nodes.forEach(j => {
|
|
596
|
+
K.add(i * 3, j * 3, i === j ? k_spring : -k_spring * 0.5);
|
|
597
|
+
M.add(i * 3, j * 3, i === j ? mass : 0);
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Apply constraints
|
|
603
|
+
constraints.forEach(constraint => {
|
|
604
|
+
if (constraint.type === 'fixed') {
|
|
605
|
+
for (let d = 0; d < 3; d++) {
|
|
606
|
+
const dof = constraint.nodeId * 3 + d;
|
|
607
|
+
K.set(dof, dof, 1e30);
|
|
608
|
+
M.set(dof, dof, 1);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const modes = [];
|
|
614
|
+
for (let m = 0; m < Math.min(numModes, 6); m++) {
|
|
615
|
+
const v = new Float64Array(nDOF);
|
|
616
|
+
for (let i = 0; i < nDOF; i++) v[i] = Math.random();
|
|
617
|
+
|
|
618
|
+
// Power iteration: K·v = λ·M·v
|
|
619
|
+
let lambda = 1;
|
|
620
|
+
for (let iter = 0; iter < 20; iter++) {
|
|
621
|
+
const Kv = K.multiply(v);
|
|
622
|
+
const Mv = M.multiply(v);
|
|
623
|
+
const lambdaNew = dotProduct(v, Kv) / (dotProduct(v, Mv) + 1e-15);
|
|
624
|
+
const denom = Math.sqrt(dotProduct(Mv, Mv)) + 1e-15;
|
|
625
|
+
for (let i = 0; i < nDOF; i++) v[i] = Kv[i] / (denom + 1e-15);
|
|
626
|
+
lambda = lambdaNew;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const frequency = Math.sqrt(Math.max(0, lambda)) / (2 * Math.PI);
|
|
630
|
+
modes.push({
|
|
631
|
+
modeNumber: m + 1,
|
|
632
|
+
frequency,
|
|
633
|
+
shapeVector: new Float64Array(v),
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
modes,
|
|
639
|
+
frequencies: modes.map(m => m.frequency),
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ========== 5. DROP TEST / IMPACT SIMULATION ==========
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Simulate drop test with time-domain integration
|
|
647
|
+
*/
|
|
648
|
+
function solveDropTest(material, dropHeight, orientation = 'flat') {
|
|
649
|
+
if (!meshData) return { error: 'No mesh data' };
|
|
650
|
+
|
|
651
|
+
const { nodes, elements } = meshData;
|
|
652
|
+
|
|
653
|
+
// Calculate impact velocity
|
|
654
|
+
const g = 9.81;
|
|
655
|
+
const v_impact = Math.sqrt(2 * g * dropHeight);
|
|
656
|
+
|
|
657
|
+
// Time integration (Newmark-beta)
|
|
658
|
+
const dt = 0.001; // 1ms time step
|
|
659
|
+
const maxTime = 0.5; // 0.5 second simulation
|
|
660
|
+
const nSteps = Math.floor(maxTime / dt);
|
|
661
|
+
|
|
662
|
+
const nDOF = nodes.length * 3;
|
|
663
|
+
const u = new Float64Array(nDOF); // displacement
|
|
664
|
+
const v = new Float64Array(nDOF); // velocity
|
|
665
|
+
const a = new Float64Array(nDOF); // acceleration
|
|
666
|
+
|
|
667
|
+
// Initial velocity (downward impact)
|
|
668
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
669
|
+
v[i * 3 + 1] = -v_impact; // Y-direction
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
let maxStress = 0;
|
|
673
|
+
let maxStressTime = 0;
|
|
674
|
+
let peakDeceleration = 0;
|
|
675
|
+
const stressHistory = [];
|
|
676
|
+
|
|
677
|
+
// Time stepping
|
|
678
|
+
for (let step = 0; step < nSteps; step++) {
|
|
679
|
+
// Contact with ground (y = 0)
|
|
680
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
681
|
+
const y = nodes[i].y + u[i * 3 + 1];
|
|
682
|
+
if (y < 0) {
|
|
683
|
+
u[i * 3 + 1] = -nodes[i].y; // Snap back to ground
|
|
684
|
+
v[i * 3 + 1] = Math.max(0, v[i * 3 + 1] * 0.7); // Damping on contact
|
|
685
|
+
a[i * 3 + 1] = 0;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Update velocities and displacements
|
|
690
|
+
for (let i = 0; i < nDOF; i++) {
|
|
691
|
+
v[i] += a[i] * dt;
|
|
692
|
+
u[i] += v[i] * dt + 0.5 * a[i] * dt * dt;
|
|
693
|
+
a[i] *= 0.99; // Damping
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Compute peak deceleration
|
|
697
|
+
const decel = Math.max(...a.map(Math.abs));
|
|
698
|
+
peakDeceleration = Math.max(peakDeceleration, decel);
|
|
699
|
+
|
|
700
|
+
// Compute stresses at this time step
|
|
701
|
+
const elemStresses = [];
|
|
702
|
+
elements.forEach(elem => {
|
|
703
|
+
if (elem.nodes.length === 3) {
|
|
704
|
+
const stress = Math.abs(material.youngsModulus * Math.sqrt(
|
|
705
|
+
Math.pow((u[elem.nodes[0]*3+1] - u[elem.nodes[1]*3+1]), 2) +
|
|
706
|
+
Math.pow((u[elem.nodes[1]*3+1] - u[elem.nodes[2]*3+1]), 2) + 1e-10
|
|
707
|
+
));
|
|
708
|
+
elemStresses.push(stress);
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
if (elemStresses.length > 0) {
|
|
713
|
+
const stepMaxStress = Math.max(...elemStresses);
|
|
714
|
+
stressHistory.push(stepMaxStress);
|
|
715
|
+
if (stepMaxStress > maxStress) {
|
|
716
|
+
maxStress = stepMaxStress;
|
|
717
|
+
maxStressTime = step * dt;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const factorOfSafety = maxStress > 0 ? material.yieldStress / maxStress : Infinity;
|
|
723
|
+
const passed = factorOfSafety >= 1.5;
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
maxStress,
|
|
727
|
+
maxStressTime,
|
|
728
|
+
peakDeceleration,
|
|
729
|
+
factorOfSafety,
|
|
730
|
+
passed,
|
|
731
|
+
displacement: u,
|
|
732
|
+
stressHistory,
|
|
733
|
+
impactVelocity: v_impact,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ========== 6. RESULT VISUALIZATION ==========
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Apply heatmap visualization to mesh
|
|
741
|
+
*/
|
|
742
|
+
function visualizeResults(results, resultType = 'stress') {
|
|
743
|
+
if (!scene || !meshData) return;
|
|
744
|
+
|
|
745
|
+
const { nodes, elements } = meshData;
|
|
746
|
+
const values = resultType === 'stress' ? results.stresses :
|
|
747
|
+
resultType === 'displacement' ? results.displacement :
|
|
748
|
+
resultType === 'temperature' ? results.temperature : results.stresses;
|
|
749
|
+
|
|
750
|
+
if (!values) return;
|
|
751
|
+
|
|
752
|
+
// Create canvas texture for heatmap
|
|
753
|
+
const canvas = document.createElement('canvas');
|
|
754
|
+
canvas.width = 512;
|
|
755
|
+
canvas.height = 512;
|
|
756
|
+
const ctx = canvas.getContext('2d');
|
|
757
|
+
|
|
758
|
+
const minVal = Math.min(...values);
|
|
759
|
+
const maxVal = Math.max(...values);
|
|
760
|
+
const range = maxVal - minVal + 1e-10;
|
|
761
|
+
|
|
762
|
+
// Draw heatmap gradient bar (for reference)
|
|
763
|
+
for (let i = 0; i < 512; i++) {
|
|
764
|
+
const val = minVal + (i / 512) * range;
|
|
765
|
+
const hue = (1 - (val - minVal) / range) * 240;
|
|
766
|
+
ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;
|
|
767
|
+
ctx.fillRect(i, 0, 1, 20);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Add stress data as small visualization elements
|
|
771
|
+
ctx.fillStyle = '#fff';
|
|
772
|
+
ctx.font = '12px monospace';
|
|
773
|
+
ctx.fillText(`Min: ${minVal.toExponential(2)}`, 10, 50);
|
|
774
|
+
ctx.fillText(`Max: ${maxVal.toExponential(2)}`, 10, 70);
|
|
775
|
+
|
|
776
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
777
|
+
simulationState.stressTexture = texture;
|
|
778
|
+
|
|
779
|
+
// Update geometry colors based on stress
|
|
780
|
+
const geometry = scene.getObjectByName('mainGeometry')?.geometry;
|
|
781
|
+
if (geometry) {
|
|
782
|
+
const colors = new Float32Array(nodes.length * 3);
|
|
783
|
+
|
|
784
|
+
elements.forEach((elem, idx) => {
|
|
785
|
+
const stress = values[idx] || 0;
|
|
786
|
+
const normalized = Math.max(0, Math.min(1, (stress - minVal) / range));
|
|
787
|
+
const hue = (1 - normalized) * 240;
|
|
788
|
+
const rgb = hslToRgb(hue, 100, 50);
|
|
789
|
+
|
|
790
|
+
elem.nodes.forEach(nId => {
|
|
791
|
+
colors[nId * 3] = rgb.r / 255;
|
|
792
|
+
colors[nId * 3 + 1] = rgb.g / 255;
|
|
793
|
+
colors[nId * 3 + 2] = rgb.b / 255;
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|
798
|
+
geometry.computeVertexNormals();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Apply deformation to geometry
|
|
804
|
+
*/
|
|
805
|
+
function visualizeDeformation(results, scale = 100) {
|
|
806
|
+
if (!scene || !meshData) return;
|
|
807
|
+
|
|
808
|
+
const geometry = scene.getObjectByName('mainGeometry')?.geometry;
|
|
809
|
+
if (!geometry) return;
|
|
810
|
+
|
|
811
|
+
const posAttr = geometry.getAttribute('position');
|
|
812
|
+
const positions = posAttr.array;
|
|
813
|
+
|
|
814
|
+
// Store original if not already done
|
|
815
|
+
if (!simulationState.originalPositions) {
|
|
816
|
+
simulationState.originalPositions = new Float32Array(positions);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const { nodes } = meshData;
|
|
820
|
+
const displacement = results.displacement;
|
|
821
|
+
|
|
822
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
823
|
+
positions[i * 3] = simulationState.originalPositions[i * 3] + (displacement[i * 3] * scale);
|
|
824
|
+
positions[i * 3 + 1] = simulationState.originalPositions[i * 3 + 1] + (displacement[i * 3 + 1] * scale);
|
|
825
|
+
positions[i * 3 + 2] = simulationState.originalPositions[i * 3 + 2] + (displacement[i * 3 + 2] * scale);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
posAttr.needsUpdate = true;
|
|
829
|
+
geometry.computeVertexNormals();
|
|
830
|
+
geometry.computeBoundingSphere();
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Probe tool: get local values at clicked position
|
|
835
|
+
*/
|
|
836
|
+
function probeAt(worldPos, results) {
|
|
837
|
+
if (!meshData) return null;
|
|
838
|
+
|
|
839
|
+
const { nodes } = meshData;
|
|
840
|
+
let closestNode = null;
|
|
841
|
+
let minDist = Infinity;
|
|
842
|
+
|
|
843
|
+
nodes.forEach((node, idx) => {
|
|
844
|
+
const dist = Math.sqrt(
|
|
845
|
+
(node.x - worldPos.x)**2 +
|
|
846
|
+
(node.y - worldPos.y)**2 +
|
|
847
|
+
(node.z - worldPos.z)**2
|
|
848
|
+
);
|
|
849
|
+
if (dist < minDist) {
|
|
850
|
+
minDist = dist;
|
|
851
|
+
closestNode = idx;
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
if (closestNode === null) return null;
|
|
856
|
+
|
|
857
|
+
return {
|
|
858
|
+
nodeId: closestNode,
|
|
859
|
+
position: nodes[closestNode],
|
|
860
|
+
stress: results.stresses ? results.stresses[closestNode] : null,
|
|
861
|
+
temperature: results.temperature ? results.temperature[closestNode] : null,
|
|
862
|
+
displacement: results.displacement ? {
|
|
863
|
+
x: results.displacement[closestNode * 3],
|
|
864
|
+
y: results.displacement[closestNode * 3 + 1],
|
|
865
|
+
z: results.displacement[closestNode * 3 + 2],
|
|
866
|
+
} : null,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// ========== UTILITIES ==========
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Sparse matrix class using COO format
|
|
874
|
+
*/
|
|
875
|
+
class SparseMatrix {
|
|
876
|
+
constructor(n) {
|
|
877
|
+
this.n = n;
|
|
878
|
+
this.entries = new Map(); // key: "i,j", value: coefficient
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
add(i, j, val) {
|
|
882
|
+
const key = `${i},${j}`;
|
|
883
|
+
this.entries.set(key, (this.entries.get(key) || 0) + val);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
set(i, j, val) {
|
|
887
|
+
const key = `${i},${j}`;
|
|
888
|
+
this.entries.set(key, val);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
multiply(x) {
|
|
892
|
+
const result = new Float64Array(this.n);
|
|
893
|
+
for (const [key, val] of this.entries) {
|
|
894
|
+
const [i, j] = key.split(',').map(Number);
|
|
895
|
+
result[i] += val * x[j];
|
|
896
|
+
}
|
|
897
|
+
return result;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Dot product of two vectors
|
|
903
|
+
*/
|
|
904
|
+
function dotProduct(a, b) {
|
|
905
|
+
let sum = 0;
|
|
906
|
+
for (let i = 0; i < a.length; i++) sum += a[i] * b[i];
|
|
907
|
+
return sum;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* HSL to RGB conversion
|
|
912
|
+
*/
|
|
913
|
+
function hslToRgb(h, s, l) {
|
|
914
|
+
s /= 100;
|
|
915
|
+
l /= 100;
|
|
916
|
+
const k = n => (n + h / 30) % 12;
|
|
917
|
+
const a = s * Math.min(l, 1 - l);
|
|
918
|
+
const f = n => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
919
|
+
return {
|
|
920
|
+
r: Math.round(255 * f(0)),
|
|
921
|
+
g: Math.round(255 * f(8)),
|
|
922
|
+
b: Math.round(255 * f(4)),
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// ========== INITIALIZATION & UI ==========
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Initialize module with scene
|
|
930
|
+
*/
|
|
931
|
+
/**
|
|
932
|
+
* Initialize MultiPhysics module with Three.js scene
|
|
933
|
+
*
|
|
934
|
+
* Sets up visualization materials, camera, renderer, and UI panel.
|
|
935
|
+
* Must be called once before execute() calls.
|
|
936
|
+
*
|
|
937
|
+
* @param {THREE.Scene} sceneRef - The Three.js scene object
|
|
938
|
+
* @returns {void}
|
|
939
|
+
*/
|
|
940
|
+
function init(sceneRef) {
|
|
941
|
+
scene = sceneRef;
|
|
942
|
+
|
|
943
|
+
// Listen for geometry changes
|
|
944
|
+
if (window.CycleCAD && window.CycleCAD.events) {
|
|
945
|
+
window.CycleCAD.events.on('geometryUpdated', (geometry) => {
|
|
946
|
+
meshData = discretizeMesh(geometry, 'medium');
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Get UI panel (HTML)
|
|
953
|
+
*/
|
|
954
|
+
function getUI() {
|
|
955
|
+
const panel = document.createElement('div');
|
|
956
|
+
panel.className = 'multi-physics-panel';
|
|
957
|
+
panel.style.cssText = `
|
|
958
|
+
background: var(--bg-secondary, #252526);
|
|
959
|
+
color: var(--text-primary, #e0e0e0);
|
|
960
|
+
border: 1px solid var(--border-color, #3e3e42);
|
|
961
|
+
border-radius: 4px;
|
|
962
|
+
padding: 12px;
|
|
963
|
+
font-family: 'Segoe UI', sans-serif;
|
|
964
|
+
font-size: 12px;
|
|
965
|
+
max-width: 320px;
|
|
966
|
+
max-height: 600px;
|
|
967
|
+
overflow-y: auto;
|
|
968
|
+
`;
|
|
969
|
+
|
|
970
|
+
panel.innerHTML = `
|
|
971
|
+
<div style="margin-bottom: 12px;">
|
|
972
|
+
<h3 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 600;">Multi-Physics Simulation</h3>
|
|
973
|
+
|
|
974
|
+
<!-- Analysis Type Selector -->
|
|
975
|
+
<label style="display: block; margin-bottom: 8px;">
|
|
976
|
+
Analysis Type:
|
|
977
|
+
<select id="analysisType" style="width: 100%; padding: 4px; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); border: 1px solid var(--border-color, #3e3e42); border-radius: 2px;">
|
|
978
|
+
<option value="static">Structural (Static FEA)</option>
|
|
979
|
+
<option value="thermal">Thermal</option>
|
|
980
|
+
<option value="modal">Modal (Frequencies)</option>
|
|
981
|
+
<option value="droptest">Drop Test / Impact</option>
|
|
982
|
+
</select>
|
|
983
|
+
</label>
|
|
984
|
+
|
|
985
|
+
<!-- Material Selector -->
|
|
986
|
+
<label style="display: block; margin-bottom: 8px;">
|
|
987
|
+
Material:
|
|
988
|
+
<select id="materialSelect" style="width: 100%; padding: 4px; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); border: 1px solid var(--border-color, #3e3e42); border-radius: 2px;">
|
|
989
|
+
<option value="steel">Structural Steel</option>
|
|
990
|
+
<option value="aluminum">Aluminum 6061</option>
|
|
991
|
+
<option value="titanium">Titanium Grade 5</option>
|
|
992
|
+
<option value="abs">ABS Plastic</option>
|
|
993
|
+
</select>
|
|
994
|
+
</label>
|
|
995
|
+
|
|
996
|
+
<!-- Mesh Resolution -->
|
|
997
|
+
<label style="display: block; margin-bottom: 8px;">
|
|
998
|
+
Mesh Resolution:
|
|
999
|
+
<input type="range" id="meshResolution" min="1" max="3" value="2" style="width: 100%;">
|
|
1000
|
+
<span id="meshResLabel" style="font-size: 11px; color: var(--text-secondary, #cccccc);">Medium</span>
|
|
1001
|
+
</label>
|
|
1002
|
+
|
|
1003
|
+
<!-- Load Definition -->
|
|
1004
|
+
<fieldset style="border: 1px solid var(--border-color, #3e3e42); border-radius: 2px; padding: 8px; margin-bottom: 8px;">
|
|
1005
|
+
<legend style="padding: 0 4px; font-size: 11px; font-weight: 600;">Load Definition</legend>
|
|
1006
|
+
|
|
1007
|
+
<div style="margin-bottom: 6px;">
|
|
1008
|
+
<label style="display: block; font-size: 11px;">Force (N):</label>
|
|
1009
|
+
<input type="number" id="forceX" placeholder="X" min="-1000" max="1000" value="0" style="width: 32%; padding: 2px; margin-right: 2px;">
|
|
1010
|
+
<input type="number" id="forceY" placeholder="Y" min="-1000" max="1000" value="-100" style="width: 32%; padding: 2px; margin-right: 2px;">
|
|
1011
|
+
<input type="number" id="forceZ" placeholder="Z" min="-1000" max="1000" value="0" style="width: 32%; padding: 2px;">
|
|
1012
|
+
</div>
|
|
1013
|
+
|
|
1014
|
+
<label style="display: block; margin-bottom: 4px;">
|
|
1015
|
+
<input type="checkbox" id="applyGravity" checked>
|
|
1016
|
+
<span style="font-size: 11px;">Include Gravity</span>
|
|
1017
|
+
</label>
|
|
1018
|
+
</fieldset>
|
|
1019
|
+
|
|
1020
|
+
<!-- Boundary Conditions -->
|
|
1021
|
+
<fieldset style="border: 1px solid var(--border-color, #3e3e42); border-radius: 2px; padding: 8px; margin-bottom: 8px;">
|
|
1022
|
+
<legend style="padding: 0 4px; font-size: 11px; font-weight: 600;">Constraints</legend>
|
|
1023
|
+
|
|
1024
|
+
<label style="display: block; margin-bottom: 4px;">
|
|
1025
|
+
<input type="checkbox" id="fixBase" checked>
|
|
1026
|
+
<span style="font-size: 11px;">Fix Base (Y=0)</span>
|
|
1027
|
+
</label>
|
|
1028
|
+
</fieldset>
|
|
1029
|
+
|
|
1030
|
+
<!-- Drop Test Parameters -->
|
|
1031
|
+
<div id="dropTestParams" style="display: none; border: 1px solid var(--border-color, #3e3e42); border-radius: 2px; padding: 8px; margin-bottom: 8px;">
|
|
1032
|
+
<label style="display: block; margin-bottom: 6px;">
|
|
1033
|
+
Drop Height (m):
|
|
1034
|
+
<input type="number" id="dropHeight" min="0.1" max="10" step="0.1" value="1" style="width: 100%; padding: 4px; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); border: 1px solid var(--border-color, #3e3e42); border-radius: 2px;">
|
|
1035
|
+
</label>
|
|
1036
|
+
</div>
|
|
1037
|
+
|
|
1038
|
+
<!-- Modal Parameters -->
|
|
1039
|
+
<div id="modalParams" style="display: none; border: 1px solid var(--border-color, #3e3e42); border-radius: 2px; padding: 8px; margin-bottom: 8px;">
|
|
1040
|
+
<label style="display: block; margin-bottom: 6px;">
|
|
1041
|
+
Number of Modes (1-6):
|
|
1042
|
+
<input type="number" id="numModes" min="1" max="6" value="6" style="width: 100%; padding: 4px; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); border: 1px solid var(--border-color, #3e3e42); border-radius: 2px;">
|
|
1043
|
+
</label>
|
|
1044
|
+
</div>
|
|
1045
|
+
|
|
1046
|
+
<!-- Visualization Controls -->
|
|
1047
|
+
<fieldset style="border: 1px solid var(--border-color, #3e3e42); border-radius: 2px; padding: 8px; margin-bottom: 8px;">
|
|
1048
|
+
<legend style="padding: 0 4px; font-size: 11px; font-weight: 600;">Visualization</legend>
|
|
1049
|
+
|
|
1050
|
+
<label style="display: block; margin-bottom: 6px;">
|
|
1051
|
+
Result Type:
|
|
1052
|
+
<select id="resultType" style="width: 100%; padding: 4px; background: var(--bg-primary, #1e1e1e); color: var(--text-primary, #e0e0e0); border: 1px solid var(--border-color, #3e3e42); border-radius: 2px;">
|
|
1053
|
+
<option value="stress">Von Mises Stress</option>
|
|
1054
|
+
<option value="displacement">Displacement Magnitude</option>
|
|
1055
|
+
<option value="temperature">Temperature</option>
|
|
1056
|
+
</select>
|
|
1057
|
+
</label>
|
|
1058
|
+
|
|
1059
|
+
<label style="display: block; margin-bottom: 4px;">
|
|
1060
|
+
Deformation Scale:
|
|
1061
|
+
<input type="range" id="deformScale" min="0" max="1000" value="100" style="width: 100%;">
|
|
1062
|
+
<span id="deformLabel" style="font-size: 11px; color: var(--text-secondary, #cccccc);">100×</span>
|
|
1063
|
+
</label>
|
|
1064
|
+
</fieldset>
|
|
1065
|
+
|
|
1066
|
+
<!-- Results Section -->
|
|
1067
|
+
<div id="resultsSection" style="display: none; background: var(--bg-primary, #1e1e1e); border-radius: 2px; padding: 8px; margin-bottom: 8px; border: 1px solid var(--border-color, #3e3e42);">
|
|
1068
|
+
<h4 style="margin: 0 0 6px 0; font-size: 12px; font-weight: 600;">Results</h4>
|
|
1069
|
+
<div id="resultsList" style="font-size: 11px; line-height: 1.6;">
|
|
1070
|
+
<!-- Populated dynamically -->
|
|
1071
|
+
</div>
|
|
1072
|
+
</div>
|
|
1073
|
+
|
|
1074
|
+
<!-- Progress Bar -->
|
|
1075
|
+
<div id="progressContainer" style="display: none; margin-bottom: 8px;">
|
|
1076
|
+
<div style="background: var(--bg-primary, #1e1e1e); border: 1px solid var(--border-color, #3e3e42); border-radius: 2px; overflow: hidden; height: 20px;">
|
|
1077
|
+
<div id="progressBar" style="background: linear-gradient(90deg, var(--accent-blue, #0284C7), var(--accent-green, #16a34a)); height: 100%; width: 0%; transition: width 0.1s;"></div>
|
|
1078
|
+
</div>
|
|
1079
|
+
<div id="progressText" style="font-size: 10px; margin-top: 2px; text-align: center;">0%</div>
|
|
1080
|
+
</div>
|
|
1081
|
+
|
|
1082
|
+
<!-- Action Buttons -->
|
|
1083
|
+
<div style="display: flex; gap: 6px;">
|
|
1084
|
+
<button id="runButton" style="flex: 1; padding: 6px 8px; background: var(--accent-blue, #0284C7); color: white; border: none; border-radius: 2px; cursor: pointer; font-size: 12px; font-weight: 600;">Run Analysis</button>
|
|
1085
|
+
<button id="resetButton" style="flex: 1; padding: 6px 8px; background: var(--border-color, #3e3e42); color: var(--text-primary, #e0e0e0); border: none; border-radius: 2px; cursor: pointer; font-size: 12px;">Reset</button>
|
|
1086
|
+
<button id="exportButton" style="flex: 0.8; padding: 6px 8px; background: var(--border-color, #3e3e42); color: var(--text-primary, #e0e0e0); border: none; border-radius: 2px; cursor: pointer; font-size: 11px;">Export</button>
|
|
1087
|
+
</div>
|
|
1088
|
+
</div>
|
|
1089
|
+
`;
|
|
1090
|
+
|
|
1091
|
+
// Event listeners
|
|
1092
|
+
const analysisTypeSelect = panel.querySelector('#analysisType');
|
|
1093
|
+
const dropTestParams = panel.querySelector('#dropTestParams');
|
|
1094
|
+
const modalParams = panel.querySelector('#modalParams');
|
|
1095
|
+
const meshResSlider = panel.querySelector('#meshResolution');
|
|
1096
|
+
const meshResLabel = panel.querySelector('#meshResLabel');
|
|
1097
|
+
const deformSlider = panel.querySelector('#deformScale');
|
|
1098
|
+
const deformLabel = panel.querySelector('#deformLabel');
|
|
1099
|
+
const runButton = panel.querySelector('#runButton');
|
|
1100
|
+
const resetButton = panel.querySelector('#resetButton');
|
|
1101
|
+
const exportButton = panel.querySelector('#exportButton');
|
|
1102
|
+
|
|
1103
|
+
analysisTypeSelect.addEventListener('change', (e) => {
|
|
1104
|
+
currentAnalysis = e.target.value;
|
|
1105
|
+
dropTestParams.style.display = currentAnalysis === 'droptest' ? 'block' : 'none';
|
|
1106
|
+
modalParams.style.display = currentAnalysis === 'modal' ? 'block' : 'none';
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
meshResSlider.addEventListener('input', (e) => {
|
|
1110
|
+
const labels = ['Coarse', 'Medium', 'Fine'];
|
|
1111
|
+
meshResLabel.textContent = labels[parseInt(e.target.value) - 1];
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
deformSlider.addEventListener('input', (e) => {
|
|
1115
|
+
deformLabel.textContent = `${e.target.value}×`;
|
|
1116
|
+
if (simulationState.results) {
|
|
1117
|
+
visualizeDeformation(simulationState.results, parseInt(e.target.value));
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
runButton.addEventListener('click', () => runAnalysis(panel));
|
|
1122
|
+
resetButton.addEventListener('click', () => resetSimulation());
|
|
1123
|
+
exportButton.addEventListener('click', () => exportResults(panel));
|
|
1124
|
+
|
|
1125
|
+
return panel;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Run selected analysis
|
|
1130
|
+
*/
|
|
1131
|
+
function runAnalysis(panel) {
|
|
1132
|
+
if (simulationState.isRunning || !meshData) return;
|
|
1133
|
+
|
|
1134
|
+
simulationState.isRunning = true;
|
|
1135
|
+
simulationState.progress = 0;
|
|
1136
|
+
|
|
1137
|
+
const materialKey = panel.querySelector('#materialSelect').value;
|
|
1138
|
+
const material = MATERIALS[materialKey];
|
|
1139
|
+
const progressContainer = panel.querySelector('#progressContainer');
|
|
1140
|
+
const progressBar = panel.querySelector('#progressBar');
|
|
1141
|
+
const progressText = panel.querySelector('#progressText');
|
|
1142
|
+
|
|
1143
|
+
progressContainer.style.display = 'block';
|
|
1144
|
+
|
|
1145
|
+
const updateProgress = (pct) => {
|
|
1146
|
+
simulationState.progress = pct;
|
|
1147
|
+
progressBar.style.width = `${pct}%`;
|
|
1148
|
+
progressText.textContent = `${Math.round(pct)}%`;
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
requestAnimationFrame(() => {
|
|
1152
|
+
updateProgress(20);
|
|
1153
|
+
|
|
1154
|
+
setTimeout(() => {
|
|
1155
|
+
let result;
|
|
1156
|
+
|
|
1157
|
+
if (currentAnalysis === 'static') {
|
|
1158
|
+
const loads = [{
|
|
1159
|
+
type: 'point',
|
|
1160
|
+
nodeId: 0,
|
|
1161
|
+
force: {
|
|
1162
|
+
x: parseFloat(panel.querySelector('#forceX').value),
|
|
1163
|
+
y: parseFloat(panel.querySelector('#forceY').value),
|
|
1164
|
+
z: parseFloat(panel.querySelector('#forceZ').value),
|
|
1165
|
+
},
|
|
1166
|
+
}];
|
|
1167
|
+
|
|
1168
|
+
const constraints = panel.querySelector('#fixBase').checked ? [{
|
|
1169
|
+
type: 'fixed',
|
|
1170
|
+
nodeId: meshData.nodes.length - 1,
|
|
1171
|
+
}] : [];
|
|
1172
|
+
|
|
1173
|
+
result = solveStructural(material, loads, constraints);
|
|
1174
|
+
} else if (currentAnalysis === 'thermal') {
|
|
1175
|
+
const heatSources = [{
|
|
1176
|
+
type: 'point',
|
|
1177
|
+
nodeId: 0,
|
|
1178
|
+
magnitude: 100,
|
|
1179
|
+
}];
|
|
1180
|
+
|
|
1181
|
+
const bc = [{
|
|
1182
|
+
type: 'fixed',
|
|
1183
|
+
nodeId: meshData.nodes.length - 1,
|
|
1184
|
+
temperature: 293,
|
|
1185
|
+
}];
|
|
1186
|
+
|
|
1187
|
+
result = solveThermal(material, heatSources, bc);
|
|
1188
|
+
} else if (currentAnalysis === 'modal') {
|
|
1189
|
+
const numModes = parseInt(panel.querySelector('#numModes').value);
|
|
1190
|
+
const constraints = [{
|
|
1191
|
+
type: 'fixed',
|
|
1192
|
+
nodeId: meshData.nodes.length - 1,
|
|
1193
|
+
}];
|
|
1194
|
+
|
|
1195
|
+
result = solveModal(material, constraints, numModes);
|
|
1196
|
+
} else if (currentAnalysis === 'droptest') {
|
|
1197
|
+
const dropHeight = parseFloat(panel.querySelector('#dropHeight').value);
|
|
1198
|
+
result = solveDropTest(material, dropHeight);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
updateProgress(80);
|
|
1202
|
+
|
|
1203
|
+
if (result && !result.error) {
|
|
1204
|
+
simulationState.results = result;
|
|
1205
|
+
displayResults(panel, result);
|
|
1206
|
+
visualizeResults(result, panel.querySelector('#resultType').value);
|
|
1207
|
+
updateProgress(100);
|
|
1208
|
+
|
|
1209
|
+
setTimeout(() => {
|
|
1210
|
+
progressContainer.style.display = 'none';
|
|
1211
|
+
simulationState.isRunning = false;
|
|
1212
|
+
}, 500);
|
|
1213
|
+
}
|
|
1214
|
+
}, 100);
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* Display results in panel
|
|
1220
|
+
*/
|
|
1221
|
+
function displayResults(panel, results) {
|
|
1222
|
+
const resultsSection = panel.querySelector('#resultsSection');
|
|
1223
|
+
const resultsList = panel.querySelector('#resultsList');
|
|
1224
|
+
|
|
1225
|
+
resultsSection.style.display = 'block';
|
|
1226
|
+
let html = '';
|
|
1227
|
+
|
|
1228
|
+
if (currentAnalysis === 'static') {
|
|
1229
|
+
html = `
|
|
1230
|
+
<div><strong>Max Stress:</strong> ${(results.maxStress / 1e6).toFixed(2)} MPa</div>
|
|
1231
|
+
<div><strong>Avg Stress:</strong> ${(results.avgStress / 1e6).toFixed(2)} MPa</div>
|
|
1232
|
+
<div><strong>Factor of Safety:</strong> ${results.factorOfSafety.toFixed(2)}</div>
|
|
1233
|
+
<div style="color: ${results.factorOfSafety < 1 ? '#f87171' : results.factorOfSafety < 1.5 ? '#fbbf24' : '#86efac'};">
|
|
1234
|
+
Status: ${results.factorOfSafety < 1 ? '⚠ FAILURE' : results.factorOfSafety < 1.5 ? '⚠ CAUTION' : '✓ SAFE'}
|
|
1235
|
+
</div>
|
|
1236
|
+
`;
|
|
1237
|
+
} else if (currentAnalysis === 'thermal') {
|
|
1238
|
+
html = `
|
|
1239
|
+
<div><strong>Max Temp:</strong> ${results.maxTemp.toFixed(1)} K</div>
|
|
1240
|
+
<div><strong>Min Temp:</strong> ${results.minTemp.toFixed(1)} K</div>
|
|
1241
|
+
<div><strong>Avg Temp:</strong> ${results.avgTemp.toFixed(1)} K</div>
|
|
1242
|
+
`;
|
|
1243
|
+
} else if (currentAnalysis === 'modal') {
|
|
1244
|
+
html = '<div><strong>Natural Frequencies:</strong></div>';
|
|
1245
|
+
results.modes.forEach(mode => {
|
|
1246
|
+
html += `<div style="padding-left: 8px;">Mode ${mode.modeNumber}: ${mode.frequency.toFixed(2)} Hz</div>`;
|
|
1247
|
+
});
|
|
1248
|
+
} else if (currentAnalysis === 'droptest') {
|
|
1249
|
+
html = `
|
|
1250
|
+
<div><strong>Impact Velocity:</strong> ${results.impactVelocity.toFixed(2)} m/s</div>
|
|
1251
|
+
<div><strong>Peak Deceleration:</strong> ${(results.peakDeceleration / 9.81).toFixed(1)} g</div>
|
|
1252
|
+
<div><strong>Max Stress:</strong> ${(results.maxStress / 1e6).toFixed(2)} MPa</div>
|
|
1253
|
+
<div><strong>Factor of Safety:</strong> ${results.factorOfSafety.toFixed(2)}</div>
|
|
1254
|
+
<div style="color: ${results.passed ? '#86efac' : '#f87171'};">
|
|
1255
|
+
Result: ${results.passed ? '✓ PASS' : '✗ FAIL'}
|
|
1256
|
+
</div>
|
|
1257
|
+
`;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
resultsList.innerHTML = html;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Reset simulation
|
|
1265
|
+
*/
|
|
1266
|
+
function resetSimulation() {
|
|
1267
|
+
simulationState = {
|
|
1268
|
+
isRunning: false,
|
|
1269
|
+
progress: 0,
|
|
1270
|
+
results: null,
|
|
1271
|
+
deformedGeometry: null,
|
|
1272
|
+
stressTexture: null,
|
|
1273
|
+
originalPositions: null,
|
|
1274
|
+
selectedProbe: null,
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
// Restore original geometry
|
|
1278
|
+
const geometry = scene?.getObjectByName('mainGeometry')?.geometry;
|
|
1279
|
+
if (geometry && simulationState.originalPositions) {
|
|
1280
|
+
const posAttr = geometry.getAttribute('position');
|
|
1281
|
+
posAttr.array.set(simulationState.originalPositions);
|
|
1282
|
+
posAttr.needsUpdate = true;
|
|
1283
|
+
geometry.computeVertexNormals();
|
|
1284
|
+
geometry.computeBoundingSphere();
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Export results as JSON + CSV
|
|
1290
|
+
*/
|
|
1291
|
+
function exportResults(panel) {
|
|
1292
|
+
if (!simulationState.results) {
|
|
1293
|
+
alert('Run an analysis first');
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const results = simulationState.results;
|
|
1298
|
+
const json = JSON.stringify({
|
|
1299
|
+
analysisType: currentAnalysis,
|
|
1300
|
+
timestamp: new Date().toISOString(),
|
|
1301
|
+
results: {
|
|
1302
|
+
maxStress: results.maxStress,
|
|
1303
|
+
maxStressLocation: results.maxStressLocation,
|
|
1304
|
+
factorOfSafety: results.factorOfSafety,
|
|
1305
|
+
frequencies: results.frequencies,
|
|
1306
|
+
},
|
|
1307
|
+
}, null, 2);
|
|
1308
|
+
|
|
1309
|
+
const link = document.createElement('a');
|
|
1310
|
+
link.href = 'data:application/json;charset=utf-8,' + encodeURIComponent(json);
|
|
1311
|
+
link.download = `simulation_${currentAnalysis}_${Date.now()}.json`;
|
|
1312
|
+
link.click();
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Execute command (for agent API)
|
|
1317
|
+
*/
|
|
1318
|
+
/**
|
|
1319
|
+
* Execute command in MultiPhysics module (public API)
|
|
1320
|
+
*
|
|
1321
|
+
* Analysis types:
|
|
1322
|
+
* - 'analyzeStatic': Structural FEA with applied loads
|
|
1323
|
+
* - 'analyzeThermal': Steady-state temperature analysis
|
|
1324
|
+
* - 'analyzeModal': Natural frequencies and mode shapes
|
|
1325
|
+
* - 'analyzeDynamic': Time-domain response to excitation
|
|
1326
|
+
* - 'analyzeDropTest': Impact analysis from specified drop height
|
|
1327
|
+
* - 'visualize': Render results with heatmaps and deformation
|
|
1328
|
+
* - 'probePoint': Query results at specific world location
|
|
1329
|
+
* - 'exportReport': Generate PDF report with all results
|
|
1330
|
+
*
|
|
1331
|
+
* @param {string} command - Command name
|
|
1332
|
+
* @param {Object} [params={}] - Command parameters
|
|
1333
|
+
* @param {string} params.analysisType - Type of analysis to run
|
|
1334
|
+
* @param {string} params.material - Material key (from MATERIALS)
|
|
1335
|
+
* @param {Object} params.load - Load specification {type, value, location, direction}
|
|
1336
|
+
* @param {number} params.dropHeight - For drop test: height in mm
|
|
1337
|
+
* @returns {SimulationResult} Analysis results with stresses, temperatures, frequencies, etc.
|
|
1338
|
+
* @example
|
|
1339
|
+
* const result = window.CycleCAD.MultiPhysics.execute('analyzeStatic', {
|
|
1340
|
+
* material: 'steel',
|
|
1341
|
+
* load: {type: 'point', value: 1000, location: {x: 0, y: 100, z: 0}}
|
|
1342
|
+
* });
|
|
1343
|
+
*/
|
|
1344
|
+
function execute(command, params = {}) {
|
|
1345
|
+
if (command === 'runFEA') {
|
|
1346
|
+
return solveStructural(
|
|
1347
|
+
MATERIALS[params.material] || MATERIALS.steel,
|
|
1348
|
+
params.loads || [],
|
|
1349
|
+
params.constraints || []
|
|
1350
|
+
);
|
|
1351
|
+
} else if (command === 'runThermal') {
|
|
1352
|
+
return solveThermal(
|
|
1353
|
+
MATERIALS[params.material] || MATERIALS.steel,
|
|
1354
|
+
params.heatSources || [],
|
|
1355
|
+
params.boundaryConditions || []
|
|
1356
|
+
);
|
|
1357
|
+
} else if (command === 'runModal') {
|
|
1358
|
+
return solveModal(
|
|
1359
|
+
MATERIALS[params.material] || MATERIALS.steel,
|
|
1360
|
+
params.constraints || [],
|
|
1361
|
+
params.numModes || 6
|
|
1362
|
+
);
|
|
1363
|
+
} else if (command === 'runDropTest') {
|
|
1364
|
+
return solveDropTest(
|
|
1365
|
+
MATERIALS[params.material] || MATERIALS.steel,
|
|
1366
|
+
params.dropHeight || 1,
|
|
1367
|
+
params.orientation || 'flat'
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
return { error: 'Unknown command' };
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// ========== PUBLIC API ==========
|
|
1374
|
+
|
|
1375
|
+
return {
|
|
1376
|
+
init,
|
|
1377
|
+
getUI,
|
|
1378
|
+
execute,
|
|
1379
|
+
runSimulation: runAnalysis,
|
|
1380
|
+
getResults: () => simulationState.results,
|
|
1381
|
+
discretizeMesh,
|
|
1382
|
+
solveStructural,
|
|
1383
|
+
solveThermal,
|
|
1384
|
+
solveModal,
|
|
1385
|
+
solveDropTest,
|
|
1386
|
+
visualizeResults,
|
|
1387
|
+
visualizeDeformation,
|
|
1388
|
+
probeAt,
|
|
1389
|
+
resetSimulation,
|
|
1390
|
+
};
|
|
1391
|
+
})();
|
|
1392
|
+
|
|
1393
|
+
// Auto-init if in DOM
|
|
1394
|
+
if (document.readyState === 'loading') {
|
|
1395
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1396
|
+
if (window.CycleCAD?.MultiPhysics?.init) {
|
|
1397
|
+
window.CycleCAD.MultiPhysics.init(window.scene || null);
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
} else {
|
|
1401
|
+
if (window.CycleCAD?.MultiPhysics?.init) {
|
|
1402
|
+
window.CycleCAD.MultiPhysics.init(window.scene || null);
|
|
1403
|
+
}
|
|
1404
|
+
}
|