@vcad/mcp 0.9.4-main.39 → 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.
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 08cd95d4487b522463d0a1a3b58b84935a1f7d08 at 2026-07-26T13:56:33.334Z. 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
@@ -165,6 +165,7 @@ __export(vcad_kernel_wasm_exports, {
165
165
  estimate_cost_for_process: () => estimate_cost_for_process,
166
166
  eulerXyzDegToQuat: () => eulerXyzDegToQuat,
167
167
  evalVcadSource: () => evalVcadSource,
168
+ evalVcadSourceParametric: () => evalVcadSourceParametric,
168
169
  evalVcadSourceWithModules: () => evalVcadSourceWithModules,
169
170
  evaluateDocument: () => evaluateDocument,
170
171
  evaluateSheetMetalChain: () => evaluateSheetMetalChain,
@@ -1777,6 +1778,17 @@ function evalVcadSource(source) {
1777
1778
  }
1778
1779
  return takeFromExternrefTable0(ret[0]);
1779
1780
  }
1781
+ function evalVcadSourceParametric(source, modules_json) {
1782
+ const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1783
+ const len0 = WASM_VECTOR_LEN;
1784
+ var ptr1 = isLikeNone(modules_json) ? 0 : passStringToWasm0(modules_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1785
+ var len1 = WASM_VECTOR_LEN;
1786
+ const ret = wasm.evalVcadSourceParametric(ptr0, len0, ptr1, len1);
1787
+ if (ret[2]) {
1788
+ throw takeFromExternrefTable0(ret[1]);
1789
+ }
1790
+ return takeFromExternrefTable0(ret[0]);
1791
+ }
1780
1792
  function evalVcadSourceWithModules(source, modules_json) {
1781
1793
  const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1782
1794
  const len0 = WASM_VECTOR_LEN;
@@ -8984,6 +8996,38 @@ function costSheetMetalChain(chain, kernel2, rates, quantity2 = 1) {
8984
8996
  }
8985
8997
  return { breakdown: parsed.breakdown, rates: parsed.rates };
8986
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
+ }
8987
9031
  var DEFAULT_SHOP_PROFILE, DEFAULT_COST_RATES, DEFAULT_NESTING_PARAMS;
8988
9032
  var init_sheet_metal = __esm({
8989
9033
  "../engine/dist/sheet-metal.js"() {
@@ -11526,6 +11570,7 @@ var init_dist = __esm({
11526
11570
  WasmAnnotationLayer: wasmModule7.WasmAnnotationLayer,
11527
11571
  projectMesh: wasmModule7.projectMesh,
11528
11572
  importStepBuffer: wasmModule7.importStepBuffer,
11573
+ documentToStepBuffer: wasmModule7.documentToStepBuffer,
11529
11574
  importUrdfBuffer: wasmModule7.importUrdfBuffer,
11530
11575
  exportProjectedViewToDxf: wasmModule7.exportProjectedViewToDxf,
11531
11576
  offsetSectionMesh: wasmModule7.offsetSectionMesh,
@@ -11551,6 +11596,7 @@ var init_dist = __esm({
11551
11596
  getSheetMetalBendTable: wasmModule7.getSheetMetalBendTable,
11552
11597
  getSheetMetalShopCatalog: wasmModule7.getSheetMetalShopCatalog,
11553
11598
  sheetMetalFoldedStep: wasmModule7.sheetMetalFoldedStep,
11599
+ flattenSolidToSheetMetal: wasmModule7.flattenSolidToSheetMetal,
11554
11600
  mesh_clearance: wasmModule7.mesh_clearance,
11555
11601
  topologyOptimizeBox: wasmModule7.topologyOptimizeBox,
11556
11602
  topologyOptimizeMesh: wasmModule7.topologyOptimizeMesh,
@@ -12127,6 +12173,12 @@ var init_dist = __esm({
12127
12173
  nestSheetMetalParts(parts, params) {
12128
12174
  return nestSheetMetalParts2(parts, this.kernel, params);
12129
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
+ }
12130
12182
  /** Compute a feasible bend sequence (outermost-first) for the
12131
12183
  * sheet-metal part in `doc`. Returns `null` if there is none. */
12132
12184
  sheetMetalSequence(doc) {
@@ -12202,6 +12254,19 @@ var init_dist = __esm({
12202
12254
  return null;
12203
12255
  return foldedSheetMetalStep(chain, this.kernel);
12204
12256
  }
12257
+ /**
12258
+ * Export the document's scene roots to a STEP AP214 buffer, preserving
12259
+ * BRep through booleans, transforms, fillets, and sweeps — one STEP body
12260
+ * per visible root. Throws when a root evaluated to mesh-only geometry
12261
+ * (the kernel error names the offending roots) or when the loaded WASM
12262
+ * kernel predates the binding.
12263
+ */
12264
+ documentStep(doc) {
12265
+ if (!this.kernel.documentToStepBuffer) {
12266
+ throw new Error("documentToStepBuffer is not available in this kernel build \u2014 rebuild the WASM kernel");
12267
+ }
12268
+ return this.kernel.documentToStepBuffer(JSON.stringify(doc));
12269
+ }
12205
12270
  /** Create a detail view (magnified region) from a projected view.
12206
12271
  *
12207
12272
  * @param view - The parent projected view
@@ -40977,6 +41042,23 @@ var init_CHANGELOG = __esm({
40977
41042
  "verify_receipt"
40978
41043
  ]
40979
41044
  },
41045
+ {
41046
+ id: "2026-07-26-step-export-booleans",
41047
+ version: "0.9.4",
41048
+ date: "2026-07-26",
41049
+ category: "feat",
41050
+ title: "STEP export works on real parts, booleans included",
41051
+ summary: "STEP AP214 export now serializes the evaluated BRep \u2014 booleans, transforms, fillets, and sweeps keep analytic faces \u2014 with one named body per part; mesh-only parts are refused by name.",
41052
+ features: [
41053
+ "step",
41054
+ "export",
41055
+ "booleans",
41056
+ "cnc"
41057
+ ],
41058
+ mcpTools: [
41059
+ "export_cad"
41060
+ ]
41061
+ },
40980
41062
  {
40981
41063
  id: "2026-07-26-render-loon-directly",
40982
41064
  version: "0.9.4",
@@ -40990,6 +41072,20 @@ var init_CHANGELOG = __esm({
40990
41072
  "rendering"
40991
41073
  ]
40992
41074
  },
41075
+ {
41076
+ id: "2026-07-26-photoreal-path-tracer",
41077
+ version: "0.9.4",
41078
+ date: "2026-07-26",
41079
+ category: "feat",
41080
+ title: "Photorealistic rendering with --photoreal",
41081
+ summary: "vcad-render gains a physically-based path tracer: GGX materials with clearcoat, studio softbox lighting, global illumination, and a real camera lens with depth of field.",
41082
+ features: [
41083
+ "rendering",
41084
+ "raytracing",
41085
+ "cli",
41086
+ "materials"
41087
+ ]
41088
+ },
40993
41089
  {
40994
41090
  id: "2026-07-26-parametric-loon-datums",
40995
41091
  version: "0.9.4",
@@ -41048,6 +41144,24 @@ var init_CHANGELOG = __esm({
41048
41144
  "export_cad"
41049
41145
  ]
41050
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
+ },
41051
41165
  {
41052
41166
  id: "2026-07-26-fastener-forms",
41053
41167
  version: "0.9.4",
@@ -47874,13 +47988,13 @@ function computeIntegrity(doc, engine) {
47874
47988
  cx += v * (p1[0] + p2[0] + p3[0]) / 4;
47875
47989
  cy += v * (p1[1] + p2[1] + p3[1]) / 4;
47876
47990
  cz += v * (p1[2] + p2[2] + p3[2]) / 4;
47877
- for (const q of [p1, p2, p3]) {
47878
- bbox.min.x = Math.min(bbox.min.x, q[0]);
47879
- bbox.min.y = Math.min(bbox.min.y, q[1]);
47880
- bbox.min.z = Math.min(bbox.min.z, q[2]);
47881
- bbox.max.x = Math.max(bbox.max.x, q[0]);
47882
- bbox.max.y = Math.max(bbox.max.y, q[1]);
47883
- 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]);
47884
47998
  }
47885
47999
  }
47886
48000
  const label = labels[partIndex] ?? `part ${partIndex + 1}`;
@@ -50295,8 +50409,8 @@ function buildPartLabels(doc) {
50295
50409
  });
50296
50410
  }
50297
50411
  function eulerXyzDegToQuat2(rotation) {
50298
- const q = exportKernel().eulerXyzDegToQuat(rotation.x, rotation.y, rotation.z);
50299
- 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]];
50300
50414
  }
50301
50415
  function buildGlb(inputMeshes, name, animation) {
50302
50416
  let f32Len = 0;
@@ -51125,9 +51239,9 @@ var init_store = __esm({
51125
51239
  async listOrders(userId, filter) {
51126
51240
  try {
51127
51241
  const limit = typeof filter.limit === "number" ? filter.limit : 50;
51128
- let q = `?user_id=eq.${encodeURIComponent(userId)}&order=created_at.desc&limit=${limit}`;
51129
- if (filter.status) q += `&state=eq.${encodeURIComponent(filter.status)}`;
51130
- 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), {
51131
51245
  method: "GET",
51132
51246
  headers: this.headers()
51133
51247
  });
@@ -51331,8 +51445,8 @@ var init_store = __esm({
51331
51445
  * can retry before deciding what to say. Never throws. */
51332
51446
  async postRow(table, row, onConflict) {
51333
51447
  try {
51334
- const q = onConflict ? `?on_conflict=${onConflict}` : "";
51335
- const res = await fabricateFetch(this.url(table, q), {
51448
+ const q2 = onConflict ? `?on_conflict=${onConflict}` : "";
51449
+ const res = await fabricateFetch(this.url(table, q2), {
51336
51450
  method: "POST",
51337
51451
  headers: this.headers({
51338
51452
  Prefer: onConflict ? "resolution=merge-duplicates,return=minimal" : "return=minimal"
@@ -53378,6 +53492,7 @@ var init_tool_metadata = __esm({
53378
53492
  // ── Sheet metal ────────────────────────────────────────────────────────
53379
53493
  sheet_metal_create: { title: "Create Sheet Metal", annotations: RW },
53380
53494
  sheet_metal_unfold: { title: "Unfold Sheet Metal", annotations: RO },
53495
+ flat_pattern_from_solid: { title: "Flat Pattern From Solid", annotations: RO },
53381
53496
  sheet_metal_check: { title: "Check Sheet Metal", annotations: RO },
53382
53497
  sheet_metal_materials: { title: "Sheet Metal Materials", annotations: RO },
53383
53498
  sheet_metal_bend_table: { title: "Sheet Metal Bend Table", annotations: RO },
@@ -54693,34 +54808,34 @@ var init_broker = __esm({
54693
54808
  );
54694
54809
  const built = [];
54695
54810
  for (const adapter of eligible) {
54696
- let q;
54811
+ let q2;
54697
54812
  try {
54698
- q = await adapter.quote(req);
54813
+ q2 = await adapter.quote(req);
54699
54814
  } catch {
54700
- q = null;
54815
+ q2 = null;
54701
54816
  }
54702
- if (!q) continue;
54817
+ if (!q2) continue;
54703
54818
  const landed2 = estimateLandedCost({
54704
54819
  region: adapter.region,
54705
54820
  supportsDdp: adapter.supportsDdp
54706
54821
  });
54707
- const markedUp = applyMargin(q.fab_cost_minor);
54822
+ const markedUp = applyMargin(q2.fab_cost_minor);
54708
54823
  const totalMinor = markedUp + landed2.shipping_minor + landed2.duty_minor;
54709
- const orderable = CONTRACTED_FABS.has(adapter.key) && q.pricing_basis === "binding";
54824
+ const orderable = CONTRACTED_FABS.has(adapter.key) && q2.pricing_basis === "binding";
54710
54825
  built.push({
54711
- fabCost: q.fab_cost_minor,
54826
+ fabCost: q2.fab_cost_minor,
54712
54827
  option: {
54713
54828
  fab: adapter.key,
54714
54829
  fab_label: adapter.label,
54715
54830
  region: adapter.region,
54716
54831
  unit_price_minor: Math.round(totalMinor / req.quantity),
54717
54832
  total_minor: totalMinor,
54718
- lead_time_days: q.lead_time_days,
54719
- in_spec: q.in_spec,
54720
- 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,
54721
54836
  supports_ddp: adapter.supportsDdp,
54722
54837
  orderable,
54723
- notes: q.notes
54838
+ notes: q2.notes
54724
54839
  }
54725
54840
  });
54726
54841
  }
@@ -55910,7 +56025,7 @@ function measureGroups(engine, candidates, groupA, groupB) {
55910
56025
  error: `No part with id or name ${missing.map((m) => `"${m}"`).join(", ")}. Available: ${available || "none"}`
55911
56026
  };
55912
56027
  }
55913
- 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));
55914
56029
  if (overlap.length > 0) {
55915
56030
  return {
55916
56031
  error: `Parts cannot appear in both groups: ${overlap.map((p) => p.id).join(", ")}`
@@ -57979,17 +58094,19 @@ function exportCad(input, engine) {
57979
58094
  const filename = String(args.filename ?? "");
57980
58095
  const stepExt = filename.toLowerCase().split(".").pop();
57981
58096
  if (stepExt === "step" || stepExt === "stp") {
57982
- const step = engine.foldedSheetMetalStep(ir);
57983
- if (step === null) {
57984
- throw new Error(
57985
- "STEP export is only available for sheet-metal documents (the folded body needs B-rep bend geometry). Use .stl or .glb for mesh exports."
57986
- );
58097
+ const foldedStep = engine.foldedSheetMetalStep(ir);
58098
+ if (foldedStep !== null) {
58099
+ const bytes3 = new TextEncoder().encode(foldedStep);
58100
+ return deliver(filename, bytes3, {
58101
+ format: stepExt,
58102
+ parts: 1,
58103
+ note: "Folded sheet-metal body (AP214) with cylindrical bend faces \u2014 bends/angles/directions auto-detect in 3D fab pipelines."
58104
+ });
57987
58105
  }
57988
- const bytes2 = new TextEncoder().encode(step);
58106
+ const bytes2 = engine.documentStep(ir);
57989
58107
  return deliver(filename, bytes2, {
57990
58108
  format: stepExt,
57991
- parts: 1,
57992
- note: "Folded sheet-metal body (AP214) with cylindrical bend faces \u2014 bends/angles/directions auto-detect in 3D fab pipelines."
58109
+ note: "BRep AP214 export \u2014 analytic faces (planes/cylinders/spheres/cones/tori/NURBS) preserved through booleans; ready for CNC quoting."
57993
58110
  });
57994
58111
  }
57995
58112
  const scene = engine.evaluate(ir);
@@ -58021,7 +58138,7 @@ function exportCad(input, engine) {
58021
58138
  bytes = toGlbBytes(scene, filename);
58022
58139
  break;
58023
58140
  default:
58024
- throw new Error(`Unsupported format: .${ext}. Use .stl, .glb, or .step (sheet-metal only)`);
58141
+ throw new Error(`Unsupported format: .${ext}. Use .stl, .glb, or .step`);
58025
58142
  }
58026
58143
  return deliver(filename, bytes, {
58027
58144
  format: ext,
@@ -58060,7 +58177,7 @@ var init_export = __esm({
58060
58177
  },
58061
58178
  filename: {
58062
58179
  type: "string",
58063
- description: "Output filename with extension (.stl, .glb, or \u2014 for sheet-metal documents \u2014 .step/.stp), relative to the server working directory (or VCAD_MCP_EXPORT_DIR if set). STEP exports the FOLDED sheet-metal body (AP214) with true cylindrical bend faces sized by the document's shop profile, so fab services with a 3D pipeline (e.g. SendCutSend) auto-detect bends, angles, and directions with zero data entry."
58180
+ description: "Output filename with extension (.stl, .glb, or .step/.stp), relative to the server working directory (or VCAD_MCP_EXPORT_DIR if set). STEP is a BRep AP214 export: booleans, transforms, fillets, and sweeps keep true analytic faces (the format CNC vendors quote from). Sheet-metal documents export the FOLDED body with cylindrical bend faces auto-detected by 3D fab pipelines (e.g. SendCutSend). Parts built from imported meshes have no BRep and are refused by name \u2014 export those as STL."
58064
58181
  },
58065
58182
  joint_state: jointStateSchemaProp
58066
58183
  },
@@ -58070,7 +58187,7 @@ var init_export = __esm({
58070
58187
  {
58071
58188
  name: "export_cad",
58072
58189
  pack: null,
58073
- description: "Export a CAD document to a file. Supports STL (3D printing), GLB (visualization), and \u2014 for sheet-metal documents \u2014 STEP AP214 of the FOLDED body with true cylindrical bend faces (fab 3D pipelines like SendCutSend auto-detect bends/angles/directions; zero data entry). Format is determined by file extension. Pass `joint_state` to export a jointed assembly at a real pose (joint id or name \u2192 degrees, or mm for sliders) instead of its zero pose.",
58190
+ description: "Export a CAD document to a file. Supports STL (3D printing), GLB (visualization), and STEP AP214 (CNC quoting: BRep with true analytic faces, preserved through booleans/transforms/fillets/sweeps; sheet-metal documents export the FOLDED body with cylindrical bend faces that 3D fab pipelines like SendCutSend auto-detect). Mesh-only parts (imported meshes) can't go to STEP and are refused by name \u2014 use STL for those. Format is determined by file extension. Pass `joint_state` to export a jointed assembly at a real pose (joint id or name \u2192 degrees, or mm for sliders) instead of its zero pose.",
58074
58191
  inputSchema: exportCadSchema,
58075
58192
  handler: (a, c) => exportCad(a, c.engine),
58076
58193
  behavior: behavior({})
@@ -60068,10 +60185,10 @@ function detectStaleNets(pcb) {
60068
60185
  const onSegBody = (p, t2) => segDist(p, t2) <= t2.width / 2 + 0.02;
60069
60186
  const anchored = (p, selfIdx, halfW) => {
60070
60187
  if (pads.some(
60071
- (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
60072
60189
  ))
60073
60190
  return true;
60074
- 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;
60075
60192
  for (let j = 0; j < traces.length; j++) {
60076
60193
  if (j === selfIdx) continue;
60077
60194
  if (segDist(p, traces[j]) <= traces[j].width / 2 + halfW + 0.02) return true;
@@ -61871,7 +61988,7 @@ function seedScan(residual, lo, hi, perDim = 12) {
61871
61988
  const idx = new Array(n).fill(0);
61872
61989
  const total = Math.pow(perDim, n);
61873
61990
  for (let c = 0; c < total; c++) {
61874
- 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));
61875
61992
  const cost = sumSq(residual(x));
61876
61993
  if (cost < best.cost) {
61877
61994
  best.cost = cost;
@@ -62412,7 +62529,7 @@ function calcRf(args) {
62412
62529
  return fail2("topology must be 'series_rlc' or 'parallel_rlc'");
62413
62530
  }
62414
62531
  const f0 = 1 / (2 * Math.PI * Math.sqrt(l * c));
62415
- 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);
62416
62533
  const fStart = args.f_start_hz ?? f0 * 0.1;
62417
62534
  const fStop = args.f_stop_hz ?? f0 * 10;
62418
62535
  const points = Math.min(256, Math.max(3, Math.round(args.points ?? 21)));
@@ -62458,7 +62575,7 @@ function calcRf(args) {
62458
62575
  success: true,
62459
62576
  topology,
62460
62577
  resonance_hz: Math.round(f0),
62461
- q_factor: Number.isFinite(q) ? r3(q) : 999999,
62578
+ q_factor: Number.isFinite(q2) ? r3(q2) : 999999,
62462
62579
  z_at_resonance_ohm: r3(mag(z0re, z0im)),
62463
62580
  best_match: {
62464
62581
  f_hz: best.f_hz,
@@ -62473,8 +62590,8 @@ function calcRf(args) {
62473
62590
  l_henry: l,
62474
62591
  c_farad: c
62475
62592
  }),
62476
- ...Number.isFinite(q) ? [
62477
- emClaim("q_factor", r3(q), "dimensionless", "rlc-analytic", {
62593
+ ...Number.isFinite(q2) ? [
62594
+ emClaim("q_factor", r3(q2), "dimensionless", "rlc-analytic", {
62478
62595
  topology,
62479
62596
  r_ohm: r,
62480
62597
  l_henry: l,
@@ -63053,17 +63170,17 @@ async function setBoardOutline(args) {
63053
63170
  };
63054
63171
  }
63055
63172
  function scoreFootprintFamily(query, fam) {
63056
- const q = query.toLowerCase().trim();
63057
- if (!q) return 0;
63173
+ const q2 = query.toLowerCase().trim();
63174
+ if (!q2) return 0;
63058
63175
  const cands = [fam.family, fam.label, ...fam.aliases].map((s) => s.toLowerCase());
63059
63176
  let best = 0;
63060
63177
  for (const c of cands) {
63061
- if (c === q) {
63178
+ if (c === q2) {
63062
63179
  best = Math.max(best, 1);
63063
- } else if (c.includes(q) || q.includes(c)) {
63180
+ } else if (c.includes(q2) || q2.includes(c)) {
63064
63181
  best = Math.max(best, 0.75);
63065
63182
  } else {
63066
- 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);
63067
63184
  best = Math.max(best, sim * 0.6);
63068
63185
  }
63069
63186
  }
@@ -64696,16 +64813,16 @@ function windSegClosestPt(a, b, c, d) {
64696
64813
  };
64697
64814
  let best = [a, clamp(a, c, d)];
64698
64815
  let bestD = Infinity;
64699
- for (const [p, q] of [
64816
+ for (const [p, q2] of [
64700
64817
  [a, clamp(a, c, d)],
64701
64818
  [b, clamp(b, c, d)],
64702
64819
  [c, clamp(c, a, b)],
64703
64820
  [d, clamp(d, a, b)]
64704
64821
  ]) {
64705
- 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);
64706
64823
  if (dd < bestD) {
64707
64824
  bestD = dd;
64708
- best = [p, q];
64825
+ best = [p, q2];
64709
64826
  }
64710
64827
  }
64711
64828
  return { x: (best[0].x + best[1].x) / 2, y: (best[0].y + best[1].y) / 2 };
@@ -65323,7 +65440,7 @@ function calcMotorInduction(args, polePairs, turnsPerPhase, windingFactor, inner
65323
65440
  outer_radius_mm: outerR,
65324
65441
  end_effect_factor: endEffect
65325
65442
  };
65326
- 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);
65327
65444
  return {
65328
65445
  content: [
65329
65446
  {
@@ -65751,11 +65868,11 @@ function polygonCentroid(poly) {
65751
65868
  let cy = 0;
65752
65869
  for (let i = 0; i < poly.length; i++) {
65753
65870
  const p = poly[i];
65754
- const q = poly[(i + 1) % poly.length];
65755
- 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;
65756
65873
  a += cross;
65757
- cx += (p.x + q.x) * cross;
65758
- cy += (p.y + q.y) * cross;
65874
+ cx += (p.x + q2.x) * cross;
65875
+ cy += (p.y + q2.y) * cross;
65759
65876
  }
65760
65877
  if (Math.abs(a) < 1e-12) {
65761
65878
  const n = poly.length || 1;
@@ -74151,8 +74268,8 @@ function normaliseEngravings(raw) {
74151
74268
  if (Array.isArray(p) && p.length >= 2)
74152
74269
  return { x: Number(p[0]), y: Number(p[1]) };
74153
74270
  if (p && typeof p === "object" && "x" in p && "y" in p) {
74154
- const q = p;
74155
- return { x: Number(q.x), y: Number(q.y) };
74271
+ const q2 = p;
74272
+ return { x: Number(q2.x), y: Number(q2.y) };
74156
74273
  }
74157
74274
  return null;
74158
74275
  }).filter((p) => p !== null) : [];
@@ -74817,6 +74934,222 @@ var init_sheet_metal2 = __esm({
74817
74934
  }
74818
74935
  });
74819
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
+
74820
75153
  // src/tools/acoustics.ts
74821
75154
  import { writeFileSync as writeFileSync5 } from "node:fs";
74822
75155
  function lookupMaterial(engine, name) {
@@ -74985,7 +75318,7 @@ function simulateStrike(input, engine) {
74985
75318
  }
74986
75319
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
74987
75320
  }
74988
- var cents, MATERIAL_ALIASES, simulateStrikeSchema, round23, round4, toolDefs41;
75321
+ var cents, MATERIAL_ALIASES, simulateStrikeSchema, round23, round4, toolDefs42;
74989
75322
  var init_acoustics = __esm({
74990
75323
  "src/tools/acoustics.ts"() {
74991
75324
  "use strict";
@@ -75059,7 +75392,7 @@ var init_acoustics = __esm({
75059
75392
  };
75060
75393
  round23 = (x) => Math.round(x * 100) / 100;
75061
75394
  round4 = (x) => Math.round(x * 1e4) / 1e4;
75062
- toolDefs41 = [
75395
+ toolDefs42 = [
75063
75396
  {
75064
75397
  name: "simulate_strike",
75065
75398
  pack: "sheet_metal",
@@ -75210,7 +75543,7 @@ function importStep(input, engine) {
75210
75543
  ]
75211
75544
  };
75212
75545
  }
75213
- var MAX_STEP_BYTES, importStepSchema, toolDefs42;
75546
+ var MAX_STEP_BYTES, importStepSchema, toolDefs43;
75214
75547
  var init_import = __esm({
75215
75548
  "src/tools/import.ts"() {
75216
75549
  "use strict";
@@ -75242,7 +75575,7 @@ var init_import = __esm({
75242
75575
  }
75243
75576
  }
75244
75577
  };
75245
- toolDefs42 = [
75578
+ toolDefs43 = [
75246
75579
  {
75247
75580
  name: "import_step",
75248
75581
  pack: null,
@@ -75370,7 +75703,7 @@ function importEagle(_input) {
75370
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."
75371
75704
  );
75372
75705
  }
75373
- var MAX_PCB_BYTES, importKicadSchema, importEagleSchema, toolDefs43;
75706
+ var MAX_PCB_BYTES, importKicadSchema, importEagleSchema, toolDefs44;
75374
75707
  var init_import_pcb = __esm({
75375
75708
  "src/tools/import-pcb.ts"() {
75376
75709
  "use strict";
@@ -75411,7 +75744,7 @@ var init_import_pcb = __esm({
75411
75744
  }
75412
75745
  }
75413
75746
  };
75414
- toolDefs43 = [
75747
+ toolDefs44 = [
75415
75748
  {
75416
75749
  name: "import_kicad",
75417
75750
  pack: "ecad",
@@ -75507,7 +75840,7 @@ Compressed URL param: ${encoded.length} bytes`
75507
75840
  ]
75508
75841
  };
75509
75842
  }
75510
- var openInBrowserSchema, toolDefs44;
75843
+ var openInBrowserSchema, toolDefs45;
75511
75844
  var init_share = __esm({
75512
75845
  "src/tools/share.ts"() {
75513
75846
  "use strict";
@@ -75531,7 +75864,7 @@ var init_share = __esm({
75531
75864
  }
75532
75865
  }
75533
75866
  };
75534
- toolDefs44 = [
75867
+ toolDefs45 = [
75535
75868
  {
75536
75869
  name: "open_in_browser",
75537
75870
  pack: null,
@@ -75964,7 +76297,7 @@ function batchReset(input) {
75964
76297
  };
75965
76298
  }
75966
76299
  }
75967
- 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;
75968
76301
  var init_gym = __esm({
75969
76302
  "src/tools/gym.ts"() {
75970
76303
  "use strict";
@@ -76126,7 +76459,7 @@ var init_gym = __esm({
76126
76459
  },
76127
76460
  required: ["batch_id"]
76128
76461
  };
76129
- toolDefs45 = [
76462
+ toolDefs46 = [
76130
76463
  {
76131
76464
  name: "create_robot_env",
76132
76465
  pack: "physics",
@@ -76484,7 +76817,7 @@ async function renderMolecule(input) {
76484
76817
  return fail(err7);
76485
76818
  }
76486
76819
  }
76487
- 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;
76488
76821
  var init_atoms2 = __esm({
76489
76822
  "src/tools/atoms.ts"() {
76490
76823
  "use strict";
@@ -76619,7 +76952,7 @@ var init_atoms2 = __esm({
76619
76952
  S: 1.05,
76620
76953
  P: 1.07
76621
76954
  };
76622
- toolDefs46 = [
76955
+ toolDefs47 = [
76623
76956
  {
76624
76957
  name: "load_structure",
76625
76958
  pack: "atoms",
@@ -77827,7 +78160,7 @@ function errorResult(text) {
77827
78160
  isError: true
77828
78161
  };
77829
78162
  }
77830
- 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;
77831
78164
  var init_record2 = __esm({
77832
78165
  "src/tools/record.ts"() {
77833
78166
  "use strict";
@@ -77891,7 +78224,7 @@ var init_record2 = __esm({
77891
78224
  DEFAULT_FPS = 30;
77892
78225
  MIN_FPS = 1;
77893
78226
  MAX_FPS = 60;
77894
- toolDefs47 = [
78227
+ toolDefs48 = [
77895
78228
  {
77896
78229
  name: "record_simulation",
77897
78230
  pack: "physics",
@@ -77984,7 +78317,7 @@ ${text}`
77984
78317
  ]
77985
78318
  };
77986
78319
  }
77987
- var getChangelogSchema, toolDefs48;
78320
+ var getChangelogSchema, toolDefs49;
77988
78321
  var init_changelog2 = __esm({
77989
78322
  "src/tools/changelog.ts"() {
77990
78323
  "use strict";
@@ -78024,7 +78357,7 @@ var init_changelog2 = __esm({
78024
78357
  }
78025
78358
  }
78026
78359
  };
78027
- toolDefs48 = [
78360
+ toolDefs49 = [
78028
78361
  {
78029
78362
  name: "get_changelog",
78030
78363
  pack: null,
@@ -78173,7 +78506,7 @@ function getSimVersion(input) {
78173
78506
  ]
78174
78507
  };
78175
78508
  }
78176
- var getSimReplaySchema, getSimVersionSchema, toolDefs49;
78509
+ var getSimReplaySchema, getSimVersionSchema, toolDefs50;
78177
78510
  var init_sim_replay = __esm({
78178
78511
  "src/tools/sim-replay.ts"() {
78179
78512
  "use strict";
@@ -78201,7 +78534,7 @@ var init_sim_replay = __esm({
78201
78534
  },
78202
78535
  required: ["env_id"]
78203
78536
  };
78204
- toolDefs49 = [
78537
+ toolDefs50 = [
78205
78538
  {
78206
78539
  name: "get_sim_replay",
78207
78540
  pack: null,
@@ -78320,7 +78653,7 @@ async function getOrderFeed(input, store, user) {
78320
78653
  version: version2
78321
78654
  });
78322
78655
  }
78323
- var toUsd4, getOrderFeedSchema, toolDefs50;
78656
+ var toUsd4, getOrderFeedSchema, toolDefs51;
78324
78657
  var init_order_feed = __esm({
78325
78658
  "src/tools/order-feed.ts"() {
78326
78659
  "use strict";
@@ -78339,7 +78672,7 @@ var init_order_feed = __esm({
78339
78672
  },
78340
78673
  required: ["document_id"]
78341
78674
  };
78342
- toolDefs50 = [
78675
+ toolDefs51 = [
78343
78676
  {
78344
78677
  name: "get_order_feed",
78345
78678
  pack: "fabricate",
@@ -79241,7 +79574,7 @@ ${issues.map((i) => i.problem).join("\n")}`
79241
79574
  note: "Rollout compiled to the document timeline. Preview with render_sequence, ship with export_video."
79242
79575
  });
79243
79576
  }
79244
- 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;
79245
79578
  var init_animate = __esm({
79246
79579
  "src/tools/animate.ts"() {
79247
79580
  "use strict";
@@ -79393,7 +79726,7 @@ var init_animate = __esm({
79393
79726
  },
79394
79727
  required: ["env_id", "document_id"]
79395
79728
  };
79396
- toolDefs51 = [
79729
+ toolDefs52 = [
79397
79730
  {
79398
79731
  name: "animate",
79399
79732
  pack: null,
@@ -79455,6 +79788,7 @@ function buildInstructions(kernelPrompt) {
79455
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).",
79456
79789
  "- Ship with `export_cad` (STL/GLB/STEP) or `open_in_browser` (vcad.io deep link).",
79457
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.",
79458
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.",
79459
79793
  "",
79460
79794
  "",
@@ -80071,6 +80405,7 @@ async function createServer(existingEngine, context = { user: null }) {
80071
80405
  function slimPreviewForInlineUi(result, docId, toolName, clientHasInlineUi) {
80072
80406
  if (!clientHasInlineUi) return;
80073
80407
  if (toolName === "sheet_metal_unfold") return;
80408
+ if (toolName === "flat_pattern_from_solid") return;
80074
80409
  if (PURE_JSON_RESULT_TOOLS.has(toolName)) return;
80075
80410
  const alwaysSlim = toolName === "create_cad_loon";
80076
80411
  const totalChars = result.content.reduce(
@@ -80309,6 +80644,7 @@ var init_server3 = __esm({
80309
80644
  init_loon_macros();
80310
80645
  init_dfm2();
80311
80646
  init_sheet_metal2();
80647
+ init_flat_pattern();
80312
80648
  init_acoustics();
80313
80649
  init_import();
80314
80650
  init_import_pcb();
@@ -80323,8 +80659,8 @@ var init_server3 = __esm({
80323
80659
  init_order_feed();
80324
80660
  init_animate();
80325
80661
  PKG_VERSION = (() => {
80326
- if ("0.9.4-main.39") {
80327
- return "0.9.4-main.39";
80662
+ if ("0.9.4-main.41") {
80663
+ return "0.9.4-main.41";
80328
80664
  }
80329
80665
  try {
80330
80666
  const req = createRequire2(import.meta.url);
@@ -80333,8 +80669,8 @@ var init_server3 = __esm({
80333
80669
  return "0.0.0";
80334
80670
  }
80335
80671
  })();
80336
- BUILD_SHA = "08cd95d4487b522463d0a1a3b58b84935a1f7d08";
80337
- BUILD_TIME = "2026-07-26T13:56:33.334Z";
80672
+ BUILD_SHA = "b00f071e4b839c6448769107ed84182597d763ed";
80673
+ BUILD_TIME = "2026-07-26T14:41:39.682Z";
80338
80674
  SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
80339
80675
  VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
80340
80676
  INSTANCE_ID = randomUUID6().slice(0, 8);
@@ -80407,11 +80743,12 @@ var init_server3 = __esm({
80407
80743
  ...toolDefs46,
80408
80744
  ...toolDefs47,
80409
80745
  ...toolDefs48,
80746
+ ...toolDefs49,
80410
80747
  ...toolDefs20,
80411
80748
  ...toolDefs19,
80412
- ...toolDefs49,
80413
80749
  ...toolDefs50,
80414
- ...toolDefs51
80750
+ ...toolDefs51,
80751
+ ...toolDefs52
80415
80752
  ];
80416
80753
  LIST_TOOL_ORDER = [
80417
80754
  // ── Session lifecycle ──────────────────────────────────────
@@ -80518,6 +80855,7 @@ var init_server3 = __esm({
80518
80855
  "sheet_metal_suggest_fix",
80519
80856
  "sheet_metal_sequence",
80520
80857
  "sheet_metal_nest",
80858
+ "flat_pattern_from_solid",
80521
80859
  "simulate_strike",
80522
80860
  // ── Import + share ─────────────────────────────────────────
80523
80861
  "import_step",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vcad/mcp",
3
- "version": "0.9.4-main.39",
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": "08cd95d4487b522463d0a1a3b58b84935a1f7d08",
23
- "builtAt": "2026-07-26T13:56:33.334Z"
22
+ "sha": "b00f071e4b839c6448769107ed84182597d763ed",
23
+ "builtAt": "2026-07-26T14:41:39.682Z"
24
24
  }
25
25
  }
Binary file