@vcad/mcp 0.9.4-main.40 → 0.9.4-main.41

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.mjs +355 -76
  3. package/package.json +3 -3
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 6cac4f4f2c7c80a9122ee16857ec2b49f2dcd1f4 at 2026-07-26T14:38:17.346Z. Source: https://github.com/ecto/vcad
9
+ Built from b00f071e4b839c6448769107ed84182597d763ed at 2026-07-26T14:41:39.682Z. Source: https://github.com/ecto/vcad
package/index.mjs CHANGED
@@ -8996,6 +8996,38 @@ function costSheetMetalChain(chain, kernel2, rates, quantity2 = 1) {
8996
8996
  }
8997
8997
  return { breakdown: parsed.breakdown, rates: parsed.rates };
8998
8998
  }
8999
+ function flattenSolidToSheetMetal(mesh, kernel2, options = {}) {
9000
+ if (!kernel2.flattenSolidToSheetMetal) {
9001
+ throw new Error("kernel.flattenSolidToSheetMetal not available \u2014 rebuild @vcad/kernel-wasm");
9002
+ }
9003
+ const request = {
9004
+ positions: Array.from(mesh.positions),
9005
+ indices: Array.from(mesh.indices),
9006
+ material: options.material,
9007
+ manualK: options.manualK,
9008
+ shopProfile: options.shopProfile,
9009
+ volumeTolFrac: options.volumeTolFrac,
9010
+ planeAngleTolDeg: options.planeAngleTolDeg,
9011
+ planeOffsetTol: options.planeOffsetTol
9012
+ };
9013
+ const parsed = JSON.parse(kernel2.flattenSolidToSheetMetal(JSON.stringify(request)));
9014
+ if (parsed.error) {
9015
+ throw new Error(parsed.error);
9016
+ }
9017
+ return {
9018
+ flatPattern: parsed.flat_pattern,
9019
+ model: parsed.model,
9020
+ dxf: parsed.dxf,
9021
+ violations: parsed.violations,
9022
+ thickness: parsed.thickness,
9023
+ panels: parsed.panels,
9024
+ bends: parsed.bends,
9025
+ solidVolumeMm3: parsed.solid_volume_mm3,
9026
+ recoveredVolumeMm3: parsed.recovered_volume_mm3,
9027
+ volumeErrorFrac: parsed.volume_error_frac,
9028
+ warnings: parsed.warnings ?? []
9029
+ };
9030
+ }
8999
9031
  var DEFAULT_SHOP_PROFILE, DEFAULT_COST_RATES, DEFAULT_NESTING_PARAMS;
9000
9032
  var init_sheet_metal = __esm({
9001
9033
  "../engine/dist/sheet-metal.js"() {
@@ -11564,6 +11596,7 @@ var init_dist = __esm({
11564
11596
  getSheetMetalBendTable: wasmModule7.getSheetMetalBendTable,
11565
11597
  getSheetMetalShopCatalog: wasmModule7.getSheetMetalShopCatalog,
11566
11598
  sheetMetalFoldedStep: wasmModule7.sheetMetalFoldedStep,
11599
+ flattenSolidToSheetMetal: wasmModule7.flattenSolidToSheetMetal,
11567
11600
  mesh_clearance: wasmModule7.mesh_clearance,
11568
11601
  topologyOptimizeBox: wasmModule7.topologyOptimizeBox,
11569
11602
  topologyOptimizeMesh: wasmModule7.topologyOptimizeMesh,
@@ -12140,6 +12173,12 @@ var init_dist = __esm({
12140
12173
  nestSheetMetalParts(parts, params) {
12141
12174
  return nestSheetMetalParts2(parts, this.kernel, params);
12142
12175
  }
12176
+ /** Recover a flat pattern (panels, bends, DXF) from a solid part's mesh —
12177
+ * the mechanical counterpart of `board_from_solid`. Throws when the solid
12178
+ * is not constant-thickness sheet. */
12179
+ flattenSolidToSheetMetal(mesh, options) {
12180
+ return flattenSolidToSheetMetal(mesh, this.kernel, options);
12181
+ }
12143
12182
  /** Compute a feasible bend sequence (outermost-first) for the
12144
12183
  * sheet-metal part in `doc`. Returns `null` if there is none. */
12145
12184
  sheetMetalSequence(doc) {
@@ -41105,6 +41144,24 @@ var init_CHANGELOG = __esm({
41105
41144
  "export_cad"
41106
41145
  ]
41107
41146
  },
41147
+ {
41148
+ id: "2026-07-26-flat-pattern-from-solid",
41149
+ version: "0.9.4",
41150
+ date: "2026-07-26",
41151
+ category: "feat",
41152
+ title: "Flat patterns from parts you already modelled",
41153
+ summary: "flat_pattern_from_solid turns any constant-thickness solid into a fab-ready DXF plus a bend table \u2014 no sheet-metal re-authoring \u2014 batching a whole assembly into unique patterns x quantity.",
41154
+ features: [
41155
+ "sheet-metal",
41156
+ "manufacturing",
41157
+ "mcp"
41158
+ ],
41159
+ mcpTools: [
41160
+ "flat_pattern_from_solid",
41161
+ "sheet_metal_nest",
41162
+ "sheet_metal_cost"
41163
+ ]
41164
+ },
41108
41165
  {
41109
41166
  id: "2026-07-26-fastener-forms",
41110
41167
  version: "0.9.4",
@@ -47931,13 +47988,13 @@ function computeIntegrity(doc, engine) {
47931
47988
  cx += v * (p1[0] + p2[0] + p3[0]) / 4;
47932
47989
  cy += v * (p1[1] + p2[1] + p3[1]) / 4;
47933
47990
  cz += v * (p1[2] + p2[2] + p3[2]) / 4;
47934
- for (const q of [p1, p2, p3]) {
47935
- bbox.min.x = Math.min(bbox.min.x, q[0]);
47936
- bbox.min.y = Math.min(bbox.min.y, q[1]);
47937
- bbox.min.z = Math.min(bbox.min.z, q[2]);
47938
- bbox.max.x = Math.max(bbox.max.x, q[0]);
47939
- bbox.max.y = Math.max(bbox.max.y, q[1]);
47940
- bbox.max.z = Math.max(bbox.max.z, q[2]);
47991
+ for (const q2 of [p1, p2, p3]) {
47992
+ bbox.min.x = Math.min(bbox.min.x, q2[0]);
47993
+ bbox.min.y = Math.min(bbox.min.y, q2[1]);
47994
+ bbox.min.z = Math.min(bbox.min.z, q2[2]);
47995
+ bbox.max.x = Math.max(bbox.max.x, q2[0]);
47996
+ bbox.max.y = Math.max(bbox.max.y, q2[1]);
47997
+ bbox.max.z = Math.max(bbox.max.z, q2[2]);
47941
47998
  }
47942
47999
  }
47943
48000
  const label = labels[partIndex] ?? `part ${partIndex + 1}`;
@@ -50352,8 +50409,8 @@ function buildPartLabels(doc) {
50352
50409
  });
50353
50410
  }
50354
50411
  function eulerXyzDegToQuat2(rotation) {
50355
- const q = exportKernel().eulerXyzDegToQuat(rotation.x, rotation.y, rotation.z);
50356
- return [q[0], q[1], q[2], q[3]];
50412
+ const q2 = exportKernel().eulerXyzDegToQuat(rotation.x, rotation.y, rotation.z);
50413
+ return [q2[0], q2[1], q2[2], q2[3]];
50357
50414
  }
50358
50415
  function buildGlb(inputMeshes, name, animation) {
50359
50416
  let f32Len = 0;
@@ -51182,9 +51239,9 @@ var init_store = __esm({
51182
51239
  async listOrders(userId, filter) {
51183
51240
  try {
51184
51241
  const limit = typeof filter.limit === "number" ? filter.limit : 50;
51185
- let q = `?user_id=eq.${encodeURIComponent(userId)}&order=created_at.desc&limit=${limit}`;
51186
- if (filter.status) q += `&state=eq.${encodeURIComponent(filter.status)}`;
51187
- const res = await fabricateFetch(this.url("orders", q), {
51242
+ let q2 = `?user_id=eq.${encodeURIComponent(userId)}&order=created_at.desc&limit=${limit}`;
51243
+ if (filter.status) q2 += `&state=eq.${encodeURIComponent(filter.status)}`;
51244
+ const res = await fabricateFetch(this.url("orders", q2), {
51188
51245
  method: "GET",
51189
51246
  headers: this.headers()
51190
51247
  });
@@ -51388,8 +51445,8 @@ var init_store = __esm({
51388
51445
  * can retry before deciding what to say. Never throws. */
51389
51446
  async postRow(table, row, onConflict) {
51390
51447
  try {
51391
- const q = onConflict ? `?on_conflict=${onConflict}` : "";
51392
- const res = await fabricateFetch(this.url(table, q), {
51448
+ const q2 = onConflict ? `?on_conflict=${onConflict}` : "";
51449
+ const res = await fabricateFetch(this.url(table, q2), {
51393
51450
  method: "POST",
51394
51451
  headers: this.headers({
51395
51452
  Prefer: onConflict ? "resolution=merge-duplicates,return=minimal" : "return=minimal"
@@ -53435,6 +53492,7 @@ var init_tool_metadata = __esm({
53435
53492
  // ── Sheet metal ────────────────────────────────────────────────────────
53436
53493
  sheet_metal_create: { title: "Create Sheet Metal", annotations: RW },
53437
53494
  sheet_metal_unfold: { title: "Unfold Sheet Metal", annotations: RO },
53495
+ flat_pattern_from_solid: { title: "Flat Pattern From Solid", annotations: RO },
53438
53496
  sheet_metal_check: { title: "Check Sheet Metal", annotations: RO },
53439
53497
  sheet_metal_materials: { title: "Sheet Metal Materials", annotations: RO },
53440
53498
  sheet_metal_bend_table: { title: "Sheet Metal Bend Table", annotations: RO },
@@ -54750,34 +54808,34 @@ var init_broker = __esm({
54750
54808
  );
54751
54809
  const built = [];
54752
54810
  for (const adapter of eligible) {
54753
- let q;
54811
+ let q2;
54754
54812
  try {
54755
- q = await adapter.quote(req);
54813
+ q2 = await adapter.quote(req);
54756
54814
  } catch {
54757
- q = null;
54815
+ q2 = null;
54758
54816
  }
54759
- if (!q) continue;
54817
+ if (!q2) continue;
54760
54818
  const landed2 = estimateLandedCost({
54761
54819
  region: adapter.region,
54762
54820
  supportsDdp: adapter.supportsDdp
54763
54821
  });
54764
- const markedUp = applyMargin(q.fab_cost_minor);
54822
+ const markedUp = applyMargin(q2.fab_cost_minor);
54765
54823
  const totalMinor = markedUp + landed2.shipping_minor + landed2.duty_minor;
54766
- const orderable = CONTRACTED_FABS.has(adapter.key) && q.pricing_basis === "binding";
54824
+ const orderable = CONTRACTED_FABS.has(adapter.key) && q2.pricing_basis === "binding";
54767
54825
  built.push({
54768
- fabCost: q.fab_cost_minor,
54826
+ fabCost: q2.fab_cost_minor,
54769
54827
  option: {
54770
54828
  fab: adapter.key,
54771
54829
  fab_label: adapter.label,
54772
54830
  region: adapter.region,
54773
54831
  unit_price_minor: Math.round(totalMinor / req.quantity),
54774
54832
  total_minor: totalMinor,
54775
- lead_time_days: q.lead_time_days,
54776
- in_spec: q.in_spec,
54777
- pricing_basis: q.pricing_basis,
54833
+ lead_time_days: q2.lead_time_days,
54834
+ in_spec: q2.in_spec,
54835
+ pricing_basis: q2.pricing_basis,
54778
54836
  supports_ddp: adapter.supportsDdp,
54779
54837
  orderable,
54780
- notes: q.notes
54838
+ notes: q2.notes
54781
54839
  }
54782
54840
  });
54783
54841
  }
@@ -55967,7 +56025,7 @@ function measureGroups(engine, candidates, groupA, groupB) {
55967
56025
  error: `No part with id or name ${missing.map((m) => `"${m}"`).join(", ")}. Available: ${available || "none"}`
55968
56026
  };
55969
56027
  }
55970
- const overlap = a.parts.filter((p) => b.parts.some((q) => q.id === p.id));
56028
+ const overlap = a.parts.filter((p) => b.parts.some((q2) => q2.id === p.id));
55971
56029
  if (overlap.length > 0) {
55972
56030
  return {
55973
56031
  error: `Parts cannot appear in both groups: ${overlap.map((p) => p.id).join(", ")}`
@@ -60127,10 +60185,10 @@ function detectStaleNets(pcb) {
60127
60185
  const onSegBody = (p, t2) => segDist(p, t2) <= t2.width / 2 + 0.02;
60128
60186
  const anchored = (p, selfIdx, halfW) => {
60129
60187
  if (pads.some(
60130
- (q) => Math.hypot(p.x - q.pos.x, p.y - q.pos.y) <= q.halfExtent + halfW + 0.02
60188
+ (q2) => Math.hypot(p.x - q2.pos.x, p.y - q2.pos.y) <= q2.halfExtent + halfW + 0.02
60131
60189
  ))
60132
60190
  return true;
60133
- if (vias.some((q) => Math.hypot(p.x - q.x, p.y - q.y) <= halfW + 0.42)) return true;
60191
+ if (vias.some((q2) => Math.hypot(p.x - q2.x, p.y - q2.y) <= halfW + 0.42)) return true;
60134
60192
  for (let j = 0; j < traces.length; j++) {
60135
60193
  if (j === selfIdx) continue;
60136
60194
  if (segDist(p, traces[j]) <= traces[j].width / 2 + halfW + 0.02) return true;
@@ -61930,7 +61988,7 @@ function seedScan(residual, lo, hi, perDim = 12) {
61930
61988
  const idx = new Array(n).fill(0);
61931
61989
  const total = Math.pow(perDim, n);
61932
61990
  for (let c = 0; c < total; c++) {
61933
- const x = idx.map((q, d) => lo[d] + (hi[d] - lo[d]) * q / (perDim - 1));
61991
+ const x = idx.map((q2, d) => lo[d] + (hi[d] - lo[d]) * q2 / (perDim - 1));
61934
61992
  const cost = sumSq(residual(x));
61935
61993
  if (cost < best.cost) {
61936
61994
  best.cost = cost;
@@ -62471,7 +62529,7 @@ function calcRf(args) {
62471
62529
  return fail2("topology must be 'series_rlc' or 'parallel_rlc'");
62472
62530
  }
62473
62531
  const f0 = 1 / (2 * Math.PI * Math.sqrt(l * c));
62474
- const q = topology === "series_rlc" ? r > 0 ? 1 / r * Math.sqrt(l / c) : Infinity : r * Math.sqrt(c / l);
62532
+ const q2 = topology === "series_rlc" ? r > 0 ? 1 / r * Math.sqrt(l / c) : Infinity : r * Math.sqrt(c / l);
62475
62533
  const fStart = args.f_start_hz ?? f0 * 0.1;
62476
62534
  const fStop = args.f_stop_hz ?? f0 * 10;
62477
62535
  const points = Math.min(256, Math.max(3, Math.round(args.points ?? 21)));
@@ -62517,7 +62575,7 @@ function calcRf(args) {
62517
62575
  success: true,
62518
62576
  topology,
62519
62577
  resonance_hz: Math.round(f0),
62520
- q_factor: Number.isFinite(q) ? r3(q) : 999999,
62578
+ q_factor: Number.isFinite(q2) ? r3(q2) : 999999,
62521
62579
  z_at_resonance_ohm: r3(mag(z0re, z0im)),
62522
62580
  best_match: {
62523
62581
  f_hz: best.f_hz,
@@ -62532,8 +62590,8 @@ function calcRf(args) {
62532
62590
  l_henry: l,
62533
62591
  c_farad: c
62534
62592
  }),
62535
- ...Number.isFinite(q) ? [
62536
- emClaim("q_factor", r3(q), "dimensionless", "rlc-analytic", {
62593
+ ...Number.isFinite(q2) ? [
62594
+ emClaim("q_factor", r3(q2), "dimensionless", "rlc-analytic", {
62537
62595
  topology,
62538
62596
  r_ohm: r,
62539
62597
  l_henry: l,
@@ -63112,17 +63170,17 @@ async function setBoardOutline(args) {
63112
63170
  };
63113
63171
  }
63114
63172
  function scoreFootprintFamily(query, fam) {
63115
- const q = query.toLowerCase().trim();
63116
- if (!q) return 0;
63173
+ const q2 = query.toLowerCase().trim();
63174
+ if (!q2) return 0;
63117
63175
  const cands = [fam.family, fam.label, ...fam.aliases].map((s) => s.toLowerCase());
63118
63176
  let best = 0;
63119
63177
  for (const c of cands) {
63120
- if (c === q) {
63178
+ if (c === q2) {
63121
63179
  best = Math.max(best, 1);
63122
- } else if (c.includes(q) || q.includes(c)) {
63180
+ } else if (c.includes(q2) || q2.includes(c)) {
63123
63181
  best = Math.max(best, 0.75);
63124
63182
  } else {
63125
- const sim = 1 - editDistance(q, c) / Math.max(q.length, c.length);
63183
+ const sim = 1 - editDistance(q2, c) / Math.max(q2.length, c.length);
63126
63184
  best = Math.max(best, sim * 0.6);
63127
63185
  }
63128
63186
  }
@@ -64755,16 +64813,16 @@ function windSegClosestPt(a, b, c, d) {
64755
64813
  };
64756
64814
  let best = [a, clamp(a, c, d)];
64757
64815
  let bestD = Infinity;
64758
- for (const [p, q] of [
64816
+ for (const [p, q2] of [
64759
64817
  [a, clamp(a, c, d)],
64760
64818
  [b, clamp(b, c, d)],
64761
64819
  [c, clamp(c, a, b)],
64762
64820
  [d, clamp(d, a, b)]
64763
64821
  ]) {
64764
- const dd = Math.hypot(p.x - q.x, p.y - q.y);
64822
+ const dd = Math.hypot(p.x - q2.x, p.y - q2.y);
64765
64823
  if (dd < bestD) {
64766
64824
  bestD = dd;
64767
- best = [p, q];
64825
+ best = [p, q2];
64768
64826
  }
64769
64827
  }
64770
64828
  return { x: (best[0].x + best[1].x) / 2, y: (best[0].y + best[1].y) / 2 };
@@ -65382,7 +65440,7 @@ function calcMotorInduction(args, polePairs, turnsPerPhase, windingFactor, inner
65382
65440
  outer_radius_mm: outerR,
65383
65441
  end_effect_factor: endEffect
65384
65442
  };
65385
- const claim2 = (q, v, unit) => emClaim(q, sig6(v), unit, "thin-sheet-induction", inductionInputs);
65443
+ const claim2 = (q2, v, unit) => emClaim(q2, sig6(v), unit, "thin-sheet-induction", inductionInputs);
65386
65444
  return {
65387
65445
  content: [
65388
65446
  {
@@ -65810,11 +65868,11 @@ function polygonCentroid(poly) {
65810
65868
  let cy = 0;
65811
65869
  for (let i = 0; i < poly.length; i++) {
65812
65870
  const p = poly[i];
65813
- const q = poly[(i + 1) % poly.length];
65814
- const cross = p.x * q.y - q.x * p.y;
65871
+ const q2 = poly[(i + 1) % poly.length];
65872
+ const cross = p.x * q2.y - q2.x * p.y;
65815
65873
  a += cross;
65816
- cx += (p.x + q.x) * cross;
65817
- cy += (p.y + q.y) * cross;
65874
+ cx += (p.x + q2.x) * cross;
65875
+ cy += (p.y + q2.y) * cross;
65818
65876
  }
65819
65877
  if (Math.abs(a) < 1e-12) {
65820
65878
  const n = poly.length || 1;
@@ -74210,8 +74268,8 @@ function normaliseEngravings(raw) {
74210
74268
  if (Array.isArray(p) && p.length >= 2)
74211
74269
  return { x: Number(p[0]), y: Number(p[1]) };
74212
74270
  if (p && typeof p === "object" && "x" in p && "y" in p) {
74213
- const q = p;
74214
- return { x: Number(q.x), y: Number(q.y) };
74271
+ const q2 = p;
74272
+ return { x: Number(q2.x), y: Number(q2.y) };
74215
74273
  }
74216
74274
  return null;
74217
74275
  }).filter((p) => p !== null) : [];
@@ -74876,6 +74934,222 @@ var init_sheet_metal2 = __esm({
74876
74934
  }
74877
74935
  });
74878
74936
 
74937
+ // src/tools/flat-pattern.ts
74938
+ function q(v, dp = 3) {
74939
+ const f = 10 ** dp;
74940
+ const r = Math.round(v * f) / f;
74941
+ return r === 0 ? 0 : r;
74942
+ }
74943
+ function ringSignature(ring) {
74944
+ const n = ring.length;
74945
+ if (n < 3) return "";
74946
+ const terms = [];
74947
+ for (let i = 0; i < n; i++) {
74948
+ const a = ring[(i + n - 1) % n];
74949
+ const b = ring[i];
74950
+ const c = ring[(i + 1) % n];
74951
+ const e1 = [b[0] - a[0], b[1] - a[1]];
74952
+ const e2 = [c[0] - b[0], c[1] - b[1]];
74953
+ const len = Math.hypot(e2[0], e2[1]);
74954
+ const turn = Math.atan2(
74955
+ e1[0] * e2[1] - e1[1] * e2[0],
74956
+ e1[0] * e2[0] + e1[1] * e2[1]
74957
+ );
74958
+ terms.push(`${q(len, 2)}:${q(turn, 3)}`);
74959
+ }
74960
+ let best = null;
74961
+ for (let r = 0; r < n; r++) {
74962
+ const cand = terms.slice(r).concat(terms.slice(0, r)).join(",");
74963
+ if (best === null || cand < best) best = cand;
74964
+ }
74965
+ return best ?? "";
74966
+ }
74967
+ function patternSignature(r) {
74968
+ const sil = r.flatPattern.silhouette_2d ?? [];
74969
+ const outer = ringSignature(sil[0] ?? []);
74970
+ const holes = sil.slice(1).map((h) => ringSignature(h)).sort().join("|");
74971
+ const bends = r.bends.map((b) => `${q(b.angle_deg, 2)}/${q(b.radius, 3)}/${q(b.length, 2)}/${b.direction}`).sort().join("|");
74972
+ return `t${q(r.thickness)}|${outer}|H:${holes}|B:${bends}`;
74973
+ }
74974
+ function solidParts(doc, engine) {
74975
+ const scene = engine.evaluate(doc);
74976
+ const visibleRoots = doc.roots.filter((e) => e.visible !== false);
74977
+ const out = [];
74978
+ for (let i = 0; i < scene.parts.length && i < visibleRoots.length; i++) {
74979
+ const rootId = visibleRoots[i].root;
74980
+ const node = doc.nodes[String(rootId)];
74981
+ const mesh = scene.parts[i].mesh;
74982
+ if (!mesh || mesh.positions.length === 0) continue;
74983
+ out.push({ rootId, name: node?.name ?? void 0, mesh });
74984
+ }
74985
+ return out;
74986
+ }
74987
+ function flatPatternFromSolid(input, engine) {
74988
+ const a = input ?? {};
74989
+ const doc = getSession(String(a.document_id ?? ""));
74990
+ const includeDxf = a.include_dxf !== false;
74991
+ const options = {
74992
+ material: typeof a.material === "string" ? a.material : void 0,
74993
+ manualK: typeof a.manual_k === "number" ? a.manual_k : void 0,
74994
+ shopProfile: typeof a.shop_profile === "string" ? a.shop_profile : void 0,
74995
+ volumeTolFrac: typeof a.volume_tolerance === "number" ? a.volume_tolerance : void 0
74996
+ };
74997
+ const all = solidParts(doc, engine);
74998
+ if (all.length === 0) {
74999
+ throw new Error("Document has no solid parts to flatten");
75000
+ }
75001
+ const partId = a.part_id !== void 0 ? String(a.part_id) : void 0;
75002
+ const targets = partId ? all.filter((p) => String(p.rootId) === partId) : all;
75003
+ if (targets.length === 0) {
75004
+ throw new Error(
75005
+ `No part with id "${partId}". Available: ${all.map((c) => `${c.rootId}${c.name ? ` (${c.name})` : ""}`).join(", ")}`
75006
+ );
75007
+ }
75008
+ const groups = /* @__PURE__ */ new Map();
75009
+ const failed = [];
75010
+ for (const part of targets) {
75011
+ let r;
75012
+ try {
75013
+ r = engine.flattenSolidToSheetMetal(part.mesh, options);
75014
+ } catch (e) {
75015
+ failed.push({
75016
+ root_id: part.rootId,
75017
+ ...part.name ? { name: part.name } : {},
75018
+ reason: e instanceof Error ? e.message : String(e)
75019
+ });
75020
+ continue;
75021
+ }
75022
+ const sig = patternSignature(r);
75023
+ const existing = groups.get(sig);
75024
+ if (existing) {
75025
+ existing.quantity += 1;
75026
+ existing.parts.push({
75027
+ root_id: part.rootId,
75028
+ ...part.name ? { name: part.name } : {}
75029
+ });
75030
+ continue;
75031
+ }
75032
+ const [minX, minY, maxX, maxY] = r.flatPattern.bbox;
75033
+ const creases = r.flatPattern.creases ?? [];
75034
+ groups.set(sig, {
75035
+ pattern_id: `pattern-${groups.size + 1}`,
75036
+ quantity: 1,
75037
+ parts: [{ root_id: part.rootId, ...part.name ? { name: part.name } : {} }],
75038
+ thickness_mm: r.thickness,
75039
+ flat: {
75040
+ bbox: r.flatPattern.bbox,
75041
+ area_mm2: r.flatPattern.area_mm2,
75042
+ width_mm: Math.abs(maxX - minX),
75043
+ height_mm: Math.abs(maxY - minY)
75044
+ },
75045
+ panels: r.panels,
75046
+ bend_table: r.bends.map((b, i) => ({
75047
+ bend: b.bend,
75048
+ angle_deg: b.angle_deg,
75049
+ radius_mm: b.radius,
75050
+ length_mm: b.length,
75051
+ direction: b.direction,
75052
+ k_factor: b.k_factor,
75053
+ allowance_mm: b.angle_deg * Math.PI / 180 * (b.radius + b.k_factor * r.thickness),
75054
+ line: creases[i] ? creases[i].line : []
75055
+ })),
75056
+ verification: {
75057
+ solid_volume_mm3: r.solidVolumeMm3,
75058
+ recovered_volume_mm3: r.recoveredVolumeMm3,
75059
+ error_frac: r.volumeErrorFrac,
75060
+ status: "verified"
75061
+ },
75062
+ violations: r.violations,
75063
+ warnings: r.warnings,
75064
+ ...includeDxf ? { dxf: r.dxf } : {}
75065
+ });
75066
+ }
75067
+ const patterns = [...groups.values()];
75068
+ if (patterns.length === 0) {
75069
+ throw new Error(
75070
+ `No part in this document is constant-thickness sheet. ${failed.map((f) => `${f.name ?? f.root_id}: ${f.reason}`).join("; ")}`
75071
+ );
75072
+ }
75073
+ const totalParts = patterns.reduce((s, p) => s + p.quantity, 0);
75074
+ const bends = patterns.reduce((s, p) => s + p.bend_table.length * p.quantity, 0);
75075
+ return {
75076
+ content: [
75077
+ {
75078
+ type: "text",
75079
+ text: JSON.stringify(
75080
+ {
75081
+ success: true,
75082
+ patterns,
75083
+ ...failed.length > 0 ? { not_sheet_metal: failed } : {},
75084
+ nest_input: patterns.map((p) => ({
75085
+ name: p.pattern_id,
75086
+ width_mm: p.flat.width_mm,
75087
+ height_mm: p.flat.height_mm,
75088
+ quantity: p.quantity
75089
+ })),
75090
+ summary: `${patterns.length} unique pattern(s) covering ${totalParts} part(s), ${bends} bend(s) total${failed.length > 0 ? `; ${failed.length} part(s) are not sheet metal` : ""}. Every pattern is volume-verified against its solid.`,
75091
+ note: "Each DXF is a fab-ready merged silhouette in millimetres: one closed exterior polyline + hole loops on CUT, DASHED bend centerlines on BEND_UP/BEND_DOWN. DXF carries no bend angles \u2014 use `bend_table` (angle, inside radius, direction, allowance, crease line) when entering them in the fab's UI. Hand `nest_input` to sheet_metal_nest, then sheet_metal_cost, for a quote."
75092
+ },
75093
+ null,
75094
+ 2
75095
+ )
75096
+ }
75097
+ ]
75098
+ };
75099
+ }
75100
+ var flatPatternFromSolidSchema, toolDefs41;
75101
+ var init_flat_pattern = __esm({
75102
+ "src/tools/flat-pattern.ts"() {
75103
+ "use strict";
75104
+ init_session2();
75105
+ init_tool_def();
75106
+ flatPatternFromSolidSchema = {
75107
+ type: "object",
75108
+ properties: {
75109
+ document_id: {
75110
+ type: "string",
75111
+ description: "CAD session id (open_document / create_cad_loon) holding the solid(s)."
75112
+ },
75113
+ part_id: {
75114
+ type: "string",
75115
+ description: "Root id of a single part to flatten (from `read`). Omit to flatten every solid part in the document and group identical parts into one pattern \xD7 quantity."
75116
+ },
75117
+ material: {
75118
+ type: "string",
75119
+ description: 'Material key for K-factor lookup (e.g. "al-soft", "steel-mild"). Default "al-soft". See sheet_metal_materials.'
75120
+ },
75121
+ manual_k: {
75122
+ type: "number",
75123
+ description: "Override the K-factor for every recovered bend (skips the bend table)."
75124
+ },
75125
+ shop_profile: {
75126
+ type: "string",
75127
+ description: 'Fab-service catalog id (e.g. "sendcutsend") to run manufacturability against. Default: generic shop.'
75128
+ },
75129
+ volume_tolerance: {
75130
+ type: "number",
75131
+ description: "Max relative volume error the round-trip check accepts (default 0.02). Re-extruding the emitted profile by the detected thickness must reproduce the solid's volume; a mismatch fails the part."
75132
+ },
75133
+ include_dxf: {
75134
+ type: "boolean",
75135
+ description: "Include the DXF string per pattern. Default true."
75136
+ }
75137
+ },
75138
+ required: ["document_id"]
75139
+ };
75140
+ toolDefs41 = [
75141
+ {
75142
+ name: "flat_pattern_from_solid",
75143
+ pack: "sheet_metal",
75144
+ description: "Emit the flat pattern (DXF + bend table) for a part that was modelled as an ordinary solid \u2014 extruded sketch, boolean, imported STEP \u2014 with no sheet-metal authoring required. The mechanical counterpart of board_from_solid: it detects the constant-thickness walls and the cylindrical bends between them, returns the cut profile (outer boundary + every interior hole) with the detected thickness, and a bend table (line positions, angles, direction, allowance) for bent parts. Called without part_id it batches the whole document and groups identical parts into one pattern \xD7 quantity (mirror images stay separate). Fails closed: re-extruding the emitted profile by the detected thickness must reproduce the solid's volume, so a part that isn't really constant-thickness sheet is reported as such instead of yielding a wrong outline.",
75145
+ inputSchema: flatPatternFromSolidSchema,
75146
+ handler: (a, c) => flatPatternFromSolid(a, c.engine),
75147
+ behavior: behavior({ geometry: true })
75148
+ }
75149
+ ];
75150
+ }
75151
+ });
75152
+
74879
75153
  // src/tools/acoustics.ts
74880
75154
  import { writeFileSync as writeFileSync5 } from "node:fs";
74881
75155
  function lookupMaterial(engine, name) {
@@ -75044,7 +75318,7 @@ function simulateStrike(input, engine) {
75044
75318
  }
75045
75319
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
75046
75320
  }
75047
- var cents, MATERIAL_ALIASES, simulateStrikeSchema, round23, round4, toolDefs41;
75321
+ var cents, MATERIAL_ALIASES, simulateStrikeSchema, round23, round4, toolDefs42;
75048
75322
  var init_acoustics = __esm({
75049
75323
  "src/tools/acoustics.ts"() {
75050
75324
  "use strict";
@@ -75118,7 +75392,7 @@ var init_acoustics = __esm({
75118
75392
  };
75119
75393
  round23 = (x) => Math.round(x * 100) / 100;
75120
75394
  round4 = (x) => Math.round(x * 1e4) / 1e4;
75121
- toolDefs41 = [
75395
+ toolDefs42 = [
75122
75396
  {
75123
75397
  name: "simulate_strike",
75124
75398
  pack: "sheet_metal",
@@ -75269,7 +75543,7 @@ function importStep(input, engine) {
75269
75543
  ]
75270
75544
  };
75271
75545
  }
75272
- var MAX_STEP_BYTES, importStepSchema, toolDefs42;
75546
+ var MAX_STEP_BYTES, importStepSchema, toolDefs43;
75273
75547
  var init_import = __esm({
75274
75548
  "src/tools/import.ts"() {
75275
75549
  "use strict";
@@ -75301,7 +75575,7 @@ var init_import = __esm({
75301
75575
  }
75302
75576
  }
75303
75577
  };
75304
- toolDefs42 = [
75578
+ toolDefs43 = [
75305
75579
  {
75306
75580
  name: "import_step",
75307
75581
  pack: null,
@@ -75429,7 +75703,7 @@ function importEagle(_input) {
75429
75703
  "Eagle (.brd) import is not yet supported. Export your board from Eagle as KiCad (File > Export > KiCad .kicad_pcb) and use import_kicad instead."
75430
75704
  );
75431
75705
  }
75432
- var MAX_PCB_BYTES, importKicadSchema, importEagleSchema, toolDefs43;
75706
+ var MAX_PCB_BYTES, importKicadSchema, importEagleSchema, toolDefs44;
75433
75707
  var init_import_pcb = __esm({
75434
75708
  "src/tools/import-pcb.ts"() {
75435
75709
  "use strict";
@@ -75470,7 +75744,7 @@ var init_import_pcb = __esm({
75470
75744
  }
75471
75745
  }
75472
75746
  };
75473
- toolDefs43 = [
75747
+ toolDefs44 = [
75474
75748
  {
75475
75749
  name: "import_kicad",
75476
75750
  pack: "ecad",
@@ -75566,7 +75840,7 @@ Compressed URL param: ${encoded.length} bytes`
75566
75840
  ]
75567
75841
  };
75568
75842
  }
75569
- var openInBrowserSchema, toolDefs44;
75843
+ var openInBrowserSchema, toolDefs45;
75570
75844
  var init_share = __esm({
75571
75845
  "src/tools/share.ts"() {
75572
75846
  "use strict";
@@ -75590,7 +75864,7 @@ var init_share = __esm({
75590
75864
  }
75591
75865
  }
75592
75866
  };
75593
- toolDefs44 = [
75867
+ toolDefs45 = [
75594
75868
  {
75595
75869
  name: "open_in_browser",
75596
75870
  pack: null,
@@ -76023,7 +76297,7 @@ function batchReset(input) {
76023
76297
  };
76024
76298
  }
76025
76299
  }
76026
- var simulations, MAX_TRAJECTORY, envRecords, batchGroups, INLINE_DOC_DESC, SESSION_DOC_DESC, createRobotEnvSchema, gymStepSchema, gymResetSchema, gymObserveSchema, gymCloseSchema, batchCreateEnvsSchema, batchStepSchema, batchResetSchema, toolDefs45;
76300
+ var simulations, MAX_TRAJECTORY, envRecords, batchGroups, INLINE_DOC_DESC, SESSION_DOC_DESC, createRobotEnvSchema, gymStepSchema, gymResetSchema, gymObserveSchema, gymCloseSchema, batchCreateEnvsSchema, batchStepSchema, batchResetSchema, toolDefs46;
76027
76301
  var init_gym = __esm({
76028
76302
  "src/tools/gym.ts"() {
76029
76303
  "use strict";
@@ -76185,7 +76459,7 @@ var init_gym = __esm({
76185
76459
  },
76186
76460
  required: ["batch_id"]
76187
76461
  };
76188
- toolDefs45 = [
76462
+ toolDefs46 = [
76189
76463
  {
76190
76464
  name: "create_robot_env",
76191
76465
  pack: "physics",
@@ -76543,7 +76817,7 @@ async function renderMolecule(input) {
76543
76817
  return fail(err7);
76544
76818
  }
76545
76819
  }
76546
- var loadStructureSchema, inspectMoleculeSchema, configSchema, minimizeEnergySchema, mdRunSchema, designMaterialSchema, homogenizeMaterialSchema, renderMoleculeSchema, CPK, DEFAULT_RADIUS, toolDefs46;
76820
+ var loadStructureSchema, inspectMoleculeSchema, configSchema, minimizeEnergySchema, mdRunSchema, designMaterialSchema, homogenizeMaterialSchema, renderMoleculeSchema, CPK, DEFAULT_RADIUS, toolDefs47;
76547
76821
  var init_atoms2 = __esm({
76548
76822
  "src/tools/atoms.ts"() {
76549
76823
  "use strict";
@@ -76678,7 +76952,7 @@ var init_atoms2 = __esm({
76678
76952
  S: 1.05,
76679
76953
  P: 1.07
76680
76954
  };
76681
- toolDefs46 = [
76955
+ toolDefs47 = [
76682
76956
  {
76683
76957
  name: "load_structure",
76684
76958
  pack: "atoms",
@@ -77886,7 +78160,7 @@ function errorResult(text) {
77886
78160
  isError: true
77887
78161
  };
77888
78162
  }
77889
- var recordSimulationSchema, SVG_SCALE2, MAX_STEPS, MIN_STEPS, DEFAULT_WIDTH_PX2, MIN_WIDTH_PX, MAX_WIDTH_PX, DEFAULT_FPS, MIN_FPS, MAX_FPS, toolDefs47;
78163
+ var recordSimulationSchema, SVG_SCALE2, MAX_STEPS, MIN_STEPS, DEFAULT_WIDTH_PX2, MIN_WIDTH_PX, MAX_WIDTH_PX, DEFAULT_FPS, MIN_FPS, MAX_FPS, toolDefs48;
77890
78164
  var init_record2 = __esm({
77891
78165
  "src/tools/record.ts"() {
77892
78166
  "use strict";
@@ -77950,7 +78224,7 @@ var init_record2 = __esm({
77950
78224
  DEFAULT_FPS = 30;
77951
78225
  MIN_FPS = 1;
77952
78226
  MAX_FPS = 60;
77953
- toolDefs47 = [
78227
+ toolDefs48 = [
77954
78228
  {
77955
78229
  name: "record_simulation",
77956
78230
  pack: "physics",
@@ -78043,7 +78317,7 @@ ${text}`
78043
78317
  ]
78044
78318
  };
78045
78319
  }
78046
- var getChangelogSchema, toolDefs48;
78320
+ var getChangelogSchema, toolDefs49;
78047
78321
  var init_changelog2 = __esm({
78048
78322
  "src/tools/changelog.ts"() {
78049
78323
  "use strict";
@@ -78083,7 +78357,7 @@ var init_changelog2 = __esm({
78083
78357
  }
78084
78358
  }
78085
78359
  };
78086
- toolDefs48 = [
78360
+ toolDefs49 = [
78087
78361
  {
78088
78362
  name: "get_changelog",
78089
78363
  pack: null,
@@ -78232,7 +78506,7 @@ function getSimVersion(input) {
78232
78506
  ]
78233
78507
  };
78234
78508
  }
78235
- var getSimReplaySchema, getSimVersionSchema, toolDefs49;
78509
+ var getSimReplaySchema, getSimVersionSchema, toolDefs50;
78236
78510
  var init_sim_replay = __esm({
78237
78511
  "src/tools/sim-replay.ts"() {
78238
78512
  "use strict";
@@ -78260,7 +78534,7 @@ var init_sim_replay = __esm({
78260
78534
  },
78261
78535
  required: ["env_id"]
78262
78536
  };
78263
- toolDefs49 = [
78537
+ toolDefs50 = [
78264
78538
  {
78265
78539
  name: "get_sim_replay",
78266
78540
  pack: null,
@@ -78379,7 +78653,7 @@ async function getOrderFeed(input, store, user) {
78379
78653
  version: version2
78380
78654
  });
78381
78655
  }
78382
- var toUsd4, getOrderFeedSchema, toolDefs50;
78656
+ var toUsd4, getOrderFeedSchema, toolDefs51;
78383
78657
  var init_order_feed = __esm({
78384
78658
  "src/tools/order-feed.ts"() {
78385
78659
  "use strict";
@@ -78398,7 +78672,7 @@ var init_order_feed = __esm({
78398
78672
  },
78399
78673
  required: ["document_id"]
78400
78674
  };
78401
- toolDefs50 = [
78675
+ toolDefs51 = [
78402
78676
  {
78403
78677
  name: "get_order_feed",
78404
78678
  pack: "fabricate",
@@ -79300,7 +79574,7 @@ ${issues.map((i) => i.problem).join("\n")}`
79300
79574
  note: "Rollout compiled to the document timeline. Preview with render_sequence, ship with export_video."
79301
79575
  });
79302
79576
  }
79303
- var MAX_FRAMES, MAX_GEO_SAMPLES, MAX_VERIFY_FRAMES, INLINE_GLB_MAX_BASE64, DEFAULT_WIDTH_PX3, MIN_WIDTH_PX2, MAX_WIDTH_PX2, SVG_SCALE3, KEYFRAMES_DESCRIPTION, animateSchema, renderSequenceSchema, CAMERA_NODE, exportVideoSchema, ffmpegAvailable, timelineFromSimulationSchema, toolDefs51;
79577
+ var MAX_FRAMES, MAX_GEO_SAMPLES, MAX_VERIFY_FRAMES, INLINE_GLB_MAX_BASE64, DEFAULT_WIDTH_PX3, MIN_WIDTH_PX2, MAX_WIDTH_PX2, SVG_SCALE3, KEYFRAMES_DESCRIPTION, animateSchema, renderSequenceSchema, CAMERA_NODE, exportVideoSchema, ffmpegAvailable, timelineFromSimulationSchema, toolDefs52;
79304
79578
  var init_animate = __esm({
79305
79579
  "src/tools/animate.ts"() {
79306
79580
  "use strict";
@@ -79452,7 +79726,7 @@ var init_animate = __esm({
79452
79726
  },
79453
79727
  required: ["env_id", "document_id"]
79454
79728
  };
79455
- toolDefs51 = [
79729
+ toolDefs52 = [
79456
79730
  {
79457
79731
  name: "animate",
79458
79732
  pack: null,
@@ -79514,6 +79788,7 @@ function buildInstructions(kernelPrompt) {
79514
79788
  "- See your work with `render_view` (isometric PNG); measure with `inspect_cad` (whole-document volume, area, bbox, center of mass), `inspect_part` / `describe_scene` (per-part bbox, size, center, material), and `measure` (min distance/overlap between two parts, or one part's bbox+volume+center of mass).",
79515
79789
  "- Ship with `export_cad` (STL/GLB/STEP) or `open_in_browser` (vcad.io deep link).",
79516
79790
  "- Fix in place: when geometry is wrong, prefer `update` on the offending node over deleting parts and starting over.",
79791
+ "- Cut sheet parts you already modelled: `flat_pattern_from_solid` turns any constant-thickness solid \u2014 extruded sketch, boolean, imported STEP \u2014 into a fab-ready DXF plus a bend table, no sheet-metal re-authoring. Called without `part_id` it does the whole document and groups identical parts into one pattern \xD7 quantity; hand its `nest_input` to `sheet_metal_nest`, then `sheet_metal_cost`, for a quote. It fails closed: the emitted profile re-extruded by the detected thickness must reproduce the solid's volume.",
79517
79792
  "- Deliver the project bill of materials with `bom_create` \u2192 `bom_export` (markdown/CSV/JSON with landed-cost totals): link quote_manufacturing quotes on manufactured lines, and source COTS hardware (bearings, shafts, standoffs, screws, ferrite magnets) with `search_mechanical_parts`. All BOM prices are estimates and flagged as such.",
79518
79793
  "",
79519
79794
  "",
@@ -80130,6 +80405,7 @@ async function createServer(existingEngine, context = { user: null }) {
80130
80405
  function slimPreviewForInlineUi(result, docId, toolName, clientHasInlineUi) {
80131
80406
  if (!clientHasInlineUi) return;
80132
80407
  if (toolName === "sheet_metal_unfold") return;
80408
+ if (toolName === "flat_pattern_from_solid") return;
80133
80409
  if (PURE_JSON_RESULT_TOOLS.has(toolName)) return;
80134
80410
  const alwaysSlim = toolName === "create_cad_loon";
80135
80411
  const totalChars = result.content.reduce(
@@ -80368,6 +80644,7 @@ var init_server3 = __esm({
80368
80644
  init_loon_macros();
80369
80645
  init_dfm2();
80370
80646
  init_sheet_metal2();
80647
+ init_flat_pattern();
80371
80648
  init_acoustics();
80372
80649
  init_import();
80373
80650
  init_import_pcb();
@@ -80382,8 +80659,8 @@ var init_server3 = __esm({
80382
80659
  init_order_feed();
80383
80660
  init_animate();
80384
80661
  PKG_VERSION = (() => {
80385
- if ("0.9.4-main.40") {
80386
- return "0.9.4-main.40";
80662
+ if ("0.9.4-main.41") {
80663
+ return "0.9.4-main.41";
80387
80664
  }
80388
80665
  try {
80389
80666
  const req = createRequire2(import.meta.url);
@@ -80392,8 +80669,8 @@ var init_server3 = __esm({
80392
80669
  return "0.0.0";
80393
80670
  }
80394
80671
  })();
80395
- BUILD_SHA = "6cac4f4f2c7c80a9122ee16857ec2b49f2dcd1f4";
80396
- BUILD_TIME = "2026-07-26T14:38:17.346Z";
80672
+ BUILD_SHA = "b00f071e4b839c6448769107ed84182597d763ed";
80673
+ BUILD_TIME = "2026-07-26T14:41:39.682Z";
80397
80674
  SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
80398
80675
  VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
80399
80676
  INSTANCE_ID = randomUUID6().slice(0, 8);
@@ -80466,11 +80743,12 @@ var init_server3 = __esm({
80466
80743
  ...toolDefs46,
80467
80744
  ...toolDefs47,
80468
80745
  ...toolDefs48,
80746
+ ...toolDefs49,
80469
80747
  ...toolDefs20,
80470
80748
  ...toolDefs19,
80471
- ...toolDefs49,
80472
80749
  ...toolDefs50,
80473
- ...toolDefs51
80750
+ ...toolDefs51,
80751
+ ...toolDefs52
80474
80752
  ];
80475
80753
  LIST_TOOL_ORDER = [
80476
80754
  // ── Session lifecycle ──────────────────────────────────────
@@ -80577,6 +80855,7 @@ var init_server3 = __esm({
80577
80855
  "sheet_metal_suggest_fix",
80578
80856
  "sheet_metal_sequence",
80579
80857
  "sheet_metal_nest",
80858
+ "flat_pattern_from_solid",
80580
80859
  "simulate_strike",
80581
80860
  // ── Import + share ─────────────────────────────────────────
80582
80861
  "import_step",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vcad/mcp",
3
- "version": "0.9.4-main.40",
3
+ "version": "0.9.4-main.41",
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": "6cac4f4f2c7c80a9122ee16857ec2b49f2dcd1f4",
23
- "builtAt": "2026-07-26T14:38:17.346Z"
22
+ "sha": "b00f071e4b839c6448769107ed84182597d763ed",
23
+ "builtAt": "2026-07-26T14:41:39.682Z"
24
24
  }
25
25
  }