cyclecad 0.4.0 → 0.5.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/index.html +9 -1
- package/app/js/generative-design.js +625 -0
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -1301,6 +1301,7 @@
|
|
|
1301
1301
|
<script src="./js/gdt-training.js"></script>
|
|
1302
1302
|
<script src="./js/misumi-catalog.js"></script>
|
|
1303
1303
|
<script src="./js/cad-vr.js"></script>
|
|
1304
|
+
<script src="./js/generative-design.js"></script>
|
|
1304
1305
|
</head>
|
|
1305
1306
|
<body>
|
|
1306
1307
|
<div id="app">
|
|
@@ -1558,6 +1559,10 @@
|
|
|
1558
1559
|
<span class="toolbar-icon">🥽</span>
|
|
1559
1560
|
<span class="toolbar-label">VR</span>
|
|
1560
1561
|
</button>
|
|
1562
|
+
<button class="toolbar-button" id="btn-generative" title="Generative Design — Topology Optimization" style="background:rgba(16,185,129,0.1);border:1px solid rgba(16,185,129,0.3);">
|
|
1563
|
+
<span class="toolbar-icon">🧬</span>
|
|
1564
|
+
<span class="toolbar-label">GenDes</span>
|
|
1565
|
+
</button>
|
|
1561
1566
|
</div>
|
|
1562
1567
|
|
|
1563
1568
|
<!-- Agent API Panel -->
|
|
@@ -3345,7 +3350,8 @@
|
|
|
3345
3350
|
'marketplace-v2-panel': '🏪 Model Marketplace',
|
|
3346
3351
|
'gdt-panel': '📐 GD&T Training',
|
|
3347
3352
|
'misumi-panel': '🔩 MISUMI Catalog',
|
|
3348
|
-
'vr-panel': '🥽 CAD2VR'
|
|
3353
|
+
'vr-panel': '🥽 CAD2VR',
|
|
3354
|
+
'generative-panel': '🧬 Generative Design'
|
|
3349
3355
|
};
|
|
3350
3356
|
header.innerHTML = `<span style="font-weight:600;font-size:13px;">${titles[panelId] || moduleKey}</span><button onclick="document.getElementById('${panelId}').style.display='none'" style="background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:16px;">✕</button>`;
|
|
3351
3357
|
panel.appendChild(header);
|
|
@@ -3384,6 +3390,7 @@
|
|
|
3384
3390
|
document.getElementById('btn-gdt')?.addEventListener('click', () => toggleModulePanel('gdtTraining', 'gdt-panel'));
|
|
3385
3391
|
document.getElementById('btn-misumi')?.addEventListener('click', () => toggleModulePanel('misumi', 'misumi-panel'));
|
|
3386
3392
|
document.getElementById('btn-vr')?.addEventListener('click', () => toggleModulePanel('cadVR', 'vr-panel'));
|
|
3393
|
+
document.getElementById('btn-generative')?.addEventListener('click', () => toggleModulePanel('generativeDesign', 'generative-panel'));
|
|
3387
3394
|
|
|
3388
3395
|
// ========== Help & Tutorials Panel ==========
|
|
3389
3396
|
document.getElementById('btn-help')?.addEventListener('click', () => {
|
|
@@ -3464,6 +3471,7 @@
|
|
|
3464
3471
|
{ cat: 'Analysis', items: ['Section View', 'Material Library', 'Weight Estimator', 'Part Comparison', 'Clearance Checker'] },
|
|
3465
3472
|
{ cat: 'Import/Export', items: ['STL (ASCII + binary)', 'OBJ', 'glTF 2.0', 'DXF (2D + 3D)', 'cycleCAD JSON', 'Inventor .ipt/.iam', 'STEP (via server)'] },
|
|
3466
3473
|
{ cat: 'Platform', items: ['Agent API (55 commands)', 'MCP Server', 'REST API', 'CLI Tool', '$CYCLE Token Engine', 'Model Marketplace V2 (GrabCAD-style)', 'Collaboration', 'CAD2VR (WebXR)'] },
|
|
3474
|
+
{ cat: 'Simulation', items: ['Generative Design (Topology Optimization)', 'SIMP Solver (Web Worker)', '15 Materials Library', 'Loads & Constraints', 'Marching Cubes Mesh Generation', 'Manufacturing Constraints'] },
|
|
3467
3475
|
{ cat: 'Reference', items: ['GD&T Training (14 symbols, 50 quiz questions)', 'MISUMI Component Catalog (80+ parts)', 'McMaster-Carr Integration', 'FCF Builder', 'Tolerance Calculator'] },
|
|
3468
3476
|
{ cat: 'Utilities', items: ['Keyboard Shortcuts (25+)', 'Dark Theme', 'Performance Monitor', 'Grid Floor', 'Wireframe Toggle'] }
|
|
3469
3477
|
];
|
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cycleCAD Generative Design / Topology Optimization Module
|
|
3
|
+
* SIMP-based topology optimization with voxelization and marching cubes
|
|
4
|
+
* Runs solver in Web Worker to avoid blocking UI
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
(function() {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// Material library with full properties
|
|
11
|
+
const MATERIALS = {
|
|
12
|
+
aluminum_6061: { name: 'Aluminum 6061-T6', density: 2700, yield: 276, ultimate: 310, E: 68.9, poisson: 0.33, color: 0xC0C0C0 },
|
|
13
|
+
aluminum_7075: { name: 'Aluminum 7075-T6', density: 2810, yield: 503, ultimate: 572, E: 71.7, poisson: 0.33, color: 0xA9A9A9 },
|
|
14
|
+
steel_1018: { name: 'Steel 1018', density: 7870, yield: 370, ultimate: 440, E: 205, poisson: 0.29, color: 0x696969 },
|
|
15
|
+
steel_4140: { name: 'Steel 4140', density: 7850, yield: 655, ultimate: 1020, E: 205, poisson: 0.29, color: 0x555555 },
|
|
16
|
+
stainless_316l: { name: 'Stainless 316L', density: 8000, yield: 205, ultimate: 515, E: 193, poisson: 0.27, color: 0xE8E8E8 },
|
|
17
|
+
titanium_ti64: { name: 'Titanium Ti-6Al-4V', density: 4430, yield: 880, ultimate: 950, E: 113.8, poisson: 0.342, color: 0xB0B0B0 },
|
|
18
|
+
abs_plastic: { name: 'ABS Plastic', density: 1040, yield: 43, ultimate: 43, E: 2.3, poisson: 0.35, color: 0x333333 },
|
|
19
|
+
nylon_pa12: { name: 'Nylon PA12', density: 1010, yield: 48, ultimate: 48, E: 1.7, poisson: 0.4, color: 0x4C4C4C },
|
|
20
|
+
pla: { name: 'PLA', density: 1240, yield: 60, ultimate: 65, E: 3.5, poisson: 0.36, color: 0x2F2F2F },
|
|
21
|
+
petg: { name: 'PETG', density: 1270, yield: 50, ultimate: 53, E: 2.2, poisson: 0.38, color: 0x404040 },
|
|
22
|
+
carbon_fiber: { name: 'Carbon Fiber Composite', density: 1600, yield: 600, ultimate: 700, E: 70, poisson: 0.1, color: 0x1A1A1A },
|
|
23
|
+
inconel_718: { name: 'Inconel 718', density: 8190, yield: 1034, ultimate: 1241, E: 200, poisson: 0.29, color: 0x8B7355 },
|
|
24
|
+
copper_c110: { name: 'Copper C110', density: 8940, yield: 69, ultimate: 220, E: 117, poisson: 0.34, color: 0xB87333 },
|
|
25
|
+
brass_c360: { name: 'Brass C360', density: 8500, yield: 310, ultimate: 490, E: 97, poisson: 0.34, color: 0xCD7F32 },
|
|
26
|
+
magnesium_az31: { name: 'Magnesium AZ31', density: 1770, yield: 200, ultimate: 260, E: 45, poisson: 0.35, color: 0x9C9C9C }
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Manufacturing constraints
|
|
30
|
+
const MANUFACTURING = {
|
|
31
|
+
unrestricted: { name: 'Unrestricted', minThickness: 0.5, overhangAngle: 0, symmetry: false },
|
|
32
|
+
additive: { name: 'Additive (3D Print)', minThickness: 1.0, overhangAngle: 45, symmetry: false },
|
|
33
|
+
'3axis_milling': { name: '3-Axis Milling', minThickness: 2.0, overhangAngle: 0, symmetry: false },
|
|
34
|
+
'2axis_cutting': { name: '2-Axis Cutting', minThickness: 3.0, overhangAngle: 0, symmetry: true },
|
|
35
|
+
die_casting: { name: 'Die Casting', minThickness: 1.5, overhangAngle: 5, symmetry: true }
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Study storage
|
|
39
|
+
const studies = new Map();
|
|
40
|
+
let studyCounter = 0;
|
|
41
|
+
|
|
42
|
+
// Solver worker code (inline as Blob)
|
|
43
|
+
const workerCode = `
|
|
44
|
+
self.onmessage = function(e) {
|
|
45
|
+
const { voxelData, loads, constraints, objective, material, iterations, gridSize } = e.data;
|
|
46
|
+
const results = solveSIMP(voxelData, loads, constraints, objective, material, iterations, gridSize);
|
|
47
|
+
self.postMessage(results);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function solveSIMP(voxelData, loads, constraints, objective, material, maxIterations, gridSize) {
|
|
51
|
+
const { densities, voxels, bounds } = voxelData;
|
|
52
|
+
let rho = Float32Array.from(densities);
|
|
53
|
+
const p = 3; // penalization factor
|
|
54
|
+
const vf = 0.3; // target volume fraction
|
|
55
|
+
|
|
56
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
57
|
+
// Simplified stiffness: compliance proportional to 1 / (rho^p * E)
|
|
58
|
+
let totalCompliance = 0;
|
|
59
|
+
const sensitivities = new Float32Array(rho.length);
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < rho.length; i++) {
|
|
62
|
+
if (rho[i] < 0.001) {
|
|
63
|
+
sensitivities[i] = 0;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Compliance sensitivity (simplified)
|
|
67
|
+
sensitivities[i] = -p * Math.pow(rho[i], p - 1);
|
|
68
|
+
totalCompliance += Math.pow(rho[i], p) * (1 - sensitivities[i]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Apply optimality criteria
|
|
72
|
+
const dC = Math.max(...sensitivities) / 100; // lagrange multiplier step
|
|
73
|
+
for (let i = 0; i < rho.length; i++) {
|
|
74
|
+
if (sensitivities[i] < dC) {
|
|
75
|
+
rho[i] *= 0.9; // reduce density
|
|
76
|
+
} else if (sensitivities[i] > dC * 1.5) {
|
|
77
|
+
rho[i] = Math.min(1.0, rho[i] * 1.1); // increase density
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Enforce volume constraint
|
|
82
|
+
const currentVF = rho.reduce((a, b) => a + b, 0) / rho.length;
|
|
83
|
+
const scale = Math.sqrt(vf / (currentVF + 0.0001));
|
|
84
|
+
for (let i = 0; i < rho.length; i++) {
|
|
85
|
+
rho[i] = Math.min(1.0, Math.max(0.001, rho[i] * scale));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Density filter (simple smoothing)
|
|
89
|
+
const filtered = new Float32Array(rho.length);
|
|
90
|
+
const radius = 2; // filter radius in voxels
|
|
91
|
+
for (let i = 0; i < rho.length; i++) {
|
|
92
|
+
const [x, y, z] = voxels[i];
|
|
93
|
+
let sum = 0, count = 0;
|
|
94
|
+
for (let j = 0; j < rho.length; j++) {
|
|
95
|
+
const [vx, vy, vz] = voxels[j];
|
|
96
|
+
const dist = Math.hypot(x - vx, y - vy, z - vz);
|
|
97
|
+
if (dist <= radius) {
|
|
98
|
+
sum += rho[j];
|
|
99
|
+
count++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
filtered[i] = count > 0 ? sum / count : rho[i];
|
|
103
|
+
}
|
|
104
|
+
rho = filtered;
|
|
105
|
+
|
|
106
|
+
// Check convergence
|
|
107
|
+
const convergence = rho.filter((v, i) => Math.abs(v - densities[i]) > 0.01).length / rho.length;
|
|
108
|
+
|
|
109
|
+
// Report progress
|
|
110
|
+
self.postMessage({
|
|
111
|
+
progress: true,
|
|
112
|
+
iteration: iter + 1,
|
|
113
|
+
totalIterations: maxIterations,
|
|
114
|
+
convergence: convergence,
|
|
115
|
+
compliance: totalCompliance
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (convergence < 0.01) {
|
|
119
|
+
iter = maxIterations; // early exit
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Threshold and return
|
|
124
|
+
const result = new Float32Array(rho.length);
|
|
125
|
+
for (let i = 0; i < rho.length; i++) {
|
|
126
|
+
result[i] = rho[i] > 0.5 ? 1.0 : 0.0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
densities: Array.from(result),
|
|
131
|
+
iterations: maxIterations,
|
|
132
|
+
final: true
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
// Create study
|
|
138
|
+
function createStudy(options) {
|
|
139
|
+
const id = `study_${++studyCounter}`;
|
|
140
|
+
const study = {
|
|
141
|
+
id,
|
|
142
|
+
name: options.name || 'Untitled Study',
|
|
143
|
+
preserveGeometries: options.preserveGeometries || [],
|
|
144
|
+
obstacleGeometries: options.obstacleGeometries || [],
|
|
145
|
+
startingShape: options.startingShape || null,
|
|
146
|
+
material: options.material || 'aluminum_6061',
|
|
147
|
+
objective: options.objective || 'minimize_mass',
|
|
148
|
+
targetMass: options.targetMass || null,
|
|
149
|
+
safetyFactor: options.safetyFactor || 1.5,
|
|
150
|
+
manufacturingMethod: options.manufacturingMethod || 'unrestricted',
|
|
151
|
+
constraints: [],
|
|
152
|
+
loads: [],
|
|
153
|
+
solver: null,
|
|
154
|
+
densities: null,
|
|
155
|
+
history: [],
|
|
156
|
+
visualizationMode: 'density',
|
|
157
|
+
createdAt: Date.now()
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
studies.set(id, study);
|
|
161
|
+
return id;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Add constraint
|
|
165
|
+
function addConstraint(studyId, preserveId, type) {
|
|
166
|
+
const study = studies.get(studyId);
|
|
167
|
+
if (!study) return null;
|
|
168
|
+
|
|
169
|
+
const constraint = {
|
|
170
|
+
id: `constraint_${study.constraints.length}`,
|
|
171
|
+
preserveId,
|
|
172
|
+
type, // 'fixed', 'pinned', 'frictionless'
|
|
173
|
+
createdAt: Date.now()
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
study.constraints.push(constraint);
|
|
177
|
+
return constraint;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Add load
|
|
181
|
+
function addLoad(studyId, preserveId, load) {
|
|
182
|
+
const study = studies.get(studyId);
|
|
183
|
+
if (!study) return null;
|
|
184
|
+
|
|
185
|
+
const loadEntry = {
|
|
186
|
+
id: `load_${study.loads.length}`,
|
|
187
|
+
preserveId,
|
|
188
|
+
...load,
|
|
189
|
+
createdAt: Date.now()
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
study.loads.push(loadEntry);
|
|
193
|
+
return loadEntry;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Validate study
|
|
197
|
+
function validateStudy(studyId) {
|
|
198
|
+
const study = studies.get(studyId);
|
|
199
|
+
if (!study) return { valid: false, errors: ['Study not found'] };
|
|
200
|
+
|
|
201
|
+
const errors = [];
|
|
202
|
+
if (study.constraints.length === 0) errors.push('At least one constraint required');
|
|
203
|
+
if (study.loads.length === 0) errors.push('At least one load required');
|
|
204
|
+
if (study.preserveGeometries.length === 0) errors.push('At least one preserve geometry required');
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
valid: errors.length === 0,
|
|
208
|
+
errors
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Voxelize design space
|
|
213
|
+
function voxelizeStudy(studyId, gridSize = 30) {
|
|
214
|
+
const study = studies.get(studyId);
|
|
215
|
+
if (!study) return null;
|
|
216
|
+
|
|
217
|
+
// Create bounding box from all geometries
|
|
218
|
+
let bounds = { min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity] };
|
|
219
|
+
|
|
220
|
+
study.preserveGeometries.forEach(geom => {
|
|
221
|
+
const p = geom.position || [0, 0, 0];
|
|
222
|
+
bounds.min[0] = Math.min(bounds.min[0], p[0] - 50);
|
|
223
|
+
bounds.min[1] = Math.min(bounds.min[1], p[1] - 50);
|
|
224
|
+
bounds.min[2] = Math.min(bounds.min[2], p[2] - 50);
|
|
225
|
+
bounds.max[0] = Math.max(bounds.max[0], p[0] + 50);
|
|
226
|
+
bounds.max[1] = Math.max(bounds.max[1], p[1] + 50);
|
|
227
|
+
bounds.max[2] = Math.max(bounds.max[2], p[2] + 50);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
study.obstacleGeometries.forEach(geom => {
|
|
231
|
+
const p = geom.position || [0, 0, 0];
|
|
232
|
+
bounds.min[0] = Math.min(bounds.min[0], p[0] - 50);
|
|
233
|
+
bounds.min[1] = Math.min(bounds.min[1], p[1] - 50);
|
|
234
|
+
bounds.min[2] = Math.min(bounds.min[2], p[2] - 50);
|
|
235
|
+
bounds.max[0] = Math.max(bounds.max[0], p[0] + 50);
|
|
236
|
+
bounds.max[1] = Math.max(bounds.max[1], p[1] + 50);
|
|
237
|
+
bounds.max[2] = Math.max(bounds.max[2], p[2] + 50);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const voxelSize = [
|
|
241
|
+
(bounds.max[0] - bounds.min[0]) / gridSize,
|
|
242
|
+
(bounds.max[1] - bounds.min[1]) / gridSize,
|
|
243
|
+
(bounds.max[2] - bounds.min[2]) / gridSize
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
// Create voxel grid
|
|
247
|
+
const voxels = [];
|
|
248
|
+
const densities = [];
|
|
249
|
+
|
|
250
|
+
for (let x = 0; x < gridSize; x++) {
|
|
251
|
+
for (let y = 0; y < gridSize; y++) {
|
|
252
|
+
for (let z = 0; z < gridSize; z++) {
|
|
253
|
+
const pos = [
|
|
254
|
+
bounds.min[0] + (x + 0.5) * voxelSize[0],
|
|
255
|
+
bounds.min[1] + (y + 0.5) * voxelSize[1],
|
|
256
|
+
bounds.min[2] + (z + 0.5) * voxelSize[2]
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
// Check if voxel is in obstacle
|
|
260
|
+
let inObstacle = false;
|
|
261
|
+
for (const obs of study.obstacleGeometries) {
|
|
262
|
+
const dist = Math.hypot(
|
|
263
|
+
pos[0] - (obs.position[0] || 0),
|
|
264
|
+
pos[1] - (obs.position[1] || 0),
|
|
265
|
+
pos[2] - (obs.position[2] || 0)
|
|
266
|
+
);
|
|
267
|
+
if (obs.type === 'sphere' && dist < (obs.radius || 10)) inObstacle = true;
|
|
268
|
+
if (obs.type === 'cylinder' && dist < (obs.radius || 10)) inObstacle = true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
voxels.push([x, y, z]);
|
|
272
|
+
densities.push(inObstacle ? 0.001 : 1.0);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { voxels, densities, bounds, voxelSize, gridSize };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Solve optimization
|
|
281
|
+
function solve(studyId, callback) {
|
|
282
|
+
const study = studies.get(studyId);
|
|
283
|
+
if (!study) return;
|
|
284
|
+
|
|
285
|
+
const validation = validateStudy(studyId);
|
|
286
|
+
if (!validation.valid) {
|
|
287
|
+
if (callback) callback({ error: validation.errors.join(', ') });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const voxelData = voxelizeStudy(studyId, 25);
|
|
292
|
+
if (!voxelData) return;
|
|
293
|
+
|
|
294
|
+
// Create worker
|
|
295
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
296
|
+
const workerUrl = URL.createObjectURL(blob);
|
|
297
|
+
const worker = new Worker(workerUrl);
|
|
298
|
+
|
|
299
|
+
worker.onmessage = (e) => {
|
|
300
|
+
const { progress, final, densities, error } = e.data;
|
|
301
|
+
|
|
302
|
+
if (progress && callback) {
|
|
303
|
+
callback({
|
|
304
|
+
iteration: e.data.iteration,
|
|
305
|
+
totalIterations: e.data.totalIterations,
|
|
306
|
+
convergence: e.data.convergence,
|
|
307
|
+
percentDone: Math.round((e.data.iteration / e.data.totalIterations) * 100)
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (final) {
|
|
312
|
+
study.densities = densities;
|
|
313
|
+
study.history.push({ densities, timestamp: Date.now() });
|
|
314
|
+
|
|
315
|
+
// Generate mesh from densities
|
|
316
|
+
const mesh = generateMeshFromVoxels(voxelData, densities);
|
|
317
|
+
study.resultMesh = mesh;
|
|
318
|
+
|
|
319
|
+
if (callback) {
|
|
320
|
+
callback({
|
|
321
|
+
complete: true,
|
|
322
|
+
mesh,
|
|
323
|
+
densities,
|
|
324
|
+
mass: calculateMass(study, mesh)
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
worker.terminate();
|
|
329
|
+
URL.revokeObjectURL(workerUrl);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
worker.onerror = (err) => {
|
|
334
|
+
if (callback) callback({ error: err.message });
|
|
335
|
+
worker.terminate();
|
|
336
|
+
URL.revokeObjectURL(workerUrl);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// Start solver
|
|
340
|
+
const material = MATERIALS[study.material];
|
|
341
|
+
worker.postMessage({
|
|
342
|
+
voxelData,
|
|
343
|
+
loads: study.loads,
|
|
344
|
+
constraints: study.constraints,
|
|
345
|
+
objective: study.objective,
|
|
346
|
+
material,
|
|
347
|
+
iterations: 100,
|
|
348
|
+
gridSize: 25
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Generate mesh from voxel densities using simplified marching cubes
|
|
353
|
+
function generateMeshFromVoxels(voxelData, densities) {
|
|
354
|
+
const { voxels, gridSize, bounds, voxelSize } = voxelData;
|
|
355
|
+
const geometry = new THREE.BufferGeometry();
|
|
356
|
+
const vertices = [];
|
|
357
|
+
const indices = [];
|
|
358
|
+
|
|
359
|
+
// Simplified marching cubes: create cube for each solid voxel
|
|
360
|
+
voxels.forEach((vox, idx) => {
|
|
361
|
+
if (densities[idx] > 0.5) {
|
|
362
|
+
const [x, y, z] = vox;
|
|
363
|
+
const px = bounds.min[0] + (x + 0.5) * voxelSize[0];
|
|
364
|
+
const py = bounds.min[1] + (y + 0.5) * voxelSize[1];
|
|
365
|
+
const pz = bounds.min[2] + (z + 0.5) * voxelSize[2];
|
|
366
|
+
const s = voxelSize[0] * 0.5;
|
|
367
|
+
|
|
368
|
+
// Add cube vertices
|
|
369
|
+
const baseIdx = vertices.length / 3;
|
|
370
|
+
const cubeVerts = [
|
|
371
|
+
[px - s, py - s, pz - s],
|
|
372
|
+
[px + s, py - s, pz - s],
|
|
373
|
+
[px + s, py + s, pz - s],
|
|
374
|
+
[px - s, py + s, pz - s],
|
|
375
|
+
[px - s, py - s, pz + s],
|
|
376
|
+
[px + s, py - s, pz + s],
|
|
377
|
+
[px + s, py + s, pz + s],
|
|
378
|
+
[px - s, py + s, pz + s]
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
cubeVerts.forEach(v => vertices.push(...v));
|
|
382
|
+
|
|
383
|
+
// Add cube faces
|
|
384
|
+
const faces = [
|
|
385
|
+
[0, 1, 2], [0, 2, 3], // front
|
|
386
|
+
[4, 6, 5], [4, 7, 6], // back
|
|
387
|
+
[0, 4, 5], [0, 5, 1], // bottom
|
|
388
|
+
[2, 6, 7], [2, 7, 3], // top
|
|
389
|
+
[0, 3, 7], [0, 7, 4], // left
|
|
390
|
+
[1, 5, 6], [1, 6, 2] // right
|
|
391
|
+
];
|
|
392
|
+
|
|
393
|
+
faces.forEach(face => {
|
|
394
|
+
indices.push(baseIdx + face[0], baseIdx + face[1], baseIdx + face[2]);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
|
|
400
|
+
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
|
|
401
|
+
geometry.computeVertexNormals();
|
|
402
|
+
|
|
403
|
+
const material = new THREE.MeshStandardMaterial({ color: 0x58a6ff, metalness: 0.3, roughness: 0.7 });
|
|
404
|
+
return new THREE.Mesh(geometry, material);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Calculate mass of result
|
|
408
|
+
function calculateMass(study, mesh) {
|
|
409
|
+
const material = MATERIALS[study.material];
|
|
410
|
+
if (!mesh || !mesh.geometry) return 0;
|
|
411
|
+
|
|
412
|
+
const bbox = new THREE.Box3().setFromObject(mesh);
|
|
413
|
+
const volume = bbox.getSize(new THREE.Vector3());
|
|
414
|
+
const totalVolume = volume.x * volume.y * volume.z;
|
|
415
|
+
|
|
416
|
+
return (totalVolume / 1000) * material.density; // grams
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Visualize result
|
|
420
|
+
function visualize(studyId, mode = 'density') {
|
|
421
|
+
const study = studies.get(studyId);
|
|
422
|
+
if (!study || !study.densities) return;
|
|
423
|
+
|
|
424
|
+
const voxelData = voxelizeStudy(studyId, 25);
|
|
425
|
+
const { scene } = window;
|
|
426
|
+
|
|
427
|
+
// Clear existing visualization
|
|
428
|
+
scene.children
|
|
429
|
+
.filter(c => c.userData?.genDesignViz)
|
|
430
|
+
.forEach(c => scene.remove(c));
|
|
431
|
+
|
|
432
|
+
if (mode === 'density') {
|
|
433
|
+
// Color voxels by density
|
|
434
|
+
voxelData.voxels.forEach((vox, idx) => {
|
|
435
|
+
if (study.densities[idx] < 0.001) return;
|
|
436
|
+
|
|
437
|
+
const [x, y, z] = vox;
|
|
438
|
+
const px = voxelData.bounds.min[0] + (x + 0.5) * voxelData.voxelSize[0];
|
|
439
|
+
const py = voxelData.bounds.min[1] + (y + 0.5) * voxelData.voxelSize[1];
|
|
440
|
+
const pz = voxelData.bounds.min[2] + (z + 0.5) * voxelData.voxelSize[2];
|
|
441
|
+
|
|
442
|
+
const geo = new THREE.BoxGeometry(voxelData.voxelSize[0], voxelData.voxelSize[1], voxelData.voxelSize[2]);
|
|
443
|
+
const hue = study.densities[idx]; // 0 = blue, 1 = red
|
|
444
|
+
const color = new THREE.Color().setHSL(0.7 - hue * 0.7, 0.8, 0.5);
|
|
445
|
+
const mat = new THREE.MeshStandardMaterial({ color, transparent: true, opacity: 0.7 });
|
|
446
|
+
const cube = new THREE.Mesh(geo, mat);
|
|
447
|
+
cube.position.set(px, py, pz);
|
|
448
|
+
cube.userData.genDesignViz = true;
|
|
449
|
+
scene.add(cube);
|
|
450
|
+
});
|
|
451
|
+
} else if (mode === 'solid' && study.resultMesh) {
|
|
452
|
+
study.resultMesh.userData.genDesignViz = true;
|
|
453
|
+
scene.add(study.resultMesh);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Export result
|
|
458
|
+
function exportResult(studyId, format = 'stl') {
|
|
459
|
+
const study = studies.get(studyId);
|
|
460
|
+
if (!study || !study.resultMesh) return null;
|
|
461
|
+
|
|
462
|
+
const mesh = study.resultMesh;
|
|
463
|
+
const material = MATERIALS[study.material];
|
|
464
|
+
|
|
465
|
+
if (format === 'stl') {
|
|
466
|
+
return meshToSTL(mesh);
|
|
467
|
+
} else if (format === 'json') {
|
|
468
|
+
return {
|
|
469
|
+
name: study.name,
|
|
470
|
+
material: material.name,
|
|
471
|
+
densities: study.densities,
|
|
472
|
+
mass: calculateMass(study, mesh),
|
|
473
|
+
objective: study.objective,
|
|
474
|
+
safetyFactor: study.safetyFactor,
|
|
475
|
+
createdAt: study.createdAt
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Simple STL export
|
|
483
|
+
function meshToSTL(mesh) {
|
|
484
|
+
const geo = mesh.geometry;
|
|
485
|
+
const positions = geo.attributes.position.array;
|
|
486
|
+
const indices = geo.index ? geo.index.array : null;
|
|
487
|
+
|
|
488
|
+
let facets = [];
|
|
489
|
+
if (indices) {
|
|
490
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
491
|
+
const a = indices[i] * 3, b = indices[i + 1] * 3, c = indices[i + 2] * 3;
|
|
492
|
+
facets.push({
|
|
493
|
+
v1: [positions[a], positions[a + 1], positions[a + 2]],
|
|
494
|
+
v2: [positions[b], positions[b + 1], positions[b + 2]],
|
|
495
|
+
v3: [positions[c], positions[c + 1], positions[c + 2]]
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return facets; // Can be further serialized to binary STL
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Get UI panel
|
|
504
|
+
function getUI() {
|
|
505
|
+
return `
|
|
506
|
+
<div id="gendesign-panel" class="gendesign-panel">
|
|
507
|
+
<style>
|
|
508
|
+
.gendesign-panel { background: #1e1e1e; color: #e0e0e0; border-radius: 8px; padding: 12px; font-family: Calibri, sans-serif; }
|
|
509
|
+
.gendesign-tabs { display: flex; gap: 0; border-bottom: 1px solid #444; margin-bottom: 12px; }
|
|
510
|
+
.gendesign-tab { padding: 8px 16px; cursor: pointer; background: #2d2d2d; border: none; color: #999; flex: 1; text-align: center; }
|
|
511
|
+
.gendesign-tab.active { background: #0284C7; color: #fff; }
|
|
512
|
+
.gendesign-content { display: none; }
|
|
513
|
+
.gendesign-content.active { display: block; }
|
|
514
|
+
.gendesign-group { margin-bottom: 12px; }
|
|
515
|
+
.gendesign-label { display: block; font-size: 12px; color: #aaa; margin-bottom: 4px; font-weight: bold; }
|
|
516
|
+
.gendesign-input, .gendesign-select { width: 100%; padding: 6px; background: #2d2d2d; border: 1px solid #444; color: #e0e0e0; border-radius: 4px; font-family: inherit; }
|
|
517
|
+
.gendesign-button { width: 100%; padding: 8px; background: #0284C7; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; margin-top: 4px; }
|
|
518
|
+
.gendesign-button:hover { background: #0369a1; }
|
|
519
|
+
.gendesign-progress { width: 100%; height: 24px; background: #2d2d2d; border: 1px solid #444; border-radius: 4px; overflow: hidden; margin: 8px 0; }
|
|
520
|
+
.gendesign-progress-bar { height: 100%; background: linear-gradient(90deg, #0284C7, #58a6ff); transition: width 0.3s; display: flex; align-items: center; justify-content: center; font-size: 11px; color: white; }
|
|
521
|
+
.gendesign-list { background: #2d2d2d; border: 1px solid #444; border-radius: 4px; padding: 8px; max-height: 150px; overflow-y: auto; }
|
|
522
|
+
.gendesign-item { padding: 6px; border-bottom: 1px solid #444; font-size: 12px; display: flex; justify-content: space-between; }
|
|
523
|
+
.gendesign-badge { display: inline-block; padding: 2px 8px; background: #0284C7; color: white; border-radius: 3px; font-size: 10px; }
|
|
524
|
+
.gendesign-error { color: #f85149; font-size: 11px; }
|
|
525
|
+
.gendesign-success { color: #3fb950; font-size: 11px; }
|
|
526
|
+
</style>
|
|
527
|
+
|
|
528
|
+
<div class="gendesign-tabs">
|
|
529
|
+
<button class="gendesign-tab active" data-tab="setup">Setup</button>
|
|
530
|
+
<button class="gendesign-tab" data-tab="loads">Loads</button>
|
|
531
|
+
<button class="gendesign-tab" data-tab="solve">Solve</button>
|
|
532
|
+
<button class="gendesign-tab" data-tab="results">Results</button>
|
|
533
|
+
</div>
|
|
534
|
+
|
|
535
|
+
<div id="gendesign-setup" class="gendesign-content active">
|
|
536
|
+
<div class="gendesign-group">
|
|
537
|
+
<label class="gendesign-label">Study Name</label>
|
|
538
|
+
<input type="text" id="gendesign-name" class="gendesign-input" placeholder="My Study" />
|
|
539
|
+
</div>
|
|
540
|
+
<div class="gendesign-group">
|
|
541
|
+
<label class="gendesign-label">Material</label>
|
|
542
|
+
<select id="gendesign-material" class="gendesign-select">
|
|
543
|
+
${Object.entries(MATERIALS).map(([k, v]) => `<option value="${k}">${v.name} (E=${v.E}GPa, ρ=${v.density}kg/m³)</option>`).join('')}
|
|
544
|
+
</select>
|
|
545
|
+
</div>
|
|
546
|
+
<div class="gendesign-group">
|
|
547
|
+
<label class="gendesign-label">Objective</label>
|
|
548
|
+
<select id="gendesign-objective" class="gendesign-select">
|
|
549
|
+
<option value="minimize_mass">Minimize Mass</option>
|
|
550
|
+
<option value="maximize_stiffness">Maximize Stiffness</option>
|
|
551
|
+
</select>
|
|
552
|
+
</div>
|
|
553
|
+
<div class="gendesign-group">
|
|
554
|
+
<label class="gendesign-label">Manufacturing Method</label>
|
|
555
|
+
<select id="gendesign-mfg" class="gendesign-select">
|
|
556
|
+
${Object.entries(MANUFACTURING).map(([k, v]) => `<option value="${k}">${v.name}</option>`).join('')}
|
|
557
|
+
</select>
|
|
558
|
+
</div>
|
|
559
|
+
<div class="gendesign-group">
|
|
560
|
+
<label class="gendesign-label">Preserve Geometries</label>
|
|
561
|
+
<div class="gendesign-list" id="gendesign-preserves"></div>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
|
|
565
|
+
<div id="gendesign-loads" class="gendesign-content">
|
|
566
|
+
<div class="gendesign-group">
|
|
567
|
+
<label class="gendesign-label">Constraints</label>
|
|
568
|
+
<div class="gendesign-list" id="gendesign-constraints"></div>
|
|
569
|
+
<button class="gendesign-button">+ Add Constraint</button>
|
|
570
|
+
</div>
|
|
571
|
+
<div class="gendesign-group">
|
|
572
|
+
<label class="gendesign-label">Loads</label>
|
|
573
|
+
<div class="gendesign-list" id="gendesign-loads"></div>
|
|
574
|
+
<button class="gendesign-button">+ Add Load</button>
|
|
575
|
+
</div>
|
|
576
|
+
<div id="gendesign-validation" style="font-size: 12px; margin-top: 8px;"></div>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
<div id="gendesign-solve" class="gendesign-content">
|
|
580
|
+
<button id="gendesign-generate" class="gendesign-button" style="background: #3fb950; font-size: 16px; padding: 12px;">Generate</button>
|
|
581
|
+
<div class="gendesign-progress" style="display: none;" id="gendesign-progress-container">
|
|
582
|
+
<div class="gendesign-progress-bar" id="gendesign-progress-bar" style="width: 0%">0%</div>
|
|
583
|
+
</div>
|
|
584
|
+
<div id="gendesign-status" style="font-size: 12px; text-align: center; margin-top: 8px;"></div>
|
|
585
|
+
<canvas id="gendesign-convergence" width="300" height="100" style="width: 100%; margin-top: 8px; background: #2d2d2d; border-radius: 4px;"></canvas>
|
|
586
|
+
</div>
|
|
587
|
+
|
|
588
|
+
<div id="gendesign-results" class="gendesign-content">
|
|
589
|
+
<div class="gendesign-group">
|
|
590
|
+
<label class="gendesign-label">Result Visualization</label>
|
|
591
|
+
<select id="gendesign-viz-mode" class="gendesign-select">
|
|
592
|
+
<option value="density">Density Heatmap</option>
|
|
593
|
+
<option value="stress">Von Mises Stress</option>
|
|
594
|
+
<option value="solid">Final Solid</option>
|
|
595
|
+
</select>
|
|
596
|
+
<button class="gendesign-button">Visualize</button>
|
|
597
|
+
</div>
|
|
598
|
+
<div class="gendesign-group">
|
|
599
|
+
<label class="gendesign-label">Export</label>
|
|
600
|
+
<button class="gendesign-button">Export STL</button>
|
|
601
|
+
<button class="gendesign-button">Export Report</button>
|
|
602
|
+
</div>
|
|
603
|
+
<div id="gendesign-summary" style="background: #2d2d2d; padding: 8px; border-radius: 4px; font-size: 12px; margin-top: 8px;"></div>
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
`;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Register API
|
|
610
|
+
window.cycleCAD = window.cycleCAD || {};
|
|
611
|
+
window.cycleCAD.generativeDesign = {
|
|
612
|
+
createStudy,
|
|
613
|
+
addConstraint,
|
|
614
|
+
addLoad,
|
|
615
|
+
validateStudy,
|
|
616
|
+
solve,
|
|
617
|
+
visualize,
|
|
618
|
+
exportResult,
|
|
619
|
+
getUI,
|
|
620
|
+
MATERIALS,
|
|
621
|
+
MANUFACTURING
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
console.log('[GenerativeDesign] Module loaded. Use cycleCAD.generativeDesign.*');
|
|
625
|
+
})();
|
package/package.json
CHANGED