cyclecad 0.1.3 → 0.1.4

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.
@@ -0,0 +1,743 @@
1
+ /**
2
+ * rebuild-guide.js
3
+ * Generates step-by-step reconstruction guides for Inventor parts
4
+ * Supports both cycleCAD and Fusion 360 Free workflows
5
+ *
6
+ * Usage:
7
+ * const guide = generateGuide(parsedPart);
8
+ * renderGuide(container, guide);
9
+ * exportGuideHTML(guide, 'my-part.html');
10
+ */
11
+
12
+ // Feature type mappings and instructions
13
+ const FEATURE_INSTRUCTIONS = {
14
+ Sketch: {
15
+ cycleCAD: {
16
+ text: 'Click Sketch button (S) → Select XY/XZ/YZ plane → Draw profile using Line (L), Rectangle (R), Circle (C) tools → Press Enter to finish',
17
+ steps: [
18
+ 'Click Sketch button or press S',
19
+ 'Select the appropriate plane (XY, XZ, or YZ)',
20
+ 'Use drawing tools: Line (L), Rectangle (R), Circle (C), Arc (A)',
21
+ 'Apply constraints: horizontal, vertical, distance, radius',
22
+ 'Press Enter to finish sketch'
23
+ ],
24
+ time: '2-5 min'
25
+ },
26
+ fusion360: {
27
+ text: 'DESIGN workspace → CREATE → New Sketch → Select plane → Sketch toolbar (Line, Rectangle, Circle) → Finish Sketch',
28
+ steps: [
29
+ 'Switch to DESIGN workspace if needed',
30
+ 'Click CREATE → New Sketch',
31
+ 'Select the plane or face to sketch on',
32
+ 'Use Sketch toolbar: Line, Rectangle, Circle, Arc',
33
+ 'Apply geometric and dimensional constraints',
34
+ 'Click Finish Sketch'
35
+ ],
36
+ time: '2-5 min',
37
+ note: 'Available in Fusion 360 Free'
38
+ }
39
+ },
40
+ Extrude: {
41
+ cycleCAD: {
42
+ text: 'Select sketch profile → Click Extrude (E) → Set distance → Choose direction (one side/symmetric/two sides) → Click OK',
43
+ steps: [
44
+ 'Select the sketch profile',
45
+ 'Click Extrude button or press E',
46
+ 'Set extrusion distance in input field',
47
+ 'Choose direction: One Side, Symmetric, or Two Sides',
48
+ 'Click OK to apply extrusion'
49
+ ],
50
+ time: '1-2 min'
51
+ },
52
+ fusion360: {
53
+ text: 'Select sketch → DESIGN → CREATE → Extrude → Set distance and direction → OK',
54
+ steps: [
55
+ 'Ensure sketch is selected',
56
+ 'Click CREATE → Extrude',
57
+ 'In properties panel, set distance',
58
+ 'Choose operation: New Body, Add, Cut, or Intersect',
59
+ 'Select direction: Forward, Backward, or Symmetric',
60
+ 'Click OK'
61
+ ],
62
+ time: '1-2 min',
63
+ note: 'Available in Fusion 360 Free'
64
+ }
65
+ },
66
+ Revolve: {
67
+ cycleCAD: {
68
+ text: 'Select sketch profile → Click Revolve (V) → Pick axis → Set angle (360° for full revolution) → Click OK',
69
+ steps: [
70
+ 'Select the sketch profile',
71
+ 'Click Revolve button or press V',
72
+ 'Select the axis of revolution from sketch geometry',
73
+ 'Set revolution angle (360° for complete revolution)',
74
+ 'Choose operation: Add, Cut, or Intersect',
75
+ 'Click OK'
76
+ ],
77
+ time: '1-2 min'
78
+ },
79
+ fusion360: {
80
+ text: 'Select sketch → CREATE → Revolve → Pick axis → Set angle → OK',
81
+ steps: [
82
+ 'Select sketch profile',
83
+ 'Click CREATE → Revolve',
84
+ 'Select axis of revolution from sketch',
85
+ 'Set angle: 360° for full or partial revolution',
86
+ 'Choose operation: New Body, Add, Cut, or Intersect',
87
+ 'Click OK'
88
+ ],
89
+ time: '1-2 min',
90
+ note: 'Available in Fusion 360 Free'
91
+ }
92
+ },
93
+ Fillet: {
94
+ cycleCAD: {
95
+ text: 'Select edge(s) → Click Fillet (F) → Set radius value → Click OK',
96
+ steps: [
97
+ 'Select edge(s) to fillet (hold Ctrl for multiple)',
98
+ 'Click Fillet button or press F',
99
+ 'Set fillet radius in input field',
100
+ 'Preview updates in viewport',
101
+ 'Click OK to apply'
102
+ ],
103
+ time: '1 min'
104
+ },
105
+ fusion360: {
106
+ text: 'Select edge(s) → DESIGN → Modify → Fillet → Set radius → OK',
107
+ steps: [
108
+ 'Select edge(s) to fillet',
109
+ 'Click Modify → Fillet',
110
+ 'In properties panel, set radius value',
111
+ 'Adjust if needed',
112
+ 'Click OK'
113
+ ],
114
+ time: '1 min',
115
+ note: 'Available in Fusion 360 Free'
116
+ }
117
+ },
118
+ Chamfer: {
119
+ cycleCAD: {
120
+ text: 'Select edge(s) → Click Chamfer → Set distance and angle → Click OK',
121
+ steps: [
122
+ 'Select edge(s) to chamfer',
123
+ 'Click Chamfer button',
124
+ 'Set chamfer distance and angle',
125
+ 'Preview updates',
126
+ 'Click OK'
127
+ ],
128
+ time: '1 min'
129
+ },
130
+ fusion360: {
131
+ text: 'Select edge(s) → DESIGN → Modify → Chamfer → Set parameters → OK',
132
+ steps: [
133
+ 'Select edge(s)',
134
+ 'Click Modify → Chamfer',
135
+ 'Set chamfer distance/size',
136
+ 'Choose chamfer type: Distance or Angle',
137
+ 'Click OK'
138
+ ],
139
+ time: '1 min',
140
+ note: 'Available in Fusion 360 Free'
141
+ }
142
+ },
143
+ Hole: {
144
+ cycleCAD: {
145
+ text: 'Select face → Click Hole (H) → Choose type (simple/counterbore/countersink) → Set diameter and depth',
146
+ steps: [
147
+ 'Select the face where hole will be placed',
148
+ 'Click Hole button or press H',
149
+ 'Choose hole type: Simple, Counterbore, or Countersink',
150
+ 'Set hole diameter',
151
+ 'Set hole depth',
152
+ 'Click OK'
153
+ ],
154
+ time: '1-2 min'
155
+ },
156
+ fusion360: {
157
+ text: 'Select face → CREATE → Hole → Choose type → Set parameters → OK',
158
+ steps: [
159
+ 'Select face for hole placement',
160
+ 'Click CREATE → Hole',
161
+ 'Choose hole type: Simple, Counterbore, or Countersink',
162
+ 'Set hole diameter from standard or custom value',
163
+ 'Set depth',
164
+ 'Click OK'
165
+ ],
166
+ time: '1-2 min',
167
+ note: 'Available in Fusion 360 Free'
168
+ }
169
+ },
170
+ Mirror: {
171
+ cycleCAD: {
172
+ text: 'Select feature(s) → Click Mirror → Select mirror plane (XY/XZ/YZ) → Click OK',
173
+ steps: [
174
+ 'Select feature(s) to mirror',
175
+ 'Click Mirror button',
176
+ 'Select mirror plane: XY, XZ, or YZ',
177
+ 'Preview shows mirrored geometry',
178
+ 'Click OK'
179
+ ],
180
+ time: '1 min'
181
+ },
182
+ fusion360: {
183
+ text: 'Select feature(s) → CREATE → Mirror → Select plane → OK',
184
+ steps: [
185
+ 'Select feature(s) to mirror',
186
+ 'Click CREATE → Mirror',
187
+ 'Select mirror plane',
188
+ 'Choose operation: New Body or Add',
189
+ 'Click OK'
190
+ ],
191
+ time: '1 min',
192
+ note: 'Available in Fusion 360 Free'
193
+ }
194
+ },
195
+ Pattern: {
196
+ cycleCAD: {
197
+ text: 'Select feature → Click Pattern (P) → Choose Rectangular/Circular → Set count and spacing → Click OK',
198
+ steps: [
199
+ 'Select feature to pattern',
200
+ 'Click Pattern button or press P',
201
+ 'Choose pattern type: Rectangular or Circular',
202
+ 'Set column count and spacing',
203
+ 'Set row count and spacing (if rectangular)',
204
+ 'Set number of instances (if circular)',
205
+ 'Click OK'
206
+ ],
207
+ time: '1-2 min'
208
+ },
209
+ fusion360: {
210
+ text: 'Select feature → CREATE → Pattern → Choose type → Set parameters → OK',
211
+ steps: [
212
+ 'Select feature to pattern',
213
+ 'Click CREATE → Pattern',
214
+ 'Choose Rectangular or Circular Pattern',
215
+ 'Set count and spacing parameters',
216
+ 'Adjust quantity',
217
+ 'Click OK'
218
+ ],
219
+ time: '1-2 min',
220
+ note: 'Available in Fusion 360 Free'
221
+ }
222
+ },
223
+ Shell: {
224
+ cycleCAD: {
225
+ text: 'Select face(s) to remove → Click Shell → Set wall thickness → Click OK',
226
+ steps: [
227
+ 'Select face(s) to remove (shell operation)',
228
+ 'Click Shell button',
229
+ 'Set wall thickness value',
230
+ 'Preview shows hollowed geometry',
231
+ 'Click OK'
232
+ ],
233
+ time: '1 min'
234
+ },
235
+ fusion360: {
236
+ text: 'Select face(s) → DESIGN → Modify → Shell → Set thickness → OK',
237
+ steps: [
238
+ 'Select face(s) to remove',
239
+ 'Click Modify → Shell',
240
+ 'Set wall thickness',
241
+ 'Click OK'
242
+ ],
243
+ time: '1 min',
244
+ note: 'Available in Fusion 360 Free'
245
+ }
246
+ },
247
+ Boolean: {
248
+ cycleCAD: {
249
+ text: 'Select bodies → Click Boolean → Choose Union/Cut/Intersect → Click OK',
250
+ steps: [
251
+ 'Select first body (or ensure active)',
252
+ 'Select second body',
253
+ 'Click Boolean button',
254
+ 'Choose operation: Union, Cut, or Intersect',
255
+ 'Click OK'
256
+ ],
257
+ time: '1 min'
258
+ },
259
+ fusion360: {
260
+ text: 'Select bodies → DESIGN → Modify → Combine → Choose operation → OK',
261
+ steps: [
262
+ 'Select first body',
263
+ 'Click Modify → Combine',
264
+ 'Select second body',
265
+ 'Choose operation: Union, Cut, or Intersect',
266
+ 'Click OK'
267
+ ],
268
+ time: '1 min',
269
+ note: 'Available in Fusion 360 Free'
270
+ }
271
+ },
272
+ 'Sheet Metal Flange': {
273
+ cycleCAD: {
274
+ text: 'Switch to Sheet Metal workspace → Select edge → Create Flange → Set parameters',
275
+ steps: [
276
+ 'Switch to Sheet Metal workspace',
277
+ 'Select edge on sheet metal face',
278
+ 'Click Create Flange',
279
+ 'Set flange length and angle',
280
+ 'Click OK'
281
+ ],
282
+ time: '1-2 min'
283
+ },
284
+ fusion360: {
285
+ text: 'Switch to SHEET METAL workspace → SELECT → CREATE → Flange → Set parameters → OK',
286
+ steps: [
287
+ 'Switch to SHEET METAL workspace',
288
+ 'Select edge to flange from',
289
+ 'Click CREATE → Flange',
290
+ 'Set flange length, angle, and offset',
291
+ 'Click OK'
292
+ ],
293
+ time: '1-2 min',
294
+ note: 'Requires SHEET METAL workspace'
295
+ }
296
+ }
297
+ };
298
+
299
+ /**
300
+ * Generate reconstruction guide from parsed Inventor part data
301
+ * @param {Object} parsedPart - Output from inventor-parser.js
302
+ * @returns {Object} Guide with cycleCAD steps, Fusion360 steps, and HTML
303
+ */
304
+ export function generateGuide(parsedPart) {
305
+ const { fileName, partType, features = [], metadata = {} } = parsedPart;
306
+
307
+ const cycleCADSteps = [];
308
+ const fusion360Steps = [];
309
+
310
+ let stepNum = 1;
311
+
312
+ // Add intro step
313
+ cycleCADSteps.push({
314
+ num: stepNum,
315
+ type: 'Intro',
316
+ title: `Reconstruct ${fileName}`,
317
+ description: `Part type: ${partType || 'Solid Part'}`,
318
+ time: '< 1 min'
319
+ });
320
+
321
+ fusion360Steps.push({
322
+ num: stepNum,
323
+ type: 'Intro',
324
+ title: `Reconstruct ${fileName} in Fusion 360`,
325
+ description: `Part type: ${partType || 'Solid Part'} → Switch to DESIGN workspace`,
326
+ time: '< 1 min',
327
+ note: 'Free version available at fusion360.autodesk.com'
328
+ });
329
+
330
+ stepNum++;
331
+
332
+ // Process each feature
333
+ features.forEach((feature, idx) => {
334
+ const featureType = feature.type || 'Unknown';
335
+ const instructions = FEATURE_INSTRUCTIONS[featureType];
336
+
337
+ if (instructions) {
338
+ const ccInstructions = instructions.cycleCAD;
339
+ const f360Instructions = instructions.fusion360;
340
+
341
+ cycleCADSteps.push({
342
+ num: stepNum,
343
+ type: featureType,
344
+ icon: feature.icon || '⚙️',
345
+ title: `${stepNum - 1}. ${featureType}`,
346
+ description: ccInstructions.text,
347
+ detailedSteps: ccInstructions.steps,
348
+ time: ccInstructions.time,
349
+ note: ccInstructions.note
350
+ });
351
+
352
+ fusion360Steps.push({
353
+ num: stepNum,
354
+ type: featureType,
355
+ icon: feature.icon || '⚙️',
356
+ title: `${stepNum - 1}. ${featureType}`,
357
+ description: f360Instructions.text,
358
+ detailedSteps: f360Instructions.steps,
359
+ time: f360Instructions.time,
360
+ note: f360Instructions.note
361
+ });
362
+
363
+ stepNum++;
364
+ }
365
+ });
366
+
367
+ // Add final step
368
+ cycleCADSteps.push({
369
+ num: stepNum,
370
+ type: 'Complete',
371
+ title: `${stepNum - 1}. Save Your Model`,
372
+ description: 'File → Save (Ctrl+S) → Choose format (cycleCAD, STEP, IGES)',
373
+ time: '< 1 min'
374
+ });
375
+
376
+ fusion360Steps.push({
377
+ num: stepNum,
378
+ type: 'Complete',
379
+ title: `${stepNum - 1}. Save Your Model`,
380
+ description: 'File → Save → Enter project name → Finish',
381
+ time: '< 1 min',
382
+ note: 'Automatically saved to cloud'
383
+ });
384
+
385
+ const html = renderGuideHTML({
386
+ fileName,
387
+ partType,
388
+ cycleCADSteps,
389
+ fusion360Steps,
390
+ estimatedTime: calculateEstimatedTime(cycleCADSteps)
391
+ });
392
+
393
+ return {
394
+ cycleCAD: cycleCADSteps,
395
+ fusion360: fusion360Steps,
396
+ html,
397
+ fileName,
398
+ partType
399
+ };
400
+ }
401
+
402
+ /**
403
+ * Render guide into a container
404
+ * @param {HTMLElement} container - Target container
405
+ * @param {Object} guide - Generated guide object
406
+ */
407
+ export function renderGuide(container, guide) {
408
+ if (!container) {
409
+ console.error('Container element not found');
410
+ return;
411
+ }
412
+ container.innerHTML = guide.html;
413
+
414
+ // Attach tab switcher
415
+ const tabs = container.querySelectorAll('.guide-tab');
416
+ const panels = container.querySelectorAll('.guide-panel');
417
+
418
+ tabs.forEach(tab => {
419
+ tab.addEventListener('click', () => {
420
+ const tabName = tab.dataset.tab;
421
+
422
+ tabs.forEach(t => t.classList.remove('active'));
423
+ panels.forEach(p => p.classList.remove('active'));
424
+
425
+ tab.classList.add('active');
426
+ container.querySelector(`[data-panel="${tabName}"]`).classList.add('active');
427
+ });
428
+ });
429
+
430
+ // Attach export button
431
+ const exportBtn = container.querySelector('.guide-export-btn');
432
+ if (exportBtn) {
433
+ exportBtn.addEventListener('click', () => {
434
+ exportGuideHTML(guide, `${guide.fileName.split('.')[0]}-rebuild-guide.html`);
435
+ });
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Export guide as standalone HTML file
441
+ * @param {Object} guide - Generated guide object
442
+ * @param {String} fileName - Output filename
443
+ */
444
+ export function exportGuideHTML(guide, fileName) {
445
+ const template = `<!DOCTYPE html>
446
+ <html lang="en">
447
+ <head>
448
+ <meta charset="UTF-8">
449
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
450
+ <title>Rebuild Guide: ${guide.fileName}</title>
451
+ <style>
452
+ * { margin: 0; padding: 0; box-sizing: border-box; }
453
+ body {
454
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
455
+ background: #f5f5f5;
456
+ color: #333;
457
+ }
458
+ .guide-container {
459
+ max-width: 1200px;
460
+ margin: 20px auto;
461
+ background: white;
462
+ border-radius: 8px;
463
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
464
+ overflow: hidden;
465
+ }
466
+ .guide-header {
467
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
468
+ color: white;
469
+ padding: 30px;
470
+ }
471
+ .guide-header h1 { font-size: 28px; margin-bottom: 10px; }
472
+ .guide-header p { opacity: 0.9; }
473
+ .guide-meta {
474
+ display: flex;
475
+ gap: 20px;
476
+ margin-top: 15px;
477
+ font-size: 14px;
478
+ opacity: 0.8;
479
+ }
480
+ .guide-tabs {
481
+ display: flex;
482
+ border-bottom: 2px solid #eee;
483
+ background: #fafafa;
484
+ }
485
+ .guide-tab {
486
+ flex: 1;
487
+ padding: 15px;
488
+ text-align: center;
489
+ cursor: pointer;
490
+ font-weight: 500;
491
+ border-bottom: 3px solid transparent;
492
+ transition: all 0.3s;
493
+ }
494
+ .guide-tab:hover { background: #f0f0f0; }
495
+ .guide-tab.active {
496
+ border-bottom-color: #667eea;
497
+ color: #667eea;
498
+ background: white;
499
+ }
500
+ .guide-content {
501
+ padding: 30px;
502
+ display: flex;
503
+ gap: 30px;
504
+ }
505
+ .guide-panel {
506
+ display: none;
507
+ flex: 1;
508
+ }
509
+ .guide-panel.active { display: block; }
510
+ .guide-step {
511
+ margin-bottom: 25px;
512
+ padding: 15px;
513
+ background: #f9f9f9;
514
+ border-left: 4px solid #667eea;
515
+ border-radius: 4px;
516
+ }
517
+ .guide-step.intro, .guide-step.complete {
518
+ background: #f0f4ff;
519
+ border-left-color: #667eea;
520
+ }
521
+ .guide-step-header {
522
+ display: flex;
523
+ align-items: center;
524
+ gap: 10px;
525
+ margin-bottom: 10px;
526
+ }
527
+ .guide-step-icon {
528
+ font-size: 24px;
529
+ }
530
+ .guide-step-title {
531
+ font-size: 16px;
532
+ font-weight: 600;
533
+ color: #333;
534
+ }
535
+ .guide-step-time {
536
+ font-size: 12px;
537
+ color: #999;
538
+ margin-left: auto;
539
+ }
540
+ .guide-step-desc {
541
+ font-size: 14px;
542
+ color: #666;
543
+ margin: 10px 0;
544
+ line-height: 1.5;
545
+ }
546
+ .guide-step-substeps {
547
+ margin-top: 10px;
548
+ padding-left: 20px;
549
+ font-size: 13px;
550
+ color: #555;
551
+ }
552
+ .guide-step-substeps li {
553
+ list-style: decimal;
554
+ margin-bottom: 5px;
555
+ }
556
+ .guide-step-note {
557
+ margin-top: 10px;
558
+ padding: 8px 12px;
559
+ background: #fffacd;
560
+ border-left: 3px solid #ffd700;
561
+ font-size: 12px;
562
+ color: #333;
563
+ border-radius: 3px;
564
+ }
565
+ .guide-footer {
566
+ padding: 20px 30px;
567
+ background: #fafafa;
568
+ border-top: 1px solid #eee;
569
+ text-align: center;
570
+ font-size: 12px;
571
+ color: #999;
572
+ }
573
+ .guide-export-btn {
574
+ display: inline-block;
575
+ padding: 10px 20px;
576
+ background: #667eea;
577
+ color: white;
578
+ border: none;
579
+ border-radius: 4px;
580
+ cursor: pointer;
581
+ font-weight: 500;
582
+ margin-top: 10px;
583
+ }
584
+ .guide-export-btn:hover {
585
+ background: #764ba2;
586
+ }
587
+ @media (max-width: 768px) {
588
+ .guide-content {
589
+ flex-direction: column;
590
+ gap: 0;
591
+ }
592
+ .guide-tabs {
593
+ flex-direction: column;
594
+ }
595
+ }
596
+ </style>
597
+ </head>
598
+ <body>
599
+ <div class="guide-container">
600
+ <div class="guide-header">
601
+ <h1>🔧 Rebuild Guide</h1>
602
+ <p>${guide.fileName}</p>
603
+ <div class="guide-meta">
604
+ <span>📋 Part Type: ${guide.partType || 'Solid Part'}</span>
605
+ <span>⏱️ Estimated Time: ${calculateEstimatedTime(guide.cycleCAD)}</span>
606
+ </div>
607
+ </div>
608
+
609
+ <div class="guide-tabs">
610
+ <div class="guide-tab active" data-tab="cyclecad">cycleCAD</div>
611
+ <div class="guide-tab" data-tab="fusion360">Fusion 360 Free</div>
612
+ </div>
613
+
614
+ <div class="guide-content">
615
+ <div class="guide-panel active" data-panel="cyclecad">
616
+ ${guide.cycleCAD.map(step => renderStep(step)).join('')}
617
+ </div>
618
+ <div class="guide-panel" data-panel="fusion360">
619
+ ${guide.fusion360.map(step => renderStep(step)).join('')}
620
+ </div>
621
+ </div>
622
+
623
+ <div class="guide-footer">
624
+ <p>Generated by cycleCAD Rebuild Guide • ${new Date().toLocaleDateString()}</p>
625
+ </div>
626
+ </div>
627
+
628
+ <script>
629
+ document.querySelectorAll('.guide-tab').forEach(tab => {
630
+ tab.addEventListener('click', function() {
631
+ const tabName = this.dataset.tab;
632
+ document.querySelectorAll('.guide-tab').forEach(t => t.classList.remove('active'));
633
+ document.querySelectorAll('.guide-panel').forEach(p => p.classList.remove('active'));
634
+ this.classList.add('active');
635
+ document.querySelector('[data-panel="' + tabName + '"]').classList.add('active');
636
+ });
637
+ });
638
+ </script>
639
+ </body>
640
+ </html>`;
641
+
642
+ const blob = new Blob([template], { type: 'text/html' });
643
+ const url = URL.createObjectURL(blob);
644
+ const link = document.createElement('a');
645
+ link.href = url;
646
+ link.download = fileName;
647
+ link.click();
648
+ URL.revokeObjectURL(url);
649
+ }
650
+
651
+ // Helper functions
652
+
653
+ /**
654
+ * Render step HTML
655
+ */
656
+ function renderStep(step) {
657
+ const substeps = step.detailedSteps
658
+ ? `<ol class="guide-step-substeps">${step.detailedSteps.map(s => `<li>${s}</li>`).join('')}</ol>`
659
+ : '';
660
+
661
+ const note = step.note ? `<div class="guide-step-note">💡 ${step.note}</div>` : '';
662
+
663
+ return `
664
+ <div class="guide-step ${step.type.toLowerCase()}">
665
+ <div class="guide-step-header">
666
+ <span class="guide-step-icon">${step.icon || '⚙️'}</span>
667
+ <span class="guide-step-title">${step.title}</span>
668
+ <span class="guide-step-time">${step.time || ''}</span>
669
+ </div>
670
+ <div class="guide-step-desc">${step.description}</div>
671
+ ${substeps}
672
+ ${note}
673
+ </div>
674
+ `;
675
+ }
676
+
677
+ /**
678
+ * Render complete guide HTML
679
+ */
680
+ function renderGuideHTML(data) {
681
+ const cyclecadPanel = data.cycleCADSteps.map(step => renderStep(step)).join('');
682
+ const fusion360Panel = data.fusion360Steps.map(step => renderStep(step)).join('');
683
+
684
+ return `
685
+ <div class="guide-wrapper" style="max-width: 1200px; margin: 0 auto; padding: 20px;">
686
+ <div class="guide-header" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 8px; margin-bottom: 20px;">
687
+ <h1>🔧 ${data.fileName} - Rebuild Guide</h1>
688
+ <p>Part Type: ${data.partType || 'Solid Part'} | Estimated Time: ${data.estimatedTime}</p>
689
+ </div>
690
+
691
+ <div style="display: flex; gap: 20px; margin-bottom: 20px; border-bottom: 2px solid #eee;">
692
+ <button class="guide-tab active" data-tab="cyclecad" style="flex: 1; padding: 15px; background: none; border: none; border-bottom: 3px solid #667eea; cursor: pointer; font-weight: 500;">cycleCAD</button>
693
+ <button class="guide-tab" data-tab="fusion360" style="flex: 1; padding: 15px; background: none; border: none; border-bottom: 3px solid transparent; cursor: pointer; font-weight: 500;">Fusion 360 Free</button>
694
+ </div>
695
+
696
+ <div class="guide-panel active" data-panel="cyclecad" style="display: block;">
697
+ ${cyclecadPanel}
698
+ </div>
699
+
700
+ <div class="guide-panel" data-panel="fusion360" style="display: none;">
701
+ ${fusion360Panel}
702
+ </div>
703
+
704
+ <div style="margin-top: 30px; text-align: center;">
705
+ <button class="guide-export-btn" style="padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500;">📥 Download as HTML</button>
706
+ </div>
707
+ </div>
708
+ `;
709
+ }
710
+
711
+ /**
712
+ * Calculate estimated time for all steps
713
+ */
714
+ function calculateEstimatedTime(steps) {
715
+ const timePattern = /(\d+)-?(\d*)?\s*(?:min|hr)/i;
716
+ let totalMinutes = 0;
717
+
718
+ steps.forEach(step => {
719
+ if (step.time) {
720
+ const match = step.time.match(timePattern);
721
+ if (match) {
722
+ const min = parseInt(match[1]);
723
+ const max = match[2] ? parseInt(match[2]) : min;
724
+ totalMinutes += (min + max) / 2;
725
+ }
726
+ }
727
+ });
728
+
729
+ if (totalMinutes < 60) {
730
+ return `~${Math.ceil(totalMinutes)} min`;
731
+ } else {
732
+ const hours = Math.floor(totalMinutes / 60);
733
+ const mins = totalMinutes % 60;
734
+ return mins > 0 ? `~${hours}h ${mins}m` : `~${hours}h`;
735
+ }
736
+ }
737
+
738
+ export default {
739
+ generateGuide,
740
+ renderGuide,
741
+ exportGuideHTML,
742
+ FEATURE_INSTRUCTIONS
743
+ };