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.
@@ -1894,10 +1894,5 @@ function addFeature(id, type, mesh, params) {
1894
1894
  export {
1895
1895
  on,
1896
1896
  off,
1897
- emit,
1898
- undo,
1899
- redo,
1900
- canUndo,
1901
- canRedo,
1902
- getModules
1897
+ emit
1903
1898
  };
@@ -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
+ })();