@vcad/mcp 0.9.4-main.74 → 0.9.4-main.75

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 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 cad60a705d41503617de2b556150aaeae3e893c8 at 2026-08-19T15:53:22.260Z. Source: https://github.com/ecto/vcad
9
+ Built from 89230349e9d445891fbcb1efd7f0cc9425ba972e at 2026-08-25T23:30:10.326Z. 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 wasm_bindgen_fdb3f87e5dacef0f___convert__closures_____invoke___wasm_bindgen_fdb3f87e5dacef0f___JsValue__wasm_bindgen_fdb3f87e5dacef0f___JsValue_____(a, state0.b, arg02, arg12);
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.wasm_bindgen_fdb3f87e5dacef0f___closure__destroy___dyn_core_9b3796e30d99ddb7___ops__function__FnMut__wgpu_43013bc91c6d6b83___backend__webgpu__webgpu_sys__gen_GpuUncapturedErrorEvent__GpuUncapturedErrorEvent____Output_______, wasm_bindgen_fdb3f87e5dacef0f___convert__closures_____invoke___wgpu_43013bc91c6d6b83___backend__webgpu__webgpu_sys__gen_GpuUncapturedErrorEvent__GpuUncapturedErrorEvent_____);
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.wasm_bindgen_fdb3f87e5dacef0f___closure__destroy___dyn_core_9b3796e30d99ddb7___ops__function__FnMut__wasm_bindgen_fdb3f87e5dacef0f___JsValue____Output_______, wasm_bindgen_fdb3f87e5dacef0f___convert__closures_____invoke___wasm_bindgen_fdb3f87e5dacef0f___JsValue_____);
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 wasm_bindgen_fdb3f87e5dacef0f___convert__closures_____invoke___wgpu_43013bc91c6d6b83___backend__webgpu__webgpu_sys__gen_GpuUncapturedErrorEvent__GpuUncapturedErrorEvent_____(arg0, arg1, arg2) {
5422
- wasm.wasm_bindgen_fdb3f87e5dacef0f___convert__closures_____invoke___wgpu_43013bc91c6d6b83___backend__webgpu__webgpu_sys__gen_GpuUncapturedErrorEvent__GpuUncapturedErrorEvent_____(arg0, arg1, arg2);
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 wasm_bindgen_fdb3f87e5dacef0f___convert__closures_____invoke___wasm_bindgen_fdb3f87e5dacef0f___JsValue_____(arg0, arg1, arg2) {
5425
- wasm.wasm_bindgen_fdb3f87e5dacef0f___convert__closures_____invoke___wasm_bindgen_fdb3f87e5dacef0f___JsValue_____(arg0, arg1, arg2);
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 wasm_bindgen_fdb3f87e5dacef0f___convert__closures_____invoke___wasm_bindgen_fdb3f87e5dacef0f___JsValue__wasm_bindgen_fdb3f87e5dacef0f___JsValue_____(arg0, arg1, arg2, arg3) {
5428
- wasm.wasm_bindgen_fdb3f87e5dacef0f___convert__closures_____invoke___wasm_bindgen_fdb3f87e5dacef0f___JsValue__wasm_bindgen_fdb3f87e5dacef0f___JsValue_____(arg0, arg1, arg2, arg3);
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,42 @@ 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-19-photoreal-animation",
41655
+ version: "0.9.4",
41656
+ date: "2026-08-19",
41657
+ category: "perf",
41658
+ title: "Photoreal animation evaluates geometry once",
41659
+ 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.",
41660
+ 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.",
41661
+ features: [
41662
+ "rendering",
41663
+ "photoreal",
41664
+ "animation",
41665
+ "assemblies",
41666
+ "joints",
41667
+ "cli"
41668
+ ]
41669
+ },
41634
41670
  {
41635
41671
  id: "2026-08-19-loon-modules-and-root-cache",
41636
41672
  version: "0.9.4",
@@ -41650,6 +41686,21 @@ var init_CHANGELOG = __esm({
41650
41686
  "create_cad_loon"
41651
41687
  ]
41652
41688
  },
41689
+ {
41690
+ id: "2026-08-19-builtin-material-library",
41691
+ version: "0.9.4",
41692
+ date: "2026-08-19",
41693
+ category: "feat",
41694
+ title: "Renders know the standard material names",
41695
+ 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.',
41696
+ 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.`,
41697
+ features: [
41698
+ "rendering",
41699
+ "photoreal",
41700
+ "materials",
41701
+ "loon"
41702
+ ]
41703
+ },
41653
41704
  {
41654
41705
  id: "2026-08-17-altium-pcb-import",
41655
41706
  version: "0.9.4",
@@ -53211,8 +53262,129 @@ var init_inspect = __esm({
53211
53262
  }
53212
53263
  });
53213
53264
 
53214
- // src/tools/measure.ts
53215
- function evaluateParts(doc, engine) {
53265
+ // src/tools/sweep.ts
53266
+ function worseThan(a, b) {
53267
+ if (a.intersecting !== b.intersecting) return a.intersecting;
53268
+ return a.distance < b.distance;
53269
+ }
53270
+ function parseSweep(raw) {
53271
+ if (raw === void 0 || raw === null) return {};
53272
+ if (!Array.isArray(raw)) {
53273
+ return { error: "`sweep` must be an array of {joint, from, to, steps} axes." };
53274
+ }
53275
+ const sweep = [];
53276
+ for (const entry of raw) {
53277
+ if (!entry || typeof entry !== "object") {
53278
+ return { error: "Each `sweep` axis must be an object {joint, from, to, steps}." };
53279
+ }
53280
+ const e = entry;
53281
+ const joint = typeof e.joint === "string" ? e.joint.trim() : "";
53282
+ const from = Number(e.from);
53283
+ const to = Number(e.to);
53284
+ const steps = Number(e.steps);
53285
+ if (!joint || !Number.isFinite(from) || !Number.isFinite(to) || !Number.isFinite(steps)) {
53286
+ return {
53287
+ error: "Each `sweep` axis needs a `joint` id/name plus numeric `from`, `to`, and `steps`."
53288
+ };
53289
+ }
53290
+ sweep.push({ joint, from, to, steps });
53291
+ }
53292
+ return sweep.length > 0 ? { sweep } : {};
53293
+ }
53294
+ function jointLimits(kind) {
53295
+ const k = kind;
53296
+ if (!k) return void 0;
53297
+ if (k.type === "Revolute" || k.type === "Slider") return k.limits;
53298
+ return void 0;
53299
+ }
53300
+ function jointUnit(kind) {
53301
+ return kind?.type === "Slider" ? "mm" : "deg";
53302
+ }
53303
+ function resolveSweepAxes(doc, axes) {
53304
+ const joints = doc.joints ?? [];
53305
+ const resolved = [];
53306
+ const warnings = [];
53307
+ for (const axis of axes) {
53308
+ const wanted = String(axis.joint);
53309
+ const found = joints.find((j) => j.id === wanted) ?? joints.find((j) => j.name === wanted);
53310
+ if (!found) {
53311
+ const available = joints.map((j) => `${j.id}${j.name ? ` (${j.name})` : ""}`).join(", ");
53312
+ return { error: `No joint with id or name "${wanted}". Available: ${available || "none"}` };
53313
+ }
53314
+ if (!Number.isFinite(axis.from) || !Number.isFinite(axis.to)) {
53315
+ return { error: `Sweep axis "${wanted}" needs finite \`from\` and \`to\`.` };
53316
+ }
53317
+ const steps = Math.trunc(axis.steps);
53318
+ if (!Number.isFinite(steps) || steps < 1) {
53319
+ return { error: `Sweep axis "${wanted}" needs \`steps\` >= 1.` };
53320
+ }
53321
+ const limits = jointLimits(found.kind);
53322
+ if (limits) {
53323
+ const [lo, hi] = limits;
53324
+ const lowest = Math.min(axis.from, axis.to);
53325
+ const highest = Math.max(axis.from, axis.to);
53326
+ if (lowest < lo || highest > hi) {
53327
+ warnings.push(
53328
+ `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.`
53329
+ );
53330
+ }
53331
+ }
53332
+ resolved.push({ joint: found.id, from: axis.from, to: axis.to, steps });
53333
+ }
53334
+ if (resolved.length === 0) return { error: "`sweep` needs at least one joint axis." };
53335
+ const total = resolved.reduce((n, a) => n * (a.steps + 1), 1);
53336
+ if (total > MAX_SWEEP_POSES) {
53337
+ return {
53338
+ error: `Sweep grid is ${total} poses (limit ${MAX_SWEEP_POSES}). Lower \`steps\` or sweep fewer joints at once.`
53339
+ };
53340
+ }
53341
+ return { axes: resolved, ...warnings.length > 0 ? { warnings } : {} };
53342
+ }
53343
+ function poseGrid(axes) {
53344
+ let poses = [[]];
53345
+ for (const axis of axes) {
53346
+ const n = Math.trunc(axis.steps);
53347
+ const next = [];
53348
+ for (const pose of poses) {
53349
+ for (let i = 0; i <= n; i++) {
53350
+ const state = axis.from + (axis.to - axis.from) * i / n;
53351
+ next.push([...pose, { joint: axis.joint, state }]);
53352
+ }
53353
+ }
53354
+ poses = next;
53355
+ }
53356
+ return poses;
53357
+ }
53358
+ function roundPose(pose) {
53359
+ return pose.map((p) => ({ joint: p.joint, state: round6(p.state) }));
53360
+ }
53361
+ function poseTransforms(doc, poses) {
53362
+ const joints = doc.joints ?? [];
53363
+ const byId = new Map(joints.map((j) => [j.id, j]));
53364
+ const saved = joints.map((j) => j.state);
53365
+ try {
53366
+ return poses.map((pose) => {
53367
+ for (const { joint, state } of pose) {
53368
+ const j = byId.get(joint);
53369
+ if (j) j.state = state;
53370
+ }
53371
+ return solveForwardKinematics2(doc);
53372
+ });
53373
+ } finally {
53374
+ joints.forEach((j, i) => {
53375
+ j.state = saved[i];
53376
+ });
53377
+ }
53378
+ }
53379
+ function placeMesh(mesh, transform2) {
53380
+ if (!transform2) return mesh;
53381
+ return transformMesh(mesh, {
53382
+ translate: transform2.translation,
53383
+ rotate: transform2.rotation,
53384
+ scale: transform2.scale
53385
+ });
53386
+ }
53387
+ function poseableParts(doc, engine) {
53216
53388
  const scene = engine.evaluate(doc);
53217
53389
  const visibleRoots = doc.roots.filter((e) => e.visible !== false);
53218
53390
  const partMaterials = doc.part_materials;
@@ -53220,31 +53392,78 @@ function evaluateParts(doc, engine) {
53220
53392
  for (let i = 0; i < scene.parts.length && i < visibleRoots.length; i++) {
53221
53393
  const root = visibleRoots[i];
53222
53394
  const rootId = String(root.root);
53395
+ const node = doc.nodes[rootId];
53223
53396
  const mesh = scene.parts[i].mesh;
53224
53397
  if (!mesh || mesh.positions.length === 0) continue;
53225
- const node = doc.nodes[rootId];
53226
53398
  out.push({
53227
53399
  id: rootId,
53228
53400
  name: node?.name ?? void 0,
53229
53401
  material: root.material ?? partMaterials?.[rootId] ?? void 0,
53230
- mesh
53402
+ localMesh: mesh
53231
53403
  });
53232
53404
  }
53233
53405
  for (const inst of scene.instances ?? []) {
53234
- const mesh = inst.transform ? transformMesh(inst.mesh, {
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;
53406
+ if (!inst.mesh || inst.mesh.positions.length === 0) continue;
53240
53407
  out.push({
53241
53408
  id: inst.instanceId,
53242
53409
  name: inst.name ?? void 0,
53410
+ localMesh: inst.mesh,
53411
+ transform: inst.transform ?? void 0,
53412
+ instanceId: inst.instanceId
53413
+ });
53414
+ }
53415
+ return out;
53416
+ }
53417
+ function placeParts(parts, worldTransforms) {
53418
+ const out = [];
53419
+ for (const p of parts) {
53420
+ const transform2 = (p.instanceId ? worldTransforms?.get(p.instanceId) : void 0) ?? p.transform;
53421
+ const mesh = placeMesh(p.localMesh, transform2);
53422
+ if (!mesh || mesh.positions.length === 0) continue;
53423
+ out.push({
53424
+ id: p.id,
53425
+ ...p.name ? { name: p.name } : {},
53426
+ ...p.material ? { material: p.material } : {},
53243
53427
  mesh
53244
53428
  });
53245
53429
  }
53246
53430
  return out;
53247
53431
  }
53432
+ var MAX_SWEEP_POSES, MAX_SWEEP_SAMPLES, round6, sweepSchemaProp;
53433
+ var init_sweep = __esm({
53434
+ "src/tools/sweep.ts"() {
53435
+ "use strict";
53436
+ init_dist();
53437
+ MAX_SWEEP_POSES = 4096;
53438
+ MAX_SWEEP_SAMPLES = 256;
53439
+ round6 = (v) => Math.round(v * 1e6) / 1e6;
53440
+ sweepSchemaProp = {
53441
+ type: "array",
53442
+ 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.",
53443
+ items: {
53444
+ type: "object",
53445
+ properties: {
53446
+ joint: { type: "string", description: "Joint id or name to drive." },
53447
+ from: {
53448
+ type: "number",
53449
+ description: "Start of travel (degrees for revolute, mm for prismatic)."
53450
+ },
53451
+ to: { type: "number", description: "End of travel." },
53452
+ steps: {
53453
+ type: "number",
53454
+ description: "Number of intervals; steps + 1 poses are sampled."
53455
+ }
53456
+ },
53457
+ required: ["joint", "from", "to", "steps"]
53458
+ }
53459
+ };
53460
+ }
53461
+ });
53462
+
53463
+ // src/tools/measure.ts
53464
+ function evaluateParts(doc, engine) {
53465
+ return placeParts(poseableParts(doc, engine));
53466
+ }
53248
53467
  function findPart(candidates, wanted) {
53249
53468
  return candidates.find((c) => c.id === wanted) ?? candidates.find((c) => c.name === wanted);
53250
53469
  }
@@ -53394,6 +53613,96 @@ function measureResult(doc, engine, partIds) {
53394
53613
  note: "Distance is negative when the parts overlap (penetration depth). Measured from evaluated tessellations \u2014 accuracy is tessellation-bound."
53395
53614
  };
53396
53615
  }
53616
+ function measureSweepResult(doc, engine, partIds, sweep) {
53617
+ if (partIds.length !== 2) {
53618
+ throw new MeasureError(
53619
+ `\`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).`
53620
+ );
53621
+ }
53622
+ let base;
53623
+ try {
53624
+ base = poseableParts(doc, engine);
53625
+ } catch (e) {
53626
+ throw new MeasureError(e instanceof Error ? e.message : String(e));
53627
+ }
53628
+ const pick2 = (wanted) => {
53629
+ const found = base.find((p) => p.id === wanted) ?? base.find((p) => p.name === wanted);
53630
+ if (!found) {
53631
+ const available = base.map((p) => `${p.id}${p.name ? ` (${p.name})` : ""}`).join(", ") || "none";
53632
+ throw new MeasureError(
53633
+ `No part with id or name "${wanted}". Available: ${available}`
53634
+ );
53635
+ }
53636
+ return found;
53637
+ };
53638
+ const [pa, pb] = partIds.map((id) => pick2(String(id)));
53639
+ if (pa.id === pb.id) {
53640
+ throw new MeasureError(
53641
+ `measure needs two distinct parts \u2014 "${pa.id}" was given twice.`
53642
+ );
53643
+ }
53644
+ const { axes, warnings, error: error2 } = resolveSweepAxes(doc, sweep);
53645
+ if (error2 || !axes) throw new MeasureError(error2 ?? "sweep could not be resolved");
53646
+ const poses = poseGrid(axes);
53647
+ let transforms;
53648
+ try {
53649
+ transforms = poseTransforms(doc, poses);
53650
+ } catch (e) {
53651
+ throw new MeasureError(e instanceof Error ? e.message : String(e));
53652
+ }
53653
+ const keepSamples = poses.length <= MAX_SWEEP_SAMPLES;
53654
+ const samples = [];
53655
+ let worst2;
53656
+ for (let i = 0; i < poses.length; i++) {
53657
+ const placed = placeParts([pa, pb], transforms[i]);
53658
+ if (placed.length < 2) {
53659
+ throw new MeasureError(
53660
+ "One of the parts has an empty mesh at some pose \u2014 nothing to measure."
53661
+ );
53662
+ }
53663
+ let r;
53664
+ try {
53665
+ r = engine.meshClearance(placed[0].mesh, placed[1].mesh);
53666
+ } catch (e) {
53667
+ throw new MeasureError(e instanceof Error ? e.message : String(e));
53668
+ }
53669
+ if (keepSamples) {
53670
+ samples.push({
53671
+ pose: roundPose(poses[i]),
53672
+ distance_mm: r6(r.distance),
53673
+ intersecting: r.intersecting
53674
+ });
53675
+ }
53676
+ if (!worst2 || worseThan(r, worst2.r)) {
53677
+ worst2 = { r, pose: poses[i], a: placed[0], b: placed[1] };
53678
+ }
53679
+ }
53680
+ if (!worst2) throw new MeasureError("Sweep produced no poses to measure.");
53681
+ const distance = r6(worst2.r.distance);
53682
+ return {
53683
+ mode: "sweep",
53684
+ distance_mm: distance,
53685
+ contact: distance <= 0,
53686
+ intersecting: worst2.r.intersecting,
53687
+ worst_pose: roundPose(worst2.pose),
53688
+ poses_checked: poses.length,
53689
+ sweep: axes,
53690
+ closest_points: {
53691
+ a: worst2.r.pointA.map(r6),
53692
+ b: worst2.r.pointB.map(r6)
53693
+ },
53694
+ parts: {
53695
+ a: partSnapshot(worst2.a),
53696
+ b: partSnapshot(worst2.b)
53697
+ },
53698
+ ...keepSamples ? { samples } : {
53699
+ samples_omitted: true,
53700
+ 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.`
53701
+ },
53702
+ ...warnings && warnings.length > 0 ? { sweep_warnings: warnings } : {},
53703
+ 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."
53704
+ };
53705
+ }
53397
53706
  function measure(args, engine) {
53398
53707
  const documentId = String(args.document_id ?? "");
53399
53708
  if (!documentId) {
@@ -53405,20 +53714,33 @@ function measure(args, engine) {
53405
53714
  "Pass `part_ids` as an array of one or two part ids (or part names)."
53406
53715
  );
53407
53716
  }
53717
+ const { sweep, error: sweepParseError } = parseSweep(args.sweep);
53718
+ if (sweepParseError) return err(sweepParseError);
53719
+ let stored;
53720
+ try {
53721
+ stored = getSession(documentId);
53722
+ } catch (e) {
53723
+ return err(e instanceof Error ? e.message : String(e));
53724
+ }
53408
53725
  let doc;
53726
+ let pose;
53409
53727
  try {
53410
- doc = getSession(documentId);
53728
+ ({ doc, pose } = applyJointState(stored, args.joint_state));
53411
53729
  } catch (e) {
53412
53730
  return err(e instanceof Error ? e.message : String(e));
53413
53731
  }
53414
53732
  let payload;
53415
53733
  try {
53416
- payload = measureResult(doc, engine, partIds);
53734
+ payload = sweep && sweep.length > 0 ? measureSweepResult(doc, engine, partIds, sweep) : measureResult(doc, engine, partIds);
53417
53735
  } catch (e) {
53418
53736
  if (e instanceof MeasureError) return err(e.message);
53419
53737
  throw e;
53420
53738
  }
53421
- const body = { document_id: documentId, ...payload };
53739
+ const body = {
53740
+ document_id: documentId,
53741
+ ...pose ? { pose } : {},
53742
+ ...payload
53743
+ };
53422
53744
  return {
53423
53745
  content: [{ type: "text", text: JSON.stringify(body) }],
53424
53746
  structuredContent: { measure: body, document_id: documentId }
@@ -53434,10 +53756,11 @@ var r6, point, MeasureError, measureSchema, toolDefs6;
53434
53756
  var init_measure = __esm({
53435
53757
  "src/tools/measure.ts"() {
53436
53758
  "use strict";
53437
- init_dist();
53438
53759
  init_inspect();
53439
53760
  init_session_core();
53440
53761
  init_tool_def();
53762
+ init_pose();
53763
+ init_sweep();
53441
53764
  r6 = (v) => Math.round(v * 1e6) / 1e6;
53442
53765
  point = (p) => ({
53443
53766
  x: r6(p.x),
@@ -53457,7 +53780,9 @@ var init_measure = __esm({
53457
53780
  type: "array",
53458
53781
  items: { type: "string" },
53459
53782
  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
- }
53783
+ },
53784
+ joint_state: jointStateSchemaProp,
53785
+ sweep: sweepSchemaProp
53461
53786
  },
53462
53787
  required: ["document_id", "part_ids"]
53463
53788
  };
@@ -53465,7 +53790,7 @@ var init_measure = __esm({
53465
53790
  {
53466
53791
  name: "measure",
53467
53792
  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, so accuracy is tessellation-bound.",
53793
+ 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
53794
  inputSchema: measureSchema,
53470
53795
  handler: (a, c) => measure(a, c.engine),
53471
53796
  behavior: behavior({})
@@ -58016,51 +58341,6 @@ function clearanceHolds(distanceMm, minMm, allowContact, intersecting = false) {
58016
58341
  if (intersecting) return false;
58017
58342
  return distanceMm >= minMm || allowContact && clearanceVerdict(distanceMm) === "touching";
58018
58343
  }
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
58344
  function resolveGroup(candidates, ids) {
58065
58345
  const parts = /* @__PURE__ */ new Map();
58066
58346
  const missing = [];
@@ -58072,67 +58352,6 @@ function resolveGroup(candidates, ids) {
58072
58352
  }
58073
58353
  return { parts: [...parts.values()], missing };
58074
58354
  }
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
58355
  function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
58137
58356
  if (groupA.length === 0 || groupB.length === 0) {
58138
58357
  return { error: "Both `group_a` and `group_b` need at least one part id." };
@@ -58146,7 +58365,7 @@ function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
58146
58365
  if (!sweep || sweep.length === 0) {
58147
58366
  return measureGroups(engine, placeParts(base), groupA, groupB);
58148
58367
  }
58149
- const { axes, error: sweepError } = resolveSweepAxes(doc, sweep);
58368
+ const { axes, warnings, error: sweepError } = resolveSweepAxes(doc, sweep);
58150
58369
  if (sweepError || !axes) return { error: sweepError ?? "sweep could not be resolved" };
58151
58370
  const poses = poseGrid(axes);
58152
58371
  let transforms;
@@ -58156,6 +58375,8 @@ function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
58156
58375
  return { error: e instanceof Error ? e.message : String(e) };
58157
58376
  }
58158
58377
  let worst2;
58378
+ const samples = [];
58379
+ const keepSamples = poses.length <= MAX_SWEEP_SAMPLES;
58159
58380
  for (let i = 0; i < poses.length; i++) {
58160
58381
  const { result, error: error2 } = measureGroups(
58161
58382
  engine,
@@ -58164,6 +58385,13 @@ function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
58164
58385
  groupB
58165
58386
  );
58166
58387
  if (error2 || !result) return { error: error2 };
58388
+ if (keepSamples) {
58389
+ samples.push({
58390
+ pose: roundPose(poses[i]),
58391
+ distance_mm: result.distance_mm,
58392
+ intersecting: result.intersecting
58393
+ });
58394
+ }
58167
58395
  if (!worst2 || worseThan(
58168
58396
  { distance: result.distance_mm, intersecting: result.intersecting },
58169
58397
  { distance: worst2.result.distance_mm, intersecting: worst2.result.intersecting }
@@ -58175,8 +58403,10 @@ function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
58175
58403
  return {
58176
58404
  result: {
58177
58405
  ...worst2.result,
58178
- worst_pose: worst2.pose.map((p) => ({ joint: p.joint, state: round6(p.state) })),
58179
- poses_checked: poses.length
58406
+ worst_pose: roundPose(worst2.pose),
58407
+ poses_checked: poses.length,
58408
+ ...keepSamples ? { samples } : {},
58409
+ ...warnings && warnings.length > 0 ? { sweep_warnings: warnings } : {}
58180
58410
  }
58181
58411
  };
58182
58412
  }
@@ -58260,10 +58490,12 @@ function computeInterferenceAudit(doc, engine, opts) {
58260
58490
  return { error: e instanceof Error ? e.message : String(e) };
58261
58491
  }
58262
58492
  let poses = [];
58493
+ let sweepWarnings;
58263
58494
  let transforms = [void 0];
58264
58495
  if (opts.sweep && opts.sweep.length > 0) {
58265
- const { axes, error: error2 } = resolveSweepAxes(doc, opts.sweep);
58496
+ const { axes, warnings, error: error2 } = resolveSweepAxes(doc, opts.sweep);
58266
58497
  if (error2 || !axes) return { error: error2 ?? "sweep could not be resolved" };
58498
+ sweepWarnings = warnings;
58267
58499
  poses = poseGrid(axes);
58268
58500
  try {
58269
58501
  transforms = poseTransforms(doc, poses);
@@ -58354,7 +58586,8 @@ function computeInterferenceAudit(doc, engine, opts) {
58354
58586
  queries,
58355
58587
  ...poses.length > 0 ? { poses_checked: poses.length } : {},
58356
58588
  findings,
58357
- unresolved_ignores: [...new Set(unresolved)]
58589
+ unresolved_ignores: [...new Set(unresolved)],
58590
+ ...sweepWarnings && sweepWarnings.length > 0 ? { sweep_warnings: sweepWarnings } : {}
58358
58591
  }
58359
58592
  };
58360
58593
  }
@@ -58372,6 +58605,7 @@ async function checkClearance(args, engine) {
58372
58605
  const groupB = stringArray(args.group_b);
58373
58606
  const label = typeof args.label === "string" && args.label.trim() ? args.label.trim() : void 0;
58374
58607
  const allowContact = asBool(args.allow_contact);
58608
+ const includeSamples = asBool(args.include_samples);
58375
58609
  const { sweep, error: sweepParseError } = parseSweep(args.sweep);
58376
58610
  if (sweepParseError) return err3(sweepParseError);
58377
58611
  if (label && args.joint_state !== void 0 && args.joint_state !== null) {
@@ -58427,6 +58661,11 @@ async function checkClearance(args, engine) {
58427
58661
  worst_pair: result.worst_pair,
58428
58662
  ...result.worst_pose ? { worst_pose: result.worst_pose } : {},
58429
58663
  ...result.poses_checked !== void 0 ? { poses_checked: result.poses_checked } : {},
58664
+ ...result.sweep_warnings ? { sweep_warnings: result.sweep_warnings } : {},
58665
+ ...includeSamples && result.poses_checked !== void 0 ? result.samples ? { samples: result.samples } : {
58666
+ samples_omitted: true,
58667
+ 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.`
58668
+ } : {},
58430
58669
  pairs_checked: result.pairs_checked,
58431
58670
  group_a: result.group_a,
58432
58671
  group_b: result.group_b,
@@ -58443,30 +58682,6 @@ async function checkClearance(args, engine) {
58443
58682
  }
58444
58683
  };
58445
58684
  }
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
58685
  function parseIgnorePairs(raw) {
58471
58686
  if (raw === void 0 || raw === null) return { pairs: [] };
58472
58687
  if (!Array.isArray(raw)) {
@@ -58510,6 +58725,7 @@ function auditClearance(documentId, doc, engine, args, sweep, label, pose) {
58510
58725
  pairs_broadphase: result.pairs_broadphase,
58511
58726
  queries: result.queries,
58512
58727
  ...result.poses_checked !== void 0 ? { poses_checked: result.poses_checked } : {},
58728
+ ...result.sweep_warnings ? { sweep_warnings: result.sweep_warnings } : {},
58513
58729
  ...result.unresolved_ignores.length > 0 ? {
58514
58730
  unresolved_ignores: result.unresolved_ignores,
58515
58731
  note: `ignore_pairs named parts that do not exist: ${result.unresolved_ignores.join(", ")} \u2014 those pairs were NOT whitelisted.`
@@ -58674,16 +58890,17 @@ function err3(text) {
58674
58890
  isError: true
58675
58891
  };
58676
58892
  }
58677
- var CLEARANCE_CLAIM_PREFIX, MECH_DOMAIN2, CLEARANCE_ORACLE, STALE_EPS_MM, CONTACT_EPS_MM, round6, checkClearanceSchema, MAX_SWEEP_POSES, pairKey, toolDefs11;
58893
+ var CLEARANCE_CLAIM_PREFIX, MECH_DOMAIN2, CLEARANCE_ORACLE, STALE_EPS_MM, CONTACT_EPS_MM, checkClearanceSchema, pairKey, toolDefs11;
58678
58894
  var init_clearance = __esm({
58679
58895
  "src/tools/clearance.ts"() {
58680
58896
  "use strict";
58681
58897
  init_tool_def();
58682
- init_dist();
58683
58898
  init_receipt_unified();
58684
58899
  init_pose();
58685
58900
  init_session_core();
58686
58901
  init_arg_coerce();
58902
+ init_sweep();
58903
+ init_sweep();
58687
58904
  CLEARANCE_CLAIM_PREFIX = "mech.clearance.";
58688
58905
  MECH_DOMAIN2 = "mechanical";
58689
58906
  CLEARANCE_ORACLE = {
@@ -58692,7 +58909,6 @@ var init_clearance = __esm({
58692
58909
  };
58693
58910
  STALE_EPS_MM = 1e-6;
58694
58911
  CONTACT_EPS_MM = 1e-3;
58695
- round6 = (v) => Math.round(v * 1e6) / 1e6;
58696
58912
  checkClearanceSchema = {
58697
58913
  type: "object",
58698
58914
  properties: {
@@ -58723,19 +58939,10 @@ var init_clearance = __esm({
58723
58939
  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
58940
  },
58725
58941
  joint_state: jointStateSchemaProp,
58726
- sweep: {
58727
- type: "array",
58728
- 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.",
58729
- items: {
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
- }
58942
+ sweep: sweepSchemaProp,
58943
+ include_samples: {
58944
+ type: "boolean",
58945
+ 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
58946
  },
58740
58947
  ignore_pairs: {
58741
58948
  type: "array",
@@ -58756,13 +58963,12 @@ var init_clearance = __esm({
58756
58963
  },
58757
58964
  required: ["document_id"]
58758
58965
  };
58759
- MAX_SWEEP_POSES = 4096;
58760
58966
  pairKey = (a, b) => a < b ? `${a}\0${b}` : `${b}\0${a}`;
58761
58967
  toolDefs11 = [
58762
58968
  {
58763
58969
  name: "check_clearance",
58764
58970
  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'.",
58971
+ 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
58972
  inputSchema: checkClearanceSchema,
58767
58973
  handler: (a, c) => checkClearance(a, c.engine),
58768
58974
  behavior: behavior({ writesDoc: true })
@@ -84052,8 +84258,8 @@ var init_server3 = __esm({
84052
84258
  init_order_feed();
84053
84259
  init_animate();
84054
84260
  PKG_VERSION = (() => {
84055
- if ("0.9.4-main.74") {
84056
- return "0.9.4-main.74";
84261
+ if ("0.9.4-main.75") {
84262
+ return "0.9.4-main.75";
84057
84263
  }
84058
84264
  try {
84059
84265
  const req = createRequire2(import.meta.url);
@@ -84062,8 +84268,8 @@ var init_server3 = __esm({
84062
84268
  return "0.0.0";
84063
84269
  }
84064
84270
  })();
84065
- BUILD_SHA = "cad60a705d41503617de2b556150aaeae3e893c8";
84066
- BUILD_TIME = "2026-08-19T15:53:22.260Z";
84271
+ BUILD_SHA = "89230349e9d445891fbcb1efd7f0cc9425ba972e";
84272
+ BUILD_TIME = "2026-08-25T23:30:10.326Z";
84067
84273
  SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
84068
84274
  VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
84069
84275
  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.74",
3
+ "version": "0.9.4-main.75",
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": "cad60a705d41503617de2b556150aaeae3e893c8",
23
- "builtAt": "2026-08-19T15:53:22.260Z"
22
+ "sha": "89230349e9d445891fbcb1efd7f0cc9425ba972e",
23
+ "builtAt": "2026-08-25T23:30:10.326Z"
24
24
  }
25
25
  }
Binary file