cyclecad 3.2.0 → 3.4.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 (65) hide show
  1. package/DOCKER-SETUP-VERIFICATION.md +399 -0
  2. package/DOCKER-TESTING.md +463 -0
  3. package/FUSION360_MODULES.md +478 -0
  4. package/FUSION_MODULES_README.md +352 -0
  5. package/INTEGRATION_SNIPPETS.md +608 -0
  6. package/KILLER-FEATURES-DELIVERY.md +469 -0
  7. package/MODULES_SUMMARY.txt +337 -0
  8. package/QUICK_REFERENCE.txt +298 -0
  9. package/README-DOCKER-TESTING.txt +438 -0
  10. package/app/index.html +23 -10
  11. package/app/js/fusion-help.json +1808 -0
  12. package/app/js/help-module-v3.js +1096 -0
  13. package/app/js/killer-features-help.json +395 -0
  14. package/app/js/killer-features.js +1508 -0
  15. package/app/js/modules/fusion-assembly.js +842 -0
  16. package/app/js/modules/fusion-cam.js +785 -0
  17. package/app/js/modules/fusion-data.js +814 -0
  18. package/app/js/modules/fusion-drawing.js +844 -0
  19. package/app/js/modules/fusion-inspection.js +756 -0
  20. package/app/js/modules/fusion-render.js +774 -0
  21. package/app/js/modules/fusion-simulation.js +986 -0
  22. package/app/js/modules/fusion-sketch.js +1044 -0
  23. package/app/js/modules/fusion-solid.js +1095 -0
  24. package/app/js/modules/fusion-surface.js +949 -0
  25. package/app/tests/FUSION_TEST_SUITE.md +266 -0
  26. package/app/tests/README.md +77 -0
  27. package/app/tests/TESTING-CHECKLIST.md +177 -0
  28. package/app/tests/TEST_SUITE_SUMMARY.txt +236 -0
  29. package/app/tests/brep-live-test.html +848 -0
  30. package/app/tests/docker-integration-test.html +811 -0
  31. package/app/tests/fusion-all-tests.html +670 -0
  32. package/app/tests/fusion-assembly-tests.html +461 -0
  33. package/app/tests/fusion-cam-tests.html +421 -0
  34. package/app/tests/fusion-simulation-tests.html +421 -0
  35. package/app/tests/fusion-sketch-tests.html +613 -0
  36. package/app/tests/fusion-solid-tests.html +529 -0
  37. package/app/tests/index.html +453 -0
  38. package/app/tests/killer-features-test.html +509 -0
  39. package/app/tests/run-tests.html +874 -0
  40. package/app/tests/step-import-live-test.html +1115 -0
  41. package/app/tests/test-agent-v3.html +93 -696
  42. package/architecture-dashboard.html +1970 -0
  43. package/docs/API-REFERENCE.md +1423 -0
  44. package/docs/BREP-LIVE-TEST-GUIDE.md +453 -0
  45. package/docs/DEVELOPER-GUIDE-v3.md +795 -0
  46. package/docs/DOCKER-QUICK-TEST.md +376 -0
  47. package/docs/FUSION-FEATURES-GUIDE.md +2513 -0
  48. package/docs/FUSION-TUTORIAL.md +1203 -0
  49. package/docs/INFRASTRUCTURE-GUIDE-INDEX.md +327 -0
  50. package/docs/KEYBOARD-SHORTCUTS.md +402 -0
  51. package/docs/KILLER-FEATURES-INTEGRATION.md +412 -0
  52. package/docs/KILLER-FEATURES-SUMMARY.md +424 -0
  53. package/docs/KILLER-FEATURES-TUTORIAL.md +784 -0
  54. package/docs/KILLER-FEATURES.md +562 -0
  55. package/docs/QUICK-REFERENCE.md +282 -0
  56. package/docs/README-v3-DOCS.md +274 -0
  57. package/docs/TUTORIAL-v3.md +1190 -0
  58. package/docs/architecture-dashboard.html +1970 -0
  59. package/docs/architecture-v3.html +1038 -0
  60. package/linkedin-post-v3.md +58 -0
  61. package/package.json +1 -1
  62. package/scripts/dev-setup.sh +338 -0
  63. package/scripts/docker-health-check.sh +159 -0
  64. package/scripts/integration-test.sh +311 -0
  65. package/scripts/test-docker.sh +515 -0
@@ -0,0 +1,842 @@
1
+ /**
2
+ * cycleCAD — Fusion 360 Assembly Module
3
+ * Full assembly workspace with joints, constraints, motion studies, and exploded views.
4
+ *
5
+ * Features:
6
+ * - 7 joint types: Rigid, Revolute, Slider, Cylindrical, Pin-Slot, Planar, Ball
7
+ * - Joint Origins, As-Built Joints, Rigid Groups
8
+ * - Motion Links (gear ratios), Motion Studies, Contact Sets
9
+ * - Drive Joints (animated), Exploded Views (step-by-step)
10
+ * - Interference detection, Assembly tree, Ground components
11
+ * - Insert from file/library, Full keyframe animation support
12
+ *
13
+ * Version: 1.0.0
14
+ */
15
+
16
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
17
+
18
+ /**
19
+ * Fusion Assembly Module
20
+ * Manages complex assemblies with joints, motion studies, and interference detection
21
+ */
22
+ class FusionAssemblyModule {
23
+ constructor(scene, camera, renderer) {
24
+ this.scene = scene;
25
+ this.camera = camera;
26
+ this.renderer = renderer;
27
+
28
+ // Assembly data structures
29
+ this.components = new Map(); // componentId -> { mesh, instances, properties }
30
+ this.joints = new Map(); // jointId -> joint definition
31
+ this.rigidGroups = new Map(); // groupId -> set of component IDs
32
+ this.motionLinks = new Map(); // linkId -> { drivingJoint, drivenJoint, gearRatio }
33
+ this.motionStudies = new Map(); // studyId -> { keyframes, duration, playback }
34
+ this.contactSets = new Map(); // setId -> { comp1, comp2, type }
35
+ this.explodedViews = new Map(); // viewId -> { steps, components, positions }
36
+
37
+ // Joint origins and assembly origins
38
+ this.jointOrigins = new Map(); // jointId -> { position, quaternion }
39
+ this.assemblyOrigin = new THREE.Vector3(0, 0, 0);
40
+
41
+ // Animation state
42
+ this.isAnimating = false;
43
+ this.currentMotionStudy = null;
44
+ this.currentExplodedView = null;
45
+ this.currentTime = 0;
46
+
47
+ // Collision detection state
48
+ this.collisionPairs = [];
49
+ this.groundComponents = new Set(); // IDs of fixed components
50
+
51
+ // Animation loop handle
52
+ this.animationFrameId = null;
53
+ }
54
+
55
+ /**
56
+ * Initialize assembly module UI
57
+ */
58
+ init() {
59
+ this.setupEventListeners();
60
+ this.setupKeyframes();
61
+ }
62
+
63
+ /**
64
+ * Get UI panel for assembly controls
65
+ */
66
+ getUI() {
67
+ const panel = document.createElement('div');
68
+ panel.className = 'fusion-assembly-panel';
69
+ panel.innerHTML = `
70
+ <style>
71
+ .fusion-assembly-panel {
72
+ padding: 16px;
73
+ font-size: 12px;
74
+ background: var(--bg-secondary);
75
+ color: var(--text-primary);
76
+ border-radius: 4px;
77
+ max-height: 600px;
78
+ overflow-y: auto;
79
+ }
80
+ .fusion-assembly-panel h3 {
81
+ margin: 0 0 12px 0;
82
+ font-size: 14px;
83
+ font-weight: 600;
84
+ color: var(--text-primary);
85
+ }
86
+ .assembly-section {
87
+ margin-bottom: 16px;
88
+ padding-bottom: 12px;
89
+ border-bottom: 1px solid var(--border-color);
90
+ }
91
+ .assembly-section:last-child {
92
+ border-bottom: none;
93
+ margin-bottom: 0;
94
+ }
95
+ .joint-list {
96
+ display: flex;
97
+ flex-direction: column;
98
+ gap: 8px;
99
+ }
100
+ .joint-item {
101
+ padding: 8px;
102
+ background: var(--bg-primary);
103
+ border-radius: 3px;
104
+ cursor: pointer;
105
+ transition: background 0.2s;
106
+ }
107
+ .joint-item:hover {
108
+ background: var(--bg-tertiary);
109
+ }
110
+ .joint-type {
111
+ font-weight: 600;
112
+ color: var(--accent-color);
113
+ }
114
+ .motion-controls {
115
+ display: flex;
116
+ gap: 6px;
117
+ margin-top: 8px;
118
+ }
119
+ .motion-controls button {
120
+ padding: 6px 10px;
121
+ font-size: 11px;
122
+ background: var(--button-bg);
123
+ border: 1px solid var(--border-color);
124
+ border-radius: 3px;
125
+ cursor: pointer;
126
+ color: var(--text-primary);
127
+ flex: 1;
128
+ }
129
+ .motion-controls button:hover {
130
+ background: var(--button-hover-bg);
131
+ }
132
+ .motion-controls button:active {
133
+ background: var(--button-active-bg);
134
+ }
135
+ .slider-group {
136
+ display: flex;
137
+ gap: 8px;
138
+ align-items: center;
139
+ margin: 8px 0;
140
+ }
141
+ .slider-group label {
142
+ font-weight: 600;
143
+ width: 60px;
144
+ font-size: 11px;
145
+ }
146
+ .slider-group input[type="range"] {
147
+ flex: 1;
148
+ }
149
+ .slider-group span {
150
+ width: 40px;
151
+ text-align: right;
152
+ font-size: 11px;
153
+ color: var(--text-secondary);
154
+ }
155
+ .component-tree {
156
+ max-height: 250px;
157
+ overflow-y: auto;
158
+ border: 1px solid var(--border-color);
159
+ border-radius: 3px;
160
+ padding: 6px;
161
+ }
162
+ .component-node {
163
+ padding: 4px 6px;
164
+ cursor: pointer;
165
+ border-radius: 2px;
166
+ transition: background 0.2s;
167
+ user-select: none;
168
+ font-size: 11px;
169
+ }
170
+ .component-node:hover {
171
+ background: var(--bg-tertiary);
172
+ }
173
+ .component-node.ground::before {
174
+ content: '🔒 ';
175
+ }
176
+ .component-node-indent {
177
+ margin-left: 16px;
178
+ }
179
+ </style>
180
+
181
+ <div class="assembly-section">
182
+ <h3>Components</h3>
183
+ <div class="component-tree" id="assemblyComponentTree"></div>
184
+ <div style="margin-top: 8px; display: flex; gap: 6px;">
185
+ <button onclick="window.fusionAssembly?.insertComponent()">Insert Component</button>
186
+ <button onclick="window.fusionAssembly?.groundComponent()">Ground</button>
187
+ </div>
188
+ </div>
189
+
190
+ <div class="assembly-section">
191
+ <h3>Joints</h3>
192
+ <div class="joint-list" id="assemblyJointList"></div>
193
+ <button onclick="window.fusionAssembly?.createJoint()" style="width: 100%; padding: 8px; margin-top: 8px;">Create Joint</button>
194
+ </div>
195
+
196
+ <div class="assembly-section">
197
+ <h3>Motion Studies</h3>
198
+ <div id="assemblyMotionStudies" style="display: flex; gap: 6px; margin-bottom: 8px;">
199
+ <button onclick="window.fusionAssembly?.createMotionStudy()" style="flex: 1;">New Study</button>
200
+ <button onclick="window.fusionAssembly?.playMotionStudy()" style="flex: 1;">Play</button>
201
+ </div>
202
+ <div class="slider-group">
203
+ <label>Time:</label>
204
+ <input type="range" id="assemblyTimeSlider" min="0" max="100" value="0" step="1">
205
+ <span id="assemblyTimeDisplay">0.0s</span>
206
+ </div>
207
+ </div>
208
+
209
+ <div class="assembly-section">
210
+ <h3>Exploded View</h3>
211
+ <div style="display: flex; gap: 6px;">
212
+ <button onclick="window.fusionAssembly?.createExplodedView()" style="flex: 1;">Create</button>
213
+ <button onclick="window.fusionAssembly?.editExplode()" style="flex: 1;">Edit</button>
214
+ <button onclick="window.fusionAssembly?.assembleAll()" style="flex: 1;">Assemble</button>
215
+ </div>
216
+ </div>
217
+
218
+ <div class="assembly-section">
219
+ <h3>Analysis</h3>
220
+ <div style="display: flex; gap: 6px;">
221
+ <button onclick="window.fusionAssembly?.checkInterference()" style="flex: 1;">Interference</button>
222
+ <button onclick="window.fusionAssembly?.contactSet()" style="flex: 1;">Contact Set</button>
223
+ </div>
224
+ </div>
225
+ `;
226
+
227
+ window.fusionAssembly = this;
228
+ return panel;
229
+ }
230
+
231
+ /**
232
+ * Create a component instance in the assembly
233
+ * @param {THREE.Mesh} mesh - The 3D geometry
234
+ * @param {string} name - Component name
235
+ * @returns {string} Component ID
236
+ */
237
+ addComponent(mesh, name = 'Component') {
238
+ const componentId = `comp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
239
+
240
+ const component = {
241
+ id: componentId,
242
+ name: name,
243
+ mesh: mesh,
244
+ instances: [mesh],
245
+ originalMatrix: mesh.matrix.clone(),
246
+ position: mesh.position.clone(),
247
+ quaternion: mesh.quaternion.clone(),
248
+ isGrounded: false,
249
+ properties: {
250
+ mass: 1.0,
251
+ material: 'Steel',
252
+ appearance: 0x888888
253
+ }
254
+ };
255
+
256
+ this.components.set(componentId, component);
257
+ this.updateComponentTree();
258
+ return componentId;
259
+ }
260
+
261
+ /**
262
+ * Create a joint between two components
263
+ * Supports: Rigid, Revolute, Slider, Cylindrical, Pin-Slot, Planar, Ball
264
+ */
265
+ createJoint(type = 'Revolute', comp1Id, comp2Id, origin = null, axis = new THREE.Vector3(0, 0, 1)) {
266
+ if (!this.components.has(comp1Id) || !this.components.has(comp2Id)) {
267
+ console.warn('Invalid component IDs for joint');
268
+ return null;
269
+ }
270
+
271
+ const jointId = `joint_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
272
+
273
+ // Default origin is midpoint between component centers
274
+ if (!origin) {
275
+ const comp1 = this.components.get(comp1Id);
276
+ const comp2 = this.components.get(comp2Id);
277
+ origin = new THREE.Vector3()
278
+ .addVectors(comp1.position, comp2.position)
279
+ .multiplyScalar(0.5);
280
+ }
281
+
282
+ const joint = {
283
+ id: jointId,
284
+ type: type, // Rigid | Revolute | Slider | Cylindrical | Pin-Slot | Planar | Ball
285
+ component1: comp1Id,
286
+ component2: comp2Id,
287
+ origin: origin,
288
+ axis: axis.normalize(),
289
+
290
+ // Joint parameters (varies by type)
291
+ minAngle: type === 'Revolute' ? 0 : null,
292
+ maxAngle: type === 'Revolute' ? Math.PI * 2 : null,
293
+ minDistance: type === 'Slider' ? 0 : null,
294
+ maxDistance: type === 'Slider' ? 100 : null,
295
+
296
+ // Current state
297
+ currentValue: 0, // Angle for Revolute, distance for Slider
298
+ velocity: 0,
299
+ acceleration: 0,
300
+
301
+ // Drive properties
302
+ isDriven: false,
303
+ driveExpression: null, // Time-based function
304
+
305
+ // Rigid group (for Rigid joints)
306
+ rigidGroupId: null
307
+ };
308
+
309
+ this.joints.set(jointId, joint);
310
+
311
+ // Store joint origin
312
+ this.jointOrigins.set(jointId, {
313
+ position: origin.clone(),
314
+ quaternion: new THREE.Quaternion()
315
+ });
316
+
317
+ // For Rigid joints, create a rigid group
318
+ if (type === 'Rigid') {
319
+ const groupId = `rgroup_${jointId}`;
320
+ this.rigidGroups.set(groupId, new Set([comp1Id, comp2Id]));
321
+ joint.rigidGroupId = groupId;
322
+ }
323
+
324
+ this.updateJointList();
325
+ return jointId;
326
+ }
327
+
328
+ /**
329
+ * Set joint as driven with time-based animation
330
+ */
331
+ driveJoint(jointId, expression) {
332
+ const joint = this.joints.get(jointId);
333
+ if (!joint) return;
334
+
335
+ joint.isDriven = true;
336
+ joint.driveExpression = expression; // e.g., (t) => t * Math.PI / 2
337
+ }
338
+
339
+ /**
340
+ * Create a motion link (gear ratio between joints)
341
+ */
342
+ createMotionLink(drivingJointId, drivenJointId, gearRatio = 1.0) {
343
+ const linkId = `mlink_${Date.now()}`;
344
+ this.motionLinks.set(linkId, {
345
+ id: linkId,
346
+ drivingJoint: drivingJointId,
347
+ drivenJoint: drivenJointId,
348
+ gearRatio: gearRatio
349
+ });
350
+ return linkId;
351
+ }
352
+
353
+ /**
354
+ * Create a motion study (keyframe animation)
355
+ */
356
+ createMotionStudy(name = 'Study1') {
357
+ const studyId = `study_${Date.now()}`;
358
+ this.motionStudies.set(studyId, {
359
+ id: studyId,
360
+ name: name,
361
+ keyframes: [], // { time, joints: { jointId: value } }
362
+ duration: 5000, // ms
363
+ playbackSpeed: 1.0,
364
+ loop: true,
365
+ isPlaying: false
366
+ });
367
+ this.currentMotionStudy = studyId;
368
+ return studyId;
369
+ }
370
+
371
+ /**
372
+ * Add keyframe to current motion study
373
+ */
374
+ addKeyframe(time, jointValues) {
375
+ if (!this.currentMotionStudy) return;
376
+
377
+ const study = this.motionStudies.get(this.currentMotionStudy);
378
+ study.keyframes.push({
379
+ time: time,
380
+ joints: jointValues // { jointId: angle/distance }
381
+ });
382
+
383
+ // Sort keyframes by time
384
+ study.keyframes.sort((a, b) => a.time - b.time);
385
+ }
386
+
387
+ /**
388
+ * Play motion study with smooth interpolation
389
+ */
390
+ playMotionStudy(studyId = null) {
391
+ const study = this.motionStudies.get(studyId || this.currentMotionStudy);
392
+ if (!study || study.isPlaying) return;
393
+
394
+ study.isPlaying = true;
395
+ this.isAnimating = true;
396
+ this.currentTime = 0;
397
+
398
+ const startTime = performance.now();
399
+ const duration = study.duration;
400
+
401
+ const animate = (currentTime) => {
402
+ const elapsed = currentTime - startTime;
403
+ const progress = (elapsed % duration) / duration;
404
+ this.currentTime = progress * duration;
405
+
406
+ // Interpolate joint values
407
+ this.interpolateJoints(study, progress);
408
+ this.updateComponentTransforms();
409
+
410
+ if (study.loop || elapsed < duration) {
411
+ this.animationFrameId = requestAnimationFrame(animate);
412
+ } else {
413
+ study.isPlaying = false;
414
+ this.isAnimating = false;
415
+ }
416
+ };
417
+
418
+ this.animationFrameId = requestAnimationFrame(animate);
419
+ }
420
+
421
+ /**
422
+ * Interpolate joint values between keyframes
423
+ */
424
+ interpolateJoints(study, progress) {
425
+ const time = progress * study.duration;
426
+
427
+ for (const joint of this.joints.values()) {
428
+ if (!joint.isDriven) continue;
429
+
430
+ // Find surrounding keyframes
431
+ let kf1 = null, kf2 = null;
432
+ for (let i = 0; i < study.keyframes.length; i++) {
433
+ if (study.keyframes[i].time <= time) kf1 = study.keyframes[i];
434
+ if (study.keyframes[i].time >= time && !kf2) kf2 = study.keyframes[i];
435
+ }
436
+
437
+ if (!kf1 || !kf2) continue;
438
+
439
+ // Linear interpolation
440
+ const t = kf1 === kf2 ? 0 : (time - kf1.time) / (kf2.time - kf1.time);
441
+ const val1 = kf1.joints[joint.id] || 0;
442
+ const val2 = kf2.joints[joint.id] || 0;
443
+
444
+ joint.currentValue = val1 + (val2 - val1) * t;
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Update component transforms based on joint values
450
+ */
451
+ updateComponentTransforms() {
452
+ // For each joint, calculate component positions
453
+ for (const [jointId, joint] of this.joints) {
454
+ const comp1 = this.components.get(joint.component1);
455
+ const comp2 = this.components.get(joint.component2);
456
+
457
+ if (!comp1 || !comp2) continue;
458
+
459
+ // Apply joint transformations
460
+ switch (joint.type) {
461
+ case 'Revolute':
462
+ // Rotate comp2 around joint axis
463
+ const axis = joint.axis;
464
+ const quat = new THREE.Quaternion();
465
+ quat.setFromAxisAngle(axis, joint.currentValue);
466
+ comp2.mesh.quaternion.multiplyQuaternions(quat, comp2.quaternion);
467
+ break;
468
+
469
+ case 'Slider':
470
+ // Translate comp2 along joint axis
471
+ const displacement = joint.axis.clone().multiplyScalar(joint.currentValue);
472
+ comp2.mesh.position.copy(comp2.position).add(displacement);
473
+ break;
474
+
475
+ case 'Cylindrical':
476
+ // Both rotation and translation
477
+ const axis2 = joint.axis;
478
+ const quat2 = new THREE.Quaternion();
479
+ quat2.setFromAxisAngle(axis2, joint.currentValue * 0.5);
480
+ comp2.mesh.quaternion.multiplyQuaternions(quat2, comp2.quaternion);
481
+
482
+ const disp = joint.axis.clone().multiplyScalar(joint.currentValue * 10);
483
+ comp2.mesh.position.copy(comp2.position).add(disp);
484
+ break;
485
+ }
486
+ }
487
+
488
+ // Handle rigid groups (locked together)
489
+ for (const [groupId, componentSet] of this.rigidGroups) {
490
+ const comps = Array.from(componentSet).map(id => this.components.get(id));
491
+ // All components in group maintain relative transforms
492
+ // (simplified for demo)
493
+ }
494
+
495
+ // Apply motion links (gear ratios)
496
+ for (const [linkId, link] of this.motionLinks) {
497
+ const drivingJoint = this.joints.get(link.drivingJoint);
498
+ const drivenJoint = this.joints.get(link.drivenJoint);
499
+
500
+ if (drivingJoint && drivenJoint) {
501
+ drivenJoint.currentValue = drivingJoint.currentValue * link.gearRatio;
502
+ }
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Create exploded view with auto-generated steps
508
+ */
509
+ createExplodedView(name = 'Exploded') {
510
+ const viewId = `explode_${Date.now()}`;
511
+ const steps = [];
512
+
513
+ // Auto-generate explosion steps (one per component)
514
+ let stepNum = 0;
515
+ for (const [compId, comp] of this.components) {
516
+ const direction = new THREE.Vector3(
517
+ Math.cos(stepNum * Math.PI / this.components.size),
518
+ 0,
519
+ Math.sin(stepNum * Math.PI / this.components.size)
520
+ );
521
+ const distance = 50 + stepNum * 10;
522
+
523
+ steps.push({
524
+ stepNum: stepNum,
525
+ component: compId,
526
+ targetPosition: comp.mesh.position.clone().add(direction.multiplyScalar(distance)),
527
+ targetRotation: comp.mesh.quaternion.clone(),
528
+ duration: 500
529
+ });
530
+ stepNum++;
531
+ }
532
+
533
+ this.explodedViews.set(viewId, {
534
+ id: viewId,
535
+ name: name,
536
+ steps: steps,
537
+ currentStep: 0,
538
+ components: Array.from(this.components.keys()),
539
+ positions: new Map(),
540
+ isExploded: false
541
+ });
542
+
543
+ this.currentExplodedView = viewId;
544
+ return viewId;
545
+ }
546
+
547
+ /**
548
+ * Animate to exploded view
549
+ */
550
+ explode(viewId = null) {
551
+ const view = this.explodedViews.get(viewId || this.currentExplodedView);
552
+ if (!view || view.isExploded) return;
553
+
554
+ view.isExploded = true;
555
+ const startTime = performance.now();
556
+ const totalDuration = view.steps.reduce((sum, step) => sum + step.duration, 0);
557
+
558
+ const animate = (currentTime) => {
559
+ const elapsed = currentTime - startTime;
560
+ let cumulativeTime = 0;
561
+
562
+ for (const step of view.steps) {
563
+ const stepStart = cumulativeTime;
564
+ const stepEnd = cumulativeTime + step.duration;
565
+
566
+ if (elapsed >= stepStart && elapsed <= stepEnd) {
567
+ const progress = (elapsed - stepStart) / step.duration;
568
+ const comp = this.components.get(step.component);
569
+
570
+ if (comp) {
571
+ // Smooth interpolation
572
+ comp.mesh.position.lerpVectors(
573
+ comp.position,
574
+ step.targetPosition,
575
+ this.easeInOutCubic(progress)
576
+ );
577
+ }
578
+ }
579
+
580
+ cumulativeTime = stepEnd;
581
+ }
582
+
583
+ if (elapsed < totalDuration) {
584
+ this.animationFrameId = requestAnimationFrame(animate);
585
+ }
586
+ };
587
+
588
+ this.animationFrameId = requestAnimationFrame(animate);
589
+ }
590
+
591
+ /**
592
+ * Animate to assembled view
593
+ */
594
+ assembleAll(viewId = null) {
595
+ const view = this.explodedViews.get(viewId || this.currentExplodedView);
596
+ if (!view || !view.isExploded) return;
597
+
598
+ view.isExploded = false;
599
+ const startTime = performance.now();
600
+ const totalDuration = view.steps.reduce((sum, step) => sum + step.duration, 0);
601
+
602
+ const animate = (currentTime) => {
603
+ const elapsed = currentTime - startTime;
604
+ let cumulativeTime = 0;
605
+
606
+ for (const step of view.steps) {
607
+ const stepStart = cumulativeTime;
608
+ const stepEnd = cumulativeTime + step.duration;
609
+
610
+ if (elapsed >= stepStart && elapsed <= stepEnd) {
611
+ const progress = (elapsed - stepStart) / step.duration;
612
+ const comp = this.components.get(step.component);
613
+
614
+ if (comp) {
615
+ // Reverse interpolation
616
+ comp.mesh.position.lerpVectors(
617
+ step.targetPosition,
618
+ comp.position,
619
+ this.easeInOutCubic(progress)
620
+ );
621
+ }
622
+ }
623
+
624
+ cumulativeTime = stepEnd;
625
+ }
626
+
627
+ if (elapsed < totalDuration) {
628
+ this.animationFrameId = requestAnimationFrame(animate);
629
+ }
630
+ };
631
+
632
+ this.animationFrameId = requestAnimationFrame(animate);
633
+ }
634
+
635
+ /**
636
+ * Check for interference (collision) between components
637
+ */
638
+ checkInterference() {
639
+ this.collisionPairs = [];
640
+ const componentArray = Array.from(this.components.values());
641
+
642
+ for (let i = 0; i < componentArray.length; i++) {
643
+ for (let j = i + 1; j < componentArray.length; j++) {
644
+ const comp1 = componentArray[i];
645
+ const comp2 = componentArray[j];
646
+
647
+ if (this.checkBBoxCollision(comp1.mesh, comp2.mesh)) {
648
+ this.collisionPairs.push({
649
+ component1: comp1.name,
650
+ component2: comp2.name,
651
+ comp1Id: comp1.id,
652
+ comp2Id: comp2.id,
653
+ severity: 'medium'
654
+ });
655
+
656
+ // Highlight colliding components
657
+ comp1.mesh.material.color.setHex(0xff6666);
658
+ comp2.mesh.material.color.setHex(0xff6666);
659
+ }
660
+ }
661
+ }
662
+
663
+ return this.collisionPairs;
664
+ }
665
+
666
+ /**
667
+ * Check bbox collision between two meshes
668
+ */
669
+ checkBBoxCollision(mesh1, mesh2) {
670
+ const box1 = new THREE.Box3().setFromObject(mesh1);
671
+ const box2 = new THREE.Box3().setFromObject(mesh2);
672
+ return box1.intersectsBox(box2);
673
+ }
674
+
675
+ /**
676
+ * Create contact set (physical contact between components)
677
+ */
678
+ contactSet(comp1Id, comp2Id, type = 'face-to-face') {
679
+ const setId = `contact_${Date.now()}`;
680
+ this.contactSets.set(setId, {
681
+ id: setId,
682
+ component1: comp1Id,
683
+ component2: comp2Id,
684
+ type: type, // face-to-face | edge-to-face | vertex-to-face
685
+ restitution: 0.5,
686
+ friction: 0.3
687
+ });
688
+ return setId;
689
+ }
690
+
691
+ /**
692
+ * Ground component (fix in place)
693
+ */
694
+ groundComponent(componentId = null) {
695
+ if (!componentId) {
696
+ // Show dialog to select component
697
+ console.log('Select component to ground');
698
+ return;
699
+ }
700
+
701
+ const comp = this.components.get(componentId);
702
+ if (comp) {
703
+ comp.isGrounded = true;
704
+ this.groundComponents.add(componentId);
705
+ this.updateComponentTree();
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Insert component from file
711
+ */
712
+ insertComponent(filePath = null) {
713
+ if (!filePath) {
714
+ console.log('Open file picker to import component');
715
+ return;
716
+ }
717
+
718
+ // Load geometry from file (STL, STEP, OBJ, etc.)
719
+ // For now, create a placeholder box
720
+ const geometry = new THREE.BoxGeometry(20, 20, 20);
721
+ const material = new THREE.MeshPhongMaterial({ color: 0x888888 });
722
+ const mesh = new THREE.Mesh(geometry, material);
723
+
724
+ this.scene.add(mesh);
725
+ this.addComponent(mesh, 'Imported_Component');
726
+ }
727
+
728
+ /**
729
+ * Update component tree display
730
+ */
731
+ updateComponentTree() {
732
+ const treeDiv = document.getElementById('assemblyComponentTree');
733
+ if (!treeDiv) return;
734
+
735
+ treeDiv.innerHTML = '';
736
+ for (const [compId, comp] of this.components) {
737
+ const nodeDiv = document.createElement('div');
738
+ nodeDiv.className = `component-node ${comp.isGrounded ? 'ground' : ''}`;
739
+ nodeDiv.textContent = comp.name;
740
+ nodeDiv.onclick = () => {
741
+ comp.mesh.material.color.setHex(0xffff00);
742
+ setTimeout(() => {
743
+ comp.mesh.material.color.setHex(comp.properties.appearance);
744
+ }, 200);
745
+ };
746
+ treeDiv.appendChild(nodeDiv);
747
+ }
748
+ }
749
+
750
+ /**
751
+ * Update joint list display
752
+ */
753
+ updateJointList() {
754
+ const listDiv = document.getElementById('assemblyJointList');
755
+ if (!listDiv) return;
756
+
757
+ listDiv.innerHTML = '';
758
+ for (const [jointId, joint] of this.joints) {
759
+ const itemDiv = document.createElement('div');
760
+ itemDiv.className = 'joint-item';
761
+ itemDiv.innerHTML = `
762
+ <div><span class="joint-type">${joint.type}</span></div>
763
+ <div style="font-size: 10px; color: var(--text-secondary);">
764
+ ${this.components.get(joint.component1)?.name || 'Unknown'} ↔
765
+ ${this.components.get(joint.component2)?.name || 'Unknown'}
766
+ </div>
767
+ `;
768
+ listDiv.appendChild(itemDiv);
769
+ }
770
+ }
771
+
772
+ /**
773
+ * Setup event listeners
774
+ */
775
+ setupEventListeners() {
776
+ const timeSlider = document.getElementById('assemblyTimeSlider');
777
+ const timeDisplay = document.getElementById('assemblyTimeDisplay');
778
+
779
+ if (timeSlider) {
780
+ timeSlider.addEventListener('input', (e) => {
781
+ const study = this.motionStudies.get(this.currentMotionStudy);
782
+ if (study) {
783
+ this.currentTime = parseFloat(e.target.value) * study.duration / 100;
784
+ const progress = this.currentTime / study.duration;
785
+ this.interpolateJoints(study, progress);
786
+ this.updateComponentTransforms();
787
+ if (timeDisplay) {
788
+ timeDisplay.textContent = (this.currentTime / 1000).toFixed(1) + 's';
789
+ }
790
+ }
791
+ });
792
+ }
793
+ }
794
+
795
+ /**
796
+ * Setup keyframe controls
797
+ */
798
+ setupKeyframes() {
799
+ // Initialize keyframe system (ready for animation)
800
+ }
801
+
802
+ /**
803
+ * Easing function for smooth animation
804
+ */
805
+ easeInOutCubic(t) {
806
+ return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
807
+ }
808
+
809
+ /**
810
+ * Execute command from agent API
811
+ */
812
+ execute(command, params) {
813
+ switch (command) {
814
+ case 'addComponent':
815
+ return this.addComponent(params.mesh, params.name);
816
+ case 'createJoint':
817
+ return this.createJoint(params.type, params.comp1, params.comp2, params.origin, params.axis);
818
+ case 'driveJoint':
819
+ return this.driveJoint(params.jointId, params.expression);
820
+ case 'createMotionStudy':
821
+ return this.createMotionStudy(params.name);
822
+ case 'addKeyframe':
823
+ return this.addKeyframe(params.time, params.values);
824
+ case 'playMotionStudy':
825
+ return this.playMotionStudy(params.studyId);
826
+ case 'createExplodedView':
827
+ return this.createExplodedView(params.name);
828
+ case 'explode':
829
+ return this.explode(params.viewId);
830
+ case 'assemble':
831
+ return this.assembleAll(params.viewId);
832
+ case 'checkInterference':
833
+ return this.checkInterference();
834
+ case 'groundComponent':
835
+ return this.groundComponent(params.componentId);
836
+ default:
837
+ console.warn(`Unknown assembly command: ${command}`);
838
+ }
839
+ }
840
+ }
841
+
842
+ export default FusionAssemblyModule;