@vcad/mcp 0.9.4-main.35 → 0.9.4-main.38
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/README.md +1 -1
- package/index.mjs +626 -57
- package/package.json +3 -3
- package/vcad_kernel_wasm_bg.wasm +0 -0
package/README.md
CHANGED
|
@@ -6,4 +6,4 @@ vcad's MCP server as a self-contained bundle (server + BRep kernel WASM).
|
|
|
6
6
|
{ "mcpServers": { "vcad": { "command": "npx", "args": ["-y", "@vcad/mcp"] } } }
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Built from
|
|
9
|
+
Built from 254d4e9d13825ac74a895590703959d77898e1f3 at 2026-07-26T13:42:07.736Z. Source: https://github.com/ecto/vcad
|
package/index.mjs
CHANGED
|
@@ -11559,6 +11559,7 @@ var init_dist = __esm({
|
|
|
11559
11559
|
circuitTransient: wasmModule7.circuitTransient,
|
|
11560
11560
|
circuitTune: wasmModule7.circuitTune,
|
|
11561
11561
|
feaAnalyzeMesh: wasmModule7.feaAnalyzeMesh,
|
|
11562
|
+
feaCheckBeam: wasmModule7.feaCheckBeam,
|
|
11562
11563
|
emSimulate: wasmModule7.emSimulate,
|
|
11563
11564
|
antennaAnalyze: wasmModule7.antennaAnalyze,
|
|
11564
11565
|
photonicsSimulate: wasmModule7.photonicsSimulate,
|
|
@@ -11887,6 +11888,22 @@ var init_dist = __esm({
|
|
|
11887
11888
|
}
|
|
11888
11889
|
return fn(specJson, optionsJson, positions, indices);
|
|
11889
11890
|
}
|
|
11891
|
+
/**
|
|
11892
|
+
* Closed-form check of a prismatic member: exact section properties,
|
|
11893
|
+
* beam bending with the Timoshenko shear term, Bredt thin-wall (or
|
|
11894
|
+
* Saint-Venant series) torsion, and Euler buckling — the route for
|
|
11895
|
+
* thin-walled sheet-metal and tube-frame members, which no affordable
|
|
11896
|
+
* lattice pitch can resolve. Same fail-closed contract and
|
|
11897
|
+
* `vcad.fea-claims/1` predicted claims as `feaAnalyzeMesh`; see
|
|
11898
|
+
* `vcad-kernel-fea::section`.
|
|
11899
|
+
*/
|
|
11900
|
+
feaCheckBeam(caseJson) {
|
|
11901
|
+
const fn = this.kernel.feaCheckBeam;
|
|
11902
|
+
if (typeof fn !== "function") {
|
|
11903
|
+
throw new Error("feaCheckBeam is not exported by this kernel WASM build \u2014 rebuild packages/kernel-wasm");
|
|
11904
|
+
}
|
|
11905
|
+
return fn(caseJson);
|
|
11906
|
+
}
|
|
11890
11907
|
/**
|
|
11891
11908
|
* EM field simulation (2D/axisymmetric finite-volume magnetostatics or
|
|
11892
11909
|
* electrostatics): inductance, force, torque, capacitance extraction and
|
|
@@ -40914,6 +40931,25 @@ var init_CHANGELOG = __esm({
|
|
|
40914
40931
|
CHANGELOG_default = {
|
|
40915
40932
|
$schema: "./changelog.schema.json",
|
|
40916
40933
|
entries: [
|
|
40934
|
+
{
|
|
40935
|
+
id: "2026-07-26-swept-and-all-pairs-clearance",
|
|
40936
|
+
version: "0.9.4",
|
|
40937
|
+
date: "2026-07-26",
|
|
40938
|
+
category: "feat",
|
|
40939
|
+
title: "check_clearance sweeps the range of motion",
|
|
40940
|
+
summary: "Pass `sweep` to drive joints through their travel and get the worst pose, or omit both groups to audit every part pair in the document for interference.",
|
|
40941
|
+
features: [
|
|
40942
|
+
"clearance",
|
|
40943
|
+
"assembly",
|
|
40944
|
+
"kinematics",
|
|
40945
|
+
"mcp"
|
|
40946
|
+
],
|
|
40947
|
+
mcpTools: [
|
|
40948
|
+
"check_clearance",
|
|
40949
|
+
"build_receipt",
|
|
40950
|
+
"verify_receipt"
|
|
40951
|
+
]
|
|
40952
|
+
},
|
|
40917
40953
|
{
|
|
40918
40954
|
id: "2026-07-26-render-loon-directly",
|
|
40919
40955
|
version: "0.9.4",
|
|
@@ -40965,6 +41001,25 @@ var init_CHANGELOG = __esm({
|
|
|
40965
41001
|
"export_cad"
|
|
40966
41002
|
]
|
|
40967
41003
|
},
|
|
41004
|
+
{
|
|
41005
|
+
id: "2026-07-26-fastener-forms",
|
|
41006
|
+
version: "0.9.4",
|
|
41007
|
+
date: "2026-07-26",
|
|
41008
|
+
category: "feat",
|
|
41009
|
+
title: "Fasteners are a first-class form",
|
|
41010
|
+
summary: "bolt, bolt-circle, clearance-hole and tapped-hole: catalog-backed hardware oriented by its own axis, with real countersunk heads, length checks, and BOM lines counted off the geometry.",
|
|
41011
|
+
features: [
|
|
41012
|
+
"loon",
|
|
41013
|
+
"fasteners",
|
|
41014
|
+
"bom",
|
|
41015
|
+
"assembly"
|
|
41016
|
+
],
|
|
41017
|
+
mcpTools: [
|
|
41018
|
+
"create_cad_loon",
|
|
41019
|
+
"bom_create",
|
|
41020
|
+
"search_mechanical_parts"
|
|
41021
|
+
]
|
|
41022
|
+
},
|
|
40968
41023
|
{
|
|
40969
41024
|
id: "2026-07-26-document-source-provenance",
|
|
40970
41025
|
version: "0.9.4",
|
|
@@ -40984,6 +41039,23 @@ var init_CHANGELOG = __esm({
|
|
|
40984
41039
|
"load_document"
|
|
40985
41040
|
]
|
|
40986
41041
|
},
|
|
41042
|
+
{
|
|
41043
|
+
id: "2026-07-26-beam-check-thin-walled-fea",
|
|
41044
|
+
version: "0.9.4",
|
|
41045
|
+
date: "2026-07-26",
|
|
41046
|
+
category: "feat",
|
|
41047
|
+
title: "beam_check: audited structural answer for thin walls",
|
|
41048
|
+
summary: "Sheet-metal and tube-frame members had no audited FEA answer at any resolution; beam_check prices them in closed form (Bredt torsion, beam bending, Euler buckling), and analyze_structure now measures the wall and refuses with the cell arithmetic instead of a bare Unverifiable.",
|
|
41049
|
+
features: [
|
|
41050
|
+
"fea",
|
|
41051
|
+
"sheet-metal",
|
|
41052
|
+
"receipts"
|
|
41053
|
+
],
|
|
41054
|
+
mcpTools: [
|
|
41055
|
+
"beam_check",
|
|
41056
|
+
"analyze_structure"
|
|
41057
|
+
]
|
|
41058
|
+
},
|
|
40987
41059
|
{
|
|
40988
41060
|
id: "2026-07-26-backwards-extrude-inverted-winding",
|
|
40989
41061
|
version: "0.9.4",
|
|
@@ -53305,6 +53377,7 @@ var init_tool_metadata = __esm({
|
|
|
53305
53377
|
solve_thermal: { title: "Solve Thermal", annotations: RO },
|
|
53306
53378
|
simulate_flow: { title: "Simulate Flow", annotations: RO },
|
|
53307
53379
|
analyze_structure: { title: "Analyze Structure", annotations: RO },
|
|
53380
|
+
beam_check: { title: "Check Beam / Section", annotations: RO },
|
|
53308
53381
|
simulate_neutron_shield: {
|
|
53309
53382
|
title: "Simulate Neutron Shield",
|
|
53310
53383
|
annotations: RO
|
|
@@ -55576,10 +55649,23 @@ function clearanceVerdict(distanceMm) {
|
|
|
55576
55649
|
if (distanceMm <= CONTACT_EPS_MM) return "touching";
|
|
55577
55650
|
return "clear";
|
|
55578
55651
|
}
|
|
55579
|
-
function clearanceHolds(distanceMm, minMm, allowContact) {
|
|
55652
|
+
function clearanceHolds(distanceMm, minMm, allowContact, intersecting = false) {
|
|
55653
|
+
if (intersecting) return false;
|
|
55580
55654
|
return distanceMm >= minMm || allowContact && clearanceVerdict(distanceMm) === "touching";
|
|
55581
55655
|
}
|
|
55582
|
-
function
|
|
55656
|
+
function worseThan(a, b) {
|
|
55657
|
+
if (a.intersecting !== b.intersecting) return a.intersecting;
|
|
55658
|
+
return a.distance < b.distance;
|
|
55659
|
+
}
|
|
55660
|
+
function placeMesh(mesh, transform2) {
|
|
55661
|
+
if (!transform2) return mesh;
|
|
55662
|
+
return transformMesh(mesh, {
|
|
55663
|
+
translate: transform2.translation,
|
|
55664
|
+
rotate: transform2.rotation,
|
|
55665
|
+
scale: transform2.scale
|
|
55666
|
+
});
|
|
55667
|
+
}
|
|
55668
|
+
function poseableParts(doc, engine) {
|
|
55583
55669
|
const scene = engine.evaluate(doc);
|
|
55584
55670
|
const visibleRoots = doc.roots.filter((e) => e.visible !== false);
|
|
55585
55671
|
const out = [];
|
|
@@ -55588,23 +55674,30 @@ function partCandidates(doc, engine) {
|
|
|
55588
55674
|
const node = doc.nodes[String(rootId)];
|
|
55589
55675
|
const mesh = scene.parts[i].mesh;
|
|
55590
55676
|
if (!mesh || mesh.positions.length === 0) continue;
|
|
55591
|
-
out.push({ id: String(rootId), name: node?.name ?? void 0, mesh });
|
|
55677
|
+
out.push({ id: String(rootId), name: node?.name ?? void 0, localMesh: mesh });
|
|
55592
55678
|
}
|
|
55593
55679
|
for (const inst of scene.instances ?? []) {
|
|
55594
|
-
|
|
55595
|
-
translate: inst.transform.translation,
|
|
55596
|
-
rotate: inst.transform.rotation,
|
|
55597
|
-
scale: inst.transform.scale
|
|
55598
|
-
}) : inst.mesh;
|
|
55599
|
-
if (!mesh || mesh.positions.length === 0) continue;
|
|
55680
|
+
if (!inst.mesh || inst.mesh.positions.length === 0) continue;
|
|
55600
55681
|
out.push({
|
|
55601
55682
|
id: inst.instanceId,
|
|
55602
55683
|
name: inst.name ?? void 0,
|
|
55603
|
-
mesh
|
|
55684
|
+
localMesh: inst.mesh,
|
|
55685
|
+
transform: inst.transform ?? void 0,
|
|
55686
|
+
instanceId: inst.instanceId
|
|
55604
55687
|
});
|
|
55605
55688
|
}
|
|
55606
55689
|
return out;
|
|
55607
55690
|
}
|
|
55691
|
+
function placeParts(parts, worldTransforms) {
|
|
55692
|
+
const out = [];
|
|
55693
|
+
for (const p of parts) {
|
|
55694
|
+
const transform2 = (p.instanceId ? worldTransforms?.get(p.instanceId) : void 0) ?? p.transform;
|
|
55695
|
+
const mesh = placeMesh(p.localMesh, transform2);
|
|
55696
|
+
if (!mesh || mesh.positions.length === 0) continue;
|
|
55697
|
+
out.push({ id: p.id, ...p.name ? { name: p.name } : {}, mesh });
|
|
55698
|
+
}
|
|
55699
|
+
return out;
|
|
55700
|
+
}
|
|
55608
55701
|
function resolveGroup(candidates, ids) {
|
|
55609
55702
|
const parts = /* @__PURE__ */ new Map();
|
|
55610
55703
|
const missing = [];
|
|
@@ -55616,16 +55709,115 @@ function resolveGroup(candidates, ids) {
|
|
|
55616
55709
|
}
|
|
55617
55710
|
return { parts: [...parts.values()], missing };
|
|
55618
55711
|
}
|
|
55619
|
-
function
|
|
55712
|
+
function resolveSweepAxes(doc, axes) {
|
|
55713
|
+
const joints = doc.joints ?? [];
|
|
55714
|
+
const resolved = [];
|
|
55715
|
+
for (const axis of axes) {
|
|
55716
|
+
const wanted = String(axis.joint);
|
|
55717
|
+
const found = joints.find((j) => j.id === wanted) ?? joints.find((j) => j.name === wanted);
|
|
55718
|
+
if (!found) {
|
|
55719
|
+
const available = joints.map((j) => `${j.id}${j.name ? ` (${j.name})` : ""}`).join(", ");
|
|
55720
|
+
return { error: `No joint with id or name "${wanted}". Available: ${available || "none"}` };
|
|
55721
|
+
}
|
|
55722
|
+
if (!Number.isFinite(axis.from) || !Number.isFinite(axis.to)) {
|
|
55723
|
+
return { error: `Sweep axis "${wanted}" needs finite \`from\` and \`to\`.` };
|
|
55724
|
+
}
|
|
55725
|
+
const steps = Math.trunc(axis.steps);
|
|
55726
|
+
if (!Number.isFinite(steps) || steps < 1) {
|
|
55727
|
+
return { error: `Sweep axis "${wanted}" needs \`steps\` >= 1.` };
|
|
55728
|
+
}
|
|
55729
|
+
resolved.push({ joint: found.id, from: axis.from, to: axis.to, steps });
|
|
55730
|
+
}
|
|
55731
|
+
if (resolved.length === 0) return { error: "`sweep` needs at least one joint axis." };
|
|
55732
|
+
const total = resolved.reduce((n, a) => n * (a.steps + 1), 1);
|
|
55733
|
+
if (total > MAX_SWEEP_POSES) {
|
|
55734
|
+
return {
|
|
55735
|
+
error: `Sweep grid is ${total} poses (limit ${MAX_SWEEP_POSES}). Lower \`steps\` or sweep fewer joints at once.`
|
|
55736
|
+
};
|
|
55737
|
+
}
|
|
55738
|
+
return { axes: resolved };
|
|
55739
|
+
}
|
|
55740
|
+
function poseGrid(axes) {
|
|
55741
|
+
let poses = [[]];
|
|
55742
|
+
for (const axis of axes) {
|
|
55743
|
+
const n = Math.trunc(axis.steps);
|
|
55744
|
+
const next = [];
|
|
55745
|
+
for (const pose of poses) {
|
|
55746
|
+
for (let i = 0; i <= n; i++) {
|
|
55747
|
+
const state = axis.from + (axis.to - axis.from) * i / n;
|
|
55748
|
+
next.push([...pose, { joint: axis.joint, state }]);
|
|
55749
|
+
}
|
|
55750
|
+
}
|
|
55751
|
+
poses = next;
|
|
55752
|
+
}
|
|
55753
|
+
return poses;
|
|
55754
|
+
}
|
|
55755
|
+
function poseTransforms(doc, poses) {
|
|
55756
|
+
const joints = doc.joints ?? [];
|
|
55757
|
+
const byId = new Map(joints.map((j) => [j.id, j]));
|
|
55758
|
+
const saved = joints.map((j) => j.state);
|
|
55759
|
+
try {
|
|
55760
|
+
return poses.map((pose) => {
|
|
55761
|
+
for (const { joint, state } of pose) {
|
|
55762
|
+
const j = byId.get(joint);
|
|
55763
|
+
if (j) j.state = state;
|
|
55764
|
+
}
|
|
55765
|
+
return solveForwardKinematics2(doc);
|
|
55766
|
+
});
|
|
55767
|
+
} finally {
|
|
55768
|
+
joints.forEach((j, i) => {
|
|
55769
|
+
j.state = saved[i];
|
|
55770
|
+
});
|
|
55771
|
+
}
|
|
55772
|
+
}
|
|
55773
|
+
function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
|
|
55620
55774
|
if (groupA.length === 0 || groupB.length === 0) {
|
|
55621
55775
|
return { error: "Both `group_a` and `group_b` need at least one part id." };
|
|
55622
55776
|
}
|
|
55623
|
-
let
|
|
55777
|
+
let base;
|
|
55778
|
+
try {
|
|
55779
|
+
base = poseableParts(doc, engine);
|
|
55780
|
+
} catch (e) {
|
|
55781
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
55782
|
+
}
|
|
55783
|
+
if (!sweep || sweep.length === 0) {
|
|
55784
|
+
return measureGroups(engine, placeParts(base), groupA, groupB);
|
|
55785
|
+
}
|
|
55786
|
+
const { axes, error: sweepError } = resolveSweepAxes(doc, sweep);
|
|
55787
|
+
if (sweepError || !axes) return { error: sweepError ?? "sweep could not be resolved" };
|
|
55788
|
+
const poses = poseGrid(axes);
|
|
55789
|
+
let transforms;
|
|
55624
55790
|
try {
|
|
55625
|
-
|
|
55791
|
+
transforms = poseTransforms(doc, poses);
|
|
55626
55792
|
} catch (e) {
|
|
55627
55793
|
return { error: e instanceof Error ? e.message : String(e) };
|
|
55628
55794
|
}
|
|
55795
|
+
let worst2;
|
|
55796
|
+
for (let i = 0; i < poses.length; i++) {
|
|
55797
|
+
const { result, error: error2 } = measureGroups(
|
|
55798
|
+
engine,
|
|
55799
|
+
placeParts(base, transforms[i]),
|
|
55800
|
+
groupA,
|
|
55801
|
+
groupB
|
|
55802
|
+
);
|
|
55803
|
+
if (error2 || !result) return { error: error2 };
|
|
55804
|
+
if (!worst2 || worseThan(
|
|
55805
|
+
{ distance: result.distance_mm, intersecting: result.intersecting },
|
|
55806
|
+
{ distance: worst2.result.distance_mm, intersecting: worst2.result.intersecting }
|
|
55807
|
+
)) {
|
|
55808
|
+
worst2 = { result, pose: poses[i] };
|
|
55809
|
+
}
|
|
55810
|
+
}
|
|
55811
|
+
if (!worst2) return { error: "Sweep produced no poses to measure." };
|
|
55812
|
+
return {
|
|
55813
|
+
result: {
|
|
55814
|
+
...worst2.result,
|
|
55815
|
+
worst_pose: worst2.pose.map((p) => ({ joint: p.joint, state: round6(p.state) })),
|
|
55816
|
+
poses_checked: poses.length
|
|
55817
|
+
}
|
|
55818
|
+
};
|
|
55819
|
+
}
|
|
55820
|
+
function measureGroups(engine, candidates, groupA, groupB) {
|
|
55629
55821
|
const a = resolveGroup(candidates, groupA);
|
|
55630
55822
|
const b = resolveGroup(candidates, groupB);
|
|
55631
55823
|
const missing = [...a.missing, ...b.missing];
|
|
@@ -55652,7 +55844,7 @@ function computeGroupClearance(doc, engine, groupA, groupB) {
|
|
|
55652
55844
|
return { error: e instanceof Error ? e.message : String(e) };
|
|
55653
55845
|
}
|
|
55654
55846
|
pairs += 1;
|
|
55655
|
-
if (!worst2 || r
|
|
55847
|
+
if (!worst2 || worseThan(r, worst2.r)) {
|
|
55656
55848
|
worst2 = { a: pa, b: pb, r };
|
|
55657
55849
|
}
|
|
55658
55850
|
}
|
|
@@ -55677,6 +55869,132 @@ function computeGroupClearance(doc, engine, groupA, groupB) {
|
|
|
55677
55869
|
}
|
|
55678
55870
|
};
|
|
55679
55871
|
}
|
|
55872
|
+
function meshAabb(mesh) {
|
|
55873
|
+
const p = mesh.positions;
|
|
55874
|
+
const min = [Infinity, Infinity, Infinity];
|
|
55875
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
55876
|
+
for (let i = 0; i + 2 < p.length; i += 3) {
|
|
55877
|
+
for (let k = 0; k < 3; k++) {
|
|
55878
|
+
const v = p[i + k];
|
|
55879
|
+
if (v < min[k]) min[k] = v;
|
|
55880
|
+
if (v > max[k]) max[k] = v;
|
|
55881
|
+
}
|
|
55882
|
+
}
|
|
55883
|
+
return { min, max };
|
|
55884
|
+
}
|
|
55885
|
+
function aabbNear(a, b, margin) {
|
|
55886
|
+
for (let k = 0; k < 3; k++) {
|
|
55887
|
+
if (a.min[k] - margin > b.max[k]) return false;
|
|
55888
|
+
if (b.min[k] - margin > a.max[k]) return false;
|
|
55889
|
+
}
|
|
55890
|
+
return true;
|
|
55891
|
+
}
|
|
55892
|
+
function computeInterferenceAudit(doc, engine, opts) {
|
|
55893
|
+
let base;
|
|
55894
|
+
try {
|
|
55895
|
+
base = poseableParts(doc, engine);
|
|
55896
|
+
} catch (e) {
|
|
55897
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
55898
|
+
}
|
|
55899
|
+
let poses = [];
|
|
55900
|
+
let transforms = [void 0];
|
|
55901
|
+
if (opts.sweep && opts.sweep.length > 0) {
|
|
55902
|
+
const { axes, error: error2 } = resolveSweepAxes(doc, opts.sweep);
|
|
55903
|
+
if (error2 || !axes) return { error: error2 ?? "sweep could not be resolved" };
|
|
55904
|
+
poses = poseGrid(axes);
|
|
55905
|
+
try {
|
|
55906
|
+
transforms = poseTransforms(doc, poses);
|
|
55907
|
+
} catch (e) {
|
|
55908
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
55909
|
+
}
|
|
55910
|
+
}
|
|
55911
|
+
const first = placeParts(base, transforms[0]);
|
|
55912
|
+
if (first.length < 2) {
|
|
55913
|
+
return { error: "Fewer than two measurable parts \u2014 nothing to audit." };
|
|
55914
|
+
}
|
|
55915
|
+
const ignore = /* @__PURE__ */ new Set();
|
|
55916
|
+
const unresolved = [];
|
|
55917
|
+
const resolveId = (raw) => {
|
|
55918
|
+
const wanted = String(raw);
|
|
55919
|
+
return (first.find((c) => c.id === wanted) ?? first.find((c) => c.name === wanted))?.id;
|
|
55920
|
+
};
|
|
55921
|
+
for (const [rawA, rawB] of opts.ignorePairs) {
|
|
55922
|
+
const a = resolveId(rawA);
|
|
55923
|
+
const b = resolveId(rawB);
|
|
55924
|
+
if (!a) unresolved.push(String(rawA));
|
|
55925
|
+
if (!b) unresolved.push(String(rawB));
|
|
55926
|
+
if (a && b) ignore.add(pairKey(a, b));
|
|
55927
|
+
}
|
|
55928
|
+
for (const joint of doc.joints ?? []) {
|
|
55929
|
+
const parent = joint.parentInstanceId;
|
|
55930
|
+
if (!parent) continue;
|
|
55931
|
+
const fixed = joint.kind?.type === "Fixed";
|
|
55932
|
+
if (opts.ignoreAdjacent || fixed && opts.ignoreFixedJoints) {
|
|
55933
|
+
ignore.add(pairKey(parent, joint.childInstanceId));
|
|
55934
|
+
}
|
|
55935
|
+
}
|
|
55936
|
+
const margin = Math.max(opts.minMm, 0);
|
|
55937
|
+
const worstByPair = /* @__PURE__ */ new Map();
|
|
55938
|
+
const broadphase = /* @__PURE__ */ new Set();
|
|
55939
|
+
let queries = 0;
|
|
55940
|
+
let ignored = 0;
|
|
55941
|
+
for (let p = 0; p < transforms.length; p++) {
|
|
55942
|
+
const parts = p === 0 ? first : placeParts(base, transforms[p]);
|
|
55943
|
+
const boxes = parts.map((c) => meshAabb(c.mesh));
|
|
55944
|
+
for (let i = 0; i < parts.length; i++) {
|
|
55945
|
+
for (let j = i + 1; j < parts.length; j++) {
|
|
55946
|
+
const key = pairKey(parts[i].id, parts[j].id);
|
|
55947
|
+
if (ignore.has(key)) {
|
|
55948
|
+
if (p === 0) ignored += 1;
|
|
55949
|
+
continue;
|
|
55950
|
+
}
|
|
55951
|
+
if (!aabbNear(boxes[i], boxes[j], margin + CONTACT_EPS_MM)) continue;
|
|
55952
|
+
broadphase.add(key);
|
|
55953
|
+
let r;
|
|
55954
|
+
try {
|
|
55955
|
+
r = engine.meshClearance(parts[i].mesh, parts[j].mesh);
|
|
55956
|
+
} catch (e) {
|
|
55957
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
55958
|
+
}
|
|
55959
|
+
queries += 1;
|
|
55960
|
+
const prev = worstByPair.get(key);
|
|
55961
|
+
if (!prev || worseThan(r, prev.r)) {
|
|
55962
|
+
worstByPair.set(key, {
|
|
55963
|
+
a: parts[i],
|
|
55964
|
+
b: parts[j],
|
|
55965
|
+
r,
|
|
55966
|
+
...poses.length > 0 ? { pose: poses[p] } : {}
|
|
55967
|
+
});
|
|
55968
|
+
}
|
|
55969
|
+
}
|
|
55970
|
+
}
|
|
55971
|
+
}
|
|
55972
|
+
const named = (p) => ({ id: p.id, ...p.name ? { name: p.name } : {} });
|
|
55973
|
+
const findings = [...worstByPair.values()].filter((w) => !clearanceHolds(round6(w.r.distance), opts.minMm, false, w.r.intersecting)).map((w) => ({
|
|
55974
|
+
a: named(w.a),
|
|
55975
|
+
b: named(w.b),
|
|
55976
|
+
distance_mm: round6(w.r.distance),
|
|
55977
|
+
verdict: w.r.intersecting ? "intersecting" : clearanceVerdict(round6(w.r.distance)),
|
|
55978
|
+
point_a: w.r.pointA.map(round6),
|
|
55979
|
+
point_b: w.r.pointB.map(round6),
|
|
55980
|
+
...w.pose ? { worst_pose: w.pose.map((s) => ({ joint: s.joint, state: round6(s.state) })) } : {}
|
|
55981
|
+
})).sort(
|
|
55982
|
+
(x, y) => Number(y.verdict === "intersecting") - Number(x.verdict === "intersecting") || x.distance_mm - y.distance_mm
|
|
55983
|
+
);
|
|
55984
|
+
const n = first.length;
|
|
55985
|
+
return {
|
|
55986
|
+
result: {
|
|
55987
|
+
parts_checked: n,
|
|
55988
|
+
pairs_total: n * (n - 1) / 2,
|
|
55989
|
+
pairs_ignored: ignored,
|
|
55990
|
+
pairs_broadphase: broadphase.size,
|
|
55991
|
+
queries,
|
|
55992
|
+
...poses.length > 0 ? { poses_checked: poses.length } : {},
|
|
55993
|
+
findings,
|
|
55994
|
+
unresolved_ignores: [...new Set(unresolved)]
|
|
55995
|
+
}
|
|
55996
|
+
};
|
|
55997
|
+
}
|
|
55680
55998
|
function upsertClearanceSpec(doc, spec) {
|
|
55681
55999
|
const specs = doc.clearance_specs ?? [];
|
|
55682
56000
|
const idx = specs.findIndex((s) => s.label === spec.label);
|
|
@@ -55689,16 +56007,13 @@ async function checkClearance(args, engine) {
|
|
|
55689
56007
|
if (!documentId) return err3("Pass `document_id` (the CAD session).");
|
|
55690
56008
|
const groupA = stringArray(args.group_a);
|
|
55691
56009
|
const groupB = stringArray(args.group_b);
|
|
55692
|
-
if (!groupA || !groupB) {
|
|
55693
|
-
return err3("Pass `group_a` and `group_b` as arrays of part ids (or part names).");
|
|
55694
|
-
}
|
|
55695
|
-
const minMm = typeof args.min_mm === "number" ? args.min_mm : NaN;
|
|
55696
|
-
if (!Number.isFinite(minMm)) return err3("Pass `min_mm`, the required minimum separation in mm.");
|
|
55697
56010
|
const label = typeof args.label === "string" && args.label.trim() ? args.label.trim() : void 0;
|
|
55698
56011
|
const allowContact = asBool(args.allow_contact);
|
|
56012
|
+
const { sweep, error: sweepParseError } = parseSweep(args.sweep);
|
|
56013
|
+
if (sweepParseError) return err3(sweepParseError);
|
|
55699
56014
|
if (label && args.joint_state !== void 0 && args.joint_state !== null) {
|
|
55700
56015
|
return err3(
|
|
55701
|
-
"`label` and `joint_state` cannot be combined: a persisted clearance spec is re-measured at the document's stored joint states, so an assertion captured at an ad-hoc pose could not be re-verified. Either drop `label` (one-off pose measurement), or set the joint states on the document and assert there."
|
|
56016
|
+
"`label` and `joint_state` cannot be combined: a persisted clearance spec is re-measured at the document's stored joint states, so an assertion captured at an ad-hoc pose could not be re-verified. Either drop `label` (one-off pose measurement), use `sweep` (persisted with the spec and re-verified over the same range), or set the joint states on the document and assert there."
|
|
55702
56017
|
);
|
|
55703
56018
|
}
|
|
55704
56019
|
const stored = getSession(documentId);
|
|
@@ -55709,10 +56024,20 @@ async function checkClearance(args, engine) {
|
|
|
55709
56024
|
} catch (e) {
|
|
55710
56025
|
return err3(e instanceof Error ? e.message : String(e));
|
|
55711
56026
|
}
|
|
55712
|
-
|
|
56027
|
+
if (!groupA && !groupB) {
|
|
56028
|
+
return auditClearance(documentId, doc, engine, args, sweep, label, pose);
|
|
56029
|
+
}
|
|
56030
|
+
if (!groupA || !groupB) {
|
|
56031
|
+
return err3(
|
|
56032
|
+
"Pass both `group_a` and `group_b` as arrays of part ids (or part names) \u2014 or neither, to audit every pair in the document."
|
|
56033
|
+
);
|
|
56034
|
+
}
|
|
56035
|
+
const minMm = typeof args.min_mm === "number" ? args.min_mm : NaN;
|
|
56036
|
+
if (!Number.isFinite(minMm)) return err3("Pass `min_mm`, the required minimum separation in mm.");
|
|
56037
|
+
const { result, error: error2 } = computeGroupClearance(doc, engine, groupA, groupB, sweep);
|
|
55713
56038
|
if (error2 || !result) return err3(error2 ?? "Clearance could not be computed.");
|
|
55714
56039
|
const verdict = clearanceVerdict(result.distance_mm);
|
|
55715
|
-
const pass2 = clearanceHolds(result.distance_mm, minMm, allowContact);
|
|
56040
|
+
const pass2 = clearanceHolds(result.distance_mm, minMm, allowContact, result.intersecting);
|
|
55716
56041
|
let specSaved = false;
|
|
55717
56042
|
if (label) {
|
|
55718
56043
|
upsertClearanceSpec(stored, {
|
|
@@ -55720,7 +56045,8 @@ async function checkClearance(args, engine) {
|
|
|
55720
56045
|
group_a: result.group_a.map((p) => p.id),
|
|
55721
56046
|
group_b: result.group_b.map((p) => p.id),
|
|
55722
56047
|
min_mm: minMm,
|
|
55723
|
-
...allowContact ? { allow_contact: true } : {}
|
|
56048
|
+
...allowContact ? { allow_contact: true } : {},
|
|
56049
|
+
...sweep && sweep.length > 0 ? { sweep } : {}
|
|
55724
56050
|
});
|
|
55725
56051
|
specSaved = true;
|
|
55726
56052
|
}
|
|
@@ -55736,6 +56062,8 @@ async function checkClearance(args, engine) {
|
|
|
55736
56062
|
...pose ? { pose } : {},
|
|
55737
56063
|
intersecting: result.intersecting,
|
|
55738
56064
|
worst_pair: result.worst_pair,
|
|
56065
|
+
...result.worst_pose ? { worst_pose: result.worst_pose } : {},
|
|
56066
|
+
...result.poses_checked !== void 0 ? { poses_checked: result.poses_checked } : {},
|
|
55739
56067
|
pairs_checked: result.pairs_checked,
|
|
55740
56068
|
group_a: result.group_a,
|
|
55741
56069
|
group_b: result.group_b,
|
|
@@ -55752,11 +56080,92 @@ async function checkClearance(args, engine) {
|
|
|
55752
56080
|
}
|
|
55753
56081
|
};
|
|
55754
56082
|
}
|
|
56083
|
+
function parseSweep(raw) {
|
|
56084
|
+
if (raw === void 0 || raw === null) return {};
|
|
56085
|
+
if (!Array.isArray(raw)) {
|
|
56086
|
+
return { error: "`sweep` must be an array of {joint, from, to, steps} axes." };
|
|
56087
|
+
}
|
|
56088
|
+
const sweep = [];
|
|
56089
|
+
for (const entry of raw) {
|
|
56090
|
+
if (!entry || typeof entry !== "object") {
|
|
56091
|
+
return { error: "Each `sweep` axis must be an object {joint, from, to, steps}." };
|
|
56092
|
+
}
|
|
56093
|
+
const e = entry;
|
|
56094
|
+
const joint = typeof e.joint === "string" ? e.joint.trim() : "";
|
|
56095
|
+
const from = Number(e.from);
|
|
56096
|
+
const to = Number(e.to);
|
|
56097
|
+
const steps = Number(e.steps);
|
|
56098
|
+
if (!joint || !Number.isFinite(from) || !Number.isFinite(to) || !Number.isFinite(steps)) {
|
|
56099
|
+
return {
|
|
56100
|
+
error: "Each `sweep` axis needs a `joint` id/name plus numeric `from`, `to`, and `steps`."
|
|
56101
|
+
};
|
|
56102
|
+
}
|
|
56103
|
+
sweep.push({ joint, from, to, steps });
|
|
56104
|
+
}
|
|
56105
|
+
return sweep.length > 0 ? { sweep } : {};
|
|
56106
|
+
}
|
|
56107
|
+
function parseIgnorePairs(raw) {
|
|
56108
|
+
if (raw === void 0 || raw === null) return { pairs: [] };
|
|
56109
|
+
if (!Array.isArray(raw)) {
|
|
56110
|
+
return { pairs: [], error: "`ignore_pairs` must be an array of [part_a, part_b] pairs." };
|
|
56111
|
+
}
|
|
56112
|
+
const pairs = [];
|
|
56113
|
+
for (const entry of raw) {
|
|
56114
|
+
if (!Array.isArray(entry) || entry.length !== 2) {
|
|
56115
|
+
return { pairs: [], error: "Each `ignore_pairs` entry must be a two-element array." };
|
|
56116
|
+
}
|
|
56117
|
+
pairs.push([String(entry[0]), String(entry[1])]);
|
|
56118
|
+
}
|
|
56119
|
+
return { pairs };
|
|
56120
|
+
}
|
|
56121
|
+
function auditClearance(documentId, doc, engine, args, sweep, label, pose) {
|
|
56122
|
+
const minMm = typeof args.min_mm === "number" && Number.isFinite(args.min_mm) ? args.min_mm : 0;
|
|
56123
|
+
const { pairs: ignorePairs, error: ignoreError } = parseIgnorePairs(args.ignore_pairs);
|
|
56124
|
+
if (ignoreError) return err3(ignoreError);
|
|
56125
|
+
const ignoreFixedJoints = args.ignore_fixed_joints === void 0 ? true : asBool(args.ignore_fixed_joints);
|
|
56126
|
+
const ignoreAdjacent = asBool(args.ignore_adjacent);
|
|
56127
|
+
const { result, error: error2 } = computeInterferenceAudit(doc, engine, {
|
|
56128
|
+
minMm,
|
|
56129
|
+
ignorePairs,
|
|
56130
|
+
ignoreFixedJoints,
|
|
56131
|
+
ignoreAdjacent,
|
|
56132
|
+
sweep
|
|
56133
|
+
});
|
|
56134
|
+
if (error2 || !result) return err3(error2 ?? "Interference audit could not be computed.");
|
|
56135
|
+
const payload = {
|
|
56136
|
+
success: true,
|
|
56137
|
+
document_id: documentId,
|
|
56138
|
+
mode: "audit",
|
|
56139
|
+
required_mm: minMm,
|
|
56140
|
+
...pose ? { pose } : {},
|
|
56141
|
+
pass: result.findings.length === 0,
|
|
56142
|
+
findings: result.findings,
|
|
56143
|
+
interference_count: result.findings.length,
|
|
56144
|
+
parts_checked: result.parts_checked,
|
|
56145
|
+
pairs_total: result.pairs_total,
|
|
56146
|
+
pairs_ignored: result.pairs_ignored,
|
|
56147
|
+
pairs_broadphase: result.pairs_broadphase,
|
|
56148
|
+
queries: result.queries,
|
|
56149
|
+
...result.poses_checked !== void 0 ? { poses_checked: result.poses_checked } : {},
|
|
56150
|
+
...result.unresolved_ignores.length > 0 ? {
|
|
56151
|
+
unresolved_ignores: result.unresolved_ignores,
|
|
56152
|
+
note: `ignore_pairs named parts that do not exist: ${result.unresolved_ignores.join(", ")} \u2014 those pairs were NOT whitelisted.`
|
|
56153
|
+
} : {},
|
|
56154
|
+
...label ? {
|
|
56155
|
+
spec_saved: false,
|
|
56156
|
+
label_note: "`label` persists only pairwise assertions; an audit is a whole-document scan, so nothing was saved. Name the offending pair and re-run with group_a/group_b to persist it."
|
|
56157
|
+
} : {}
|
|
56158
|
+
};
|
|
56159
|
+
return {
|
|
56160
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
56161
|
+
structuredContent: { clearance: payload, document_id: documentId }
|
|
56162
|
+
};
|
|
56163
|
+
}
|
|
55755
56164
|
function clearanceReceiptClaims(doc, engine) {
|
|
55756
56165
|
const specs = doc.clearance_specs ?? [];
|
|
55757
56166
|
return specs.map((spec) => {
|
|
55758
56167
|
const id = `${CLEARANCE_CLAIM_PREFIX}${spec.label}`;
|
|
55759
|
-
const description = `clearance "${spec.label}" at least ${spec.min_mm} mm`;
|
|
56168
|
+
const description = spec.sweep?.length ? `clearance "${spec.label}" at least ${spec.min_mm} mm across the swept range of motion` : `clearance "${spec.label}" at least ${spec.min_mm} mm`;
|
|
55760
56169
|
const subject = `${spec.group_a.join("+")} vs ${spec.group_b.join("+")}`;
|
|
55761
56170
|
if (!engine) {
|
|
55762
56171
|
return {
|
|
@@ -55770,7 +56179,13 @@ function clearanceReceiptClaims(doc, engine) {
|
|
|
55770
56179
|
subject
|
|
55771
56180
|
};
|
|
55772
56181
|
}
|
|
55773
|
-
const { result, error: error2 } = computeGroupClearance(
|
|
56182
|
+
const { result, error: error2 } = computeGroupClearance(
|
|
56183
|
+
doc,
|
|
56184
|
+
engine,
|
|
56185
|
+
spec.group_a,
|
|
56186
|
+
spec.group_b,
|
|
56187
|
+
spec.sweep
|
|
56188
|
+
);
|
|
55774
56189
|
if (error2 || !result) {
|
|
55775
56190
|
return {
|
|
55776
56191
|
...unverifiableClaim(
|
|
@@ -55790,8 +56205,15 @@ function clearanceReceiptClaims(doc, engine) {
|
|
|
55790
56205
|
group_b: spec.group_b,
|
|
55791
56206
|
required_mm: spec.min_mm,
|
|
55792
56207
|
measured_mm: result.distance_mm,
|
|
55793
|
-
holds: clearanceHolds(result.distance_mm, spec.min_mm, allowContact),
|
|
55794
|
-
...allowContact ? { allow_contact: true } : {}
|
|
56208
|
+
holds: clearanceHolds(result.distance_mm, spec.min_mm, allowContact, result.intersecting),
|
|
56209
|
+
...allowContact ? { allow_contact: true } : {},
|
|
56210
|
+
// A swept claim carries its grid so verify_receipt re-checks the same
|
|
56211
|
+
// range of motion — re-verifying at one pose would silently weaken it.
|
|
56212
|
+
...spec.sweep?.length ? {
|
|
56213
|
+
sweep: spec.sweep,
|
|
56214
|
+
worst_pose: result.worst_pose ?? [],
|
|
56215
|
+
poses_checked: result.poses_checked ?? 0
|
|
56216
|
+
} : {}
|
|
55795
56217
|
};
|
|
55796
56218
|
return {
|
|
55797
56219
|
id,
|
|
@@ -55820,7 +56242,13 @@ function verifyClearanceClaims(doc, engine, receipt) {
|
|
|
55820
56242
|
});
|
|
55821
56243
|
continue;
|
|
55822
56244
|
}
|
|
55823
|
-
const { result, error: error2 } = computeGroupClearance(
|
|
56245
|
+
const { result, error: error2 } = computeGroupClearance(
|
|
56246
|
+
doc,
|
|
56247
|
+
engine,
|
|
56248
|
+
stored.group_a,
|
|
56249
|
+
stored.group_b,
|
|
56250
|
+
stored.sweep
|
|
56251
|
+
);
|
|
55824
56252
|
if (error2 || !result) {
|
|
55825
56253
|
checks.push({
|
|
55826
56254
|
label,
|
|
@@ -55832,32 +56260,28 @@ function verifyClearanceClaims(doc, engine, receipt) {
|
|
|
55832
56260
|
continue;
|
|
55833
56261
|
}
|
|
55834
56262
|
const measured = result.distance_mm;
|
|
55835
|
-
|
|
56263
|
+
const sweptFields = result.poses_checked ? { worst_pose: result.worst_pose, poses_checked: result.poses_checked } : {};
|
|
56264
|
+
const common = {
|
|
56265
|
+
label,
|
|
56266
|
+
required_mm: stored.required_mm,
|
|
56267
|
+
stored_mm: stored.measured_mm,
|
|
56268
|
+
measured_mm: measured,
|
|
56269
|
+
...sweptFields
|
|
56270
|
+
};
|
|
56271
|
+
if (!clearanceHolds(measured, stored.required_mm, stored.allow_contact === true, result.intersecting)) {
|
|
55836
56272
|
checks.push({
|
|
55837
|
-
|
|
56273
|
+
...common,
|
|
55838
56274
|
status: "Violated",
|
|
55839
|
-
required_mm: stored.required_mm
|
|
55840
|
-
stored_mm: stored.measured_mm,
|
|
55841
|
-
measured_mm: measured,
|
|
55842
|
-
reason: `measured ${measured} mm is below the required ${stored.required_mm} mm`
|
|
56275
|
+
reason: result.poses_checked ? `measured ${measured} mm is below the required ${stored.required_mm} mm somewhere in the swept range of motion` : `measured ${measured} mm is below the required ${stored.required_mm} mm`
|
|
55843
56276
|
});
|
|
55844
56277
|
} else if (Math.abs(measured - stored.measured_mm) > STALE_EPS_MM) {
|
|
55845
56278
|
checks.push({
|
|
55846
|
-
|
|
56279
|
+
...common,
|
|
55847
56280
|
status: "Stale",
|
|
55848
|
-
required_mm: stored.required_mm,
|
|
55849
|
-
stored_mm: stored.measured_mm,
|
|
55850
|
-
measured_mm: measured,
|
|
55851
56281
|
reason: "geometry changed since the receipt was built, but the clearance still holds"
|
|
55852
56282
|
});
|
|
55853
56283
|
} else {
|
|
55854
|
-
checks.push({
|
|
55855
|
-
label,
|
|
55856
|
-
status: "Holds",
|
|
55857
|
-
required_mm: stored.required_mm,
|
|
55858
|
-
stored_mm: stored.measured_mm,
|
|
55859
|
-
measured_mm: measured
|
|
55860
|
-
});
|
|
56284
|
+
checks.push({ ...common, status: "Holds" });
|
|
55861
56285
|
}
|
|
55862
56286
|
}
|
|
55863
56287
|
const status = checks.some((c) => c.status === "Violated") ? "Violated" : checks.some((c) => c.status === "Stale") ? "Stale" : "Holds";
|
|
@@ -55887,7 +56311,7 @@ function err3(text) {
|
|
|
55887
56311
|
isError: true
|
|
55888
56312
|
};
|
|
55889
56313
|
}
|
|
55890
|
-
var CLEARANCE_CLAIM_PREFIX, MECH_DOMAIN2, CLEARANCE_ORACLE, STALE_EPS_MM, CONTACT_EPS_MM, round6, checkClearanceSchema, toolDefs11;
|
|
56314
|
+
var CLEARANCE_CLAIM_PREFIX, MECH_DOMAIN2, CLEARANCE_ORACLE, STALE_EPS_MM, CONTACT_EPS_MM, round6, checkClearanceSchema, MAX_SWEEP_POSES, pairKey, toolDefs11;
|
|
55891
56315
|
var init_clearance = __esm({
|
|
55892
56316
|
"src/tools/clearance.ts"() {
|
|
55893
56317
|
"use strict";
|
|
@@ -55935,15 +56359,47 @@ var init_clearance = __esm({
|
|
|
55935
56359
|
type: "boolean",
|
|
55936
56360
|
description: "Treat exact surface contact (measured distance within 0.001 mm of zero) as passing even though it is below `min_mm` \u2014 for parts designed to touch, e.g. a stage bolted flush to the chamber floor. Penetration beyond the tolerance still fails. Persisted with the spec when `label` is given."
|
|
55937
56361
|
},
|
|
55938
|
-
joint_state: jointStateSchemaProp
|
|
56362
|
+
joint_state: jointStateSchemaProp,
|
|
56363
|
+
sweep: {
|
|
56364
|
+
type: "array",
|
|
56365
|
+
description: "Range-of-motion sweep: drive these joints across their travel and report the WORST pose, not the authored one. Each axis is {joint, from, to, steps}; multiple axes form a Cartesian grid (capped at 4096 poses). Joint states are restored afterwards \u2014 the sweep never edits the document.",
|
|
56366
|
+
items: {
|
|
56367
|
+
type: "object",
|
|
56368
|
+
properties: {
|
|
56369
|
+
joint: { type: "string", description: "Joint id or name to drive." },
|
|
56370
|
+
from: { type: "number", description: "Start of travel (degrees for revolute, mm for prismatic)." },
|
|
56371
|
+
to: { type: "number", description: "End of travel." },
|
|
56372
|
+
steps: { type: "number", description: "Number of intervals; steps + 1 poses are sampled." }
|
|
56373
|
+
},
|
|
56374
|
+
required: ["joint", "from", "to", "steps"]
|
|
56375
|
+
}
|
|
56376
|
+
},
|
|
56377
|
+
ignore_pairs: {
|
|
56378
|
+
type: "array",
|
|
56379
|
+
description: "Audit mode: part id/name pairs whose contact is intended (a bolt in its own hole, a bearing pressed into its bore). Each entry is a two-element array.",
|
|
56380
|
+
items: {
|
|
56381
|
+
type: "array",
|
|
56382
|
+
items: { type: "string" }
|
|
56383
|
+
}
|
|
56384
|
+
},
|
|
56385
|
+
ignore_fixed_joints: {
|
|
56386
|
+
type: "boolean",
|
|
56387
|
+
description: "Audit mode: skip pairs joined by a Fixed joint \u2014 they are bolted together and always 'interfere'. Default true."
|
|
56388
|
+
},
|
|
56389
|
+
ignore_adjacent: {
|
|
56390
|
+
type: "boolean",
|
|
56391
|
+
description: "Audit mode: skip every directly-jointed parent/child pair, not just Fixed ones. Default false \u2014 adjacent links can and do clash away from the joint axis."
|
|
56392
|
+
}
|
|
55939
56393
|
},
|
|
55940
|
-
required: ["document_id"
|
|
56394
|
+
required: ["document_id"]
|
|
55941
56395
|
};
|
|
56396
|
+
MAX_SWEEP_POSES = 4096;
|
|
56397
|
+
pairKey = (a, b) => a < b ? `${a}\0${b}` : `${b}\0${a}`;
|
|
55942
56398
|
toolDefs11 = [
|
|
55943
56399
|
{
|
|
55944
56400
|
name: "check_clearance",
|
|
55945
56401
|
pack: null,
|
|
55946
|
-
description: "Measure the minimum distance between two groups of parts in a CAD session and assert it stays above `min_mm` \u2014 air gaps, press fits, screw-head clearances. Reports the measured minimum (negative = penetration depth), a clear/touching/intersecting verdict, the worst part pair, and pass/fail. Pass `allow_contact: true` for parts designed to touch (e.g. bolted flush) so exact contact passes instead of reading as an intersection. Pass `joint_state` to measure at
|
|
56402
|
+
description: "Measure the minimum distance between two groups of parts in a CAD session and assert it stays above `min_mm` \u2014 air gaps, press fits, screw-head clearances. Reports the measured minimum (negative = penetration depth), a clear/touching/intersecting verdict, the worst part pair, and pass/fail. Pass `allow_contact: true` for parts designed to touch (e.g. bolted flush) so exact contact passes instead of reading as an intersection. Pass `joint_state` to measure at one real pose (joint id or name \u2192 degrees, or mm for sliders) instead of the stored one. Give it a `label` to persist the assertion on the document: build_receipt then emits it as a mech.clearance claim and verify_receipt re-verifies it as Holds / Stale / Violated when geometry changes. Two modes beyond the single-pair snapshot: (1) `sweep` \u2014 pass [{joint, from, to, steps}] to drive the mechanism through its range of motion and report the WORST pose, not the one it was authored in (a linkage modelled at mid-travel routinely clears there and collides at both ends); persisted with the label, so the assertion re-verifies over the same range. (2) audit \u2014 omit `group_a`/`group_b` entirely to broadphase EVERY part pair in the document and list everything that interpenetrates, with penetration depth; whitelist intended contact with `ignore_pairs` (Fixed-joint pairs are skipped by default). Combine both for 'does this machine ever hit itself anywhere in its workspace'.",
|
|
55947
56403
|
inputSchema: checkClearanceSchema,
|
|
55948
56404
|
handler: (a, c) => checkClearance(a, c.engine),
|
|
55949
56405
|
behavior: behavior({ writesDoc: true })
|
|
@@ -56746,6 +57202,14 @@ function totalsSummary(totals) {
|
|
|
56746
57202
|
currency: totals.currency
|
|
56747
57203
|
};
|
|
56748
57204
|
}
|
|
57205
|
+
function documentHardware(documentId) {
|
|
57206
|
+
try {
|
|
57207
|
+
const doc = documents.get(documentId);
|
|
57208
|
+
return Array.isArray(doc?.hardware) ? doc.hardware : [];
|
|
57209
|
+
} catch {
|
|
57210
|
+
return [];
|
|
57211
|
+
}
|
|
57212
|
+
}
|
|
56749
57213
|
async function bomCreate(input, store, user) {
|
|
56750
57214
|
const args = input ?? {};
|
|
56751
57215
|
const owner = ownerId(user);
|
|
@@ -56760,7 +57224,23 @@ async function bomCreate(input, store, user) {
|
|
|
56760
57224
|
created_at: now,
|
|
56761
57225
|
updated_at: now
|
|
56762
57226
|
};
|
|
56763
|
-
const
|
|
57227
|
+
const fromGeometry = args.from_geometry !== false;
|
|
57228
|
+
const geometryLines = [];
|
|
57229
|
+
if (fromGeometry && bom.document_id) {
|
|
57230
|
+
for (const hw of documentHardware(bom.document_id)) {
|
|
57231
|
+
geometryLines.push({
|
|
57232
|
+
kind: "cots",
|
|
57233
|
+
name: hw.spec,
|
|
57234
|
+
catalog_id: hw.catalog_id ?? void 0,
|
|
57235
|
+
qty: hw.qty,
|
|
57236
|
+
notes: "counted from the model geometry"
|
|
57237
|
+
});
|
|
57238
|
+
}
|
|
57239
|
+
}
|
|
57240
|
+
const lineInputs = [
|
|
57241
|
+
...geometryLines,
|
|
57242
|
+
...Array.isArray(args.lines) ? args.lines : []
|
|
57243
|
+
];
|
|
56764
57244
|
for (let i = 0; i < lineInputs.length; i++) {
|
|
56765
57245
|
const built = await buildLine(lineInputs[i] ?? {}, store, owner);
|
|
56766
57246
|
if ("error" in built) return err4(`lines[${i}]: ${built.error}`);
|
|
@@ -56936,6 +57416,7 @@ var init_bom = __esm({
|
|
|
56936
57416
|
init_pricing();
|
|
56937
57417
|
init_types4();
|
|
56938
57418
|
init_mech_parts();
|
|
57419
|
+
init_session_core();
|
|
56939
57420
|
init_tool_def();
|
|
56940
57421
|
PRICING_NOTE = "All prices are ESTIMATES (Phase-0 quote estimates, catalog price bands, or caller-supplied numbers) \u2014 not binding quotes.";
|
|
56941
57422
|
memBoms = /* @__PURE__ */ new Map();
|
|
@@ -57001,6 +57482,10 @@ var init_bom = __esm({
|
|
|
57001
57482
|
type: "array",
|
|
57002
57483
|
items: { type: "object", properties: lineProperties, required: ["kind"] },
|
|
57003
57484
|
description: "Optional full line list to build the BOM in ONE call (recommended on serverless \u2014 BOMs are in-memory per instance, so one-shot creation is the robust path)."
|
|
57485
|
+
},
|
|
57486
|
+
from_geometry: {
|
|
57487
|
+
type: "boolean",
|
|
57488
|
+
description: "Seed COTS lines from the hardware the document's geometry declares \u2014 every loon `bolt`/`bolt-circle` (plus its washers and nuts), with quantities already multiplied through patterns. Default true when document_id is given."
|
|
57004
57489
|
}
|
|
57005
57490
|
},
|
|
57006
57491
|
required: []
|
|
@@ -57035,7 +57520,7 @@ var init_bom = __esm({
|
|
|
57035
57520
|
{
|
|
57036
57521
|
name: "bom_create",
|
|
57037
57522
|
pack: "bom",
|
|
57038
|
-
description: "Create a project bill of materials \u2014 the deliverable that collects every manufactured part (PCBs, sheet metal, 3D prints; link quote_manufacturing quotes by quote_id) and COTS part (bearings, shafts, screws, magnets; link search_mechanical_parts entries by catalog_id) in one place. Optionally attach a document_id and assembly notes. Pass the full `lines` array to build the whole BOM in ONE call \u2014 recommended, since BOMs are in-memory per server instance. All prices are estimates and flagged as such.",
|
|
57523
|
+
description: "Create a project bill of materials \u2014 the deliverable that collects every manufactured part (PCBs, sheet metal, 3D prints; link quote_manufacturing quotes by quote_id) and COTS part (bearings, shafts, screws, magnets; link search_mechanical_parts entries by catalog_id) in one place. Optionally attach a document_id and assembly notes. Pass the full `lines` array to build the whole BOM in ONE call \u2014 recommended, since BOMs are in-memory per server instance. When document_id names a resident session, fastener lines are seeded from the geometry itself (every loon `bolt`/`bolt-circle`, quantities multiplied through patterns) \u2014 pass from_geometry:false to opt out. All prices are estimates and flagged as such.",
|
|
57039
57524
|
inputSchema: bomCreateSchema,
|
|
57040
57525
|
handler: (a, c) => bomCreate(a, c.fabricateStore, c.user),
|
|
57041
57526
|
behavior: behavior({})
|
|
@@ -71828,7 +72313,7 @@ var init_structure = __esm({
|
|
|
71828
72313
|
{
|
|
71829
72314
|
name: "analyze_structure",
|
|
71830
72315
|
pack: null,
|
|
71831
|
-
description: 'Static structural FEA on a part\'s real geometry with fail-closed mesh-convergence gating: linear-tet fill of the tessellated interior at 2+ refinement levels, linear-elastic solve (PCG), returning max von Mises stress, max displacement, discretization-error estimates, and (with yield_strength_mpa) a safety factor \u2014 plus `vcad.fea-claims/1` and unified-receipt claims. If QoIs disagree across levels the verdict is Unverifiable and NO stress/displacement claim is emitted (raise resolution, or fillet the singular corner it points at). Claims carry basis "predicted" and roll up Provisional until hardware is load-tested. Scope: small-displacement linear elasticity, one isotropic material; no plasticity, buckling, contact, or dynamic loads; boundary is staircase-approximated at the lattice pitch. Use predict_physics for the fast coarse steering loop; use this for the audited answer.',
|
|
72316
|
+
description: 'Static structural FEA on a part\'s real geometry with fail-closed mesh-convergence gating: linear-tet fill of the tessellated interior at 2+ refinement levels, linear-elastic solve (PCG), returning max von Mises stress, max displacement, discretization-error estimates, and (with yield_strength_mpa) a safety factor \u2014 plus `vcad.fea-claims/1` and unified-receipt claims. If QoIs disagree across levels the verdict is Unverifiable and NO stress/displacement claim is emitted (raise resolution, or fillet the singular corner it points at). Claims carry basis "predicted" and roll up Provisional until hardware is load-tested. Scope: small-displacement linear elasticity, one isotropic material; no plasticity, buckling, contact, or dynamic loads; boundary is staircase-approximated at the lattice pitch. THIN-WALLED PARTS (sheet metal, tube frame, plate) are outside this tool: it measures the thinnest load-bearing section and fails closed with the cell arithmetic and a pointer to beam_check, which is the more accurate answer for a prismatic member anyway. Use predict_physics for the fast coarse steering loop; use this for the audited answer on chunky geometry.',
|
|
71832
72317
|
inputSchema: {
|
|
71833
72318
|
type: "object",
|
|
71834
72319
|
required: ["document_id", "part", "loads", "supports"],
|
|
@@ -71893,7 +72378,7 @@ var init_structure = __esm({
|
|
|
71893
72378
|
},
|
|
71894
72379
|
resolution: {
|
|
71895
72380
|
type: "number",
|
|
71896
|
-
description: "Coarse-level lattice cells along the longest bbox axis (default 24). The finest level is resolution * 2^(levels-1), capped at 160 for this tier. Keep >= ~6 cells through the thinnest load-bearing section."
|
|
72381
|
+
description: "Coarse-level lattice cells along the longest bbox axis (default 24). The finest level is resolution * 2^(levels-1), capped at 160 for this tier. Keep >= ~6 cells through the thinnest load-bearing section \u2014 the tool measures the part and refuses below ~4, with the cell arithmetic attached. Raising this is NOT the lever for a thin wall (a 2 mm wall on a 300 mm member needs ~950 cells): use beam_check."
|
|
71897
72382
|
},
|
|
71898
72383
|
levels: {
|
|
71899
72384
|
type: "number",
|
|
@@ -71944,6 +72429,89 @@ var init_structure = __esm({
|
|
|
71944
72429
|
});
|
|
71945
72430
|
},
|
|
71946
72431
|
behavior: behavior({})
|
|
72432
|
+
},
|
|
72433
|
+
{
|
|
72434
|
+
name: "beam_check",
|
|
72435
|
+
pack: null,
|
|
72436
|
+
description: 'Closed-form structural check of a PRISMATIC member \u2014 the audited answer for thin-walled geometry, where the lattice in analyze_structure cannot put enough cells through the wall at any affordable resolution. Give it a profile (rect, rect_tube, round, round_tube, i_beam), a span, an end condition, and a load case; get section properties (A, I_y, I_z, J, section moduli, torsional stiffness), stresses (bending, axial, torsional and transverse shear, von Mises), deflection with the Timoshenko shear term, twist, the Euler buckling load under compression, and a safety factor \u2014 plus `vcad.fea-claims/1` and unified-receipt claims under `structure.beam.*`. Exact integrals where they exist (round, round tube, rectangle bending), the convergent Saint-Venant series for solid-rectangle torsion, Bredt closed thin-wall theory for tube torsion \u2014 every approximation is stated in the returned notes. For a constant cross-section this is not a fallback: it beats a staircased lattice on accuracy and costs microseconds. Fail-closed: too stubby for beam theory (L/depth < 5), too thick-walled for Bredt, deflecting past a tenth of the span, torque on an open section, or buckling before it yields \u2014 any of those makes the verdict Unverifiable and NO QoI is claimed, with the reason naming the route forward. Claims carry basis "predicted" and roll up Provisional until hardware is load-tested. Scope: one uniform section over the whole span, idealized ends, no stress concentrations (holes, welds, corner radii), no local wall buckling, no fatigue. Needs no document \u2014 it is geometry-by-description, so it works before the part exists.',
|
|
72437
|
+
inputSchema: {
|
|
72438
|
+
type: "object",
|
|
72439
|
+
required: ["profile", "length_mm", "end_condition"],
|
|
72440
|
+
properties: {
|
|
72441
|
+
profile: {
|
|
72442
|
+
type: "object",
|
|
72443
|
+
required: ["type"],
|
|
72444
|
+
description: 'Cross-section. The member runs along X and the section lives in (Y, Z): width_mm is the Y extent, height_mm the Z extent. One of: {"type":"rect",width_mm,height_mm}, {"type":"rect_tube",width_mm,height_mm,wall_mm}, {"type":"round",diameter_mm}, {"type":"round_tube",diameter_mm,wall_mm}, {"type":"i_beam",width_mm,height_mm,flange_mm,web_mm}. Tube dimensions are OUTSIDE dimensions.',
|
|
72445
|
+
properties: {
|
|
72446
|
+
type: {
|
|
72447
|
+
type: "string",
|
|
72448
|
+
enum: ["rect", "rect_tube", "round", "round_tube", "i_beam"]
|
|
72449
|
+
},
|
|
72450
|
+
width_mm: { type: "number", description: "Y extent, mm." },
|
|
72451
|
+
height_mm: { type: "number", description: "Z extent, mm." },
|
|
72452
|
+
wall_mm: { type: "number", description: "Wall thickness, mm." },
|
|
72453
|
+
diameter_mm: { type: "number", description: "Outside diameter, mm." },
|
|
72454
|
+
flange_mm: { type: "number", description: "Flange thickness, mm." },
|
|
72455
|
+
web_mm: { type: "number", description: "Web thickness, mm." }
|
|
72456
|
+
}
|
|
72457
|
+
},
|
|
72458
|
+
length_mm: {
|
|
72459
|
+
type: "number",
|
|
72460
|
+
description: "Free span between supports, mm."
|
|
72461
|
+
},
|
|
72462
|
+
end_condition: {
|
|
72463
|
+
type: "string",
|
|
72464
|
+
enum: [
|
|
72465
|
+
"cantilever_tip",
|
|
72466
|
+
"cantilever_uniform",
|
|
72467
|
+
"simple_center",
|
|
72468
|
+
"simple_uniform",
|
|
72469
|
+
"fixed_fixed_center",
|
|
72470
|
+
"fixed_fixed_uniform"
|
|
72471
|
+
],
|
|
72472
|
+
description: "Support and load arrangement. `*_uniform` spreads the transverse force over the span; `*_center`/`*_tip` concentrates it. Also sets the Euler effective-length factor (cantilever K=2, simple K=1, fixed-fixed K=0.5)."
|
|
72473
|
+
},
|
|
72474
|
+
transverse_force_n: {
|
|
72475
|
+
type: "number",
|
|
72476
|
+
description: "Total transverse force, N (magnitude). Default 0."
|
|
72477
|
+
},
|
|
72478
|
+
bend_axis: {
|
|
72479
|
+
type: "string",
|
|
72480
|
+
enum: ["y", "z"],
|
|
72481
|
+
description: "Which principal axis to bend about: `y` uses I_y and deflects along Z (default), `z` uses I_z and deflects along Y. Matters for any non-square section."
|
|
72482
|
+
},
|
|
72483
|
+
torque_nmm: {
|
|
72484
|
+
type: "number",
|
|
72485
|
+
description: "Torque about the member axis, N\xB7mm (1 N\xB7m = 1000 N\xB7mm). Default 0. This is the case closed-form theory answers best and the lattice answers worst."
|
|
72486
|
+
},
|
|
72487
|
+
axial_force_n: {
|
|
72488
|
+
type: "number",
|
|
72489
|
+
description: "Axial force, N \u2014 positive tension, negative compression. Compression is also checked against Euler buckling, and a member that buckles is reported Unverifiable rather than 'safe'. Default 0."
|
|
72490
|
+
},
|
|
72491
|
+
youngs_modulus_mpa: {
|
|
72492
|
+
type: "number",
|
|
72493
|
+
description: "Young's modulus, MPa. Default 69000 (6061 aluminum); steel ~200000, PLA ~2300."
|
|
72494
|
+
},
|
|
72495
|
+
poisson: {
|
|
72496
|
+
type: "number",
|
|
72497
|
+
description: "Poisson's ratio in [0, 0.5). Default 0.33; sets G = E/(2(1+nu))."
|
|
72498
|
+
},
|
|
72499
|
+
yield_strength_mpa: {
|
|
72500
|
+
type: "number",
|
|
72501
|
+
description: "Yield strength, MPa. When given, safety_factor = yield / von Mises is computed and claimed (applicable checks only)."
|
|
72502
|
+
}
|
|
72503
|
+
}
|
|
72504
|
+
},
|
|
72505
|
+
handler: (args, ctx) => {
|
|
72506
|
+
const a = args;
|
|
72507
|
+
const started = performance.now();
|
|
72508
|
+
const out = ctx.engine.feaCheckBeam(JSON.stringify(a));
|
|
72509
|
+
return textResult7({
|
|
72510
|
+
solve_ms: Math.round(performance.now() - started),
|
|
72511
|
+
...out
|
|
72512
|
+
});
|
|
72513
|
+
},
|
|
72514
|
+
behavior: behavior({})
|
|
71947
72515
|
}
|
|
71948
72516
|
];
|
|
71949
72517
|
}
|
|
@@ -79647,8 +80215,8 @@ var init_server3 = __esm({
|
|
|
79647
80215
|
init_order_feed();
|
|
79648
80216
|
init_animate();
|
|
79649
80217
|
PKG_VERSION = (() => {
|
|
79650
|
-
if ("0.9.4-main.
|
|
79651
|
-
return "0.9.4-main.
|
|
80218
|
+
if ("0.9.4-main.38") {
|
|
80219
|
+
return "0.9.4-main.38";
|
|
79652
80220
|
}
|
|
79653
80221
|
try {
|
|
79654
80222
|
const req = createRequire2(import.meta.url);
|
|
@@ -79657,8 +80225,8 @@ var init_server3 = __esm({
|
|
|
79657
80225
|
return "0.0.0";
|
|
79658
80226
|
}
|
|
79659
80227
|
})();
|
|
79660
|
-
BUILD_SHA = "
|
|
79661
|
-
BUILD_TIME = "2026-07-26T13:07
|
|
80228
|
+
BUILD_SHA = "254d4e9d13825ac74a895590703959d77898e1f3";
|
|
80229
|
+
BUILD_TIME = "2026-07-26T13:42:07.736Z";
|
|
79662
80230
|
SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
|
|
79663
80231
|
VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
|
|
79664
80232
|
INSTANCE_ID = randomUUID6().slice(0, 8);
|
|
@@ -79806,6 +80374,7 @@ var init_server3 = __esm({
|
|
|
79806
80374
|
"solve_thermal",
|
|
79807
80375
|
"simulate_flow",
|
|
79808
80376
|
"analyze_structure",
|
|
80377
|
+
"beam_check",
|
|
79809
80378
|
"simulate_neutron_shield",
|
|
79810
80379
|
"simulate_lattice_gauge",
|
|
79811
80380
|
"analyze_tolerance_stackup",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vcad/mcp",
|
|
3
|
-
"version": "0.9.4-main.
|
|
3
|
+
"version": "0.9.4-main.38",
|
|
4
4
|
"description": "vcad MCP server — parametric CAD + PCB design tools for AI agents (self-contained: bundled server + kernel WASM)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"homepage": "https://vcad.io",
|
|
21
21
|
"vcadBuild": {
|
|
22
|
-
"sha": "
|
|
23
|
-
"builtAt": "2026-07-26T13:07
|
|
22
|
+
"sha": "254d4e9d13825ac74a895590703959d77898e1f3",
|
|
23
|
+
"builtAt": "2026-07-26T13:42:07.736Z"
|
|
24
24
|
}
|
|
25
25
|
}
|
package/vcad_kernel_wasm_bg.wasm
CHANGED
|
Binary file
|