cyclecad 3.11.0 → 3.12.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 CHANGED
@@ -1412,7 +1412,7 @@
1412
1412
  </div>
1413
1413
  <div class="status-item">
1414
1414
  <span class="status-label">Version:</span>
1415
- <span class="status-value">v0.9.0</span>
1415
+ <span class="status-value" id="status-version">v3.12.0</span>
1416
1416
  </div>
1417
1417
  </div>
1418
1418
 
@@ -1455,12 +1455,15 @@ window._dismissSplash = function(action) {
1455
1455
  <script>
1456
1456
  (function() {
1457
1457
  function dismiss() { document.getElementById("welcome-panel").style.display = "none"; }
1458
- document.getElementById("splash-sketch").addEventListener("click", dismiss);
1459
- document.getElementById("splash-import").addEventListener("click", dismiss);
1460
- document.getElementById("splash-textcad").addEventListener("click", function() { dismiss(); setTimeout(() => { if (window.app) window.app.handleMenuAction('tools-text-to-cad'); }, 100); });
1461
- document.getElementById("splash-imagecad").addEventListener("click", function() { dismiss(); setTimeout(() => { if (window.app) window.app.handleMenuAction('tools-image-to-cad'); }, 100); });
1462
- document.getElementById("splash-openscad").addEventListener("click", function() { dismiss(); setTimeout(() => { if (window.app) window.app.handleMenuAction('tools-openscad'); }, 100); });
1463
- document.getElementById("splash-inventor").addEventListener("click", dismiss);
1458
+ // Queue the action on the pending hook so the main module picks it up after load.
1459
+ // If the module is already loaded, the defineProperty setter dispatches immediately with a 100ms delay.
1460
+ function dispatchAction(action) { dismiss(); window._pendingSplashAction = action; }
1461
+ document.getElementById("splash-sketch") .addEventListener("click", function() { dispatchAction('sketch-new'); });
1462
+ document.getElementById("splash-import") .addEventListener("click", function() { dispatchAction('file-import'); });
1463
+ document.getElementById("splash-textcad") .addEventListener("click", function() { dispatchAction('tools-text-to-cad'); });
1464
+ document.getElementById("splash-imagecad").addEventListener("click", function() { dispatchAction('tools-image-to-cad'); });
1465
+ document.getElementById("splash-openscad").addEventListener("click", function() { dispatchAction('tools-openscad'); });
1466
+ document.getElementById("splash-inventor").addEventListener("click", function() { dispatchAction('file-import'); });
1464
1467
  })();
1465
1468
  </script>
1466
1469
 
@@ -1959,9 +1962,10 @@ window._dismissSplash = function(action) {
1959
1962
  })();
1960
1963
  }
1961
1964
  break;
1962
- case 'help-about':
1963
- showDialog('About cycleCAD', 'cycleCAD v0.9.0 - Fusion 360 Clone<br>Open-source parametric 3D CAD modeler<br><br>Built with Three.js, supporting STEP/IGES import, full parametric modeling, and AI-powered design assistance.');
1964
- break;
1965
+ case 'help-about': {
1966
+ const v = (window.CycleCAD && window.CycleCAD.version) ? window.CycleCAD.version : '3.12.0';
1967
+ showDialog('About cycleCAD', 'cycleCAD v' + v + '<br>Part of the cycleCAD Suite — parametric CAD, ExplodeView, Pentacad.<br><br>Open-source (MIT) parametric 3D CAD modeller. Built with Three.js, OpenCascade.js for real B-rep, supporting STEP / IGES / GLB import, full parametric modelling, and AI-powered design assistance via the AI Copilot + AI Engineering Analyst.');
1968
+ break; }
1965
1969
  case 'tools-ai-copilot':
1966
1970
  if (window.CycleCAD && window.CycleCAD.AICopilot) {
1967
1971
  showDialog('✨ AI Copilot — multi-step CAD from natural language', '');
@@ -2953,6 +2957,32 @@ window._dismissSplash = function(action) {
2953
2957
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initDialogDrag);
2954
2958
  else initDialogDrag();
2955
2959
  })();
2960
+
2961
+ // ─── Dynamic version badge — fetch from /package.json on load, fall back to embedded ───
2962
+ (function(){
2963
+ const FALLBACK_VERSION = '3.12.0';
2964
+ async function updateVersion() {
2965
+ const badge = document.getElementById('status-version');
2966
+ if (!badge) return;
2967
+ try {
2968
+ const r = await fetch('/package.json', { cache: 'no-cache' });
2969
+ if (!r.ok) throw new Error('package.json HTTP ' + r.status);
2970
+ const pkg = await r.json();
2971
+ if (pkg && pkg.version) {
2972
+ badge.textContent = 'v' + pkg.version;
2973
+ window.CycleCAD = window.CycleCAD || {};
2974
+ window.CycleCAD.version = pkg.version;
2975
+ }
2976
+ } catch (err) {
2977
+ // Keep hardcoded fallback; log quietly for debugging.
2978
+ badge.textContent = 'v' + FALLBACK_VERSION;
2979
+ window.CycleCAD = window.CycleCAD || {};
2980
+ window.CycleCAD.version = FALLBACK_VERSION;
2981
+ }
2982
+ }
2983
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', updateVersion);
2984
+ else updateVersion();
2985
+ })();
2956
2986
  </script>
2957
2987
  </body>
2958
2988
  </html>
@@ -179,6 +179,279 @@
179
179
  };
180
180
  }
181
181
 
182
+ // ===================================================================
183
+ // GEAR MATERIAL DATA — AGMA Grade 1 steel allowables (Shigley Table 14-3 / 14-6)
184
+ // Keys: hardness in HB (Brinell). Values: {S_t, S_c} in MPa (converted from psi).
185
+ // S_t: allowable bending stress. S_c: allowable contact (surface) stress.
186
+ // Formulas for through-hardened steel (AGMA 2001):
187
+ // S_t = 77 * HB + 12,800 psi (Grade 1 bending)
188
+ // S_c = 322 * HB + 29,100 psi (Grade 1 contact)
189
+ // 1 psi = 0.00689476 MPa.
190
+ // ===================================================================
191
+ /**
192
+ * Allowable bending stress for Grade 1 through-hardened gear steel (AGMA 2001).
193
+ * @param {number} HB Brinell hardness (180–400 typical).
194
+ * @returns {number} S_t in MPa.
195
+ */
196
+ function gearAllowableBending(HB) {
197
+ return (77 * HB + 12800) * 0.00689476;
198
+ }
199
+ /**
200
+ * Allowable contact stress for Grade 1 through-hardened gear steel (AGMA 2001).
201
+ * @param {number} HB Brinell hardness.
202
+ * @returns {number} S_c in MPa.
203
+ */
204
+ function gearAllowableContact(HB) {
205
+ return (322 * HB + 29100) * 0.00689476;
206
+ }
207
+
208
+ // AGMA geometry factor J (bending) — approximated from Shigley Fig. 14-6
209
+ // for external spur gears at 20° pressure angle. Interpolated by number of teeth.
210
+ const J_TABLE = [
211
+ [12, 0.245], [14, 0.265], [17, 0.295], [20, 0.32], [25, 0.345],
212
+ [30, 0.365], [35, 0.38], [40, 0.39], [50, 0.41], [75, 0.435], [100, 0.45]
213
+ ];
214
+ function gearGeometryJ(teeth) {
215
+ const z = Math.max(J_TABLE[0][0], Math.min(J_TABLE[J_TABLE.length-1][0], teeth));
216
+ for (let i = 0; i < J_TABLE.length - 1; i++) {
217
+ const [z1, j1] = J_TABLE[i], [z2, j2] = J_TABLE[i+1];
218
+ if (z >= z1 && z <= z2) return j1 + (j2 - j1) * (z - z1) / (z2 - z1);
219
+ }
220
+ return 0.4;
221
+ }
222
+
223
+ // AGMA geometry factor I (pitting) — external gear pair, 20° pressure angle.
224
+ // Approximated from Shigley Eq. 14-23 with m_N = 1 for spur gears.
225
+ function gearGeometryI(pinionTeeth, gearTeeth, pressureAngle) {
226
+ const phi = (pressureAngle || 20) * Math.PI / 180;
227
+ const mG = gearTeeth / pinionTeeth;
228
+ return (Math.cos(phi) * Math.sin(phi) / 2) * (mG / (mG + 1));
229
+ }
230
+
231
+ /**
232
+ * Spur gear AGMA bending + pitting analysis (Shigley Ch. 14).
233
+ *
234
+ * Uses the fundamental AGMA 2001 stress equations with sensible default modifying factors.
235
+ * For safety-critical applications, confirm each K-factor per AGMA 908-B89.
236
+ *
237
+ * @param {object} p
238
+ * @param {number} p.pinionTeeth z_P — pinion tooth count.
239
+ * @param {number} p.gearTeeth z_G — gear tooth count.
240
+ * @param {number} p.module m — in mm.
241
+ * @param {number} p.faceWidth F — in mm.
242
+ * @param {number} p.torque T_P — torque on pinion in N·m.
243
+ * @param {number} p.pinionHB HB_P — Brinell hardness of pinion.
244
+ * @param {number} p.gearHB HB_G — Brinell hardness of gear.
245
+ * @param {number} [p.overload=1.0] K_o — 1.0 uniform / 1.25 moderate / 1.5 heavy shock.
246
+ * @param {number} [p.dynamic=1.1] K_v — dynamic factor (≈1.0 precision, 1.1–1.3 typical).
247
+ * @param {number} [p.loadDist=1.3] K_m — load distribution (1.3 good alignment).
248
+ * @param {number} [p.reliability=1.0] K_R — 1.0 @ 99% reliability, 1.25 @ 99.9%.
249
+ * @param {number} [p.pressureAngle=20] φ — pressure angle in degrees.
250
+ * @param {number} [p.Z_E=190] Elastic coefficient for steel-steel (MPa^0.5).
251
+ * @returns {object} {inputs, pinion:{…}, gear:{…}, verdict, …}
252
+ */
253
+ function spurGearAnalysis(p) {
254
+ const z_P = Math.max(12, Math.round(Number(p.pinionTeeth) || 20));
255
+ const z_G = Math.max(12, Math.round(Number(p.gearTeeth) || 40));
256
+ const m = Math.max(0.5, Number(p.module) || 2);
257
+ const F = Math.max(3, Number(p.faceWidth) || 25);
258
+ const T_P = Math.max(0, Number(p.torque) || 0);
259
+ const HB_P = Math.max(150, Number(p.pinionHB) || 240);
260
+ const HB_G = Math.max(150, Number(p.gearHB) || HB_P);
261
+ const K_o = Math.max(1, Number(p.overload) || 1.0);
262
+ const K_v = Math.max(1, Number(p.dynamic) || 1.1);
263
+ const K_m = Math.max(1, Number(p.loadDist) || 1.3);
264
+ const K_R = Math.max(1, Number(p.reliability) || 1.0);
265
+ const phi = Number(p.pressureAngle) || 20;
266
+ const Z_E = Number(p.Z_E) || 190;
267
+ const K_s = 1.0; // size factor — ignore for typical sizes
268
+ const K_B = 1.0; // rim thickness — solid blank
269
+ const C_f = 1.0; // surface condition — clean-cut
270
+ const Y_N = 1.0, Z_N = 1.0, C_H = 1.0, K_T = 1.0; // nominal life factors
271
+ const mG = z_G / z_P;
272
+
273
+ // Pitch diameters and tangential load
274
+ const d_P = m * z_P; // pinion pitch dia in mm
275
+ const d_G = m * z_G; // gear pitch dia
276
+ const T_Pnmm = T_P * 1000; // convert N·m → N·mm
277
+ const W_t = (d_P > 0) ? (2 * T_Pnmm) / d_P : 0; // tangential load N
278
+
279
+ // Geometry factors
280
+ const J_P = gearGeometryJ(z_P);
281
+ const J_G = gearGeometryJ(z_G);
282
+ const I = gearGeometryI(z_P, z_G, phi);
283
+
284
+ // AGMA bending stress (both gears see same W_t but different J)
285
+ const sigma_b_P = (W_t * K_o * K_v * K_s * K_m * K_B) / (F * m * J_P);
286
+ const sigma_b_G = (W_t * K_o * K_v * K_s * K_m * K_B) / (F * m * J_G);
287
+
288
+ // AGMA contact stress (same magnitude for both meshing teeth)
289
+ const sigma_c = (W_t > 0 && d_P > 0 && I > 0)
290
+ ? Z_E * Math.sqrt((W_t * K_o * K_v * K_s * K_m * C_f) / (d_P * F * I))
291
+ : 0;
292
+
293
+ // Allowable stresses (material)
294
+ const St_P = gearAllowableBending(HB_P);
295
+ const St_G = gearAllowableBending(HB_G);
296
+ const Sc_P = gearAllowableContact(HB_P);
297
+ const Sc_G = gearAllowableContact(HB_G);
298
+
299
+ // Factors of safety — Shigley Eq. 14-41 / 14-42 simplified
300
+ const SF_bending_P = St_P * Y_N / (sigma_b_P * K_T * K_R);
301
+ const SF_bending_G = St_G * Y_N / (sigma_b_G * K_T * K_R);
302
+ const SF_contact_P = (Sc_P * Z_N * C_H) / (sigma_c * K_T * K_R);
303
+ const SF_contact_G = (Sc_G * Z_N * C_H) / (sigma_c * K_T * K_R);
304
+
305
+ const SF_min = Math.min(SF_bending_P, SF_bending_G, SF_contact_P, SF_contact_G);
306
+ const safe = SF_min >= 1.0;
307
+ const verdict = SF_min >= 2.0 ? 'SAFE (margin ≥ 2)' :
308
+ SF_min >= 1.5 ? 'SAFE (margin ≥ 1.5 — industry typical)' :
309
+ SF_min >= 1.0 ? 'MARGINAL (factor < 1.5 — review assumptions)' :
310
+ 'UNSAFE (factor < 1.0 — tooth will fail)';
311
+ const verdictClass = SF_min >= 1.5 ? 'pass' : SF_min >= 1.0 ? 'warn' : 'fail';
312
+ const notes = [];
313
+ if (SF_bending_P < SF_bending_G) notes.push('Pinion is the weaker gear in bending (lower J factor) — as expected.');
314
+ if (SF_contact_P < SF_bending_P) notes.push('Contact stress governs over bending — consider surface hardening (carburizing/induction) for higher S_c.');
315
+ if (SF_min < 1.5) notes.push('Margin below industry-typical 1.5 — increase module, face width, or hardness.');
316
+
317
+ return {
318
+ inputs: { z_P, z_G, m, F, T_P, HB_P, HB_G, K_o, K_v, K_m, K_R, phi, Z_E,
319
+ d_P, d_G, mG, W_t, J_P, J_G, I, St_P, St_G, Sc_P, Sc_G },
320
+ pinion: { SF_bending: SF_bending_P, SF_contact: SF_contact_P,
321
+ sigma_b: sigma_b_P, sigma_c, S_t: St_P, S_c: Sc_P, J: J_P },
322
+ gear: { SF_bending: SF_bending_G, SF_contact: SF_contact_G,
323
+ sigma_b: sigma_b_G, sigma_c, S_t: St_G, S_c: Sc_G, J: J_G },
324
+ SF_min, safe, verdict, verdictClass, notes
325
+ };
326
+ }
327
+
328
+ // ===================================================================
329
+ // SHAFT FATIGUE — Goodman / Soderberg (Shigley Ch. 7)
330
+ // ===================================================================
331
+ // Shaft material data (wrought carbon steel from Shigley Table A-20).
332
+ const SHAFT_MATERIALS = Object.freeze({
333
+ '1020_hr': { label: 'AISI 1020 hot-rolled', S_ut: 380, S_y: 210 },
334
+ '1020_cd': { label: 'AISI 1020 cold-drawn', S_ut: 470, S_y: 390 },
335
+ '1040_hr': { label: 'AISI 1040 hot-rolled', S_ut: 520, S_y: 290 },
336
+ '1040_cd': { label: 'AISI 1040 cold-drawn', S_ut: 590, S_y: 490 },
337
+ '1050_hr': { label: 'AISI 1050 hot-rolled', S_ut: 620, S_y: 340 },
338
+ '1050_cd': { label: 'AISI 1050 cold-drawn', S_ut: 690, S_y: 580 },
339
+ '4140_Q&T': { label: 'AISI 4140 Q&T 425°C', S_ut: 1020, S_y: 900 },
340
+ '4340_Q&T': { label: 'AISI 4340 Q&T 425°C', S_ut: 1280, S_y: 1140 }
341
+ });
342
+
343
+ // Marin surface factor — Shigley Eq. 6-19, a*S_ut^b.
344
+ const SURFACE_FACTORS = {
345
+ 'ground': { a: 1.58, b: -0.085 },
346
+ 'machined': { a: 4.51, b: -0.265 },
347
+ 'cold-drawn': { a: 4.51, b: -0.265 },
348
+ 'hot-rolled': { a: 57.7, b: -0.718 },
349
+ 'as-forged': { a: 272, b: -0.995 }
350
+ };
351
+
352
+ /**
353
+ * Shaft fatigue analysis using Goodman / Soderberg criteria.
354
+ *
355
+ * Given mean and alternating stresses (computed from bending moment + torque amplitudes),
356
+ * applies Marin modifying factors to estimate the endurance limit and returns factors of
357
+ * safety per Goodman (common) and Soderberg (conservative).
358
+ *
359
+ * @param {object} p
360
+ * @param {string} p.material Key into SHAFT_MATERIALS.
361
+ * @param {number} p.diameter Shaft diameter in mm.
362
+ * @param {number} p.M_a Alternating bending moment amplitude in N·m.
363
+ * @param {number} p.M_m Mean bending moment in N·m (often 0 for rotating-bending).
364
+ * @param {number} p.T_a Alternating torque amplitude in N·m.
365
+ * @param {number} p.T_m Mean torque in N·m.
366
+ * @param {number} [p.Kf=2.0] Fatigue stress concentration for bending (≥1).
367
+ * @param {number} [p.Kfs=1.5] Fatigue stress concentration for torsion.
368
+ * @param {string} [p.surface='machined'] Surface finish preset.
369
+ * @param {number} [p.reliability=0.99] Reliability (0.5–0.999999).
370
+ * @param {number} [p.temperatureC=25] Operating temperature in °C.
371
+ * @returns {object}
372
+ */
373
+ function shaftFatigueAnalysis(p) {
374
+ const mat = SHAFT_MATERIALS[p.material] || SHAFT_MATERIALS['1050_cd'];
375
+ const d = Math.max(5, Number(p.diameter) || 25); // mm
376
+ const M_a = Math.max(0, Number(p.M_a) || 0); // N·m
377
+ const M_m = Math.max(0, Number(p.M_m) || 0);
378
+ const T_a = Math.max(0, Number(p.T_a) || 0);
379
+ const T_m = Math.max(0, Number(p.T_m) || 0);
380
+ const Kf = Math.max(1, Number(p.Kf) || 2.0);
381
+ const Kfs = Math.max(1, Number(p.Kfs) || 1.5);
382
+ const surfaceKey = p.surface || 'machined';
383
+ const surf = SURFACE_FACTORS[surfaceKey] || SURFACE_FACTORS.machined;
384
+ const R = Math.max(0.5, Math.min(0.999999, Number(p.reliability) || 0.99));
385
+ const T_C = Number(p.temperatureC) || 25;
386
+
387
+ // Marin surface factor k_a = a * (S_ut[MPa])^b
388
+ const k_a = surf.a * Math.pow(mat.S_ut, surf.b);
389
+ // Size factor k_b — Shigley Eq. 6-20 (rotating bending, 2.79 ≤ d ≤ 51 mm)
390
+ let k_b;
391
+ if (d <= 51) k_b = 1.24 * Math.pow(d, -0.107);
392
+ else if (d <= 254) k_b = 1.51 * Math.pow(d, -0.157);
393
+ else k_b = 0.60;
394
+ // Loading factor k_c = 1 for combined bending + torsion (we handle each via Kf/Kfs)
395
+ const k_c = 1.0;
396
+ // Temperature factor k_d — Eq. 6-27
397
+ const k_d = (T_C <= 70) ? 1.0 : (0.975 + 0.432e-3*T_C - 0.115e-5*T_C*T_C + 0.104e-8*T_C*T_C*T_C);
398
+ // Reliability factor k_e — Shigley Table 6-5 (z_a from normal distribution)
399
+ const z_a = (R === 0.99 ? 2.326 : R === 0.999 ? 3.091 : R === 0.95 ? 1.645 : R === 0.90 ? 1.288 : 2.326);
400
+ const k_e = 1 - 0.08 * z_a;
401
+ // Miscellaneous k_f = 1 for this scope
402
+ const k_f = 1.0;
403
+ // Uncorrected endurance limit S_e' — Shigley Eq. 6-10
404
+ const S_e_prime = (mat.S_ut <= 1400) ? 0.5 * mat.S_ut : 700;
405
+ // Corrected endurance limit
406
+ const S_e = k_a * k_b * k_c * k_d * k_e * k_f * S_e_prime;
407
+
408
+ // Stresses — bending σ_a/σ_m (Kf applied), torsion τ_a/τ_m (Kfs applied)
409
+ // σ = 32·M / (π·d^3) [M in N·mm, d in mm → MPa]
410
+ // τ = 16·T / (π·d^3)
411
+ const d_m = d; // mm
412
+ const sigma_a = (32 * M_a * 1000) / (Math.PI * Math.pow(d_m, 3));
413
+ const sigma_m = (32 * M_m * 1000) / (Math.PI * Math.pow(d_m, 3));
414
+ const tau_a = (16 * T_a * 1000) / (Math.PI * Math.pow(d_m, 3));
415
+ const tau_m = (16 * T_m * 1000) / (Math.PI * Math.pow(d_m, 3));
416
+
417
+ // Von Mises effective stresses (amplitude + mean) with fatigue concentrations
418
+ const sigma_prime_a = Math.sqrt(Math.pow(Kf * sigma_a, 2) + 3 * Math.pow(Kfs * tau_a, 2));
419
+ const sigma_prime_m = Math.sqrt(Math.pow(Kf * sigma_m, 2) + 3 * Math.pow(Kfs * tau_m, 2));
420
+
421
+ // Goodman: 1/n = σ_a'/S_e + σ_m'/S_ut
422
+ const goodmanInv = (sigma_prime_a / S_e) + (sigma_prime_m / mat.S_ut);
423
+ const n_Goodman = goodmanInv > 0 ? 1 / goodmanInv : Infinity;
424
+
425
+ // Soderberg: 1/n = σ_a'/S_e + σ_m'/S_y (conservative — yields instead of UTS)
426
+ const soderbergInv = (sigma_prime_a / S_e) + (sigma_prime_m / mat.S_y);
427
+ const n_Soderberg = soderbergInv > 0 ? 1 / soderbergInv : Infinity;
428
+
429
+ // First-cycle yield check — static
430
+ const sigma_max = sigma_prime_a + sigma_prime_m;
431
+ const n_yield = mat.S_y / sigma_max;
432
+
433
+ // Verdict
434
+ const n_fatigue = Math.min(n_Goodman, n_Soderberg);
435
+ let verdict, verdictClass;
436
+ if (n_fatigue >= 2.0 && n_yield >= 2.0) { verdict = 'SAFE (margin ≥ 2)'; verdictClass = 'pass'; }
437
+ else if (n_fatigue >= 1.5 && n_yield >= 1.5) { verdict = 'SAFE (industry typical)'; verdictClass = 'pass'; }
438
+ else if (n_fatigue >= 1.0 && n_yield >= 1.0) { verdict = 'MARGINAL — factor below 1.5'; verdictClass = 'warn'; }
439
+ else { verdict = 'UNSAFE — factor < 1.0'; verdictClass = 'fail'; }
440
+ const notes = [];
441
+ if (n_yield < n_fatigue) notes.push('First-cycle yield governs — increase diameter or use higher-strength material.');
442
+ if (sigma_prime_m > sigma_prime_a) notes.push('Mean stress dominant — consider rotating-bending to convert static to fully-reversed.');
443
+ if (n_Goodman > n_Soderberg + 0.3) notes.push('Soderberg (conservative) significantly lower — review if proof strength, not UTS, is the correct allowable.');
444
+
445
+ return {
446
+ inputs: { material: p.material || '1050_cd', material_label: mat.label, d, S_ut: mat.S_ut, S_y: mat.S_y,
447
+ M_a, M_m, T_a, T_m, Kf, Kfs, surfaceKey, reliability: R, temperatureC: T_C },
448
+ marin: { k_a, k_b, k_c, k_d, k_e, k_f, S_e_prime, S_e },
449
+ stresses: { sigma_a, sigma_m, tau_a, tau_m, sigma_prime_a, sigma_prime_m, sigma_max },
450
+ n_Goodman, n_Soderberg, n_yield,
451
+ verdict, verdictClass, notes
452
+ };
453
+ }
454
+
182
455
  // ===================================================================
183
456
  // UNIT TESTS — verify core against MecAgent screenshot values
184
457
  // ===================================================================
@@ -206,6 +479,45 @@
206
479
  test('τ (shear)', r.combinedStress.tau, 79, 1.5); // 6687.5/84.3 ≈ 79.3
207
480
  test('σ_vm', r.combinedStress.sigma_vm, 558, 2); // √(542² + 3·79²)
208
481
 
482
+ // ------- GEAR TEST — Shigley Example 14-5 adapted -------
483
+ // 17T pinion / 52T gear, m=2mm, F=30mm, 2.5kW @ 1800rpm → T_P ≈ 13.26 N·m
484
+ // Through-hardened 240 HB both gears, K_o=1, K_v=1, K_m=1.3
485
+ const g = spurGearAnalysis({
486
+ pinionTeeth: 17, gearTeeth: 52, module: 2, faceWidth: 30,
487
+ torque: 13.26, pinionHB: 240, gearHB: 240,
488
+ overload: 1, dynamic: 1, loadDist: 1.3
489
+ });
490
+ // Tangential load W_t = 2·T / d_P = 2·13260 / 34 ≈ 780 N
491
+ test('gear W_t', g.inputs.W_t, 780, 3);
492
+ // J for 17 teeth ≈ 0.295 per interpolation table
493
+ test('gear J (pinion)', g.pinion.J, 0.295, 0.01);
494
+ // σ_b (pinion) = 780·1·1·1.3 / (30·2·0.295) = 1014/17.7 ≈ 57.3 MPa
495
+ test('gear σ_b pinion', g.pinion.sigma_b, 57.3, 1.5);
496
+ // S_t @240HB = (77·240 + 12800)·psi→MPa = 31280·0.00689 ≈ 215.7 MPa
497
+ test('gear S_t @240HB', g.pinion.S_t, 215.7, 1.5);
498
+ // SF_bending_P ≈ 215.7 / 57.3 ≈ 3.77
499
+ test('gear SF_bending pinion', g.pinion.SF_bending, 3.77, 0.2);
500
+
501
+ // ------- SHAFT TEST — clean case with rotating-bending + constant torque -------
502
+ // AISI 1050 CD, d=25mm, M_a=100 N·m (rotating bending so M_m=0), T_m=50 N·m, T_a=0
503
+ // Kf=2.0 (fillet), Kfs=1.5, machined surface, R=0.99, T=25°C
504
+ const sh = shaftFatigueAnalysis({
505
+ material: '1050_cd', diameter: 25,
506
+ M_a: 100, M_m: 0, T_a: 0, T_m: 50,
507
+ Kf: 2.0, Kfs: 1.5, surface: 'machined', reliability: 0.99, temperatureC: 25
508
+ });
509
+ // σ_a = 32·100000 / (π·25³) = 32e5 / 49087 ≈ 65.2 MPa
510
+ test('shaft σ_a', sh.stresses.sigma_a, 65.2, 0.5);
511
+ // τ_m = 16·50000 / (π·25³) ≈ 16.3 MPa
512
+ test('shaft τ_m', sh.stresses.tau_m, 16.3, 0.3);
513
+ // σ_prime_a with Kf·σ_a only (no mean bending, no alternating torque) ≈ Kf·σ_a = 130.4
514
+ test('shaft σ′_a', sh.stresses.sigma_prime_a, 130.4, 1.0);
515
+ // σ_prime_m = √3 · Kfs · τ_m ≈ √3 · 1.5 · 16.3 ≈ 42.4
516
+ test('shaft σ′_m', sh.stresses.sigma_prime_m, 42.4, 1.0);
517
+ // Must be finite + positive
518
+ test('shaft n_Goodman finite', Number.isFinite(sh.n_Goodman) && sh.n_Goodman > 0 ? 1 : 0, 1, 0);
519
+ test('shaft n_Soderberg ≤ Goodman', (sh.n_Soderberg <= sh.n_Goodman + 1e-6) ? 1 : 0, 1, 0);
520
+
209
521
  return { results, allPass: results.every(r => r.pass) };
210
522
  }
211
523
 
@@ -585,8 +897,20 @@
585
897
  // ===================================================================
586
898
  let uiEl = null;
587
899
  window.CycleCAD.AIEngineer = {
900
+ // ---- v1: bolted-joint ----
588
901
  analyze: boltedJointAnalysis,
589
902
  parsePrompt: parseBoltedJointPrompt,
903
+ // ---- v2: gears ----
904
+ analyzeGear: spurGearAnalysis,
905
+ gearAllowableBending,
906
+ gearAllowableContact,
907
+ gearGeometryJ,
908
+ gearGeometryI,
909
+ // ---- v2: shafts ----
910
+ analyzeShaft: shaftFatigueAnalysis,
911
+ SHAFT_MATERIALS,
912
+ SURFACE_FACTORS,
913
+ // ---- shared ----
590
914
  runSelfTests,
591
915
  STEEL_GRADES,
592
916
  BOLT_STRESS_AREA,
@@ -597,17 +921,19 @@
597
921
  if (!t.allPass) {
598
922
  console.warn('[AI Engineer] self-test failures:', t.results.filter(r => !r.pass));
599
923
  } else {
600
- console.log('[AI Engineer] self-tests pass (' + t.results.length + '/' + t.results.length + ' against MecAgent reference values)');
924
+ console.log('[AI Engineer] self-tests pass (' + t.results.length + '/' + t.results.length + ' across bolted-joint + gears + shafts)');
601
925
  }
602
926
  return t.allPass;
603
927
  },
604
928
  getUI: () => { if (!uiEl) uiEl = buildUI(); return uiEl; },
605
929
  execute: (cmd, params) => {
606
- if (cmd === 'analyze') return boltedJointAnalysis(params || {});
607
- if (cmd === 'parse') return parseBoltedJointPrompt((params && params.prompt) || '');
608
- if (cmd === 'show') { if (!uiEl) uiEl = buildUI(); return uiEl; }
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; }
609
935
  }
610
936
  };
611
937
 
612
- console.log('AI Engineering Analyst v1.0 module loaded');
938
+ console.log('AI Engineering Analyst v2.0 module loaded (bolted-joint + gears + shafts)');
613
939
  })();