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.
- package/app/index.html +5 -4
- package/app/js/modules/ai-copilot.js +4 -1
- package/app/js/modules/ai-engineer-rag.js +689 -0
- package/app/js/modules/ai-engineer.js +529 -60
- package/app/js/modules/text-to-cad.js +561 -33
- package/package.json +1 -1
- package/server/converter.py +528 -0
|
@@ -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
|
|
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
|
-
|
|
626
|
-
|
|
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
|
-
|
|
950
|
+
currentFields().forEach(([key,,,,, type]) => {
|
|
650
951
|
const el = S.els['in_' + key];
|
|
651
952
|
if (!el) return;
|
|
652
|
-
|
|
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
|
|
968
|
+
const analyze = currentAnalyze();
|
|
969
|
+
const r = analyze(params);
|
|
661
970
|
S.lastResult = r;
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
793
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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')
|
|
931
|
-
if (cmd === 'analyze-gear')
|
|
932
|
-
if (cmd === 'analyze-shaft')
|
|
933
|
-
if (cmd === '
|
|
934
|
-
if (cmd === '
|
|
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
|
|