@vcad/mcp 0.9.4-main.73 → 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 +1 -1
- package/index.mjs +471 -196
- 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 89230349e9d445891fbcb1efd7f0cc9425ba972e at 2026-08-25T23:30:10.326Z. Source: https://github.com/ecto/vcad
|
package/index.mjs
CHANGED
|
@@ -225,6 +225,10 @@ __export(vcad_kernel_wasm_exports, {
|
|
|
225
225
|
op_shell: () => op_shell,
|
|
226
226
|
op_sweep_helix: () => op_sweep_helix,
|
|
227
227
|
op_sweep_line: () => op_sweep_line,
|
|
228
|
+
parseAltiumAsciiPcb: () => parseAltiumAsciiPcb,
|
|
229
|
+
parseAltiumPcbDoc: () => parseAltiumPcbDoc,
|
|
230
|
+
parseAltiumPcbLib: () => parseAltiumPcbLib,
|
|
231
|
+
parseEagleBrd: () => parseEagleBrd,
|
|
228
232
|
parseKicadPcb: () => parseKicadPcb,
|
|
229
233
|
parseVCode: () => parseVCode,
|
|
230
234
|
parseVcadFile: () => parseVcadFile,
|
|
@@ -2443,6 +2447,42 @@ function op_sweep_line(profile_json, start, end, twist_angle, scale_start, scale
|
|
|
2443
2447
|
}
|
|
2444
2448
|
return Solid.__wrap(ret[0]);
|
|
2445
2449
|
}
|
|
2450
|
+
function parseAltiumAsciiPcb(content) {
|
|
2451
|
+
const ptr0 = passStringToWasm0(content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
2452
|
+
const len0 = WASM_VECTOR_LEN;
|
|
2453
|
+
const ret = wasm.parseAltiumAsciiPcb(ptr0, len0);
|
|
2454
|
+
if (ret[2]) {
|
|
2455
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
2456
|
+
}
|
|
2457
|
+
return takeFromExternrefTable0(ret[0]);
|
|
2458
|
+
}
|
|
2459
|
+
function parseAltiumPcbDoc(bytes) {
|
|
2460
|
+
const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_malloc);
|
|
2461
|
+
const len0 = WASM_VECTOR_LEN;
|
|
2462
|
+
const ret = wasm.parseAltiumPcbDoc(ptr0, len0);
|
|
2463
|
+
if (ret[2]) {
|
|
2464
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
2465
|
+
}
|
|
2466
|
+
return takeFromExternrefTable0(ret[0]);
|
|
2467
|
+
}
|
|
2468
|
+
function parseAltiumPcbLib(bytes) {
|
|
2469
|
+
const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_malloc);
|
|
2470
|
+
const len0 = WASM_VECTOR_LEN;
|
|
2471
|
+
const ret = wasm.parseAltiumPcbLib(ptr0, len0);
|
|
2472
|
+
if (ret[2]) {
|
|
2473
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
2474
|
+
}
|
|
2475
|
+
return takeFromExternrefTable0(ret[0]);
|
|
2476
|
+
}
|
|
2477
|
+
function parseEagleBrd(content) {
|
|
2478
|
+
const ptr0 = passStringToWasm0(content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
2479
|
+
const len0 = WASM_VECTOR_LEN;
|
|
2480
|
+
const ret = wasm.parseEagleBrd(ptr0, len0);
|
|
2481
|
+
if (ret[2]) {
|
|
2482
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
2483
|
+
}
|
|
2484
|
+
return takeFromExternrefTable0(ret[0]);
|
|
2485
|
+
}
|
|
2446
2486
|
function parseKicadPcb(content) {
|
|
2447
2487
|
const ptr0 = passStringToWasm0(content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
2448
2488
|
const len0 = WASM_VECTOR_LEN;
|
|
@@ -4665,7 +4705,7 @@ function __wbg_get_imports() {
|
|
|
4665
4705
|
const a = state0.a;
|
|
4666
4706
|
state0.a = 0;
|
|
4667
4707
|
try {
|
|
4668
|
-
return
|
|
4708
|
+
return wasm_bindgen_22639e9cee8fd77e___convert__closures_____invoke___wasm_bindgen_22639e9cee8fd77e___JsValue__wasm_bindgen_22639e9cee8fd77e___JsValue_____(a, state0.b, arg02, arg12);
|
|
4669
4709
|
} finally {
|
|
4670
4710
|
state0.a = a;
|
|
4671
4711
|
}
|
|
@@ -5300,11 +5340,11 @@ function __wbg_get_imports() {
|
|
|
5300
5340
|
arg0.writeTexture(arg1, arg2, arg3, arg4);
|
|
5301
5341
|
},
|
|
5302
5342
|
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
|
5303
|
-
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_____);
|
|
5304
5344
|
return ret;
|
|
5305
5345
|
},
|
|
5306
5346
|
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
|
5307
|
-
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_____);
|
|
5308
5348
|
return ret;
|
|
5309
5349
|
},
|
|
5310
5350
|
__wbindgen_cast_0000000000000003: function(arg0) {
|
|
@@ -5378,14 +5418,14 @@ function __wbg_get_imports() {
|
|
|
5378
5418
|
"./vcad_kernel_wasm_bg.js": import0
|
|
5379
5419
|
};
|
|
5380
5420
|
}
|
|
5381
|
-
function
|
|
5382
|
-
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);
|
|
5383
5423
|
}
|
|
5384
|
-
function
|
|
5385
|
-
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);
|
|
5386
5426
|
}
|
|
5387
|
-
function
|
|
5388
|
-
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);
|
|
5389
5429
|
}
|
|
5390
5430
|
function addToExternrefTable0(obj) {
|
|
5391
5431
|
const idx = wasm.__externref_table_alloc();
|
|
@@ -10890,25 +10930,25 @@ function importBindings(wasm3) {
|
|
|
10890
10930
|
parseEagleBrd: (content) => bind("parseEagleBrd")(content)
|
|
10891
10931
|
};
|
|
10892
10932
|
}
|
|
10893
|
-
async function
|
|
10933
|
+
async function parseEagleBrd2(content) {
|
|
10894
10934
|
const wasm3 = await loadEcadWasm();
|
|
10895
10935
|
if (!wasm3)
|
|
10896
10936
|
return null;
|
|
10897
10937
|
return importBindings(wasm3).parseEagleBrd(content);
|
|
10898
10938
|
}
|
|
10899
|
-
async function
|
|
10939
|
+
async function parseAltiumAsciiPcb2(content) {
|
|
10900
10940
|
const wasm3 = await loadEcadWasm();
|
|
10901
10941
|
if (!wasm3)
|
|
10902
10942
|
return null;
|
|
10903
10943
|
return importBindings(wasm3).parseAltiumAsciiPcb(content);
|
|
10904
10944
|
}
|
|
10905
|
-
async function
|
|
10945
|
+
async function parseAltiumPcbDoc2(bytes) {
|
|
10906
10946
|
const wasm3 = await loadEcadWasm();
|
|
10907
10947
|
if (!wasm3)
|
|
10908
10948
|
return null;
|
|
10909
10949
|
return importBindings(wasm3).parseAltiumPcbDoc(bytes);
|
|
10910
10950
|
}
|
|
10911
|
-
async function
|
|
10951
|
+
async function parseAltiumPcbLib2(bytes) {
|
|
10912
10952
|
const wasm3 = await loadEcadWasm();
|
|
10913
10953
|
if (!wasm3)
|
|
10914
10954
|
return null;
|
|
@@ -11759,11 +11799,11 @@ __export(dist_exports, {
|
|
|
11759
11799
|
mountingHolesFromPcb: () => mountingHolesFromPcb,
|
|
11760
11800
|
netContinuity: () => netContinuity,
|
|
11761
11801
|
outlineAabb: () => outlineAabb,
|
|
11762
|
-
parseAltiumAsciiPcb: () =>
|
|
11763
|
-
parseAltiumPcbDoc: () =>
|
|
11764
|
-
parseAltiumPcbLib: () =>
|
|
11802
|
+
parseAltiumAsciiPcb: () => parseAltiumAsciiPcb2,
|
|
11803
|
+
parseAltiumPcbDoc: () => parseAltiumPcbDoc2,
|
|
11804
|
+
parseAltiumPcbLib: () => parseAltiumPcbLib2,
|
|
11765
11805
|
parseBindingKey: () => parseBindingKey,
|
|
11766
|
-
parseEagleBrd: () =>
|
|
11806
|
+
parseEagleBrd: () => parseEagleBrd2,
|
|
11767
11807
|
parseExpression: () => parse,
|
|
11768
11808
|
parseKicadPcb: () => parseKicadPcb2,
|
|
11769
11809
|
parseXyz: () => parseXyz,
|
|
@@ -41591,6 +41631,76 @@ var init_CHANGELOG = __esm({
|
|
|
41591
41631
|
CHANGELOG_default = {
|
|
41592
41632
|
$schema: "./changelog.schema.json",
|
|
41593
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
|
+
},
|
|
41670
|
+
{
|
|
41671
|
+
id: "2026-08-19-loon-modules-and-root-cache",
|
|
41672
|
+
version: "0.9.4",
|
|
41673
|
+
date: "2026-08-19",
|
|
41674
|
+
category: "feat",
|
|
41675
|
+
title: "Loon lib path and a content-addressed evaluation cache",
|
|
41676
|
+
summary: "[use] modules resolve next to the file then on $VCAD_LOON_PATH, identically in vcad, vcad-render and MCP; evaluated root meshes are cached on disk keyed by expression + kernel build.",
|
|
41677
|
+
features: [
|
|
41678
|
+
"loon",
|
|
41679
|
+
"modules",
|
|
41680
|
+
"cache",
|
|
41681
|
+
"cli",
|
|
41682
|
+
"render"
|
|
41683
|
+
],
|
|
41684
|
+
mcpTools: [
|
|
41685
|
+
"load_document",
|
|
41686
|
+
"create_cad_loon"
|
|
41687
|
+
]
|
|
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
|
+
},
|
|
41594
41704
|
{
|
|
41595
41705
|
id: "2026-08-17-altium-pcb-import",
|
|
41596
41706
|
version: "0.9.4",
|
|
@@ -50177,7 +50287,7 @@ var init_source_provenance = __esm({
|
|
|
50177
50287
|
|
|
50178
50288
|
// src/tools/loon.ts
|
|
50179
50289
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
50180
|
-
import { isAbsolute as isAbsolute2, join as join2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
50290
|
+
import { delimiter, isAbsolute as isAbsolute2, join as join2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
50181
50291
|
function composeLoonProgram(input) {
|
|
50182
50292
|
const { source, use_loons, loons } = input;
|
|
50183
50293
|
const names = [
|
|
@@ -50208,12 +50318,18 @@ function readModule(base, name) {
|
|
|
50208
50318
|
}
|
|
50209
50319
|
return null;
|
|
50210
50320
|
}
|
|
50321
|
+
function loonLibDirs(env = process.env) {
|
|
50322
|
+
const raw = env.VCAD_LOON_PATH;
|
|
50323
|
+
if (!raw) return [];
|
|
50324
|
+
return raw.split(delimiter).filter((d) => d.length > 0);
|
|
50325
|
+
}
|
|
50211
50326
|
function composeLoonModules(input) {
|
|
50212
50327
|
const { source, loons, modules, base_dir } = input;
|
|
50213
50328
|
const map = {};
|
|
50214
50329
|
for (const m of loons ?? []) map[m.name] = m.source;
|
|
50215
50330
|
Object.assign(map, modules ?? {});
|
|
50216
|
-
|
|
50331
|
+
const dirs = [...base_dir ? [base_dir] : [], ...loonLibDirs()];
|
|
50332
|
+
if (dirs.length) {
|
|
50217
50333
|
const pending2 = [
|
|
50218
50334
|
...importedNames(source),
|
|
50219
50335
|
...Object.values(map).flatMap(importedNames)
|
|
@@ -50223,7 +50339,11 @@ function composeLoonModules(input) {
|
|
|
50223
50339
|
const name = pending2.pop();
|
|
50224
50340
|
if (seen.has(name) || map[name]) continue;
|
|
50225
50341
|
seen.add(name);
|
|
50226
|
-
|
|
50342
|
+
let src = null;
|
|
50343
|
+
for (const dir of dirs) {
|
|
50344
|
+
src = readModule(dir, name);
|
|
50345
|
+
if (src !== null) break;
|
|
50346
|
+
}
|
|
50227
50347
|
if (src === null) continue;
|
|
50228
50348
|
map[name] = src;
|
|
50229
50349
|
pending2.push(...importedNames(src));
|
|
@@ -53142,8 +53262,129 @@ var init_inspect = __esm({
|
|
|
53142
53262
|
}
|
|
53143
53263
|
});
|
|
53144
53264
|
|
|
53145
|
-
// src/tools/
|
|
53146
|
-
function
|
|
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) {
|
|
53147
53388
|
const scene = engine.evaluate(doc);
|
|
53148
53389
|
const visibleRoots = doc.roots.filter((e) => e.visible !== false);
|
|
53149
53390
|
const partMaterials = doc.part_materials;
|
|
@@ -53151,31 +53392,78 @@ function evaluateParts(doc, engine) {
|
|
|
53151
53392
|
for (let i = 0; i < scene.parts.length && i < visibleRoots.length; i++) {
|
|
53152
53393
|
const root = visibleRoots[i];
|
|
53153
53394
|
const rootId = String(root.root);
|
|
53395
|
+
const node = doc.nodes[rootId];
|
|
53154
53396
|
const mesh = scene.parts[i].mesh;
|
|
53155
53397
|
if (!mesh || mesh.positions.length === 0) continue;
|
|
53156
|
-
const node = doc.nodes[rootId];
|
|
53157
53398
|
out.push({
|
|
53158
53399
|
id: rootId,
|
|
53159
53400
|
name: node?.name ?? void 0,
|
|
53160
53401
|
material: root.material ?? partMaterials?.[rootId] ?? void 0,
|
|
53161
|
-
mesh
|
|
53402
|
+
localMesh: mesh
|
|
53162
53403
|
});
|
|
53163
53404
|
}
|
|
53164
53405
|
for (const inst of scene.instances ?? []) {
|
|
53165
|
-
|
|
53166
|
-
translate: inst.transform.translation,
|
|
53167
|
-
rotate: inst.transform.rotation,
|
|
53168
|
-
scale: inst.transform.scale
|
|
53169
|
-
}) : inst.mesh;
|
|
53170
|
-
if (!mesh || mesh.positions.length === 0) continue;
|
|
53406
|
+
if (!inst.mesh || inst.mesh.positions.length === 0) continue;
|
|
53171
53407
|
out.push({
|
|
53172
53408
|
id: inst.instanceId,
|
|
53173
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 } : {},
|
|
53174
53427
|
mesh
|
|
53175
53428
|
});
|
|
53176
53429
|
}
|
|
53177
53430
|
return out;
|
|
53178
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
|
+
}
|
|
53179
53467
|
function findPart(candidates, wanted) {
|
|
53180
53468
|
return candidates.find((c) => c.id === wanted) ?? candidates.find((c) => c.name === wanted);
|
|
53181
53469
|
}
|
|
@@ -53325,6 +53613,96 @@ function measureResult(doc, engine, partIds) {
|
|
|
53325
53613
|
note: "Distance is negative when the parts overlap (penetration depth). Measured from evaluated tessellations \u2014 accuracy is tessellation-bound."
|
|
53326
53614
|
};
|
|
53327
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
|
+
}
|
|
53328
53706
|
function measure(args, engine) {
|
|
53329
53707
|
const documentId = String(args.document_id ?? "");
|
|
53330
53708
|
if (!documentId) {
|
|
@@ -53336,20 +53714,33 @@ function measure(args, engine) {
|
|
|
53336
53714
|
"Pass `part_ids` as an array of one or two part ids (or part names)."
|
|
53337
53715
|
);
|
|
53338
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
|
+
}
|
|
53339
53725
|
let doc;
|
|
53726
|
+
let pose;
|
|
53340
53727
|
try {
|
|
53341
|
-
doc =
|
|
53728
|
+
({ doc, pose } = applyJointState(stored, args.joint_state));
|
|
53342
53729
|
} catch (e) {
|
|
53343
53730
|
return err(e instanceof Error ? e.message : String(e));
|
|
53344
53731
|
}
|
|
53345
53732
|
let payload;
|
|
53346
53733
|
try {
|
|
53347
|
-
payload = measureResult(doc, engine, partIds);
|
|
53734
|
+
payload = sweep && sweep.length > 0 ? measureSweepResult(doc, engine, partIds, sweep) : measureResult(doc, engine, partIds);
|
|
53348
53735
|
} catch (e) {
|
|
53349
53736
|
if (e instanceof MeasureError) return err(e.message);
|
|
53350
53737
|
throw e;
|
|
53351
53738
|
}
|
|
53352
|
-
const body = {
|
|
53739
|
+
const body = {
|
|
53740
|
+
document_id: documentId,
|
|
53741
|
+
...pose ? { pose } : {},
|
|
53742
|
+
...payload
|
|
53743
|
+
};
|
|
53353
53744
|
return {
|
|
53354
53745
|
content: [{ type: "text", text: JSON.stringify(body) }],
|
|
53355
53746
|
structuredContent: { measure: body, document_id: documentId }
|
|
@@ -53365,10 +53756,11 @@ var r6, point, MeasureError, measureSchema, toolDefs6;
|
|
|
53365
53756
|
var init_measure = __esm({
|
|
53366
53757
|
"src/tools/measure.ts"() {
|
|
53367
53758
|
"use strict";
|
|
53368
|
-
init_dist();
|
|
53369
53759
|
init_inspect();
|
|
53370
53760
|
init_session_core();
|
|
53371
53761
|
init_tool_def();
|
|
53762
|
+
init_pose();
|
|
53763
|
+
init_sweep();
|
|
53372
53764
|
r6 = (v) => Math.round(v * 1e6) / 1e6;
|
|
53373
53765
|
point = (p) => ({
|
|
53374
53766
|
x: r6(p.x),
|
|
@@ -53388,7 +53780,9 @@ var init_measure = __esm({
|
|
|
53388
53780
|
type: "array",
|
|
53389
53781
|
items: { type: "string" },
|
|
53390
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."
|
|
53391
|
-
}
|
|
53783
|
+
},
|
|
53784
|
+
joint_state: jointStateSchemaProp,
|
|
53785
|
+
sweep: sweepSchemaProp
|
|
53392
53786
|
},
|
|
53393
53787
|
required: ["document_id", "part_ids"]
|
|
53394
53788
|
};
|
|
@@ -53396,7 +53790,7 @@ var init_measure = __esm({
|
|
|
53396
53790
|
{
|
|
53397
53791
|
name: "measure",
|
|
53398
53792
|
pack: null,
|
|
53399
|
-
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
|
|
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.",
|
|
53400
53794
|
inputSchema: measureSchema,
|
|
53401
53795
|
handler: (a, c) => measure(a, c.engine),
|
|
53402
53796
|
behavior: behavior({})
|
|
@@ -57947,51 +58341,6 @@ function clearanceHolds(distanceMm, minMm, allowContact, intersecting = false) {
|
|
|
57947
58341
|
if (intersecting) return false;
|
|
57948
58342
|
return distanceMm >= minMm || allowContact && clearanceVerdict(distanceMm) === "touching";
|
|
57949
58343
|
}
|
|
57950
|
-
function worseThan(a, b) {
|
|
57951
|
-
if (a.intersecting !== b.intersecting) return a.intersecting;
|
|
57952
|
-
return a.distance < b.distance;
|
|
57953
|
-
}
|
|
57954
|
-
function placeMesh(mesh, transform2) {
|
|
57955
|
-
if (!transform2) return mesh;
|
|
57956
|
-
return transformMesh(mesh, {
|
|
57957
|
-
translate: transform2.translation,
|
|
57958
|
-
rotate: transform2.rotation,
|
|
57959
|
-
scale: transform2.scale
|
|
57960
|
-
});
|
|
57961
|
-
}
|
|
57962
|
-
function poseableParts(doc, engine) {
|
|
57963
|
-
const scene = engine.evaluate(doc);
|
|
57964
|
-
const visibleRoots = doc.roots.filter((e) => e.visible !== false);
|
|
57965
|
-
const out = [];
|
|
57966
|
-
for (let i = 0; i < scene.parts.length && i < visibleRoots.length; i++) {
|
|
57967
|
-
const rootId = visibleRoots[i].root;
|
|
57968
|
-
const node = doc.nodes[String(rootId)];
|
|
57969
|
-
const mesh = scene.parts[i].mesh;
|
|
57970
|
-
if (!mesh || mesh.positions.length === 0) continue;
|
|
57971
|
-
out.push({ id: String(rootId), name: node?.name ?? void 0, localMesh: mesh });
|
|
57972
|
-
}
|
|
57973
|
-
for (const inst of scene.instances ?? []) {
|
|
57974
|
-
if (!inst.mesh || inst.mesh.positions.length === 0) continue;
|
|
57975
|
-
out.push({
|
|
57976
|
-
id: inst.instanceId,
|
|
57977
|
-
name: inst.name ?? void 0,
|
|
57978
|
-
localMesh: inst.mesh,
|
|
57979
|
-
transform: inst.transform ?? void 0,
|
|
57980
|
-
instanceId: inst.instanceId
|
|
57981
|
-
});
|
|
57982
|
-
}
|
|
57983
|
-
return out;
|
|
57984
|
-
}
|
|
57985
|
-
function placeParts(parts, worldTransforms) {
|
|
57986
|
-
const out = [];
|
|
57987
|
-
for (const p of parts) {
|
|
57988
|
-
const transform2 = (p.instanceId ? worldTransforms?.get(p.instanceId) : void 0) ?? p.transform;
|
|
57989
|
-
const mesh = placeMesh(p.localMesh, transform2);
|
|
57990
|
-
if (!mesh || mesh.positions.length === 0) continue;
|
|
57991
|
-
out.push({ id: p.id, ...p.name ? { name: p.name } : {}, mesh });
|
|
57992
|
-
}
|
|
57993
|
-
return out;
|
|
57994
|
-
}
|
|
57995
58344
|
function resolveGroup(candidates, ids) {
|
|
57996
58345
|
const parts = /* @__PURE__ */ new Map();
|
|
57997
58346
|
const missing = [];
|
|
@@ -58003,67 +58352,6 @@ function resolveGroup(candidates, ids) {
|
|
|
58003
58352
|
}
|
|
58004
58353
|
return { parts: [...parts.values()], missing };
|
|
58005
58354
|
}
|
|
58006
|
-
function resolveSweepAxes(doc, axes) {
|
|
58007
|
-
const joints = doc.joints ?? [];
|
|
58008
|
-
const resolved = [];
|
|
58009
|
-
for (const axis of axes) {
|
|
58010
|
-
const wanted = String(axis.joint);
|
|
58011
|
-
const found = joints.find((j) => j.id === wanted) ?? joints.find((j) => j.name === wanted);
|
|
58012
|
-
if (!found) {
|
|
58013
|
-
const available = joints.map((j) => `${j.id}${j.name ? ` (${j.name})` : ""}`).join(", ");
|
|
58014
|
-
return { error: `No joint with id or name "${wanted}". Available: ${available || "none"}` };
|
|
58015
|
-
}
|
|
58016
|
-
if (!Number.isFinite(axis.from) || !Number.isFinite(axis.to)) {
|
|
58017
|
-
return { error: `Sweep axis "${wanted}" needs finite \`from\` and \`to\`.` };
|
|
58018
|
-
}
|
|
58019
|
-
const steps = Math.trunc(axis.steps);
|
|
58020
|
-
if (!Number.isFinite(steps) || steps < 1) {
|
|
58021
|
-
return { error: `Sweep axis "${wanted}" needs \`steps\` >= 1.` };
|
|
58022
|
-
}
|
|
58023
|
-
resolved.push({ joint: found.id, from: axis.from, to: axis.to, steps });
|
|
58024
|
-
}
|
|
58025
|
-
if (resolved.length === 0) return { error: "`sweep` needs at least one joint axis." };
|
|
58026
|
-
const total = resolved.reduce((n, a) => n * (a.steps + 1), 1);
|
|
58027
|
-
if (total > MAX_SWEEP_POSES) {
|
|
58028
|
-
return {
|
|
58029
|
-
error: `Sweep grid is ${total} poses (limit ${MAX_SWEEP_POSES}). Lower \`steps\` or sweep fewer joints at once.`
|
|
58030
|
-
};
|
|
58031
|
-
}
|
|
58032
|
-
return { axes: resolved };
|
|
58033
|
-
}
|
|
58034
|
-
function poseGrid(axes) {
|
|
58035
|
-
let poses = [[]];
|
|
58036
|
-
for (const axis of axes) {
|
|
58037
|
-
const n = Math.trunc(axis.steps);
|
|
58038
|
-
const next = [];
|
|
58039
|
-
for (const pose of poses) {
|
|
58040
|
-
for (let i = 0; i <= n; i++) {
|
|
58041
|
-
const state = axis.from + (axis.to - axis.from) * i / n;
|
|
58042
|
-
next.push([...pose, { joint: axis.joint, state }]);
|
|
58043
|
-
}
|
|
58044
|
-
}
|
|
58045
|
-
poses = next;
|
|
58046
|
-
}
|
|
58047
|
-
return poses;
|
|
58048
|
-
}
|
|
58049
|
-
function poseTransforms(doc, poses) {
|
|
58050
|
-
const joints = doc.joints ?? [];
|
|
58051
|
-
const byId = new Map(joints.map((j) => [j.id, j]));
|
|
58052
|
-
const saved = joints.map((j) => j.state);
|
|
58053
|
-
try {
|
|
58054
|
-
return poses.map((pose) => {
|
|
58055
|
-
for (const { joint, state } of pose) {
|
|
58056
|
-
const j = byId.get(joint);
|
|
58057
|
-
if (j) j.state = state;
|
|
58058
|
-
}
|
|
58059
|
-
return solveForwardKinematics2(doc);
|
|
58060
|
-
});
|
|
58061
|
-
} finally {
|
|
58062
|
-
joints.forEach((j, i) => {
|
|
58063
|
-
j.state = saved[i];
|
|
58064
|
-
});
|
|
58065
|
-
}
|
|
58066
|
-
}
|
|
58067
58355
|
function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
|
|
58068
58356
|
if (groupA.length === 0 || groupB.length === 0) {
|
|
58069
58357
|
return { error: "Both `group_a` and `group_b` need at least one part id." };
|
|
@@ -58077,7 +58365,7 @@ function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
|
|
|
58077
58365
|
if (!sweep || sweep.length === 0) {
|
|
58078
58366
|
return measureGroups(engine, placeParts(base), groupA, groupB);
|
|
58079
58367
|
}
|
|
58080
|
-
const { axes, error: sweepError } = resolveSweepAxes(doc, sweep);
|
|
58368
|
+
const { axes, warnings, error: sweepError } = resolveSweepAxes(doc, sweep);
|
|
58081
58369
|
if (sweepError || !axes) return { error: sweepError ?? "sweep could not be resolved" };
|
|
58082
58370
|
const poses = poseGrid(axes);
|
|
58083
58371
|
let transforms;
|
|
@@ -58087,6 +58375,8 @@ function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
|
|
|
58087
58375
|
return { error: e instanceof Error ? e.message : String(e) };
|
|
58088
58376
|
}
|
|
58089
58377
|
let worst2;
|
|
58378
|
+
const samples = [];
|
|
58379
|
+
const keepSamples = poses.length <= MAX_SWEEP_SAMPLES;
|
|
58090
58380
|
for (let i = 0; i < poses.length; i++) {
|
|
58091
58381
|
const { result, error: error2 } = measureGroups(
|
|
58092
58382
|
engine,
|
|
@@ -58095,6 +58385,13 @@ function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
|
|
|
58095
58385
|
groupB
|
|
58096
58386
|
);
|
|
58097
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
|
+
}
|
|
58098
58395
|
if (!worst2 || worseThan(
|
|
58099
58396
|
{ distance: result.distance_mm, intersecting: result.intersecting },
|
|
58100
58397
|
{ distance: worst2.result.distance_mm, intersecting: worst2.result.intersecting }
|
|
@@ -58106,8 +58403,10 @@ function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
|
|
|
58106
58403
|
return {
|
|
58107
58404
|
result: {
|
|
58108
58405
|
...worst2.result,
|
|
58109
|
-
worst_pose: worst2.pose
|
|
58110
|
-
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 } : {}
|
|
58111
58410
|
}
|
|
58112
58411
|
};
|
|
58113
58412
|
}
|
|
@@ -58191,10 +58490,12 @@ function computeInterferenceAudit(doc, engine, opts) {
|
|
|
58191
58490
|
return { error: e instanceof Error ? e.message : String(e) };
|
|
58192
58491
|
}
|
|
58193
58492
|
let poses = [];
|
|
58493
|
+
let sweepWarnings;
|
|
58194
58494
|
let transforms = [void 0];
|
|
58195
58495
|
if (opts.sweep && opts.sweep.length > 0) {
|
|
58196
|
-
const { axes, error: error2 } = resolveSweepAxes(doc, opts.sweep);
|
|
58496
|
+
const { axes, warnings, error: error2 } = resolveSweepAxes(doc, opts.sweep);
|
|
58197
58497
|
if (error2 || !axes) return { error: error2 ?? "sweep could not be resolved" };
|
|
58498
|
+
sweepWarnings = warnings;
|
|
58198
58499
|
poses = poseGrid(axes);
|
|
58199
58500
|
try {
|
|
58200
58501
|
transforms = poseTransforms(doc, poses);
|
|
@@ -58285,7 +58586,8 @@ function computeInterferenceAudit(doc, engine, opts) {
|
|
|
58285
58586
|
queries,
|
|
58286
58587
|
...poses.length > 0 ? { poses_checked: poses.length } : {},
|
|
58287
58588
|
findings,
|
|
58288
|
-
unresolved_ignores: [...new Set(unresolved)]
|
|
58589
|
+
unresolved_ignores: [...new Set(unresolved)],
|
|
58590
|
+
...sweepWarnings && sweepWarnings.length > 0 ? { sweep_warnings: sweepWarnings } : {}
|
|
58289
58591
|
}
|
|
58290
58592
|
};
|
|
58291
58593
|
}
|
|
@@ -58303,6 +58605,7 @@ async function checkClearance(args, engine) {
|
|
|
58303
58605
|
const groupB = stringArray(args.group_b);
|
|
58304
58606
|
const label = typeof args.label === "string" && args.label.trim() ? args.label.trim() : void 0;
|
|
58305
58607
|
const allowContact = asBool(args.allow_contact);
|
|
58608
|
+
const includeSamples = asBool(args.include_samples);
|
|
58306
58609
|
const { sweep, error: sweepParseError } = parseSweep(args.sweep);
|
|
58307
58610
|
if (sweepParseError) return err3(sweepParseError);
|
|
58308
58611
|
if (label && args.joint_state !== void 0 && args.joint_state !== null) {
|
|
@@ -58358,6 +58661,11 @@ async function checkClearance(args, engine) {
|
|
|
58358
58661
|
worst_pair: result.worst_pair,
|
|
58359
58662
|
...result.worst_pose ? { worst_pose: result.worst_pose } : {},
|
|
58360
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
|
+
} : {},
|
|
58361
58669
|
pairs_checked: result.pairs_checked,
|
|
58362
58670
|
group_a: result.group_a,
|
|
58363
58671
|
group_b: result.group_b,
|
|
@@ -58374,30 +58682,6 @@ async function checkClearance(args, engine) {
|
|
|
58374
58682
|
}
|
|
58375
58683
|
};
|
|
58376
58684
|
}
|
|
58377
|
-
function parseSweep(raw) {
|
|
58378
|
-
if (raw === void 0 || raw === null) return {};
|
|
58379
|
-
if (!Array.isArray(raw)) {
|
|
58380
|
-
return { error: "`sweep` must be an array of {joint, from, to, steps} axes." };
|
|
58381
|
-
}
|
|
58382
|
-
const sweep = [];
|
|
58383
|
-
for (const entry of raw) {
|
|
58384
|
-
if (!entry || typeof entry !== "object") {
|
|
58385
|
-
return { error: "Each `sweep` axis must be an object {joint, from, to, steps}." };
|
|
58386
|
-
}
|
|
58387
|
-
const e = entry;
|
|
58388
|
-
const joint = typeof e.joint === "string" ? e.joint.trim() : "";
|
|
58389
|
-
const from = Number(e.from);
|
|
58390
|
-
const to = Number(e.to);
|
|
58391
|
-
const steps = Number(e.steps);
|
|
58392
|
-
if (!joint || !Number.isFinite(from) || !Number.isFinite(to) || !Number.isFinite(steps)) {
|
|
58393
|
-
return {
|
|
58394
|
-
error: "Each `sweep` axis needs a `joint` id/name plus numeric `from`, `to`, and `steps`."
|
|
58395
|
-
};
|
|
58396
|
-
}
|
|
58397
|
-
sweep.push({ joint, from, to, steps });
|
|
58398
|
-
}
|
|
58399
|
-
return sweep.length > 0 ? { sweep } : {};
|
|
58400
|
-
}
|
|
58401
58685
|
function parseIgnorePairs(raw) {
|
|
58402
58686
|
if (raw === void 0 || raw === null) return { pairs: [] };
|
|
58403
58687
|
if (!Array.isArray(raw)) {
|
|
@@ -58441,6 +58725,7 @@ function auditClearance(documentId, doc, engine, args, sweep, label, pose) {
|
|
|
58441
58725
|
pairs_broadphase: result.pairs_broadphase,
|
|
58442
58726
|
queries: result.queries,
|
|
58443
58727
|
...result.poses_checked !== void 0 ? { poses_checked: result.poses_checked } : {},
|
|
58728
|
+
...result.sweep_warnings ? { sweep_warnings: result.sweep_warnings } : {},
|
|
58444
58729
|
...result.unresolved_ignores.length > 0 ? {
|
|
58445
58730
|
unresolved_ignores: result.unresolved_ignores,
|
|
58446
58731
|
note: `ignore_pairs named parts that do not exist: ${result.unresolved_ignores.join(", ")} \u2014 those pairs were NOT whitelisted.`
|
|
@@ -58605,16 +58890,17 @@ function err3(text) {
|
|
|
58605
58890
|
isError: true
|
|
58606
58891
|
};
|
|
58607
58892
|
}
|
|
58608
|
-
var CLEARANCE_CLAIM_PREFIX, MECH_DOMAIN2, CLEARANCE_ORACLE, STALE_EPS_MM, CONTACT_EPS_MM,
|
|
58893
|
+
var CLEARANCE_CLAIM_PREFIX, MECH_DOMAIN2, CLEARANCE_ORACLE, STALE_EPS_MM, CONTACT_EPS_MM, checkClearanceSchema, pairKey, toolDefs11;
|
|
58609
58894
|
var init_clearance = __esm({
|
|
58610
58895
|
"src/tools/clearance.ts"() {
|
|
58611
58896
|
"use strict";
|
|
58612
58897
|
init_tool_def();
|
|
58613
|
-
init_dist();
|
|
58614
58898
|
init_receipt_unified();
|
|
58615
58899
|
init_pose();
|
|
58616
58900
|
init_session_core();
|
|
58617
58901
|
init_arg_coerce();
|
|
58902
|
+
init_sweep();
|
|
58903
|
+
init_sweep();
|
|
58618
58904
|
CLEARANCE_CLAIM_PREFIX = "mech.clearance.";
|
|
58619
58905
|
MECH_DOMAIN2 = "mechanical";
|
|
58620
58906
|
CLEARANCE_ORACLE = {
|
|
@@ -58623,7 +58909,6 @@ var init_clearance = __esm({
|
|
|
58623
58909
|
};
|
|
58624
58910
|
STALE_EPS_MM = 1e-6;
|
|
58625
58911
|
CONTACT_EPS_MM = 1e-3;
|
|
58626
|
-
round6 = (v) => Math.round(v * 1e6) / 1e6;
|
|
58627
58912
|
checkClearanceSchema = {
|
|
58628
58913
|
type: "object",
|
|
58629
58914
|
properties: {
|
|
@@ -58654,19 +58939,10 @@ var init_clearance = __esm({
|
|
|
58654
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."
|
|
58655
58940
|
},
|
|
58656
58941
|
joint_state: jointStateSchemaProp,
|
|
58657
|
-
sweep:
|
|
58658
|
-
|
|
58659
|
-
|
|
58660
|
-
|
|
58661
|
-
type: "object",
|
|
58662
|
-
properties: {
|
|
58663
|
-
joint: { type: "string", description: "Joint id or name to drive." },
|
|
58664
|
-
from: { type: "number", description: "Start of travel (degrees for revolute, mm for prismatic)." },
|
|
58665
|
-
to: { type: "number", description: "End of travel." },
|
|
58666
|
-
steps: { type: "number", description: "Number of intervals; steps + 1 poses are sampled." }
|
|
58667
|
-
},
|
|
58668
|
-
required: ["joint", "from", "to", "steps"]
|
|
58669
|
-
}
|
|
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."
|
|
58670
58946
|
},
|
|
58671
58947
|
ignore_pairs: {
|
|
58672
58948
|
type: "array",
|
|
@@ -58687,13 +58963,12 @@ var init_clearance = __esm({
|
|
|
58687
58963
|
},
|
|
58688
58964
|
required: ["document_id"]
|
|
58689
58965
|
};
|
|
58690
|
-
MAX_SWEEP_POSES = 4096;
|
|
58691
58966
|
pairKey = (a, b) => a < b ? `${a}\0${b}` : `${b}\0${a}`;
|
|
58692
58967
|
toolDefs11 = [
|
|
58693
58968
|
{
|
|
58694
58969
|
name: "check_clearance",
|
|
58695
58970
|
pack: null,
|
|
58696
|
-
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'.",
|
|
58697
58972
|
inputSchema: checkClearanceSchema,
|
|
58698
58973
|
handler: (a, c) => checkClearance(a, c.engine),
|
|
58699
58974
|
behavior: behavior({ writesDoc: true })
|
|
@@ -78728,7 +79003,7 @@ async function importAltium(input) {
|
|
|
78728
79003
|
const binary = isCompoundFile(bytes);
|
|
78729
79004
|
let pcb;
|
|
78730
79005
|
try {
|
|
78731
|
-
pcb = binary ? await
|
|
79006
|
+
pcb = binary ? await parseAltiumPcbDoc2(new Uint8Array(bytes)) : await parseAltiumAsciiPcb2(bytes.toString("utf8"));
|
|
78732
79007
|
} catch (e) {
|
|
78733
79008
|
return importError(
|
|
78734
79009
|
`Could not parse the Altium ${binary ? "binary" : "ASCII"} .PcbDoc: ` + (e instanceof Error ? e.message : String(e))
|
|
@@ -78769,7 +79044,7 @@ async function importAltiumLibrary(input) {
|
|
|
78769
79044
|
}
|
|
78770
79045
|
let lib;
|
|
78771
79046
|
try {
|
|
78772
|
-
lib = await
|
|
79047
|
+
lib = await parseAltiumPcbLib2(new Uint8Array(bytes));
|
|
78773
79048
|
} catch (e) {
|
|
78774
79049
|
return importError(
|
|
78775
79050
|
`Could not parse the Altium .PcbLib: ${e instanceof Error ? e.message : String(e)}`
|
|
@@ -78818,7 +79093,7 @@ async function importEagle(input) {
|
|
|
78818
79093
|
}
|
|
78819
79094
|
let pcb;
|
|
78820
79095
|
try {
|
|
78821
|
-
pcb = await
|
|
79096
|
+
pcb = await parseEagleBrd2(content);
|
|
78822
79097
|
} catch (e) {
|
|
78823
79098
|
return importError(
|
|
78824
79099
|
`Could not parse the Eagle .brd: ${e instanceof Error ? e.message : String(e)}`
|
|
@@ -83983,8 +84258,8 @@ var init_server3 = __esm({
|
|
|
83983
84258
|
init_order_feed();
|
|
83984
84259
|
init_animate();
|
|
83985
84260
|
PKG_VERSION = (() => {
|
|
83986
|
-
if ("0.9.4-main.
|
|
83987
|
-
return "0.9.4-main.
|
|
84261
|
+
if ("0.9.4-main.75") {
|
|
84262
|
+
return "0.9.4-main.75";
|
|
83988
84263
|
}
|
|
83989
84264
|
try {
|
|
83990
84265
|
const req = createRequire2(import.meta.url);
|
|
@@ -83993,8 +84268,8 @@ var init_server3 = __esm({
|
|
|
83993
84268
|
return "0.0.0";
|
|
83994
84269
|
}
|
|
83995
84270
|
})();
|
|
83996
|
-
BUILD_SHA = "
|
|
83997
|
-
BUILD_TIME = "2026-08-
|
|
84271
|
+
BUILD_SHA = "89230349e9d445891fbcb1efd7f0cc9425ba972e";
|
|
84272
|
+
BUILD_TIME = "2026-08-25T23:30:10.326Z";
|
|
83998
84273
|
SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
|
|
83999
84274
|
VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
|
|
84000
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.
|
|
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": "
|
|
23
|
-
"builtAt": "2026-08-
|
|
22
|
+
"sha": "89230349e9d445891fbcb1efd7f0cc9425ba972e",
|
|
23
|
+
"builtAt": "2026-08-25T23:30:10.326Z"
|
|
24
24
|
}
|
|
25
25
|
}
|
package/vcad_kernel_wasm_bg.wasm
CHANGED
|
Binary file
|