@vcad/mcp 0.9.4-main.35 → 0.9.4-main.36
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 +455 -53
- 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 3b9087f300b569c70aa24cebc52af06d15e59187 at 2026-07-26T13:20:01.737Z. Source: https://github.com/ecto/vcad
|
package/index.mjs
CHANGED
|
@@ -40914,6 +40914,25 @@ var init_CHANGELOG = __esm({
|
|
|
40914
40914
|
CHANGELOG_default = {
|
|
40915
40915
|
$schema: "./changelog.schema.json",
|
|
40916
40916
|
entries: [
|
|
40917
|
+
{
|
|
40918
|
+
id: "2026-07-26-swept-and-all-pairs-clearance",
|
|
40919
|
+
version: "0.9.4",
|
|
40920
|
+
date: "2026-07-26",
|
|
40921
|
+
category: "feat",
|
|
40922
|
+
title: "check_clearance sweeps the range of motion",
|
|
40923
|
+
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.",
|
|
40924
|
+
features: [
|
|
40925
|
+
"clearance",
|
|
40926
|
+
"assembly",
|
|
40927
|
+
"kinematics",
|
|
40928
|
+
"mcp"
|
|
40929
|
+
],
|
|
40930
|
+
mcpTools: [
|
|
40931
|
+
"check_clearance",
|
|
40932
|
+
"build_receipt",
|
|
40933
|
+
"verify_receipt"
|
|
40934
|
+
]
|
|
40935
|
+
},
|
|
40917
40936
|
{
|
|
40918
40937
|
id: "2026-07-26-render-loon-directly",
|
|
40919
40938
|
version: "0.9.4",
|
|
@@ -55576,10 +55595,23 @@ function clearanceVerdict(distanceMm) {
|
|
|
55576
55595
|
if (distanceMm <= CONTACT_EPS_MM) return "touching";
|
|
55577
55596
|
return "clear";
|
|
55578
55597
|
}
|
|
55579
|
-
function clearanceHolds(distanceMm, minMm, allowContact) {
|
|
55598
|
+
function clearanceHolds(distanceMm, minMm, allowContact, intersecting = false) {
|
|
55599
|
+
if (intersecting) return false;
|
|
55580
55600
|
return distanceMm >= minMm || allowContact && clearanceVerdict(distanceMm) === "touching";
|
|
55581
55601
|
}
|
|
55582
|
-
function
|
|
55602
|
+
function worseThan(a, b) {
|
|
55603
|
+
if (a.intersecting !== b.intersecting) return a.intersecting;
|
|
55604
|
+
return a.distance < b.distance;
|
|
55605
|
+
}
|
|
55606
|
+
function placeMesh(mesh, transform2) {
|
|
55607
|
+
if (!transform2) return mesh;
|
|
55608
|
+
return transformMesh(mesh, {
|
|
55609
|
+
translate: transform2.translation,
|
|
55610
|
+
rotate: transform2.rotation,
|
|
55611
|
+
scale: transform2.scale
|
|
55612
|
+
});
|
|
55613
|
+
}
|
|
55614
|
+
function poseableParts(doc, engine) {
|
|
55583
55615
|
const scene = engine.evaluate(doc);
|
|
55584
55616
|
const visibleRoots = doc.roots.filter((e) => e.visible !== false);
|
|
55585
55617
|
const out = [];
|
|
@@ -55588,23 +55620,30 @@ function partCandidates(doc, engine) {
|
|
|
55588
55620
|
const node = doc.nodes[String(rootId)];
|
|
55589
55621
|
const mesh = scene.parts[i].mesh;
|
|
55590
55622
|
if (!mesh || mesh.positions.length === 0) continue;
|
|
55591
|
-
out.push({ id: String(rootId), name: node?.name ?? void 0, mesh });
|
|
55623
|
+
out.push({ id: String(rootId), name: node?.name ?? void 0, localMesh: mesh });
|
|
55592
55624
|
}
|
|
55593
55625
|
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;
|
|
55626
|
+
if (!inst.mesh || inst.mesh.positions.length === 0) continue;
|
|
55600
55627
|
out.push({
|
|
55601
55628
|
id: inst.instanceId,
|
|
55602
55629
|
name: inst.name ?? void 0,
|
|
55603
|
-
mesh
|
|
55630
|
+
localMesh: inst.mesh,
|
|
55631
|
+
transform: inst.transform ?? void 0,
|
|
55632
|
+
instanceId: inst.instanceId
|
|
55604
55633
|
});
|
|
55605
55634
|
}
|
|
55606
55635
|
return out;
|
|
55607
55636
|
}
|
|
55637
|
+
function placeParts(parts, worldTransforms) {
|
|
55638
|
+
const out = [];
|
|
55639
|
+
for (const p of parts) {
|
|
55640
|
+
const transform2 = (p.instanceId ? worldTransforms?.get(p.instanceId) : void 0) ?? p.transform;
|
|
55641
|
+
const mesh = placeMesh(p.localMesh, transform2);
|
|
55642
|
+
if (!mesh || mesh.positions.length === 0) continue;
|
|
55643
|
+
out.push({ id: p.id, ...p.name ? { name: p.name } : {}, mesh });
|
|
55644
|
+
}
|
|
55645
|
+
return out;
|
|
55646
|
+
}
|
|
55608
55647
|
function resolveGroup(candidates, ids) {
|
|
55609
55648
|
const parts = /* @__PURE__ */ new Map();
|
|
55610
55649
|
const missing = [];
|
|
@@ -55616,16 +55655,115 @@ function resolveGroup(candidates, ids) {
|
|
|
55616
55655
|
}
|
|
55617
55656
|
return { parts: [...parts.values()], missing };
|
|
55618
55657
|
}
|
|
55619
|
-
function
|
|
55658
|
+
function resolveSweepAxes(doc, axes) {
|
|
55659
|
+
const joints = doc.joints ?? [];
|
|
55660
|
+
const resolved = [];
|
|
55661
|
+
for (const axis of axes) {
|
|
55662
|
+
const wanted = String(axis.joint);
|
|
55663
|
+
const found = joints.find((j) => j.id === wanted) ?? joints.find((j) => j.name === wanted);
|
|
55664
|
+
if (!found) {
|
|
55665
|
+
const available = joints.map((j) => `${j.id}${j.name ? ` (${j.name})` : ""}`).join(", ");
|
|
55666
|
+
return { error: `No joint with id or name "${wanted}". Available: ${available || "none"}` };
|
|
55667
|
+
}
|
|
55668
|
+
if (!Number.isFinite(axis.from) || !Number.isFinite(axis.to)) {
|
|
55669
|
+
return { error: `Sweep axis "${wanted}" needs finite \`from\` and \`to\`.` };
|
|
55670
|
+
}
|
|
55671
|
+
const steps = Math.trunc(axis.steps);
|
|
55672
|
+
if (!Number.isFinite(steps) || steps < 1) {
|
|
55673
|
+
return { error: `Sweep axis "${wanted}" needs \`steps\` >= 1.` };
|
|
55674
|
+
}
|
|
55675
|
+
resolved.push({ joint: found.id, from: axis.from, to: axis.to, steps });
|
|
55676
|
+
}
|
|
55677
|
+
if (resolved.length === 0) return { error: "`sweep` needs at least one joint axis." };
|
|
55678
|
+
const total = resolved.reduce((n, a) => n * (a.steps + 1), 1);
|
|
55679
|
+
if (total > MAX_SWEEP_POSES) {
|
|
55680
|
+
return {
|
|
55681
|
+
error: `Sweep grid is ${total} poses (limit ${MAX_SWEEP_POSES}). Lower \`steps\` or sweep fewer joints at once.`
|
|
55682
|
+
};
|
|
55683
|
+
}
|
|
55684
|
+
return { axes: resolved };
|
|
55685
|
+
}
|
|
55686
|
+
function poseGrid(axes) {
|
|
55687
|
+
let poses = [[]];
|
|
55688
|
+
for (const axis of axes) {
|
|
55689
|
+
const n = Math.trunc(axis.steps);
|
|
55690
|
+
const next = [];
|
|
55691
|
+
for (const pose of poses) {
|
|
55692
|
+
for (let i = 0; i <= n; i++) {
|
|
55693
|
+
const state = axis.from + (axis.to - axis.from) * i / n;
|
|
55694
|
+
next.push([...pose, { joint: axis.joint, state }]);
|
|
55695
|
+
}
|
|
55696
|
+
}
|
|
55697
|
+
poses = next;
|
|
55698
|
+
}
|
|
55699
|
+
return poses;
|
|
55700
|
+
}
|
|
55701
|
+
function poseTransforms(doc, poses) {
|
|
55702
|
+
const joints = doc.joints ?? [];
|
|
55703
|
+
const byId = new Map(joints.map((j) => [j.id, j]));
|
|
55704
|
+
const saved = joints.map((j) => j.state);
|
|
55705
|
+
try {
|
|
55706
|
+
return poses.map((pose) => {
|
|
55707
|
+
for (const { joint, state } of pose) {
|
|
55708
|
+
const j = byId.get(joint);
|
|
55709
|
+
if (j) j.state = state;
|
|
55710
|
+
}
|
|
55711
|
+
return solveForwardKinematics2(doc);
|
|
55712
|
+
});
|
|
55713
|
+
} finally {
|
|
55714
|
+
joints.forEach((j, i) => {
|
|
55715
|
+
j.state = saved[i];
|
|
55716
|
+
});
|
|
55717
|
+
}
|
|
55718
|
+
}
|
|
55719
|
+
function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
|
|
55620
55720
|
if (groupA.length === 0 || groupB.length === 0) {
|
|
55621
55721
|
return { error: "Both `group_a` and `group_b` need at least one part id." };
|
|
55622
55722
|
}
|
|
55623
|
-
let
|
|
55723
|
+
let base;
|
|
55724
|
+
try {
|
|
55725
|
+
base = poseableParts(doc, engine);
|
|
55726
|
+
} catch (e) {
|
|
55727
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
55728
|
+
}
|
|
55729
|
+
if (!sweep || sweep.length === 0) {
|
|
55730
|
+
return measureGroups(engine, placeParts(base), groupA, groupB);
|
|
55731
|
+
}
|
|
55732
|
+
const { axes, error: sweepError } = resolveSweepAxes(doc, sweep);
|
|
55733
|
+
if (sweepError || !axes) return { error: sweepError ?? "sweep could not be resolved" };
|
|
55734
|
+
const poses = poseGrid(axes);
|
|
55735
|
+
let transforms;
|
|
55624
55736
|
try {
|
|
55625
|
-
|
|
55737
|
+
transforms = poseTransforms(doc, poses);
|
|
55626
55738
|
} catch (e) {
|
|
55627
55739
|
return { error: e instanceof Error ? e.message : String(e) };
|
|
55628
55740
|
}
|
|
55741
|
+
let worst2;
|
|
55742
|
+
for (let i = 0; i < poses.length; i++) {
|
|
55743
|
+
const { result, error: error2 } = measureGroups(
|
|
55744
|
+
engine,
|
|
55745
|
+
placeParts(base, transforms[i]),
|
|
55746
|
+
groupA,
|
|
55747
|
+
groupB
|
|
55748
|
+
);
|
|
55749
|
+
if (error2 || !result) return { error: error2 };
|
|
55750
|
+
if (!worst2 || worseThan(
|
|
55751
|
+
{ distance: result.distance_mm, intersecting: result.intersecting },
|
|
55752
|
+
{ distance: worst2.result.distance_mm, intersecting: worst2.result.intersecting }
|
|
55753
|
+
)) {
|
|
55754
|
+
worst2 = { result, pose: poses[i] };
|
|
55755
|
+
}
|
|
55756
|
+
}
|
|
55757
|
+
if (!worst2) return { error: "Sweep produced no poses to measure." };
|
|
55758
|
+
return {
|
|
55759
|
+
result: {
|
|
55760
|
+
...worst2.result,
|
|
55761
|
+
worst_pose: worst2.pose.map((p) => ({ joint: p.joint, state: round6(p.state) })),
|
|
55762
|
+
poses_checked: poses.length
|
|
55763
|
+
}
|
|
55764
|
+
};
|
|
55765
|
+
}
|
|
55766
|
+
function measureGroups(engine, candidates, groupA, groupB) {
|
|
55629
55767
|
const a = resolveGroup(candidates, groupA);
|
|
55630
55768
|
const b = resolveGroup(candidates, groupB);
|
|
55631
55769
|
const missing = [...a.missing, ...b.missing];
|
|
@@ -55652,7 +55790,7 @@ function computeGroupClearance(doc, engine, groupA, groupB) {
|
|
|
55652
55790
|
return { error: e instanceof Error ? e.message : String(e) };
|
|
55653
55791
|
}
|
|
55654
55792
|
pairs += 1;
|
|
55655
|
-
if (!worst2 || r
|
|
55793
|
+
if (!worst2 || worseThan(r, worst2.r)) {
|
|
55656
55794
|
worst2 = { a: pa, b: pb, r };
|
|
55657
55795
|
}
|
|
55658
55796
|
}
|
|
@@ -55677,6 +55815,132 @@ function computeGroupClearance(doc, engine, groupA, groupB) {
|
|
|
55677
55815
|
}
|
|
55678
55816
|
};
|
|
55679
55817
|
}
|
|
55818
|
+
function meshAabb(mesh) {
|
|
55819
|
+
const p = mesh.positions;
|
|
55820
|
+
const min = [Infinity, Infinity, Infinity];
|
|
55821
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
55822
|
+
for (let i = 0; i + 2 < p.length; i += 3) {
|
|
55823
|
+
for (let k = 0; k < 3; k++) {
|
|
55824
|
+
const v = p[i + k];
|
|
55825
|
+
if (v < min[k]) min[k] = v;
|
|
55826
|
+
if (v > max[k]) max[k] = v;
|
|
55827
|
+
}
|
|
55828
|
+
}
|
|
55829
|
+
return { min, max };
|
|
55830
|
+
}
|
|
55831
|
+
function aabbNear(a, b, margin) {
|
|
55832
|
+
for (let k = 0; k < 3; k++) {
|
|
55833
|
+
if (a.min[k] - margin > b.max[k]) return false;
|
|
55834
|
+
if (b.min[k] - margin > a.max[k]) return false;
|
|
55835
|
+
}
|
|
55836
|
+
return true;
|
|
55837
|
+
}
|
|
55838
|
+
function computeInterferenceAudit(doc, engine, opts) {
|
|
55839
|
+
let base;
|
|
55840
|
+
try {
|
|
55841
|
+
base = poseableParts(doc, engine);
|
|
55842
|
+
} catch (e) {
|
|
55843
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
55844
|
+
}
|
|
55845
|
+
let poses = [];
|
|
55846
|
+
let transforms = [void 0];
|
|
55847
|
+
if (opts.sweep && opts.sweep.length > 0) {
|
|
55848
|
+
const { axes, error: error2 } = resolveSweepAxes(doc, opts.sweep);
|
|
55849
|
+
if (error2 || !axes) return { error: error2 ?? "sweep could not be resolved" };
|
|
55850
|
+
poses = poseGrid(axes);
|
|
55851
|
+
try {
|
|
55852
|
+
transforms = poseTransforms(doc, poses);
|
|
55853
|
+
} catch (e) {
|
|
55854
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
55855
|
+
}
|
|
55856
|
+
}
|
|
55857
|
+
const first = placeParts(base, transforms[0]);
|
|
55858
|
+
if (first.length < 2) {
|
|
55859
|
+
return { error: "Fewer than two measurable parts \u2014 nothing to audit." };
|
|
55860
|
+
}
|
|
55861
|
+
const ignore = /* @__PURE__ */ new Set();
|
|
55862
|
+
const unresolved = [];
|
|
55863
|
+
const resolveId = (raw) => {
|
|
55864
|
+
const wanted = String(raw);
|
|
55865
|
+
return (first.find((c) => c.id === wanted) ?? first.find((c) => c.name === wanted))?.id;
|
|
55866
|
+
};
|
|
55867
|
+
for (const [rawA, rawB] of opts.ignorePairs) {
|
|
55868
|
+
const a = resolveId(rawA);
|
|
55869
|
+
const b = resolveId(rawB);
|
|
55870
|
+
if (!a) unresolved.push(String(rawA));
|
|
55871
|
+
if (!b) unresolved.push(String(rawB));
|
|
55872
|
+
if (a && b) ignore.add(pairKey(a, b));
|
|
55873
|
+
}
|
|
55874
|
+
for (const joint of doc.joints ?? []) {
|
|
55875
|
+
const parent = joint.parentInstanceId;
|
|
55876
|
+
if (!parent) continue;
|
|
55877
|
+
const fixed = joint.kind?.type === "Fixed";
|
|
55878
|
+
if (opts.ignoreAdjacent || fixed && opts.ignoreFixedJoints) {
|
|
55879
|
+
ignore.add(pairKey(parent, joint.childInstanceId));
|
|
55880
|
+
}
|
|
55881
|
+
}
|
|
55882
|
+
const margin = Math.max(opts.minMm, 0);
|
|
55883
|
+
const worstByPair = /* @__PURE__ */ new Map();
|
|
55884
|
+
const broadphase = /* @__PURE__ */ new Set();
|
|
55885
|
+
let queries = 0;
|
|
55886
|
+
let ignored = 0;
|
|
55887
|
+
for (let p = 0; p < transforms.length; p++) {
|
|
55888
|
+
const parts = p === 0 ? first : placeParts(base, transforms[p]);
|
|
55889
|
+
const boxes = parts.map((c) => meshAabb(c.mesh));
|
|
55890
|
+
for (let i = 0; i < parts.length; i++) {
|
|
55891
|
+
for (let j = i + 1; j < parts.length; j++) {
|
|
55892
|
+
const key = pairKey(parts[i].id, parts[j].id);
|
|
55893
|
+
if (ignore.has(key)) {
|
|
55894
|
+
if (p === 0) ignored += 1;
|
|
55895
|
+
continue;
|
|
55896
|
+
}
|
|
55897
|
+
if (!aabbNear(boxes[i], boxes[j], margin + CONTACT_EPS_MM)) continue;
|
|
55898
|
+
broadphase.add(key);
|
|
55899
|
+
let r;
|
|
55900
|
+
try {
|
|
55901
|
+
r = engine.meshClearance(parts[i].mesh, parts[j].mesh);
|
|
55902
|
+
} catch (e) {
|
|
55903
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
55904
|
+
}
|
|
55905
|
+
queries += 1;
|
|
55906
|
+
const prev = worstByPair.get(key);
|
|
55907
|
+
if (!prev || worseThan(r, prev.r)) {
|
|
55908
|
+
worstByPair.set(key, {
|
|
55909
|
+
a: parts[i],
|
|
55910
|
+
b: parts[j],
|
|
55911
|
+
r,
|
|
55912
|
+
...poses.length > 0 ? { pose: poses[p] } : {}
|
|
55913
|
+
});
|
|
55914
|
+
}
|
|
55915
|
+
}
|
|
55916
|
+
}
|
|
55917
|
+
}
|
|
55918
|
+
const named = (p) => ({ id: p.id, ...p.name ? { name: p.name } : {} });
|
|
55919
|
+
const findings = [...worstByPair.values()].filter((w) => !clearanceHolds(round6(w.r.distance), opts.minMm, false, w.r.intersecting)).map((w) => ({
|
|
55920
|
+
a: named(w.a),
|
|
55921
|
+
b: named(w.b),
|
|
55922
|
+
distance_mm: round6(w.r.distance),
|
|
55923
|
+
verdict: w.r.intersecting ? "intersecting" : clearanceVerdict(round6(w.r.distance)),
|
|
55924
|
+
point_a: w.r.pointA.map(round6),
|
|
55925
|
+
point_b: w.r.pointB.map(round6),
|
|
55926
|
+
...w.pose ? { worst_pose: w.pose.map((s) => ({ joint: s.joint, state: round6(s.state) })) } : {}
|
|
55927
|
+
})).sort(
|
|
55928
|
+
(x, y) => Number(y.verdict === "intersecting") - Number(x.verdict === "intersecting") || x.distance_mm - y.distance_mm
|
|
55929
|
+
);
|
|
55930
|
+
const n = first.length;
|
|
55931
|
+
return {
|
|
55932
|
+
result: {
|
|
55933
|
+
parts_checked: n,
|
|
55934
|
+
pairs_total: n * (n - 1) / 2,
|
|
55935
|
+
pairs_ignored: ignored,
|
|
55936
|
+
pairs_broadphase: broadphase.size,
|
|
55937
|
+
queries,
|
|
55938
|
+
...poses.length > 0 ? { poses_checked: poses.length } : {},
|
|
55939
|
+
findings,
|
|
55940
|
+
unresolved_ignores: [...new Set(unresolved)]
|
|
55941
|
+
}
|
|
55942
|
+
};
|
|
55943
|
+
}
|
|
55680
55944
|
function upsertClearanceSpec(doc, spec) {
|
|
55681
55945
|
const specs = doc.clearance_specs ?? [];
|
|
55682
55946
|
const idx = specs.findIndex((s) => s.label === spec.label);
|
|
@@ -55689,16 +55953,13 @@ async function checkClearance(args, engine) {
|
|
|
55689
55953
|
if (!documentId) return err3("Pass `document_id` (the CAD session).");
|
|
55690
55954
|
const groupA = stringArray(args.group_a);
|
|
55691
55955
|
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
55956
|
const label = typeof args.label === "string" && args.label.trim() ? args.label.trim() : void 0;
|
|
55698
55957
|
const allowContact = asBool(args.allow_contact);
|
|
55958
|
+
const { sweep, error: sweepParseError } = parseSweep(args.sweep);
|
|
55959
|
+
if (sweepParseError) return err3(sweepParseError);
|
|
55699
55960
|
if (label && args.joint_state !== void 0 && args.joint_state !== null) {
|
|
55700
55961
|
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."
|
|
55962
|
+
"`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
55963
|
);
|
|
55703
55964
|
}
|
|
55704
55965
|
const stored = getSession(documentId);
|
|
@@ -55709,10 +55970,20 @@ async function checkClearance(args, engine) {
|
|
|
55709
55970
|
} catch (e) {
|
|
55710
55971
|
return err3(e instanceof Error ? e.message : String(e));
|
|
55711
55972
|
}
|
|
55712
|
-
|
|
55973
|
+
if (!groupA && !groupB) {
|
|
55974
|
+
return auditClearance(documentId, doc, engine, args, sweep, label, pose);
|
|
55975
|
+
}
|
|
55976
|
+
if (!groupA || !groupB) {
|
|
55977
|
+
return err3(
|
|
55978
|
+
"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."
|
|
55979
|
+
);
|
|
55980
|
+
}
|
|
55981
|
+
const minMm = typeof args.min_mm === "number" ? args.min_mm : NaN;
|
|
55982
|
+
if (!Number.isFinite(minMm)) return err3("Pass `min_mm`, the required minimum separation in mm.");
|
|
55983
|
+
const { result, error: error2 } = computeGroupClearance(doc, engine, groupA, groupB, sweep);
|
|
55713
55984
|
if (error2 || !result) return err3(error2 ?? "Clearance could not be computed.");
|
|
55714
55985
|
const verdict = clearanceVerdict(result.distance_mm);
|
|
55715
|
-
const pass2 = clearanceHolds(result.distance_mm, minMm, allowContact);
|
|
55986
|
+
const pass2 = clearanceHolds(result.distance_mm, minMm, allowContact, result.intersecting);
|
|
55716
55987
|
let specSaved = false;
|
|
55717
55988
|
if (label) {
|
|
55718
55989
|
upsertClearanceSpec(stored, {
|
|
@@ -55720,7 +55991,8 @@ async function checkClearance(args, engine) {
|
|
|
55720
55991
|
group_a: result.group_a.map((p) => p.id),
|
|
55721
55992
|
group_b: result.group_b.map((p) => p.id),
|
|
55722
55993
|
min_mm: minMm,
|
|
55723
|
-
...allowContact ? { allow_contact: true } : {}
|
|
55994
|
+
...allowContact ? { allow_contact: true } : {},
|
|
55995
|
+
...sweep && sweep.length > 0 ? { sweep } : {}
|
|
55724
55996
|
});
|
|
55725
55997
|
specSaved = true;
|
|
55726
55998
|
}
|
|
@@ -55736,6 +56008,8 @@ async function checkClearance(args, engine) {
|
|
|
55736
56008
|
...pose ? { pose } : {},
|
|
55737
56009
|
intersecting: result.intersecting,
|
|
55738
56010
|
worst_pair: result.worst_pair,
|
|
56011
|
+
...result.worst_pose ? { worst_pose: result.worst_pose } : {},
|
|
56012
|
+
...result.poses_checked !== void 0 ? { poses_checked: result.poses_checked } : {},
|
|
55739
56013
|
pairs_checked: result.pairs_checked,
|
|
55740
56014
|
group_a: result.group_a,
|
|
55741
56015
|
group_b: result.group_b,
|
|
@@ -55752,11 +56026,92 @@ async function checkClearance(args, engine) {
|
|
|
55752
56026
|
}
|
|
55753
56027
|
};
|
|
55754
56028
|
}
|
|
56029
|
+
function parseSweep(raw) {
|
|
56030
|
+
if (raw === void 0 || raw === null) return {};
|
|
56031
|
+
if (!Array.isArray(raw)) {
|
|
56032
|
+
return { error: "`sweep` must be an array of {joint, from, to, steps} axes." };
|
|
56033
|
+
}
|
|
56034
|
+
const sweep = [];
|
|
56035
|
+
for (const entry of raw) {
|
|
56036
|
+
if (!entry || typeof entry !== "object") {
|
|
56037
|
+
return { error: "Each `sweep` axis must be an object {joint, from, to, steps}." };
|
|
56038
|
+
}
|
|
56039
|
+
const e = entry;
|
|
56040
|
+
const joint = typeof e.joint === "string" ? e.joint.trim() : "";
|
|
56041
|
+
const from = Number(e.from);
|
|
56042
|
+
const to = Number(e.to);
|
|
56043
|
+
const steps = Number(e.steps);
|
|
56044
|
+
if (!joint || !Number.isFinite(from) || !Number.isFinite(to) || !Number.isFinite(steps)) {
|
|
56045
|
+
return {
|
|
56046
|
+
error: "Each `sweep` axis needs a `joint` id/name plus numeric `from`, `to`, and `steps`."
|
|
56047
|
+
};
|
|
56048
|
+
}
|
|
56049
|
+
sweep.push({ joint, from, to, steps });
|
|
56050
|
+
}
|
|
56051
|
+
return sweep.length > 0 ? { sweep } : {};
|
|
56052
|
+
}
|
|
56053
|
+
function parseIgnorePairs(raw) {
|
|
56054
|
+
if (raw === void 0 || raw === null) return { pairs: [] };
|
|
56055
|
+
if (!Array.isArray(raw)) {
|
|
56056
|
+
return { pairs: [], error: "`ignore_pairs` must be an array of [part_a, part_b] pairs." };
|
|
56057
|
+
}
|
|
56058
|
+
const pairs = [];
|
|
56059
|
+
for (const entry of raw) {
|
|
56060
|
+
if (!Array.isArray(entry) || entry.length !== 2) {
|
|
56061
|
+
return { pairs: [], error: "Each `ignore_pairs` entry must be a two-element array." };
|
|
56062
|
+
}
|
|
56063
|
+
pairs.push([String(entry[0]), String(entry[1])]);
|
|
56064
|
+
}
|
|
56065
|
+
return { pairs };
|
|
56066
|
+
}
|
|
56067
|
+
function auditClearance(documentId, doc, engine, args, sweep, label, pose) {
|
|
56068
|
+
const minMm = typeof args.min_mm === "number" && Number.isFinite(args.min_mm) ? args.min_mm : 0;
|
|
56069
|
+
const { pairs: ignorePairs, error: ignoreError } = parseIgnorePairs(args.ignore_pairs);
|
|
56070
|
+
if (ignoreError) return err3(ignoreError);
|
|
56071
|
+
const ignoreFixedJoints = args.ignore_fixed_joints === void 0 ? true : asBool(args.ignore_fixed_joints);
|
|
56072
|
+
const ignoreAdjacent = asBool(args.ignore_adjacent);
|
|
56073
|
+
const { result, error: error2 } = computeInterferenceAudit(doc, engine, {
|
|
56074
|
+
minMm,
|
|
56075
|
+
ignorePairs,
|
|
56076
|
+
ignoreFixedJoints,
|
|
56077
|
+
ignoreAdjacent,
|
|
56078
|
+
sweep
|
|
56079
|
+
});
|
|
56080
|
+
if (error2 || !result) return err3(error2 ?? "Interference audit could not be computed.");
|
|
56081
|
+
const payload = {
|
|
56082
|
+
success: true,
|
|
56083
|
+
document_id: documentId,
|
|
56084
|
+
mode: "audit",
|
|
56085
|
+
required_mm: minMm,
|
|
56086
|
+
...pose ? { pose } : {},
|
|
56087
|
+
pass: result.findings.length === 0,
|
|
56088
|
+
findings: result.findings,
|
|
56089
|
+
interference_count: result.findings.length,
|
|
56090
|
+
parts_checked: result.parts_checked,
|
|
56091
|
+
pairs_total: result.pairs_total,
|
|
56092
|
+
pairs_ignored: result.pairs_ignored,
|
|
56093
|
+
pairs_broadphase: result.pairs_broadphase,
|
|
56094
|
+
queries: result.queries,
|
|
56095
|
+
...result.poses_checked !== void 0 ? { poses_checked: result.poses_checked } : {},
|
|
56096
|
+
...result.unresolved_ignores.length > 0 ? {
|
|
56097
|
+
unresolved_ignores: result.unresolved_ignores,
|
|
56098
|
+
note: `ignore_pairs named parts that do not exist: ${result.unresolved_ignores.join(", ")} \u2014 those pairs were NOT whitelisted.`
|
|
56099
|
+
} : {},
|
|
56100
|
+
...label ? {
|
|
56101
|
+
spec_saved: false,
|
|
56102
|
+
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."
|
|
56103
|
+
} : {}
|
|
56104
|
+
};
|
|
56105
|
+
return {
|
|
56106
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
56107
|
+
structuredContent: { clearance: payload, document_id: documentId }
|
|
56108
|
+
};
|
|
56109
|
+
}
|
|
55755
56110
|
function clearanceReceiptClaims(doc, engine) {
|
|
55756
56111
|
const specs = doc.clearance_specs ?? [];
|
|
55757
56112
|
return specs.map((spec) => {
|
|
55758
56113
|
const id = `${CLEARANCE_CLAIM_PREFIX}${spec.label}`;
|
|
55759
|
-
const description = `clearance "${spec.label}" at least ${spec.min_mm} mm`;
|
|
56114
|
+
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
56115
|
const subject = `${spec.group_a.join("+")} vs ${spec.group_b.join("+")}`;
|
|
55761
56116
|
if (!engine) {
|
|
55762
56117
|
return {
|
|
@@ -55770,7 +56125,13 @@ function clearanceReceiptClaims(doc, engine) {
|
|
|
55770
56125
|
subject
|
|
55771
56126
|
};
|
|
55772
56127
|
}
|
|
55773
|
-
const { result, error: error2 } = computeGroupClearance(
|
|
56128
|
+
const { result, error: error2 } = computeGroupClearance(
|
|
56129
|
+
doc,
|
|
56130
|
+
engine,
|
|
56131
|
+
spec.group_a,
|
|
56132
|
+
spec.group_b,
|
|
56133
|
+
spec.sweep
|
|
56134
|
+
);
|
|
55774
56135
|
if (error2 || !result) {
|
|
55775
56136
|
return {
|
|
55776
56137
|
...unverifiableClaim(
|
|
@@ -55790,8 +56151,15 @@ function clearanceReceiptClaims(doc, engine) {
|
|
|
55790
56151
|
group_b: spec.group_b,
|
|
55791
56152
|
required_mm: spec.min_mm,
|
|
55792
56153
|
measured_mm: result.distance_mm,
|
|
55793
|
-
holds: clearanceHolds(result.distance_mm, spec.min_mm, allowContact),
|
|
55794
|
-
...allowContact ? { allow_contact: true } : {}
|
|
56154
|
+
holds: clearanceHolds(result.distance_mm, spec.min_mm, allowContact, result.intersecting),
|
|
56155
|
+
...allowContact ? { allow_contact: true } : {},
|
|
56156
|
+
// A swept claim carries its grid so verify_receipt re-checks the same
|
|
56157
|
+
// range of motion — re-verifying at one pose would silently weaken it.
|
|
56158
|
+
...spec.sweep?.length ? {
|
|
56159
|
+
sweep: spec.sweep,
|
|
56160
|
+
worst_pose: result.worst_pose ?? [],
|
|
56161
|
+
poses_checked: result.poses_checked ?? 0
|
|
56162
|
+
} : {}
|
|
55795
56163
|
};
|
|
55796
56164
|
return {
|
|
55797
56165
|
id,
|
|
@@ -55820,7 +56188,13 @@ function verifyClearanceClaims(doc, engine, receipt) {
|
|
|
55820
56188
|
});
|
|
55821
56189
|
continue;
|
|
55822
56190
|
}
|
|
55823
|
-
const { result, error: error2 } = computeGroupClearance(
|
|
56191
|
+
const { result, error: error2 } = computeGroupClearance(
|
|
56192
|
+
doc,
|
|
56193
|
+
engine,
|
|
56194
|
+
stored.group_a,
|
|
56195
|
+
stored.group_b,
|
|
56196
|
+
stored.sweep
|
|
56197
|
+
);
|
|
55824
56198
|
if (error2 || !result) {
|
|
55825
56199
|
checks.push({
|
|
55826
56200
|
label,
|
|
@@ -55832,32 +56206,28 @@ function verifyClearanceClaims(doc, engine, receipt) {
|
|
|
55832
56206
|
continue;
|
|
55833
56207
|
}
|
|
55834
56208
|
const measured = result.distance_mm;
|
|
55835
|
-
|
|
56209
|
+
const sweptFields = result.poses_checked ? { worst_pose: result.worst_pose, poses_checked: result.poses_checked } : {};
|
|
56210
|
+
const common = {
|
|
56211
|
+
label,
|
|
56212
|
+
required_mm: stored.required_mm,
|
|
56213
|
+
stored_mm: stored.measured_mm,
|
|
56214
|
+
measured_mm: measured,
|
|
56215
|
+
...sweptFields
|
|
56216
|
+
};
|
|
56217
|
+
if (!clearanceHolds(measured, stored.required_mm, stored.allow_contact === true, result.intersecting)) {
|
|
55836
56218
|
checks.push({
|
|
55837
|
-
|
|
56219
|
+
...common,
|
|
55838
56220
|
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`
|
|
56221
|
+
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
56222
|
});
|
|
55844
56223
|
} else if (Math.abs(measured - stored.measured_mm) > STALE_EPS_MM) {
|
|
55845
56224
|
checks.push({
|
|
55846
|
-
|
|
56225
|
+
...common,
|
|
55847
56226
|
status: "Stale",
|
|
55848
|
-
required_mm: stored.required_mm,
|
|
55849
|
-
stored_mm: stored.measured_mm,
|
|
55850
|
-
measured_mm: measured,
|
|
55851
56227
|
reason: "geometry changed since the receipt was built, but the clearance still holds"
|
|
55852
56228
|
});
|
|
55853
56229
|
} 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
|
-
});
|
|
56230
|
+
checks.push({ ...common, status: "Holds" });
|
|
55861
56231
|
}
|
|
55862
56232
|
}
|
|
55863
56233
|
const status = checks.some((c) => c.status === "Violated") ? "Violated" : checks.some((c) => c.status === "Stale") ? "Stale" : "Holds";
|
|
@@ -55887,7 +56257,7 @@ function err3(text) {
|
|
|
55887
56257
|
isError: true
|
|
55888
56258
|
};
|
|
55889
56259
|
}
|
|
55890
|
-
var CLEARANCE_CLAIM_PREFIX, MECH_DOMAIN2, CLEARANCE_ORACLE, STALE_EPS_MM, CONTACT_EPS_MM, round6, checkClearanceSchema, toolDefs11;
|
|
56260
|
+
var CLEARANCE_CLAIM_PREFIX, MECH_DOMAIN2, CLEARANCE_ORACLE, STALE_EPS_MM, CONTACT_EPS_MM, round6, checkClearanceSchema, MAX_SWEEP_POSES, pairKey, toolDefs11;
|
|
55891
56261
|
var init_clearance = __esm({
|
|
55892
56262
|
"src/tools/clearance.ts"() {
|
|
55893
56263
|
"use strict";
|
|
@@ -55935,15 +56305,47 @@ var init_clearance = __esm({
|
|
|
55935
56305
|
type: "boolean",
|
|
55936
56306
|
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
56307
|
},
|
|
55938
|
-
joint_state: jointStateSchemaProp
|
|
56308
|
+
joint_state: jointStateSchemaProp,
|
|
56309
|
+
sweep: {
|
|
56310
|
+
type: "array",
|
|
56311
|
+
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.",
|
|
56312
|
+
items: {
|
|
56313
|
+
type: "object",
|
|
56314
|
+
properties: {
|
|
56315
|
+
joint: { type: "string", description: "Joint id or name to drive." },
|
|
56316
|
+
from: { type: "number", description: "Start of travel (degrees for revolute, mm for prismatic)." },
|
|
56317
|
+
to: { type: "number", description: "End of travel." },
|
|
56318
|
+
steps: { type: "number", description: "Number of intervals; steps + 1 poses are sampled." }
|
|
56319
|
+
},
|
|
56320
|
+
required: ["joint", "from", "to", "steps"]
|
|
56321
|
+
}
|
|
56322
|
+
},
|
|
56323
|
+
ignore_pairs: {
|
|
56324
|
+
type: "array",
|
|
56325
|
+
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.",
|
|
56326
|
+
items: {
|
|
56327
|
+
type: "array",
|
|
56328
|
+
items: { type: "string" }
|
|
56329
|
+
}
|
|
56330
|
+
},
|
|
56331
|
+
ignore_fixed_joints: {
|
|
56332
|
+
type: "boolean",
|
|
56333
|
+
description: "Audit mode: skip pairs joined by a Fixed joint \u2014 they are bolted together and always 'interfere'. Default true."
|
|
56334
|
+
},
|
|
56335
|
+
ignore_adjacent: {
|
|
56336
|
+
type: "boolean",
|
|
56337
|
+
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."
|
|
56338
|
+
}
|
|
55939
56339
|
},
|
|
55940
|
-
required: ["document_id"
|
|
56340
|
+
required: ["document_id"]
|
|
55941
56341
|
};
|
|
56342
|
+
MAX_SWEEP_POSES = 4096;
|
|
56343
|
+
pairKey = (a, b) => a < b ? `${a}\0${b}` : `${b}\0${a}`;
|
|
55942
56344
|
toolDefs11 = [
|
|
55943
56345
|
{
|
|
55944
56346
|
name: "check_clearance",
|
|
55945
56347
|
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
|
|
56348
|
+
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
56349
|
inputSchema: checkClearanceSchema,
|
|
55948
56350
|
handler: (a, c) => checkClearance(a, c.engine),
|
|
55949
56351
|
behavior: behavior({ writesDoc: true })
|
|
@@ -79647,8 +80049,8 @@ var init_server3 = __esm({
|
|
|
79647
80049
|
init_order_feed();
|
|
79648
80050
|
init_animate();
|
|
79649
80051
|
PKG_VERSION = (() => {
|
|
79650
|
-
if ("0.9.4-main.
|
|
79651
|
-
return "0.9.4-main.
|
|
80052
|
+
if ("0.9.4-main.36") {
|
|
80053
|
+
return "0.9.4-main.36";
|
|
79652
80054
|
}
|
|
79653
80055
|
try {
|
|
79654
80056
|
const req = createRequire2(import.meta.url);
|
|
@@ -79657,8 +80059,8 @@ var init_server3 = __esm({
|
|
|
79657
80059
|
return "0.0.0";
|
|
79658
80060
|
}
|
|
79659
80061
|
})();
|
|
79660
|
-
BUILD_SHA = "
|
|
79661
|
-
BUILD_TIME = "2026-07-26T13:
|
|
80062
|
+
BUILD_SHA = "3b9087f300b569c70aa24cebc52af06d15e59187";
|
|
80063
|
+
BUILD_TIME = "2026-07-26T13:20:01.737Z";
|
|
79662
80064
|
SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
|
|
79663
80065
|
VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
|
|
79664
80066
|
INSTANCE_ID = randomUUID6().slice(0, 8);
|
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.36",
|
|
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:
|
|
22
|
+
"sha": "3b9087f300b569c70aa24cebc52af06d15e59187",
|
|
23
|
+
"builtAt": "2026-07-26T13:20:01.737Z"
|
|
24
24
|
}
|
|
25
25
|
}
|
package/vcad_kernel_wasm_bg.wasm
CHANGED
|
Binary file
|