@vcad/mcp 0.9.4-main.74 → 0.9.4-main.76
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 +419 -182
- 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 58bebb998fe85b92d30bbc46939ed4dceaf4adc4 at 2026-08-26T13:14:00.987Z. Source: https://github.com/ecto/vcad
|
package/index.mjs
CHANGED
|
@@ -4705,7 +4705,7 @@ function __wbg_get_imports() {
|
|
|
4705
4705
|
const a = state0.a;
|
|
4706
4706
|
state0.a = 0;
|
|
4707
4707
|
try {
|
|
4708
|
-
return
|
|
4708
|
+
return wasm_bindgen_22639e9cee8fd77e___convert__closures_____invoke___wasm_bindgen_22639e9cee8fd77e___JsValue__wasm_bindgen_22639e9cee8fd77e___JsValue_____(a, state0.b, arg02, arg12);
|
|
4709
4709
|
} finally {
|
|
4710
4710
|
state0.a = a;
|
|
4711
4711
|
}
|
|
@@ -5340,11 +5340,11 @@ function __wbg_get_imports() {
|
|
|
5340
5340
|
arg0.writeTexture(arg1, arg2, arg3, arg4);
|
|
5341
5341
|
},
|
|
5342
5342
|
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
|
5343
|
-
const ret = makeMutClosure(arg0, arg1, wasm.
|
|
5343
|
+
const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_22639e9cee8fd77e___closure__destroy___dyn_core_f0fd674eaa06beef___ops__function__FnMut__wgpu_56e928f2b7a75f7a___backend__webgpu__webgpu_sys__gen_GpuUncapturedErrorEvent__GpuUncapturedErrorEvent____Output_______, wasm_bindgen_22639e9cee8fd77e___convert__closures_____invoke___wgpu_56e928f2b7a75f7a___backend__webgpu__webgpu_sys__gen_GpuUncapturedErrorEvent__GpuUncapturedErrorEvent_____);
|
|
5344
5344
|
return ret;
|
|
5345
5345
|
},
|
|
5346
5346
|
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
|
5347
|
-
const ret = makeMutClosure(arg0, arg1, wasm.
|
|
5347
|
+
const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen_22639e9cee8fd77e___closure__destroy___dyn_core_f0fd674eaa06beef___ops__function__FnMut__wasm_bindgen_22639e9cee8fd77e___JsValue____Output_______, wasm_bindgen_22639e9cee8fd77e___convert__closures_____invoke___wasm_bindgen_22639e9cee8fd77e___JsValue_____);
|
|
5348
5348
|
return ret;
|
|
5349
5349
|
},
|
|
5350
5350
|
__wbindgen_cast_0000000000000003: function(arg0) {
|
|
@@ -5418,14 +5418,14 @@ function __wbg_get_imports() {
|
|
|
5418
5418
|
"./vcad_kernel_wasm_bg.js": import0
|
|
5419
5419
|
};
|
|
5420
5420
|
}
|
|
5421
|
-
function
|
|
5422
|
-
wasm.
|
|
5421
|
+
function wasm_bindgen_22639e9cee8fd77e___convert__closures_____invoke___wgpu_56e928f2b7a75f7a___backend__webgpu__webgpu_sys__gen_GpuUncapturedErrorEvent__GpuUncapturedErrorEvent_____(arg0, arg1, arg2) {
|
|
5422
|
+
wasm.wasm_bindgen_22639e9cee8fd77e___convert__closures_____invoke___wgpu_56e928f2b7a75f7a___backend__webgpu__webgpu_sys__gen_GpuUncapturedErrorEvent__GpuUncapturedErrorEvent_____(arg0, arg1, arg2);
|
|
5423
5423
|
}
|
|
5424
|
-
function
|
|
5425
|
-
wasm.
|
|
5424
|
+
function wasm_bindgen_22639e9cee8fd77e___convert__closures_____invoke___wasm_bindgen_22639e9cee8fd77e___JsValue_____(arg0, arg1, arg2) {
|
|
5425
|
+
wasm.wasm_bindgen_22639e9cee8fd77e___convert__closures_____invoke___wasm_bindgen_22639e9cee8fd77e___JsValue_____(arg0, arg1, arg2);
|
|
5426
5426
|
}
|
|
5427
|
-
function
|
|
5428
|
-
wasm.
|
|
5427
|
+
function wasm_bindgen_22639e9cee8fd77e___convert__closures_____invoke___wasm_bindgen_22639e9cee8fd77e___JsValue__wasm_bindgen_22639e9cee8fd77e___JsValue_____(arg0, arg1, arg2, arg3) {
|
|
5428
|
+
wasm.wasm_bindgen_22639e9cee8fd77e___convert__closures_____invoke___wasm_bindgen_22639e9cee8fd77e___JsValue__wasm_bindgen_22639e9cee8fd77e___JsValue_____(arg0, arg1, arg2, arg3);
|
|
5429
5429
|
}
|
|
5430
5430
|
function addToExternrefTable0(obj) {
|
|
5431
5431
|
const idx = wasm.__externref_table_alloc();
|
|
@@ -41631,6 +41631,73 @@ var init_CHANGELOG = __esm({
|
|
|
41631
41631
|
CHANGELOG_default = {
|
|
41632
41632
|
$schema: "./changelog.schema.json",
|
|
41633
41633
|
entries: [
|
|
41634
|
+
{
|
|
41635
|
+
id: "2026-08-25-measure-joint-sweep",
|
|
41636
|
+
version: "0.9.4",
|
|
41637
|
+
date: "2026-08-25",
|
|
41638
|
+
category: "feat",
|
|
41639
|
+
title: "measure sweeps a mechanism's range of motion",
|
|
41640
|
+
summary: "measure now takes joint_state and sweep: the worst distance across a joint's travel plus the full per-pose margin curve; check_clearance gains include_samples and out-of-limits warnings.",
|
|
41641
|
+
features: [
|
|
41642
|
+
"measure",
|
|
41643
|
+
"clearance",
|
|
41644
|
+
"assembly",
|
|
41645
|
+
"joints",
|
|
41646
|
+
"kinematics"
|
|
41647
|
+
],
|
|
41648
|
+
mcpTools: [
|
|
41649
|
+
"measure",
|
|
41650
|
+
"check_clearance"
|
|
41651
|
+
]
|
|
41652
|
+
},
|
|
41653
|
+
{
|
|
41654
|
+
id: "2026-08-25-export-web-glb",
|
|
41655
|
+
version: "0.9.4",
|
|
41656
|
+
date: "2026-08-25",
|
|
41657
|
+
category: "feat",
|
|
41658
|
+
title: "vcad-render: web export (named-node GLB + JSON)",
|
|
41659
|
+
summary: "--export-web writes a Y-up GLB with one named, materialed glTF node per part; --export-web-js writes a primitive scene graph for web consumers.",
|
|
41660
|
+
features: [
|
|
41661
|
+
"render",
|
|
41662
|
+
"export",
|
|
41663
|
+
"glb",
|
|
41664
|
+
"web"
|
|
41665
|
+
],
|
|
41666
|
+
mcpTools: []
|
|
41667
|
+
},
|
|
41668
|
+
{
|
|
41669
|
+
id: "2026-08-25-animate-drafting-style",
|
|
41670
|
+
version: "0.9.4",
|
|
41671
|
+
date: "2026-08-25",
|
|
41672
|
+
category: "feat",
|
|
41673
|
+
title: "Animate in any style, not just photoreal",
|
|
41674
|
+
summary: "vcad-render --animate now works without --photoreal: drafting line art by default, --raytrace as a middle tier, with one camera pinned across the whole sequence.",
|
|
41675
|
+
details: "--animate used to require --photoreal, so the cheapest and most legible style \u2014 the default drafting line art \u2014 could not be animated at all, and a sequence meant tens of seconds a frame. It is now a style-agnostic path: the front half (resolve the timeline, evaluate the document exactly once, sample the sequence, bake one pose vector per frame) is shared, and three back ends turn a pose into pixels \u2014 drafting (default), --raytrace, and --photoreal. The drafting tier tessellates each part once and only re-poses the cached meshes per frame, which puts a 37-part machine at ~0.35 s a frame instead of minutes. Every tier now frames its camera once, on the union of all posed bounds, and hands that same framing to every frame, so all PNGs share one canvas size and scale and the assembly no longer swims about; --auto-aspect resolves once against that union. --trim is refused with --animate (a per-frame crop is per-frame sizing, the drift the shared camera prevents) and names the flag to drop.",
|
|
41676
|
+
features: [
|
|
41677
|
+
"rendering",
|
|
41678
|
+
"animation",
|
|
41679
|
+
"assemblies",
|
|
41680
|
+
"joints",
|
|
41681
|
+
"cli"
|
|
41682
|
+
]
|
|
41683
|
+
},
|
|
41684
|
+
{
|
|
41685
|
+
id: "2026-08-19-photoreal-animation",
|
|
41686
|
+
version: "0.9.4",
|
|
41687
|
+
date: "2026-08-19",
|
|
41688
|
+
category: "perf",
|
|
41689
|
+
title: "Photoreal animation evaluates geometry once",
|
|
41690
|
+
summary: "vcad-render --photoreal --animate poses a jointed assembly over a timeline, evaluating the document once for the whole sequence instead of once per frame.",
|
|
41691
|
+
details: "A joint-animated assembly is the same parts in different places, so the expensive half of a photoreal render \u2014 evaluating the parametric DAG into BReps and building a BVH over each part \u2014 belongs outside the frame loop. --animate puts it there: evaluate once, build one BLAS per part, then per frame solve forward kinematics (the same vcad-eval solver the app's assembly mode and the MCP timeline use), hand each object its new object\u2192world transform, and rebuild only the top-level acceleration structure before tracing. Poses come from the document's own timeline, or from a keyframe file with a joint-track shorthand; joints resolve by id or by authored name and a name that matches nothing fails with the list that does. The camera is framed on the union of every pose, so the subject never swims or clips. Frames land as frame_NNNN.png in the -o directory and are muxed to H.264 when ffmpeg is on PATH. On a 35-part machine at 640px/16spp the once-only evaluation was 215s against ~85s of tracing per frame \u2014 previously repaid in full on every frame.",
|
|
41692
|
+
features: [
|
|
41693
|
+
"rendering",
|
|
41694
|
+
"photoreal",
|
|
41695
|
+
"animation",
|
|
41696
|
+
"assemblies",
|
|
41697
|
+
"joints",
|
|
41698
|
+
"cli"
|
|
41699
|
+
]
|
|
41700
|
+
},
|
|
41634
41701
|
{
|
|
41635
41702
|
id: "2026-08-19-loon-modules-and-root-cache",
|
|
41636
41703
|
version: "0.9.4",
|
|
@@ -41650,6 +41717,21 @@ var init_CHANGELOG = __esm({
|
|
|
41650
41717
|
"create_cad_loon"
|
|
41651
41718
|
]
|
|
41652
41719
|
},
|
|
41720
|
+
{
|
|
41721
|
+
id: "2026-08-19-builtin-material-library",
|
|
41722
|
+
version: "0.9.4",
|
|
41723
|
+
date: "2026-08-19",
|
|
41724
|
+
category: "feat",
|
|
41725
|
+
title: "Renders know the standard material names",
|
|
41726
|
+
summary: 'A part named "aluminum" or "copper" with no [material] block rendered as clay grey; named materials now resolve to a built-in PBR library.',
|
|
41727
|
+
details: `Hand-written .loon documents name a material rather than define one \u2014 [root frame-rails "aluminum"] and nothing else \u2014 so the document's materials table stayed empty and every part fell through to the path tracer's default clay. The renderer now resolves an undeclared name against a built-in library of ~40 well-known materials (metals with measured F0 reflectance, plastics, composites, organics), case- and separator-insensitive so abs-black and abs_black are one material. Document-declared materials always win. Both the photoreal and drafting paths resolve through the same table, so their hues agree.`,
|
|
41728
|
+
features: [
|
|
41729
|
+
"rendering",
|
|
41730
|
+
"photoreal",
|
|
41731
|
+
"materials",
|
|
41732
|
+
"loon"
|
|
41733
|
+
]
|
|
41734
|
+
},
|
|
41653
41735
|
{
|
|
41654
41736
|
id: "2026-08-17-altium-pcb-import",
|
|
41655
41737
|
version: "0.9.4",
|
|
@@ -53211,8 +53293,129 @@ var init_inspect = __esm({
|
|
|
53211
53293
|
}
|
|
53212
53294
|
});
|
|
53213
53295
|
|
|
53214
|
-
// src/tools/
|
|
53215
|
-
function
|
|
53296
|
+
// src/tools/sweep.ts
|
|
53297
|
+
function worseThan(a, b) {
|
|
53298
|
+
if (a.intersecting !== b.intersecting) return a.intersecting;
|
|
53299
|
+
return a.distance < b.distance;
|
|
53300
|
+
}
|
|
53301
|
+
function parseSweep(raw) {
|
|
53302
|
+
if (raw === void 0 || raw === null) return {};
|
|
53303
|
+
if (!Array.isArray(raw)) {
|
|
53304
|
+
return { error: "`sweep` must be an array of {joint, from, to, steps} axes." };
|
|
53305
|
+
}
|
|
53306
|
+
const sweep = [];
|
|
53307
|
+
for (const entry of raw) {
|
|
53308
|
+
if (!entry || typeof entry !== "object") {
|
|
53309
|
+
return { error: "Each `sweep` axis must be an object {joint, from, to, steps}." };
|
|
53310
|
+
}
|
|
53311
|
+
const e = entry;
|
|
53312
|
+
const joint = typeof e.joint === "string" ? e.joint.trim() : "";
|
|
53313
|
+
const from = Number(e.from);
|
|
53314
|
+
const to = Number(e.to);
|
|
53315
|
+
const steps = Number(e.steps);
|
|
53316
|
+
if (!joint || !Number.isFinite(from) || !Number.isFinite(to) || !Number.isFinite(steps)) {
|
|
53317
|
+
return {
|
|
53318
|
+
error: "Each `sweep` axis needs a `joint` id/name plus numeric `from`, `to`, and `steps`."
|
|
53319
|
+
};
|
|
53320
|
+
}
|
|
53321
|
+
sweep.push({ joint, from, to, steps });
|
|
53322
|
+
}
|
|
53323
|
+
return sweep.length > 0 ? { sweep } : {};
|
|
53324
|
+
}
|
|
53325
|
+
function jointLimits(kind) {
|
|
53326
|
+
const k = kind;
|
|
53327
|
+
if (!k) return void 0;
|
|
53328
|
+
if (k.type === "Revolute" || k.type === "Slider") return k.limits;
|
|
53329
|
+
return void 0;
|
|
53330
|
+
}
|
|
53331
|
+
function jointUnit(kind) {
|
|
53332
|
+
return kind?.type === "Slider" ? "mm" : "deg";
|
|
53333
|
+
}
|
|
53334
|
+
function resolveSweepAxes(doc, axes) {
|
|
53335
|
+
const joints = doc.joints ?? [];
|
|
53336
|
+
const resolved = [];
|
|
53337
|
+
const warnings = [];
|
|
53338
|
+
for (const axis of axes) {
|
|
53339
|
+
const wanted = String(axis.joint);
|
|
53340
|
+
const found = joints.find((j) => j.id === wanted) ?? joints.find((j) => j.name === wanted);
|
|
53341
|
+
if (!found) {
|
|
53342
|
+
const available = joints.map((j) => `${j.id}${j.name ? ` (${j.name})` : ""}`).join(", ");
|
|
53343
|
+
return { error: `No joint with id or name "${wanted}". Available: ${available || "none"}` };
|
|
53344
|
+
}
|
|
53345
|
+
if (!Number.isFinite(axis.from) || !Number.isFinite(axis.to)) {
|
|
53346
|
+
return { error: `Sweep axis "${wanted}" needs finite \`from\` and \`to\`.` };
|
|
53347
|
+
}
|
|
53348
|
+
const steps = Math.trunc(axis.steps);
|
|
53349
|
+
if (!Number.isFinite(steps) || steps < 1) {
|
|
53350
|
+
return { error: `Sweep axis "${wanted}" needs \`steps\` >= 1.` };
|
|
53351
|
+
}
|
|
53352
|
+
const limits = jointLimits(found.kind);
|
|
53353
|
+
if (limits) {
|
|
53354
|
+
const [lo, hi] = limits;
|
|
53355
|
+
const lowest = Math.min(axis.from, axis.to);
|
|
53356
|
+
const highest = Math.max(axis.from, axis.to);
|
|
53357
|
+
if (lowest < lo || highest > hi) {
|
|
53358
|
+
warnings.push(
|
|
53359
|
+
`sweep axis "${found.id}"${found.name ? ` ("${found.name}")` : ""} spans [${lowest}, ${highest}] ${jointUnit(found.kind)}, outside its declared limits [${lo}, ${hi}] \u2014 the swept range includes poses the joint cannot reach, so the reported worst case may not be physically attainable.`
|
|
53360
|
+
);
|
|
53361
|
+
}
|
|
53362
|
+
}
|
|
53363
|
+
resolved.push({ joint: found.id, from: axis.from, to: axis.to, steps });
|
|
53364
|
+
}
|
|
53365
|
+
if (resolved.length === 0) return { error: "`sweep` needs at least one joint axis." };
|
|
53366
|
+
const total = resolved.reduce((n, a) => n * (a.steps + 1), 1);
|
|
53367
|
+
if (total > MAX_SWEEP_POSES) {
|
|
53368
|
+
return {
|
|
53369
|
+
error: `Sweep grid is ${total} poses (limit ${MAX_SWEEP_POSES}). Lower \`steps\` or sweep fewer joints at once.`
|
|
53370
|
+
};
|
|
53371
|
+
}
|
|
53372
|
+
return { axes: resolved, ...warnings.length > 0 ? { warnings } : {} };
|
|
53373
|
+
}
|
|
53374
|
+
function poseGrid(axes) {
|
|
53375
|
+
let poses = [[]];
|
|
53376
|
+
for (const axis of axes) {
|
|
53377
|
+
const n = Math.trunc(axis.steps);
|
|
53378
|
+
const next = [];
|
|
53379
|
+
for (const pose of poses) {
|
|
53380
|
+
for (let i = 0; i <= n; i++) {
|
|
53381
|
+
const state = axis.from + (axis.to - axis.from) * i / n;
|
|
53382
|
+
next.push([...pose, { joint: axis.joint, state }]);
|
|
53383
|
+
}
|
|
53384
|
+
}
|
|
53385
|
+
poses = next;
|
|
53386
|
+
}
|
|
53387
|
+
return poses;
|
|
53388
|
+
}
|
|
53389
|
+
function roundPose(pose) {
|
|
53390
|
+
return pose.map((p) => ({ joint: p.joint, state: round6(p.state) }));
|
|
53391
|
+
}
|
|
53392
|
+
function poseTransforms(doc, poses) {
|
|
53393
|
+
const joints = doc.joints ?? [];
|
|
53394
|
+
const byId = new Map(joints.map((j) => [j.id, j]));
|
|
53395
|
+
const saved = joints.map((j) => j.state);
|
|
53396
|
+
try {
|
|
53397
|
+
return poses.map((pose) => {
|
|
53398
|
+
for (const { joint, state } of pose) {
|
|
53399
|
+
const j = byId.get(joint);
|
|
53400
|
+
if (j) j.state = state;
|
|
53401
|
+
}
|
|
53402
|
+
return solveForwardKinematics2(doc);
|
|
53403
|
+
});
|
|
53404
|
+
} finally {
|
|
53405
|
+
joints.forEach((j, i) => {
|
|
53406
|
+
j.state = saved[i];
|
|
53407
|
+
});
|
|
53408
|
+
}
|
|
53409
|
+
}
|
|
53410
|
+
function placeMesh(mesh, transform2) {
|
|
53411
|
+
if (!transform2) return mesh;
|
|
53412
|
+
return transformMesh(mesh, {
|
|
53413
|
+
translate: transform2.translation,
|
|
53414
|
+
rotate: transform2.rotation,
|
|
53415
|
+
scale: transform2.scale
|
|
53416
|
+
});
|
|
53417
|
+
}
|
|
53418
|
+
function poseableParts(doc, engine) {
|
|
53216
53419
|
const scene = engine.evaluate(doc);
|
|
53217
53420
|
const visibleRoots = doc.roots.filter((e) => e.visible !== false);
|
|
53218
53421
|
const partMaterials = doc.part_materials;
|
|
@@ -53220,31 +53423,78 @@ function evaluateParts(doc, engine) {
|
|
|
53220
53423
|
for (let i = 0; i < scene.parts.length && i < visibleRoots.length; i++) {
|
|
53221
53424
|
const root = visibleRoots[i];
|
|
53222
53425
|
const rootId = String(root.root);
|
|
53426
|
+
const node = doc.nodes[rootId];
|
|
53223
53427
|
const mesh = scene.parts[i].mesh;
|
|
53224
53428
|
if (!mesh || mesh.positions.length === 0) continue;
|
|
53225
|
-
const node = doc.nodes[rootId];
|
|
53226
53429
|
out.push({
|
|
53227
53430
|
id: rootId,
|
|
53228
53431
|
name: node?.name ?? void 0,
|
|
53229
53432
|
material: root.material ?? partMaterials?.[rootId] ?? void 0,
|
|
53230
|
-
mesh
|
|
53433
|
+
localMesh: mesh
|
|
53231
53434
|
});
|
|
53232
53435
|
}
|
|
53233
53436
|
for (const inst of scene.instances ?? []) {
|
|
53234
|
-
|
|
53235
|
-
translate: inst.transform.translation,
|
|
53236
|
-
rotate: inst.transform.rotation,
|
|
53237
|
-
scale: inst.transform.scale
|
|
53238
|
-
}) : inst.mesh;
|
|
53239
|
-
if (!mesh || mesh.positions.length === 0) continue;
|
|
53437
|
+
if (!inst.mesh || inst.mesh.positions.length === 0) continue;
|
|
53240
53438
|
out.push({
|
|
53241
53439
|
id: inst.instanceId,
|
|
53242
53440
|
name: inst.name ?? void 0,
|
|
53441
|
+
localMesh: inst.mesh,
|
|
53442
|
+
transform: inst.transform ?? void 0,
|
|
53443
|
+
instanceId: inst.instanceId
|
|
53444
|
+
});
|
|
53445
|
+
}
|
|
53446
|
+
return out;
|
|
53447
|
+
}
|
|
53448
|
+
function placeParts(parts, worldTransforms) {
|
|
53449
|
+
const out = [];
|
|
53450
|
+
for (const p of parts) {
|
|
53451
|
+
const transform2 = (p.instanceId ? worldTransforms?.get(p.instanceId) : void 0) ?? p.transform;
|
|
53452
|
+
const mesh = placeMesh(p.localMesh, transform2);
|
|
53453
|
+
if (!mesh || mesh.positions.length === 0) continue;
|
|
53454
|
+
out.push({
|
|
53455
|
+
id: p.id,
|
|
53456
|
+
...p.name ? { name: p.name } : {},
|
|
53457
|
+
...p.material ? { material: p.material } : {},
|
|
53243
53458
|
mesh
|
|
53244
53459
|
});
|
|
53245
53460
|
}
|
|
53246
53461
|
return out;
|
|
53247
53462
|
}
|
|
53463
|
+
var MAX_SWEEP_POSES, MAX_SWEEP_SAMPLES, round6, sweepSchemaProp;
|
|
53464
|
+
var init_sweep = __esm({
|
|
53465
|
+
"src/tools/sweep.ts"() {
|
|
53466
|
+
"use strict";
|
|
53467
|
+
init_dist();
|
|
53468
|
+
MAX_SWEEP_POSES = 4096;
|
|
53469
|
+
MAX_SWEEP_SAMPLES = 256;
|
|
53470
|
+
round6 = (v) => Math.round(v * 1e6) / 1e6;
|
|
53471
|
+
sweepSchemaProp = {
|
|
53472
|
+
type: "array",
|
|
53473
|
+
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). Endpoints outside a joint's declared limits are reported as warnings, not clamped. Joint states are restored afterwards \u2014 the sweep never edits the document.",
|
|
53474
|
+
items: {
|
|
53475
|
+
type: "object",
|
|
53476
|
+
properties: {
|
|
53477
|
+
joint: { type: "string", description: "Joint id or name to drive." },
|
|
53478
|
+
from: {
|
|
53479
|
+
type: "number",
|
|
53480
|
+
description: "Start of travel (degrees for revolute, mm for prismatic)."
|
|
53481
|
+
},
|
|
53482
|
+
to: { type: "number", description: "End of travel." },
|
|
53483
|
+
steps: {
|
|
53484
|
+
type: "number",
|
|
53485
|
+
description: "Number of intervals; steps + 1 poses are sampled."
|
|
53486
|
+
}
|
|
53487
|
+
},
|
|
53488
|
+
required: ["joint", "from", "to", "steps"]
|
|
53489
|
+
}
|
|
53490
|
+
};
|
|
53491
|
+
}
|
|
53492
|
+
});
|
|
53493
|
+
|
|
53494
|
+
// src/tools/measure.ts
|
|
53495
|
+
function evaluateParts(doc, engine) {
|
|
53496
|
+
return placeParts(poseableParts(doc, engine));
|
|
53497
|
+
}
|
|
53248
53498
|
function findPart(candidates, wanted) {
|
|
53249
53499
|
return candidates.find((c) => c.id === wanted) ?? candidates.find((c) => c.name === wanted);
|
|
53250
53500
|
}
|
|
@@ -53394,6 +53644,96 @@ function measureResult(doc, engine, partIds) {
|
|
|
53394
53644
|
note: "Distance is negative when the parts overlap (penetration depth). Measured from evaluated tessellations \u2014 accuracy is tessellation-bound."
|
|
53395
53645
|
};
|
|
53396
53646
|
}
|
|
53647
|
+
function measureSweepResult(doc, engine, partIds, sweep) {
|
|
53648
|
+
if (partIds.length !== 2) {
|
|
53649
|
+
throw new MeasureError(
|
|
53650
|
+
`\`sweep\` measures the distance between TWO parts across a range of motion \u2014 ${partIds.length} part id was given. Pass two \`part_ids\`, or drop \`sweep\` (a single part's bbox/volume/center of mass is a per-pose property; use \`joint_state\` to read it at one pose).`
|
|
53651
|
+
);
|
|
53652
|
+
}
|
|
53653
|
+
let base;
|
|
53654
|
+
try {
|
|
53655
|
+
base = poseableParts(doc, engine);
|
|
53656
|
+
} catch (e) {
|
|
53657
|
+
throw new MeasureError(e instanceof Error ? e.message : String(e));
|
|
53658
|
+
}
|
|
53659
|
+
const pick2 = (wanted) => {
|
|
53660
|
+
const found = base.find((p) => p.id === wanted) ?? base.find((p) => p.name === wanted);
|
|
53661
|
+
if (!found) {
|
|
53662
|
+
const available = base.map((p) => `${p.id}${p.name ? ` (${p.name})` : ""}`).join(", ") || "none";
|
|
53663
|
+
throw new MeasureError(
|
|
53664
|
+
`No part with id or name "${wanted}". Available: ${available}`
|
|
53665
|
+
);
|
|
53666
|
+
}
|
|
53667
|
+
return found;
|
|
53668
|
+
};
|
|
53669
|
+
const [pa, pb] = partIds.map((id) => pick2(String(id)));
|
|
53670
|
+
if (pa.id === pb.id) {
|
|
53671
|
+
throw new MeasureError(
|
|
53672
|
+
`measure needs two distinct parts \u2014 "${pa.id}" was given twice.`
|
|
53673
|
+
);
|
|
53674
|
+
}
|
|
53675
|
+
const { axes, warnings, error: error2 } = resolveSweepAxes(doc, sweep);
|
|
53676
|
+
if (error2 || !axes) throw new MeasureError(error2 ?? "sweep could not be resolved");
|
|
53677
|
+
const poses = poseGrid(axes);
|
|
53678
|
+
let transforms;
|
|
53679
|
+
try {
|
|
53680
|
+
transforms = poseTransforms(doc, poses);
|
|
53681
|
+
} catch (e) {
|
|
53682
|
+
throw new MeasureError(e instanceof Error ? e.message : String(e));
|
|
53683
|
+
}
|
|
53684
|
+
const keepSamples = poses.length <= MAX_SWEEP_SAMPLES;
|
|
53685
|
+
const samples = [];
|
|
53686
|
+
let worst2;
|
|
53687
|
+
for (let i = 0; i < poses.length; i++) {
|
|
53688
|
+
const placed = placeParts([pa, pb], transforms[i]);
|
|
53689
|
+
if (placed.length < 2) {
|
|
53690
|
+
throw new MeasureError(
|
|
53691
|
+
"One of the parts has an empty mesh at some pose \u2014 nothing to measure."
|
|
53692
|
+
);
|
|
53693
|
+
}
|
|
53694
|
+
let r;
|
|
53695
|
+
try {
|
|
53696
|
+
r = engine.meshClearance(placed[0].mesh, placed[1].mesh);
|
|
53697
|
+
} catch (e) {
|
|
53698
|
+
throw new MeasureError(e instanceof Error ? e.message : String(e));
|
|
53699
|
+
}
|
|
53700
|
+
if (keepSamples) {
|
|
53701
|
+
samples.push({
|
|
53702
|
+
pose: roundPose(poses[i]),
|
|
53703
|
+
distance_mm: r6(r.distance),
|
|
53704
|
+
intersecting: r.intersecting
|
|
53705
|
+
});
|
|
53706
|
+
}
|
|
53707
|
+
if (!worst2 || worseThan(r, worst2.r)) {
|
|
53708
|
+
worst2 = { r, pose: poses[i], a: placed[0], b: placed[1] };
|
|
53709
|
+
}
|
|
53710
|
+
}
|
|
53711
|
+
if (!worst2) throw new MeasureError("Sweep produced no poses to measure.");
|
|
53712
|
+
const distance = r6(worst2.r.distance);
|
|
53713
|
+
return {
|
|
53714
|
+
mode: "sweep",
|
|
53715
|
+
distance_mm: distance,
|
|
53716
|
+
contact: distance <= 0,
|
|
53717
|
+
intersecting: worst2.r.intersecting,
|
|
53718
|
+
worst_pose: roundPose(worst2.pose),
|
|
53719
|
+
poses_checked: poses.length,
|
|
53720
|
+
sweep: axes,
|
|
53721
|
+
closest_points: {
|
|
53722
|
+
a: worst2.r.pointA.map(r6),
|
|
53723
|
+
b: worst2.r.pointB.map(r6)
|
|
53724
|
+
},
|
|
53725
|
+
parts: {
|
|
53726
|
+
a: partSnapshot(worst2.a),
|
|
53727
|
+
b: partSnapshot(worst2.b)
|
|
53728
|
+
},
|
|
53729
|
+
...keepSamples ? { samples } : {
|
|
53730
|
+
samples_omitted: true,
|
|
53731
|
+
samples_note: `Sweep grid is ${poses.length} poses (over the ${MAX_SWEEP_SAMPLES}-pose sample cap) \u2014 the per-pose margin curve was omitted. Lower \`steps\` to get it.`
|
|
53732
|
+
},
|
|
53733
|
+
...warnings && warnings.length > 0 ? { sweep_warnings: warnings } : {},
|
|
53734
|
+
note: "`distance_mm` is the WORST (minimum) distance over the swept range, not the authored pose; negative means the parts overlap there (penetration depth). `parts` bboxes are those of the worst pose. Measured from evaluated tessellations \u2014 accuracy is tessellation-bound. Joint states are restored: a sweep never edits the document."
|
|
53735
|
+
};
|
|
53736
|
+
}
|
|
53397
53737
|
function measure(args, engine) {
|
|
53398
53738
|
const documentId = String(args.document_id ?? "");
|
|
53399
53739
|
if (!documentId) {
|
|
@@ -53405,20 +53745,33 @@ function measure(args, engine) {
|
|
|
53405
53745
|
"Pass `part_ids` as an array of one or two part ids (or part names)."
|
|
53406
53746
|
);
|
|
53407
53747
|
}
|
|
53748
|
+
const { sweep, error: sweepParseError } = parseSweep(args.sweep);
|
|
53749
|
+
if (sweepParseError) return err(sweepParseError);
|
|
53750
|
+
let stored;
|
|
53751
|
+
try {
|
|
53752
|
+
stored = getSession(documentId);
|
|
53753
|
+
} catch (e) {
|
|
53754
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
53755
|
+
}
|
|
53408
53756
|
let doc;
|
|
53757
|
+
let pose;
|
|
53409
53758
|
try {
|
|
53410
|
-
doc =
|
|
53759
|
+
({ doc, pose } = applyJointState(stored, args.joint_state));
|
|
53411
53760
|
} catch (e) {
|
|
53412
53761
|
return err(e instanceof Error ? e.message : String(e));
|
|
53413
53762
|
}
|
|
53414
53763
|
let payload;
|
|
53415
53764
|
try {
|
|
53416
|
-
payload = measureResult(doc, engine, partIds);
|
|
53765
|
+
payload = sweep && sweep.length > 0 ? measureSweepResult(doc, engine, partIds, sweep) : measureResult(doc, engine, partIds);
|
|
53417
53766
|
} catch (e) {
|
|
53418
53767
|
if (e instanceof MeasureError) return err(e.message);
|
|
53419
53768
|
throw e;
|
|
53420
53769
|
}
|
|
53421
|
-
const body = {
|
|
53770
|
+
const body = {
|
|
53771
|
+
document_id: documentId,
|
|
53772
|
+
...pose ? { pose } : {},
|
|
53773
|
+
...payload
|
|
53774
|
+
};
|
|
53422
53775
|
return {
|
|
53423
53776
|
content: [{ type: "text", text: JSON.stringify(body) }],
|
|
53424
53777
|
structuredContent: { measure: body, document_id: documentId }
|
|
@@ -53434,10 +53787,11 @@ var r6, point, MeasureError, measureSchema, toolDefs6;
|
|
|
53434
53787
|
var init_measure = __esm({
|
|
53435
53788
|
"src/tools/measure.ts"() {
|
|
53436
53789
|
"use strict";
|
|
53437
|
-
init_dist();
|
|
53438
53790
|
init_inspect();
|
|
53439
53791
|
init_session_core();
|
|
53440
53792
|
init_tool_def();
|
|
53793
|
+
init_pose();
|
|
53794
|
+
init_sweep();
|
|
53441
53795
|
r6 = (v) => Math.round(v * 1e6) / 1e6;
|
|
53442
53796
|
point = (p) => ({
|
|
53443
53797
|
x: r6(p.x),
|
|
@@ -53457,7 +53811,9 @@ var init_measure = __esm({
|
|
|
53457
53811
|
type: "array",
|
|
53458
53812
|
items: { type: "string" },
|
|
53459
53813
|
description: "One or two part ids (or part names). One id \u2192 that part's bbox, volume, and center of mass. Two ids \u2192 the minimum distance between them (0/negative = contact/overlap) plus each part's bbox."
|
|
53460
|
-
}
|
|
53814
|
+
},
|
|
53815
|
+
joint_state: jointStateSchemaProp,
|
|
53816
|
+
sweep: sweepSchemaProp
|
|
53461
53817
|
},
|
|
53462
53818
|
required: ["document_id", "part_ids"]
|
|
53463
53819
|
};
|
|
@@ -53465,7 +53821,7 @@ var init_measure = __esm({
|
|
|
53465
53821
|
{
|
|
53466
53822
|
name: "measure",
|
|
53467
53823
|
pack: null,
|
|
53468
|
-
description: "Measure geometry in an open session. Pass `part_ids` with two ids to get the minimum distance between those parts (0/negative = contact/overlap) plus each part's world-space bounding box; pass one id to get that part's bbox, volume, and center of mass. Complements `inspect_cad` (whole-document aggregate) and `check_clearance` (named, persisted air-gap assertions). Distances are measured from evaluated tessellations
|
|
53824
|
+
description: "Measure geometry in an open session. Pass `part_ids` with two ids to get the minimum distance between those parts (0/negative = contact/overlap) plus each part's world-space bounding box; pass one id to get that part's bbox, volume, and center of mass. Pass `joint_state` (joint id or name \u2192 degrees, mm for sliders) to measure a jointed assembly at one real pose instead of the stored one. Pass `sweep` \u2014 [{joint, from, to, steps}] \u2014 with TWO part ids to drive the mechanism through its range of motion and get the WORST (minimum) distance over the whole travel, the pose that realizes it (`worst_pose`), and `samples`: the full per-pose margin curve, so you can see where the clearance collapses rather than only that it does (omitted above 256 poses). Sweep endpoints outside a joint's declared limits are reported in `sweep_warnings`, not clamped; joint states are always restored, so a sweep never edits the document. Complements `inspect_cad` (whole-document aggregate) and `check_clearance` (named, persisted air-gap assertions with pass/fail and whole-document audits). Distances are measured from evaluated tessellations \u2014 swept poses re-place those same meshes \u2014 so accuracy is tessellation-bound.",
|
|
53469
53825
|
inputSchema: measureSchema,
|
|
53470
53826
|
handler: (a, c) => measure(a, c.engine),
|
|
53471
53827
|
behavior: behavior({})
|
|
@@ -58016,51 +58372,6 @@ function clearanceHolds(distanceMm, minMm, allowContact, intersecting = false) {
|
|
|
58016
58372
|
if (intersecting) return false;
|
|
58017
58373
|
return distanceMm >= minMm || allowContact && clearanceVerdict(distanceMm) === "touching";
|
|
58018
58374
|
}
|
|
58019
|
-
function worseThan(a, b) {
|
|
58020
|
-
if (a.intersecting !== b.intersecting) return a.intersecting;
|
|
58021
|
-
return a.distance < b.distance;
|
|
58022
|
-
}
|
|
58023
|
-
function placeMesh(mesh, transform2) {
|
|
58024
|
-
if (!transform2) return mesh;
|
|
58025
|
-
return transformMesh(mesh, {
|
|
58026
|
-
translate: transform2.translation,
|
|
58027
|
-
rotate: transform2.rotation,
|
|
58028
|
-
scale: transform2.scale
|
|
58029
|
-
});
|
|
58030
|
-
}
|
|
58031
|
-
function poseableParts(doc, engine) {
|
|
58032
|
-
const scene = engine.evaluate(doc);
|
|
58033
|
-
const visibleRoots = doc.roots.filter((e) => e.visible !== false);
|
|
58034
|
-
const out = [];
|
|
58035
|
-
for (let i = 0; i < scene.parts.length && i < visibleRoots.length; i++) {
|
|
58036
|
-
const rootId = visibleRoots[i].root;
|
|
58037
|
-
const node = doc.nodes[String(rootId)];
|
|
58038
|
-
const mesh = scene.parts[i].mesh;
|
|
58039
|
-
if (!mesh || mesh.positions.length === 0) continue;
|
|
58040
|
-
out.push({ id: String(rootId), name: node?.name ?? void 0, localMesh: mesh });
|
|
58041
|
-
}
|
|
58042
|
-
for (const inst of scene.instances ?? []) {
|
|
58043
|
-
if (!inst.mesh || inst.mesh.positions.length === 0) continue;
|
|
58044
|
-
out.push({
|
|
58045
|
-
id: inst.instanceId,
|
|
58046
|
-
name: inst.name ?? void 0,
|
|
58047
|
-
localMesh: inst.mesh,
|
|
58048
|
-
transform: inst.transform ?? void 0,
|
|
58049
|
-
instanceId: inst.instanceId
|
|
58050
|
-
});
|
|
58051
|
-
}
|
|
58052
|
-
return out;
|
|
58053
|
-
}
|
|
58054
|
-
function placeParts(parts, worldTransforms) {
|
|
58055
|
-
const out = [];
|
|
58056
|
-
for (const p of parts) {
|
|
58057
|
-
const transform2 = (p.instanceId ? worldTransforms?.get(p.instanceId) : void 0) ?? p.transform;
|
|
58058
|
-
const mesh = placeMesh(p.localMesh, transform2);
|
|
58059
|
-
if (!mesh || mesh.positions.length === 0) continue;
|
|
58060
|
-
out.push({ id: p.id, ...p.name ? { name: p.name } : {}, mesh });
|
|
58061
|
-
}
|
|
58062
|
-
return out;
|
|
58063
|
-
}
|
|
58064
58375
|
function resolveGroup(candidates, ids) {
|
|
58065
58376
|
const parts = /* @__PURE__ */ new Map();
|
|
58066
58377
|
const missing = [];
|
|
@@ -58072,67 +58383,6 @@ function resolveGroup(candidates, ids) {
|
|
|
58072
58383
|
}
|
|
58073
58384
|
return { parts: [...parts.values()], missing };
|
|
58074
58385
|
}
|
|
58075
|
-
function resolveSweepAxes(doc, axes) {
|
|
58076
|
-
const joints = doc.joints ?? [];
|
|
58077
|
-
const resolved = [];
|
|
58078
|
-
for (const axis of axes) {
|
|
58079
|
-
const wanted = String(axis.joint);
|
|
58080
|
-
const found = joints.find((j) => j.id === wanted) ?? joints.find((j) => j.name === wanted);
|
|
58081
|
-
if (!found) {
|
|
58082
|
-
const available = joints.map((j) => `${j.id}${j.name ? ` (${j.name})` : ""}`).join(", ");
|
|
58083
|
-
return { error: `No joint with id or name "${wanted}". Available: ${available || "none"}` };
|
|
58084
|
-
}
|
|
58085
|
-
if (!Number.isFinite(axis.from) || !Number.isFinite(axis.to)) {
|
|
58086
|
-
return { error: `Sweep axis "${wanted}" needs finite \`from\` and \`to\`.` };
|
|
58087
|
-
}
|
|
58088
|
-
const steps = Math.trunc(axis.steps);
|
|
58089
|
-
if (!Number.isFinite(steps) || steps < 1) {
|
|
58090
|
-
return { error: `Sweep axis "${wanted}" needs \`steps\` >= 1.` };
|
|
58091
|
-
}
|
|
58092
|
-
resolved.push({ joint: found.id, from: axis.from, to: axis.to, steps });
|
|
58093
|
-
}
|
|
58094
|
-
if (resolved.length === 0) return { error: "`sweep` needs at least one joint axis." };
|
|
58095
|
-
const total = resolved.reduce((n, a) => n * (a.steps + 1), 1);
|
|
58096
|
-
if (total > MAX_SWEEP_POSES) {
|
|
58097
|
-
return {
|
|
58098
|
-
error: `Sweep grid is ${total} poses (limit ${MAX_SWEEP_POSES}). Lower \`steps\` or sweep fewer joints at once.`
|
|
58099
|
-
};
|
|
58100
|
-
}
|
|
58101
|
-
return { axes: resolved };
|
|
58102
|
-
}
|
|
58103
|
-
function poseGrid(axes) {
|
|
58104
|
-
let poses = [[]];
|
|
58105
|
-
for (const axis of axes) {
|
|
58106
|
-
const n = Math.trunc(axis.steps);
|
|
58107
|
-
const next = [];
|
|
58108
|
-
for (const pose of poses) {
|
|
58109
|
-
for (let i = 0; i <= n; i++) {
|
|
58110
|
-
const state = axis.from + (axis.to - axis.from) * i / n;
|
|
58111
|
-
next.push([...pose, { joint: axis.joint, state }]);
|
|
58112
|
-
}
|
|
58113
|
-
}
|
|
58114
|
-
poses = next;
|
|
58115
|
-
}
|
|
58116
|
-
return poses;
|
|
58117
|
-
}
|
|
58118
|
-
function poseTransforms(doc, poses) {
|
|
58119
|
-
const joints = doc.joints ?? [];
|
|
58120
|
-
const byId = new Map(joints.map((j) => [j.id, j]));
|
|
58121
|
-
const saved = joints.map((j) => j.state);
|
|
58122
|
-
try {
|
|
58123
|
-
return poses.map((pose) => {
|
|
58124
|
-
for (const { joint, state } of pose) {
|
|
58125
|
-
const j = byId.get(joint);
|
|
58126
|
-
if (j) j.state = state;
|
|
58127
|
-
}
|
|
58128
|
-
return solveForwardKinematics2(doc);
|
|
58129
|
-
});
|
|
58130
|
-
} finally {
|
|
58131
|
-
joints.forEach((j, i) => {
|
|
58132
|
-
j.state = saved[i];
|
|
58133
|
-
});
|
|
58134
|
-
}
|
|
58135
|
-
}
|
|
58136
58386
|
function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
|
|
58137
58387
|
if (groupA.length === 0 || groupB.length === 0) {
|
|
58138
58388
|
return { error: "Both `group_a` and `group_b` need at least one part id." };
|
|
@@ -58146,7 +58396,7 @@ function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
|
|
|
58146
58396
|
if (!sweep || sweep.length === 0) {
|
|
58147
58397
|
return measureGroups(engine, placeParts(base), groupA, groupB);
|
|
58148
58398
|
}
|
|
58149
|
-
const { axes, error: sweepError } = resolveSweepAxes(doc, sweep);
|
|
58399
|
+
const { axes, warnings, error: sweepError } = resolveSweepAxes(doc, sweep);
|
|
58150
58400
|
if (sweepError || !axes) return { error: sweepError ?? "sweep could not be resolved" };
|
|
58151
58401
|
const poses = poseGrid(axes);
|
|
58152
58402
|
let transforms;
|
|
@@ -58156,6 +58406,8 @@ function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
|
|
|
58156
58406
|
return { error: e instanceof Error ? e.message : String(e) };
|
|
58157
58407
|
}
|
|
58158
58408
|
let worst2;
|
|
58409
|
+
const samples = [];
|
|
58410
|
+
const keepSamples = poses.length <= MAX_SWEEP_SAMPLES;
|
|
58159
58411
|
for (let i = 0; i < poses.length; i++) {
|
|
58160
58412
|
const { result, error: error2 } = measureGroups(
|
|
58161
58413
|
engine,
|
|
@@ -58164,6 +58416,13 @@ function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
|
|
|
58164
58416
|
groupB
|
|
58165
58417
|
);
|
|
58166
58418
|
if (error2 || !result) return { error: error2 };
|
|
58419
|
+
if (keepSamples) {
|
|
58420
|
+
samples.push({
|
|
58421
|
+
pose: roundPose(poses[i]),
|
|
58422
|
+
distance_mm: result.distance_mm,
|
|
58423
|
+
intersecting: result.intersecting
|
|
58424
|
+
});
|
|
58425
|
+
}
|
|
58167
58426
|
if (!worst2 || worseThan(
|
|
58168
58427
|
{ distance: result.distance_mm, intersecting: result.intersecting },
|
|
58169
58428
|
{ distance: worst2.result.distance_mm, intersecting: worst2.result.intersecting }
|
|
@@ -58175,8 +58434,10 @@ function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
|
|
|
58175
58434
|
return {
|
|
58176
58435
|
result: {
|
|
58177
58436
|
...worst2.result,
|
|
58178
|
-
worst_pose: worst2.pose
|
|
58179
|
-
poses_checked: poses.length
|
|
58437
|
+
worst_pose: roundPose(worst2.pose),
|
|
58438
|
+
poses_checked: poses.length,
|
|
58439
|
+
...keepSamples ? { samples } : {},
|
|
58440
|
+
...warnings && warnings.length > 0 ? { sweep_warnings: warnings } : {}
|
|
58180
58441
|
}
|
|
58181
58442
|
};
|
|
58182
58443
|
}
|
|
@@ -58260,10 +58521,12 @@ function computeInterferenceAudit(doc, engine, opts) {
|
|
|
58260
58521
|
return { error: e instanceof Error ? e.message : String(e) };
|
|
58261
58522
|
}
|
|
58262
58523
|
let poses = [];
|
|
58524
|
+
let sweepWarnings;
|
|
58263
58525
|
let transforms = [void 0];
|
|
58264
58526
|
if (opts.sweep && opts.sweep.length > 0) {
|
|
58265
|
-
const { axes, error: error2 } = resolveSweepAxes(doc, opts.sweep);
|
|
58527
|
+
const { axes, warnings, error: error2 } = resolveSweepAxes(doc, opts.sweep);
|
|
58266
58528
|
if (error2 || !axes) return { error: error2 ?? "sweep could not be resolved" };
|
|
58529
|
+
sweepWarnings = warnings;
|
|
58267
58530
|
poses = poseGrid(axes);
|
|
58268
58531
|
try {
|
|
58269
58532
|
transforms = poseTransforms(doc, poses);
|
|
@@ -58354,7 +58617,8 @@ function computeInterferenceAudit(doc, engine, opts) {
|
|
|
58354
58617
|
queries,
|
|
58355
58618
|
...poses.length > 0 ? { poses_checked: poses.length } : {},
|
|
58356
58619
|
findings,
|
|
58357
|
-
unresolved_ignores: [...new Set(unresolved)]
|
|
58620
|
+
unresolved_ignores: [...new Set(unresolved)],
|
|
58621
|
+
...sweepWarnings && sweepWarnings.length > 0 ? { sweep_warnings: sweepWarnings } : {}
|
|
58358
58622
|
}
|
|
58359
58623
|
};
|
|
58360
58624
|
}
|
|
@@ -58372,6 +58636,7 @@ async function checkClearance(args, engine) {
|
|
|
58372
58636
|
const groupB = stringArray(args.group_b);
|
|
58373
58637
|
const label = typeof args.label === "string" && args.label.trim() ? args.label.trim() : void 0;
|
|
58374
58638
|
const allowContact = asBool(args.allow_contact);
|
|
58639
|
+
const includeSamples = asBool(args.include_samples);
|
|
58375
58640
|
const { sweep, error: sweepParseError } = parseSweep(args.sweep);
|
|
58376
58641
|
if (sweepParseError) return err3(sweepParseError);
|
|
58377
58642
|
if (label && args.joint_state !== void 0 && args.joint_state !== null) {
|
|
@@ -58427,6 +58692,11 @@ async function checkClearance(args, engine) {
|
|
|
58427
58692
|
worst_pair: result.worst_pair,
|
|
58428
58693
|
...result.worst_pose ? { worst_pose: result.worst_pose } : {},
|
|
58429
58694
|
...result.poses_checked !== void 0 ? { poses_checked: result.poses_checked } : {},
|
|
58695
|
+
...result.sweep_warnings ? { sweep_warnings: result.sweep_warnings } : {},
|
|
58696
|
+
...includeSamples && result.poses_checked !== void 0 ? result.samples ? { samples: result.samples } : {
|
|
58697
|
+
samples_omitted: true,
|
|
58698
|
+
samples_note: `Sweep grid is ${result.poses_checked} poses (over the ${MAX_SWEEP_SAMPLES}-pose sample cap) \u2014 per-pose samples were omitted. Lower \`steps\` to get the margin curve.`
|
|
58699
|
+
} : {},
|
|
58430
58700
|
pairs_checked: result.pairs_checked,
|
|
58431
58701
|
group_a: result.group_a,
|
|
58432
58702
|
group_b: result.group_b,
|
|
@@ -58443,30 +58713,6 @@ async function checkClearance(args, engine) {
|
|
|
58443
58713
|
}
|
|
58444
58714
|
};
|
|
58445
58715
|
}
|
|
58446
|
-
function parseSweep(raw) {
|
|
58447
|
-
if (raw === void 0 || raw === null) return {};
|
|
58448
|
-
if (!Array.isArray(raw)) {
|
|
58449
|
-
return { error: "`sweep` must be an array of {joint, from, to, steps} axes." };
|
|
58450
|
-
}
|
|
58451
|
-
const sweep = [];
|
|
58452
|
-
for (const entry of raw) {
|
|
58453
|
-
if (!entry || typeof entry !== "object") {
|
|
58454
|
-
return { error: "Each `sweep` axis must be an object {joint, from, to, steps}." };
|
|
58455
|
-
}
|
|
58456
|
-
const e = entry;
|
|
58457
|
-
const joint = typeof e.joint === "string" ? e.joint.trim() : "";
|
|
58458
|
-
const from = Number(e.from);
|
|
58459
|
-
const to = Number(e.to);
|
|
58460
|
-
const steps = Number(e.steps);
|
|
58461
|
-
if (!joint || !Number.isFinite(from) || !Number.isFinite(to) || !Number.isFinite(steps)) {
|
|
58462
|
-
return {
|
|
58463
|
-
error: "Each `sweep` axis needs a `joint` id/name plus numeric `from`, `to`, and `steps`."
|
|
58464
|
-
};
|
|
58465
|
-
}
|
|
58466
|
-
sweep.push({ joint, from, to, steps });
|
|
58467
|
-
}
|
|
58468
|
-
return sweep.length > 0 ? { sweep } : {};
|
|
58469
|
-
}
|
|
58470
58716
|
function parseIgnorePairs(raw) {
|
|
58471
58717
|
if (raw === void 0 || raw === null) return { pairs: [] };
|
|
58472
58718
|
if (!Array.isArray(raw)) {
|
|
@@ -58510,6 +58756,7 @@ function auditClearance(documentId, doc, engine, args, sweep, label, pose) {
|
|
|
58510
58756
|
pairs_broadphase: result.pairs_broadphase,
|
|
58511
58757
|
queries: result.queries,
|
|
58512
58758
|
...result.poses_checked !== void 0 ? { poses_checked: result.poses_checked } : {},
|
|
58759
|
+
...result.sweep_warnings ? { sweep_warnings: result.sweep_warnings } : {},
|
|
58513
58760
|
...result.unresolved_ignores.length > 0 ? {
|
|
58514
58761
|
unresolved_ignores: result.unresolved_ignores,
|
|
58515
58762
|
note: `ignore_pairs named parts that do not exist: ${result.unresolved_ignores.join(", ")} \u2014 those pairs were NOT whitelisted.`
|
|
@@ -58674,16 +58921,17 @@ function err3(text) {
|
|
|
58674
58921
|
isError: true
|
|
58675
58922
|
};
|
|
58676
58923
|
}
|
|
58677
|
-
var CLEARANCE_CLAIM_PREFIX, MECH_DOMAIN2, CLEARANCE_ORACLE, STALE_EPS_MM, CONTACT_EPS_MM,
|
|
58924
|
+
var CLEARANCE_CLAIM_PREFIX, MECH_DOMAIN2, CLEARANCE_ORACLE, STALE_EPS_MM, CONTACT_EPS_MM, checkClearanceSchema, pairKey, toolDefs11;
|
|
58678
58925
|
var init_clearance = __esm({
|
|
58679
58926
|
"src/tools/clearance.ts"() {
|
|
58680
58927
|
"use strict";
|
|
58681
58928
|
init_tool_def();
|
|
58682
|
-
init_dist();
|
|
58683
58929
|
init_receipt_unified();
|
|
58684
58930
|
init_pose();
|
|
58685
58931
|
init_session_core();
|
|
58686
58932
|
init_arg_coerce();
|
|
58933
|
+
init_sweep();
|
|
58934
|
+
init_sweep();
|
|
58687
58935
|
CLEARANCE_CLAIM_PREFIX = "mech.clearance.";
|
|
58688
58936
|
MECH_DOMAIN2 = "mechanical";
|
|
58689
58937
|
CLEARANCE_ORACLE = {
|
|
@@ -58692,7 +58940,6 @@ var init_clearance = __esm({
|
|
|
58692
58940
|
};
|
|
58693
58941
|
STALE_EPS_MM = 1e-6;
|
|
58694
58942
|
CONTACT_EPS_MM = 1e-3;
|
|
58695
|
-
round6 = (v) => Math.round(v * 1e6) / 1e6;
|
|
58696
58943
|
checkClearanceSchema = {
|
|
58697
58944
|
type: "object",
|
|
58698
58945
|
properties: {
|
|
@@ -58723,19 +58970,10 @@ var init_clearance = __esm({
|
|
|
58723
58970
|
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."
|
|
58724
58971
|
},
|
|
58725
58972
|
joint_state: jointStateSchemaProp,
|
|
58726
|
-
sweep:
|
|
58727
|
-
|
|
58728
|
-
|
|
58729
|
-
|
|
58730
|
-
type: "object",
|
|
58731
|
-
properties: {
|
|
58732
|
-
joint: { type: "string", description: "Joint id or name to drive." },
|
|
58733
|
-
from: { type: "number", description: "Start of travel (degrees for revolute, mm for prismatic)." },
|
|
58734
|
-
to: { type: "number", description: "End of travel." },
|
|
58735
|
-
steps: { type: "number", description: "Number of intervals; steps + 1 poses are sampled." }
|
|
58736
|
-
},
|
|
58737
|
-
required: ["joint", "from", "to", "steps"]
|
|
58738
|
-
}
|
|
58973
|
+
sweep: sweepSchemaProp,
|
|
58974
|
+
include_samples: {
|
|
58975
|
+
type: "boolean",
|
|
58976
|
+
description: "Swept queries only: return the per-pose margin curve as `samples` \u2014 one {pose, distance_mm, intersecting} entry per pose, in grid order. Omitted (with a note) when the grid exceeds 256 poses. Never persisted with a labelled spec."
|
|
58739
58977
|
},
|
|
58740
58978
|
ignore_pairs: {
|
|
58741
58979
|
type: "array",
|
|
@@ -58756,13 +58994,12 @@ var init_clearance = __esm({
|
|
|
58756
58994
|
},
|
|
58757
58995
|
required: ["document_id"]
|
|
58758
58996
|
};
|
|
58759
|
-
MAX_SWEEP_POSES = 4096;
|
|
58760
58997
|
pairKey = (a, b) => a < b ? `${a}\0${b}` : `${b}\0${a}`;
|
|
58761
58998
|
toolDefs11 = [
|
|
58762
58999
|
{
|
|
58763
59000
|
name: "check_clearance",
|
|
58764
59001
|
pack: null,
|
|
58765
|
-
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'.",
|
|
59002
|
+
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. Sweep endpoints beyond a joint's declared limits are reported in `sweep_warnings` rather than clamped, and `include_samples: true` returns the per-pose margin curve as `samples` (one {pose, distance_mm, intersecting} per pose, omitted above 256 poses and never persisted). Swept distances come from the same tessellated meshes, re-placed per pose, so accuracy stays tessellation-bound. (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'.",
|
|
58766
59003
|
inputSchema: checkClearanceSchema,
|
|
58767
59004
|
handler: (a, c) => checkClearance(a, c.engine),
|
|
58768
59005
|
behavior: behavior({ writesDoc: true })
|
|
@@ -84052,8 +84289,8 @@ var init_server3 = __esm({
|
|
|
84052
84289
|
init_order_feed();
|
|
84053
84290
|
init_animate();
|
|
84054
84291
|
PKG_VERSION = (() => {
|
|
84055
|
-
if ("0.9.4-main.
|
|
84056
|
-
return "0.9.4-main.
|
|
84292
|
+
if ("0.9.4-main.76") {
|
|
84293
|
+
return "0.9.4-main.76";
|
|
84057
84294
|
}
|
|
84058
84295
|
try {
|
|
84059
84296
|
const req = createRequire2(import.meta.url);
|
|
@@ -84062,8 +84299,8 @@ var init_server3 = __esm({
|
|
|
84062
84299
|
return "0.0.0";
|
|
84063
84300
|
}
|
|
84064
84301
|
})();
|
|
84065
|
-
BUILD_SHA = "
|
|
84066
|
-
BUILD_TIME = "2026-08-
|
|
84302
|
+
BUILD_SHA = "58bebb998fe85b92d30bbc46939ed4dceaf4adc4";
|
|
84303
|
+
BUILD_TIME = "2026-08-26T13:14:00.987Z";
|
|
84067
84304
|
SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
|
|
84068
84305
|
VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
|
|
84069
84306
|
INSTANCE_ID = randomUUID7().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.76",
|
|
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-08-
|
|
22
|
+
"sha": "58bebb998fe85b92d30bbc46939ed4dceaf4adc4",
|
|
23
|
+
"builtAt": "2026-08-26T13:14:00.987Z"
|
|
24
24
|
}
|
|
25
25
|
}
|
package/vcad_kernel_wasm_bg.wasm
CHANGED
|
Binary file
|