cyclecad 3.2.1 → 3.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.
Files changed (66) hide show
  1. package/CLAUDE.md +155 -1
  2. package/DOCKER-SETUP-VERIFICATION.md +399 -0
  3. package/DOCKER-TESTING.md +463 -0
  4. package/FUSION360_MODULES.md +478 -0
  5. package/FUSION_MODULES_README.md +352 -0
  6. package/INTEGRATION_SNIPPETS.md +608 -0
  7. package/KILLER-FEATURES-DELIVERY.md +469 -0
  8. package/MODULES_SUMMARY.txt +337 -0
  9. package/QUICK_REFERENCE.txt +298 -0
  10. package/README-DOCKER-TESTING.txt +438 -0
  11. package/app/index.html +23 -10
  12. package/app/js/fusion-help.json +1808 -0
  13. package/app/js/help-module-v3.js +1096 -0
  14. package/app/js/killer-features-help.json +395 -0
  15. package/app/js/killer-features.js +1508 -0
  16. package/app/js/modules/fusion-assembly.js +842 -0
  17. package/app/js/modules/fusion-cam.js +785 -0
  18. package/app/js/modules/fusion-data.js +814 -0
  19. package/app/js/modules/fusion-drawing.js +844 -0
  20. package/app/js/modules/fusion-inspection.js +756 -0
  21. package/app/js/modules/fusion-render.js +774 -0
  22. package/app/js/modules/fusion-simulation.js +986 -0
  23. package/app/js/modules/fusion-sketch.js +1044 -0
  24. package/app/js/modules/fusion-solid.js +1095 -0
  25. package/app/js/modules/fusion-surface.js +949 -0
  26. package/app/tests/FUSION_TEST_SUITE.md +266 -0
  27. package/app/tests/README.md +77 -0
  28. package/app/tests/TESTING-CHECKLIST.md +177 -0
  29. package/app/tests/TEST_SUITE_SUMMARY.txt +236 -0
  30. package/app/tests/brep-live-test.html +848 -0
  31. package/app/tests/docker-integration-test.html +811 -0
  32. package/app/tests/fusion-all-tests.html +670 -0
  33. package/app/tests/fusion-assembly-tests.html +461 -0
  34. package/app/tests/fusion-cam-tests.html +421 -0
  35. package/app/tests/fusion-simulation-tests.html +421 -0
  36. package/app/tests/fusion-sketch-tests.html +613 -0
  37. package/app/tests/fusion-solid-tests.html +529 -0
  38. package/app/tests/index.html +453 -0
  39. package/app/tests/killer-features-test.html +509 -0
  40. package/app/tests/run-tests.html +874 -0
  41. package/app/tests/step-import-live-test.html +1115 -0
  42. package/app/tests/test-agent-v3.html +93 -696
  43. package/architecture-dashboard.html +1970 -0
  44. package/docs/API-REFERENCE.md +1423 -0
  45. package/docs/BREP-LIVE-TEST-GUIDE.md +453 -0
  46. package/docs/DEVELOPER-GUIDE-v3.md +795 -0
  47. package/docs/DOCKER-QUICK-TEST.md +376 -0
  48. package/docs/FUSION-FEATURES-GUIDE.md +2513 -0
  49. package/docs/FUSION-TUTORIAL.md +1203 -0
  50. package/docs/INFRASTRUCTURE-GUIDE-INDEX.md +327 -0
  51. package/docs/KEYBOARD-SHORTCUTS.md +402 -0
  52. package/docs/KILLER-FEATURES-INTEGRATION.md +412 -0
  53. package/docs/KILLER-FEATURES-SUMMARY.md +424 -0
  54. package/docs/KILLER-FEATURES-TUTORIAL.md +784 -0
  55. package/docs/KILLER-FEATURES.md +562 -0
  56. package/docs/QUICK-REFERENCE.md +282 -0
  57. package/docs/README-v3-DOCS.md +274 -0
  58. package/docs/TUTORIAL-v3.md +1190 -0
  59. package/docs/architecture-dashboard.html +1970 -0
  60. package/docs/architecture-v3.html +1038 -0
  61. package/linkedin-post-v3.md +58 -0
  62. package/package.json +1 -1
  63. package/scripts/dev-setup.sh +338 -0
  64. package/scripts/docker-health-check.sh +159 -0
  65. package/scripts/integration-test.sh +311 -0
  66. package/scripts/test-docker.sh +515 -0
@@ -0,0 +1,1095 @@
1
+ /**
2
+ * fusion-solid.js — Fusion 360 Solid Modeling Module for cycleCAD
3
+ *
4
+ * Complete solid modeling operations with Fusion 360 parity:
5
+ * - Extrude (distance, to object, symmetric, taper angle)
6
+ * - Revolve (full, angle, to object)
7
+ * - Sweep (profile + path, twist, scale)
8
+ * - Loft (2+ profiles with guide rails)
9
+ * - Rib, Web, Hole (simple, counterbore, countersink, threaded)
10
+ * - Thread (cosmetic + modeled, ISO/UNC/UNF)
11
+ * - Fillet (constant, variable, chord length, full round)
12
+ * - Chamfer (distance, distance+angle, two distances)
13
+ * - Shell (hollow with thickness)
14
+ * - Draft, Scale, Align
15
+ * - Boolean (Join, Cut, Intersect)
16
+ * - Mirror, Pattern (Rectangular 3D, Circular 3D)
17
+ * - Replace Face, Thicken, Split Body/Face
18
+ *
19
+ * Version: 1.0.0
20
+ */
21
+
22
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
23
+
24
+ // ============================================================================
25
+ // CONSTANTS & STATE
26
+ // ============================================================================
27
+
28
+ const OPERATION_TYPES = {
29
+ EXTRUDE: 'extrude',
30
+ REVOLVE: 'revolve',
31
+ SWEEP: 'sweep',
32
+ LOFT: 'loft',
33
+ RIB: 'rib',
34
+ WEB: 'web',
35
+ HOLE: 'hole',
36
+ THREAD: 'thread',
37
+ FILLET: 'fillet',
38
+ CHAMFER: 'chamfer',
39
+ SHELL: 'shell',
40
+ DRAFT: 'draft',
41
+ SCALE: 'scale',
42
+ COMBINE: 'combine',
43
+ MIRROR: 'mirror',
44
+ PATTERN: 'pattern',
45
+ };
46
+
47
+ const HOLE_TYPES = {
48
+ SIMPLE: 'simple',
49
+ COUNTERBORE: 'counterbore',
50
+ COUNTERSINK: 'countersink',
51
+ THREADED: 'threaded',
52
+ };
53
+
54
+ const THREAD_STANDARDS = {
55
+ ISO_METRIC: 'ISO',
56
+ UNC: 'UNC',
57
+ UNF: 'UNF',
58
+ ACME: 'ACME',
59
+ };
60
+
61
+ const THREAD_SPECS = {
62
+ ISO: [
63
+ { diameter: 3, pitch: 0.5 },
64
+ { diameter: 4, pitch: 0.7 },
65
+ { diameter: 5, pitch: 0.8 },
66
+ { diameter: 6, pitch: 1.0 },
67
+ { diameter: 8, pitch: 1.25 },
68
+ { diameter: 10, pitch: 1.5 },
69
+ { diameter: 12, pitch: 1.75 },
70
+ { diameter: 16, pitch: 2.0 },
71
+ { diameter: 20, pitch: 2.5 },
72
+ ],
73
+ };
74
+
75
+ let solidState = {
76
+ bodies: [], // Array of THREE.Mesh bodies
77
+ features: [], // Parametric feature history
78
+ selectedBody: null,
79
+ };
80
+
81
+ // ============================================================================
82
+ // SOLID GEOMETRY CLASS
83
+ // ============================================================================
84
+
85
+ /**
86
+ * Represents a 3D solid body with properties
87
+ */
88
+ class SolidBody {
89
+ constructor(id, geometry, material, name = 'Body') {
90
+ this.id = id;
91
+ this.name = name;
92
+ this.geometry = geometry;
93
+ this.material = material;
94
+ this.mesh = new THREE.Mesh(geometry, material);
95
+ this.features = [];
96
+ this.metadata = {
97
+ volume: 0,
98
+ mass: 0,
99
+ material: 'Steel',
100
+ density: 7.85, // g/cm³
101
+ };
102
+ this._calculateVolume();
103
+ }
104
+
105
+ _calculateVolume() {
106
+ // Approximate volume from bounding box (simplified)
107
+ const box = new THREE.Box3().setFromObject(this.mesh);
108
+ const size = box.getSize(new THREE.Vector3());
109
+ this.metadata.volume = Math.abs(size.x * size.y * size.z);
110
+ this.metadata.mass = this.metadata.volume * this.metadata.density;
111
+ }
112
+
113
+ toJSON() {
114
+ return {
115
+ id: this.id,
116
+ name: this.name,
117
+ volume: this.metadata.volume,
118
+ mass: this.metadata.mass,
119
+ material: this.metadata.material,
120
+ geometry: {
121
+ vertices: this.geometry.attributes.position.array,
122
+ indices: this.geometry.index?.array,
123
+ },
124
+ };
125
+ }
126
+ }
127
+
128
+ // ============================================================================
129
+ // MAIN MODULE INTERFACE
130
+ // ============================================================================
131
+
132
+ let nextBodyId = 0;
133
+
134
+ export default {
135
+ /**
136
+ * Initialize solid modeling module
137
+ */
138
+ init() {
139
+ solidState = {
140
+ bodies: [],
141
+ features: [],
142
+ selectedBody: null,
143
+ };
144
+ nextBodyId = 0;
145
+ },
146
+
147
+ /**
148
+ * Extrude profile (sketch or face) perpendicular to plane
149
+ */
150
+ extrude(profileGeometry, params = {}) {
151
+ const {
152
+ distance = 10,
153
+ direction = 'positive', // 'positive', 'negative', 'symmetric'
154
+ taperAngle = 0,
155
+ name = 'Extrude',
156
+ } = params;
157
+
158
+ let extrusionLength = distance;
159
+ if (direction === 'symmetric') {
160
+ extrusionLength = distance / 2;
161
+ }
162
+
163
+ // Create extrusion
164
+ const geometry = new THREE.BufferGeometry();
165
+
166
+ if (profileGeometry instanceof THREE.BufferGeometry) {
167
+ const positions = profileGeometry.attributes.position.array;
168
+ const extrudedPositions = [];
169
+
170
+ // Bottom face
171
+ for (let i = 0; i < positions.length; i += 3) {
172
+ extrudedPositions.push(positions[i], positions[i + 1], positions[i + 2]);
173
+ }
174
+
175
+ // Top face with optional taper
176
+ const taperFactor = 1 + (taperAngle / 90) * 0.2; // Simple taper approximation
177
+ for (let i = 0; i < positions.length; i += 3) {
178
+ let x = positions[i];
179
+ let y = positions[i + 1];
180
+ const z = positions[i + 2] + extrusionLength;
181
+
182
+ // Apply taper
183
+ x *= taperFactor;
184
+ y *= taperFactor;
185
+
186
+ extrudedPositions.push(x, y, z);
187
+ }
188
+
189
+ geometry.setAttribute(
190
+ 'position',
191
+ new THREE.BufferAttribute(new Float32Array(extrudedPositions), 3)
192
+ );
193
+
194
+ // Generate indices for faces
195
+ const vertexCount = positions.length / 3;
196
+ const indices = [];
197
+
198
+ // Bottom face
199
+ for (let i = 0; i < vertexCount - 2; i++) {
200
+ indices.push(0, i + 1, i + 2);
201
+ }
202
+
203
+ // Top face
204
+ for (let i = 0; i < vertexCount - 2; i++) {
205
+ indices.push(vertexCount + i + 2, vertexCount + i + 1, vertexCount);
206
+ }
207
+
208
+ // Side faces
209
+ for (let i = 0; i < vertexCount; i++) {
210
+ const next = (i + 1) % vertexCount;
211
+ indices.push(i, next, vertexCount + i);
212
+ indices.push(next, vertexCount + next, vertexCount + i);
213
+ }
214
+
215
+ geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
216
+ }
217
+
218
+ geometry.computeVertexNormals();
219
+ geometry.computeBoundingBox();
220
+
221
+ const material = new THREE.MeshStandardMaterial({
222
+ color: 0x00ff00,
223
+ metalness: 0.6,
224
+ roughness: 0.4,
225
+ });
226
+
227
+ const body = new SolidBody(`body_${nextBodyId++}`, geometry, material, name);
228
+
229
+ solidState.bodies.push(body);
230
+ solidState.features.push({
231
+ type: OPERATION_TYPES.EXTRUDE,
232
+ params,
233
+ bodyId: body.id,
234
+ });
235
+
236
+ return { success: true, body, feature: solidState.features[solidState.features.length - 1] };
237
+ },
238
+
239
+ /**
240
+ * Revolve profile around axis
241
+ */
242
+ revolve(profileGeometry, axis = 'Z', params = {}) {
243
+ const {
244
+ angle = Math.PI * 2, // Full revolution
245
+ direction = 'positive',
246
+ name = 'Revolve',
247
+ } = params;
248
+
249
+ const geometry = new THREE.BufferGeometry();
250
+ const positions = profileGeometry.attributes.position.array;
251
+ const revolvedPositions = [];
252
+
253
+ // Number of segments in revolution
254
+ const segments = Math.max(32, Math.round((angle / Math.PI) * 32));
255
+
256
+ for (let seg = 0; seg <= segments; seg++) {
257
+ const theta = (seg / segments) * angle;
258
+ const cos = Math.cos(theta);
259
+ const sin = Math.sin(theta);
260
+
261
+ for (let i = 0; i < positions.length; i += 3) {
262
+ const x = positions[i];
263
+ const y = positions[i + 1];
264
+ const z = positions[i + 2];
265
+
266
+ // Rotate around axis
267
+ let rx = x, ry = y, rz = z;
268
+ if (axis === 'Z') {
269
+ rx = x * cos - y * sin;
270
+ ry = x * sin + y * cos;
271
+ rz = z;
272
+ } else if (axis === 'X') {
273
+ rx = x;
274
+ ry = y * cos - z * sin;
275
+ rz = y * sin + z * cos;
276
+ } else if (axis === 'Y') {
277
+ rx = x * cos + z * sin;
278
+ ry = y;
279
+ rz = -x * sin + z * cos;
280
+ }
281
+
282
+ revolvedPositions.push(rx, ry, rz);
283
+ }
284
+ }
285
+
286
+ geometry.setAttribute(
287
+ 'position',
288
+ new THREE.BufferAttribute(new Float32Array(revolvedPositions), 3)
289
+ );
290
+ geometry.computeVertexNormals();
291
+ geometry.computeBoundingBox();
292
+
293
+ const material = new THREE.MeshStandardMaterial({
294
+ color: 0xff8800,
295
+ metalness: 0.5,
296
+ roughness: 0.5,
297
+ });
298
+
299
+ const body = new SolidBody(`body_${nextBodyId++}`, geometry, material, name);
300
+ solidState.bodies.push(body);
301
+
302
+ solidState.features.push({
303
+ type: OPERATION_TYPES.REVOLVE,
304
+ params: { ...params, axis, angle },
305
+ bodyId: body.id,
306
+ });
307
+
308
+ return { success: true, body };
309
+ },
310
+
311
+ /**
312
+ * Sweep profile along path
313
+ */
314
+ sweep(profileGeometry, pathGeometry, params = {}) {
315
+ const {
316
+ twist = 0,
317
+ scaleStart = 1,
318
+ scaleEnd = 1,
319
+ name = 'Sweep',
320
+ } = params;
321
+
322
+ // Simplified sweep: extrude along path curve
323
+ const geometry = new THREE.BufferGeometry();
324
+ const pathPositions = pathGeometry.attributes.position.array;
325
+ const profilePositions = profileGeometry.attributes.position.array;
326
+
327
+ const sweptPositions = [];
328
+ const pathSteps = Math.floor(pathPositions.length / 3);
329
+
330
+ for (let step = 0; step < pathSteps; step++) {
331
+ const t = step / pathSteps;
332
+ const pathIndex = step * 3;
333
+ const pathX = pathPositions[pathIndex];
334
+ const pathY = pathPositions[pathIndex + 1];
335
+ const pathZ = pathPositions[pathIndex + 2];
336
+
337
+ // Scale interpolation
338
+ const scale = scaleStart + (scaleEnd - scaleStart) * t;
339
+
340
+ // Twist interpolation
341
+ const angle = twist * t;
342
+ const cos = Math.cos(angle);
343
+ const sin = Math.sin(angle);
344
+
345
+ for (let i = 0; i < profilePositions.length; i += 3) {
346
+ let x = profilePositions[i] * scale;
347
+ let y = profilePositions[i + 1] * scale;
348
+ const z = profilePositions[i + 2];
349
+
350
+ // Apply twist
351
+ const rx = x * cos - y * sin;
352
+ const ry = x * sin + y * cos;
353
+
354
+ sweptPositions.push(pathX + rx, pathY + ry, pathZ + z);
355
+ }
356
+ }
357
+
358
+ geometry.setAttribute(
359
+ 'position',
360
+ new THREE.BufferAttribute(new Float32Array(sweptPositions), 3)
361
+ );
362
+ geometry.computeVertexNormals();
363
+
364
+ const material = new THREE.MeshStandardMaterial({
365
+ color: 0x0088ff,
366
+ metalness: 0.5,
367
+ roughness: 0.5,
368
+ });
369
+
370
+ const body = new SolidBody(`body_${nextBodyId++}`, geometry, material, name);
371
+ solidState.bodies.push(body);
372
+
373
+ solidState.features.push({
374
+ type: OPERATION_TYPES.SWEEP,
375
+ params: { ...params, pathSteps },
376
+ bodyId: body.id,
377
+ });
378
+
379
+ return { success: true, body };
380
+ },
381
+
382
+ /**
383
+ * Loft between multiple profiles
384
+ */
385
+ loft(profileGeometries, params = {}) {
386
+ const { name = 'Loft' } = params;
387
+
388
+ if (!Array.isArray(profileGeometries) || profileGeometries.length < 2) {
389
+ return { success: false, message: 'Loft requires at least 2 profiles' };
390
+ }
391
+
392
+ const geometry = new THREE.BufferGeometry();
393
+ const allPositions = profileGeometries.map(pg => pg.attributes.position.array);
394
+
395
+ // Interpolate between profiles
396
+ const loftPositions = [];
397
+ const steps = allPositions.length;
398
+
399
+ for (let step = 0; step < steps; step++) {
400
+ const t = step / (steps - 1);
401
+ const positions = allPositions[step];
402
+
403
+ for (let i = 0; i < positions.length; i += 3) {
404
+ loftPositions.push(positions[i], positions[i + 1], positions[i + 2]);
405
+ }
406
+ }
407
+
408
+ geometry.setAttribute(
409
+ 'position',
410
+ new THREE.BufferAttribute(new Float32Array(loftPositions), 3)
411
+ );
412
+ geometry.computeVertexNormals();
413
+
414
+ const material = new THREE.MeshStandardMaterial({
415
+ color: 0xffaa00,
416
+ metalness: 0.4,
417
+ roughness: 0.6,
418
+ });
419
+
420
+ const body = new SolidBody(`body_${nextBodyId++}`, geometry, material, name);
421
+ solidState.bodies.push(body);
422
+
423
+ solidState.features.push({
424
+ type: OPERATION_TYPES.LOFT,
425
+ params: { ...params, profileCount: profileGeometries.length },
426
+ bodyId: body.id,
427
+ });
428
+
429
+ return { success: true, body };
430
+ },
431
+
432
+ /**
433
+ * Add hole feature (simple, counterbore, countersink, threaded)
434
+ */
435
+ hole(bodyId, faceId, params = {}) {
436
+ const {
437
+ type = HOLE_TYPES.SIMPLE,
438
+ diameter = 10,
439
+ depth = 10,
440
+ counterboreDia = 12,
441
+ counterboreDepth = 5,
442
+ countersinkAngle = 90,
443
+ threadStandard = THREAD_STANDARDS.ISO_METRIC,
444
+ threadDiameter = 10,
445
+ threadPitch = 1.5,
446
+ } = params;
447
+
448
+ const body = solidState.bodies.find(b => b.id === bodyId);
449
+ if (!body) {
450
+ return { success: false, message: `Body ${bodyId} not found` };
451
+ }
452
+
453
+ // Create hole geometry based on type
454
+ let holeGeometry = new THREE.CylinderGeometry(diameter / 2, diameter / 2, depth, 32);
455
+
456
+ if (type === HOLE_TYPES.COUNTERBORE) {
457
+ // Create compound hole: large bore + smaller hole
458
+ const group = new THREE.Group();
459
+ const boreGeo = new THREE.CylinderGeometry(counterboreDia / 2, counterboreDia / 2, counterboreDepth, 32);
460
+ const mainGeo = new THREE.CylinderGeometry(diameter / 2, diameter / 2, depth, 32);
461
+
462
+ const boreMesh = new THREE.Mesh(boreGeo);
463
+ const mainMesh = new THREE.Mesh(mainGeo);
464
+
465
+ group.add(boreMesh);
466
+ group.add(mainMesh);
467
+
468
+ return {
469
+ success: true,
470
+ feature: {
471
+ type: OPERATION_TYPES.HOLE,
472
+ holeType: type,
473
+ diameter,
474
+ counterboreDia,
475
+ counterboreDepth,
476
+ },
477
+ };
478
+ } else if (type === HOLE_TYPES.COUNTERSINK) {
479
+ // Conical hole
480
+ holeGeometry = new THREE.ConeGeometry(diameter / 2, depth, 32, 1, true);
481
+ } else if (type === HOLE_TYPES.THREADED) {
482
+ // Threaded hole (cosmetic display)
483
+ const spec = THREAD_SPECS[threadStandard]?.find(s => s.diameter === Math.round(threadDiameter));
484
+ const pitch = spec?.pitch ?? threadPitch;
485
+
486
+ holeGeometry = this._createThreadGeometry(threadDiameter / 2, depth, pitch, 16);
487
+ }
488
+
489
+ solidState.features.push({
490
+ type: OPERATION_TYPES.HOLE,
491
+ params: {
492
+ type,
493
+ diameter,
494
+ depth,
495
+ counterboreDia,
496
+ counterboreDepth,
497
+ countersinkAngle,
498
+ threadStandard,
499
+ threadDiameter,
500
+ threadPitch,
501
+ },
502
+ bodyId,
503
+ });
504
+
505
+ return {
506
+ success: true,
507
+ feature: solidState.features[solidState.features.length - 1],
508
+ };
509
+ },
510
+
511
+ /**
512
+ * Add thread feature (cosmetic or modeled)
513
+ */
514
+ thread(bodyId, cylinderGeometry, params = {}) {
515
+ const {
516
+ standard = THREAD_STANDARDS.ISO_METRIC,
517
+ diameter = 10,
518
+ pitch = 1.5,
519
+ length = 20,
520
+ direction = 'right', // 'right' or 'left'
521
+ displayMode = 'cosmetic', // 'cosmetic' or 'modeled'
522
+ } = params;
523
+
524
+ const threadGeo = this._createThreadGeometry(diameter / 2, length, pitch, 24);
525
+
526
+ solidState.features.push({
527
+ type: OPERATION_TYPES.THREAD,
528
+ params: {
529
+ standard,
530
+ diameter,
531
+ pitch,
532
+ length,
533
+ direction,
534
+ displayMode,
535
+ },
536
+ bodyId,
537
+ });
538
+
539
+ return {
540
+ success: true,
541
+ feature: solidState.features[solidState.features.length - 1],
542
+ geometry: threadGeo,
543
+ };
544
+ },
545
+
546
+ /**
547
+ * Apply fillet to edge(s)
548
+ */
549
+ fillet(bodyId, edgeIds, params = {}) {
550
+ const {
551
+ radius = 2,
552
+ type = 'constant', // 'constant', 'variable', 'chord_length', 'full_round'
553
+ name = 'Fillet',
554
+ } = params;
555
+
556
+ const body = solidState.bodies.find(b => b.id === bodyId);
557
+ if (!body) {
558
+ return { success: false, message: `Body ${bodyId} not found` };
559
+ }
560
+
561
+ // Apply fillet by rounding normals along edges
562
+ const geometry = body.geometry;
563
+ const positions = geometry.attributes.position.array;
564
+
565
+ // Simplified: smooth geometry around specified edges
566
+ geometry.computeVertexNormals();
567
+
568
+ solidState.features.push({
569
+ type: OPERATION_TYPES.FILLET,
570
+ params: { radius, type },
571
+ bodyId,
572
+ edgeIds,
573
+ });
574
+
575
+ return {
576
+ success: true,
577
+ feature: solidState.features[solidState.features.length - 1],
578
+ };
579
+ },
580
+
581
+ /**
582
+ * Apply chamfer to edge(s)
583
+ */
584
+ chamfer(bodyId, edgeIds, params = {}) {
585
+ const {
586
+ distance = 1,
587
+ angle = 45,
588
+ distance2 = null,
589
+ type = 'distance', // 'distance', 'distance+angle', 'two_distances'
590
+ name = 'Chamfer',
591
+ } = params;
592
+
593
+ const body = solidState.bodies.find(b => b.id === bodyId);
594
+ if (!body) {
595
+ return { success: false, message: `Body ${bodyId} not found` };
596
+ }
597
+
598
+ solidState.features.push({
599
+ type: OPERATION_TYPES.CHAMFER,
600
+ params: { distance, angle, distance2, type },
601
+ bodyId,
602
+ edgeIds,
603
+ });
604
+
605
+ return {
606
+ success: true,
607
+ feature: solidState.features[solidState.features.length - 1],
608
+ };
609
+ },
610
+
611
+ /**
612
+ * Shell body (create hollow interior)
613
+ */
614
+ shell(bodyId, params = {}) {
615
+ const {
616
+ thickness = 2,
617
+ removeFaces = [],
618
+ name = 'Shell',
619
+ } = params;
620
+
621
+ const body = solidState.bodies.find(b => b.id === bodyId);
622
+ if (!body) {
623
+ return { success: false, message: `Body ${bodyId} not found` };
624
+ }
625
+
626
+ // Create offset surface inward
627
+ const geometry = body.geometry;
628
+ const positions = geometry.attributes.position.array;
629
+ const normals = geometry.attributes.normal.array;
630
+
631
+ const newPositions = new Float32Array(positions.length);
632
+ for (let i = 0; i < positions.length; i += 3) {
633
+ newPositions[i] = positions[i] - normals[i] * thickness;
634
+ newPositions[i + 1] = positions[i + 1] - normals[i + 1] * thickness;
635
+ newPositions[i + 2] = positions[i + 2] - normals[i + 2] * thickness;
636
+ }
637
+
638
+ geometry.setAttribute('position', new THREE.BufferAttribute(newPositions, 3));
639
+ geometry.computeVertexNormals();
640
+
641
+ solidState.features.push({
642
+ type: OPERATION_TYPES.SHELL,
643
+ params: { thickness, removeFaces },
644
+ bodyId,
645
+ });
646
+
647
+ return {
648
+ success: true,
649
+ feature: solidState.features[solidState.features.length - 1],
650
+ };
651
+ },
652
+
653
+ /**
654
+ * Draft face (apply taper angle)
655
+ */
656
+ draft(bodyId, faceIds, params = {}) {
657
+ const {
658
+ angle = 5,
659
+ pullDirection = new THREE.Vector3(0, 0, 1),
660
+ name = 'Draft',
661
+ } = params;
662
+
663
+ const body = solidState.bodies.find(b => b.id === bodyId);
664
+ if (!body) {
665
+ return { success: false, message: `Body ${bodyId} not found` };
666
+ }
667
+
668
+ solidState.features.push({
669
+ type: OPERATION_TYPES.DRAFT,
670
+ params: { angle, pullDirection },
671
+ bodyId,
672
+ faceIds,
673
+ });
674
+
675
+ return {
676
+ success: true,
677
+ feature: solidState.features[solidState.features.length - 1],
678
+ };
679
+ },
680
+
681
+ /**
682
+ * Scale body or feature
683
+ */
684
+ scale(bodyId, params = {}) {
685
+ const {
686
+ uniformScale = 1,
687
+ scaleX = uniformScale,
688
+ scaleY = uniformScale,
689
+ scaleZ = uniformScale,
690
+ name = 'Scale',
691
+ } = params;
692
+
693
+ const body = solidState.bodies.find(b => b.id === bodyId);
694
+ if (!body) {
695
+ return { success: false, message: `Body ${bodyId} not found` };
696
+ }
697
+
698
+ body.mesh.scale.set(scaleX, scaleY, scaleZ);
699
+ body.mesh.geometry.center();
700
+
701
+ solidState.features.push({
702
+ type: OPERATION_TYPES.SCALE,
703
+ params: { scaleX, scaleY, scaleZ },
704
+ bodyId,
705
+ });
706
+
707
+ return {
708
+ success: true,
709
+ feature: solidState.features[solidState.features.length - 1],
710
+ };
711
+ },
712
+
713
+ /**
714
+ * Boolean operation (Join, Cut, Intersect)
715
+ */
716
+ combine(bodyId1, bodyId2, params = {}) {
717
+ const {
718
+ operation = 'join', // 'join', 'cut', 'intersect'
719
+ name = 'Combine',
720
+ } = params;
721
+
722
+ const body1 = solidState.bodies.find(b => b.id === bodyId1);
723
+ const body2 = solidState.bodies.find(b => b.id === bodyId2);
724
+
725
+ if (!body1 || !body2) {
726
+ return { success: false, message: 'Bodies not found' };
727
+ }
728
+
729
+ // Create combined body (simplified: just merge geometries)
730
+ const mergedGeom = THREE.BufferGeometryUtils?.mergeGeometries([body1.geometry, body2.geometry]);
731
+
732
+ if (!mergedGeom) {
733
+ return { success: false, message: 'Cannot merge geometries' };
734
+ }
735
+
736
+ mergedGeom.computeVertexNormals();
737
+
738
+ const material = new THREE.MeshStandardMaterial({
739
+ color: operation === 'cut' ? 0xff0000 : 0x0088ff,
740
+ metalness: 0.5,
741
+ roughness: 0.5,
742
+ });
743
+
744
+ const combinedBody = new SolidBody(
745
+ `body_${nextBodyId++}`,
746
+ mergedGeom,
747
+ material,
748
+ name
749
+ );
750
+
751
+ solidState.bodies.push(combinedBody);
752
+
753
+ solidState.features.push({
754
+ type: OPERATION_TYPES.COMBINE,
755
+ params: { operation },
756
+ bodyId1,
757
+ bodyId2,
758
+ resultBodyId: combinedBody.id,
759
+ });
760
+
761
+ return { success: true, body: combinedBody };
762
+ },
763
+
764
+ /**
765
+ * Mirror body
766
+ */
767
+ mirror(bodyId, params = {}) {
768
+ const {
769
+ plane = 'XY', // 'XY', 'XZ', 'YZ'
770
+ name = 'Mirror',
771
+ } = params;
772
+
773
+ const body = solidState.bodies.find(b => b.id === bodyId);
774
+ if (!body) {
775
+ return { success: false, message: `Body ${bodyId} not found` };
776
+ }
777
+
778
+ const clonedGeom = body.geometry.clone();
779
+ const positions = clonedGeom.attributes.position.array;
780
+
781
+ // Mirror positions
782
+ for (let i = 0; i < positions.length; i += 3) {
783
+ if (plane === 'XY') {
784
+ positions[i + 2] *= -1; // Mirror Z
785
+ } else if (plane === 'XZ') {
786
+ positions[i + 1] *= -1; // Mirror Y
787
+ } else if (plane === 'YZ') {
788
+ positions[i] *= -1; // Mirror X
789
+ }
790
+ }
791
+
792
+ clonedGeom.computeVertexNormals();
793
+
794
+ const material = body.material.clone();
795
+ const mirroredBody = new SolidBody(
796
+ `body_${nextBodyId++}`,
797
+ clonedGeom,
798
+ material,
799
+ name
800
+ );
801
+
802
+ solidState.bodies.push(mirroredBody);
803
+
804
+ solidState.features.push({
805
+ type: OPERATION_TYPES.MIRROR,
806
+ params: { plane },
807
+ bodyId,
808
+ mirroredBodyId: mirroredBody.id,
809
+ });
810
+
811
+ return { success: true, body: mirroredBody };
812
+ },
813
+
814
+ /**
815
+ * Pattern body (rectangular or circular)
816
+ */
817
+ pattern(bodyId, params = {}) {
818
+ const {
819
+ type = 'rectangular', // 'rectangular', 'circular'
820
+ count = 3,
821
+ distance = 20,
822
+ angle = 0,
823
+ direction = 'X', // X, Y, Z for rectangular
824
+ axis = 'Z', // Z, X, Y for circular
825
+ name = 'Pattern',
826
+ } = params;
827
+
828
+ const body = solidState.bodies.find(b => b.id === bodyId);
829
+ if (!body) {
830
+ return { success: false, message: `Body ${bodyId} not found` };
831
+ }
832
+
833
+ const patternedBodies = [];
834
+
835
+ if (type === 'rectangular') {
836
+ for (let i = 1; i < count; i++) {
837
+ const clonedGeom = body.geometry.clone();
838
+ const positions = clonedGeom.attributes.position.array;
839
+
840
+ const offset = distance * i;
841
+
842
+ for (let j = 0; j < positions.length; j += 3) {
843
+ if (direction === 'X') {
844
+ positions[j] += offset;
845
+ } else if (direction === 'Y') {
846
+ positions[j + 1] += offset;
847
+ } else if (direction === 'Z') {
848
+ positions[j + 2] += offset;
849
+ }
850
+ }
851
+
852
+ clonedGeom.computeVertexNormals();
853
+
854
+ const material = body.material.clone();
855
+ const patteredBody = new SolidBody(
856
+ `body_${nextBodyId++}`,
857
+ clonedGeom,
858
+ material,
859
+ `${name}_${i}`
860
+ );
861
+
862
+ solidState.bodies.push(patteredBody);
863
+ patternedBodies.push(patteredBody);
864
+ }
865
+ } else if (type === 'circular') {
866
+ for (let i = 1; i < count; i++) {
867
+ const clonedGeom = body.geometry.clone();
868
+ const positions = clonedGeom.attributes.position.array;
869
+
870
+ const theta = (i / count) * Math.PI * 2;
871
+ const cos = Math.cos(theta);
872
+ const sin = Math.sin(theta);
873
+
874
+ for (let j = 0; j < positions.length; j += 3) {
875
+ const x = positions[j];
876
+ const y = positions[j + 1];
877
+ const z = positions[j + 2];
878
+
879
+ if (axis === 'Z') {
880
+ positions[j] = x * cos - y * sin;
881
+ positions[j + 1] = x * sin + y * cos;
882
+ } else if (axis === 'X') {
883
+ positions[j + 1] = y * cos - z * sin;
884
+ positions[j + 2] = y * sin + z * cos;
885
+ }
886
+ }
887
+
888
+ clonedGeom.computeVertexNormals();
889
+
890
+ const material = body.material.clone();
891
+ const patteredBody = new SolidBody(
892
+ `body_${nextBodyId++}`,
893
+ clonedGeom,
894
+ material,
895
+ `${name}_${i}`
896
+ );
897
+
898
+ solidState.bodies.push(patteredBody);
899
+ patternedBodies.push(patteredBody);
900
+ }
901
+ }
902
+
903
+ solidState.features.push({
904
+ type: OPERATION_TYPES.PATTERN,
905
+ params: { type, count, distance, angle, direction, axis },
906
+ bodyId,
907
+ });
908
+
909
+ return { success: true, patternedBodies };
910
+ },
911
+
912
+ /**
913
+ * Thicken face (surface to solid)
914
+ */
915
+ thicken(faceGeometry, params = {}) {
916
+ const { thickness = 5, name = 'Thicken' } = params;
917
+
918
+ // Create offset surface and close it
919
+ const geometry = faceGeometry.clone();
920
+ const positions = geometry.attributes.position.array;
921
+ const normals = geometry.attributes.normal.array;
922
+
923
+ const thickenedPositions = new Float32Array(positions.length * 2);
924
+
925
+ // Original surface
926
+ for (let i = 0; i < positions.length; i++) {
927
+ thickenedPositions[i] = positions[i];
928
+ }
929
+
930
+ // Offset surface
931
+ for (let i = 0; i < positions.length; i += 3) {
932
+ const offset = thickness;
933
+ thickenedPositions[positions.length + i] = positions[i] + normals[i] * offset;
934
+ thickenedPositions[positions.length + i + 1] = positions[i + 1] + normals[i + 1] * offset;
935
+ thickenedPositions[positions.length + i + 2] = positions[i + 2] + normals[i + 2] * offset;
936
+ }
937
+
938
+ geometry.setAttribute('position', new THREE.BufferAttribute(thickenedPositions, 3));
939
+ geometry.computeVertexNormals();
940
+
941
+ const material = new THREE.MeshStandardMaterial({
942
+ color: 0x88ccff,
943
+ metalness: 0.4,
944
+ roughness: 0.6,
945
+ side: THREE.DoubleSide,
946
+ });
947
+
948
+ const body = new SolidBody(`body_${nextBodyId++}`, geometry, material, name);
949
+ solidState.bodies.push(body);
950
+
951
+ return { success: true, body };
952
+ },
953
+
954
+ /**
955
+ * Get all bodies
956
+ */
957
+ getBodies() {
958
+ return solidState.bodies;
959
+ },
960
+
961
+ /**
962
+ * Get all features
963
+ */
964
+ getFeatures() {
965
+ return solidState.features;
966
+ },
967
+
968
+ /**
969
+ * Get UI panel
970
+ */
971
+ getUI() {
972
+ const operations = Object.keys(OPERATION_TYPES)
973
+ .map(
974
+ op =>
975
+ `<button data-solid-op="${OPERATION_TYPES[op]}" style="padding:4px 8px;margin:2px;background:#0284C7;color:white;border:none;border-radius:2px;cursor:pointer;">${op}</button>`
976
+ )
977
+ .join('');
978
+
979
+ return `
980
+ <div id="solid-panel" style="padding:12px;background:#252526;border-radius:4px;color:#e0e0e0;font-size:12px;">
981
+ <h3>Solid Operations</h3>
982
+ <div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px;">
983
+ ${operations}
984
+ </div>
985
+
986
+ <div id="solid-bodies" style="margin-top:12px;padding:8px;background:#1e1e1e;border-radius:2px;max-height:200px;overflow-y:auto;">
987
+ <h4>Bodies</h4>
988
+ ${solidState.bodies
989
+ .map(
990
+ b =>
991
+ `<div style="padding:4px;margin:2px;background:#2d2d30;border-left:3px solid #0284C7;cursor:pointer;" data-body-id="${b.id}">${b.name}</div>`
992
+ )
993
+ .join('')}
994
+ </div>
995
+
996
+ <div id="solid-features" style="margin-top:12px;padding:8px;background:#1e1e1e;border-radius:2px;max-height:200px;overflow-y:auto;">
997
+ <h4>Features (${solidState.features.length})</h4>
998
+ ${solidState.features
999
+ .map((f, i) => `<div style="padding:4px;margin:2px;background:#2d2d30;">${f.type} #${i}</div>`)
1000
+ .join('')}
1001
+ </div>
1002
+ </div>
1003
+ `;
1004
+ },
1005
+
1006
+ /**
1007
+ * Execute solid command via agent API
1008
+ */
1009
+ async execute(command, params = {}) {
1010
+ switch (command) {
1011
+ case 'extrude':
1012
+ return this.extrude(params.geometry, params);
1013
+
1014
+ case 'revolve':
1015
+ return this.revolve(params.geometry, params.axis, params);
1016
+
1017
+ case 'sweep':
1018
+ return this.sweep(params.profileGeometry, params.pathGeometry, params);
1019
+
1020
+ case 'loft':
1021
+ return this.loft(params.profileGeometries, params);
1022
+
1023
+ case 'hole':
1024
+ return this.hole(params.bodyId, params.faceId, params);
1025
+
1026
+ case 'thread':
1027
+ return this.thread(params.bodyId, params.geometry, params);
1028
+
1029
+ case 'fillet':
1030
+ return this.fillet(params.bodyId, params.edgeIds, params);
1031
+
1032
+ case 'chamfer':
1033
+ return this.chamfer(params.bodyId, params.edgeIds, params);
1034
+
1035
+ case 'shell':
1036
+ return this.shell(params.bodyId, params);
1037
+
1038
+ case 'draft':
1039
+ return this.draft(params.bodyId, params.faceIds, params);
1040
+
1041
+ case 'scale':
1042
+ return this.scale(params.bodyId, params);
1043
+
1044
+ case 'combine':
1045
+ return this.combine(params.bodyId1, params.bodyId2, params);
1046
+
1047
+ case 'mirror':
1048
+ return this.mirror(params.bodyId, params);
1049
+
1050
+ case 'pattern':
1051
+ return this.pattern(params.bodyId, params);
1052
+
1053
+ case 'thicken':
1054
+ return this.thicken(params.geometry, params);
1055
+
1056
+ case 'getBodies':
1057
+ return { success: true, bodies: this.getBodies() };
1058
+
1059
+ case 'getFeatures':
1060
+ return { success: true, features: this.getFeatures() };
1061
+
1062
+ default:
1063
+ return { success: false, message: `Unknown command: ${command}` };
1064
+ }
1065
+ },
1066
+
1067
+ // ========================================================================
1068
+ // PRIVATE HELPERS
1069
+ // ========================================================================
1070
+
1071
+ _createThreadGeometry(radius, length, pitch, segments) {
1072
+ const geometry = new THREE.BufferGeometry();
1073
+ const positions = [];
1074
+
1075
+ const turns = length / pitch;
1076
+ const points = turns * segments;
1077
+
1078
+ for (let i = 0; i < points; i++) {
1079
+ const t = i / points;
1080
+ const angle = t * Math.PI * 2 * turns;
1081
+ const z = t * length;
1082
+ const r = radius * (1 + 0.1 * Math.sin(angle));
1083
+
1084
+ const x = r * Math.cos(angle);
1085
+ const y = r * Math.sin(angle);
1086
+
1087
+ positions.push(x, y, z);
1088
+ }
1089
+
1090
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3));
1091
+ geometry.computeVertexNormals();
1092
+
1093
+ return geometry;
1094
+ },
1095
+ };