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

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 +370 -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 5288a75b111969d99b834a3df1c88e017376b921 at 2026-07-26T14:46:06.764Z. 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,39 @@ var init_CHANGELOG = __esm({
41105
41144
  "export_cad"
41106
41145
  ]
41107
41146
  },
41147
+ {
41148
+ id: "2026-07-26-isometric-handedness",
41149
+ version: "0.9.4",
41150
+ date: "2026-07-26",
41151
+ category: "fix",
41152
+ title: "Isometric renders are no longer mirrored",
41153
+ summary: "The default isometric view used a left-handed screen basis, so every static render was a mirror image of the app viewport; it now matches.",
41154
+ features: [
41155
+ "rendering",
41156
+ "drafting"
41157
+ ],
41158
+ mcpTools: [
41159
+ "render_view"
41160
+ ]
41161
+ },
41162
+ {
41163
+ id: "2026-07-26-flat-pattern-from-solid",
41164
+ version: "0.9.4",
41165
+ date: "2026-07-26",
41166
+ category: "feat",
41167
+ title: "Flat patterns from parts you already modelled",
41168
+ 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.",
41169
+ features: [
41170
+ "sheet-metal",
41171
+ "manufacturing",
41172
+ "mcp"
41173
+ ],
41174
+ mcpTools: [
41175
+ "flat_pattern_from_solid",
41176
+ "sheet_metal_nest",
41177
+ "sheet_metal_cost"
41178
+ ]
41179
+ },
41108
41180
  {
41109
41181
  id: "2026-07-26-fastener-forms",
41110
41182
  version: "0.9.4",
@@ -47931,13 +48003,13 @@ function computeIntegrity(doc, engine) {
47931
48003
  cx += v * (p1[0] + p2[0] + p3[0]) / 4;
47932
48004
  cy += v * (p1[1] + p2[1] + p3[1]) / 4;
47933
48005
  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]);
48006
+ for (const q2 of [p1, p2, p3]) {
48007
+ bbox.min.x = Math.min(bbox.min.x, q2[0]);
48008
+ bbox.min.y = Math.min(bbox.min.y, q2[1]);
48009
+ bbox.min.z = Math.min(bbox.min.z, q2[2]);
48010
+ bbox.max.x = Math.max(bbox.max.x, q2[0]);
48011
+ bbox.max.y = Math.max(bbox.max.y, q2[1]);
48012
+ bbox.max.z = Math.max(bbox.max.z, q2[2]);
47941
48013
  }
47942
48014
  }
47943
48015
  const label = labels[partIndex] ?? `part ${partIndex + 1}`;
@@ -50352,8 +50424,8 @@ function buildPartLabels(doc) {
50352
50424
  });
50353
50425
  }
50354
50426
  function eulerXyzDegToQuat2(rotation) {
50355
- const q = exportKernel().eulerXyzDegToQuat(rotation.x, rotation.y, rotation.z);
50356
- return [q[0], q[1], q[2], q[3]];
50427
+ const q2 = exportKernel().eulerXyzDegToQuat(rotation.x, rotation.y, rotation.z);
50428
+ return [q2[0], q2[1], q2[2], q2[3]];
50357
50429
  }
50358
50430
  function buildGlb(inputMeshes, name, animation) {
50359
50431
  let f32Len = 0;
@@ -51182,9 +51254,9 @@ var init_store = __esm({
51182
51254
  async listOrders(userId, filter) {
51183
51255
  try {
51184
51256
  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), {
51257
+ let q2 = `?user_id=eq.${encodeURIComponent(userId)}&order=created_at.desc&limit=${limit}`;
51258
+ if (filter.status) q2 += `&state=eq.${encodeURIComponent(filter.status)}`;
51259
+ const res = await fabricateFetch(this.url("orders", q2), {
51188
51260
  method: "GET",
51189
51261
  headers: this.headers()
51190
51262
  });
@@ -51388,8 +51460,8 @@ var init_store = __esm({
51388
51460
  * can retry before deciding what to say. Never throws. */
51389
51461
  async postRow(table, row, onConflict) {
51390
51462
  try {
51391
- const q = onConflict ? `?on_conflict=${onConflict}` : "";
51392
- const res = await fabricateFetch(this.url(table, q), {
51463
+ const q2 = onConflict ? `?on_conflict=${onConflict}` : "";
51464
+ const res = await fabricateFetch(this.url(table, q2), {
51393
51465
  method: "POST",
51394
51466
  headers: this.headers({
51395
51467
  Prefer: onConflict ? "resolution=merge-duplicates,return=minimal" : "return=minimal"
@@ -53435,6 +53507,7 @@ var init_tool_metadata = __esm({
53435
53507
  // ── Sheet metal ────────────────────────────────────────────────────────
53436
53508
  sheet_metal_create: { title: "Create Sheet Metal", annotations: RW },
53437
53509
  sheet_metal_unfold: { title: "Unfold Sheet Metal", annotations: RO },
53510
+ flat_pattern_from_solid: { title: "Flat Pattern From Solid", annotations: RO },
53438
53511
  sheet_metal_check: { title: "Check Sheet Metal", annotations: RO },
53439
53512
  sheet_metal_materials: { title: "Sheet Metal Materials", annotations: RO },
53440
53513
  sheet_metal_bend_table: { title: "Sheet Metal Bend Table", annotations: RO },
@@ -54750,34 +54823,34 @@ var init_broker = __esm({
54750
54823
  );
54751
54824
  const built = [];
54752
54825
  for (const adapter of eligible) {
54753
- let q;
54826
+ let q2;
54754
54827
  try {
54755
- q = await adapter.quote(req);
54828
+ q2 = await adapter.quote(req);
54756
54829
  } catch {
54757
- q = null;
54830
+ q2 = null;
54758
54831
  }
54759
- if (!q) continue;
54832
+ if (!q2) continue;
54760
54833
  const landed2 = estimateLandedCost({
54761
54834
  region: adapter.region,
54762
54835
  supportsDdp: adapter.supportsDdp
54763
54836
  });
54764
- const markedUp = applyMargin(q.fab_cost_minor);
54837
+ const markedUp = applyMargin(q2.fab_cost_minor);
54765
54838
  const totalMinor = markedUp + landed2.shipping_minor + landed2.duty_minor;
54766
- const orderable = CONTRACTED_FABS.has(adapter.key) && q.pricing_basis === "binding";
54839
+ const orderable = CONTRACTED_FABS.has(adapter.key) && q2.pricing_basis === "binding";
54767
54840
  built.push({
54768
- fabCost: q.fab_cost_minor,
54841
+ fabCost: q2.fab_cost_minor,
54769
54842
  option: {
54770
54843
  fab: adapter.key,
54771
54844
  fab_label: adapter.label,
54772
54845
  region: adapter.region,
54773
54846
  unit_price_minor: Math.round(totalMinor / req.quantity),
54774
54847
  total_minor: totalMinor,
54775
- lead_time_days: q.lead_time_days,
54776
- in_spec: q.in_spec,
54777
- pricing_basis: q.pricing_basis,
54848
+ lead_time_days: q2.lead_time_days,
54849
+ in_spec: q2.in_spec,
54850
+ pricing_basis: q2.pricing_basis,
54778
54851
  supports_ddp: adapter.supportsDdp,
54779
54852
  orderable,
54780
- notes: q.notes
54853
+ notes: q2.notes
54781
54854
  }
54782
54855
  });
54783
54856
  }
@@ -55967,7 +56040,7 @@ function measureGroups(engine, candidates, groupA, groupB) {
55967
56040
  error: `No part with id or name ${missing.map((m) => `"${m}"`).join(", ")}. Available: ${available || "none"}`
55968
56041
  };
55969
56042
  }
55970
- const overlap = a.parts.filter((p) => b.parts.some((q) => q.id === p.id));
56043
+ const overlap = a.parts.filter((p) => b.parts.some((q2) => q2.id === p.id));
55971
56044
  if (overlap.length > 0) {
55972
56045
  return {
55973
56046
  error: `Parts cannot appear in both groups: ${overlap.map((p) => p.id).join(", ")}`
@@ -60127,10 +60200,10 @@ function detectStaleNets(pcb) {
60127
60200
  const onSegBody = (p, t2) => segDist(p, t2) <= t2.width / 2 + 0.02;
60128
60201
  const anchored = (p, selfIdx, halfW) => {
60129
60202
  if (pads.some(
60130
- (q) => Math.hypot(p.x - q.pos.x, p.y - q.pos.y) <= q.halfExtent + halfW + 0.02
60203
+ (q2) => Math.hypot(p.x - q2.pos.x, p.y - q2.pos.y) <= q2.halfExtent + halfW + 0.02
60131
60204
  ))
60132
60205
  return true;
60133
- if (vias.some((q) => Math.hypot(p.x - q.x, p.y - q.y) <= halfW + 0.42)) return true;
60206
+ if (vias.some((q2) => Math.hypot(p.x - q2.x, p.y - q2.y) <= halfW + 0.42)) return true;
60134
60207
  for (let j = 0; j < traces.length; j++) {
60135
60208
  if (j === selfIdx) continue;
60136
60209
  if (segDist(p, traces[j]) <= traces[j].width / 2 + halfW + 0.02) return true;
@@ -61930,7 +62003,7 @@ function seedScan(residual, lo, hi, perDim = 12) {
61930
62003
  const idx = new Array(n).fill(0);
61931
62004
  const total = Math.pow(perDim, n);
61932
62005
  for (let c = 0; c < total; c++) {
61933
- const x = idx.map((q, d) => lo[d] + (hi[d] - lo[d]) * q / (perDim - 1));
62006
+ const x = idx.map((q2, d) => lo[d] + (hi[d] - lo[d]) * q2 / (perDim - 1));
61934
62007
  const cost = sumSq(residual(x));
61935
62008
  if (cost < best.cost) {
61936
62009
  best.cost = cost;
@@ -62471,7 +62544,7 @@ function calcRf(args) {
62471
62544
  return fail2("topology must be 'series_rlc' or 'parallel_rlc'");
62472
62545
  }
62473
62546
  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);
62547
+ const q2 = topology === "series_rlc" ? r > 0 ? 1 / r * Math.sqrt(l / c) : Infinity : r * Math.sqrt(c / l);
62475
62548
  const fStart = args.f_start_hz ?? f0 * 0.1;
62476
62549
  const fStop = args.f_stop_hz ?? f0 * 10;
62477
62550
  const points = Math.min(256, Math.max(3, Math.round(args.points ?? 21)));
@@ -62517,7 +62590,7 @@ function calcRf(args) {
62517
62590
  success: true,
62518
62591
  topology,
62519
62592
  resonance_hz: Math.round(f0),
62520
- q_factor: Number.isFinite(q) ? r3(q) : 999999,
62593
+ q_factor: Number.isFinite(q2) ? r3(q2) : 999999,
62521
62594
  z_at_resonance_ohm: r3(mag(z0re, z0im)),
62522
62595
  best_match: {
62523
62596
  f_hz: best.f_hz,
@@ -62532,8 +62605,8 @@ function calcRf(args) {
62532
62605
  l_henry: l,
62533
62606
  c_farad: c
62534
62607
  }),
62535
- ...Number.isFinite(q) ? [
62536
- emClaim("q_factor", r3(q), "dimensionless", "rlc-analytic", {
62608
+ ...Number.isFinite(q2) ? [
62609
+ emClaim("q_factor", r3(q2), "dimensionless", "rlc-analytic", {
62537
62610
  topology,
62538
62611
  r_ohm: r,
62539
62612
  l_henry: l,
@@ -63112,17 +63185,17 @@ async function setBoardOutline(args) {
63112
63185
  };
63113
63186
  }
63114
63187
  function scoreFootprintFamily(query, fam) {
63115
- const q = query.toLowerCase().trim();
63116
- if (!q) return 0;
63188
+ const q2 = query.toLowerCase().trim();
63189
+ if (!q2) return 0;
63117
63190
  const cands = [fam.family, fam.label, ...fam.aliases].map((s) => s.toLowerCase());
63118
63191
  let best = 0;
63119
63192
  for (const c of cands) {
63120
- if (c === q) {
63193
+ if (c === q2) {
63121
63194
  best = Math.max(best, 1);
63122
- } else if (c.includes(q) || q.includes(c)) {
63195
+ } else if (c.includes(q2) || q2.includes(c)) {
63123
63196
  best = Math.max(best, 0.75);
63124
63197
  } else {
63125
- const sim = 1 - editDistance(q, c) / Math.max(q.length, c.length);
63198
+ const sim = 1 - editDistance(q2, c) / Math.max(q2.length, c.length);
63126
63199
  best = Math.max(best, sim * 0.6);
63127
63200
  }
63128
63201
  }
@@ -64755,16 +64828,16 @@ function windSegClosestPt(a, b, c, d) {
64755
64828
  };
64756
64829
  let best = [a, clamp(a, c, d)];
64757
64830
  let bestD = Infinity;
64758
- for (const [p, q] of [
64831
+ for (const [p, q2] of [
64759
64832
  [a, clamp(a, c, d)],
64760
64833
  [b, clamp(b, c, d)],
64761
64834
  [c, clamp(c, a, b)],
64762
64835
  [d, clamp(d, a, b)]
64763
64836
  ]) {
64764
- const dd = Math.hypot(p.x - q.x, p.y - q.y);
64837
+ const dd = Math.hypot(p.x - q2.x, p.y - q2.y);
64765
64838
  if (dd < bestD) {
64766
64839
  bestD = dd;
64767
- best = [p, q];
64840
+ best = [p, q2];
64768
64841
  }
64769
64842
  }
64770
64843
  return { x: (best[0].x + best[1].x) / 2, y: (best[0].y + best[1].y) / 2 };
@@ -65382,7 +65455,7 @@ function calcMotorInduction(args, polePairs, turnsPerPhase, windingFactor, inner
65382
65455
  outer_radius_mm: outerR,
65383
65456
  end_effect_factor: endEffect
65384
65457
  };
65385
- const claim2 = (q, v, unit) => emClaim(q, sig6(v), unit, "thin-sheet-induction", inductionInputs);
65458
+ const claim2 = (q2, v, unit) => emClaim(q2, sig6(v), unit, "thin-sheet-induction", inductionInputs);
65386
65459
  return {
65387
65460
  content: [
65388
65461
  {
@@ -65810,11 +65883,11 @@ function polygonCentroid(poly) {
65810
65883
  let cy = 0;
65811
65884
  for (let i = 0; i < poly.length; i++) {
65812
65885
  const p = poly[i];
65813
- const q = poly[(i + 1) % poly.length];
65814
- const cross = p.x * q.y - q.x * p.y;
65886
+ const q2 = poly[(i + 1) % poly.length];
65887
+ const cross = p.x * q2.y - q2.x * p.y;
65815
65888
  a += cross;
65816
- cx += (p.x + q.x) * cross;
65817
- cy += (p.y + q.y) * cross;
65889
+ cx += (p.x + q2.x) * cross;
65890
+ cy += (p.y + q2.y) * cross;
65818
65891
  }
65819
65892
  if (Math.abs(a) < 1e-12) {
65820
65893
  const n = poly.length || 1;
@@ -74210,8 +74283,8 @@ function normaliseEngravings(raw) {
74210
74283
  if (Array.isArray(p) && p.length >= 2)
74211
74284
  return { x: Number(p[0]), y: Number(p[1]) };
74212
74285
  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) };
74286
+ const q2 = p;
74287
+ return { x: Number(q2.x), y: Number(q2.y) };
74215
74288
  }
74216
74289
  return null;
74217
74290
  }).filter((p) => p !== null) : [];
@@ -74876,6 +74949,222 @@ var init_sheet_metal2 = __esm({
74876
74949
  }
74877
74950
  });
74878
74951
 
74952
+ // src/tools/flat-pattern.ts
74953
+ function q(v, dp = 3) {
74954
+ const f = 10 ** dp;
74955
+ const r = Math.round(v * f) / f;
74956
+ return r === 0 ? 0 : r;
74957
+ }
74958
+ function ringSignature(ring) {
74959
+ const n = ring.length;
74960
+ if (n < 3) return "";
74961
+ const terms = [];
74962
+ for (let i = 0; i < n; i++) {
74963
+ const a = ring[(i + n - 1) % n];
74964
+ const b = ring[i];
74965
+ const c = ring[(i + 1) % n];
74966
+ const e1 = [b[0] - a[0], b[1] - a[1]];
74967
+ const e2 = [c[0] - b[0], c[1] - b[1]];
74968
+ const len = Math.hypot(e2[0], e2[1]);
74969
+ const turn = Math.atan2(
74970
+ e1[0] * e2[1] - e1[1] * e2[0],
74971
+ e1[0] * e2[0] + e1[1] * e2[1]
74972
+ );
74973
+ terms.push(`${q(len, 2)}:${q(turn, 3)}`);
74974
+ }
74975
+ let best = null;
74976
+ for (let r = 0; r < n; r++) {
74977
+ const cand = terms.slice(r).concat(terms.slice(0, r)).join(",");
74978
+ if (best === null || cand < best) best = cand;
74979
+ }
74980
+ return best ?? "";
74981
+ }
74982
+ function patternSignature(r) {
74983
+ const sil = r.flatPattern.silhouette_2d ?? [];
74984
+ const outer = ringSignature(sil[0] ?? []);
74985
+ const holes = sil.slice(1).map((h) => ringSignature(h)).sort().join("|");
74986
+ const bends = r.bends.map((b) => `${q(b.angle_deg, 2)}/${q(b.radius, 3)}/${q(b.length, 2)}/${b.direction}`).sort().join("|");
74987
+ return `t${q(r.thickness)}|${outer}|H:${holes}|B:${bends}`;
74988
+ }
74989
+ function solidParts(doc, engine) {
74990
+ const scene = engine.evaluate(doc);
74991
+ const visibleRoots = doc.roots.filter((e) => e.visible !== false);
74992
+ const out = [];
74993
+ for (let i = 0; i < scene.parts.length && i < visibleRoots.length; i++) {
74994
+ const rootId = visibleRoots[i].root;
74995
+ const node = doc.nodes[String(rootId)];
74996
+ const mesh = scene.parts[i].mesh;
74997
+ if (!mesh || mesh.positions.length === 0) continue;
74998
+ out.push({ rootId, name: node?.name ?? void 0, mesh });
74999
+ }
75000
+ return out;
75001
+ }
75002
+ function flatPatternFromSolid(input, engine) {
75003
+ const a = input ?? {};
75004
+ const doc = getSession(String(a.document_id ?? ""));
75005
+ const includeDxf = a.include_dxf !== false;
75006
+ const options = {
75007
+ material: typeof a.material === "string" ? a.material : void 0,
75008
+ manualK: typeof a.manual_k === "number" ? a.manual_k : void 0,
75009
+ shopProfile: typeof a.shop_profile === "string" ? a.shop_profile : void 0,
75010
+ volumeTolFrac: typeof a.volume_tolerance === "number" ? a.volume_tolerance : void 0
75011
+ };
75012
+ const all = solidParts(doc, engine);
75013
+ if (all.length === 0) {
75014
+ throw new Error("Document has no solid parts to flatten");
75015
+ }
75016
+ const partId = a.part_id !== void 0 ? String(a.part_id) : void 0;
75017
+ const targets = partId ? all.filter((p) => String(p.rootId) === partId) : all;
75018
+ if (targets.length === 0) {
75019
+ throw new Error(
75020
+ `No part with id "${partId}". Available: ${all.map((c) => `${c.rootId}${c.name ? ` (${c.name})` : ""}`).join(", ")}`
75021
+ );
75022
+ }
75023
+ const groups = /* @__PURE__ */ new Map();
75024
+ const failed = [];
75025
+ for (const part of targets) {
75026
+ let r;
75027
+ try {
75028
+ r = engine.flattenSolidToSheetMetal(part.mesh, options);
75029
+ } catch (e) {
75030
+ failed.push({
75031
+ root_id: part.rootId,
75032
+ ...part.name ? { name: part.name } : {},
75033
+ reason: e instanceof Error ? e.message : String(e)
75034
+ });
75035
+ continue;
75036
+ }
75037
+ const sig = patternSignature(r);
75038
+ const existing = groups.get(sig);
75039
+ if (existing) {
75040
+ existing.quantity += 1;
75041
+ existing.parts.push({
75042
+ root_id: part.rootId,
75043
+ ...part.name ? { name: part.name } : {}
75044
+ });
75045
+ continue;
75046
+ }
75047
+ const [minX, minY, maxX, maxY] = r.flatPattern.bbox;
75048
+ const creases = r.flatPattern.creases ?? [];
75049
+ groups.set(sig, {
75050
+ pattern_id: `pattern-${groups.size + 1}`,
75051
+ quantity: 1,
75052
+ parts: [{ root_id: part.rootId, ...part.name ? { name: part.name } : {} }],
75053
+ thickness_mm: r.thickness,
75054
+ flat: {
75055
+ bbox: r.flatPattern.bbox,
75056
+ area_mm2: r.flatPattern.area_mm2,
75057
+ width_mm: Math.abs(maxX - minX),
75058
+ height_mm: Math.abs(maxY - minY)
75059
+ },
75060
+ panels: r.panels,
75061
+ bend_table: r.bends.map((b, i) => ({
75062
+ bend: b.bend,
75063
+ angle_deg: b.angle_deg,
75064
+ radius_mm: b.radius,
75065
+ length_mm: b.length,
75066
+ direction: b.direction,
75067
+ k_factor: b.k_factor,
75068
+ allowance_mm: b.angle_deg * Math.PI / 180 * (b.radius + b.k_factor * r.thickness),
75069
+ line: creases[i] ? creases[i].line : []
75070
+ })),
75071
+ verification: {
75072
+ solid_volume_mm3: r.solidVolumeMm3,
75073
+ recovered_volume_mm3: r.recoveredVolumeMm3,
75074
+ error_frac: r.volumeErrorFrac,
75075
+ status: "verified"
75076
+ },
75077
+ violations: r.violations,
75078
+ warnings: r.warnings,
75079
+ ...includeDxf ? { dxf: r.dxf } : {}
75080
+ });
75081
+ }
75082
+ const patterns = [...groups.values()];
75083
+ if (patterns.length === 0) {
75084
+ throw new Error(
75085
+ `No part in this document is constant-thickness sheet. ${failed.map((f) => `${f.name ?? f.root_id}: ${f.reason}`).join("; ")}`
75086
+ );
75087
+ }
75088
+ const totalParts = patterns.reduce((s, p) => s + p.quantity, 0);
75089
+ const bends = patterns.reduce((s, p) => s + p.bend_table.length * p.quantity, 0);
75090
+ return {
75091
+ content: [
75092
+ {
75093
+ type: "text",
75094
+ text: JSON.stringify(
75095
+ {
75096
+ success: true,
75097
+ patterns,
75098
+ ...failed.length > 0 ? { not_sheet_metal: failed } : {},
75099
+ nest_input: patterns.map((p) => ({
75100
+ name: p.pattern_id,
75101
+ width_mm: p.flat.width_mm,
75102
+ height_mm: p.flat.height_mm,
75103
+ quantity: p.quantity
75104
+ })),
75105
+ 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.`,
75106
+ 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."
75107
+ },
75108
+ null,
75109
+ 2
75110
+ )
75111
+ }
75112
+ ]
75113
+ };
75114
+ }
75115
+ var flatPatternFromSolidSchema, toolDefs41;
75116
+ var init_flat_pattern = __esm({
75117
+ "src/tools/flat-pattern.ts"() {
75118
+ "use strict";
75119
+ init_session2();
75120
+ init_tool_def();
75121
+ flatPatternFromSolidSchema = {
75122
+ type: "object",
75123
+ properties: {
75124
+ document_id: {
75125
+ type: "string",
75126
+ description: "CAD session id (open_document / create_cad_loon) holding the solid(s)."
75127
+ },
75128
+ part_id: {
75129
+ type: "string",
75130
+ 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."
75131
+ },
75132
+ material: {
75133
+ type: "string",
75134
+ description: 'Material key for K-factor lookup (e.g. "al-soft", "steel-mild"). Default "al-soft". See sheet_metal_materials.'
75135
+ },
75136
+ manual_k: {
75137
+ type: "number",
75138
+ description: "Override the K-factor for every recovered bend (skips the bend table)."
75139
+ },
75140
+ shop_profile: {
75141
+ type: "string",
75142
+ description: 'Fab-service catalog id (e.g. "sendcutsend") to run manufacturability against. Default: generic shop.'
75143
+ },
75144
+ volume_tolerance: {
75145
+ type: "number",
75146
+ 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."
75147
+ },
75148
+ include_dxf: {
75149
+ type: "boolean",
75150
+ description: "Include the DXF string per pattern. Default true."
75151
+ }
75152
+ },
75153
+ required: ["document_id"]
75154
+ };
75155
+ toolDefs41 = [
75156
+ {
75157
+ name: "flat_pattern_from_solid",
75158
+ pack: "sheet_metal",
75159
+ 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.",
75160
+ inputSchema: flatPatternFromSolidSchema,
75161
+ handler: (a, c) => flatPatternFromSolid(a, c.engine),
75162
+ behavior: behavior({ geometry: true })
75163
+ }
75164
+ ];
75165
+ }
75166
+ });
75167
+
74879
75168
  // src/tools/acoustics.ts
74880
75169
  import { writeFileSync as writeFileSync5 } from "node:fs";
74881
75170
  function lookupMaterial(engine, name) {
@@ -75044,7 +75333,7 @@ function simulateStrike(input, engine) {
75044
75333
  }
75045
75334
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
75046
75335
  }
75047
- var cents, MATERIAL_ALIASES, simulateStrikeSchema, round23, round4, toolDefs41;
75336
+ var cents, MATERIAL_ALIASES, simulateStrikeSchema, round23, round4, toolDefs42;
75048
75337
  var init_acoustics = __esm({
75049
75338
  "src/tools/acoustics.ts"() {
75050
75339
  "use strict";
@@ -75118,7 +75407,7 @@ var init_acoustics = __esm({
75118
75407
  };
75119
75408
  round23 = (x) => Math.round(x * 100) / 100;
75120
75409
  round4 = (x) => Math.round(x * 1e4) / 1e4;
75121
- toolDefs41 = [
75410
+ toolDefs42 = [
75122
75411
  {
75123
75412
  name: "simulate_strike",
75124
75413
  pack: "sheet_metal",
@@ -75269,7 +75558,7 @@ function importStep(input, engine) {
75269
75558
  ]
75270
75559
  };
75271
75560
  }
75272
- var MAX_STEP_BYTES, importStepSchema, toolDefs42;
75561
+ var MAX_STEP_BYTES, importStepSchema, toolDefs43;
75273
75562
  var init_import = __esm({
75274
75563
  "src/tools/import.ts"() {
75275
75564
  "use strict";
@@ -75301,7 +75590,7 @@ var init_import = __esm({
75301
75590
  }
75302
75591
  }
75303
75592
  };
75304
- toolDefs42 = [
75593
+ toolDefs43 = [
75305
75594
  {
75306
75595
  name: "import_step",
75307
75596
  pack: null,
@@ -75429,7 +75718,7 @@ function importEagle(_input) {
75429
75718
  "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
75719
  );
75431
75720
  }
75432
- var MAX_PCB_BYTES, importKicadSchema, importEagleSchema, toolDefs43;
75721
+ var MAX_PCB_BYTES, importKicadSchema, importEagleSchema, toolDefs44;
75433
75722
  var init_import_pcb = __esm({
75434
75723
  "src/tools/import-pcb.ts"() {
75435
75724
  "use strict";
@@ -75470,7 +75759,7 @@ var init_import_pcb = __esm({
75470
75759
  }
75471
75760
  }
75472
75761
  };
75473
- toolDefs43 = [
75762
+ toolDefs44 = [
75474
75763
  {
75475
75764
  name: "import_kicad",
75476
75765
  pack: "ecad",
@@ -75566,7 +75855,7 @@ Compressed URL param: ${encoded.length} bytes`
75566
75855
  ]
75567
75856
  };
75568
75857
  }
75569
- var openInBrowserSchema, toolDefs44;
75858
+ var openInBrowserSchema, toolDefs45;
75570
75859
  var init_share = __esm({
75571
75860
  "src/tools/share.ts"() {
75572
75861
  "use strict";
@@ -75590,7 +75879,7 @@ var init_share = __esm({
75590
75879
  }
75591
75880
  }
75592
75881
  };
75593
- toolDefs44 = [
75882
+ toolDefs45 = [
75594
75883
  {
75595
75884
  name: "open_in_browser",
75596
75885
  pack: null,
@@ -76023,7 +76312,7 @@ function batchReset(input) {
76023
76312
  };
76024
76313
  }
76025
76314
  }
76026
- var simulations, MAX_TRAJECTORY, envRecords, batchGroups, INLINE_DOC_DESC, SESSION_DOC_DESC, createRobotEnvSchema, gymStepSchema, gymResetSchema, gymObserveSchema, gymCloseSchema, batchCreateEnvsSchema, batchStepSchema, batchResetSchema, toolDefs45;
76315
+ var simulations, MAX_TRAJECTORY, envRecords, batchGroups, INLINE_DOC_DESC, SESSION_DOC_DESC, createRobotEnvSchema, gymStepSchema, gymResetSchema, gymObserveSchema, gymCloseSchema, batchCreateEnvsSchema, batchStepSchema, batchResetSchema, toolDefs46;
76027
76316
  var init_gym = __esm({
76028
76317
  "src/tools/gym.ts"() {
76029
76318
  "use strict";
@@ -76185,7 +76474,7 @@ var init_gym = __esm({
76185
76474
  },
76186
76475
  required: ["batch_id"]
76187
76476
  };
76188
- toolDefs45 = [
76477
+ toolDefs46 = [
76189
76478
  {
76190
76479
  name: "create_robot_env",
76191
76480
  pack: "physics",
@@ -76543,7 +76832,7 @@ async function renderMolecule(input) {
76543
76832
  return fail(err7);
76544
76833
  }
76545
76834
  }
76546
- var loadStructureSchema, inspectMoleculeSchema, configSchema, minimizeEnergySchema, mdRunSchema, designMaterialSchema, homogenizeMaterialSchema, renderMoleculeSchema, CPK, DEFAULT_RADIUS, toolDefs46;
76835
+ var loadStructureSchema, inspectMoleculeSchema, configSchema, minimizeEnergySchema, mdRunSchema, designMaterialSchema, homogenizeMaterialSchema, renderMoleculeSchema, CPK, DEFAULT_RADIUS, toolDefs47;
76547
76836
  var init_atoms2 = __esm({
76548
76837
  "src/tools/atoms.ts"() {
76549
76838
  "use strict";
@@ -76678,7 +76967,7 @@ var init_atoms2 = __esm({
76678
76967
  S: 1.05,
76679
76968
  P: 1.07
76680
76969
  };
76681
- toolDefs46 = [
76970
+ toolDefs47 = [
76682
76971
  {
76683
76972
  name: "load_structure",
76684
76973
  pack: "atoms",
@@ -77886,7 +78175,7 @@ function errorResult(text) {
77886
78175
  isError: true
77887
78176
  };
77888
78177
  }
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;
78178
+ 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
78179
  var init_record2 = __esm({
77891
78180
  "src/tools/record.ts"() {
77892
78181
  "use strict";
@@ -77950,7 +78239,7 @@ var init_record2 = __esm({
77950
78239
  DEFAULT_FPS = 30;
77951
78240
  MIN_FPS = 1;
77952
78241
  MAX_FPS = 60;
77953
- toolDefs47 = [
78242
+ toolDefs48 = [
77954
78243
  {
77955
78244
  name: "record_simulation",
77956
78245
  pack: "physics",
@@ -78043,7 +78332,7 @@ ${text}`
78043
78332
  ]
78044
78333
  };
78045
78334
  }
78046
- var getChangelogSchema, toolDefs48;
78335
+ var getChangelogSchema, toolDefs49;
78047
78336
  var init_changelog2 = __esm({
78048
78337
  "src/tools/changelog.ts"() {
78049
78338
  "use strict";
@@ -78083,7 +78372,7 @@ var init_changelog2 = __esm({
78083
78372
  }
78084
78373
  }
78085
78374
  };
78086
- toolDefs48 = [
78375
+ toolDefs49 = [
78087
78376
  {
78088
78377
  name: "get_changelog",
78089
78378
  pack: null,
@@ -78232,7 +78521,7 @@ function getSimVersion(input) {
78232
78521
  ]
78233
78522
  };
78234
78523
  }
78235
- var getSimReplaySchema, getSimVersionSchema, toolDefs49;
78524
+ var getSimReplaySchema, getSimVersionSchema, toolDefs50;
78236
78525
  var init_sim_replay = __esm({
78237
78526
  "src/tools/sim-replay.ts"() {
78238
78527
  "use strict";
@@ -78260,7 +78549,7 @@ var init_sim_replay = __esm({
78260
78549
  },
78261
78550
  required: ["env_id"]
78262
78551
  };
78263
- toolDefs49 = [
78552
+ toolDefs50 = [
78264
78553
  {
78265
78554
  name: "get_sim_replay",
78266
78555
  pack: null,
@@ -78379,7 +78668,7 @@ async function getOrderFeed(input, store, user) {
78379
78668
  version: version2
78380
78669
  });
78381
78670
  }
78382
- var toUsd4, getOrderFeedSchema, toolDefs50;
78671
+ var toUsd4, getOrderFeedSchema, toolDefs51;
78383
78672
  var init_order_feed = __esm({
78384
78673
  "src/tools/order-feed.ts"() {
78385
78674
  "use strict";
@@ -78398,7 +78687,7 @@ var init_order_feed = __esm({
78398
78687
  },
78399
78688
  required: ["document_id"]
78400
78689
  };
78401
- toolDefs50 = [
78690
+ toolDefs51 = [
78402
78691
  {
78403
78692
  name: "get_order_feed",
78404
78693
  pack: "fabricate",
@@ -79300,7 +79589,7 @@ ${issues.map((i) => i.problem).join("\n")}`
79300
79589
  note: "Rollout compiled to the document timeline. Preview with render_sequence, ship with export_video."
79301
79590
  });
79302
79591
  }
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;
79592
+ 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
79593
  var init_animate = __esm({
79305
79594
  "src/tools/animate.ts"() {
79306
79595
  "use strict";
@@ -79452,7 +79741,7 @@ var init_animate = __esm({
79452
79741
  },
79453
79742
  required: ["env_id", "document_id"]
79454
79743
  };
79455
- toolDefs51 = [
79744
+ toolDefs52 = [
79456
79745
  {
79457
79746
  name: "animate",
79458
79747
  pack: null,
@@ -79514,6 +79803,7 @@ function buildInstructions(kernelPrompt) {
79514
79803
  "- 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
79804
  "- Ship with `export_cad` (STL/GLB/STEP) or `open_in_browser` (vcad.io deep link).",
79516
79805
  "- Fix in place: when geometry is wrong, prefer `update` on the offending node over deleting parts and starting over.",
79806
+ "- 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
79807
  "- 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
79808
  "",
79519
79809
  "",
@@ -80130,6 +80420,7 @@ async function createServer(existingEngine, context = { user: null }) {
80130
80420
  function slimPreviewForInlineUi(result, docId, toolName, clientHasInlineUi) {
80131
80421
  if (!clientHasInlineUi) return;
80132
80422
  if (toolName === "sheet_metal_unfold") return;
80423
+ if (toolName === "flat_pattern_from_solid") return;
80133
80424
  if (PURE_JSON_RESULT_TOOLS.has(toolName)) return;
80134
80425
  const alwaysSlim = toolName === "create_cad_loon";
80135
80426
  const totalChars = result.content.reduce(
@@ -80368,6 +80659,7 @@ var init_server3 = __esm({
80368
80659
  init_loon_macros();
80369
80660
  init_dfm2();
80370
80661
  init_sheet_metal2();
80662
+ init_flat_pattern();
80371
80663
  init_acoustics();
80372
80664
  init_import();
80373
80665
  init_import_pcb();
@@ -80382,8 +80674,8 @@ var init_server3 = __esm({
80382
80674
  init_order_feed();
80383
80675
  init_animate();
80384
80676
  PKG_VERSION = (() => {
80385
- if ("0.9.4-main.40") {
80386
- return "0.9.4-main.40";
80677
+ if ("0.9.4-main.42") {
80678
+ return "0.9.4-main.42";
80387
80679
  }
80388
80680
  try {
80389
80681
  const req = createRequire2(import.meta.url);
@@ -80392,8 +80684,8 @@ var init_server3 = __esm({
80392
80684
  return "0.0.0";
80393
80685
  }
80394
80686
  })();
80395
- BUILD_SHA = "6cac4f4f2c7c80a9122ee16857ec2b49f2dcd1f4";
80396
- BUILD_TIME = "2026-07-26T14:38:17.346Z";
80687
+ BUILD_SHA = "5288a75b111969d99b834a3df1c88e017376b921";
80688
+ BUILD_TIME = "2026-07-26T14:46:06.764Z";
80397
80689
  SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
80398
80690
  VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
80399
80691
  INSTANCE_ID = randomUUID6().slice(0, 8);
@@ -80466,11 +80758,12 @@ var init_server3 = __esm({
80466
80758
  ...toolDefs46,
80467
80759
  ...toolDefs47,
80468
80760
  ...toolDefs48,
80761
+ ...toolDefs49,
80469
80762
  ...toolDefs20,
80470
80763
  ...toolDefs19,
80471
- ...toolDefs49,
80472
80764
  ...toolDefs50,
80473
- ...toolDefs51
80765
+ ...toolDefs51,
80766
+ ...toolDefs52
80474
80767
  ];
80475
80768
  LIST_TOOL_ORDER = [
80476
80769
  // ── Session lifecycle ──────────────────────────────────────
@@ -80577,6 +80870,7 @@ var init_server3 = __esm({
80577
80870
  "sheet_metal_suggest_fix",
80578
80871
  "sheet_metal_sequence",
80579
80872
  "sheet_metal_nest",
80873
+ "flat_pattern_from_solid",
80580
80874
  "simulate_strike",
80581
80875
  // ── Import + share ─────────────────────────────────────────
80582
80876
  "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.42",
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": "5288a75b111969d99b834a3df1c88e017376b921",
23
+ "builtAt": "2026-07-26T14:46:06.764Z"
24
24
  }
25
25
  }