cyclecad 3.10.4 → 3.11.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/.github/workflows/pages.yml +34 -0
- package/.nojekyll +0 -0
- package/CLAUDE.md +348 -3
- package/HANDOFF-2026-04-24-session-2.md +239 -0
- package/HANDOFF-2026-04-24.md +90 -0
- package/app/index.html +9 -0
- package/app/js/modules/ai-copilot.js +195 -2
- package/app/js/modules/ai-engineer.js +613 -0
- package/app/js/modules/pentacad-bridge.js +216 -0
- package/app/js/modules/pentacad-cam.js +184 -0
- package/app/js/modules/pentacad-sim.js +215 -0
- package/app/js/modules/pentacad.js +233 -0
- package/app/pentacad.html +240 -0
- package/index-agent-first.html.bak +1306 -0
- package/machines/v2-50-chb/kinematics.json +51 -0
- package/mockups/cyclecad-suite-mockup.html +1746 -0
- package/package.json +1 -1
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
/* AI Engineering Analyst v1.0 — engineering analysis with cited methodology.
|
|
2
|
+
*
|
|
3
|
+
* v1 scope: bolted-joint analysis (VDI 2230 / Shigley methodology).
|
|
4
|
+
* Verified against MecAgent demo problem (4×M12/10.9, 18kN shear, 18kN axial, 420Nm moment,
|
|
5
|
+
* μ=0.16, preload 39kN/bolt, BCD 96mm, K_s=1.5) — expected F_friction=24960N,
|
|
6
|
+
* F_max_tensile=6687.5N, σ_vm=558MPa.
|
|
7
|
+
*
|
|
8
|
+
* Future scope (tasks #11 + #12): gears (AGMA), shafts (Goodman/Soderberg), bearings (L10),
|
|
9
|
+
* welds (throat stress), plus RAG citations.
|
|
10
|
+
*
|
|
11
|
+
* Architecture:
|
|
12
|
+
* - Pure-JS analytical core (0 deps) — deterministic, unit-tested.
|
|
13
|
+
* - Natural-language parser extracts structured parameters from free-text.
|
|
14
|
+
* - Form UI with live recompute + LaTeX render (KaTeX, loaded on demand from CDN).
|
|
15
|
+
* - LLM layer (optional v1.1) calls the analytical core for every number — never fabricates results.
|
|
16
|
+
*/
|
|
17
|
+
(function(){
|
|
18
|
+
'use strict';
|
|
19
|
+
window.CycleCAD = window.CycleCAD || {};
|
|
20
|
+
|
|
21
|
+
// ===================================================================
|
|
22
|
+
// REFERENCE DATA — ISO 898-1 mechanical property classes for carbon steel bolts
|
|
23
|
+
// Values: {nominalTensile R_m, proofStress R_p0.2, yieldStrength R_el} all in MPa
|
|
24
|
+
// ===================================================================
|
|
25
|
+
const STEEL_GRADES = Object.freeze({
|
|
26
|
+
'4.6': { R_m: 400, R_p02: 225, R_el: 240, label: '4.6 (low-carbon steel, mild)' },
|
|
27
|
+
'4.8': { R_m: 400, R_p02: 310, R_el: 320, label: '4.8 (low-carbon)' },
|
|
28
|
+
'5.6': { R_m: 500, R_p02: 300, R_el: 300, label: '5.6 (medium-carbon)' },
|
|
29
|
+
'5.8': { R_m: 500, R_p02: 380, R_el: 400, label: '5.8 (medium-carbon)' },
|
|
30
|
+
'6.8': { R_m: 600, R_p02: 440, R_el: 480, label: '6.8 (medium-carbon)' },
|
|
31
|
+
'8.8': { R_m: 800, R_p02: 580, R_el: 640, label: '8.8 (quenched & tempered, most common)' },
|
|
32
|
+
'10.9': { R_m: 1000, R_p02: 830, R_el: 900, label: '10.9 (alloy, quenched & tempered)' },
|
|
33
|
+
'12.9': { R_m: 1200, R_p02: 970, R_el: 1080, label: '12.9 (alloy, Q&T high-strength)' }
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// DIN 13 stress cross-section area A_s in mm² for standard metric coarse threads.
|
|
37
|
+
// A_s = π/4 · ((d_2 + d_3) / 2)² where d_2 is pitch dia, d_3 is minor dia (nut thread root).
|
|
38
|
+
const BOLT_STRESS_AREA = Object.freeze({
|
|
39
|
+
M3: 5.03, M3_5: 6.78, M4: 8.78, M5: 14.2, M6: 20.1, M7: 28.9,
|
|
40
|
+
M8: 36.6, M10: 58.0, M12: 84.3, M14: 115, M16: 157, M18: 192,
|
|
41
|
+
M20: 245, M22: 303, M24: 353, M27: 459, M30: 561,
|
|
42
|
+
M33: 694, M36: 817, M39: 976, M42: 1120, M45: 1300, M48: 1470,
|
|
43
|
+
M52: 1760, M56: 2030
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Nominal (major) diameter in mm for standard metric coarse threads.
|
|
47
|
+
const BOLT_MAJOR_DIA = Object.freeze({
|
|
48
|
+
M3: 3, M4: 4, M5: 5, M6: 6, M8: 8, M10: 10, M12: 12, M14: 14, M16: 16,
|
|
49
|
+
M18: 18, M20: 20, M22: 22, M24: 24, M27: 27, M30: 30, M33: 33, M36: 36,
|
|
50
|
+
M39: 39, M42: 42, M45: 45, M48: 48, M52: 52, M56: 56
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Typical friction coefficients for clamped-joint faying surfaces (from VDI 2230 Table A6).
|
|
54
|
+
const FRICTION_PRESETS = Object.freeze({
|
|
55
|
+
'dry_steel_steel': 0.16,
|
|
56
|
+
'oiled_steel_steel': 0.12,
|
|
57
|
+
'aluminum_aluminum': 0.18,
|
|
58
|
+
'zinc_coated': 0.14,
|
|
59
|
+
'hot_dip_galvanized': 0.36,
|
|
60
|
+
'phosphated': 0.16,
|
|
61
|
+
'blasted_clean': 0.45
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ===================================================================
|
|
65
|
+
// ANALYTICAL CORE — bolted-joint slip + tension + combined stress
|
|
66
|
+
// ===================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Bolted joint safety analysis (VDI 2230 + Shigley approach).
|
|
70
|
+
*
|
|
71
|
+
* @param {object} p Input parameters.
|
|
72
|
+
* @param {number} p.boltCount Number of bolts z (≥1).
|
|
73
|
+
* @param {string} p.grade Steel grade, e.g. '10.9'.
|
|
74
|
+
* @param {string} p.thread Thread designation, e.g. 'M12'.
|
|
75
|
+
* @param {number} p.preload Per-bolt preload F_V [N].
|
|
76
|
+
* @param {number} p.shearForce External transverse/shear force F_Q [N].
|
|
77
|
+
* @param {number} p.axialForce External axial/separating force F_A [N].
|
|
78
|
+
* @param {number} p.moment In-plane moment M [N·mm].
|
|
79
|
+
* @param {number} p.bcd Bolt-circle diameter [mm].
|
|
80
|
+
* @param {number} p.friction Faying-surface coefficient μ.
|
|
81
|
+
* @param {number} p.safetyFactor Required slip safety factor K_s.
|
|
82
|
+
* @param {number} [p.frictionInterfaces=1] Number of friction-bearing interfaces n.
|
|
83
|
+
* @returns {object} Structured result with inputs, slip/tension/stress checks, verdict.
|
|
84
|
+
*/
|
|
85
|
+
function boltedJointAnalysis(p) {
|
|
86
|
+
// Input normalisation
|
|
87
|
+
const z = Math.max(1, Math.round(Number(p.boltCount) || 4));
|
|
88
|
+
const grade = STEEL_GRADES[p.grade] ? p.grade : '8.8';
|
|
89
|
+
const thread = BOLT_STRESS_AREA[p.thread] ? p.thread : 'M12';
|
|
90
|
+
const F_preload = Math.max(0, Number(p.preload) || 0);
|
|
91
|
+
const F_shear = Math.max(0, Number(p.shearForce) || 0);
|
|
92
|
+
const F_axial = Math.max(0, Number(p.axialForce) || 0);
|
|
93
|
+
const M = Math.max(0, Number(p.moment) || 0);
|
|
94
|
+
const bcd = Math.max(0, Number(p.bcd) || 0);
|
|
95
|
+
const mu = Math.max(0.01, Number(p.friction) || 0.15);
|
|
96
|
+
const K_s = Math.max(1, Number(p.safetyFactor) || 1.5);
|
|
97
|
+
const n_interfaces = Math.max(1, Math.round(Number(p.frictionInterfaces) || 1));
|
|
98
|
+
|
|
99
|
+
const A_s = BOLT_STRESS_AREA[thread];
|
|
100
|
+
const d = BOLT_MAJOR_DIA[thread];
|
|
101
|
+
const spec = STEEL_GRADES[grade];
|
|
102
|
+
const R_p02 = spec.R_p02;
|
|
103
|
+
const R_m = spec.R_m;
|
|
104
|
+
const r = bcd / 2;
|
|
105
|
+
|
|
106
|
+
// --- CHECK 1: Slip resistance ----------------------------------------------
|
|
107
|
+
// Friction capacity: F_friction = μ · n · z · F_V
|
|
108
|
+
const F_friction = mu * n_interfaces * z * F_preload;
|
|
109
|
+
|
|
110
|
+
// Per-bolt tangential force from in-plane moment (at BCD):
|
|
111
|
+
// F_M_tangential,i = M · r_i / Σr_j² → for a uniform circle: M / (z · r)
|
|
112
|
+
const F_moment_tangential_per_bolt = (r > 0) ? M / (z * r) : 0;
|
|
113
|
+
|
|
114
|
+
// Worst-case per-bolt tangential force (shear/z plus moment contribution, aligned):
|
|
115
|
+
const F_bolt_tangential_max = F_shear / z + F_moment_tangential_per_bolt;
|
|
116
|
+
|
|
117
|
+
// Aggregate tangential resultant that the joint must resist (sum of worst-case bolt forces):
|
|
118
|
+
const F_shear_total = z * F_bolt_tangential_max;
|
|
119
|
+
const F_required_slip = K_s * F_shear_total;
|
|
120
|
+
const slipSafe = F_friction >= F_required_slip;
|
|
121
|
+
const slipMargin = F_required_slip > 0 ? F_friction / F_required_slip : Infinity;
|
|
122
|
+
|
|
123
|
+
// --- CHECK 2: Bolt tension (worst-loaded bolt) ------------------------------
|
|
124
|
+
// External tension per bolt: F_A/z + contribution from moment at worst position
|
|
125
|
+
// (axial moment component adds on the tension side of the BCD)
|
|
126
|
+
const F_axial_per_bolt = F_axial / z;
|
|
127
|
+
const F_moment_axial_per_bolt = (r > 0) ? M / (z * r) : 0;
|
|
128
|
+
const F_max_external = F_axial_per_bolt + F_moment_axial_per_bolt;
|
|
129
|
+
|
|
130
|
+
// Simplified: assume load factor Φ = 1 (rigid joint). In reality 0.1-0.3 for gasketed.
|
|
131
|
+
// Full VDI 2230 uses F_S_max = F_V + Φ · F_A_ext. For v1, conservative (Φ=1):
|
|
132
|
+
const F_bolt_total = F_preload + F_max_external;
|
|
133
|
+
|
|
134
|
+
// --- CHECK 3: Combined stress (Von Mises) -----------------------------------
|
|
135
|
+
// σ = F_bolt_total / A_s (tensile)
|
|
136
|
+
// τ = F_bolt_tangential_max / A_s (shear from transverse loads + moment)
|
|
137
|
+
// σ_vm = √(σ² + 3τ²) (von Mises for uniaxial tension + shear)
|
|
138
|
+
const sigma = F_bolt_total / A_s;
|
|
139
|
+
const tau = F_bolt_tangential_max / A_s;
|
|
140
|
+
const sigma_vm = Math.sqrt(sigma * sigma + 3 * tau * tau);
|
|
141
|
+
const stressUtilization = R_p02 > 0 ? sigma_vm / R_p02 : Infinity;
|
|
142
|
+
const stressSafe = sigma_vm < R_p02;
|
|
143
|
+
|
|
144
|
+
// --- VERDICT ----------------------------------------------------------------
|
|
145
|
+
// Classification: joint is SAFE if both slip and stress checks pass.
|
|
146
|
+
// If stress passes but slip fails, joint is a BEARING-TYPE joint — bolts transmit shear
|
|
147
|
+
// directly. Still safe if stress is within proof strength.
|
|
148
|
+
let verdict, verdictClass, notes = [];
|
|
149
|
+
if (slipSafe && stressSafe) {
|
|
150
|
+
verdict = 'SAFE';
|
|
151
|
+
verdictClass = 'pass';
|
|
152
|
+
notes.push('Preload prevents slip; bolt stress stays below proof strength.');
|
|
153
|
+
} else if (!slipSafe && stressSafe) {
|
|
154
|
+
verdict = 'SAFE (bearing-type)';
|
|
155
|
+
verdictClass = 'warn';
|
|
156
|
+
notes.push('Slip resistance marginal — joint relies on bolt shank shear.');
|
|
157
|
+
notes.push('Consider increasing preload (higher Q_F) or adding friction (higher μ) to restore friction-type safety.');
|
|
158
|
+
} else {
|
|
159
|
+
verdict = 'UNSAFE';
|
|
160
|
+
verdictClass = 'fail';
|
|
161
|
+
if (!stressSafe) notes.push('Combined stress σ_vm = ' + sigma_vm.toFixed(1) + ' MPa exceeds proof strength ' + R_p02 + ' MPa.');
|
|
162
|
+
if (!slipSafe) notes.push('Insufficient friction capacity — bolts must also resist direct shear.');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
inputs: { z, grade, thread, d, A_s, F_preload, F_shear, F_axial, M, bcd, r, mu, K_s, n_interfaces, R_p02, R_m, gradeLabel: spec.label },
|
|
167
|
+
slipResistance: {
|
|
168
|
+
F_friction, F_moment_tangential_per_bolt, F_bolt_tangential_max,
|
|
169
|
+
F_shear_total, F_required: F_required_slip, margin: slipMargin, safe: slipSafe
|
|
170
|
+
},
|
|
171
|
+
tensionCheck: {
|
|
172
|
+
F_axial_per_bolt, F_moment_axial_per_bolt, F_max_external,
|
|
173
|
+
F_preload, F_bolt_total
|
|
174
|
+
},
|
|
175
|
+
combinedStress: {
|
|
176
|
+
sigma, tau, sigma_vm, R_p02, utilization: stressUtilization, safe: stressSafe
|
|
177
|
+
},
|
|
178
|
+
verdict, verdictClass, notes
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ===================================================================
|
|
183
|
+
// UNIT TESTS — verify core against MecAgent screenshot values
|
|
184
|
+
// ===================================================================
|
|
185
|
+
function runSelfTests() {
|
|
186
|
+
const results = [];
|
|
187
|
+
function test(name, actual, expected, tol) {
|
|
188
|
+
const pass = Math.abs(actual - expected) <= tol;
|
|
189
|
+
results.push({ name, actual, expected, pass });
|
|
190
|
+
return pass;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// MecAgent problem: 4 × M12 (10.9), F_Q=18kN, F_A=18kN, M=420Nm, μ=0.16, F_V=39kN/bolt, BCD=96mm, K_s=1.5
|
|
194
|
+
const r = boltedJointAnalysis({
|
|
195
|
+
boltCount: 4, grade: '10.9', thread: 'M12',
|
|
196
|
+
preload: 39000, shearForce: 18000, axialForce: 18000, moment: 420000,
|
|
197
|
+
bcd: 96, friction: 0.16, safetyFactor: 1.5
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Expected values from MecAgent screenshots:
|
|
201
|
+
test('F_friction', r.slipResistance.F_friction, 24960, 1); // 4·39000·0.16
|
|
202
|
+
test('F_moment/bolt', r.slipResistance.F_moment_tangential_per_bolt, 2187.5, 0.5); // 420000/(4·48)
|
|
203
|
+
test('F_bolt_max_tang', r.slipResistance.F_bolt_tangential_max, 6687.5, 0.5); // 18000/4 + 2187.5
|
|
204
|
+
test('F_bolt_total', r.tensionCheck.F_bolt_total, 45687.5, 1); // 39000 + 4500 + 2187.5
|
|
205
|
+
test('σ (tensile)', r.combinedStress.sigma, 542, 1); // 45687.5/84.3
|
|
206
|
+
test('τ (shear)', r.combinedStress.tau, 79, 1.5); // 6687.5/84.3 ≈ 79.3
|
|
207
|
+
test('σ_vm', r.combinedStress.sigma_vm, 558, 2); // √(542² + 3·79²)
|
|
208
|
+
|
|
209
|
+
return { results, allPass: results.every(r => r.pass) };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ===================================================================
|
|
213
|
+
// NATURAL-LANGUAGE PARSER — extract structured params from a free-text prompt
|
|
214
|
+
// ===================================================================
|
|
215
|
+
function parseBoltedJointPrompt(prompt) {
|
|
216
|
+
const p = (prompt || '').toLowerCase();
|
|
217
|
+
const res = {};
|
|
218
|
+
|
|
219
|
+
// Bolt count: "4 x m12 bolts", "four bolts", "z=4"
|
|
220
|
+
const wordNums = { one:1, two:2, three:3, four:4, five:5, six:6, seven:7, eight:8, ten:10, twelve:12 };
|
|
221
|
+
const mCount = p.match(/\b(\d+)\s*[x×]?\s*(?:m\d+\s+)?bolts?\b/) ||
|
|
222
|
+
p.match(/\bz\s*=\s*(\d+)\b/) ||
|
|
223
|
+
p.match(/\b(one|two|three|four|five|six|seven|eight|ten|twelve)\s*bolts?\b/);
|
|
224
|
+
if (mCount) res.boltCount = wordNums[mCount[1]] || parseInt(mCount[1]);
|
|
225
|
+
|
|
226
|
+
// Thread: "M12", "m8"
|
|
227
|
+
const mThread = p.match(/\bm\s*(\d+(?:\.\d+)?)\b/i);
|
|
228
|
+
if (mThread) {
|
|
229
|
+
const sz = parseFloat(mThread[1]);
|
|
230
|
+
const key = 'M' + (Number.isInteger(sz) ? sz : sz.toString().replace('.', '_'));
|
|
231
|
+
if (BOLT_STRESS_AREA[key]) res.thread = key;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Grade: "10.9", "class 8.8", "property class 10.9"
|
|
235
|
+
const mGrade = p.match(/\b(?:class|grade|property\s*class)\s*(\d+\.\d+)\b/i) ||
|
|
236
|
+
p.match(/\b(4\.6|4\.8|5\.6|5\.8|6\.8|8\.8|10\.9|12\.9)\b/);
|
|
237
|
+
if (mGrade) res.grade = mGrade[1];
|
|
238
|
+
|
|
239
|
+
// Forces — support kN and N units
|
|
240
|
+
const numUnit = (m) => {
|
|
241
|
+
if (!m) return null;
|
|
242
|
+
const val = parseFloat(m[1]);
|
|
243
|
+
const unit = (m[2] || '').toLowerCase();
|
|
244
|
+
if (unit.startsWith('kn')) return val * 1000;
|
|
245
|
+
return val;
|
|
246
|
+
};
|
|
247
|
+
const mShear = p.match(/(?:shear|transverse|shearing)\s*(?:force|load)?[^\d-]{0,12}(\d+(?:\.\d+)?)\s*(kn|n)\b/i);
|
|
248
|
+
if (mShear) res.shearForce = numUnit(mShear);
|
|
249
|
+
const mAxial = p.match(/(?:axial|separating|tensile)\s*(?:force|load)?[^\d-]{0,12}(\d+(?:\.\d+)?)\s*(kn|n)\b/i);
|
|
250
|
+
if (mAxial) res.axialForce = numUnit(mAxial);
|
|
251
|
+
|
|
252
|
+
// Moment (N·m or kN·m → convert to N·mm)
|
|
253
|
+
const mMoment = p.match(/(?:in.plane\s*)?moment[^\d-]{0,12}(\d+(?:\.\d+)?)\s*(nm|knm|kn\s*m|n\s*m)\b/i);
|
|
254
|
+
if (mMoment) {
|
|
255
|
+
const val = parseFloat(mMoment[1]);
|
|
256
|
+
const unit = mMoment[2].toLowerCase().replace(/\s/g, '');
|
|
257
|
+
res.moment = unit.startsWith('kn') ? val * 1e6 : val * 1000;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Preload
|
|
261
|
+
const mPre = p.match(/pre[-\s]?load(?:\s*per\s*bolt)?[^\d-]{0,12}(\d+(?:\.\d+)?)\s*(kn|n)\b/i);
|
|
262
|
+
if (mPre) res.preload = numUnit(mPre);
|
|
263
|
+
|
|
264
|
+
// BCD
|
|
265
|
+
const mBcd = p.match(/(?:bcd|bolt\s*circle[^\d]*?diameter|bolt\s*circle)[^\d-]{0,12}(\d+(?:\.\d+)?)\s*mm/i);
|
|
266
|
+
if (mBcd) res.bcd = parseFloat(mBcd[1]);
|
|
267
|
+
|
|
268
|
+
// Friction
|
|
269
|
+
const mFric = p.match(/(?:friction\s*(?:coefficient)?|[μu])[^\d-]{0,8}(\d*\.\d+)/i);
|
|
270
|
+
if (mFric) res.friction = parseFloat(mFric[1]);
|
|
271
|
+
|
|
272
|
+
// Safety factor — allow common phrasings like "safety factor against slipping: 1.5"
|
|
273
|
+
const mSf = p.match(/(?:safety\s*factor|k\s*_?\s*s|\bks\b)\s*(?:against\s+\w+)?\s*[:=]?\s*(\d+(?:\.\d+)?)/i);
|
|
274
|
+
if (mSf) res.safetyFactor = parseFloat(mSf[1]);
|
|
275
|
+
|
|
276
|
+
return res;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ===================================================================
|
|
280
|
+
// LaTeX / KaTeX integration — load on demand so it doesn't bloat initial page
|
|
281
|
+
// ===================================================================
|
|
282
|
+
let _katexLoading = null;
|
|
283
|
+
async function loadKaTeX() {
|
|
284
|
+
if (typeof window.katex !== 'undefined') return window.katex;
|
|
285
|
+
if (_katexLoading) return _katexLoading;
|
|
286
|
+
_katexLoading = new Promise((resolve, reject) => {
|
|
287
|
+
const css = document.createElement('link');
|
|
288
|
+
css.rel = 'stylesheet';
|
|
289
|
+
css.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css';
|
|
290
|
+
document.head.appendChild(css);
|
|
291
|
+
const s = document.createElement('script');
|
|
292
|
+
s.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js';
|
|
293
|
+
s.onload = () => resolve(window.katex);
|
|
294
|
+
s.onerror = () => reject(new Error('KaTeX failed to load from CDN'));
|
|
295
|
+
document.head.appendChild(s);
|
|
296
|
+
});
|
|
297
|
+
return _katexLoading;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function renderMath(el, latex, displayMode) {
|
|
301
|
+
el.textContent = latex; // fallback while KaTeX loads / if it fails
|
|
302
|
+
loadKaTeX().then(k => {
|
|
303
|
+
try { k.render(latex, el, { displayMode: !!displayMode, throwOnError: false, strict: 'ignore' }); }
|
|
304
|
+
catch(e) { el.textContent = latex; }
|
|
305
|
+
}).catch(() => {});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ===================================================================
|
|
309
|
+
// UI — form-based input with live recompute + formatted report
|
|
310
|
+
// ===================================================================
|
|
311
|
+
const S = { lastResult: null, els: {} };
|
|
312
|
+
|
|
313
|
+
const INPUT_FIELDS = [
|
|
314
|
+
// [key, label, unit, default, min]
|
|
315
|
+
['boltCount', 'Bolt count (z)', '', 4, 1],
|
|
316
|
+
['thread', 'Thread', '', 'M12', null, 'select', Object.keys(BOLT_STRESS_AREA)],
|
|
317
|
+
['grade', 'Grade (ISO 898-1)', '', '8.8', null, 'select', Object.keys(STEEL_GRADES)],
|
|
318
|
+
['preload', 'Preload per bolt (F_V)', 'N', 25000, 0],
|
|
319
|
+
['shearForce', 'Shear force (F_Q)', 'N', 10000, 0],
|
|
320
|
+
['axialForce', 'Axial/separating force (F_A)', 'N', 5000, 0],
|
|
321
|
+
['moment', 'In-plane moment (M)', 'N·mm', 100000,0],
|
|
322
|
+
['bcd', 'Bolt-circle diameter (BCD)', 'mm', 80, 0],
|
|
323
|
+
['friction', 'Friction coefficient (μ)', '', 0.15, 0.01],
|
|
324
|
+
['safetyFactor', 'Slip safety factor (K_s)', '', 1.5, 1],
|
|
325
|
+
['frictionInterfaces','Friction interfaces (n)', '', 1, 1]
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
function fmt(n, unit, digits) {
|
|
329
|
+
if (!isFinite(n)) return '—';
|
|
330
|
+
digits = digits ?? (Math.abs(n) >= 1000 ? 0 : Math.abs(n) >= 10 ? 1 : 2);
|
|
331
|
+
const s = Number(n).toFixed(digits);
|
|
332
|
+
return unit ? s + ' ' + unit : s;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function collectInputs() {
|
|
336
|
+
const out = {};
|
|
337
|
+
INPUT_FIELDS.forEach(([key,,,,, type]) => {
|
|
338
|
+
const el = S.els['in_' + key];
|
|
339
|
+
if (!el) return;
|
|
340
|
+
out[key] = (type === 'select') ? el.value : parseFloat(el.value);
|
|
341
|
+
});
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function compute() {
|
|
346
|
+
try {
|
|
347
|
+
const params = collectInputs();
|
|
348
|
+
const r = boltedJointAnalysis(params);
|
|
349
|
+
S.lastResult = r;
|
|
350
|
+
renderReport(r);
|
|
351
|
+
} catch(e) {
|
|
352
|
+
if (S.els.report) S.els.report.innerHTML = '<div class="aie-err">Error: ' + e.message + '</div>';
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function renderReport(r) {
|
|
357
|
+
const root = S.els.report;
|
|
358
|
+
if (!root) return;
|
|
359
|
+
root.innerHTML = '';
|
|
360
|
+
|
|
361
|
+
// Verdict banner
|
|
362
|
+
const verdictBg = r.verdictClass === 'pass' ? '#065f46' : r.verdictClass === 'warn' ? '#78350f' : '#7f1d1d';
|
|
363
|
+
const verdictFg = r.verdictClass === 'pass' ? '#d1fae5' : r.verdictClass === 'warn' ? '#fed7aa' : '#fecaca';
|
|
364
|
+
const banner = document.createElement('div');
|
|
365
|
+
banner.className = 'aie-verdict';
|
|
366
|
+
banner.style.cssText = 'padding:10px 14px;border-radius:6px;background:'+verdictBg+';color:'+verdictFg+';font-weight:600;font-size:14px;margin-bottom:12px;border:1px solid rgba(255,255,255,0.08)';
|
|
367
|
+
banner.textContent = 'Verdict: ' + r.verdict;
|
|
368
|
+
root.appendChild(banner);
|
|
369
|
+
if (r.notes?.length) {
|
|
370
|
+
const notes = document.createElement('ul');
|
|
371
|
+
notes.style.cssText = 'margin:4px 0 14px 18px;font-size:12px;color:#94a3b8';
|
|
372
|
+
r.notes.forEach(n => { const li = document.createElement('li'); li.textContent = n; notes.appendChild(li); });
|
|
373
|
+
root.appendChild(notes);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// CHECK 1: Slip resistance
|
|
377
|
+
const s = r.slipResistance, i = r.inputs;
|
|
378
|
+
const sec1 = section('1. Slip resistance', s.safe);
|
|
379
|
+
sec1.appendChild(math(
|
|
380
|
+
'F_{friction} = \\mu \\cdot n \\cdot z \\cdot F_V = '
|
|
381
|
+
+ i.mu + ' \\cdot ' + i.n_interfaces + ' \\cdot ' + i.z + ' \\cdot ' + i.F_preload
|
|
382
|
+
+ ' = \\mathbf{' + s.F_friction.toFixed(0) + '}\\;\\text{N}', true));
|
|
383
|
+
sec1.appendChild(math(
|
|
384
|
+
'F_{M,tangential/bolt} = \\frac{M}{z \\cdot r} = \\frac{' + i.M + '}{' + i.z + ' \\cdot ' + i.r.toFixed(1) + '} = '
|
|
385
|
+
+ s.F_moment_tangential_per_bolt.toFixed(1) + '\\;\\text{N}', true));
|
|
386
|
+
sec1.appendChild(math(
|
|
387
|
+
'F_{bolt,tangential,max} = \\frac{F_Q}{z} + F_{M,tangential/bolt} = '
|
|
388
|
+
+ (i.F_shear/i.z).toFixed(1) + ' + ' + s.F_moment_tangential_per_bolt.toFixed(1)
|
|
389
|
+
+ ' = \\mathbf{' + s.F_bolt_tangential_max.toFixed(1) + '}\\;\\text{N}', true));
|
|
390
|
+
sec1.appendChild(math(
|
|
391
|
+
'F_{required} = K_s \\cdot z \\cdot F_{bolt,tangential,max} = '
|
|
392
|
+
+ i.K_s + ' \\cdot ' + i.z + ' \\cdot ' + s.F_bolt_tangential_max.toFixed(1)
|
|
393
|
+
+ ' = \\mathbf{' + s.F_required.toFixed(0) + '}\\;\\text{N}', true));
|
|
394
|
+
const slipResult = document.createElement('div');
|
|
395
|
+
slipResult.style.cssText = 'margin-top:6px;font-size:12px;color:' + (s.safe ? '#a7f3d0' : '#fca5a5');
|
|
396
|
+
slipResult.innerHTML = s.safe
|
|
397
|
+
? '✓ ' + s.F_friction.toFixed(0) + ' N ≥ ' + s.F_required.toFixed(0) + ' N (margin ×' + s.margin.toFixed(2) + ')'
|
|
398
|
+
: '✗ ' + s.F_friction.toFixed(0) + ' N < ' + s.F_required.toFixed(0) + ' N (deficit: ' + (s.F_required - s.F_friction).toFixed(0) + ' N)';
|
|
399
|
+
sec1.appendChild(slipResult);
|
|
400
|
+
root.appendChild(sec1);
|
|
401
|
+
|
|
402
|
+
// CHECK 2: Bolt tension
|
|
403
|
+
const t = r.tensionCheck;
|
|
404
|
+
const sec2 = section('2. Maximum bolt tension', true /* always informational */);
|
|
405
|
+
sec2.appendChild(math(
|
|
406
|
+
'F_{A,bolt} = \\frac{F_A}{z} = \\frac{' + i.F_axial + '}{' + i.z + '} = ' + t.F_axial_per_bolt.toFixed(1) + '\\;\\text{N}', true));
|
|
407
|
+
sec2.appendChild(math(
|
|
408
|
+
'F_{M,axial/bolt} = \\frac{M}{z \\cdot r} = ' + t.F_moment_axial_per_bolt.toFixed(1) + '\\;\\text{N}', true));
|
|
409
|
+
sec2.appendChild(math(
|
|
410
|
+
'F_{max,external} = F_{A,bolt} + F_{M,axial/bolt} = \\mathbf{' + t.F_max_external.toFixed(1) + '}\\;\\text{N}', true));
|
|
411
|
+
sec2.appendChild(math(
|
|
412
|
+
'F_{bolt,total} = F_V + F_{max,external} = ' + i.F_preload + ' + ' + t.F_max_external.toFixed(1)
|
|
413
|
+
+ ' = \\mathbf{' + t.F_bolt_total.toFixed(1) + '}\\;\\text{N}', true));
|
|
414
|
+
root.appendChild(sec2);
|
|
415
|
+
|
|
416
|
+
// CHECK 3: Combined stress
|
|
417
|
+
const c = r.combinedStress;
|
|
418
|
+
const sec3 = section('3. Combined stress (von Mises)', c.safe);
|
|
419
|
+
sec3.appendChild(math(
|
|
420
|
+
'\\sigma = \\frac{F_{bolt,total}}{A_s} = \\frac{' + t.F_bolt_total.toFixed(1) + '}{' + i.A_s + '} = \\mathbf{'
|
|
421
|
+
+ c.sigma.toFixed(1) + '}\\;\\text{MPa}', true));
|
|
422
|
+
sec3.appendChild(math(
|
|
423
|
+
'\\tau = \\frac{F_{bolt,tangential,max}}{A_s} = \\frac{' + s.F_bolt_tangential_max.toFixed(1) + '}{' + i.A_s + '} = \\mathbf{'
|
|
424
|
+
+ c.tau.toFixed(1) + '}\\;\\text{MPa}', true));
|
|
425
|
+
sec3.appendChild(math(
|
|
426
|
+
'\\sigma_{vm} = \\sqrt{\\sigma^2 + 3\\tau^2} = \\sqrt{' + c.sigma.toFixed(1) + '^2 + 3 \\cdot ' + c.tau.toFixed(1)
|
|
427
|
+
+ '^2} = \\mathbf{' + c.sigma_vm.toFixed(1) + '}\\;\\text{MPa}', true));
|
|
428
|
+
const stressResult = document.createElement('div');
|
|
429
|
+
stressResult.style.cssText = 'margin-top:6px;font-size:12px;color:' + (c.safe ? '#a7f3d0' : '#fca5a5');
|
|
430
|
+
stressResult.innerHTML = c.safe
|
|
431
|
+
? '✓ σ_vm = ' + c.sigma_vm.toFixed(1) + ' MPa < proof strength R_p0.2 = ' + c.R_p02 + ' MPa (class '+i.grade+') (utilisation ' + (c.utilization*100).toFixed(1) + '%)'
|
|
432
|
+
: '✗ σ_vm = ' + c.sigma_vm.toFixed(1) + ' MPa ≥ R_p0.2 = ' + c.R_p02 + ' MPa (class '+i.grade+') — BOLT WILL YIELD';
|
|
433
|
+
sec3.appendChild(stressResult);
|
|
434
|
+
root.appendChild(sec3);
|
|
435
|
+
|
|
436
|
+
// Metadata footer
|
|
437
|
+
const meta = document.createElement('div');
|
|
438
|
+
meta.style.cssText = 'margin-top:14px;padding-top:10px;border-top:1px solid #334155;font-size:11px;color:#64748b';
|
|
439
|
+
meta.innerHTML = 'Method: VDI 2230 / Shigley simplified (no load-factor Φ — conservative Φ=1). ' +
|
|
440
|
+
'Thread ' + i.thread + ' A_s = ' + i.A_s + ' mm² (DIN 13). Grade ' + i.grade + ' R_p0.2 = ' + i.R_p02 + ' MPa, R_m = ' + i.R_m + ' MPa.<br>' +
|
|
441
|
+
'This is a first-pass analysis — always verify with detailed VDI 2230 if safety-critical.';
|
|
442
|
+
root.appendChild(meta);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function section(title, safe) {
|
|
446
|
+
const wrap = document.createElement('div');
|
|
447
|
+
wrap.style.cssText = 'margin-bottom:14px;padding:10px 12px;background:#0f172a;border:1px solid #334155;border-left:3px solid '+(safe?'#10b981':'#ef4444')+';border-radius:4px';
|
|
448
|
+
const h = document.createElement('div');
|
|
449
|
+
h.style.cssText = 'font-size:13px;font-weight:600;color:#e5e7eb;margin-bottom:8px';
|
|
450
|
+
h.textContent = title;
|
|
451
|
+
wrap.appendChild(h);
|
|
452
|
+
return wrap;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function math(latex, block) {
|
|
456
|
+
const el = document.createElement('div');
|
|
457
|
+
el.style.cssText = 'margin:4px 0;font-size:13px;color:#cbd5e1;overflow-x:auto';
|
|
458
|
+
renderMath(el, latex, block);
|
|
459
|
+
return el;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function applyFromPrompt() {
|
|
463
|
+
const prompt = S.els.prompt.value;
|
|
464
|
+
if (!prompt.trim()) return;
|
|
465
|
+
const parsed = parseBoltedJointPrompt(prompt);
|
|
466
|
+
Object.entries(parsed).forEach(([k, v]) => {
|
|
467
|
+
const el = S.els['in_' + k];
|
|
468
|
+
if (el) el.value = String(v);
|
|
469
|
+
});
|
|
470
|
+
compute();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function buildUI() {
|
|
474
|
+
const wrap = document.createElement('div');
|
|
475
|
+
wrap.className = 'aie-panel';
|
|
476
|
+
wrap.style.cssText = 'display:flex;flex-direction:column;gap:10px;padding:12px;min-width:460px;max-width:720px;font-family:-apple-system,sans-serif;color:#e5e7eb';
|
|
477
|
+
|
|
478
|
+
// Header
|
|
479
|
+
const header = document.createElement('div');
|
|
480
|
+
header.innerHTML = '<div style="font-size:15px;font-weight:700;color:#f1f5f9">AI Engineering Analyst</div>'
|
|
481
|
+
+ '<div style="font-size:11px;color:#94a3b8;margin-top:2px">Bolted-joint analysis (VDI 2230 / Shigley) — v1</div>';
|
|
482
|
+
wrap.appendChild(header);
|
|
483
|
+
|
|
484
|
+
// Prompt box for natural-language entry
|
|
485
|
+
const promptWrap = document.createElement('div');
|
|
486
|
+
promptWrap.style.cssText = 'display:flex;gap:6px';
|
|
487
|
+
const prompt = document.createElement('input');
|
|
488
|
+
prompt.type = 'text';
|
|
489
|
+
prompt.placeholder = 'Describe the joint (e.g. "4 × M12 bolts class 10.9, shear 18kN, axial 18kN, moment 420Nm, preload 39kN, BCD 96mm, μ=0.16, K_s=1.5")';
|
|
490
|
+
prompt.style.cssText = 'flex:1;padding:8px;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:4px;font:12px inherit';
|
|
491
|
+
const applyBtn = document.createElement('button');
|
|
492
|
+
applyBtn.textContent = 'Parse';
|
|
493
|
+
applyBtn.style.cssText = 'padding:8px 14px;background:#38bdf8;color:#0f172a;border:0;border-radius:4px;font-weight:600;cursor:pointer';
|
|
494
|
+
applyBtn.onclick = applyFromPrompt;
|
|
495
|
+
prompt.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); applyFromPrompt(); } });
|
|
496
|
+
S.els.prompt = prompt;
|
|
497
|
+
promptWrap.appendChild(prompt);
|
|
498
|
+
promptWrap.appendChild(applyBtn);
|
|
499
|
+
wrap.appendChild(promptWrap);
|
|
500
|
+
|
|
501
|
+
// Input grid
|
|
502
|
+
const grid = document.createElement('div');
|
|
503
|
+
grid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px 12px;padding:10px;background:#1e293b;border-radius:6px';
|
|
504
|
+
INPUT_FIELDS.forEach(field => {
|
|
505
|
+
const [key, label, unit, def, min, type, opts] = field;
|
|
506
|
+
const cell = document.createElement('label');
|
|
507
|
+
cell.style.cssText = 'display:flex;flex-direction:column;gap:2px;font-size:11px;color:#94a3b8';
|
|
508
|
+
const lbl = document.createElement('span');
|
|
509
|
+
lbl.innerHTML = label + (unit ? ' <span style="color:#64748b">['+unit+']</span>' : '');
|
|
510
|
+
let input;
|
|
511
|
+
if (type === 'select') {
|
|
512
|
+
input = document.createElement('select');
|
|
513
|
+
input.style.cssText = 'padding:5px;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:3px;font-size:12px';
|
|
514
|
+
opts.forEach(v => { const o = document.createElement('option'); o.value = v; o.textContent = v; input.appendChild(o); });
|
|
515
|
+
input.value = def;
|
|
516
|
+
} else {
|
|
517
|
+
input = document.createElement('input');
|
|
518
|
+
input.type = 'number';
|
|
519
|
+
input.step = 'any';
|
|
520
|
+
if (min !== null) input.min = String(min);
|
|
521
|
+
input.value = String(def);
|
|
522
|
+
input.style.cssText = 'padding:5px;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:3px;font-size:12px';
|
|
523
|
+
}
|
|
524
|
+
input.addEventListener('input', compute);
|
|
525
|
+
input.addEventListener('change', compute);
|
|
526
|
+
S.els['in_' + key] = input;
|
|
527
|
+
cell.appendChild(lbl);
|
|
528
|
+
cell.appendChild(input);
|
|
529
|
+
grid.appendChild(cell);
|
|
530
|
+
});
|
|
531
|
+
wrap.appendChild(grid);
|
|
532
|
+
|
|
533
|
+
// Preset examples
|
|
534
|
+
const presets = document.createElement('div');
|
|
535
|
+
presets.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px';
|
|
536
|
+
[
|
|
537
|
+
{ 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 } },
|
|
538
|
+
{ 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 } },
|
|
539
|
+
{ 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 } }
|
|
540
|
+
].forEach(preset => {
|
|
541
|
+
const b = document.createElement('button');
|
|
542
|
+
b.textContent = preset.label;
|
|
543
|
+
b.style.cssText = 'padding:4px 8px;background:#334155;color:#cbd5e1;border:0;border-radius:3px;cursor:pointer;font-size:11px';
|
|
544
|
+
b.onclick = () => {
|
|
545
|
+
Object.entries(preset.values).forEach(([k, v]) => { const el = S.els['in_' + k]; if (el) el.value = String(v); });
|
|
546
|
+
compute();
|
|
547
|
+
};
|
|
548
|
+
presets.appendChild(b);
|
|
549
|
+
});
|
|
550
|
+
wrap.appendChild(presets);
|
|
551
|
+
|
|
552
|
+
// Report area
|
|
553
|
+
const report = document.createElement('div');
|
|
554
|
+
report.className = 'aie-report';
|
|
555
|
+
report.style.cssText = 'padding:4px 0';
|
|
556
|
+
S.els.report = report;
|
|
557
|
+
wrap.appendChild(report);
|
|
558
|
+
|
|
559
|
+
// Self-test panel (collapsed by default)
|
|
560
|
+
const testDetails = document.createElement('details');
|
|
561
|
+
testDetails.style.cssText = 'font-size:11px;color:#64748b;margin-top:6px';
|
|
562
|
+
const summary = document.createElement('summary');
|
|
563
|
+
summary.textContent = 'Verification: MecAgent reference values';
|
|
564
|
+
summary.style.cssText = 'cursor:pointer;user-select:none';
|
|
565
|
+
testDetails.appendChild(summary);
|
|
566
|
+
const tests = runSelfTests();
|
|
567
|
+
const testList = document.createElement('ul');
|
|
568
|
+
testList.style.cssText = 'margin:6px 0 0 20px;font:11px/1.5 SF Mono,monospace';
|
|
569
|
+
tests.results.forEach(t => {
|
|
570
|
+
const li = document.createElement('li');
|
|
571
|
+
li.style.color = t.pass ? '#86efac' : '#fca5a5';
|
|
572
|
+
li.textContent = (t.pass ? '✓ ' : '✗ ') + t.name + ': actual=' + (+t.actual).toFixed(2) + ' expected≈' + t.expected;
|
|
573
|
+
testList.appendChild(li);
|
|
574
|
+
});
|
|
575
|
+
testDetails.appendChild(testList);
|
|
576
|
+
wrap.appendChild(testDetails);
|
|
577
|
+
|
|
578
|
+
// Initial compute
|
|
579
|
+
setTimeout(compute, 0);
|
|
580
|
+
return wrap;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ===================================================================
|
|
584
|
+
// PUBLIC API
|
|
585
|
+
// ===================================================================
|
|
586
|
+
let uiEl = null;
|
|
587
|
+
window.CycleCAD.AIEngineer = {
|
|
588
|
+
analyze: boltedJointAnalysis,
|
|
589
|
+
parsePrompt: parseBoltedJointPrompt,
|
|
590
|
+
runSelfTests,
|
|
591
|
+
STEEL_GRADES,
|
|
592
|
+
BOLT_STRESS_AREA,
|
|
593
|
+
BOLT_MAJOR_DIA,
|
|
594
|
+
FRICTION_PRESETS,
|
|
595
|
+
init: () => {
|
|
596
|
+
const t = runSelfTests();
|
|
597
|
+
if (!t.allPass) {
|
|
598
|
+
console.warn('[AI Engineer] self-test failures:', t.results.filter(r => !r.pass));
|
|
599
|
+
} else {
|
|
600
|
+
console.log('[AI Engineer] self-tests pass (' + t.results.length + '/' + t.results.length + ' against MecAgent reference values)');
|
|
601
|
+
}
|
|
602
|
+
return t.allPass;
|
|
603
|
+
},
|
|
604
|
+
getUI: () => { if (!uiEl) uiEl = buildUI(); return uiEl; },
|
|
605
|
+
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; }
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
console.log('AI Engineering Analyst v1.0 module loaded');
|
|
613
|
+
})();
|