@vcad/mcp 0.9.4-main.15 → 0.9.4-main.16

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 ba0bbea017e3e720ec8796167b6824f3cdf235cf at 2026-07-24T17:18:07.764Z. Source: https://github.com/ecto/vcad
9
+ Built from 63c1a040cf3352ca640144ec217f3f96271180c9 at 2026-07-24T17:21:48.535Z. Source: https://github.com/ecto/vcad
package/index.mjs CHANGED
@@ -150,6 +150,7 @@ __export(vcad_kernel_wasm_exports, {
150
150
  ecadVerifyReceipt: () => ecadVerifyReceipt,
151
151
  ecadVerifySubstitution: () => ecadVerifySubstitution,
152
152
  emSimulate: () => emSimulate,
153
+ embroideryDesignToMesh: () => embroideryDesignToMesh,
153
154
  estimatePrintCost: () => estimatePrintCost,
154
155
  estimate_cost_for_process: () => estimate_cost_for_process,
155
156
  eulerXyzDegToQuat: () => eulerXyzDegToQuat,
@@ -257,6 +258,7 @@ __export(vcad_kernel_wasm_exports, {
257
258
  toleranceAnalyze: () => toleranceAnalyze,
258
259
  topologyOptimizeBox: () => topologyOptimizeBox,
259
260
  topologyOptimizeMesh: () => topologyOptimizeMesh,
261
+ transformMeshBuffers: () => transformMeshBuffers,
260
262
  writeEmbroideryDst: () => writeEmbroideryDst,
261
263
  writeEmbroideryPes: () => writeEmbroideryPes
262
264
  });
@@ -1519,6 +1521,15 @@ function emSimulate(spec_json, params_json, options_json) {
1519
1521
  }
1520
1522
  return takeFromExternrefTable0(ret[0]);
1521
1523
  }
1524
+ function embroideryDesignToMesh(design_json) {
1525
+ const ptr0 = passStringToWasm0(design_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1526
+ const len0 = WASM_VECTOR_LEN;
1527
+ const ret = wasm.embroideryDesignToMesh(ptr0, len0);
1528
+ if (ret[2]) {
1529
+ throw takeFromExternrefTable0(ret[1]);
1530
+ }
1531
+ return takeFromExternrefTable0(ret[0]);
1532
+ }
1522
1533
  function estimatePrintCost(volume_mm3, infill_density, wall_count, line_width, material_name) {
1523
1534
  const ptr0 = passStringToWasm0(material_name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1524
1535
  const len0 = WASM_VECTOR_LEN;
@@ -2946,6 +2957,19 @@ function topologyOptimizeMesh(spec_json, positions, indices) {
2946
2957
  }
2947
2958
  return takeFromExternrefTable0(ret[0]);
2948
2959
  }
2960
+ function transformMeshBuffers(positions, normals, transform_json) {
2961
+ const ptr0 = passArrayF32ToWasm0(positions, wasm.__wbindgen_malloc);
2962
+ const len0 = WASM_VECTOR_LEN;
2963
+ var ptr1 = isLikeNone(normals) ? 0 : passArrayF32ToWasm0(normals, wasm.__wbindgen_malloc);
2964
+ var len1 = WASM_VECTOR_LEN;
2965
+ const ptr2 = passStringToWasm0(transform_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
2966
+ const len2 = WASM_VECTOR_LEN;
2967
+ const ret = wasm.transformMeshBuffers(ptr0, len0, ptr1, len1, ptr2, len2);
2968
+ if (ret[2]) {
2969
+ throw takeFromExternrefTable0(ret[1]);
2970
+ }
2971
+ return takeFromExternrefTable0(ret[0]);
2972
+ }
2949
2973
  function writeEmbroideryDst(json) {
2950
2974
  const ptr0 = passStringToWasm0(json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
2951
2975
  const len0 = WASM_VECTOR_LEN;
@@ -10895,753 +10919,57 @@ var init_atoms = __esm({
10895
10919
  });
10896
10920
 
10897
10921
  // ../engine/dist/enclosure-fit.js
10898
- function outlineAabb(outline) {
10899
- let minX = Infinity;
10900
- let maxX = -Infinity;
10901
- let minY = Infinity;
10902
- let maxY = -Infinity;
10903
- for (const v of outline.vertices) {
10904
- if (v.x < minX)
10905
- minX = v.x;
10906
- if (v.x > maxX)
10907
- maxX = v.x;
10908
- if (v.y < minY)
10909
- minY = v.y;
10910
- if (v.y > maxY)
10911
- maxY = v.y;
10912
- }
10913
- return { minX, maxX, minY, maxY };
10922
+ function enclosureWasm() {
10923
+ const wasm3 = getKernelWasmSync();
10924
+ if (!wasm3?.enclosure_fit) {
10925
+ throw new Error("enclosure checks require the kernel WASM to be initialized \u2014 await getKernelWasm() (or Engine.init()) first");
10926
+ }
10927
+ return wasm3;
10914
10928
  }
10915
- function toWorld(p, placement) {
10916
- const t2 = placement.rotationDeg * Math.PI / 180;
10917
- const cos = Math.cos(t2);
10918
- const sin = Math.sin(t2);
10919
- return {
10920
- x: placement.offset.x + p.x * cos - p.y * sin,
10921
- y: placement.offset.y + p.x * sin + p.y * cos,
10922
- z: placement.offset.z + (p.z ?? 0)
10923
- };
10929
+ function compact(obj) {
10930
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
10924
10931
  }
10925
- function nearestEdge(x, y, b) {
10926
- const d = [
10927
- ["minX", Math.abs(x - b.minX)],
10928
- ["maxX", Math.abs(x - b.maxX)],
10929
- ["minY", Math.abs(y - b.minY)],
10930
- ["maxY", Math.abs(y - b.maxY)]
10931
- ];
10932
- d.sort((a, c) => a[1] - c[1]);
10933
- return d[0][0];
10932
+ function outlineAabb(outline) {
10933
+ return JSON.parse(enclosureWasm().enclosure_outline_aabb(JSON.stringify(outline)));
10934
10934
  }
10935
- function padWorld(fp, pad) {
10936
- const t2 = (fp.rotation ?? 0) * Math.PI / 180;
10937
- const cos = Math.cos(t2);
10938
- const sin = Math.sin(t2);
10939
- return {
10940
- x: fp.position.x + pad.position.x * cos - pad.position.y * sin,
10941
- y: fp.position.y + pad.position.x * sin + pad.position.y * cos
10942
- };
10935
+ function toWorld(p, placement) {
10936
+ return JSON.parse(enclosureWasm().enclosure_to_world(p.x, p.y, p.z ?? 0, JSON.stringify(placement)));
10943
10937
  }
10944
10938
  function mountingHolesFromPcb(pcb) {
10945
- const holes = [];
10946
- for (const fp of pcb.footprints) {
10947
- const isMount = MOUNT_RE.test(fp.footprintName) || MOUNT_RE.test(fp.ref);
10948
- if (isMount) {
10949
- const pad = fp.pads[0];
10950
- let dia = 3.2;
10951
- if (pad) {
10952
- if (pad.drill && typeof pad.drill === "object") {
10953
- const dd = pad.drill;
10954
- if (typeof dd.diameter === "number")
10955
- dia = dd.diameter;
10956
- } else if (pad.shape.type === "Circle") {
10957
- dia = pad.shape.diameter;
10958
- }
10959
- }
10960
- holes.push({ x: round2(fp.position.x), y: round2(fp.position.y), diameter: round2(dia), ref: fp.ref });
10961
- continue;
10962
- }
10963
- for (const pad of fp.pads) {
10964
- if (pad.padType !== "NPTH")
10965
- continue;
10966
- const w = padWorld(fp, pad);
10967
- let dia = 3.2;
10968
- const dd = pad.drill;
10969
- if (dd && typeof dd.diameter === "number")
10970
- dia = dd.diameter;
10971
- else if (pad.shape.type === "Circle")
10972
- dia = pad.shape.diameter;
10973
- holes.push({ x: round2(w.x), y: round2(w.y), diameter: round2(dia), ref: fp.ref });
10974
- }
10975
- }
10976
- return holes;
10939
+ return JSON.parse(enclosureWasm().enclosure_mounting_holes(JSON.stringify(pcb)));
10977
10940
  }
10978
10941
  function componentExtentsFromMeshes(meshes, pcb) {
10979
- const frontByRef = new Map(pcb.footprints.map((fp) => [fp.ref, fp.front ?? true]));
10980
- const out = [];
10981
- for (const m of meshes) {
10982
- let minZ = Infinity;
10983
- let maxZ = -Infinity;
10984
- for (let i = 2; i < m.positions.length; i += 3) {
10985
- const z = m.positions[i];
10986
- if (z < minZ)
10987
- minZ = z;
10988
- if (z > maxZ)
10989
- maxZ = z;
10990
- }
10991
- if (!Number.isFinite(minZ))
10992
- continue;
10993
- out.push({
10994
- ref: m.footprint_ref,
10995
- front: frontByRef.get(m.footprint_ref) ?? true,
10996
- topZ: round2(maxZ),
10997
- bottomZ: round2(minZ)
10998
- });
10999
- }
11000
- return out;
10942
+ const payload = meshes.map((m) => ({
10943
+ footprint_ref: m.footprint_ref,
10944
+ positions: Array.from(m.positions)
10945
+ }));
10946
+ return JSON.parse(enclosureWasm().enclosure_component_extents(JSON.stringify(payload), JSON.stringify(pcb)));
11001
10947
  }
11002
10948
  function connectorsFromPcb(pcb, outline) {
11003
- const aabb = outlineAabb(outline);
11004
- const out = [];
11005
- for (const fp of pcb.footprints) {
11006
- const isConn = CONNECTOR_REF_RE.test(fp.ref) || CONNECTOR_NAME_RE.test(fp.footprintName) || CONNECTOR_NAME_RE.test(fp.value ?? "");
11007
- if (!isConn)
11008
- continue;
11009
- out.push({
11010
- ref: fp.ref,
11011
- x: round2(fp.position.x),
11012
- y: round2(fp.position.y),
11013
- edge: nearestEdge(fp.position.x, fp.position.y, aabb),
11014
- height: 0
11015
- });
11016
- }
11017
- return out;
11018
- }
11019
- function autoPlacement(input, clearance) {
11020
- const { cavity } = input;
11021
- const a = outlineAabb(input.outline);
11022
- const boardW = a.maxX - a.minX;
11023
- const boardH = a.maxY - a.minY;
11024
- const cavW = cavity.maxX - cavity.minX;
11025
- const cavH = cavity.maxY - cavity.minY;
11026
- const offX = cavity.minX + (cavW - boardW) / 2 - a.minX;
11027
- const offY = cavity.minY + (cavH - boardH) / 2 - a.minY;
11028
- const standoffTop = input.standoffs && input.standoffs.length > 0 ? Math.max(...input.standoffs.map((s) => s.topZ)) : cavity.floorZ + (input.standoffHeight ?? 0);
11029
- void clearance;
11030
- return { offset: { x: round2(offX), y: round2(offY), z: round2(standoffTop) }, rotationDeg: 0 };
11031
- }
11032
- function checkBoardFit(input, placement, clearance) {
11033
- const { cavity } = input;
11034
- let minX = Infinity;
11035
- let maxX = -Infinity;
11036
- let minY = Infinity;
11037
- let maxY = -Infinity;
11038
- for (const v of input.outline.vertices) {
11039
- const w = toWorld(v, placement);
11040
- if (w.x < minX)
11041
- minX = w.x;
11042
- if (w.x > maxX)
11043
- maxX = w.x;
11044
- if (w.y < minY)
11045
- minY = w.y;
11046
- if (w.y > maxY)
11047
- maxY = w.y;
11048
- }
11049
- const marginMinX = minX - cavity.minX;
11050
- const marginMaxX = cavity.maxX - maxX;
11051
- const marginMinY = minY - cavity.minY;
11052
- const marginMaxY = cavity.maxY - maxY;
11053
- const worst2 = Math.min(marginMinX, marginMaxX, marginMinY, marginMaxY);
11054
- const sides = [
11055
- ["-X", marginMinX],
11056
- ["+X", marginMaxX],
11057
- ["-Y", marginMinY],
11058
- ["+Y", marginMaxY]
11059
- ];
11060
- const tight = sides.filter(([, m]) => m < clearance).map(([s]) => s);
11061
- const ok4 = worst2 >= clearance - 1e-6;
11062
- return {
11063
- id: "board_fit",
11064
- label: "Board fits cavity with clearance",
11065
- status: ok4 ? "pass" : "fail",
11066
- detail: ok4 ? `Board fits with ${round2(worst2)}mm worst-case clearance (need ${clearance}mm)` : worst2 < 0 ? `Board overhangs the cavity by ${round2(-worst2)}mm on ${tight.join(", ")}` : `Clearance on ${tight.join(", ")} is ${round2(worst2)}mm < required ${clearance}mm`,
11067
- measurements: {
11068
- worst_clearance_mm: round2(worst2),
11069
- margin_minus_x: round2(marginMinX),
11070
- margin_plus_x: round2(marginMaxX),
11071
- margin_minus_y: round2(marginMinY),
11072
- margin_plus_y: round2(marginMaxY),
11073
- board_w: round2(maxX - minX),
11074
- board_h: round2(maxY - minY),
11075
- cavity_w: round2(cavity.maxX - cavity.minX),
11076
- cavity_h: round2(cavity.maxY - cavity.minY)
11077
- }
11078
- };
11079
- }
11080
- function checkLidClearance(input, placement, clearance) {
11081
- const { cavity } = input;
11082
- const extents = input.componentExtents ?? [];
11083
- if (extents.length === 0) {
11084
- return {
11085
- id: "lid_clearance",
11086
- label: "Components clear the lid",
11087
- status: "skip",
11088
- detail: "No component heights available (kernel component meshes unavailable)"
11089
- };
11090
- }
11091
- const cavityDepth = cavity.ceilZ - cavity.floorZ;
11092
- const front = extents.filter((e) => e.front);
11093
- const back = extents.filter((e) => !e.front);
11094
- let tallest = { ref: "", top: -Infinity };
11095
- for (const e of front) {
11096
- const top = placement.offset.z + e.topZ;
11097
- if (top > tallest.top)
11098
- tallest = { ref: e.ref, top };
11099
- }
11100
- const lidGap = cavity.ceilZ - tallest.top;
11101
- const topOk = front.length === 0 || lidGap >= clearance - 1e-6;
11102
- let lowest = { ref: "", bottom: Infinity };
11103
- for (const e of back) {
11104
- const bot = placement.offset.z + e.bottomZ;
11105
- if (bot < lowest.bottom)
11106
- lowest = { ref: e.ref, bottom: bot };
11107
- }
11108
- const floorGap = back.length > 0 ? lowest.bottom - cavity.floorZ : Infinity;
11109
- const botOk = back.length === 0 || floorGap >= -1e-6;
11110
- const ok4 = topOk && botOk;
11111
- let detail;
11112
- if (ok4) {
11113
- detail = `Tallest part ${tallest.ref || "\u2014"} leaves ${round2(lidGap)}mm under the lid (cavity depth ${round2(cavityDepth)}mm)`;
11114
- if (back.length > 0 && Number.isFinite(floorGap)) {
11115
- detail += `; back-side ${lowest.ref} clears floor by ${round2(floorGap)}mm`;
11116
- }
11117
- } else if (!topOk) {
11118
- detail = `${tallest.ref} is ${round2(-lidGap + clearance)}mm too tall \u2014 it ${lidGap < 0 ? "punches through" : "is within clearance of"} the lid (cavity depth ${round2(cavityDepth)}mm)`;
11119
- } else {
11120
- detail = `Back-side ${lowest.ref} collides with the floor by ${round2(-floorGap)}mm \u2014 raise the standoffs`;
11121
- }
11122
- return {
11123
- id: "lid_clearance",
11124
- label: "Components clear the lid",
11125
- status: ok4 ? "pass" : "fail",
11126
- detail,
11127
- measurements: {
11128
- cavity_depth_mm: round2(cavityDepth),
11129
- tallest_ref: tallest.ref || "none",
11130
- lid_gap_mm: Number.isFinite(lidGap) ? round2(lidGap) : "n/a",
11131
- stack_top_z: Number.isFinite(tallest.top) ? round2(tallest.top) : "n/a",
11132
- floor_gap_mm: Number.isFinite(floorGap) ? round2(floorGap) : "n/a"
11133
- }
11134
- };
11135
- }
11136
- function checkMountingHoles(input, placement) {
11137
- const holes = input.mountingHoles ?? [];
11138
- const standoffs = input.standoffs ?? [];
11139
- const tol = input.holeTolerance ?? DEFAULT_HOLE_TOL;
11140
- if (holes.length === 0) {
11141
- return {
11142
- id: "mounting_holes",
11143
- label: "Mounting holes land on standoffs",
11144
- status: "skip",
11145
- detail: "Board declares no mounting holes"
11146
- };
11147
- }
11148
- if (standoffs.length === 0) {
11149
- return {
11150
- id: "mounting_holes",
11151
- label: "Mounting holes land on standoffs",
11152
- status: "skip",
11153
- detail: `Board has ${holes.length} mounting hole(s) but no standoffs were detected in the enclosure`
11154
- };
11155
- }
11156
- let matched = 0;
11157
- let worst2 = 0;
11158
- const misses = [];
11159
- for (const h of holes) {
11160
- const w = toWorld(h, placement);
11161
- let best = Infinity;
11162
- for (const s of standoffs) {
11163
- const d = Math.hypot(w.x - s.x, w.y - s.y);
11164
- if (d < best)
11165
- best = d;
11166
- }
11167
- if (best <= tol) {
11168
- matched++;
11169
- if (best > worst2)
11170
- worst2 = best;
11171
- } else {
11172
- misses.push(`${h.ref ?? "hole"}@(${round2(w.x)},${round2(w.y)}) is ${round2(best)}mm off`);
11173
- }
11174
- }
11175
- const ok4 = matched === holes.length;
11176
- return {
11177
- id: "mounting_holes",
11178
- label: "Mounting holes land on standoffs",
11179
- status: ok4 ? "pass" : "fail",
11180
- detail: ok4 ? `All ${holes.length} mounting holes align to standoffs (worst offset ${round2(worst2)}mm, tol ${tol}mm)` : `${matched}/${holes.length} holes align \u2014 ${misses.join("; ")}`,
11181
- measurements: {
11182
- holes_total: holes.length,
11183
- holes_matched: matched,
11184
- standoffs: standoffs.length,
11185
- tolerance_mm: tol,
11186
- worst_offset_mm: round2(worst2)
11187
- }
11188
- };
11189
- }
11190
- function checkConnectors(input, placement, clearance) {
11191
- const conns = input.connectors ?? [];
11192
- const openings = input.openings ?? [];
11193
- if (conns.length === 0) {
11194
- return {
11195
- id: "connector_cutouts",
11196
- label: "Connectors align to wall cutouts",
11197
- status: "skip",
11198
- detail: "Board declares no edge connectors"
11199
- };
11200
- }
11201
- const cav = input.cavity;
11202
- let aligned = 0;
11203
- const problems = [];
11204
- for (const c of conns) {
11205
- const w = toWorld(c, placement);
11206
- const wallEdge = nearestEdge(w.x, w.y, cav);
11207
- const along = wallEdge === "minX" || wallEdge === "maxX" ? w.y : w.x;
11208
- const onWall = openings.filter((o) => o.edge === wallEdge);
11209
- if (onWall.length === 0) {
11210
- problems.push(`${c.ref} faces the ${wallEdge} wall but it has no cutout`);
11211
- continue;
11212
- }
11213
- const hit = onWall.find((o) => {
11214
- const oc = o.edge === "minX" || o.edge === "maxX" ? o.center.y : o.center.x;
11215
- return Math.abs(along - oc) <= o.width / 2 + clearance;
11216
- });
11217
- if (hit) {
11218
- aligned++;
11219
- } else {
11220
- const nearest = onWall.reduce((best, o) => {
11221
- const oc = o.edge === "minX" || o.edge === "maxX" ? o.center.y : o.center.x;
11222
- const off = Math.abs(along - oc) - o.width / 2;
11223
- return off < best ? off : best;
11224
- }, Infinity);
11225
- problems.push(`${c.ref} on ${wallEdge} misses its cutout by ${round2(nearest)}mm`);
11226
- }
11227
- }
11228
- const ok4 = aligned === conns.length;
11229
- const status = ok4 ? "pass" : openings.length === 0 ? "warn" : "fail";
11230
- return {
11231
- id: "connector_cutouts",
11232
- label: "Connectors align to wall cutouts",
11233
- status,
11234
- detail: ok4 ? `All ${conns.length} connector(s) line up with wall cutouts` : openings.length === 0 ? `No wall cutouts detected; ${conns.length} connector(s) would be enclosed: ${problems.join("; ")}` : `${aligned}/${conns.length} connectors aligned \u2014 ${problems.join("; ")}`,
11235
- measurements: {
11236
- connectors_total: conns.length,
11237
- connectors_aligned: aligned,
11238
- wall_openings: openings.length
11239
- }
11240
- };
10949
+ return JSON.parse(enclosureWasm().enclosure_connectors(JSON.stringify(pcb), JSON.stringify(outline)));
11241
10950
  }
11242
10951
  function checkEnclosureFit(input) {
11243
- const clearance = input.clearance ?? DEFAULT_CLEARANCE;
11244
- const placement = input.placement ?? autoPlacement(input, clearance);
11245
- const checks = [
11246
- checkBoardFit(input, placement, clearance),
11247
- checkLidClearance(input, placement, clearance),
11248
- checkMountingHoles(input, placement),
11249
- checkConnectors(input, placement, clearance)
11250
- ];
11251
- const failed = checks.filter((c) => c.status === "fail");
11252
- const warned = checks.filter((c) => c.status === "warn");
11253
- const passed = checks.filter((c) => c.status === "pass");
11254
- const ok4 = failed.length === 0;
11255
- const verified = ok4 && warned.length === 0;
11256
- let summary;
11257
- if (failed.length > 0) {
11258
- summary = `Enclosure fit: FAIL \u2014 ${failed.map((c) => c.label.toLowerCase()).join("; ")}`;
11259
- } else if (warned.length > 0) {
11260
- summary = `Enclosure fit: UNVERIFIED \u2014 ${passed.length} passed, ${warned.length} warning(s): ${warned.map((c) => c.detail).join("; ")}`;
11261
- } else {
11262
- summary = `Enclosure fit: PASS \u2014 ${passed.length}/${checks.length} checks (${checks.filter((c) => c.status === "pass").map((c) => c.label.toLowerCase()).join(", ")})`;
11263
- }
11264
- return { ok: ok4, verified, summary, clearance, placement, checks };
10952
+ return JSON.parse(enclosureWasm().enclosure_fit(JSON.stringify(compact(input))));
11265
10953
  }
11266
10954
  function deriveBoardFromCavity(cavity, standoffs, opts = {}) {
11267
- const clearance = opts.clearance ?? DEFAULT_CLEARANCE;
11268
- const thickness = opts.thickness ?? 1.6;
11269
- const holeDia = opts.holeDiameter ?? 3.2;
11270
- const w = round2(cavity.maxX - cavity.minX - 2 * clearance);
11271
- const h = round2(cavity.maxY - cavity.minY - 2 * clearance);
11272
- const outline = {
11273
- vertices: [
11274
- { x: 0, y: 0 },
11275
- { x: w, y: 0 },
11276
- { x: w, y: h },
11277
- { x: 0, y: h }
11278
- ],
11279
- thickness
11280
- };
11281
- const standoffTop = standoffs.length > 0 ? Math.max(...standoffs.map((s) => s.topZ)) : cavity.floorZ + (opts.standoffHeight ?? 0);
11282
- const offX = cavity.minX + clearance;
11283
- const offY = cavity.minY + clearance;
11284
- const placement = {
11285
- offset: { x: round2(offX), y: round2(offY), z: round2(standoffTop) },
11286
- rotationDeg: 0
11287
- };
11288
- const mountingHoles = [];
11289
- for (const s of standoffs) {
11290
- const lx = round2(s.x - offX);
11291
- const ly = round2(s.y - offY);
11292
- if (lx >= 0 && lx <= w && ly >= 0 && ly <= h) {
11293
- mountingHoles.push({ x: lx, y: ly, diameter: holeDia });
11294
- }
11295
- }
11296
- return { outline, mountingHoles, placement };
10955
+ return JSON.parse(enclosureWasm().enclosure_derive_board(JSON.stringify(cavity), JSON.stringify(standoffs), JSON.stringify(compact(opts))));
11297
10956
  }
11298
- var DEFAULT_CLEARANCE, DEFAULT_HOLE_TOL, round2, MOUNT_RE, CONNECTOR_REF_RE, CONNECTOR_NAME_RE;
11299
10957
  var init_enclosure_fit = __esm({
11300
10958
  "../engine/dist/enclosure-fit.js"() {
11301
10959
  "use strict";
11302
- DEFAULT_CLEARANCE = 0.5;
11303
- DEFAULT_HOLE_TOL = 0.6;
11304
- round2 = (n) => Math.round(n * 100) / 100;
11305
- MOUNT_RE = /mount(ing)?[_-]?hole|mountingpad|mounthole/i;
11306
- CONNECTOR_REF_RE = /^(J|CN|CON|USB|P)\d/i;
11307
- CONNECTOR_NAME_RE = /usb|type[-_]?c|micro|mini|conn|header|jst|molex|rj45|hdr|terminal|socket|receptacle|barrel|dcjack/i;
10960
+ init_wasm_singleton();
11308
10961
  }
11309
10962
  });
11310
10963
 
11311
10964
  // ../engine/dist/enclosure-mesh.js
11312
- function gwn(positions, indices, qx, qy, qz) {
11313
- let w = 0;
11314
- const tris = indices.length;
11315
- for (let t2 = 0; t2 < tris; t2 += 3) {
11316
- const i0 = indices[t2] * 3;
11317
- const i1 = indices[t2 + 1] * 3;
11318
- const i2 = indices[t2 + 2] * 3;
11319
- const ax = positions[i0] - qx;
11320
- const ay = positions[i0 + 1] - qy;
11321
- const az = positions[i0 + 2] - qz;
11322
- const bx = positions[i1] - qx;
11323
- const by = positions[i1 + 1] - qy;
11324
- const bz = positions[i1 + 2] - qz;
11325
- const cx = positions[i2] - qx;
11326
- const cy = positions[i2 + 1] - qy;
11327
- const cz = positions[i2 + 2] - qz;
11328
- const la = Math.sqrt(ax * ax + ay * ay + az * az);
11329
- const lb = Math.sqrt(bx * bx + by * by + bz * bz);
11330
- const lc = Math.sqrt(cx * cx + cy * cy + cz * cz);
11331
- const cbx = by * cz - bz * cy;
11332
- const cby = bz * cx - bx * cz;
11333
- const cbz = bx * cy - by * cx;
11334
- const num2 = ax * cbx + ay * cby + az * cbz;
11335
- const den = la * lb * lc + (ax * bx + ay * by + az * bz) * lc + (bx * cx + by * cy + bz * cz) * la + (cx * ax + cy * ay + cz * az) * lb;
11336
- w += Math.atan2(num2, den);
11337
- }
11338
- return w / (2 * Math.PI);
11339
- }
11340
- function buildOccupancy(positions, indices) {
11341
- let minX = Infinity;
11342
- let maxX = -Infinity;
11343
- let minY = Infinity;
11344
- let maxY = -Infinity;
11345
- let minZ = Infinity;
11346
- let maxZ = -Infinity;
11347
- for (let i = 0; i < positions.length; i += 3) {
11348
- const x = positions[i];
11349
- const y = positions[i + 1];
11350
- const z = positions[i + 2];
11351
- if (x < minX)
11352
- minX = x;
11353
- if (x > maxX)
11354
- maxX = x;
11355
- if (y < minY)
11356
- minY = y;
11357
- if (y > maxY)
11358
- maxY = y;
11359
- if (z < minZ)
11360
- minZ = z;
11361
- if (z > maxZ)
11362
- maxZ = z;
11363
- }
11364
- if (!(maxX > minX) || !(maxY > minY) || !(maxZ > minZ))
11365
- return null;
11366
- const gw = GRID_XY;
11367
- const gh = GRID_XY;
11368
- const gz = GRID_Z;
11369
- const dx = (maxX - minX) / gw;
11370
- const dy = (maxY - minY) / gh;
11371
- const dz = (maxZ - minZ) / gz;
11372
- const occ = new Uint8Array(gw * gh * gz);
11373
- for (let k = 0; k < gz; k++) {
11374
- const qz = minZ + (k + JZ) * dz;
11375
- for (let j = 0; j < gh; j++) {
11376
- const qy = minY + (j + JY) * dy;
11377
- for (let i = 0; i < gw; i++) {
11378
- const qx = minX + (i + JX) * dx;
11379
- if (Math.abs(gwn(positions, indices, qx, qy, qz)) > INSIDE) {
11380
- occ[i + gw * (j + gh * k)] = 1;
11381
- }
11382
- }
11383
- }
11384
- }
11385
- return { gw, gh, gz, ox: minX, oy: minY, oz: minZ, dx, dy, dz, minZ, maxZ, occ };
11386
- }
11387
- function bottomRunTopZ(o, i, j) {
11388
- const base = i + o.gw * j;
11389
- if (o.occ[base] !== 1)
11390
- return null;
11391
- let k = 1;
11392
- while (k < o.gz && o.occ[base + o.gw * o.gh * k] === 1)
11393
- k++;
11394
- return o.oz + k * o.dz;
11395
- }
11396
- function openAtTop(o, i, j) {
11397
- return o.occ[i + o.gw * (j + o.gh * (o.gz - 1))] !== 1;
11398
- }
11399
- function hasGap(o, i, j, kLo, kHi) {
11400
- for (let k = kLo; k <= kHi; k++) {
11401
- if (o.occ[i + o.gw * (j + o.gh * k)] !== 1)
11402
- return true;
11403
- }
11404
- return false;
11405
- }
11406
10965
  function extractEnclosureFeatures(positions, indices) {
11407
- const o = buildOccupancy(positions, indices);
11408
- if (!o) {
11409
- return {
11410
- outer: { minX: 0, maxX: 0, minY: 0, maxY: 0, minZ: 0, maxZ: 0 },
11411
- cavity: null,
11412
- standoffs: [],
11413
- openings: []
11414
- };
11415
- }
11416
- const outer = {
11417
- minX: o.ox,
11418
- maxX: o.ox + o.gw * o.dx,
11419
- minY: o.oy,
11420
- maxY: o.oy + o.gh * o.dy,
11421
- minZ: o.minZ,
11422
- maxZ: o.maxZ
11423
- };
11424
- const isPocket = new Uint8Array(o.gw * o.gh);
11425
- const firstTops = [];
11426
- const countX = new Int32Array(o.gw);
11427
- const countY = new Int32Array(o.gh);
11428
- let pocketCount = 0;
11429
- for (let j = 0; j < o.gh; j++) {
11430
- for (let i = 0; i < o.gw; i++) {
11431
- const top = bottomRunTopZ(o, i, j);
11432
- if (top != null && openAtTop(o, i, j)) {
11433
- isPocket[i + o.gw * j] = 1;
11434
- pocketCount++;
11435
- firstTops.push(top);
11436
- countX[i]++;
11437
- countY[j]++;
11438
- }
11439
- }
11440
- }
11441
- if (pocketCount === 0) {
11442
- return { outer, cavity: null, standoffs: [], openings: [] };
11443
- }
11444
- const core = (counts) => {
11445
- let peak = 0;
11446
- for (const c of counts)
11447
- if (c > peak)
11448
- peak = c;
11449
- const thr = peak * 0.5;
11450
- let lo = 0;
11451
- let hi = counts.length - 1;
11452
- while (lo < counts.length && counts[lo] < thr)
11453
- lo++;
11454
- while (hi >= 0 && counts[hi] < thr)
11455
- hi--;
11456
- return [lo, hi];
11457
- };
11458
- const [pminI, pmaxI] = core(countX);
11459
- const [pminJ, pmaxJ] = core(countY);
11460
- const sortedTops = [...firstTops].sort((a, b) => a - b);
11461
- const floorZ = sortedTops[Math.floor(sortedTops.length / 2)] ?? o.minZ;
11462
- const ceilZ = o.maxZ;
11463
- const cavity = {
11464
- minX: o.ox + pminI * o.dx,
11465
- maxX: o.ox + (pmaxI + 1) * o.dx,
11466
- minY: o.oy + pminJ * o.dy,
11467
- maxY: o.oy + (pmaxJ + 1) * o.dy,
11468
- floorZ: round22(floorZ),
11469
- ceilZ: round22(ceilZ),
11470
- hasLid: false
11471
- };
11472
- const standoffs = extractStandoffs(o, isPocket, floorZ);
11473
- const openings = extractOpenings(o, cavity, floorZ, ceilZ);
11474
- return { outer, cavity, standoffs, openings };
11475
- }
11476
- function extractStandoffs(o, isPocket, floorZ) {
11477
- const isPost = new Uint8Array(o.gw * o.gh);
11478
- for (let j = 0; j < o.gh; j++) {
11479
- for (let i = 0; i < o.gw; i++) {
11480
- if (!isPocket[i + o.gw * j])
11481
- continue;
11482
- const top = bottomRunTopZ(o, i, j);
11483
- if (top != null && top > floorZ + MIN_POST_HEIGHT)
11484
- isPost[i + o.gw * j] = 1;
11485
- }
11486
- }
11487
- const seen = new Uint8Array(o.gw * o.gh);
11488
- const standoffs = [];
11489
- for (let j = 0; j < o.gh; j++) {
11490
- for (let i = 0; i < o.gw; i++) {
11491
- const k0 = i + o.gw * j;
11492
- if (!isPost[k0] || seen[k0])
11493
- continue;
11494
- const stack = [[i, j]];
11495
- seen[k0] = 1;
11496
- let sumX = 0;
11497
- let sumY = 0;
11498
- let n = 0;
11499
- let topMax = -Infinity;
11500
- let minI = i;
11501
- let maxI = i;
11502
- let minJ = j;
11503
- let maxJ = j;
11504
- while (stack.length) {
11505
- const [ci, cj] = stack.pop();
11506
- sumX += o.ox + (ci + 0.5) * o.dx;
11507
- sumY += o.oy + (cj + 0.5) * o.dy;
11508
- n++;
11509
- const t2 = bottomRunTopZ(o, ci, cj);
11510
- if (t2 != null && t2 > topMax)
11511
- topMax = t2;
11512
- if (ci < minI)
11513
- minI = ci;
11514
- if (ci > maxI)
11515
- maxI = ci;
11516
- if (cj < minJ)
11517
- minJ = cj;
11518
- if (cj > maxJ)
11519
- maxJ = cj;
11520
- for (const [ni, nj] of [
11521
- [ci - 1, cj],
11522
- [ci + 1, cj],
11523
- [ci, cj - 1],
11524
- [ci, cj + 1]
11525
- ]) {
11526
- if (ni < 0 || nj < 0 || ni >= o.gw || nj >= o.gh)
11527
- continue;
11528
- const nk = ni + o.gw * nj;
11529
- if (isPost[nk] && !seen[nk]) {
11530
- seen[nk] = 1;
11531
- stack.push([ni, nj]);
11532
- }
11533
- }
11534
- }
11535
- if (n < 2)
11536
- continue;
11537
- const radius = Math.max(((maxI - minI + 1) * o.dx + (maxJ - minJ + 1) * o.dy) / 4, o.dx);
11538
- standoffs.push({
11539
- x: round22(sumX / n),
11540
- y: round22(sumY / n),
11541
- topZ: round22(topMax),
11542
- radius: round22(radius)
11543
- });
11544
- }
11545
- }
11546
- return standoffs;
11547
- }
11548
- function extractOpenings(o, cavity, floorZ, ceilZ) {
11549
- const pIminI = Math.round((cavity.minX - o.ox) / o.dx);
11550
- const pImaxI = Math.round((cavity.maxX - o.ox) / o.dx) - 1;
11551
- const pIminJ = Math.round((cavity.minY - o.oy) / o.dy);
11552
- const pImaxJ = Math.round((cavity.maxY - o.oy) / o.dy) - 1;
11553
- const kLo = Math.max(0, Math.floor((floorZ - o.oz) / o.dz) + 1);
11554
- const kHi = Math.min(o.gz - 1, Math.ceil((ceilZ - o.oz) / o.dz) - 1);
11555
- const openings = [];
11556
- const wallOpen = (i, j) => {
11557
- if (i < 0 || j < 0 || i >= o.gw || j >= o.gh)
11558
- return true;
11559
- return hasGap(o, i, j, kLo, kHi);
11560
- };
11561
- const scans = [];
11562
- for (const [edge, wi] of [
11563
- ["minX", pIminI - 1],
11564
- ["maxX", pImaxI + 1]
11565
- ]) {
11566
- const cells = [];
11567
- for (let j = pIminJ; j <= pImaxJ; j++) {
11568
- cells.push({ open: wallOpen(wi, j), x: o.ox + (wi + 0.5) * o.dx, y: o.oy + (j + 0.5) * o.dy, i: wi, j });
11569
- }
11570
- scans.push({ edge, cells });
11571
- }
11572
- for (const [edge, wj] of [
11573
- ["minY", pIminJ - 1],
11574
- ["maxY", pImaxJ + 1]
11575
- ]) {
11576
- const cells = [];
11577
- for (let i = pIminI; i <= pImaxI; i++) {
11578
- cells.push({ open: wallOpen(i, wj), x: o.ox + (i + 0.5) * o.dx, y: o.oy + (wj + 0.5) * o.dy, i, j: wj });
11579
- }
11580
- scans.push({ edge, cells });
11581
- }
11582
- for (const scan of scans) {
11583
- let run = [];
11584
- const flush = () => {
11585
- if (run.length >= MIN_OPENING_CELLS) {
11586
- const xs = run.map((p) => p.x);
11587
- const ys = run.map((p) => p.y);
11588
- const horiz = scan.edge === "minY" || scan.edge === "maxY";
11589
- const center = {
11590
- x: (Math.min(...xs) + Math.max(...xs)) / 2,
11591
- y: (Math.min(...ys) + Math.max(...ys)) / 2
11592
- };
11593
- const width = horiz ? Math.max(...xs) - Math.min(...xs) + o.dx : Math.max(...ys) - Math.min(...ys) + o.dy;
11594
- const mid = run[Math.floor(run.length / 2)];
11595
- const { zMin, zMax } = openingZSpan(o, mid.i, mid.j, kLo, kHi);
11596
- openings.push({
11597
- edge: scan.edge,
11598
- center: { x: round22(center.x), y: round22(center.y) },
11599
- width: round22(width),
11600
- zMin: round22(zMin),
11601
- zMax: round22(zMax)
11602
- });
11603
- }
11604
- run = [];
11605
- };
11606
- for (const c of scan.cells) {
11607
- if (c.open)
11608
- run.push({ x: c.x, y: c.y, i: c.i, j: c.j });
11609
- else
11610
- flush();
11611
- }
11612
- flush();
11613
- }
11614
- return openings;
11615
- }
11616
- function openingZSpan(o, i, j, kLo, kHi) {
11617
- let zMin = Infinity;
11618
- let zMax = -Infinity;
11619
- for (let k = kLo; k <= kHi; k++) {
11620
- if (o.occ[i + o.gw * (j + o.gh * k)] !== 1) {
11621
- const z = o.oz + (k + 0.5) * o.dz;
11622
- if (z < zMin)
11623
- zMin = z;
11624
- if (z > zMax)
11625
- zMax = z;
11626
- }
11627
- }
11628
- if (!Number.isFinite(zMin))
11629
- return { zMin: o.oz, zMax: o.maxZ };
11630
- return { zMin, zMax };
10966
+ const json = enclosureWasm().enclosure_features(Float64Array.from(positions), Uint32Array.from(indices));
10967
+ return JSON.parse(json);
11631
10968
  }
11632
- var GRID_XY, GRID_Z, INSIDE, MIN_POST_HEIGHT, MIN_OPENING_CELLS, JX, JY, JZ, round22;
11633
10969
  var init_enclosure_mesh = __esm({
11634
10970
  "../engine/dist/enclosure-mesh.js"() {
11635
10971
  "use strict";
11636
- GRID_XY = 48;
11637
- GRID_Z = 28;
11638
- INSIDE = 0.5;
11639
- MIN_POST_HEIGHT = 0.8;
11640
- MIN_OPENING_CELLS = 2;
11641
- JX = 0.5 + 0.137;
11642
- JY = 0.5 - 0.077;
11643
- JZ = 0.5 + 0.041;
11644
- round22 = (n) => Math.round(n * 100) / 100;
10972
+ init_enclosure_fit();
11645
10973
  }
11646
10974
  });
11647
10975
 
@@ -32264,13 +31592,13 @@ var init_document_store = __esm({
32264
31592
  const ang = (fp.rotation ?? 0) * Math.PI / 180;
32265
31593
  const cos = Math.cos(ang);
32266
31594
  const sin = Math.sin(ang);
32267
- const padWorld3 = fp.pads.map((p) => ({
31595
+ const padWorld2 = fp.pads.map((p) => ({
32268
31596
  x: fp.position.x + (p.position.x * cos - p.position.y * sin),
32269
31597
  y: fp.position.y + (p.position.x * sin + p.position.y * cos)
32270
31598
  }));
32271
31599
  for (const tr of pcb.traces) {
32272
31600
  for (const end of [tr.start, tr.end]) {
32273
- if (padWorld3.some((pw) => Math.abs(end.x - pw.x) < TOL && Math.abs(end.y - pw.y) < TOL)) {
31601
+ if (padWorld2.some((pw) => Math.abs(end.x - pw.x) < TOL && Math.abs(end.y - pw.y) < TOL)) {
32274
31602
  end.x += dx;
32275
31603
  end.y += dy;
32276
31604
  }
@@ -58340,7 +57668,7 @@ function computeUtilization(footprints, vertices, cutouts, shape, innerRadius, e
58340
57668
  }
58341
57669
  const boardArea = Math.abs(loopSignedArea(vertices)) - (cutouts ?? []).reduce((s, c) => s + Math.abs(loopSignedArea(c)), 0);
58342
57670
  if (!Number.isFinite(occMinX) || boardArea <= 0) return void 0;
58343
- const round25 = (v) => Math.round(v * 100) / 100;
57671
+ const round23 = (v) => Math.round(v * 100) / 100;
58344
57672
  const ceilHalf = (v) => Math.ceil(v * 2) / 2;
58345
57673
  const bbW = occMaxX - occMinX;
58346
57674
  const bbH = occMaxY - occMinY;
@@ -58353,8 +57681,8 @@ function computeUtilization(footprints, vertices, cutouts, shape, innerRadius, e
58353
57681
  suggested = {
58354
57682
  type: "circle",
58355
57683
  outer_diameter: od,
58356
- center: { x: round25(cx), y: round25(cy) },
58357
- ...innerRadius > 0 ? { inner_diameter: round25(2 * innerRadius) } : {},
57684
+ center: { x: round23(cx), y: round23(cy) },
57685
+ ...innerRadius > 0 ? { inner_diameter: round23(2 * innerRadius) } : {},
58358
57686
  note: `Minimum enclosing circle with ${margin}mm edge clearance`
58359
57687
  };
58360
57688
  } else {
@@ -58362,19 +57690,19 @@ function computeUtilization(footprints, vertices, cutouts, shape, innerRadius, e
58362
57690
  type: "rect",
58363
57691
  width: ceilHalf(bbW + 2 * margin),
58364
57692
  height: ceilHalf(bbH + 2 * margin),
58365
- origin: { x: round25(occMinX - margin), y: round25(occMinY - margin) },
57693
+ origin: { x: round23(occMinX - margin), y: round23(occMinY - margin) },
58366
57694
  note: `Minimum enclosing rectangle with ${margin}mm edge clearance`
58367
57695
  };
58368
57696
  }
58369
57697
  return {
58370
- board_area_mm2: round25(boardArea),
58371
- component_area_mm2: round25(componentArea),
57698
+ board_area_mm2: round23(boardArea),
57699
+ component_area_mm2: round23(componentArea),
58372
57700
  utilization_pct: Math.round(componentArea / boardArea * 1e3) / 10,
58373
- bounding_box: { x: round25(occMinX), y: round25(occMinY), w: round25(bbW), h: round25(bbH) },
57701
+ bounding_box: { x: round23(occMinX), y: round23(occMinY), w: round23(bbW), h: round23(bbH) },
58374
57702
  suggested_outline: suggested
58375
57703
  };
58376
57704
  }
58377
- function padWorld2(fp, pad) {
57705
+ function padWorld(fp, pad) {
58378
57706
  const ang = (fp.rotation ?? 0) * Math.PI / 180;
58379
57707
  const cos = Math.cos(ang);
58380
57708
  const sin = Math.sin(ang);
@@ -58393,7 +57721,7 @@ function detectStaleNets(pcb) {
58393
57721
  const arr = padsByNet.get(pad.net) ?? [];
58394
57722
  const sh = pad.shape;
58395
57723
  const halfExtent = sh.diameter ? sh.diameter / 2 : Math.hypot(sh.width ?? 0, sh.height ?? 0) / 2;
58396
- arr.push({ pos: padWorld2(fp, pad), layers: pad.layers, halfExtent });
57724
+ arr.push({ pos: padWorld(fp, pad), layers: pad.layers, halfExtent });
58397
57725
  padsByNet.set(pad.net, arr);
58398
57726
  }
58399
57727
  }
@@ -59277,7 +58605,7 @@ function layoutLint(pcb) {
59277
58605
  for (const fp of fps) {
59278
58606
  const prefix = refPrefix(fp.ref);
59279
58607
  for (const pad of fp.pads) {
59280
- const w = { ref: fp.ref, prefix, pad, pos: padWorld2(fp, pad) };
58608
+ const w = { ref: fp.ref, prefix, pad, pos: padWorld(fp, pad) };
59281
58609
  allPads.push(w);
59282
58610
  if (pad.net) {
59283
58611
  const arr = byNet.get(pad.net) ?? [];
@@ -59293,7 +58621,7 @@ function layoutLint(pcb) {
59293
58621
  for (const pad of fp.pads) {
59294
58622
  const net = pad.net;
59295
58623
  if (!net || LINT_GROUND_NET_RE.test(net) || LINT_SUPPLY_NET_RE.test(net)) continue;
59296
- const from = padWorld2(fp, pad);
58624
+ const from = padWorld(fp, pad);
59297
58625
  let nearest = null;
59298
58626
  for (const other of byNet.get(net) ?? []) {
59299
58627
  if (other.ref === fp.ref || other.prefix !== "U") continue;
@@ -59308,9 +58636,9 @@ function layoutLint(pcb) {
59308
58636
  warnings.push({
59309
58637
  kind: "crystal_far_from_ic",
59310
58638
  refs: [fp.ref, worst2.icRef],
59311
- distance_mm: round23(worst2.dist),
58639
+ distance_mm: round2(worst2.dist),
59312
58640
  threshold_mm: LINT_CRYSTAL_MAX_MM,
59313
- message: `Crystal ${fp.ref} is ${round23(worst2.dist)}mm from ${worst2.icRef} pad ${worst2.icPad} (net '${worst2.net}') \u2014 oscillator loops want <${LINT_CRYSTAL_MAX_MM}mm; move ${fp.ref} next to its XIN/XOUT pins.`
58641
+ message: `Crystal ${fp.ref} is ${round2(worst2.dist)}mm from ${worst2.icRef} pad ${worst2.icPad} (net '${worst2.net}') \u2014 oscillator loops want <${LINT_CRYSTAL_MAX_MM}mm; move ${fp.ref} next to its XIN/XOUT pins.`
59314
58642
  });
59315
58643
  }
59316
58644
  }
@@ -59321,7 +58649,7 @@ function layoutLint(pcb) {
59321
58649
  const pwrIdx = nets.findIndex((n) => n && LINT_SUPPLY_NET_RE.test(n) && !LINT_GROUND_NET_RE.test(n));
59322
58650
  if (gndIdx < 0 || pwrIdx < 0) continue;
59323
58651
  const pwrPad = fp.pads[pwrIdx];
59324
- const from = padWorld2(fp, pwrPad);
58652
+ const from = padWorld(fp, pwrPad);
59325
58653
  let nearest = null;
59326
58654
  for (const other of byNet.get(pwrPad.net) ?? []) {
59327
58655
  if (other.ref === fp.ref || other.prefix !== "U") continue;
@@ -59334,9 +58662,9 @@ function layoutLint(pcb) {
59334
58662
  warnings.push({
59335
58663
  kind: "decoupling_cap_far_from_pin",
59336
58664
  refs: [fp.ref, nearest.icRef],
59337
- distance_mm: round23(nearest.dist),
58665
+ distance_mm: round2(nearest.dist),
59338
58666
  threshold_mm: LINT_DECAP_MAX_MM,
59339
- message: `Decoupling cap ${fp.ref} is ${round23(nearest.dist)}mm from ${nearest.icRef} pad ${nearest.icPad} (net '${pwrPad.net}') \u2014 decouplers want <${LINT_DECAP_MAX_MM}mm to the pin they decouple.`
58667
+ message: `Decoupling cap ${fp.ref} is ${round2(nearest.dist)}mm from ${nearest.icRef} pad ${nearest.icPad} (net '${pwrPad.net}') \u2014 decouplers want <${LINT_DECAP_MAX_MM}mm to the pin they decouple.`
59340
58668
  });
59341
58669
  }
59342
58670
  }
@@ -59354,15 +58682,15 @@ function layoutLint(pcb) {
59354
58682
  for (const fp of fps) {
59355
58683
  const prefix = refPrefix(fp.ref);
59356
58684
  if (prefix !== "J" && prefix !== "P" && prefix !== "CN" && prefix !== "USB") continue;
59357
- const pts = fp.pads.length > 0 ? fp.pads.map((p) => padWorld2(fp, p)) : [fp.position];
58685
+ const pts = fp.pads.length > 0 ? fp.pads.map((p) => padWorld(fp, p)) : [fp.position];
59358
58686
  const d = Math.min(...pts.map(edgeDist));
59359
58687
  if (d > LINT_CONNECTOR_EDGE_MAX_MM) {
59360
58688
  warnings.push({
59361
58689
  kind: "connector_not_on_edge",
59362
58690
  refs: [fp.ref],
59363
- distance_mm: round23(d),
58691
+ distance_mm: round2(d),
59364
58692
  threshold_mm: LINT_CONNECTOR_EDGE_MAX_MM,
59365
- message: `Connector ${fp.ref} sits ${round23(d)}mm from the board edge \u2014 connectors want edge access (<${LINT_CONNECTOR_EDGE_MAX_MM}mm); move it to the outline.`
58693
+ message: `Connector ${fp.ref} sits ${round2(d)}mm from the board edge \u2014 connectors want edge access (<${LINT_CONNECTOR_EDGE_MAX_MM}mm); move it to the outline.`
59366
58694
  });
59367
58695
  }
59368
58696
  }
@@ -59398,9 +58726,9 @@ function layoutLint(pcb) {
59398
58726
  warnings.push({
59399
58727
  kind: "high_current_near_sensitive",
59400
58728
  refs: [c.hi.ref, c.lo.ref],
59401
- distance_mm: round23(c.dist),
58729
+ distance_mm: round2(c.dist),
59402
58730
  threshold_mm: LINT_HIGH_CURRENT_SEP_MIN_MM,
59403
- message: `High-current net '${c.hiNet}' (${c.hi.ref} pad ${c.hi.pad.number}) is ${round23(c.dist)}mm from '${c.loNet}' (${c.lo.ref} pad ${c.lo.pad.number}) \u2014 keep \u2265${LINT_HIGH_CURRENT_SEP_MIN_MM}mm between high-current and USB/analog pads.`
58731
+ message: `High-current net '${c.hiNet}' (${c.hi.ref} pad ${c.hi.pad.number}) is ${round2(c.dist)}mm from '${c.loNet}' (${c.lo.ref} pad ${c.lo.pad.number}) \u2014 keep \u2265${LINT_HIGH_CURRENT_SEP_MIN_MM}mm between high-current and USB/analog pads.`
59404
58732
  });
59405
58733
  }
59406
58734
  }
@@ -63009,7 +62337,7 @@ async function addMotorWinding(args) {
63009
62337
  if (!pad.net) continue;
63010
62338
  const arr = padsByNet.get(pad.net) ?? [];
63011
62339
  arr.push({
63012
- pos: padWorld2(fp, pad),
62340
+ pos: padWorld(fp, pad),
63013
62341
  onReturnLayer: pad.layers.includes(returnLayer),
63014
62342
  size: padApproxRadius(pad)
63015
62343
  });
@@ -63352,7 +62680,7 @@ async function addMotorWinding(args) {
63352
62680
  for (const fp of pcb.footprints) {
63353
62681
  for (const pad of fp.pads) {
63354
62682
  if (pad.net === net) continue;
63355
- const pw = padWorld2(fp, pad);
62683
+ const pw = padWorld(fp, pad);
63356
62684
  if (pointSegDist(pw, t2.a, t2.b) < padApproxRadius(pad) + halfW + clearance) return false;
63357
62685
  }
63358
62686
  }
@@ -65045,7 +64373,7 @@ async function fixDrc(args) {
65045
64373
  let corridorFail = null;
65046
64374
  for (const pt of movePoints) {
65047
64375
  const onPad = pcb.footprints.some(
65048
- (fp) => fp.pads.some((pad) => samePoint(padWorld2(fp, pad), pt))
64376
+ (fp) => fp.pads.some((pad) => samePoint(padWorld(fp, pad), pt))
65049
64377
  );
65050
64378
  if (onPad) {
65051
64379
  corridorFail = "endpoint lands on a component pad \u2014 fixing requires moving the component";
@@ -65195,7 +64523,7 @@ async function fixDrc(args) {
65195
64523
  };
65196
64524
  return { content: [{ type: "text", text: JSON.stringify(payload) }] };
65197
64525
  }
65198
- var PCB_LAYER_SET, COPPER_LAYERS, COPPER_LAYER_SET, POSITION_TOLERANCE, PIN_SEP, pinKey, createSchematicSchema, docInputProperties, placeComponentsSchema, routeNetsSchema, runDrcSchema, runErcSchema, critiqueRouteSchema, routeDiffPairSchema, lengthMatchTracesSchema, exportGerberSchema, validateForFabSchema, exportKicadSchema, addCoilSchema, boardFromSolidSchema, addCoilArraySchema, windingLayoutSchema, calcImpedanceSchema, sizeImpedanceSchema, sizePdnSchema, calcCoilSchema, sizeCoilSchema, calcRfSchema, COIL_GEOMETRY, MU0, RAIL_PIN_NAMES, PIN_TYPES, CHIP_SIZE_CODES, CONNECTIVITY_GUARD_MAX_NETS, POWER_NET_RE, DRC_CATEGORY, DRC_DELTA_SAMPLE_CAP, DRC_DELTA_FULL_BOARD_MAX, LINT_CRYSTAL_MAX_MM, LINT_DECAP_MAX_MM, LINT_CONNECTOR_EDGE_MAX_MM, LINT_HIGH_CURRENT_SEP_MIN_MM, LINT_GROUND_NET_RE, LINT_SUPPLY_NET_RE, SENSITIVE_NET_RE, HIGH_CURRENT_CLASS_RE, refPrefix, dist2d, round23, sumSq, round33, vec2Schema, setBoardOutlineSchema, FOOTPRINT_FAMILIES, listFootprintsSchema, searchFootprintsSchema, getPadPositionsSchema, getFootprintSchema, ROTATION_CONVENTION, describePcbSchema, DESCRIBE_NET_NAME_CAP, addTraceSchema, addViaSchema, OZ_TO_MM, setStackupSchema, setPlacementSchema, addZoneSchema, deleteElementProps, deleteZoneSchema, deleteTraceSchema, deleteViaSchema, COPPER_KINDS, GET_COPPER_CAP, getCopperSchema, TIE_POSITION_TOL, addNetTieSchema, deleteNetTieSchema, undoSchema, setDesignRulesSchema, sizeTraceForCurrentSchema, addViaArraySchema, MAX_VIA_ARRAY, addMotorWindingSchema, calcMotorSchema, sig6, BEARING_FRICTION_MNM, checkSelfStartSchema, solidFromBoardSchema, FR4_DENSITY_KG_M3, COPPER_DENSITY_KG_M3, DEFAULT_COPPER_THICKNESS_MM, loopArea, MOUNTING_FP_RE, KEEPOUT_ECHO_CAP, INLINE_DOC_ECHO_CAP, searchElectronicPartsSchema, resolvePartSchema, findAlternativesSchema, verifySubstitutionSchema, buildReceiptSchema, verifyReceiptSchema, FIX_EPS, FIX_DRC_NEVER, fixDrcSchema, toolDefs20;
64526
+ var PCB_LAYER_SET, COPPER_LAYERS, COPPER_LAYER_SET, POSITION_TOLERANCE, PIN_SEP, pinKey, createSchematicSchema, docInputProperties, placeComponentsSchema, routeNetsSchema, runDrcSchema, runErcSchema, critiqueRouteSchema, routeDiffPairSchema, lengthMatchTracesSchema, exportGerberSchema, validateForFabSchema, exportKicadSchema, addCoilSchema, boardFromSolidSchema, addCoilArraySchema, windingLayoutSchema, calcImpedanceSchema, sizeImpedanceSchema, sizePdnSchema, calcCoilSchema, sizeCoilSchema, calcRfSchema, COIL_GEOMETRY, MU0, RAIL_PIN_NAMES, PIN_TYPES, CHIP_SIZE_CODES, CONNECTIVITY_GUARD_MAX_NETS, POWER_NET_RE, DRC_CATEGORY, DRC_DELTA_SAMPLE_CAP, DRC_DELTA_FULL_BOARD_MAX, LINT_CRYSTAL_MAX_MM, LINT_DECAP_MAX_MM, LINT_CONNECTOR_EDGE_MAX_MM, LINT_HIGH_CURRENT_SEP_MIN_MM, LINT_GROUND_NET_RE, LINT_SUPPLY_NET_RE, SENSITIVE_NET_RE, HIGH_CURRENT_CLASS_RE, refPrefix, dist2d, round2, sumSq, round33, vec2Schema, setBoardOutlineSchema, FOOTPRINT_FAMILIES, listFootprintsSchema, searchFootprintsSchema, getPadPositionsSchema, getFootprintSchema, ROTATION_CONVENTION, describePcbSchema, DESCRIBE_NET_NAME_CAP, addTraceSchema, addViaSchema, OZ_TO_MM, setStackupSchema, setPlacementSchema, addZoneSchema, deleteElementProps, deleteZoneSchema, deleteTraceSchema, deleteViaSchema, COPPER_KINDS, GET_COPPER_CAP, getCopperSchema, TIE_POSITION_TOL, addNetTieSchema, deleteNetTieSchema, undoSchema, setDesignRulesSchema, sizeTraceForCurrentSchema, addViaArraySchema, MAX_VIA_ARRAY, addMotorWindingSchema, calcMotorSchema, sig6, BEARING_FRICTION_MNM, checkSelfStartSchema, solidFromBoardSchema, FR4_DENSITY_KG_M3, COPPER_DENSITY_KG_M3, DEFAULT_COPPER_THICKNESS_MM, loopArea, MOUNTING_FP_RE, KEEPOUT_ECHO_CAP, INLINE_DOC_ECHO_CAP, searchElectronicPartsSchema, resolvePartSchema, findAlternativesSchema, verifySubstitutionSchema, buildReceiptSchema, verifyReceiptSchema, FIX_EPS, FIX_DRC_NEVER, fixDrcSchema, toolDefs20;
65199
64527
  var init_ecad2 = __esm({
65200
64528
  "src/tools/ecad.ts"() {
65201
64529
  "use strict";
@@ -65997,7 +65325,7 @@ var init_ecad2 = __esm({
65997
65325
  HIGH_CURRENT_CLASS_RE = /pwr|power|high[-_ ]?current|motor|\bhv\b|hi[-_ ]?amp/i;
65998
65326
  refPrefix = (ref) => /^[A-Za-z]+/.exec(ref)?.[0]?.toUpperCase() ?? "";
65999
65327
  dist2d = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
66000
- round23 = (n) => Math.round(n * 100) / 100;
65328
+ round2 = (n) => Math.round(n * 100) / 100;
66001
65329
  sumSq = (v) => v.reduce((s, x) => s + x * x, 0);
66002
65330
  round33 = (v) => Math.round(v * 1e3) / 1e3;
66003
65331
  vec2Schema = {
@@ -72618,12 +71946,12 @@ function simulateStrike(input, engine) {
72618
71946
  },
72619
71947
  modes: result.modes.map((m) => ({
72620
71948
  n: m.n,
72621
- hz: round24(m.hz),
71949
+ hz: round22(m.hz),
72622
71950
  gain: round4(m.gain),
72623
71951
  q: Math.round(m.q),
72624
- t60_s: round24(m.t60_s)
71952
+ t60_s: round22(m.t60_s)
72625
71953
  })),
72626
- spectrum_peaks: result.spectrum_peaks.map((p) => ({ hz: round24(p.hz), db: round24(p.db) })),
71954
+ spectrum_peaks: result.spectrum_peaks.map((p) => ({ hz: round22(p.hz), db: round22(p.db) })),
72627
71955
  ...result.verdict ? { verdict: result.verdict } : {},
72628
71956
  limits: "1-D transverse bending only (no torsional/lateral modes); decay Q is a heuristic (material + cord-at-holes), frequencies are not"
72629
71957
  };
@@ -72652,7 +71980,7 @@ function simulateStrike(input, engine) {
72652
71980
  }
72653
71981
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
72654
71982
  }
72655
- var cents, MATERIAL_ALIASES, simulateStrikeSchema, round24, round4, toolDefs39;
71983
+ var cents, MATERIAL_ALIASES, simulateStrikeSchema, round22, round4, toolDefs39;
72656
71984
  var init_acoustics = __esm({
72657
71985
  "src/tools/acoustics.ts"() {
72658
71986
  "use strict";
@@ -72724,7 +72052,7 @@ var init_acoustics = __esm({
72724
72052
  }
72725
72053
  }
72726
72054
  };
72727
- round24 = (x) => Math.round(x * 100) / 100;
72055
+ round22 = (x) => Math.round(x * 100) / 100;
72728
72056
  round4 = (x) => Math.round(x * 1e4) / 1e4;
72729
72057
  toolDefs39 = [
72730
72058
  {
@@ -73128,8 +72456,8 @@ function parseDocument(input) {
73128
72456
  }
73129
72457
  throw new Error("Document must be JSON (starting with '{') or VCode (starting with '#')");
73130
72458
  }
73131
- function compressForUrl(compact) {
73132
- const compressed = gzipSync(Buffer.from(compact, "utf-8"), { level: 9 });
72459
+ function compressForUrl(compact2) {
72460
+ const compressed = gzipSync(Buffer.from(compact2, "utf-8"), { level: 9 });
73133
72461
  return base64urlEncode(compressed);
73134
72462
  }
73135
72463
  function openInBrowser(input) {
@@ -77805,8 +77133,8 @@ var init_server3 = __esm({
77805
77133
  init_order_feed();
77806
77134
  init_animate();
77807
77135
  PKG_VERSION = (() => {
77808
- if ("0.9.4-main.15") {
77809
- return "0.9.4-main.15";
77136
+ if ("0.9.4-main.16") {
77137
+ return "0.9.4-main.16";
77810
77138
  }
77811
77139
  try {
77812
77140
  const req = createRequire2(import.meta.url);
@@ -77815,8 +77143,8 @@ var init_server3 = __esm({
77815
77143
  return "0.0.0";
77816
77144
  }
77817
77145
  })();
77818
- BUILD_SHA = "ba0bbea017e3e720ec8796167b6824f3cdf235cf";
77819
- BUILD_TIME = "2026-07-24T17:18:07.764Z";
77146
+ BUILD_SHA = "63c1a040cf3352ca640144ec217f3f96271180c9";
77147
+ BUILD_TIME = "2026-07-24T17:21:48.535Z";
77820
77148
  SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
77821
77149
  VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
77822
77150
  INSTANCE_ID = randomUUID6().slice(0, 8);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vcad/mcp",
3
- "version": "0.9.4-main.15",
3
+ "version": "0.9.4-main.16",
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": "ba0bbea017e3e720ec8796167b6824f3cdf235cf",
23
- "builtAt": "2026-07-24T17:18:07.764Z"
22
+ "sha": "63c1a040cf3352ca640144ec217f3f96271180c9",
23
+ "builtAt": "2026-07-24T17:21:48.535Z"
24
24
  }
25
25
  }
Binary file