dsh-eda-mcp 0.7.1 → 0.7.3
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/CHANGELOG.md +165 -0
- package/README.md +10 -1
- package/README.zh.md +18 -1
- package/docs/release.md +2 -0
- package/docs/schematic-workflow.md +169 -0
- package/docs/tools.md +13 -9
- package/dsh-eda-bridge-extension-0.4.2.zip +0 -0
- package/jlc-extension/api-bridge.js +19 -3
- package/jlc-extension/extension.json +1 -1
- package/lib/bridge.mjs +487 -132
- package/lib/client.js +17 -0
- package/lib/index.js +299 -6
- package/package.json +5 -4
- package/src/bridge/index.ts +503 -110
- package/src/bridge/workflow-guide.ts +45 -0
- package/src/client/EdaMcpSettingsCard.tsx +8 -0
- package/src/client/locales.ts +4 -0
- package/src/guidance.ts +5 -0
- package/src/index.ts +49 -2
- package/src/skills-content.ts +181 -0
- package/src/skills.ts +91 -0
- package/test/seams.test.ts +16 -0
- package/test/skills.test.ts +67 -0
- package/dsh-eda-bridge-extension-0.4.0.zip +0 -0
package/lib/bridge.mjs
CHANGED
|
@@ -293,11 +293,50 @@ var BOARD_OUTLINE_WIDTH_MIL = 10;
|
|
|
293
293
|
var SILKSCREEN_HEIGHT_MIN_MM = 1;
|
|
294
294
|
var SILKSCREEN_WIDTH_MIN_MM = 0.15;
|
|
295
295
|
|
|
296
|
+
// src/bridge/workflow-guide.ts
|
|
297
|
+
var WORKFLOW_SUMMARY = [
|
|
298
|
+
"\u539F\u7406\u56FE\u5DE5\u4F5C\u89C4\u8303\uFF08\u9ED8\u8BA4\u5168\u90E8\u624B\u5DE5\uFF0C\u7981\u6B62\u56FE\u7701\u4E8B\u7528\u81EA\u52A8\u5E03\u5C40/\u81EA\u52A8\u5E03\u7EBF\uFF09\uFF1A",
|
|
299
|
+
"1) \u5E03\u5C40\uFF1A\u6309 10 \u5355\u4F4D\u6805\u683C\u5BF9\u9F50\uFF1B\u4FE1\u53F7\u4ECE\u5DE6\u5230\u53F3\u3001\u7535\u6E90\u5728\u4E0A\u3001\u5730\u5728\u4E0B\uFF1B\u540C\u7EA7\u5668\u4EF6\u7B49\u95F4\u8DDD\uFF1B\u4E0D\u538B\u5B57\u3001\u4E0D\u538B\u6807\u9898\u680F\uFF1B\u5148\u770B eda_get_page_info \u7684\u5B89\u5168\u533A\u518D\u6446\u4F4D\u3002",
|
|
300
|
+
"2) \u8FDE\u7EBF\uFF1A\u53EA\u7528 eda_draw_wire \u8D70\u76F4\u7EBF/45 \u5EA6\uFF1B**\u8DE8\u533A\u57DF\u540C\u540D\u7F51\u7EDC\u5FC5\u987B\u7528\u7F51\u7EDC\u7AEF\u53E3\uFF08eda_place_net_port\uFF09\u6216\u7F51\u7EDC\u6807\u7B7E**\uFF0C\u4E0D\u80FD\u9760\u5728\u4E24\u5904\u753B\u4E00\u6761\u53EB\u540C\u540D\u7684\u5BFC\u7EBF\u2014\u2014\u5BFC\u7EBF\u4E00\u65E6\u63A5\u89E6\u5F15\u811A\uFF0Cnet \u4F1A\u88AB\u8FDE\u901A\u6027\u91CD\u7B97\uFF0C\u6307\u5B9A\u540D\u4F1A\u88AB\u8986\u76D6\u3002",
|
|
301
|
+
"3) \u7F51\u7EDC\u6807\u7B7E\uFF1A\u5148\u8BD5 eda_place_net_label\uFF1B\u82E5\u8BE5\u6784\u5EFA\u7684 createNetLabel \u4E0D\u54CD\u5E94\uFF0C\u4F1A\u81EA\u52A8\u964D\u7EA7\u4E3A\u7EAF\u6587\u672C\u6807\u7B7E\uFF08\u65E0\u7535\u6C14\u8BED\u4E49\uFF09\uFF0C\u6B64\u65F6\u9760 stub \u5BFC\u7EBF\u627F\u8F7D\u7F51\u7EDC\uFF1B\u6279\u91CF\u7528 eda_place_net_labels \u4E00\u6B21\u8C03\u7528\uFF0C\u4E0D\u8981\u4E32\u884C\u4E0A\u767E\u6B21\u3002",
|
|
302
|
+
"4) \u6821\u9A8C\uFF1Aeda_export_netlist \u770B\u6BCF\u4E2A\u7F51\u7EDC\u7684\u7AEF\u70B9\u662F\u5426 \u22652\uFF1Beda_run_drc \u770B\u9010\u6761 DRC \u660E\u7EC6\uFF1B\u89C6\u89C9\u68C0\u67E5\u4F18\u5148\u5BFC\u51FA PNG \u81EA\u884C\u8BC6\u522B\uFF0C\u800C\u4E0D\u662F\u8BA9 EDA \u91CD\u6392\u3002",
|
|
303
|
+
"5) \u53EA\u6709\u7528\u6237\u660E\u786E\u8981\u6C42\u65F6\u624D eda_auto_layout / eda_auto_route\uFF08\u9700 force:true \u4E14\u8BBE\u7F6E\u91CC\u653E\u884C\uFF09\u3002",
|
|
304
|
+
"6) \u5F15\u811A\u4FE1\u606F\u7528 eda_get_pins \u4E00\u6B21\u62FF\u5168\uFF08\u542B pinName/pinNumber\uFF09\uFF0C\u4E0D\u8981\u9010\u811A\u8C03 raw API\u3002"
|
|
305
|
+
].join("\n");
|
|
306
|
+
var WORKFLOW_GUIDE = WORKFLOW_SUMMARY + "\n\n" + [
|
|
307
|
+
"## \u5E03\u5C40\u7EC6\u5219\u662F\u600E\u4E48\u843D\u5730\u7684",
|
|
308
|
+
"",
|
|
309
|
+
"- \u5750\u6807\u5355\u4F4D\u662F 0.01 \u82F1\u5BF8\uFF08MCP \u5355\u4F4D\uFF09\uFF1BA4 \u56FE\u6846 0..1170 / 0..825\uFF0C\u6807\u9898\u680F\u5360 right 460..1160 / bottom 10..190\uFF0C\u5B89\u5168\u533A\u89C1 eda_get_page_info\u3002",
|
|
310
|
+
"- \u4E00\u9875\u53EA\u653E\u4E00\u4E2A\u529F\u80FD\u5757\uFF1B\u5668\u4EF6\u6309\u4FE1\u53F7\u6D41\u4ECE\u5DE6\u5230\u53F3\u6392\uFF0C\u53CD\u9988\u5143\u4EF6\u653E\u5728\u653E\u5927\u5668\u4E0B\u65B9\uFF0C\u53BB\u8026\u7535\u5BB9\u8D34\u8FD1\u7535\u6E90\u811A\u3002",
|
|
311
|
+
"- \u5668\u4EF6\u4E4B\u95F4\u7559\u51FA\u81F3\u5C11 20 \u5355\u4F4D\u7528\u4E8E\u8D70\u7EBF\u548C\u6807\u7B7E\uFF1B\u7F51\u7EDC\u7AEF\u53E3\u653E\u5728\u6A21\u5757\u8FB9\u7F18\uFF0C\u5DE6\u53F3\u5404\u4E00\u5217\u3002",
|
|
312
|
+
"- \u6446\u597D\u4E00\u4E2A\u6A21\u5757\u518D\u6446\u4E0B\u4E00\u4E2A\uFF1B\u6BCF\u6B21\u6446\u5B8C\u7528 eda_get_components \u68C0\u67E5\u6CA1\u6709\u91CD\u53E0\uFF08\u5305\u56F4\u76D2\u76F8\u4EA4\uFF09\u3002",
|
|
313
|
+
"",
|
|
314
|
+
"## \u7F51\u7EDC\u4E0E\u6807\u7B7E",
|
|
315
|
+
"",
|
|
316
|
+
"- eda_place_net_port \u4EA7\u751F\u771F\u6B63\u7684\u7F51\u7EDC\u7AEF\u53E3\u56FE\u5143\uFF0C\u662F\u8DE8\u533A\u57DF\u8FDE\u63A5\u7684\u9996\u9009\u3002",
|
|
317
|
+
"- eda_place_net_label \u4F18\u5148\u8C03 sch_PrimitiveAttribute.createNetLabel\uFF1B\u8BE5 API \u5728 3.2.186 \u4E0A\u4E0D\u54CD\u5E94\uFF0C\u63A2\u6D4B\u7ED3\u8BBA\u6309\u4F1A\u8BDD\u7F13\u5B58\uFF0C\u4E4B\u540E\u76F4\u63A5\u964D\u7EA7\u4E3A\u6587\u672C\u6807\u7B7E\uFF08\u4E0D\u518D\u6BCF\u6B21\u4ED8 4 \u79D2\u8D85\u65F6\uFF09\u3002",
|
|
318
|
+
"- \u964D\u7EA7\u540E\u7684\u6587\u672C\u6807\u7B7E\u6CA1\u6709\u7535\u6C14\u8BED\u4E49\uFF1A\u8FDE\u901A\u6027\u7531 eda_place_net_label_at_pin \u540C\u65F6\u753B\u7684 stub \u5BFC\u7EBF\u627F\u62C5\u3002",
|
|
319
|
+
"- eda_get_net_labels \u4F1A\u540C\u65F6\u5217\u51FA\u5C5E\u6027\u56FE\u5143\uFF08netlabel/netport/netflag\uFF09\u548C\u6587\u672C\u6807\u7B7E\uFF0C\u5220\u9664\u7528 eda_delete_primitive \u7684\u5BF9\u5E94 kind\u3002",
|
|
320
|
+
"- \u6279\u91CF\u653E\u7F6E\u7528 eda_place_net_labels([{pinPrimitiveId, net}, ...])\uFF0C\u4E00\u6B21\u8C03\u7528\u5B8C\u6210\u5E76\u9010\u6761\u56DE\u62A5\u6210\u8D25\u3002",
|
|
321
|
+
"",
|
|
322
|
+
"## \u5BFC\u7EBF net \u88AB\u8986\u76D6\uFF08\u52A1\u5FC5\u77E5\u9053\uFF09",
|
|
323
|
+
"",
|
|
324
|
+
"eda_draw_wire \u4F20\u7684 net \u53EA\u5BF9**\u60AC\u7A7A\u5BFC\u7EBF**\u6709\u6548\uFF1B\u5BFC\u7EBF\u7AEF\u70B9\u4E00\u65E6\u843D\u5728\u5F15\u811A/\u7F51\u7EDC\u4E0A\uFF0CEDA \u4F1A\u6309\u8FDE\u901A\u6027\u91CD\u7B97\u7F51\u7EDC\u540D\uFF08\u5B9E\u6D4B TESTNET \u5E76\u5165\u4E86 LED_A\uFF09\u3002\u6240\u4EE5\u201C\u540C\u540D\u5BFC\u7EBF = \u540C\u540D\u7F51\u7EDC\u201D\u4E0D\u6210\u7ACB\uFF0C\u8DE8\u533A\u57DF\u540C\u540D\u5FC5\u987B\u7528\u7F51\u7EDC\u6807\u7B7E\u6216\u7F51\u7EDC\u7AEF\u53E3\u3002",
|
|
325
|
+
"",
|
|
326
|
+
"## \u6821\u9A8C\u987A\u5E8F",
|
|
327
|
+
"",
|
|
328
|
+
"1. eda_get_active_document \u786E\u8BA4\u5728\u539F\u7406\u56FE\u9875\uFF08\u4E0D\u662F HOME/PCB\uFF09\u3002",
|
|
329
|
+
"2. eda_export_netlist\uFF08\u5DF2\u89E3\u6790\u6210\u5BF9\u8C61\uFF09\u68C0\u67E5\u6BCF\u4E2A\u7F51\u7EDC \u22652 \u7AEF\u70B9\u3001\u65E0\u60AC\u7A7A\u3002",
|
|
330
|
+
"3. eda_run_drc \u770B\u9010\u6761\u660E\u7EC6\uFF08\u542B primitiveId \u4E0E\u5750\u6807\uFF09\u3002",
|
|
331
|
+
"4. \u9700\u8981\u4EBA\u770B\u6548\u679C\u5C31\u5BFC\u51FA\u56FE\u7247\u81EA\u884C\u8BC6\u522B\uFF0C\u4E0D\u8981\u7528\u81EA\u52A8\u5E03\u5C40\u201C\u4FEE\u201D\u7248\u9762\u3002"
|
|
332
|
+
].join("\n");
|
|
333
|
+
|
|
296
334
|
// src/bridge/index.ts
|
|
297
|
-
var VERSION = "0.4.
|
|
335
|
+
var VERSION = "0.4.2";
|
|
298
336
|
var WS_PORT = Number(process.env.DSH_EDA_MCP_WS_PORT ?? 39009);
|
|
299
337
|
var WS_HOST = "127.0.0.1";
|
|
300
338
|
var ALLOW_RAW_API = (process.env.DSH_EDA_MCP_ALLOW_RAW_API ?? "1") !== "0";
|
|
339
|
+
var ALLOW_AUTO_LAYOUT = (process.env.DSH_EDA_ALLOW_AUTO_LAYOUT ?? "0") === "1";
|
|
301
340
|
var TOOL_TIMEOUT_MS = Number(process.env.DSH_EDA_MCP_TOOL_TIMEOUT_MS ?? 6e4);
|
|
302
341
|
var MCP_UNITS_PER_INCH = 100;
|
|
303
342
|
var PAPER_SIZES = {
|
|
@@ -321,6 +360,18 @@ function safeAreas(paperName) {
|
|
|
321
360
|
lowerLeft: { left: 40, bottom: 40, right: paper.titleBlock.left - 20, top: paper.titleBlock.top - 10 }
|
|
322
361
|
};
|
|
323
362
|
}
|
|
363
|
+
function decodeTextPayload(value) {
|
|
364
|
+
const text = typeof value === "string" ? value : "";
|
|
365
|
+
if (text === "") return text;
|
|
366
|
+
if (text.charCodeAt(0) === 65279) return text.slice(1);
|
|
367
|
+
if (text.charCodeAt(0) === 255 && text.charCodeAt(1) === 254) {
|
|
368
|
+
return Buffer.from(text.slice(2), "latin1").toString("utf16le");
|
|
369
|
+
}
|
|
370
|
+
if (text.length > 3 && text.charCodeAt(1) === 0 && text.charCodeAt(3) === 0) {
|
|
371
|
+
return Buffer.from(text, "latin1").toString("utf16le");
|
|
372
|
+
}
|
|
373
|
+
return text;
|
|
374
|
+
}
|
|
324
375
|
function checkPointInFrame(x, y, paperName, margin = 0) {
|
|
325
376
|
const paper = paperOf(paperName);
|
|
326
377
|
const f = paper.frame;
|
|
@@ -539,11 +590,11 @@ var TOOLS = [
|
|
|
539
590
|
}, ["libraryUuid", "uuid", "x", "y"]) },
|
|
540
591
|
{ name: "eda_place_net_flag", description: "Place a power/ground net flag (VCC/GND etc.) on the active schematic page.", inputSchema: objectSchema({ identification: { type: "string", enum: ["Power", "Ground", "AnalogGround", "ProtectGround"] }, net: { type: "string" }, x: { type: "number" }, y: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] }, mirror: { type: "boolean" }, paper: { type: "string" }, allowOutsideFrame: { type: "boolean" } }, ["identification", "net", "x", "y"]) },
|
|
541
592
|
{ name: "eda_place_net_port", description: "Place a net port on the active schematic page.", inputSchema: objectSchema({ direction: { type: "string", enum: ["IN", "OUT", "BI"] }, net: { type: "string" }, x: { type: "number" }, y: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] }, mirror: { type: "boolean" }, paper: { type: "string" }, allowOutsideFrame: { type: "boolean" } }, ["direction", "net", "x", "y"]) },
|
|
542
|
-
{ name: "eda_get_pins", description: "List pins of one schematic component. Use the returned primitiveId with eda_place_net_flag_at_pin, eda_place_net_label_at_pin, or eda_set_no_connect.", inputSchema: objectSchema({ componentPrimitiveId: { type: "string" } }, ["componentPrimitiveId"]) },
|
|
593
|
+
{ name: "eda_get_pins", description: "List pins of one schematic component in a single round trip, including pinName and pinNumber (no per-pin sch_PrimitivePin.get needed). Use the returned primitiveId with eda_place_net_flag_at_pin, eda_place_net_label_at_pin, or eda_set_no_connect.", inputSchema: objectSchema({ componentPrimitiveId: { type: "string" } }, ["componentPrimitiveId"]) },
|
|
543
594
|
{ name: "eda_place_net_flag_at_pin", description: "Place a power/ground net flag at a pin WITHOUT covering the symbol: the pin direction is read from the pin rotation, a stub wire of stubLength MCP units (default 20) is drawn outwards from the pin with the net name, and the flag is placed at its end. Pass offsetX/offsetY to place it manually (that disables the stub) or stubLength=0 for the old on-pin behaviour.", inputSchema: objectSchema({ pinPrimitiveId: { type: "string" }, identification: { type: "string", enum: ["Power", "Ground", "AnalogGround", "ProtectGround"] }, net: { type: "string" }, stubLength: { type: "number" }, offsetX: { type: "number" }, offsetY: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] }, mirror: { type: "boolean" }, color: { type: "string" }, lineWidth: { type: "integer" }, lineType: { type: "integer" }, paper: { type: "string" }, allowOutsideFrame: { type: "boolean" } }, ["pinPrimitiveId", "identification", "net"]) },
|
|
544
595
|
{ name: "eda_place_net_label_at_pin", description: "Place a REAL net label at a pin (sch_PrimitiveAttribute.createNetLabel; falls back to text and reports method) WITHOUT covering the symbol: a stub wire of stubLength MCP units (default 20) is drawn outwards along the pin direction and the label goes at its end. offsetX/offsetY place it manually (disables the stub); stubLength=0 keeps the old on-pin behaviour.", inputSchema: objectSchema({ pinPrimitiveId: { type: "string" }, net: { type: "string" }, stubLength: { type: "number" }, offsetX: { type: "number" }, offsetY: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] }, fontSize: { type: "number" }, color: { type: "string" }, lineWidth: { type: "integer" }, lineType: { type: "integer" }, paper: { type: "string" }, allowOutsideFrame: { type: "boolean" } }, ["pinPrimitiveId", "net"]) },
|
|
545
596
|
{ name: "eda_set_no_connect", description: "Set or clear the no-connect flag on one or more schematic pins via sch_PrimitivePin.modify.", inputSchema: objectSchema({ primitiveIds: { type: "array", items: { type: "string" } }, noConnected: { type: "boolean" } }, ["primitiveIds"]) },
|
|
546
|
-
{ name: "eda_draw_wire", description: "Draw a schematic wire (polyline). Coordinates are MCP units (0.01 inch).", inputSchema: objectSchema({ points: { type: "array", items: { type: "number" } }, net: { type: "string" }, color: { type: "string" }, lineWidth: { type: "integer", minimum: 1, maximum: 10 }, lineType: { type: "integer", enum: [0, 1, 2, 3] } }, ["points"]) },
|
|
597
|
+
{ name: "eda_draw_wire", description: "Draw a schematic wire (polyline). Coordinates are MCP units (0.01 inch). IMPORTANT: the net argument only sticks on a floating wire \u2014 as soon as an endpoint touches a pin, EDA recomputes the net from connectivity and the name you passed is overwritten (measured: TESTNET merged into LED_A). To join same-named nets across regions use eda_place_net_port or a net label; do not rely on two distantly drawn wires sharing a name.", inputSchema: objectSchema({ points: { type: "array", items: { type: "number" } }, net: { type: "string" }, color: { type: "string" }, lineWidth: { type: "integer", minimum: 1, maximum: 10 }, lineType: { type: "integer", enum: [0, 1, 2, 3] } }, ["points"]) },
|
|
547
598
|
{ name: "eda_draw_rectangle", description: "Draw a rectangle. topLeftY is the UPPER y coordinate (max y); height extends downward. Coordinates are MCP units.", inputSchema: objectSchema({ topLeftX: { type: "number" }, topLeftY: { type: "number" }, width: { type: "number" }, height: { type: "number" }, cornerRadius: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] }, color: { type: "string" }, fillColor: { type: "string" }, lineWidth: { type: "integer", minimum: 1, maximum: 10 }, lineType: { type: "integer", enum: [0, 1, 2, 3] }, fillStyle: { type: "string" } }, ["topLeftX", "topLeftY", "width", "height"]) },
|
|
548
599
|
{ name: "eda_draw_functional_box", description: "Draw a dashed functional-group box around one group of components, with an optional label. Margins and coordinates are MCP units; default margin 1 unit (about 0.254 mm).", inputSchema: objectSchema({ componentIds: { type: "array", items: { type: "string" } }, label: { type: "string" }, margin: { type: "number" }, color: { type: "string" }, lineWidth: { type: "integer", minimum: 1, maximum: 10 }, lineType: { type: "integer", enum: [0, 1, 2, 3] } }) },
|
|
549
600
|
{ name: "eda_draw_circle", description: "Draw a circle. Coordinates are MCP units.", inputSchema: objectSchema({ centerX: { type: "number" }, centerY: { type: "number" }, radius: { type: "number" }, color: { type: "string" }, fillColor: { type: "string" }, lineWidth: { type: "integer", minimum: 1, maximum: 10 }, lineType: { type: "integer", enum: [0, 1, 2, 3] }, fillStyle: { type: "string" } }, ["centerX", "centerY", "radius"]) },
|
|
@@ -556,14 +607,18 @@ var TOOLS = [
|
|
|
556
607
|
{ name: "eda_get_nets", description: "List nets of the current schematic project. Uses sch_Net.* first; when the schematic is not the active document those APIs return [] and this tool falls back to parsing sch_Netlist.getNetlist, reporting which source was used.", inputSchema: objectSchema({ name: { type: "string" }, parseNetlist: { type: "boolean" } }) },
|
|
557
608
|
{ name: "eda_get_active_document", description: "Return the document/tab that is currently active (documentType, uuid, tabId, parent project). Most tab-dependent APIs (sch_Net.*, netlist export, DRC) only work when the matching document is active - use eda_open_document to switch.", inputSchema: objectSchema({}) },
|
|
558
609
|
{ name: "eda_open_document", description: "Open/activate a document tab by uuid (dmt_EditorControl.openDocument). Works for a schematic page or a PCB, so tab-dependent APIs can run.", inputSchema: objectSchema({ documentUuid: { type: "string" }, splitScreenId: { type: "string" } }, ["documentUuid"]) },
|
|
559
|
-
{ name: "eda_get_net_labels", description: "List
|
|
560
|
-
{ name: "eda_place_net_label", description: "Place a
|
|
561
|
-
{ name: "
|
|
562
|
-
{ name: "
|
|
563
|
-
{ name: "
|
|
610
|
+
{ name: "eda_get_net_labels", description: "List every label-like primitive on the active schematic page: attribute primitives (netlabel/netport/netflag) PLUS plain texts left by the createNetLabel fallback, so they can be audited and deleted. Reports kind/primitiveId/net per entry.", inputSchema: objectSchema({ parentPrimitiveId: { type: "string" } }) },
|
|
611
|
+
{ name: "eda_place_net_label", description: "Place a net label at x/y. The real API (sch_PrimitiveAttribute.createNetLabel) is probed once per session; when the build never answers, a plain TEXT label is placed instead, which carries no electrical meaning and must be paired with a stub wire. Pass probe:true to re-check.", inputSchema: objectSchema({ x: { type: "number" }, y: { type: "number" }, net: { type: "string" }, fontSize: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] } }, ["x", "y", "net"]) },
|
|
612
|
+
{ name: "eda_place_net_labels", description: "Place many net labels in ONE call (per-pin or by coordinate) and report done/failed per item: a 133-label schematic used to exceed the caller timeout with no progress. items: [{ pinPrimitiveId, net, stubLength?, offsetX?, offsetY? }] or [{ x, y, net, fontSize? }].", inputSchema: objectSchema({ items: { type: "array", items: { type: "object", additionalProperties: true } }, probe: { type: "boolean" }, concurrency: { type: "integer", minimum: 1, maximum: 8 } }, ["items"]) },
|
|
613
|
+
{ name: "eda_run_drc", description: 'Run schematic DRC with includeVerboseError. On EDA 3.2.186 the app still returns AGGREGATED entries ({type:"warn",count:N}) with no primitiveId/coordinates, so entries are reported as-is and raw keeps the untouched payload; expect counts plus a hint until the app exposes details.', inputSchema: objectSchema({ strict: { type: "boolean" } }) },
|
|
614
|
+
{ name: "eda_auto_layout", description: "Run schematic auto-layout (sch_Document.autoLayout). DISABLED by default: automated layout is not used unless the user asks for it. Pass force:true only then.", inputSchema: objectSchema({ uuids: { type: "array", items: { type: "string" } }, force: { type: "boolean" } }) },
|
|
615
|
+
{ name: "eda_auto_route", description: "Run schematic auto-routing (sch_Document.autoRouting). DISABLED by default: route by hand unless the user explicitly asks. Pass force:true only then.", inputSchema: objectSchema({ uuids: { type: "array", items: { type: "string" } }, force: { type: "boolean" } }) },
|
|
564
616
|
{ name: "eda_export_netlist", description: "Export the schematic netlist as text (default JLCEDA format). Returns null when no schematic is active - check eda_get_active_document / open the schematic page first.", inputSchema: objectSchema({ type: { type: "string" } }) },
|
|
565
|
-
{ name: "eda_export_bom", description: "Export the schematic BOM. csv
|
|
566
|
-
{ name: "
|
|
617
|
+
{ name: "eda_export_bom", description: "Export the schematic BOM. csv is decoded from UTF-16 to readable text; xlsx returns a base64 data URL.", inputSchema: objectSchema({ format: { type: "string", enum: ["csv", "xlsx"] } }) },
|
|
618
|
+
{ name: "eda_move_component", description: "Move/rotate/mirror a schematic component (sch_PrimitiveComponent.modify) without deleting and re-placing it, so the designator and value survive.", inputSchema: objectSchema({ primitiveId: { type: "string" }, x: { type: "number" }, y: { type: "number" }, rotation: { type: "number" }, mirror: { type: "boolean" } }, ["primitiveId"]) },
|
|
619
|
+
{ name: "eda_workflow_guide", description: "Return the schematic working standard: grid-aligned manual placement, signal flow, when net labels/ports are required (a wire net name is overridden by connectivity), net-label fallback behaviour, batch placement and the verification order. Read it before laying out a sheet.", inputSchema: objectSchema({}) },
|
|
620
|
+
{ name: "eda_list_api_methods", description: 'List every API method the connected extension accepts (its allow-list). Call it when eda_api_call answers "API method not allowed".', inputSchema: objectSchema({}) },
|
|
621
|
+
{ name: "eda_delete_primitive", description: 'Delete a schematic primitive by kind and primitive id. Net ports, net flags and net labels are attribute primitives and need kind "netport"/"netflag"/"netlabel"; the text fallback labels are kind "text".', inputSchema: objectSchema({ kind: { type: "string", enum: ["component", "wire", "rectangle", "circle", "arc", "polygon", "text", "netport", "netflag", "netlabel"] }, primitiveId: { type: "string" } }, ["kind", "primitiveId"]) },
|
|
567
622
|
{ name: "eda_create_board", description: "Create a board linking a schematic and a PCB (dmt_Board.createBoard).", inputSchema: objectSchema({ schematicUuid: { type: "string" }, pcbUuid: { type: "string" } }) },
|
|
568
623
|
{ name: "eda_api_call", description: "Call an allow-listed JLCEDA Pro extension API method directly.", inputSchema: objectSchema({ method: { type: "string" }, args: { type: "array" } }, ["method", "args"]) },
|
|
569
624
|
{ name: "pcb_get_components", description: "List PCB components (mil). includePins=true also reads pcb_PrimitiveComponent.getAllPinsByPrimitiveId per component, which is the accurate pad/net source; the fast summary pad list can be stale.", inputSchema: objectSchema({ layer: { type: ["string", "integer"] }, locked: { type: "boolean" }, includeBounds: { type: "boolean" }, includePins: { type: "boolean" } }) },
|
|
@@ -747,6 +802,8 @@ var WsConnection = class {
|
|
|
747
802
|
var clients = /* @__PURE__ */ new Set();
|
|
748
803
|
var pendingCalls = /* @__PURE__ */ new Map();
|
|
749
804
|
var pcbComponentBaseline = null;
|
|
805
|
+
var netLabelSupport = null;
|
|
806
|
+
var placedTextLabels = [];
|
|
750
807
|
function sleep(ms) {
|
|
751
808
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
752
809
|
}
|
|
@@ -1213,30 +1270,88 @@ async function parseNetlist(previous, timeoutMs) {
|
|
|
1213
1270
|
const nets = [...byNet.entries()].map(([net, pins]) => ({ net, pins, pinCount: pins.length }));
|
|
1214
1271
|
return { source: "sch_Netlist.getNetlist", count: nets.length, nets, netlistBytes: text.length };
|
|
1215
1272
|
}
|
|
1216
|
-
async function placeNetLabel(net, x, y, fontSize, timeoutMs) {
|
|
1273
|
+
async function placeNetLabel(net, x, y, fontSize, timeoutMs, options = {}) {
|
|
1217
1274
|
if (net === "" || net === "undefined") throw new Error("a net name is required");
|
|
1218
1275
|
assertNetName(String(net), "eda_place_net_label");
|
|
1219
1276
|
const size = fontSize === void 0 ? 12 : Number(fontSize);
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1277
|
+
if (options.probe === true) netLabelSupport = null;
|
|
1278
|
+
let failure = null;
|
|
1279
|
+
if (netLabelSupport !== false) {
|
|
1280
|
+
try {
|
|
1281
|
+
const raw2 = await callExtension("sch_PrimitiveAttribute.createNetLabel", [x, y, net], Math.min(timeoutMs, 4e3));
|
|
1282
|
+
if (raw2 === void 0 || raw2 === null) throw new Error("createNetLabel returned nothing");
|
|
1283
|
+
netLabelSupport = true;
|
|
1284
|
+
return { method: "netLabel", net, x, y, label: summarizePrimitive(raw2) };
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1287
|
+
if (!/not found|not available|not allowed|timed out|timeout|nothing|undefined/i.test(message)) throw error;
|
|
1288
|
+
failure = message;
|
|
1289
|
+
netLabelSupport = false;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
const raw = await callExtension("sch_PrimitiveText.create", [x, y, net, 0, null, null, size, false, false, false, 1], timeoutMs);
|
|
1293
|
+
const label = summarizePrimitive(raw);
|
|
1294
|
+
if (label.primitiveId !== void 0) {
|
|
1295
|
+
placedTextLabels.push({ net: String(net), x, y, primitiveId: label.primitiveId });
|
|
1296
|
+
if (placedTextLabels.length > 2e3) placedTextLabels.shift();
|
|
1297
|
+
}
|
|
1298
|
+
return {
|
|
1299
|
+
method: "text",
|
|
1300
|
+
net,
|
|
1301
|
+
x,
|
|
1302
|
+
y,
|
|
1303
|
+
created: label.primitiveId !== void 0,
|
|
1304
|
+
warning: "createNetLabel is unavailable on this EDA build (" + (failure ?? "probe already failed this session") + "), so a plain TEXT label was placed: it carries no electrical meaning. The stub wire drawn by the caller is what joins the net; eda_get_net_labels lists these texts by name and id.",
|
|
1305
|
+
...label.primitiveId === void 0 ? { hint: "No label primitive was created: the schematic page must be the ACTIVE document (sch_PrimitiveText.create is tab-dependent). Use eda_get_active_document / eda_open_document, then retry." } : {},
|
|
1306
|
+
label
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
async function placeAtPin(toolName, args, timeoutMs) {
|
|
1310
|
+
const pinPrimitiveId = String(args.pinPrimitiveId ?? "");
|
|
1311
|
+
if (pinPrimitiveId === "") throw new Error("pinPrimitiveId is required");
|
|
1312
|
+
const isFlag = toolName === "eda_place_net_flag_at_pin";
|
|
1313
|
+
assertNetName(String(args.net ?? ""), toolName);
|
|
1314
|
+
const pin = await callExtension("sch_PrimitivePin.get", [pinPrimitiveId], timeoutMs);
|
|
1315
|
+
const pinRecord = asRecord(pin);
|
|
1316
|
+
const pinX = Number(pickValue(pinRecord, "X", "x"));
|
|
1317
|
+
const pinY = Number(pickValue(pinRecord, "Y", "y"));
|
|
1318
|
+
if (!Number.isFinite(pinX) || !Number.isFinite(pinY)) throw new Error("could not read pin position for " + pinPrimitiveId);
|
|
1319
|
+
const pinRotation = Number(pickValue(pinRecord, "Rotation", "rotation") ?? 0);
|
|
1320
|
+
const direction = pinDirection(pinRotation);
|
|
1321
|
+
const explicitOffset = args.offsetX !== void 0 || args.offsetY !== void 0;
|
|
1322
|
+
const stubLength = args.stubLength === void 0 ? explicitOffset ? 0 : 20 : Math.max(0, Number(args.stubLength));
|
|
1323
|
+
const x = pinX + direction.dx * stubLength + Number(args.offsetX ?? 0);
|
|
1324
|
+
const y = pinY + direction.dy * stubLength + Number(args.offsetY ?? 0);
|
|
1325
|
+
if (args.allowOutsideFrame !== true) {
|
|
1326
|
+
const problem = checkPointInFrame(x, y, args.paper, 5) ?? checkPointAgainstTitleBlock(x, y, args.paper, 5);
|
|
1327
|
+
if (problem !== void 0) {
|
|
1328
|
+
throw new Error(toolName + " refused: " + problem + " - reduce stubLength or pass offsetX/offsetY to place it elsewhere.");
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
let stub;
|
|
1332
|
+
if (stubLength > 0 && typeof args.net === "string" && args.net !== "") {
|
|
1333
|
+
stub = summarizePrimitive(await callExtension("sch_PrimitiveWire.create", [
|
|
1334
|
+
[pinX, pinY, x, y],
|
|
1335
|
+
String(args.net),
|
|
1336
|
+
args.color === void 0 ? void 0 : String(args.color),
|
|
1337
|
+
args.lineWidth === void 0 ? void 0 : Number(args.lineWidth),
|
|
1338
|
+
args.lineType === void 0 ? void 0 : Number(args.lineType)
|
|
1339
|
+
], timeoutMs));
|
|
1340
|
+
}
|
|
1341
|
+
const placement = { pin: { primitiveId: pinPrimitiveId, x: pinX, y: pinY, rotation: pinRotation }, direction, stubLength, x, y, stub };
|
|
1342
|
+
if (isFlag) {
|
|
1343
|
+
const flag = await callExtension("sch_PrimitiveComponent.createNetFlag", [
|
|
1344
|
+
String(args.identification),
|
|
1345
|
+
String(args.net),
|
|
1232
1346
|
x,
|
|
1233
1347
|
y,
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
};
|
|
1348
|
+
args.rotation === void 0 ? pinRotation : Number(args.rotation),
|
|
1349
|
+
Boolean(args.mirror ?? false)
|
|
1350
|
+
], timeoutMs);
|
|
1351
|
+
return { ...placement, flag: summarizePrimitive(flag) };
|
|
1239
1352
|
}
|
|
1353
|
+
const label = await placeNetLabel(String(args.net), x, y, args.fontSize, timeoutMs);
|
|
1354
|
+
return { ...label, ...placement };
|
|
1240
1355
|
}
|
|
1241
1356
|
function isInternalHelperMissing(error) {
|
|
1242
1357
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1269,10 +1384,10 @@ async function resolveProjectUuid(args, timeoutMs) {
|
|
|
1269
1384
|
const teamUuid = String(pickValue(asRecord(team), "uuid", "Uuid") ?? "");
|
|
1270
1385
|
if (teamUuid === "") continue;
|
|
1271
1386
|
const owned = await callExtension("dmt_Project.getAllProjectsUuid", [teamUuid], toolTimeout(args, timeoutMs)).catch(() => []);
|
|
1272
|
-
for (const
|
|
1273
|
-
if (seen.has(
|
|
1274
|
-
seen.add(
|
|
1275
|
-
candidates.push(
|
|
1387
|
+
for (const uuid2 of Array.isArray(owned) ? owned.map(String) : []) {
|
|
1388
|
+
if (seen.has(uuid2)) continue;
|
|
1389
|
+
seen.add(uuid2);
|
|
1390
|
+
candidates.push(uuid2);
|
|
1276
1391
|
}
|
|
1277
1392
|
}
|
|
1278
1393
|
}
|
|
@@ -1291,6 +1406,56 @@ async function resolveProjectUuid(args, timeoutMs) {
|
|
|
1291
1406
|
}
|
|
1292
1407
|
return { uuid: matched.uuid, matched };
|
|
1293
1408
|
}
|
|
1409
|
+
async function waitForActiveProject(uuid2, timeoutMs = 4e3) {
|
|
1410
|
+
const deadline = Date.now() + timeoutMs;
|
|
1411
|
+
let lastActive = null;
|
|
1412
|
+
for (; ; ) {
|
|
1413
|
+
try {
|
|
1414
|
+
const info = asRecord(await callExtension("dmt_Project.getCurrentProjectInfo", [], 3e3));
|
|
1415
|
+
lastActive = String(pickValue(info, "uuid", "Uuid") ?? "") || null;
|
|
1416
|
+
if (lastActive === uuid2) return { active: true, project: info };
|
|
1417
|
+
} catch {
|
|
1418
|
+
}
|
|
1419
|
+
if (Date.now() > deadline) return { active: false, project: null, lastActive };
|
|
1420
|
+
await sleep(400);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
function internalIdsOf(info) {
|
|
1424
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1425
|
+
const boards = Array.isArray(asRecord(info).data) ? asRecord(info).data : [];
|
|
1426
|
+
for (const board of boards) {
|
|
1427
|
+
const record = asRecord(board);
|
|
1428
|
+
const boardUuid = String(pickValue(record, "uuid", "Uuid") ?? "");
|
|
1429
|
+
if (boardUuid !== "") ids.add(boardUuid);
|
|
1430
|
+
for (const docKey of ["schematic", "pcb"]) {
|
|
1431
|
+
const doc = asRecord(record[docKey]);
|
|
1432
|
+
const docUuid = String(pickValue(doc, "uuid", "Uuid") ?? "");
|
|
1433
|
+
if (docUuid !== "") ids.add(docUuid);
|
|
1434
|
+
const pages = Array.isArray(doc.pages) ? doc.pages : [];
|
|
1435
|
+
for (const page of pages) {
|
|
1436
|
+
const pageUuid = String(pickValue(asRecord(page), "uuid", "Uuid") ?? "");
|
|
1437
|
+
if (pageUuid !== "") ids.add(pageUuid);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return ids;
|
|
1442
|
+
}
|
|
1443
|
+
async function assertKnownPcbNet(toolName, net, timeoutMs, force) {
|
|
1444
|
+
const name = String(net ?? "").trim();
|
|
1445
|
+
if (name === "") throw new Error(toolName + " requires a net name");
|
|
1446
|
+
if (force === true) return;
|
|
1447
|
+
let names;
|
|
1448
|
+
try {
|
|
1449
|
+
const raw = await callExtension("pcb_Net.getAllNetsName", [], Math.min(timeoutMs, 8e3));
|
|
1450
|
+
names = new Set((Array.isArray(raw) ? raw : []).map((value) => String(value)));
|
|
1451
|
+
} catch {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (names.size === 0) return;
|
|
1455
|
+
if (names.has(name)) return;
|
|
1456
|
+
const sample = [...names].slice(0, 12).join(", ");
|
|
1457
|
+
throw new Error(toolName + ': net "' + name + '" is not on this PCB (' + names.size + " nets: " + sample + "\u2026). A write on an unknown net creates a phantom net that DRC reports as a netlist error; pass force:true if that is really intended.");
|
|
1458
|
+
}
|
|
1294
1459
|
async function executeTool(name, rawArgs, timeoutMs) {
|
|
1295
1460
|
const args = asRecord(rawArgs);
|
|
1296
1461
|
const tool = PCB_TOOL_ALIASES[name] ?? name;
|
|
@@ -1361,24 +1526,45 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
1361
1526
|
saveTo
|
|
1362
1527
|
], toolTimeout(args, timeoutMs));
|
|
1363
1528
|
const raw = imported?.result ?? imported;
|
|
1364
|
-
const
|
|
1529
|
+
const uuid2 = typeof raw === "string" ? raw : typeof raw?.uuid === "string" ? raw.uuid : void 0;
|
|
1365
1530
|
const landed = raw !== null && raw !== void 0;
|
|
1366
1531
|
let opened = false;
|
|
1367
|
-
if (args.open === true &&
|
|
1532
|
+
if (args.open === true && uuid2 !== void 0) {
|
|
1368
1533
|
try {
|
|
1369
|
-
|
|
1534
|
+
await callExtension("dmt_Project.openProject", [uuid2], timeoutMs);
|
|
1535
|
+
opened = (await waitForActiveProject(uuid2, 4e3)).active;
|
|
1370
1536
|
} catch {
|
|
1371
1537
|
opened = false;
|
|
1372
1538
|
}
|
|
1373
1539
|
}
|
|
1540
|
+
let collisions = null;
|
|
1541
|
+
if (landed && uuid2 !== void 0 && args.checkCollisions !== false) {
|
|
1542
|
+
try {
|
|
1543
|
+
const mine = await callExtension("dmt_Project.getProjectInfo", [uuid2], toolTimeout(args, timeoutMs));
|
|
1544
|
+
const myIds = internalIdsOf(mine);
|
|
1545
|
+
const others = await callExtension("dmt_Project.getAllProjectsUuid", [], toolTimeout(args, timeoutMs));
|
|
1546
|
+
const list = (Array.isArray(others) ? others.map(String) : []).filter((other) => other !== uuid2).slice(0, 20);
|
|
1547
|
+
collisions = [];
|
|
1548
|
+
for (const other of list) {
|
|
1549
|
+
const info = await callExtension("dmt_Project.getProjectInfo", [other], timeoutMs).catch(() => void 0);
|
|
1550
|
+
const shared = [...internalIdsOf(info)].filter((sharedId) => myIds.has(sharedId));
|
|
1551
|
+
if (shared.length > 0) {
|
|
1552
|
+
collisions.push({ projectUuid: other, friendlyName: pickValue(asRecord(info), "friendlyName", "FriendlyName") ?? null, sharedIds: shared });
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
} catch {
|
|
1556
|
+
collisions = null;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1374
1559
|
return {
|
|
1375
1560
|
ok: landed,
|
|
1376
1561
|
imported: landed,
|
|
1377
1562
|
name: basename(path),
|
|
1378
1563
|
size: bytes.length,
|
|
1379
1564
|
teamUuid,
|
|
1380
|
-
uuid:
|
|
1565
|
+
uuid: uuid2 ?? null,
|
|
1381
1566
|
opened,
|
|
1567
|
+
collisions,
|
|
1382
1568
|
result: raw ?? null,
|
|
1383
1569
|
hint: landed ? null : "The app returned nothing: the import did not create a project. That means this build ignores the saveTo target (same limitation as createProject), so import cannot be used to create projects here."
|
|
1384
1570
|
};
|
|
@@ -1441,10 +1627,10 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
1441
1627
|
const teamUuid = String(pickValue(asRecord(team), "uuid", "Uuid") ?? "");
|
|
1442
1628
|
if (teamUuid === "") continue;
|
|
1443
1629
|
const owned = await callExtension("dmt_Project.getAllProjectsUuid", [teamUuid], toolTimeout(args, timeoutMs)).catch(() => []);
|
|
1444
|
-
for (const
|
|
1445
|
-
if (seen.has(
|
|
1446
|
-
seen.add(
|
|
1447
|
-
list.push(
|
|
1630
|
+
for (const uuid2 of Array.isArray(owned) ? owned.map(String) : []) {
|
|
1631
|
+
if (seen.has(uuid2)) continue;
|
|
1632
|
+
seen.add(uuid2);
|
|
1633
|
+
list.push(uuid2);
|
|
1448
1634
|
}
|
|
1449
1635
|
}
|
|
1450
1636
|
aggregated = true;
|
|
@@ -1452,11 +1638,11 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
1452
1638
|
if (args.resolveNames === false) return { count: list.length, uuids: list, aggregated };
|
|
1453
1639
|
const limit = Number.isFinite(Number(args.limit)) ? Math.max(1, Number(args.limit)) : list.length;
|
|
1454
1640
|
const projects = [];
|
|
1455
|
-
for (const
|
|
1641
|
+
for (const uuid2 of list.slice(0, limit)) {
|
|
1456
1642
|
try {
|
|
1457
|
-
projects.push(await callExtension("dmt_Project.getProjectInfo", [
|
|
1643
|
+
projects.push(await callExtension("dmt_Project.getProjectInfo", [uuid2], toolTimeout(args, timeoutMs)));
|
|
1458
1644
|
} catch {
|
|
1459
|
-
projects.push({ uuid });
|
|
1645
|
+
projects.push({ uuid: uuid2 });
|
|
1460
1646
|
}
|
|
1461
1647
|
}
|
|
1462
1648
|
return { count: list.length, resolved: projects.length, aggregated, projects };
|
|
@@ -1467,10 +1653,17 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
1467
1653
|
}
|
|
1468
1654
|
case "eda_open_project": {
|
|
1469
1655
|
const resolvedProject = await resolveProjectUuid(args, timeoutMs);
|
|
1470
|
-
const
|
|
1471
|
-
const
|
|
1472
|
-
const
|
|
1473
|
-
return {
|
|
1656
|
+
const uuid2 = resolvedProject.uuid;
|
|
1657
|
+
const returned = await callExtension("dmt_Project.openProject", [uuid2], toolTimeout(args, timeoutMs));
|
|
1658
|
+
const activation = await waitForActiveProject(uuid2, args.verifyMs === void 0 ? 4e3 : Number(args.verifyMs));
|
|
1659
|
+
return {
|
|
1660
|
+
opened: activation.active,
|
|
1661
|
+
openReturned: returned !== false,
|
|
1662
|
+
uuid: uuid2,
|
|
1663
|
+
matched: resolvedProject.matched,
|
|
1664
|
+
current: activation.active ? summarizePrimitive(activation.project) : null,
|
|
1665
|
+
...activation.active ? {} : { hint: "The project did not become active within the verification window (last active: " + (activation.lastActive ?? "unknown") + "); it may still be loading, or the app refused the switch." }
|
|
1666
|
+
};
|
|
1474
1667
|
}
|
|
1475
1668
|
case "eda_get_workspace": {
|
|
1476
1669
|
const workspace = await callExtension("dmt_Workspace.getCurrentWorkspaceInfo", [], timeoutMs).catch(() => void 0);
|
|
@@ -1545,13 +1738,26 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
1545
1738
|
uuid: args.sourceUuid,
|
|
1546
1739
|
name: args.sourceName
|
|
1547
1740
|
}, timeoutMs);
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1741
|
+
let copyRaw;
|
|
1742
|
+
try {
|
|
1743
|
+
copyRaw = await callExtension("dmt_Project.copyProject", [
|
|
1744
|
+
source.uuid,
|
|
1745
|
+
args.targetTeamUuid === void 0 ? void 0 : String(args.targetTeamUuid),
|
|
1746
|
+
args.targetFolderUuid === void 0 ? void 0 : String(args.targetFolderUuid),
|
|
1747
|
+
args.newFriendlyName === void 0 ? void 0 : String(args.newFriendlyName),
|
|
1748
|
+
args.newName === void 0 ? void 0 : String(args.newName)
|
|
1749
|
+
], toolTimeout(args, timeoutMs));
|
|
1750
|
+
} catch (failure) {
|
|
1751
|
+
throw new Error("eda_copy_project failed: " + (failure instanceof Error ? failure.message : String(failure)));
|
|
1752
|
+
}
|
|
1753
|
+
const copyUuid = typeof copyRaw === "string" ? copyRaw : String(pickValue(asRecord(copyRaw), "uuid", "Uuid") ?? "");
|
|
1754
|
+
return {
|
|
1755
|
+
copied: copyUuid !== "",
|
|
1756
|
+
sourceUuid: source.uuid,
|
|
1757
|
+
uuid: copyUuid === "" ? null : copyUuid,
|
|
1758
|
+
raw: copyRaw ?? null,
|
|
1759
|
+
hint: copyUuid === "" ? "The app returned no uuid and created nothing: copyProject does not work on this build for local projects (same family as createProject/deleteProject). Duplicate it in the app, or export with eda_export_project and import it back with eda_import_project to get an independent copy." : null
|
|
1760
|
+
};
|
|
1555
1761
|
return { copied: typeof uuid === "string" && uuid !== "", sourceUuid: source.uuid, uuid: uuid ?? null };
|
|
1556
1762
|
}
|
|
1557
1763
|
case "eda_create_project": {
|
|
@@ -1568,25 +1774,26 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
1568
1774
|
throw new Error("eda_create_project: no team is available (sign in to a JLC account in JLCEDA Pro, or pass teamUuid from eda_list_teams)");
|
|
1569
1775
|
}
|
|
1570
1776
|
const folderUuid = args.folderUuid === void 0 ? "" : String(args.folderUuid).trim();
|
|
1571
|
-
const
|
|
1777
|
+
const uuid2 = await callExtension("dmt_Project.createProject", [
|
|
1572
1778
|
friendlyName,
|
|
1573
1779
|
args.name === void 0 ? void 0 : String(args.name),
|
|
1574
1780
|
teamUuid,
|
|
1575
1781
|
folderUuid === "" ? void 0 : folderUuid,
|
|
1576
1782
|
args.description === void 0 ? void 0 : String(args.description)
|
|
1577
1783
|
], toolTimeout(args, timeoutMs));
|
|
1578
|
-
if (typeof
|
|
1784
|
+
if (typeof uuid2 !== "string" || uuid2 === "") {
|
|
1579
1785
|
throw new Error("eda_create_project: the app returned no project uuid (team " + teamUuid + " from " + teamSource + "; the app may have shown a dialog or rejected the name)");
|
|
1580
1786
|
}
|
|
1581
1787
|
let opened = false;
|
|
1582
1788
|
if (args.open === true) {
|
|
1583
1789
|
try {
|
|
1584
|
-
|
|
1790
|
+
await callExtension("dmt_Project.openProject", [uuid2], timeoutMs);
|
|
1791
|
+
opened = (await waitForActiveProject(uuid2, 4e3)).active;
|
|
1585
1792
|
} catch {
|
|
1586
1793
|
opened = false;
|
|
1587
1794
|
}
|
|
1588
1795
|
}
|
|
1589
|
-
return { created: true, uuid, friendlyName, teamUuid, teamSource, folderUuid: folderUuid === "" ? null : folderUuid, opened };
|
|
1796
|
+
return { created: true, uuid: uuid2, friendlyName, teamUuid, teamSource, folderUuid: folderUuid === "" ? null : folderUuid, opened };
|
|
1590
1797
|
}
|
|
1591
1798
|
case "eda_launch": {
|
|
1592
1799
|
const probe = launchProbe();
|
|
@@ -1768,68 +1975,90 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
1768
1975
|
const componentPrimitiveId = String(args.componentPrimitiveId ?? "");
|
|
1769
1976
|
if (componentPrimitiveId === "") throw new Error("componentPrimitiveId is required");
|
|
1770
1977
|
const raw = await callExtension("sch_PrimitiveComponent.getAllPinsByPrimitiveId", [componentPrimitiveId], timeoutMs);
|
|
1771
|
-
if (Array.isArray(raw)) return raw
|
|
1772
|
-
return raw
|
|
1978
|
+
if (!Array.isArray(raw)) return raw;
|
|
1979
|
+
return raw.map((entry) => {
|
|
1980
|
+
const record = asRecord(entry);
|
|
1981
|
+
return {
|
|
1982
|
+
primitiveId: pickValue(record, "PrimitiveId", "primitiveId"),
|
|
1983
|
+
pinNumber: pickValue(record, "PinNumber", "pinNumber") ?? null,
|
|
1984
|
+
pinName: pickValue(record, "PinName", "pinName") ?? null,
|
|
1985
|
+
x: pickValue(record, "X", "x"),
|
|
1986
|
+
y: pickValue(record, "Y", "y"),
|
|
1987
|
+
rotation: pickValue(record, "Rotation", "rotation") ?? 0,
|
|
1988
|
+
pinLength: pickValue(record, "PinLength", "pinLength") ?? null,
|
|
1989
|
+
noConnected: pickValue(record, "NoConnected", "noConnected") ?? null
|
|
1990
|
+
};
|
|
1991
|
+
});
|
|
1773
1992
|
}
|
|
1774
1993
|
case "eda_place_net_flag_at_pin":
|
|
1775
|
-
case "eda_place_net_label_at_pin":
|
|
1776
|
-
|
|
1777
|
-
if (pinPrimitiveId === "") throw new Error("pinPrimitiveId is required");
|
|
1778
|
-
assertNetName(String(args.net ?? ""), name === "eda_place_net_flag_at_pin" ? "eda_place_net_flag_at_pin" : "eda_place_net_label_at_pin");
|
|
1779
|
-
const pin = await callExtension("sch_PrimitivePin.get", [pinPrimitiveId], timeoutMs);
|
|
1780
|
-
const pinRecord = asRecord(pin);
|
|
1781
|
-
const pinX = Number(pickValue(pinRecord, "X", "x"));
|
|
1782
|
-
const pinY = Number(pickValue(pinRecord, "Y", "y"));
|
|
1783
|
-
if (!Number.isFinite(pinX) || !Number.isFinite(pinY)) throw new Error("could not read pin position for " + pinPrimitiveId);
|
|
1784
|
-
const pinRotation = Number(pickValue(pinRecord, "Rotation", "rotation") ?? 0);
|
|
1785
|
-
const direction = pinDirection(pinRotation);
|
|
1786
|
-
const explicitOffset = args.offsetX !== void 0 || args.offsetY !== void 0;
|
|
1787
|
-
const stubLength = args.stubLength === void 0 ? explicitOffset ? 0 : 20 : Math.max(0, Number(args.stubLength));
|
|
1788
|
-
const x = pinX + direction.dx * stubLength + Number(args.offsetX ?? 0);
|
|
1789
|
-
const y = pinY + direction.dy * stubLength + Number(args.offsetY ?? 0);
|
|
1790
|
-
if (args.allowOutsideFrame !== true) {
|
|
1791
|
-
const problem = checkPointInFrame(x, y, args.paper, 5) ?? checkPointAgainstTitleBlock(x, y, args.paper, 5);
|
|
1792
|
-
if (problem !== void 0) {
|
|
1793
|
-
throw new Error((name === "eda_place_net_flag_at_pin" ? "eda_place_net_flag_at_pin" : "eda_place_net_label_at_pin") + " refused: " + problem + " - reduce stubLength or pass offsetX/offsetY to place it elsewhere.");
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
let stub;
|
|
1797
|
-
if (stubLength > 0 && typeof args.net === "string" && args.net !== "") {
|
|
1798
|
-
stub = summarizePrimitive(await callExtension("sch_PrimitiveWire.create", [
|
|
1799
|
-
[pinX, pinY, x, y],
|
|
1800
|
-
String(args.net),
|
|
1801
|
-
args.color === void 0 ? void 0 : String(args.color),
|
|
1802
|
-
args.lineWidth === void 0 ? void 0 : Number(args.lineWidth),
|
|
1803
|
-
args.lineType === void 0 ? void 0 : Number(args.lineType)
|
|
1804
|
-
], timeoutMs));
|
|
1805
|
-
}
|
|
1806
|
-
const placement = { pin: { primitiveId: pinPrimitiveId, x: pinX, y: pinY, rotation: pinRotation }, direction, stubLength, x, y, stub };
|
|
1807
|
-
if (name === "eda_place_net_flag_at_pin") {
|
|
1808
|
-
const flag = await callExtension("sch_PrimitiveComponent.createNetFlag", [
|
|
1809
|
-
String(args.identification),
|
|
1810
|
-
String(args.net),
|
|
1811
|
-
x,
|
|
1812
|
-
y,
|
|
1813
|
-
args.rotation === void 0 ? pinRotation : Number(args.rotation),
|
|
1814
|
-
Boolean(args.mirror ?? false)
|
|
1815
|
-
], timeoutMs);
|
|
1816
|
-
return { ...placement, flag: summarizePrimitive(flag) };
|
|
1817
|
-
}
|
|
1818
|
-
const label = await placeNetLabel(String(args.net), x, y, args.fontSize, timeoutMs);
|
|
1819
|
-
return { ...label, ...placement };
|
|
1820
|
-
}
|
|
1994
|
+
case "eda_place_net_label_at_pin":
|
|
1995
|
+
return await placeAtPin(name, args, timeoutMs);
|
|
1821
1996
|
case "eda_place_net_label": {
|
|
1822
1997
|
const x = Number(args.x);
|
|
1823
1998
|
const y = Number(args.y);
|
|
1824
1999
|
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("eda_place_net_label requires numeric x/y");
|
|
1825
|
-
return await placeNetLabel(String(args.net), x, y, args.fontSize, timeoutMs);
|
|
2000
|
+
return await placeNetLabel(String(args.net), x, y, args.fontSize, timeoutMs, { probe: args.probe === true });
|
|
2001
|
+
}
|
|
2002
|
+
case "eda_place_net_labels": {
|
|
2003
|
+
const items = Array.isArray(args.items) ? args.items.map(asRecord) : [];
|
|
2004
|
+
if (items.length === 0) throw new Error("eda_place_net_labels requires items: [{ pinPrimitiveId, net }] or [{ x, y, net }]");
|
|
2005
|
+
const concurrency = Number.isFinite(Number(args.concurrency)) ? Math.max(1, Math.min(8, Number(args.concurrency))) : 4;
|
|
2006
|
+
const results = await mapLimit(items, concurrency, async (item) => {
|
|
2007
|
+
const net = item.net === void 0 ? "" : String(item.net);
|
|
2008
|
+
try {
|
|
2009
|
+
const placed = item.pinPrimitiveId !== void 0 ? await placeAtPin("eda_place_net_label_at_pin", { ...item, net }, timeoutMs) : await placeNetLabel(net, Number(item.x), Number(item.y), item.fontSize, timeoutMs, { probe: args.probe === true });
|
|
2010
|
+
return { ok: true, net, ...placed };
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
return { ok: false, net, error: error instanceof Error ? error.message : String(error) };
|
|
2013
|
+
}
|
|
2014
|
+
});
|
|
2015
|
+
const done = results.filter((entry) => entry.ok).length;
|
|
2016
|
+
return {
|
|
2017
|
+
total: items.length,
|
|
2018
|
+
done,
|
|
2019
|
+
failed: items.length - done,
|
|
2020
|
+
method: netLabelSupport === true ? "netLabel" : "text",
|
|
2021
|
+
results
|
|
2022
|
+
};
|
|
1826
2023
|
}
|
|
1827
2024
|
case "eda_get_net_labels": {
|
|
1828
|
-
const
|
|
2025
|
+
const attributes = await callExtension("sch_PrimitiveAttribute.getAll", [
|
|
1829
2026
|
args.parentPrimitiveId === void 0 ? void 0 : String(args.parentPrimitiveId)
|
|
1830
|
-
], timeoutMs);
|
|
1831
|
-
|
|
1832
|
-
|
|
2027
|
+
], timeoutMs).catch(() => []);
|
|
2028
|
+
const texts = args.parentPrimitiveId === void 0 ? await callExtension("sch_PrimitiveText.getAll", [], timeoutMs).catch(() => []) : [];
|
|
2029
|
+
const entries = [];
|
|
2030
|
+
for (const entry of Array.isArray(attributes) ? attributes : []) {
|
|
2031
|
+
const record = asRecord(entry);
|
|
2032
|
+
entries.push({
|
|
2033
|
+
primitiveId: pickValue(record, "PrimitiveId", "primitiveId"),
|
|
2034
|
+
kind: String(pickValue(record, "PrimitiveType", "primitiveType", "Type", "type") ?? "attribute"),
|
|
2035
|
+
net: pickValue(record, "Net", "net") ?? null,
|
|
2036
|
+
userValue: pickValue(record, "UserValue", "userValue", "Value", "value") ?? null,
|
|
2037
|
+
x: pickValue(record, "X", "x"),
|
|
2038
|
+
y: pickValue(record, "Y", "y")
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
for (const entry of Array.isArray(texts) ? texts : []) {
|
|
2042
|
+
const record = asRecord(entry);
|
|
2043
|
+
const content = String(pickValue(record, "Content", "content", "Text", "text") ?? "");
|
|
2044
|
+
if (content === "") continue;
|
|
2045
|
+
entries.push({
|
|
2046
|
+
primitiveId: pickValue(record, "PrimitiveId", "primitiveId"),
|
|
2047
|
+
kind: "text",
|
|
2048
|
+
net: content,
|
|
2049
|
+
userValue: content,
|
|
2050
|
+
x: pickValue(record, "X", "x"),
|
|
2051
|
+
y: pickValue(record, "Y", "y")
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
return {
|
|
2055
|
+
count: entries.length,
|
|
2056
|
+
attributeCount: Array.isArray(attributes) ? attributes.length : 0,
|
|
2057
|
+
textCount: Array.isArray(texts) ? texts.length : 0,
|
|
2058
|
+
sessionTextLabels: placedTextLabels.length,
|
|
2059
|
+
note: 'kind "text" entries are plain texts (no electrical meaning). Delete any entry with eda_delete_primitive(kind: "netlabel"|"netport"|"netflag"|"text", primitiveId).',
|
|
2060
|
+
labels: entries
|
|
2061
|
+
};
|
|
1833
2062
|
}
|
|
1834
2063
|
case "eda_get_active_document": {
|
|
1835
2064
|
const info = await callExtension("dmt_SelectControl.getCurrentDocumentInfo", [], timeoutMs);
|
|
@@ -1837,11 +2066,35 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
1837
2066
|
const type = Number(record.documentType);
|
|
1838
2067
|
return { document: info, documentTypeName: DOCUMENT_TYPES[type] ?? String(record.documentType ?? "unknown"), documentType: record.documentType };
|
|
1839
2068
|
}
|
|
1840
|
-
case "eda_open_document":
|
|
1841
|
-
|
|
1842
|
-
|
|
2069
|
+
case "eda_open_document": {
|
|
2070
|
+
const documentUuid = String(args.documentUuid ?? "");
|
|
2071
|
+
if (documentUuid === "") throw new Error("documentUuid is required");
|
|
2072
|
+
const raw = await callExtension("dmt_EditorControl.openDocument", [
|
|
2073
|
+
documentUuid,
|
|
1843
2074
|
args.splitScreenId === void 0 ? void 0 : String(args.splitScreenId)
|
|
1844
2075
|
], timeoutMs);
|
|
2076
|
+
await sleep(600);
|
|
2077
|
+
let tabIds = [];
|
|
2078
|
+
try {
|
|
2079
|
+
const tree = asRecord(await callExtension("dmt_EditorControl.getSplitScreenTree", [], Math.min(timeoutMs, 4e3)));
|
|
2080
|
+
tabIds = (Array.isArray(tree.tabs) ? tree.tabs : []).map((tab) => String(pickValue(asRecord(tab), "tabId") ?? ""));
|
|
2081
|
+
} catch {
|
|
2082
|
+
}
|
|
2083
|
+
let activeUuid = "";
|
|
2084
|
+
try {
|
|
2085
|
+
const info = asRecord(await callExtension("dmt_SelectControl.getCurrentDocumentInfo", [], Math.min(timeoutMs, 4e3)));
|
|
2086
|
+
activeUuid = String(pickValue(info, "uuid", "Uuid") ?? "");
|
|
2087
|
+
} catch {
|
|
2088
|
+
}
|
|
2089
|
+
const tabOpened = tabIds.length > 1 || activeUuid !== "" && activeUuid !== "0";
|
|
2090
|
+
return {
|
|
2091
|
+
...raw !== null && typeof raw === "object" ? raw : { result: raw ?? null },
|
|
2092
|
+
opened: tabOpened,
|
|
2093
|
+
activeDocumentUuid: activeUuid || null,
|
|
2094
|
+
tabIds,
|
|
2095
|
+
hint: tabOpened ? null : "The app accepted openDocument but created no editor tab (only the Start Page is open). This build does not open a document programmatically from the HOME screen: open the schematic/PCB page once in the app (double-click the project), then retry. Do not assume the page is active."
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
1845
2098
|
case "eda_set_no_connect": {
|
|
1846
2099
|
const ids = Array.isArray(args.primitiveIds) ? args.primitiveIds.map(String) : [];
|
|
1847
2100
|
if (ids.length === 0) throw new Error("primitiveIds is required");
|
|
@@ -1955,8 +2208,33 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
1955
2208
|
], timeoutMs);
|
|
1956
2209
|
case "eda_save_document":
|
|
1957
2210
|
return await callExtension("sch_Document.save", [], timeoutMs);
|
|
1958
|
-
case "eda_zoom_to_fit":
|
|
1959
|
-
|
|
2211
|
+
case "eda_zoom_to_fit": {
|
|
2212
|
+
const raw = await callExtension("dmt_EditorControl.zoomToAllPrimitives", [args.tabId], timeoutMs);
|
|
2213
|
+
const record = asRecord(raw);
|
|
2214
|
+
const left = Number(pickValue(record, "left", "Left"));
|
|
2215
|
+
const right = Number(pickValue(record, "right", "Right"));
|
|
2216
|
+
const bottom = Number(pickValue(record, "bottom", "Bottom"));
|
|
2217
|
+
const top = Number(pickValue(record, "top", "Top"));
|
|
2218
|
+
if (![left, right, bottom, top].every((value) => Number.isFinite(value))) return raw;
|
|
2219
|
+
const frame = paperOf(args.paper).frame;
|
|
2220
|
+
const clamped = {
|
|
2221
|
+
left: Math.max(frame.left, left),
|
|
2222
|
+
right: Math.min(frame.right, right),
|
|
2223
|
+
bottom: Math.max(frame.bottom, bottom),
|
|
2224
|
+
top: Math.min(frame.top, top)
|
|
2225
|
+
};
|
|
2226
|
+
if (clamped.left >= clamped.right || clamped.bottom >= clamped.top) {
|
|
2227
|
+
return { ...record, requested: { left, right, bottom, top }, clamped: null, note: "the primitive bounds do not overlap the page frame" };
|
|
2228
|
+
}
|
|
2229
|
+
await callExtension("dmt_EditorControl.zoomToRegion", [
|
|
2230
|
+
clamped.left,
|
|
2231
|
+
clamped.right,
|
|
2232
|
+
clamped.top,
|
|
2233
|
+
clamped.bottom,
|
|
2234
|
+
args.tabId
|
|
2235
|
+
], timeoutMs);
|
|
2236
|
+
return { ...record, requested: { left, right, bottom, top }, clamped };
|
|
2237
|
+
}
|
|
1960
2238
|
case "eda_zoom_to_region":
|
|
1961
2239
|
return await callExtension("dmt_EditorControl.zoomToRegion", [
|
|
1962
2240
|
Number(args.left),
|
|
@@ -2000,24 +2278,99 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2000
2278
|
const parsed = await parseNetlist(Array.isArray(all) ? all : project, timeoutMs);
|
|
2001
2279
|
return parsed;
|
|
2002
2280
|
}
|
|
2003
|
-
case "eda_run_drc":
|
|
2004
|
-
|
|
2281
|
+
case "eda_run_drc": {
|
|
2282
|
+
const verbose = await callExtension("sch_Drc.check", [
|
|
2283
|
+
args.strict === void 0 ? true : Boolean(args.strict),
|
|
2284
|
+
false,
|
|
2285
|
+
true
|
|
2286
|
+
], toolTimeout(args, timeoutMs));
|
|
2287
|
+
if (Array.isArray(verbose)) {
|
|
2288
|
+
return {
|
|
2289
|
+
ok: verbose.length === 0,
|
|
2290
|
+
count: verbose.length,
|
|
2291
|
+
entries: verbose.map((entry) => {
|
|
2292
|
+
const record = asRecord(entry);
|
|
2293
|
+
return {
|
|
2294
|
+
type: pickValue(record, "Type", "type", "Level", "level") ?? null,
|
|
2295
|
+
message: pickValue(record, "Message", "message", "Error", "error", "Description", "description") ?? null,
|
|
2296
|
+
primitiveId: pickValue(record, "PrimitiveId", "primitiveId") ?? null,
|
|
2297
|
+
x: pickValue(record, "X", "x") ?? null,
|
|
2298
|
+
y: pickValue(record, "Y", "y") ?? null,
|
|
2299
|
+
raw: entry
|
|
2300
|
+
};
|
|
2301
|
+
}),
|
|
2302
|
+
hint: 'EDA 3.2.186 returns AGGREGATED entries only (e.g. {type:"warn",count:1}) even with includeVerboseError, so message/primitiveId stay null \u2014 "raw" carries exactly what the app sent. Per-problem coordinates are not available on this build; use eda_pcb_run_drc for the PCB side and the DRC panel for details.'
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
return { ok: Boolean(verbose), count: null, entries: [], raw: verbose };
|
|
2306
|
+
}
|
|
2005
2307
|
case "eda_auto_layout":
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2308
|
+
case "eda_auto_route": {
|
|
2309
|
+
if (ALLOW_AUTO_LAYOUT !== true && args.force !== true) {
|
|
2310
|
+
throw new Error(
|
|
2311
|
+
name + " is disabled by policy (settings allowAutoLayoutRouting=false, env DSH_EDA_ALLOW_AUTO_LAYOUT). Do NOT retry with force:true unless the user explicitly asked for automatic layout/routing.",
|
|
2312
|
+
"",
|
|
2313
|
+
"Do this instead, in order:",
|
|
2314
|
+
"1. eda_get_active_document \u2014 make sure a schematic page is active (eda_open_document if not).",
|
|
2315
|
+
"2. eda_get_page_info \u2014 read the A4 frame and the safe areas before placing anything.",
|
|
2316
|
+
"3. eda_get_components \u2014 see what is already placed and where (x/y + bounding boxes) to avoid overlap.",
|
|
2317
|
+
"4. Place or move parts yourself on a 10-unit grid: eda_place_component for new ones, eda_move_component (keeps designator/value) to re-grid existing ones. Signal flow left to right, power rails on top, ground at the bottom, >=20 units between symbols for wires and labels.",
|
|
2318
|
+
"5. Connect with eda_draw_wire for local nets; for anything that spans the sheet use eda_place_net_port or a net label (a wire net name is overridden by connectivity once an endpoint touches a pin).",
|
|
2319
|
+
"6. Verify: eda_export_netlist (every net should have >=2 endpoints) and eda_run_drc (per-problem entries). For a visual check export the canvas to an image and look at it, rather than letting EDA rearrange the sheet.",
|
|
2320
|
+
"7. Call eda_workflow_guide for the full standard."
|
|
2321
|
+
);
|
|
2322
|
+
}
|
|
2323
|
+
const payload = Array.isArray(args.uuids) ? { uuids: args.uuids.map(String) } : void 0;
|
|
2324
|
+
return await callExtension(name === "eda_auto_layout" ? "sch_Document.autoLayout" : "sch_Document.autoRouting", [payload], toolTimeout(args, timeoutMs));
|
|
2325
|
+
}
|
|
2326
|
+
case "eda_export_netlist": {
|
|
2327
|
+
const payload = await callExtension("sch_ManufactureData.getNetlistFile", [
|
|
2328
|
+
void 0,
|
|
2329
|
+
typeof args.type === "string" && args.type !== "" ? args.type : void 0
|
|
2330
|
+
], toolTimeout(args, timeoutMs));
|
|
2331
|
+
const text = typeof payload === "string" ? payload : payload !== null && typeof payload === "object" && typeof payload.text === "string" ? payload.text : void 0;
|
|
2332
|
+
if (text === void 0) return { parsed: false, netlist: payload ?? null };
|
|
2333
|
+
if (args.format === "text") return { parsed: false, netlistText: text, bytes: text.length };
|
|
2334
|
+
try {
|
|
2335
|
+
return { parsed: true, bytes: text.length, netlist: JSON.parse(text) };
|
|
2336
|
+
} catch {
|
|
2337
|
+
return { parsed: false, bytes: text.length, netlistText: text };
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
case "eda_export_bom": {
|
|
2341
|
+
const format = args.format === "xlsx" ? "xlsx" : "csv";
|
|
2342
|
+
const payload = await callExtension("sch_ManufactureData.getBomFile", [void 0, format], toolTimeout(args, timeoutMs));
|
|
2343
|
+
if (format === "xlsx") return payload;
|
|
2344
|
+
const raw = typeof payload === "string" ? payload : payload !== null && typeof payload === "object" && typeof payload.text === "string" ? payload.text : "";
|
|
2345
|
+
const encoding = payload !== null && typeof payload === "object" ? payload.encoding : void 0;
|
|
2346
|
+
return { format, bom: decodeTextPayload(raw), encoding: encoding ?? "unknown" };
|
|
2347
|
+
}
|
|
2348
|
+
case "eda_delete_primitive": {
|
|
2349
|
+
const kind = String(args.kind);
|
|
2350
|
+
const className = kind === "netport" || kind === "netflag" || kind === "netlabel" ? "sch_PrimitiveAttribute" : primitiveKindToClass(kind);
|
|
2351
|
+
return await callExtension(className + ".delete", [String(args.primitiveId)], timeoutMs);
|
|
2352
|
+
}
|
|
2353
|
+
case "eda_move_component":
|
|
2354
|
+
return await callExtension("sch_PrimitiveComponent.modify", [String(args.primitiveId), {
|
|
2355
|
+
x: args.x === void 0 ? void 0 : Number(args.x),
|
|
2356
|
+
y: args.y === void 0 ? void 0 : Number(args.y),
|
|
2357
|
+
rotation: args.rotation === void 0 ? void 0 : Number(args.rotation),
|
|
2358
|
+
mirror: args.mirror === void 0 ? void 0 : Boolean(args.mirror)
|
|
2359
|
+
}], timeoutMs);
|
|
2360
|
+
case "eda_list_api_methods":
|
|
2361
|
+
return { count: ALLOWED_API.size, methods: [...ALLOWED_API].sort() };
|
|
2362
|
+
case "eda_workflow_guide":
|
|
2363
|
+
return { summary: WORKFLOW_SUMMARY, guide: WORKFLOW_GUIDE, autoLayoutAllowed: ALLOW_AUTO_LAYOUT };
|
|
2364
|
+
case "eda_workflow_guide":
|
|
2365
|
+
return { summary: WORKFLOW_SUMMARY, guide: WORKFLOW_GUIDE, autoLayoutAllowed: ALLOW_AUTO_LAYOUT };
|
|
2015
2366
|
case "eda_create_board":
|
|
2016
2367
|
return await callExtension("dmt_Board.createBoard", [args.schematicUuid, args.pcbUuid], timeoutMs);
|
|
2017
2368
|
case "eda_api_call": {
|
|
2018
2369
|
if (!ALLOW_RAW_API) throw new Error("eda_api_call is disabled by settings (allowRawApi=false)");
|
|
2019
2370
|
const method = String(args.method ?? "");
|
|
2020
|
-
if (!ALLOWED_API.has(method))
|
|
2371
|
+
if (!ALLOWED_API.has(method)) {
|
|
2372
|
+
throw new Error("API method not allowed: " + method + ". The extension only accepts its allow-list (" + ALLOWED_API.size + " methods): call eda_list_api_methods to get it. Common schematic helpers such as sch_PrimitiveComponent.getAllPinsByPrimitiveId and sch_PrimitivePin.get are already wrapped by eda_get_pins.");
|
|
2373
|
+
}
|
|
2021
2374
|
return await callExtension(method, Array.isArray(args.args) ? args.args : [], timeoutMs);
|
|
2022
2375
|
}
|
|
2023
2376
|
case "pcb_get_components": {
|
|
@@ -2080,6 +2433,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2080
2433
|
if (layer === void 0) throw new Error("pcb_draw_track requires a valid layer");
|
|
2081
2434
|
const net = String(args.net);
|
|
2082
2435
|
assertNetName(net, "pcb_draw_track");
|
|
2436
|
+
await assertKnownPcbNet("pcb_draw_track", net, timeoutMs, args.force === true);
|
|
2083
2437
|
const width = checkTrackWidth(net, args.lineWidth === void 0 ? void 0 : Number(args.lineWidth), args.force === true);
|
|
2084
2438
|
if (!width.ok) throw new Error("pcb_draw_track: " + width.errors.join("; "));
|
|
2085
2439
|
const created = await callExtension("pcb_PrimitiveLine.create", [
|
|
@@ -2096,6 +2450,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2096
2450
|
case "pcb_draw_via": {
|
|
2097
2451
|
const net = String(args.net);
|
|
2098
2452
|
assertNetName(net, "pcb_draw_via");
|
|
2453
|
+
await assertKnownPcbNet("pcb_draw_via", net, timeoutMs, args.force === true);
|
|
2099
2454
|
const role = classifyNet(net);
|
|
2100
2455
|
const defaultVia = role === "signal" ? VIA_SIZES_MM[0] : VIA_SIZES_MM[1];
|
|
2101
2456
|
const hole = args.holeDiameter === void 0 ? defaultVia.hole * 39.37007874015748 : Number(args.holeDiameter);
|
|
@@ -2196,17 +2551,17 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2196
2551
|
case "pcb_save_document":
|
|
2197
2552
|
return await callExtension("pcb_Document.save", [], timeoutMs);
|
|
2198
2553
|
case "pcb_import_changes": {
|
|
2199
|
-
const
|
|
2554
|
+
const uuid2 = args.schematicUuid === void 0 || args.schematicUuid === null ? null : String(args.schematicUuid);
|
|
2200
2555
|
if (args.wait === true) {
|
|
2201
|
-
return await callExtension("pcb_Document.importChanges", [
|
|
2556
|
+
return await callExtension("pcb_Document.importChanges", [uuid2 === null ? void 0 : uuid2], toolTimeout(args, timeoutMs));
|
|
2202
2557
|
}
|
|
2203
2558
|
pcbComponentBaseline = await pcbComponentIds(timeoutMs);
|
|
2204
2559
|
let started;
|
|
2205
2560
|
try {
|
|
2206
|
-
started = asRecord(await callExtension("dsh.internal.jobStart", ["importChanges",
|
|
2561
|
+
started = asRecord(await callExtension("dsh.internal.jobStart", ["importChanges", uuid2], timeoutMs));
|
|
2207
2562
|
} catch (error) {
|
|
2208
2563
|
if (!isInternalHelperMissing(error)) throw error;
|
|
2209
|
-
const result2 = await callExtension("pcb_Document.importChanges", [
|
|
2564
|
+
const result2 = await callExtension("pcb_Document.importChanges", [uuid2 === null ? void 0 : uuid2], toolTimeout(args, timeoutMs));
|
|
2210
2565
|
const after = await pcbComponentIds(timeoutMs);
|
|
2211
2566
|
return {
|
|
2212
2567
|
triggered: true,
|