cyclecad 0.4.0 → 0.8.5
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/LICENSE +17 -27
- package/app/index.html +963 -427
- package/app/js/agent-api.js +1 -6
- package/app/js/generative-design.js +625 -0
- package/app/js/operations.js +117 -55
- package/app/js/sketch.js +44 -14
- package/app/js/tree.js +6 -1
- package/app/js/viewport.js +8 -0
- package/package.json +2 -2
package/app/js/agent-api.js
CHANGED
|
@@ -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
|
+
})();
|