cyclecad 3.12.0 → 3.13.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.
@@ -452,6 +452,193 @@
452
452
  };
453
453
  }
454
454
 
455
+ // ===================================================================
456
+ // ROLLING-ELEMENT BEARING LIFE — ISO 281 / Shigley Ch. 11
457
+ // ===================================================================
458
+ // L_10 = (C / P)^a × 10^6 rev
459
+ // a = 3 for ball bearings, 10/3 for roller bearings.
460
+ // C = basic dynamic load rating (N), bearing-specific.
461
+ // P = equivalent dynamic load (N), P = X·F_r + Y·F_a for combined loading.
462
+ // L_h = L_10 / (60 × N) in hours at N rpm.
463
+ //
464
+ // Reference dynamic load ratings C (N) for deep-groove ball bearings (SKF catalogue).
465
+ const BEARING_CATALOGUE = Object.freeze({
466
+ '608': { type: 'ball', d: 8, D: 22, B: 7, C: 3.45e3 },
467
+ '625': { type: 'ball', d: 5, D: 16, B: 5, C: 1.85e3 },
468
+ '6200': { type: 'ball', d: 10, D: 30, B: 9, C: 5.4e3 },
469
+ '6201': { type: 'ball', d: 12, D: 32, B: 10, C: 6.89e3 },
470
+ '6202': { type: 'ball', d: 15, D: 35, B: 11, C: 7.8e3 },
471
+ '6203': { type: 'ball', d: 17, D: 40, B: 12, C: 9.56e3 },
472
+ '6204': { type: 'ball', d: 20, D: 47, B: 14, C: 13.5e3 },
473
+ '6205': { type: 'ball', d: 25, D: 52, B: 15, C: 14.0e3 },
474
+ '6206': { type: 'ball', d: 30, D: 62, B: 16, C: 19.5e3 },
475
+ '6300': { type: 'ball', d: 10, D: 35, B: 11, C: 8.06e3 },
476
+ '6302': { type: 'ball', d: 15, D: 42, B: 13, C: 11.9e3 },
477
+ '6304': { type: 'ball', d: 20, D: 52, B: 15, C: 15.9e3 },
478
+ 'NJ204':{ type: 'roller', d: 20, D: 47, B: 14, C: 25.1e3 },
479
+ 'NJ206':{ type: 'roller', d: 30, D: 62, B: 16, C: 44e3 }
480
+ });
481
+
482
+ /**
483
+ * Rolling-element bearing L10 life analysis.
484
+ *
485
+ * Supports two input modes:
486
+ * (a) Designation ("6204") — looks up C from catalogue.
487
+ * (b) Explicit C override (N).
488
+ *
489
+ * Applies X/Y factors per ISO 281 to compute equivalent dynamic load P from radial F_r
490
+ * and axial F_a forces. Falls back to pure-radial (P = F_r) if F_a not given.
491
+ *
492
+ * @param {object} p
493
+ * @param {string} [p.designation] Bearing code, e.g. '6204'.
494
+ * @param {number} [p.C] Basic dynamic load rating override in N.
495
+ * @param {'ball'|'roller'} [p.type] Exponent selector (default 'ball').
496
+ * @param {number} p.radialLoad F_r in N.
497
+ * @param {number} [p.axialLoad=0] F_a in N.
498
+ * @param {number} p.rpm Rotational speed N (rev/min).
499
+ * @param {number} [p.X=1] Radial factor (0.56 for F_a/F_r > e; use default for pure radial).
500
+ * @param {number} [p.Y=0] Thrust factor.
501
+ * @param {number} [p.reliability=0.90] Life adjustment reliability (0.90 ≡ L10).
502
+ * @returns {object}
503
+ */
504
+ function bearingLifeAnalysis(p) {
505
+ const catalog = p.designation ? BEARING_CATALOGUE[String(p.designation)] : null;
506
+ const type = (p.type || (catalog && catalog.type) || 'ball');
507
+ const a = type === 'roller' ? 10/3 : 3;
508
+ const C = Number(p.C) || (catalog ? catalog.C : 0);
509
+ const F_r = Math.max(0, Number(p.radialLoad) || 0);
510
+ const F_a = Math.max(0, Number(p.axialLoad) || 0);
511
+ const rpm = Math.max(1, Number(p.rpm) || 1);
512
+ const X = Number.isFinite(p.X) ? p.X : (F_a > 0 ? 0.56 : 1);
513
+ const Y = Number.isFinite(p.Y) ? p.Y : (F_a > 0 ? 1.2 : 0);
514
+ const R = Math.max(0.5, Math.min(0.999, Number(p.reliability) || 0.9));
515
+
516
+ if (!C || C <= 0) {
517
+ return {
518
+ inputs: { designation: p.designation, type, C: 0, F_r, F_a, rpm },
519
+ error: 'Missing or invalid dynamic load rating C — provide p.C in N or a recognised p.designation.',
520
+ verdict: 'INPUT ERROR', verdictClass: 'fail'
521
+ };
522
+ }
523
+
524
+ // Equivalent dynamic load P (N)
525
+ const P = X * F_r + Y * F_a;
526
+ // L10 life (millions of revolutions)
527
+ const L10_rev = Math.pow(C / Math.max(1, P), a); // in 10^6 rev
528
+ // Convert to hours at rpm
529
+ const L10_h = (L10_rev * 1e6) / (60 * rpm);
530
+ // Life adjustment for reliability — Shigley Eq 11-12 (Weibull): L_R = L10 × (ln(1/R) / ln(1/0.9))^(1/1.483)
531
+ const a_R = Math.pow(Math.log(1/R) / Math.log(1/0.9), 1/1.483);
532
+ const L_R_rev = L10_rev * a_R;
533
+ const L_R_h = L10_h * a_R;
534
+
535
+ // Verdict benchmarks (hours):
536
+ // • < 5 000 h → short life
537
+ // • 5 000–20 000 h → typical industrial
538
+ // • > 20 000 h → long life
539
+ let verdict, verdictClass;
540
+ if (L_R_h >= 20000) { verdict = 'LONG LIFE (> 20,000 h)'; verdictClass = 'pass'; }
541
+ else if (L_R_h >= 5000) { verdict = 'TYPICAL INDUSTRIAL (5k–20k h)'; verdictClass = 'pass'; }
542
+ else if (L_R_h >= 1000) { verdict = 'SHORT LIFE (< 5,000 h) — review'; verdictClass = 'warn'; }
543
+ else { verdict = 'VERY SHORT LIFE — resize bearing up'; verdictClass = 'fail'; }
544
+ const notes = [];
545
+ if (F_a > F_r && Y === 0) notes.push('Axial load ignored — explicit X/Y factors needed for thrust-dominated duty.');
546
+ if (P > C * 0.5) notes.push('Load exceeds 50% of C — consider a larger bearing to extend life dramatically.');
547
+
548
+ return {
549
+ inputs: { designation: p.designation, type, a, C, F_r, F_a, rpm, X, Y, reliability: R, catalog },
550
+ P,
551
+ L10_rev, L10_h,
552
+ L_R_rev, L_R_h, a_R,
553
+ verdict, verdictClass, notes
554
+ };
555
+ }
556
+
557
+ // ===================================================================
558
+ // FILLET WELD — AWS D1.1 / Shigley Ch. 9 throat-stress analysis
559
+ // ===================================================================
560
+ // Throat thickness t = 0.707 · h (for 45° equal-leg fillet).
561
+ // Direct throat stress: τ = F / (t · L) for load perpendicular to weld line
562
+ // Shear + bending: combined per load case; here we expose two modes:
563
+ // 'transverse' (load pulls perpendicular to weld):
564
+ // σ = F / (0.707 · h · L) with a 1/√2 conversion for nominal direct stress
565
+ // 'longitudinal' (load parallel to weld — pure shear):
566
+ // τ = F / (0.707 · h · L)
567
+ // Allowable = 0.30 · S_ut_electrode (AWS D1.1 static).
568
+ // For cyclic loading: fatigue factor ~0.5 of static allowable (conservative).
569
+ //
570
+ // Reference electrode ultimate tensile strengths (MPa, converted from AWS E-class ksi):
571
+ const WELD_ELECTRODES = Object.freeze({
572
+ 'E60': { S_ut: 414, label: 'E60 (60 ksi UTS)', standardClass: 'AWS D1.1 Table 2.5' },
573
+ 'E70': { S_ut: 482, label: 'E70 (70 ksi UTS — most common)', standardClass: 'AWS D1.1 Table 2.5' },
574
+ 'E80': { S_ut: 552, label: 'E80 (80 ksi UTS)', standardClass: 'AWS D1.1 Table 2.5' },
575
+ 'E90': { S_ut: 620, label: 'E90 (90 ksi UTS)', standardClass: 'AWS D1.1 Table 2.5' },
576
+ 'E100': { S_ut: 689, label: 'E100 (100 ksi UTS)', standardClass: 'AWS D1.1 Table 2.5' },
577
+ 'E110': { S_ut: 758, label: 'E110 (110 ksi UTS)', standardClass: 'AWS D1.1 Table 2.5' }
578
+ });
579
+
580
+ /**
581
+ * Fillet weld throat-stress analysis (AWS D1.1 static + optional cyclic derating).
582
+ *
583
+ * @param {object} p
584
+ * @param {number} p.legSize h — fillet leg size in mm.
585
+ * @param {number} p.length L — total weld length in mm (sum of all fillet segments carrying load).
586
+ * @param {number} p.force F — applied load in N.
587
+ * @param {'transverse'|'longitudinal'|'combined'} [p.loadDirection='transverse'] — Direction relative to weld line.
588
+ * @param {string} [p.electrode='E70'] — AWS electrode class (E60/E70/E80/E90/E100/E110).
589
+ * @param {boolean} [p.cyclic=false] — Apply 0.5× fatigue derate to the allowable.
590
+ * @param {number} [p.safetyFactor=1.0] — Additional user-specified SF on top of allowable.
591
+ * @returns {object}
592
+ */
593
+ function filletWeldAnalysis(p) {
594
+ const h = Math.max(1, Number(p.legSize) || 0);
595
+ const L = Math.max(1, Number(p.length) || 0);
596
+ const F = Math.max(0, Number(p.force) || 0);
597
+ const direction = (p.loadDirection || 'transverse').toLowerCase();
598
+ const elecKey = p.electrode || 'E70';
599
+ const elec = WELD_ELECTRODES[elecKey] || WELD_ELECTRODES.E70;
600
+ const cyclic = !!p.cyclic;
601
+ const sf = Math.max(1, Number(p.safetyFactor) || 1.0);
602
+
603
+ // Throat area (mm²)
604
+ const t = 0.707 * h;
605
+ const A = t * L;
606
+ // Throat stress (MPa = N/mm²). For combined, use resultant of longitudinal shear + transverse bending.
607
+ // For this v1 treat 'transverse' and 'longitudinal' identically (same throat area);
608
+ // the nominal allowable per AWS is direction-independent for static design.
609
+ const tau = A > 0 ? F / A : 0;
610
+
611
+ // Allowable strength — AWS D1.1 static allowable is 0.30 × S_ut_electrode for fillet welds in shear.
612
+ let allowable = 0.30 * elec.S_ut;
613
+ if (cyclic) allowable *= 0.5; // fatigue derate (generic conservative)
614
+
615
+ const capacity = allowable * A; // N
616
+ const utilisation = tau / allowable; // 0..1 ideally
617
+ const safetyFactor = allowable / (tau * sf);
618
+
619
+ let verdict, verdictClass;
620
+ if (safetyFactor >= 2.0) { verdict = 'SAFE (margin ≥ 2)'; verdictClass = 'pass'; }
621
+ else if (safetyFactor >= 1.5) { verdict = 'SAFE (industry typical — margin ≥ 1.5)'; verdictClass = 'pass'; }
622
+ else if (safetyFactor >= 1.0) { verdict = 'MARGINAL (margin < 1.5)'; verdictClass = 'warn'; }
623
+ else { verdict = 'UNSAFE (weld will fail at design load)'; verdictClass = 'fail'; }
624
+ const notes = [];
625
+ if (cyclic) notes.push('Cyclic/fatigue derate applied — allowable reduced to 0.15 × S_ut (50% of static).');
626
+ if (direction === 'combined') notes.push('Combined direction treated as the more conservative of transverse/longitudinal for v1.');
627
+ if (utilisation > 0.9) notes.push('Utilisation > 90% — consider increasing leg size (h) to restore margin quickly; strength scales linearly with h.');
628
+
629
+ return {
630
+ inputs: { h, L, F, direction, electrode: elecKey, electrode_label: elec.label, S_ut_electrode: elec.S_ut,
631
+ cyclic, safetyFactor_user: sf },
632
+ throat: { t, A },
633
+ stress: { tau },
634
+ allowable,
635
+ capacity,
636
+ utilisation,
637
+ safetyFactor,
638
+ verdict, verdictClass, notes
639
+ };
640
+ }
641
+
455
642
  // ===================================================================
456
643
  // UNIT TESTS — verify core against MecAgent screenshot values
457
644
  // ===================================================================
@@ -518,6 +705,39 @@
518
705
  test('shaft n_Goodman finite', Number.isFinite(sh.n_Goodman) && sh.n_Goodman > 0 ? 1 : 0, 1, 0);
519
706
  test('shaft n_Soderberg ≤ Goodman', (sh.n_Soderberg <= sh.n_Goodman + 1e-6) ? 1 : 0, 1, 0);
520
707
 
708
+ // ------- BEARING TEST — 6204 @ 1800 rpm, 4 kN radial -------
709
+ // C = 13500 N, P = F_r = 4000 N, a = 3
710
+ // L10 = (13500/4000)^3 = 38.44 × 10^6 rev
711
+ // L10_h = 38.44e6 / (60·1800) = 355.9 hours
712
+ const b = bearingLifeAnalysis({
713
+ designation: '6204', radialLoad: 4000, rpm: 1800
714
+ });
715
+ test('bearing C from catalog', b.inputs.C, 13500, 10);
716
+ test('bearing exponent a', b.inputs.a, 3, 0);
717
+ test('bearing L10 (rev × 10^6)', b.L10_rev, 38.44, 0.5);
718
+ test('bearing L10 (hours)', b.L10_h, 355.9, 2);
719
+ test('bearing R90 ≡ L10', b.a_R, 1.0, 0.01);
720
+
721
+ // ------- WELD TEST — E70 fillet, h=6mm, L=120mm, 40 kN transverse -------
722
+ // A = 0.707·6·120 = 509.0 mm²
723
+ // τ = 40000 / 509.0 = 78.6 MPa
724
+ // allowable static = 0.30 × 482 = 144.6 MPa
725
+ // SF = 144.6 / 78.6 = 1.84
726
+ const w = filletWeldAnalysis({
727
+ legSize: 6, length: 120, force: 40000,
728
+ loadDirection: 'transverse', electrode: 'E70'
729
+ });
730
+ test('weld throat area', w.throat.A, 509.04, 0.5);
731
+ test('weld τ', w.stress.tau, 78.58, 0.5);
732
+ test('weld allowable E70', w.allowable, 144.6, 0.5);
733
+ test('weld SF', w.safetyFactor, 1.84, 0.1);
734
+ // Cyclic derate case — same loading, cyclic enabled → SF halves
735
+ const wc = filletWeldAnalysis({
736
+ legSize: 6, length: 120, force: 40000,
737
+ loadDirection: 'transverse', electrode: 'E70', cyclic: true
738
+ });
739
+ test('weld cyclic SF ≈ 0.92', wc.safetyFactor, 0.92, 0.05);
740
+
521
741
  return { results, allPass: results.every(r => r.pass) };
522
742
  }
523
743
 
@@ -618,12 +838,12 @@
618
838
  }
619
839
 
620
840
  // ===================================================================
621
- // UI — form-based input with live recompute + formatted report
841
+ // UI — form-based input with tab switcher (bolted-joint / gears / shafts / bearings / welds)
622
842
  // ===================================================================
623
- const S = { lastResult: null, els: {} };
843
+ const S = { lastResult: null, els: {}, currentTab: 'bolt' };
624
844
 
625
- const INPUT_FIELDS = [
626
- // [key, label, unit, default, min]
845
+ // Field schemas per analysis kind. Shape: [key, label, unit, default, min, type?, opts?]
846
+ const FIELDS_BOLT = [
627
847
  ['boltCount', 'Bolt count (z)', '', 4, 1],
628
848
  ['thread', 'Thread', '', 'M12', null, 'select', Object.keys(BOLT_STRESS_AREA)],
629
849
  ['grade', 'Grade (ISO 898-1)', '', '8.8', null, 'select', Object.keys(STEEL_GRADES)],
@@ -636,6 +856,84 @@
636
856
  ['safetyFactor', 'Slip safety factor (K_s)', '', 1.5, 1],
637
857
  ['frictionInterfaces','Friction interfaces (n)', '', 1, 1]
638
858
  ];
859
+ const FIELDS_GEAR = [
860
+ ['pinionTeeth', 'Pinion teeth z_P', '', 20, 12],
861
+ ['gearTeeth', 'Gear teeth z_G', '', 40, 12],
862
+ ['module', 'Module m', 'mm', 2, 0.5],
863
+ ['faceWidth', 'Face width F', 'mm', 25, 3],
864
+ ['torque', 'Pinion torque T_P', 'N·m', 10, 0],
865
+ ['pinionHB', 'Pinion hardness', 'HB', 240, 150],
866
+ ['gearHB', 'Gear hardness', 'HB', 240, 150],
867
+ ['overload', 'Overload K_o', '', 1.0, 1],
868
+ ['dynamic', 'Dynamic K_v', '', 1.1, 1],
869
+ ['loadDist', 'Load distribution K_m', '', 1.3, 1],
870
+ ['reliability', 'Reliability K_R', '', 1.0, 1],
871
+ ['pressureAngle','Pressure angle φ', '°', 20, 0]
872
+ ];
873
+ const FIELDS_SHAFT = [
874
+ ['material', 'Material', '', '1050_cd', null, 'select', Object.keys(SHAFT_MATERIALS)],
875
+ ['diameter', 'Diameter d', 'mm', 25, 5],
876
+ ['M_a', 'Alt. bending moment M_a', 'N·m', 100, 0],
877
+ ['M_m', 'Mean bending moment M_m', 'N·m', 0, 0],
878
+ ['T_a', 'Alt. torque T_a', 'N·m', 0, 0],
879
+ ['T_m', 'Mean torque T_m', 'N·m', 50, 0],
880
+ ['Kf', 'Fatigue K_f (bending)', '', 2.0, 1],
881
+ ['Kfs', 'Fatigue K_fs (torsion)', '', 1.5, 1],
882
+ ['surface', 'Surface finish', '', 'machined', null, 'select', Object.keys(SURFACE_FACTORS)],
883
+ ['reliability', 'Reliability R', '', 0.99, 0.5],
884
+ ['temperatureC', 'Temperature', '°C', 25, -50]
885
+ ];
886
+ const FIELDS_BEARING = [
887
+ ['designation', 'Designation', '', '6204', null, 'select', Object.keys(BEARING_CATALOGUE)],
888
+ ['radialLoad', 'Radial load F_r', 'N', 4000, 0],
889
+ ['axialLoad', 'Axial load F_a', 'N', 0, 0],
890
+ ['rpm', 'Speed', 'rpm', 1800, 1],
891
+ ['X', 'Radial factor X', '', 1, 0],
892
+ ['Y', 'Thrust factor Y', '', 0, 0],
893
+ ['reliability', 'Reliability', '', 0.9, 0.5]
894
+ ];
895
+ const FIELDS_WELD = [
896
+ ['legSize', 'Leg size h', 'mm', 6, 1],
897
+ ['length', 'Weld length L', 'mm', 120, 1],
898
+ ['force', 'Applied load F', 'N', 40000,0],
899
+ ['loadDirection', 'Direction', '', 'transverse', null, 'select', ['transverse','longitudinal','combined']],
900
+ ['electrode', 'Electrode', '', 'E70', null, 'select', Object.keys(WELD_ELECTRODES)],
901
+ ['cyclic', 'Cyclic? (1 = yes)', '', 0, 0],
902
+ ['safetyFactor', 'User SF multiplier', '', 1.0, 1]
903
+ ];
904
+
905
+ const PRESETS_BOLT = [
906
+ { label: 'MecAgent demo', values: { boltCount:4, thread:'M12', grade:'10.9', preload:39000, shearForce:18000, axialForce:18000, moment:420000, bcd:96, friction:0.16, safetyFactor:1.5 } },
907
+ { label: 'Flange M8 light', values: { boltCount:8, thread:'M8', grade:'8.8', preload:15000, shearForce:5000, axialForce:2000, moment:50000, bcd:60, friction:0.15, safetyFactor:1.25 } },
908
+ { label: 'Heavy M20', values: { boltCount:6, thread:'M20', grade:'8.8', preload:120000, shearForce:30000, axialForce:25000, moment:800000, bcd:200, friction:0.14, safetyFactor:1.5 } }
909
+ ];
910
+ const PRESETS_GEAR = [
911
+ { label: 'Shigley Ex 14-5', values: { pinionTeeth:17, gearTeeth:52, module:2, faceWidth:30, torque:13.26, pinionHB:240, gearHB:240, overload:1, dynamic:1, loadDist:1.3 } },
912
+ { label: 'Light duty', values: { pinionTeeth:25, gearTeeth:75, module:1.5, faceWidth:18, torque:5, pinionHB:220, gearHB:220 } },
913
+ { label: 'Heavy industrial',values: { pinionTeeth:20, gearTeeth:60, module:5, faceWidth:50, torque:250, pinionHB:320, gearHB:310, overload:1.25, loadDist:1.4 } }
914
+ ];
915
+ const PRESETS_SHAFT = [
916
+ { label: 'Rotating-bending', values: { material:'1050_cd', diameter:25, M_a:100, M_m:0, T_a:0, T_m:50, Kf:2, Kfs:1.5, surface:'machined' } },
917
+ { label: 'Output shaft 4340',values: { material:'4340_Q&T', diameter:40, M_a:300, M_m:100, T_a:50, T_m:200, Kf:1.8, Kfs:1.3, surface:'ground' } }
918
+ ];
919
+ const PRESETS_BEARING = [
920
+ { label: '6204 / 4kN', values: { designation:'6204', radialLoad:4000, rpm:1800 } },
921
+ { label: '6206 / 8kN', values: { designation:'6206', radialLoad:8000, rpm:1500 } },
922
+ { label: 'NJ204 roller', values: { designation:'NJ204', radialLoad:10000, rpm:1200 } }
923
+ ];
924
+ const PRESETS_WELD = [
925
+ { label: 'E70 6mm × 120mm', values: { legSize:6, length:120, force:40000, electrode:'E70', loadDirection:'transverse' } },
926
+ { label: 'Heavy bracket', values: { legSize:10, length:200, force:120000, electrode:'E80', loadDirection:'transverse' } },
927
+ { label: 'Cyclic tie-down', values: { legSize:5, length:80, force:15000, electrode:'E70', loadDirection:'longitudinal', cyclic:1 } }
928
+ ];
929
+
930
+ const TABS = Object.freeze({
931
+ bolt: { label: 'Bolted Joint', subtitle: 'VDI 2230 / Shigley', fields: FIELDS_BOLT, presets: PRESETS_BOLT, analyze: boltedJointAnalysis, hasPrompt: true, kind: 'bolt' },
932
+ gear: { label: 'Spur Gears', subtitle: 'AGMA bending + pitting (Shigley Ch 14)', fields: FIELDS_GEAR, presets: PRESETS_GEAR, analyze: spurGearAnalysis, hasPrompt: false, kind: 'gear' },
933
+ shaft: { label: 'Shaft Fatigue', subtitle: 'Goodman / Soderberg (Shigley Ch 7)', fields: FIELDS_SHAFT, presets: PRESETS_SHAFT, analyze: shaftFatigueAnalysis,hasPrompt: false, kind: 'shaft' },
934
+ bearing: { label: 'Bearing Life', subtitle: 'L_10 (ISO 281) — Shigley Ch 11', fields: FIELDS_BEARING, presets: PRESETS_BEARING, analyze: bearingLifeAnalysis, hasPrompt: false, kind: 'bearing' },
935
+ weld: { label: 'Fillet Welds', subtitle: 'Throat stress (AWS D1.1)', fields: FIELDS_WELD, presets: PRESETS_WELD, analyze: filletWeldAnalysis, hasPrompt: false, kind: 'weld' }
936
+ });
639
937
 
640
938
  function fmt(n, unit, digits) {
641
939
  if (!isFinite(n)) return '—';
@@ -644,27 +942,205 @@
644
942
  return unit ? s + ' ' + unit : s;
645
943
  }
646
944
 
945
+ function currentFields() { return (TABS[S.currentTab] || TABS.bolt).fields; }
946
+ function currentAnalyze() { return (TABS[S.currentTab] || TABS.bolt).analyze; }
947
+
647
948
  function collectInputs() {
648
949
  const out = {};
649
- INPUT_FIELDS.forEach(([key,,,,, type]) => {
950
+ currentFields().forEach(([key,,,,, type]) => {
650
951
  const el = S.els['in_' + key];
651
952
  if (!el) return;
652
- out[key] = (type === 'select') ? el.value : parseFloat(el.value);
953
+ if (type === 'select') {
954
+ out[key] = el.value;
955
+ } else {
956
+ // Boolean fields encoded as 0/1 numeric — map back to boolean where the function expects it
957
+ out[key] = parseFloat(el.value);
958
+ }
653
959
  });
960
+ // Weld's cyclic is encoded as 0/1 — convert to boolean
961
+ if (S.currentTab === 'weld' && 'cyclic' in out) out.cyclic = !!out.cyclic;
654
962
  return out;
655
963
  }
656
964
 
657
965
  function compute() {
658
966
  try {
659
967
  const params = collectInputs();
660
- const r = boltedJointAnalysis(params);
968
+ const analyze = currentAnalyze();
969
+ const r = analyze(params);
661
970
  S.lastResult = r;
662
- renderReport(r);
663
- } catch(e) {
971
+ if (S.currentTab === 'bolt') {
972
+ renderReport(r); // Full KaTeX-rich bolted-joint report
973
+ } else {
974
+ renderReportGeneric(r, S.currentTab);
975
+ }
976
+ } catch (e) {
664
977
  if (S.els.report) S.els.report.innerHTML = '<div class="aie-err">Error: ' + e.message + '</div>';
665
978
  }
666
979
  }
667
980
 
981
+ /**
982
+ * Generic result renderer for v2 tabs (gear / shaft / bearing / weld).
983
+ * Outputs: verdict banner + key numbers table + notes list.
984
+ * KaTeX-rendered formulas can be added per-tab in a future pass.
985
+ * @param {object} r Result object from the relevant analyze function.
986
+ * @param {string} kind Tab key — 'gear' | 'shaft' | 'bearing' | 'weld'.
987
+ */
988
+ function renderReportGeneric(r, kind) {
989
+ const root = S.els.report;
990
+ if (!root) return;
991
+ root.innerHTML = '';
992
+ if (r.error) {
993
+ root.innerHTML = '<div class="aie-err" style="padding:10px;background:#7f1d1d;color:#fecaca;border-radius:6px;font-size:13px">' + r.error + '</div>';
994
+ return;
995
+ }
996
+
997
+ // Verdict banner
998
+ const vcls = r.verdictClass || 'pass';
999
+ const bg = vcls === 'pass' ? '#065f46' : vcls === 'warn' ? '#78350f' : '#7f1d1d';
1000
+ const fg = vcls === 'pass' ? '#d1fae5' : vcls === 'warn' ? '#fed7aa' : '#fecaca';
1001
+ const banner = document.createElement('div');
1002
+ banner.style.cssText = 'padding:10px 14px;border-radius:6px;background:'+bg+';color:'+fg+';font-weight:600;font-size:14px;margin-bottom:12px;border:1px solid rgba(255,255,255,0.08)';
1003
+ banner.textContent = 'Verdict: ' + r.verdict;
1004
+ root.appendChild(banner);
1005
+
1006
+ // Notes
1007
+ if (r.notes && r.notes.length) {
1008
+ const ul = document.createElement('ul');
1009
+ ul.style.cssText = 'margin:4px 0 14px 18px;font-size:12px;color:#94a3b8';
1010
+ r.notes.forEach(n => { const li = document.createElement('li'); li.textContent = n; ul.appendChild(li); });
1011
+ root.appendChild(ul);
1012
+ }
1013
+
1014
+ // Kind-specific body
1015
+ const body = document.createElement('div');
1016
+ body.style.cssText = 'padding:10px 12px;background:#0f172a;border:1px solid #334155;border-radius:6px;font-size:13px;color:#cbd5e1;line-height:1.8';
1017
+
1018
+ if (kind === 'gear') {
1019
+ body.innerHTML =
1020
+ '<strong style="color:#f1f5f9">Geometry</strong><br>' +
1021
+ 'Pitch Ø pinion: ' + fmt(r.inputs.d_P, 'mm') + ' · Pitch Ø gear: ' + fmt(r.inputs.d_G, 'mm') +
1022
+ ' · Ratio m_G: ' + fmt(r.inputs.mG) + '<br>' +
1023
+ 'Tangential load W_t: ' + fmt(r.inputs.W_t, 'N') + '<br><br>' +
1024
+ '<strong style="color:#f1f5f9">Safety factors</strong><br>' +
1025
+ '<span style="color:#94a3b8">Pinion:</span> SF_bending = <strong>' + fmt(r.pinion.SF_bending) + '</strong> · SF_contact = <strong>' + fmt(r.pinion.SF_contact) + '</strong><br>' +
1026
+ '<span style="color:#94a3b8">Gear:</span> SF_bending = <strong>' + fmt(r.gear.SF_bending) + '</strong> · SF_contact = <strong>' + fmt(r.gear.SF_contact) + '</strong><br>' +
1027
+ '<span style="color:#94a3b8">Overall min:</span> <strong style="color:' + (r.SF_min >= 1.5 ? '#a7f3d0' : r.SF_min >= 1.0 ? '#fed7aa' : '#fca5a5') + '">' + fmt(r.SF_min) + '</strong>';
1028
+ } else if (kind === 'shaft') {
1029
+ body.innerHTML =
1030
+ '<strong style="color:#f1f5f9">Stresses</strong><br>' +
1031
+ 'σ_a = ' + fmt(r.stresses.sigma_a, 'MPa') + ' · σ_m = ' + fmt(r.stresses.sigma_m, 'MPa') +
1032
+ ' · τ_a = ' + fmt(r.stresses.tau_a, 'MPa') + ' · τ_m = ' + fmt(r.stresses.tau_m, 'MPa') + '<br>' +
1033
+ "σ'_a = " + fmt(r.stresses.sigma_prime_a, 'MPa') + " · σ'_m = " + fmt(r.stresses.sigma_prime_m, 'MPa') + '<br><br>' +
1034
+ '<strong style="color:#f1f5f9">Factors of safety</strong><br>' +
1035
+ 'Goodman: <strong>' + fmt(r.n_Goodman) + '</strong> · Soderberg: <strong>' + fmt(r.n_Soderberg) + '</strong> · First-cycle yield: <strong>' + fmt(r.n_yield) + '</strong><br>' +
1036
+ 'Endurance limit S_e (corrected): ' + fmt(r.marin.S_e, 'MPa') + ' (uncorrected ' + fmt(r.marin.S_e_prime, 'MPa') + ')';
1037
+ } else if (kind === 'bearing') {
1038
+ const cat = r.inputs.catalog;
1039
+ const tag = cat ? (r.inputs.designation + ' · Ø' + cat.d + '/' + cat.D + ' × ' + cat.B + 'mm · C = ' + fmt(cat.C/1000, 'kN')) : ('custom C = ' + fmt(r.inputs.C/1000, 'kN'));
1040
+ body.innerHTML =
1041
+ '<strong style="color:#f1f5f9">' + tag + '</strong><br>' +
1042
+ 'Equivalent load P: ' + fmt(r.P, 'N') + ' · Exponent a = ' + fmt(r.inputs.a) + '<br><br>' +
1043
+ '<strong style="color:#f1f5f9">L_10 life</strong><br>' +
1044
+ fmt(r.L10_rev) + ' × 10⁶ revolutions<br>' +
1045
+ fmt(r.L10_h, 'hours') + ' at ' + r.inputs.rpm + ' rpm' + (r.inputs.reliability !== 0.9 ? ' (L₁₀)' : '') + '<br>' +
1046
+ (r.inputs.reliability !== 0.9 ?
1047
+ ('Adjusted to R = ' + r.inputs.reliability + ': ' + fmt(r.L_R_h, 'hours')) :
1048
+ '');
1049
+ } else if (kind === 'weld') {
1050
+ body.innerHTML =
1051
+ '<strong style="color:#f1f5f9">Throat geometry</strong><br>' +
1052
+ 't = 0.707·h = ' + fmt(r.throat.t, 'mm') + ' · area A = ' + fmt(r.throat.A, 'mm²') + '<br><br>' +
1053
+ '<strong style="color:#f1f5f9">Stress</strong><br>' +
1054
+ 'τ = F / A = ' + fmt(r.stress.tau, 'MPa') + '<br>' +
1055
+ 'Allowable (' + r.inputs.electrode + ', ' + (r.inputs.cyclic ? 'cyclic' : 'static') + '): ' + fmt(r.allowable, 'MPa') + '<br>' +
1056
+ 'Capacity: ' + fmt(r.capacity, 'N') + ' · Utilisation: ' + fmt(r.utilisation * 100, '%') + '<br>' +
1057
+ 'Safety factor: <strong style="color:' + (r.safetyFactor >= 1.5 ? '#a7f3d0' : r.safetyFactor >= 1.0 ? '#fed7aa' : '#fca5a5') + '">' + fmt(r.safetyFactor) + '</strong>';
1058
+ }
1059
+ root.appendChild(body);
1060
+ }
1061
+
1062
+ /**
1063
+ * Switch the UI to a different analysis tab. Rebuilds the form grid, preset buttons,
1064
+ * and report area in-place using the tab's schema.
1065
+ * @param {string} tabKey One of 'bolt' | 'gear' | 'shaft' | 'bearing' | 'weld'.
1066
+ */
1067
+ function switchTab(tabKey) {
1068
+ if (!TABS[tabKey]) return;
1069
+ S.currentTab = tabKey;
1070
+ // Rebuild pill highlights
1071
+ if (S.els.tabBar) {
1072
+ Array.from(S.els.tabBar.children).forEach(pill => {
1073
+ const active = pill.dataset.tab === tabKey;
1074
+ pill.style.background = active ? '#38bdf8' : '#334155';
1075
+ pill.style.color = active ? '#0f172a' : '#cbd5e1';
1076
+ pill.style.fontWeight = active ? '700' : '500';
1077
+ });
1078
+ }
1079
+ // Show/hide NL prompt (bolt only)
1080
+ if (S.els.promptWrap) S.els.promptWrap.style.display = TABS[tabKey].hasPrompt ? 'flex' : 'none';
1081
+ // Update subtitle
1082
+ if (S.els.subtitle) S.els.subtitle.textContent = TABS[tabKey].label + ' — ' + TABS[tabKey].subtitle;
1083
+ // Rebuild form grid
1084
+ rebuildFormGrid();
1085
+ // Rebuild presets
1086
+ rebuildPresets();
1087
+ // Clear report
1088
+ if (S.els.report) S.els.report.innerHTML = '<div style="padding:14px 0;color:#64748b;font-size:12px;font-style:italic">Adjust inputs to see live analysis.</div>';
1089
+ // Compute once
1090
+ setTimeout(compute, 0);
1091
+ }
1092
+
1093
+ function rebuildFormGrid() {
1094
+ const grid = S.els.grid;
1095
+ if (!grid) return;
1096
+ grid.innerHTML = '';
1097
+ // Clear stale input element refs (keep prompt, report, etc.)
1098
+ Object.keys(S.els).forEach(k => { if (k.startsWith('in_')) delete S.els[k]; });
1099
+ currentFields().forEach(field => {
1100
+ const [key, label, unit, def, min, type, opts] = field;
1101
+ const cell = document.createElement('label');
1102
+ cell.style.cssText = 'display:flex;flex-direction:column;gap:2px;font-size:11px;color:#94a3b8';
1103
+ const lbl = document.createElement('span');
1104
+ lbl.innerHTML = label + (unit ? ' <span style="color:#64748b">['+unit+']</span>' : '');
1105
+ let input;
1106
+ if (type === 'select') {
1107
+ input = document.createElement('select');
1108
+ input.style.cssText = 'padding:5px;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:3px;font-size:12px';
1109
+ opts.forEach(v => { const o = document.createElement('option'); o.value = v; o.textContent = v; input.appendChild(o); });
1110
+ input.value = def;
1111
+ } else {
1112
+ input = document.createElement('input');
1113
+ input.type = 'number';
1114
+ input.step = 'any';
1115
+ if (min !== null) input.min = String(min);
1116
+ input.value = String(def);
1117
+ input.style.cssText = 'padding:5px;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:3px;font-size:12px';
1118
+ }
1119
+ input.addEventListener('input', compute);
1120
+ input.addEventListener('change', compute);
1121
+ S.els['in_' + key] = input;
1122
+ cell.appendChild(lbl);
1123
+ cell.appendChild(input);
1124
+ grid.appendChild(cell);
1125
+ });
1126
+ }
1127
+
1128
+ function rebuildPresets() {
1129
+ const presets = S.els.presets;
1130
+ if (!presets) return;
1131
+ presets.innerHTML = '';
1132
+ TABS[S.currentTab].presets.forEach(preset => {
1133
+ const b = document.createElement('button');
1134
+ b.textContent = preset.label;
1135
+ b.style.cssText = 'padding:4px 8px;background:#334155;color:#cbd5e1;border:0;border-radius:3px;cursor:pointer;font-size:11px';
1136
+ b.onclick = () => {
1137
+ Object.entries(preset.values).forEach(([k, v]) => { const el = S.els['in_' + k]; if (el) el.value = String(v); });
1138
+ compute();
1139
+ };
1140
+ presets.appendChild(b);
1141
+ });
1142
+ }
1143
+
668
1144
  function renderReport(r) {
669
1145
  const root = S.els.report;
670
1146
  if (!root) return;
@@ -789,11 +1265,32 @@
789
1265
 
790
1266
  // Header
791
1267
  const header = document.createElement('div');
792
- header.innerHTML = '<div style="font-size:15px;font-weight:700;color:#f1f5f9">AI Engineering Analyst</div>'
793
- + '<div style="font-size:11px;color:#94a3b8;margin-top:2px">Bolted-joint analysis (VDI 2230 / Shigley) — v1</div>';
1268
+ const title = document.createElement('div');
1269
+ title.style.cssText = 'font-size:15px;font-weight:700;color:#f1f5f9';
1270
+ title.textContent = 'AI Engineering Analyst';
1271
+ const subtitle = document.createElement('div');
1272
+ subtitle.style.cssText = 'font-size:11px;color:#94a3b8;margin-top:2px';
1273
+ subtitle.textContent = 'Bolted Joint — VDI 2230 / Shigley';
1274
+ S.els.subtitle = subtitle;
1275
+ header.appendChild(title);
1276
+ header.appendChild(subtitle);
794
1277
  wrap.appendChild(header);
795
1278
 
796
- // Prompt box for natural-language entry
1279
+ // Tab pills 5 analysis kinds
1280
+ const tabBar = document.createElement('div');
1281
+ tabBar.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;padding:6px;background:#0f172a;border:1px solid #334155;border-radius:6px';
1282
+ Object.entries(TABS).forEach(([key, tab]) => {
1283
+ const pill = document.createElement('button');
1284
+ pill.dataset.tab = key;
1285
+ pill.textContent = tab.label;
1286
+ pill.style.cssText = 'padding:6px 12px;background:' + (key === 'bolt' ? '#38bdf8' : '#334155') + ';color:' + (key === 'bolt' ? '#0f172a' : '#cbd5e1') + ';border:0;border-radius:4px;cursor:pointer;font-size:12px;font-weight:' + (key === 'bolt' ? '700' : '500') + ';transition:background 0.15s';
1287
+ pill.addEventListener('click', () => switchTab(key));
1288
+ tabBar.appendChild(pill);
1289
+ });
1290
+ S.els.tabBar = tabBar;
1291
+ wrap.appendChild(tabBar);
1292
+
1293
+ // Prompt box for natural-language entry (bolted-joint only)
797
1294
  const promptWrap = document.createElement('div');
798
1295
  promptWrap.style.cssText = 'display:flex;gap:6px';
799
1296
  const prompt = document.createElement('input');
@@ -806,60 +1303,24 @@
806
1303
  applyBtn.onclick = applyFromPrompt;
807
1304
  prompt.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); applyFromPrompt(); } });
808
1305
  S.els.prompt = prompt;
1306
+ S.els.promptWrap = promptWrap;
809
1307
  promptWrap.appendChild(prompt);
810
1308
  promptWrap.appendChild(applyBtn);
811
1309
  wrap.appendChild(promptWrap);
812
1310
 
813
- // Input grid
1311
+ // Input grid — populated by rebuildFormGrid()
814
1312
  const grid = document.createElement('div');
815
1313
  grid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px 12px;padding:10px;background:#1e293b;border-radius:6px';
816
- INPUT_FIELDS.forEach(field => {
817
- const [key, label, unit, def, min, type, opts] = field;
818
- const cell = document.createElement('label');
819
- cell.style.cssText = 'display:flex;flex-direction:column;gap:2px;font-size:11px;color:#94a3b8';
820
- const lbl = document.createElement('span');
821
- lbl.innerHTML = label + (unit ? ' <span style="color:#64748b">['+unit+']</span>' : '');
822
- let input;
823
- if (type === 'select') {
824
- input = document.createElement('select');
825
- input.style.cssText = 'padding:5px;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:3px;font-size:12px';
826
- opts.forEach(v => { const o = document.createElement('option'); o.value = v; o.textContent = v; input.appendChild(o); });
827
- input.value = def;
828
- } else {
829
- input = document.createElement('input');
830
- input.type = 'number';
831
- input.step = 'any';
832
- if (min !== null) input.min = String(min);
833
- input.value = String(def);
834
- input.style.cssText = 'padding:5px;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:3px;font-size:12px';
835
- }
836
- input.addEventListener('input', compute);
837
- input.addEventListener('change', compute);
838
- S.els['in_' + key] = input;
839
- cell.appendChild(lbl);
840
- cell.appendChild(input);
841
- grid.appendChild(cell);
842
- });
1314
+ S.els.grid = grid;
843
1315
  wrap.appendChild(grid);
1316
+ rebuildFormGrid();
844
1317
 
845
- // Preset examples
1318
+ // Preset examples — populated by rebuildPresets()
846
1319
  const presets = document.createElement('div');
847
1320
  presets.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px';
848
- [
849
- { label: 'MecAgent demo', values: { boltCount:4, thread:'M12', grade:'10.9', preload:39000, shearForce:18000, axialForce:18000, moment:420000, bcd:96, friction:0.16, safetyFactor:1.5 } },
850
- { label: 'Flange M8 light', values: { boltCount:8, thread:'M8', grade:'8.8', preload:15000, shearForce:5000, axialForce:2000, moment:50000, bcd:60, friction:0.15, safetyFactor:1.25 } },
851
- { label: 'Heavy M20', values: { boltCount:6, thread:'M20', grade:'8.8', preload:120000, shearForce:30000, axialForce:25000, moment:800000, bcd:200, friction:0.14, safetyFactor:1.5 } }
852
- ].forEach(preset => {
853
- const b = document.createElement('button');
854
- b.textContent = preset.label;
855
- b.style.cssText = 'padding:4px 8px;background:#334155;color:#cbd5e1;border:0;border-radius:3px;cursor:pointer;font-size:11px';
856
- b.onclick = () => {
857
- Object.entries(preset.values).forEach(([k, v]) => { const el = S.els['in_' + k]; if (el) el.value = String(v); });
858
- compute();
859
- };
860
- presets.appendChild(b);
861
- });
1321
+ S.els.presets = presets;
862
1322
  wrap.appendChild(presets);
1323
+ rebuildPresets();
863
1324
 
864
1325
  // Report area
865
1326
  const report = document.createElement('div');
@@ -910,6 +1371,12 @@
910
1371
  analyzeShaft: shaftFatigueAnalysis,
911
1372
  SHAFT_MATERIALS,
912
1373
  SURFACE_FACTORS,
1374
+ // ---- v2: bearings ----
1375
+ analyzeBearing: bearingLifeAnalysis,
1376
+ BEARING_CATALOGUE,
1377
+ // ---- v2: welds ----
1378
+ analyzeWeld: filletWeldAnalysis,
1379
+ WELD_ELECTRODES,
913
1380
  // ---- shared ----
914
1381
  runSelfTests,
915
1382
  STEEL_GRADES,
@@ -927,11 +1394,13 @@
927
1394
  },
928
1395
  getUI: () => { if (!uiEl) uiEl = buildUI(); return uiEl; },
929
1396
  execute: (cmd, params) => {
930
- if (cmd === 'analyze') return boltedJointAnalysis(params || {});
931
- if (cmd === 'analyze-gear') return spurGearAnalysis(params || {});
932
- if (cmd === 'analyze-shaft') return shaftFatigueAnalysis(params || {});
933
- if (cmd === 'parse') return parseBoltedJointPrompt((params && params.prompt) || '');
934
- if (cmd === 'show') { if (!uiEl) uiEl = buildUI(); return uiEl; }
1397
+ if (cmd === 'analyze') return boltedJointAnalysis(params || {});
1398
+ if (cmd === 'analyze-gear') return spurGearAnalysis(params || {});
1399
+ if (cmd === 'analyze-shaft') return shaftFatigueAnalysis(params || {});
1400
+ if (cmd === 'analyze-bearing') return bearingLifeAnalysis(params || {});
1401
+ if (cmd === 'analyze-weld') return filletWeldAnalysis(params || {});
1402
+ if (cmd === 'parse') return parseBoltedJointPrompt((params && params.prompt) || '');
1403
+ if (cmd === 'show') { if (!uiEl) uiEl = buildUI(); return uiEl; }
935
1404
  }
936
1405
  };
937
1406