@intelligentelectron/pcb-lens 0.0.3 → 0.0.5
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 +25 -0
- package/dist/cli/commands.js +1 -1
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/updater.d.ts +0 -1
- package/dist/cli/updater.d.ts.map +1 -1
- package/dist/cli/updater.js +1 -2
- package/dist/cli/updater.js.map +1 -1
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/{version.js → cli/version.js} +1 -1
- package/dist/cli/version.js.map +1 -0
- package/dist/index.js +4 -7
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +19 -133
- package/dist/server.js.map +1 -1
- package/dist/tools/export-cadence-board.d.ts +8 -0
- package/dist/tools/export-cadence-board.d.ts.map +1 -0
- package/dist/tools/export-cadence-board.js +108 -0
- package/dist/tools/export-cadence-board.js.map +1 -0
- package/dist/tools/export-cadence-constraints.d.ts +7 -0
- package/dist/tools/export-cadence-constraints.d.ts.map +1 -0
- package/dist/tools/export-cadence-constraints.js +93 -0
- package/dist/tools/export-cadence-constraints.js.map +1 -0
- package/dist/tools/get-design-overview.d.ts +5 -0
- package/dist/tools/get-design-overview.d.ts.map +1 -0
- package/dist/tools/get-design-overview.js +103 -0
- package/dist/tools/get-design-overview.js.map +1 -0
- package/dist/tools/lib/async-mutex.d.ts.map +1 -0
- package/dist/tools/lib/async-mutex.js.map +1 -0
- package/dist/tools/lib/cadence.d.ts +24 -0
- package/dist/tools/lib/cadence.d.ts.map +1 -0
- package/dist/tools/lib/cadence.js +58 -0
- package/dist/tools/lib/cadence.js.map +1 -0
- package/dist/{types.d.ts → tools/lib/types.d.ts} +64 -0
- package/dist/tools/lib/types.d.ts.map +1 -0
- package/dist/tools/lib/types.js.map +1 -0
- package/dist/tools/lib/wasm-embed.d.ts.map +1 -0
- package/dist/tools/lib/wasm-embed.js.map +1 -0
- package/dist/tools/lib/xml-utils.d.ts.map +1 -0
- package/dist/tools/lib/xml-utils.js.map +1 -0
- package/dist/tools/query-components.d.ts +5 -0
- package/dist/tools/query-components.d.ts.map +1 -0
- package/dist/tools/query-components.js +127 -0
- package/dist/tools/query-components.js.map +1 -0
- package/dist/tools/query-constraints.d.ts +5 -0
- package/dist/tools/query-constraints.d.ts.map +1 -0
- package/dist/tools/query-constraints.js +309 -0
- package/dist/tools/query-constraints.js.map +1 -0
- package/dist/tools/query-net.d.ts +5 -0
- package/dist/tools/query-net.d.ts.map +1 -0
- package/dist/tools/query-net.js +148 -0
- package/dist/tools/query-net.js.map +1 -0
- package/dist/tools/render-net.d.ts +5 -0
- package/dist/tools/render-net.d.ts.map +1 -0
- package/dist/tools/render-net.js +683 -0
- package/dist/tools/render-net.js.map +1 -0
- package/dist/tools/shared.d.ts +20 -0
- package/dist/tools/shared.d.ts.map +1 -0
- package/dist/tools/shared.js +102 -0
- package/dist/tools/shared.js.map +1 -0
- package/package.json +1 -1
- package/dist/async-mutex.d.ts.map +0 -1
- package/dist/async-mutex.js.map +0 -1
- package/dist/service.d.ts +0 -25
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -1270
- package/dist/service.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/dist/wasm-embed.d.ts.map +0 -1
- package/dist/wasm-embed.js.map +0 -1
- package/dist/xml-utils.d.ts.map +0 -1
- package/dist/xml-utils.js.map +0 -1
- /package/dist/{version.d.ts → cli/version.d.ts} +0 -0
- /package/dist/{async-mutex.d.ts → tools/lib/async-mutex.d.ts} +0 -0
- /package/dist/{async-mutex.js → tools/lib/async-mutex.js} +0 -0
- /package/dist/{types.js → tools/lib/types.js} +0 -0
- /package/dist/{wasm-embed.d.ts → tools/lib/wasm-embed.d.ts} +0 -0
- /package/dist/{wasm-embed.js → tools/lib/wasm-embed.js} +0 -0
- /package/dist/{xml-utils.d.ts → tools/lib/xml-utils.d.ts} +0 -0
- /package/dist/{xml-utils.js → tools/lib/xml-utils.js} +0 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { Resvg, initWasm } from "@resvg/resvg-wasm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { isErrorResult } from "./lib/types.js";
|
|
6
|
+
import { attr, numAttr, loadAllLines, scanLines } from "./lib/xml-utils.js";
|
|
7
|
+
import { extractMicronFactorFromLines, formatResult, validateFile, validatePattern, } from "./shared.js";
|
|
8
|
+
let wasmInitialized = false;
|
|
9
|
+
const resolveWasmBuffer = async () => {
|
|
10
|
+
if (typeof BUILD_VERSION !== "undefined") {
|
|
11
|
+
const { default: wasmPath } = await import("./lib/wasm-embed.js");
|
|
12
|
+
return readFile(wasmPath);
|
|
13
|
+
}
|
|
14
|
+
const wasmUrl = import.meta.resolve("@resvg/resvg-wasm/index_bg.wasm");
|
|
15
|
+
return readFile(fileURLToPath(wasmUrl));
|
|
16
|
+
};
|
|
17
|
+
const ensureWasmInitialized = async () => {
|
|
18
|
+
if (wasmInitialized)
|
|
19
|
+
return;
|
|
20
|
+
await initWasm(await resolveWasmBuffer());
|
|
21
|
+
wasmInitialized = true;
|
|
22
|
+
};
|
|
23
|
+
const formatRenderResult = async (result) => {
|
|
24
|
+
await ensureWasmInitialized();
|
|
25
|
+
const resvg = new Resvg(result.svg, { fitTo: { mode: "width", value: 1200 } });
|
|
26
|
+
const png = resvg.render().asPng();
|
|
27
|
+
return {
|
|
28
|
+
content: [
|
|
29
|
+
{
|
|
30
|
+
type: "image",
|
|
31
|
+
data: Buffer.from(png).toString("base64"),
|
|
32
|
+
mimeType: "image/png",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: "text",
|
|
36
|
+
text: JSON.stringify({ netName: result.netName, units: result.units, stats: result.stats }, null, 2),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Layer Colors
|
|
43
|
+
// =============================================================================
|
|
44
|
+
const FIXED_LAYER_COLORS = {
|
|
45
|
+
TOP: "#e74c3c",
|
|
46
|
+
BOTTOM: "#3498db",
|
|
47
|
+
};
|
|
48
|
+
const INNER_LAYER_PALETTE = [
|
|
49
|
+
"#2ecc71", // green
|
|
50
|
+
"#9b59b6", // purple
|
|
51
|
+
"#f39c12", // orange
|
|
52
|
+
"#1abc9c", // teal
|
|
53
|
+
"#e67e22", // dark orange
|
|
54
|
+
"#3498db", // (fallback blue variant)
|
|
55
|
+
"#e84393", // pink
|
|
56
|
+
"#00cec9", // cyan
|
|
57
|
+
];
|
|
58
|
+
const buildLayerColors = (layers) => {
|
|
59
|
+
const colorMap = new Map();
|
|
60
|
+
let paletteIdx = 0;
|
|
61
|
+
for (const layer of layers) {
|
|
62
|
+
const upper = layer.toUpperCase();
|
|
63
|
+
if (upper === "TOP" || upper === "BOTTOM") {
|
|
64
|
+
colorMap.set(layer, FIXED_LAYER_COLORS[upper]);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
colorMap.set(layer, INNER_LAYER_PALETTE[paletteIdx % INNER_LAYER_PALETTE.length]);
|
|
68
|
+
paletteIdx++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return colorMap;
|
|
72
|
+
};
|
|
73
|
+
// =============================================================================
|
|
74
|
+
// Extraction passes (in-memory line scanning)
|
|
75
|
+
// =============================================================================
|
|
76
|
+
const extractShapes = (lines, f) => {
|
|
77
|
+
const shapes = new Map();
|
|
78
|
+
let currentId = "";
|
|
79
|
+
scanLines(lines, (line) => {
|
|
80
|
+
if (line.includes("<EntryStandard ")) {
|
|
81
|
+
currentId = attr(line, "id") ?? "";
|
|
82
|
+
}
|
|
83
|
+
if (currentId && line.includes("<RectCenter ")) {
|
|
84
|
+
const w = numAttr(line, "width");
|
|
85
|
+
const h = numAttr(line, "height");
|
|
86
|
+
if (w !== undefined && h !== undefined) {
|
|
87
|
+
shapes.set(currentId, { type: "rect", width: w * f, height: h * f });
|
|
88
|
+
}
|
|
89
|
+
currentId = "";
|
|
90
|
+
}
|
|
91
|
+
if (currentId && line.includes("<Circle ")) {
|
|
92
|
+
const d = numAttr(line, "diameter");
|
|
93
|
+
if (d !== undefined) {
|
|
94
|
+
shapes.set(currentId, { type: "circle", width: d * f, height: d * f });
|
|
95
|
+
}
|
|
96
|
+
currentId = "";
|
|
97
|
+
}
|
|
98
|
+
if (line.includes("</Content>"))
|
|
99
|
+
return false;
|
|
100
|
+
});
|
|
101
|
+
return shapes;
|
|
102
|
+
};
|
|
103
|
+
const extractPackages = (lines) => {
|
|
104
|
+
const packages = new Map();
|
|
105
|
+
let currentPkg = "";
|
|
106
|
+
let inPad = false;
|
|
107
|
+
let inPin = false;
|
|
108
|
+
let padPin = "";
|
|
109
|
+
let padOffset = { x: 0, y: 0 };
|
|
110
|
+
let padShapeId = "";
|
|
111
|
+
const commitPad = () => {
|
|
112
|
+
if (currentPkg && padPin && padShapeId) {
|
|
113
|
+
packages.get(currentPkg).set(padPin, {
|
|
114
|
+
offsetX: padOffset.x,
|
|
115
|
+
offsetY: padOffset.y,
|
|
116
|
+
shapeId: padShapeId,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
padPin = "";
|
|
120
|
+
padOffset = { x: 0, y: 0 };
|
|
121
|
+
padShapeId = "";
|
|
122
|
+
};
|
|
123
|
+
scanLines(lines, (line) => {
|
|
124
|
+
if (line.includes("<Package ")) {
|
|
125
|
+
currentPkg = attr(line, "name") ?? "";
|
|
126
|
+
if (currentPkg && !packages.has(currentPkg)) {
|
|
127
|
+
packages.set(currentPkg, new Map());
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (line.includes("</Package>")) {
|
|
131
|
+
currentPkg = "";
|
|
132
|
+
}
|
|
133
|
+
if (!currentPkg)
|
|
134
|
+
return;
|
|
135
|
+
if (line.includes("<Pad") && (line.includes("<Pad>") || line.includes("<Pad "))) {
|
|
136
|
+
inPad = true;
|
|
137
|
+
padPin = "";
|
|
138
|
+
padOffset = { x: 0, y: 0 };
|
|
139
|
+
padShapeId = "";
|
|
140
|
+
}
|
|
141
|
+
if (inPad) {
|
|
142
|
+
if (line.includes("<PinRef ")) {
|
|
143
|
+
padPin = attr(line, "pin") ?? "";
|
|
144
|
+
}
|
|
145
|
+
if (line.includes("<Location ")) {
|
|
146
|
+
const x = numAttr(line, "x");
|
|
147
|
+
const y = numAttr(line, "y");
|
|
148
|
+
if (x !== undefined)
|
|
149
|
+
padOffset.x = x;
|
|
150
|
+
if (y !== undefined)
|
|
151
|
+
padOffset.y = y;
|
|
152
|
+
}
|
|
153
|
+
if (line.includes("<StandardPrimitiveRef ")) {
|
|
154
|
+
padShapeId = attr(line, "id") ?? "";
|
|
155
|
+
}
|
|
156
|
+
if (line.includes("</Pad>")) {
|
|
157
|
+
commitPad();
|
|
158
|
+
inPad = false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!inPad && line.includes("<Pin ") && line.includes("number=")) {
|
|
162
|
+
inPin = true;
|
|
163
|
+
padPin = attr(line, "number") ?? "";
|
|
164
|
+
padOffset = { x: 0, y: 0 };
|
|
165
|
+
padShapeId = "";
|
|
166
|
+
}
|
|
167
|
+
if (inPin) {
|
|
168
|
+
if (line.includes("<Location ")) {
|
|
169
|
+
const x = numAttr(line, "x");
|
|
170
|
+
const y = numAttr(line, "y");
|
|
171
|
+
if (x !== undefined)
|
|
172
|
+
padOffset.x = x;
|
|
173
|
+
if (y !== undefined)
|
|
174
|
+
padOffset.y = y;
|
|
175
|
+
}
|
|
176
|
+
if (line.includes("<StandardPrimitiveRef ")) {
|
|
177
|
+
padShapeId = attr(line, "id") ?? "";
|
|
178
|
+
}
|
|
179
|
+
if (line.includes("</Pin>")) {
|
|
180
|
+
commitPad();
|
|
181
|
+
inPin = false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (line.includes("<Component ") && line.includes("refDes="))
|
|
185
|
+
return false;
|
|
186
|
+
});
|
|
187
|
+
return packages;
|
|
188
|
+
};
|
|
189
|
+
const extractViaPadSizes = (lines) => {
|
|
190
|
+
const viaPads = new Map();
|
|
191
|
+
let currentName = "";
|
|
192
|
+
let currentDrill = 0;
|
|
193
|
+
let foundRegular = false;
|
|
194
|
+
scanLines(lines, (line) => {
|
|
195
|
+
if (line.includes("<PadStackDef ")) {
|
|
196
|
+
currentName = attr(line, "name") ?? "";
|
|
197
|
+
currentDrill = 0;
|
|
198
|
+
foundRegular = false;
|
|
199
|
+
}
|
|
200
|
+
if (currentName && line.includes("<PadstackHoleDef ")) {
|
|
201
|
+
currentDrill = numAttr(line, "diameter") ?? 0;
|
|
202
|
+
}
|
|
203
|
+
if (currentName && !foundRegular && line.includes('padUse="REGULAR"')) {
|
|
204
|
+
foundRegular = true;
|
|
205
|
+
}
|
|
206
|
+
if (currentName && foundRegular && line.includes("<StandardPrimitiveRef ")) {
|
|
207
|
+
const id = attr(line, "id") ?? "";
|
|
208
|
+
if (id && currentDrill > 0) {
|
|
209
|
+
viaPads.set(currentName, { padShapeId: id, drillDiameter: currentDrill });
|
|
210
|
+
}
|
|
211
|
+
foundRegular = false;
|
|
212
|
+
}
|
|
213
|
+
if (line.includes("</PadStackDef>")) {
|
|
214
|
+
currentName = "";
|
|
215
|
+
}
|
|
216
|
+
if (line.includes("<Package "))
|
|
217
|
+
return false;
|
|
218
|
+
});
|
|
219
|
+
return viaPads;
|
|
220
|
+
};
|
|
221
|
+
const extractComponents = (lines, f) => {
|
|
222
|
+
const components = [];
|
|
223
|
+
let current = null;
|
|
224
|
+
scanLines(lines, (line) => {
|
|
225
|
+
if (line.includes("<Component ") && line.includes("refDes=")) {
|
|
226
|
+
const refdes = attr(line, "refDes");
|
|
227
|
+
if (refdes) {
|
|
228
|
+
current = {
|
|
229
|
+
refdes,
|
|
230
|
+
packageRef: attr(line, "packageRef") ?? "",
|
|
231
|
+
x: 0,
|
|
232
|
+
y: 0,
|
|
233
|
+
rotation: 0,
|
|
234
|
+
mirror: false,
|
|
235
|
+
layer: attr(line, "layerRef") ?? "",
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (current) {
|
|
240
|
+
if (line.includes("<Location ")) {
|
|
241
|
+
const x = numAttr(line, "x");
|
|
242
|
+
const y = numAttr(line, "y");
|
|
243
|
+
if (x !== undefined)
|
|
244
|
+
current.x = x * f;
|
|
245
|
+
if (y !== undefined)
|
|
246
|
+
current.y = y * f;
|
|
247
|
+
}
|
|
248
|
+
if (line.includes("<Xform ")) {
|
|
249
|
+
current.rotation = numAttr(line, "rotation") ?? 0;
|
|
250
|
+
current.mirror = attr(line, "mirror") === "true";
|
|
251
|
+
}
|
|
252
|
+
if (line.includes("</Component>")) {
|
|
253
|
+
components.push(current);
|
|
254
|
+
current = null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (line.includes("<PhyNetGroup ") || line.includes("</Step>"))
|
|
258
|
+
return false;
|
|
259
|
+
});
|
|
260
|
+
return components;
|
|
261
|
+
};
|
|
262
|
+
const extractProfile = (lines, f) => {
|
|
263
|
+
const points = [];
|
|
264
|
+
const arcs = new Map();
|
|
265
|
+
let inProfile = false;
|
|
266
|
+
scanLines(lines, (line) => {
|
|
267
|
+
if (line.includes("<Profile>")) {
|
|
268
|
+
inProfile = true;
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (line.includes("</Profile>"))
|
|
272
|
+
return false;
|
|
273
|
+
if (!inProfile)
|
|
274
|
+
return;
|
|
275
|
+
if (line.includes("<PolyBegin ")) {
|
|
276
|
+
const x = numAttr(line, "x");
|
|
277
|
+
const y = numAttr(line, "y");
|
|
278
|
+
if (x !== undefined && y !== undefined)
|
|
279
|
+
points.push({ x: x * f, y: y * f });
|
|
280
|
+
}
|
|
281
|
+
if (line.includes("<PolyStepSegment ")) {
|
|
282
|
+
const x = numAttr(line, "x");
|
|
283
|
+
const y = numAttr(line, "y");
|
|
284
|
+
if (x !== undefined && y !== undefined)
|
|
285
|
+
points.push({ x: x * f, y: y * f });
|
|
286
|
+
}
|
|
287
|
+
if (line.includes("<PolyStepCurve ")) {
|
|
288
|
+
const x = numAttr(line, "x");
|
|
289
|
+
const y = numAttr(line, "y");
|
|
290
|
+
const cx = numAttr(line, "centerX");
|
|
291
|
+
const cy = numAttr(line, "centerY");
|
|
292
|
+
const cw = attr(line, "clockwise") === "true";
|
|
293
|
+
if (x !== undefined && y !== undefined && cx !== undefined && cy !== undefined) {
|
|
294
|
+
points.push({ x: x * f, y: y * f });
|
|
295
|
+
arcs.set(points.length - 1, {
|
|
296
|
+
x: x * f,
|
|
297
|
+
y: y * f,
|
|
298
|
+
centerX: cx * f,
|
|
299
|
+
centerY: cy * f,
|
|
300
|
+
clockwise: cw,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
return { points, arcs };
|
|
306
|
+
};
|
|
307
|
+
const extractNetGeometry = (lines, netName, f) => {
|
|
308
|
+
const traces = [];
|
|
309
|
+
const vias = [];
|
|
310
|
+
let currentLayer = "";
|
|
311
|
+
let insideMatchedSet = false;
|
|
312
|
+
let currentPoints = [];
|
|
313
|
+
let currentWidth = 0;
|
|
314
|
+
let inPolyline = false;
|
|
315
|
+
let currentGeometry = "";
|
|
316
|
+
const skipLayers = new Set(["REF-route", "REF-both"]);
|
|
317
|
+
scanLines(lines, (line) => {
|
|
318
|
+
if (line.includes("<LayerFeature ")) {
|
|
319
|
+
currentLayer = attr(line, "layerRef") ?? "";
|
|
320
|
+
}
|
|
321
|
+
if (line.includes("<Set ")) {
|
|
322
|
+
const net = attr(line, "net");
|
|
323
|
+
insideMatchedSet = net === netName && !skipLayers.has(currentLayer);
|
|
324
|
+
currentPoints = [];
|
|
325
|
+
currentWidth = 0;
|
|
326
|
+
inPolyline = false;
|
|
327
|
+
currentGeometry = attr(line, "geometry") ?? "";
|
|
328
|
+
}
|
|
329
|
+
if (!insideMatchedSet)
|
|
330
|
+
return;
|
|
331
|
+
if (line.includes("<Polyline")) {
|
|
332
|
+
inPolyline = true;
|
|
333
|
+
currentPoints = [];
|
|
334
|
+
currentWidth = 0;
|
|
335
|
+
}
|
|
336
|
+
if (inPolyline) {
|
|
337
|
+
if (line.includes("<PolyBegin ")) {
|
|
338
|
+
const x = numAttr(line, "x");
|
|
339
|
+
const y = numAttr(line, "y");
|
|
340
|
+
if (x !== undefined && y !== undefined)
|
|
341
|
+
currentPoints.push({ x: x * f, y: y * f });
|
|
342
|
+
}
|
|
343
|
+
if (line.includes("<PolyStepSegment ")) {
|
|
344
|
+
const x = numAttr(line, "x");
|
|
345
|
+
const y = numAttr(line, "y");
|
|
346
|
+
if (x !== undefined && y !== undefined)
|
|
347
|
+
currentPoints.push({ x: x * f, y: y * f });
|
|
348
|
+
}
|
|
349
|
+
if (line.includes("<LineDesc ")) {
|
|
350
|
+
const w = numAttr(line, "lineWidth");
|
|
351
|
+
if (w !== undefined)
|
|
352
|
+
currentWidth = w * f;
|
|
353
|
+
}
|
|
354
|
+
if (line.includes("</Polyline>")) {
|
|
355
|
+
if (currentPoints.length >= 2) {
|
|
356
|
+
traces.push({ layer: currentLayer, points: [...currentPoints], width: currentWidth });
|
|
357
|
+
}
|
|
358
|
+
inPolyline = false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (line.includes("<Hole ")) {
|
|
362
|
+
const status = attr(line, "platingStatus");
|
|
363
|
+
if (status === "VIA") {
|
|
364
|
+
const x = numAttr(line, "x");
|
|
365
|
+
const y = numAttr(line, "y");
|
|
366
|
+
const d = (numAttr(line, "diameter") ?? 0.008) * f;
|
|
367
|
+
if (x !== undefined && y !== undefined) {
|
|
368
|
+
vias.push({ x: x * f, y: y * f, drillDiameter: d, padstackRef: currentGeometry });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (line.includes("</Set>"))
|
|
373
|
+
insideMatchedSet = false;
|
|
374
|
+
});
|
|
375
|
+
return { traces, vias };
|
|
376
|
+
};
|
|
377
|
+
const extractNetPins = (lines, netName) => {
|
|
378
|
+
const pins = [];
|
|
379
|
+
const seen = new Set();
|
|
380
|
+
let insideMatchedSet = false;
|
|
381
|
+
scanLines(lines, (line) => {
|
|
382
|
+
if (line.includes("<Set ")) {
|
|
383
|
+
insideMatchedSet = attr(line, "net") === netName;
|
|
384
|
+
}
|
|
385
|
+
if (insideMatchedSet && line.includes("<PinRef ")) {
|
|
386
|
+
const compRef = attr(line, "componentRef");
|
|
387
|
+
const pin = attr(line, "pin");
|
|
388
|
+
if (compRef && pin) {
|
|
389
|
+
const key = `${compRef}.${pin}`;
|
|
390
|
+
if (!seen.has(key)) {
|
|
391
|
+
seen.add(key);
|
|
392
|
+
pins.push({ refdes: compRef, pin });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (line.includes("</Set>"))
|
|
397
|
+
insideMatchedSet = false;
|
|
398
|
+
});
|
|
399
|
+
return pins;
|
|
400
|
+
};
|
|
401
|
+
// =============================================================================
|
|
402
|
+
// Geometry helpers
|
|
403
|
+
// =============================================================================
|
|
404
|
+
const transformPin = (comp, pinDef, f) => {
|
|
405
|
+
const rad = (comp.rotation * Math.PI) / 180;
|
|
406
|
+
const cos = Math.cos(rad);
|
|
407
|
+
const sin = Math.sin(rad);
|
|
408
|
+
let dx = pinDef.offsetX * f;
|
|
409
|
+
const dy = pinDef.offsetY * f;
|
|
410
|
+
if (comp.mirror)
|
|
411
|
+
dx = -dx;
|
|
412
|
+
return {
|
|
413
|
+
x: comp.x + dx * cos - dy * sin,
|
|
414
|
+
y: comp.y + dx * sin + dy * cos,
|
|
415
|
+
};
|
|
416
|
+
};
|
|
417
|
+
const svgArc = (prev, arc) => {
|
|
418
|
+
const r = Math.sqrt((arc.centerX - prev.x) ** 2 + (arc.centerY - prev.y) ** 2);
|
|
419
|
+
const sweepFlag = arc.clockwise ? 0 : 1;
|
|
420
|
+
return `A${r},${r} 0 0 ${sweepFlag} ${arc.x},${arc.y}`;
|
|
421
|
+
};
|
|
422
|
+
const generateSvg = (data, netName) => {
|
|
423
|
+
const { profile, shapes, packages, viaPadSizes, components, net, netPins, factor } = data;
|
|
424
|
+
const allLayers = [
|
|
425
|
+
...new Set([
|
|
426
|
+
...net.traces.map((t) => t.layer),
|
|
427
|
+
...netPins
|
|
428
|
+
.map((np) => {
|
|
429
|
+
const comp = components.find((c) => c.refdes === np.refdes);
|
|
430
|
+
return comp?.layer ?? "";
|
|
431
|
+
})
|
|
432
|
+
.filter(Boolean),
|
|
433
|
+
]),
|
|
434
|
+
];
|
|
435
|
+
const layerColors = buildLayerColors(allLayers);
|
|
436
|
+
const allPx = profile.points.map((p) => p.x);
|
|
437
|
+
const allPy = profile.points.map((p) => p.y);
|
|
438
|
+
const boardMinX = Math.min(...allPx);
|
|
439
|
+
const boardMaxX = Math.max(...allPx);
|
|
440
|
+
const boardMinY = Math.min(...allPy);
|
|
441
|
+
const boardMaxY = Math.max(...allPy);
|
|
442
|
+
const boardW = boardMaxX - boardMinX;
|
|
443
|
+
const boardH = boardMaxY - boardMinY;
|
|
444
|
+
const margin = Math.max(boardW, boardH) * 0.08;
|
|
445
|
+
const vbX = boardMinX - margin;
|
|
446
|
+
const vbY = boardMinY - margin;
|
|
447
|
+
const vbW = boardW + 2 * margin;
|
|
448
|
+
const vbH = boardH + 2 * margin;
|
|
449
|
+
const svgWidth = 1200;
|
|
450
|
+
const svgHeight = Math.round(svgWidth * (vbH / vbW));
|
|
451
|
+
const fontSize = boardW * 0.008;
|
|
452
|
+
const pinFontSize = boardW * 0.005;
|
|
453
|
+
const L = [];
|
|
454
|
+
L.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${svgWidth}" height="${svgHeight}" viewBox="${vbX} ${vbY} ${vbW} ${vbH}">`);
|
|
455
|
+
L.push(` <title>Net: ${netName}</title>`);
|
|
456
|
+
L.push(` <rect x="${vbX}" y="${vbY}" width="${vbW}" height="${vbH}" fill="#1a1a2e"/>`);
|
|
457
|
+
L.push(` <g transform="scale(1,-1) translate(0,${-(boardMinY + boardMaxY)})">`);
|
|
458
|
+
// Board outline
|
|
459
|
+
if (profile.points.length > 0) {
|
|
460
|
+
let d = `M${profile.points[0].x},${profile.points[0].y}`;
|
|
461
|
+
for (let i = 1; i < profile.points.length; i++) {
|
|
462
|
+
const arc = profile.arcs.get(i);
|
|
463
|
+
d += arc
|
|
464
|
+
? ` ${svgArc(profile.points[i - 1], arc)}`
|
|
465
|
+
: ` L${profile.points[i].x},${profile.points[i].y}`;
|
|
466
|
+
}
|
|
467
|
+
d += " Z";
|
|
468
|
+
L.push(` <path d="${d}" fill="#16213e" stroke="#e0e0e0" stroke-width="${boardW * 0.003}"/>`);
|
|
469
|
+
}
|
|
470
|
+
// Component dots
|
|
471
|
+
const compMap = new Map();
|
|
472
|
+
for (const c of components)
|
|
473
|
+
compMap.set(c.refdes, c);
|
|
474
|
+
const netRefdes = new Set(netPins.map((p) => p.refdes));
|
|
475
|
+
L.push(` <!-- Components -->`);
|
|
476
|
+
L.push(` <g>`);
|
|
477
|
+
for (const c of components) {
|
|
478
|
+
const onNet = netRefdes.has(c.refdes);
|
|
479
|
+
L.push(` <circle cx="${c.x}" cy="${c.y}" r="${boardW * 0.0012}" fill="${onNet ? "#ffffff" : "#334155"}" opacity="${onNet ? 0.5 : 0.2}"/>`);
|
|
480
|
+
}
|
|
481
|
+
L.push(` </g>`);
|
|
482
|
+
// Refdes labels for net components
|
|
483
|
+
L.push(` <!-- Refdes labels -->`);
|
|
484
|
+
L.push(` <g font-family="monospace" font-size="${fontSize}" fill="#ffffff">`);
|
|
485
|
+
const labeled = new Set();
|
|
486
|
+
for (const p of netPins) {
|
|
487
|
+
if (labeled.has(p.refdes))
|
|
488
|
+
continue;
|
|
489
|
+
labeled.add(p.refdes);
|
|
490
|
+
const comp = compMap.get(p.refdes);
|
|
491
|
+
if (!comp)
|
|
492
|
+
continue;
|
|
493
|
+
L.push(` <text x="${comp.x}" y="${comp.y}" transform="scale(1,-1) translate(0,${-2 * comp.y})" dy="${-fontSize}">${comp.refdes}</text>`);
|
|
494
|
+
}
|
|
495
|
+
L.push(` </g>`);
|
|
496
|
+
// Traces by layer
|
|
497
|
+
const byLayer = new Map();
|
|
498
|
+
for (const t of net.traces) {
|
|
499
|
+
const arr = byLayer.get(t.layer) ?? [];
|
|
500
|
+
arr.push(t);
|
|
501
|
+
byLayer.set(t.layer, arr);
|
|
502
|
+
}
|
|
503
|
+
for (const [layer, layerTraces] of byLayer) {
|
|
504
|
+
const color = layerColors.get(layer) ?? "#7f8c8d";
|
|
505
|
+
L.push(` <!-- ${netName} — ${layer} -->`);
|
|
506
|
+
L.push(` <g stroke="${color}" fill="none" stroke-linecap="round" stroke-linejoin="round" opacity="0.9">`);
|
|
507
|
+
for (const t of layerTraces) {
|
|
508
|
+
const sw = t.width > 0 ? t.width : boardW * 0.003;
|
|
509
|
+
const d = t.points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x},${p.y}`).join(" ");
|
|
510
|
+
L.push(` <path d="${d}" stroke-width="${sw}"/>`);
|
|
511
|
+
}
|
|
512
|
+
L.push(` </g>`);
|
|
513
|
+
}
|
|
514
|
+
// SMD pads at pin locations
|
|
515
|
+
L.push(` <!-- SMD Pads -->`);
|
|
516
|
+
L.push(` <g>`);
|
|
517
|
+
for (const np of netPins) {
|
|
518
|
+
const comp = compMap.get(np.refdes);
|
|
519
|
+
if (!comp)
|
|
520
|
+
continue;
|
|
521
|
+
const pkg = packages.get(comp.packageRef);
|
|
522
|
+
if (!pkg)
|
|
523
|
+
continue;
|
|
524
|
+
const pinDef = pkg.get(np.pin);
|
|
525
|
+
if (!pinDef)
|
|
526
|
+
continue;
|
|
527
|
+
const pos = transformPin(comp, pinDef, factor);
|
|
528
|
+
const shape = shapes.get(pinDef.shapeId);
|
|
529
|
+
if (!shape)
|
|
530
|
+
continue;
|
|
531
|
+
const layer = comp.layer || "TOP";
|
|
532
|
+
const color = layerColors.get(layer) ?? "#7f8c8d";
|
|
533
|
+
if (shape.type === "rect") {
|
|
534
|
+
const hw = shape.width / 2;
|
|
535
|
+
const hh = shape.height / 2;
|
|
536
|
+
if (Math.abs(comp.rotation % 180) < 0.1) {
|
|
537
|
+
L.push(` <rect x="${pos.x - hw}" y="${pos.y - hh}" width="${shape.width}" height="${shape.height}" fill="${color}" opacity="0.8"/>`);
|
|
538
|
+
}
|
|
539
|
+
else if (Math.abs((comp.rotation - 90) % 180) < 0.1) {
|
|
540
|
+
L.push(` <rect x="${pos.x - hh}" y="${pos.y - hw}" width="${shape.height}" height="${shape.width}" fill="${color}" opacity="0.8"/>`);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
const rad = (comp.rotation * Math.PI) / 180;
|
|
544
|
+
L.push(` <rect x="${-hw}" y="${-hh}" width="${shape.width}" height="${shape.height}" fill="${color}" opacity="0.8" transform="translate(${pos.x},${pos.y}) rotate(${rad * (180 / Math.PI)})"/>`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
const r = shape.width / 2;
|
|
549
|
+
L.push(` <circle cx="${pos.x}" cy="${pos.y}" r="${r}" fill="${color}" opacity="0.8"/>`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
L.push(` </g>`);
|
|
553
|
+
// Vias
|
|
554
|
+
if (net.vias.length > 0) {
|
|
555
|
+
L.push(` <!-- Vias -->`);
|
|
556
|
+
L.push(` <g>`);
|
|
557
|
+
for (const v of net.vias) {
|
|
558
|
+
const viaDef = viaPadSizes.get(v.padstackRef);
|
|
559
|
+
let padDiameter;
|
|
560
|
+
if (viaDef) {
|
|
561
|
+
const padShape = shapes.get(viaDef.padShapeId);
|
|
562
|
+
padDiameter = padShape ? padShape.width : v.drillDiameter * 2.25;
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
padDiameter = v.drillDiameter * 2.25;
|
|
566
|
+
}
|
|
567
|
+
const padR = padDiameter / 2;
|
|
568
|
+
const drillR = v.drillDiameter / 2;
|
|
569
|
+
L.push(` <circle cx="${v.x}" cy="${v.y}" r="${padR}" fill="#c8a415" stroke="#a08410" stroke-width="${boardW * 0.0008}"/>`);
|
|
570
|
+
L.push(` <circle cx="${v.x}" cy="${v.y}" r="${drillR}" fill="#1a1a2e"/>`);
|
|
571
|
+
}
|
|
572
|
+
L.push(` </g>`);
|
|
573
|
+
}
|
|
574
|
+
// Pin labels
|
|
575
|
+
L.push(` <!-- Pin labels -->`);
|
|
576
|
+
L.push(` <g font-family="monospace" font-size="${pinFontSize}" fill="#f1c40f" text-anchor="middle">`);
|
|
577
|
+
for (const np of netPins) {
|
|
578
|
+
const comp = compMap.get(np.refdes);
|
|
579
|
+
if (!comp)
|
|
580
|
+
continue;
|
|
581
|
+
const pkg = packages.get(comp.packageRef);
|
|
582
|
+
const pinDef = pkg?.get(np.pin);
|
|
583
|
+
if (pinDef) {
|
|
584
|
+
const pos = transformPin(comp, pinDef, factor);
|
|
585
|
+
L.push(` <text x="${pos.x}" y="${pos.y}" transform="scale(1,-1) translate(0,${-2 * pos.y})" dy="${pinFontSize * 1.8}">${np.refdes}.${np.pin}</text>`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
L.push(` </g>`);
|
|
589
|
+
// Legend
|
|
590
|
+
const legendX = vbX + margin * 0.3;
|
|
591
|
+
const legendY = boardMaxY + margin * 0.5;
|
|
592
|
+
const legendFS = boardW * 0.01;
|
|
593
|
+
L.push(` <!-- Legend -->`);
|
|
594
|
+
L.push(` <g font-family="monospace" font-size="${legendFS}" transform="scale(1,-1) translate(0,${-2 * legendY})">`);
|
|
595
|
+
let ly = legendY;
|
|
596
|
+
for (const [layer, color] of layerColors) {
|
|
597
|
+
L.push(` <rect x="${legendX}" y="${ly}" width="${legendFS}" height="${legendFS}" fill="${color}"/>`);
|
|
598
|
+
L.push(` <text x="${legendX + legendFS * 1.5}" y="${ly + legendFS * 0.85}" fill="#e0e0e0">${layer}</text>`);
|
|
599
|
+
ly += legendFS * 1.4;
|
|
600
|
+
}
|
|
601
|
+
L.push(` </g>`);
|
|
602
|
+
L.push(` </g>`);
|
|
603
|
+
L.push(`</svg>`);
|
|
604
|
+
return L.join("\n");
|
|
605
|
+
};
|
|
606
|
+
// =============================================================================
|
|
607
|
+
// Public API
|
|
608
|
+
// =============================================================================
|
|
609
|
+
export const renderNet = async (filePath, pattern) => {
|
|
610
|
+
const err = await validateFile(filePath);
|
|
611
|
+
if (err)
|
|
612
|
+
return err;
|
|
613
|
+
const validation = validatePattern(pattern);
|
|
614
|
+
if ("error" in validation)
|
|
615
|
+
return validation;
|
|
616
|
+
const { regex } = validation;
|
|
617
|
+
const lines = await loadAllLines(filePath);
|
|
618
|
+
// Find matching net name
|
|
619
|
+
let matchedNetName = null;
|
|
620
|
+
scanLines(lines, (line) => {
|
|
621
|
+
if (line.includes("<PhyNet ")) {
|
|
622
|
+
const name = attr(line, "name");
|
|
623
|
+
if (name && regex.test(name)) {
|
|
624
|
+
matchedNetName = name;
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
if (!matchedNetName) {
|
|
630
|
+
return { error: `No net matching pattern '${pattern}' found` };
|
|
631
|
+
}
|
|
632
|
+
const factor = extractMicronFactorFromLines(lines);
|
|
633
|
+
const shapes = extractShapes(lines, factor);
|
|
634
|
+
const packages = extractPackages(lines);
|
|
635
|
+
const viaPadSizes = extractViaPadSizes(lines);
|
|
636
|
+
const components = extractComponents(lines, factor);
|
|
637
|
+
const profile = extractProfile(lines, factor);
|
|
638
|
+
const net = extractNetGeometry(lines, matchedNetName, factor);
|
|
639
|
+
const netPins = extractNetPins(lines, matchedNetName);
|
|
640
|
+
// Count resolved pads
|
|
641
|
+
const compMap = new Map(components.map((c) => [c.refdes, c]));
|
|
642
|
+
let resolvedPads = 0;
|
|
643
|
+
for (const np of netPins) {
|
|
644
|
+
const comp = compMap.get(np.refdes);
|
|
645
|
+
if (!comp)
|
|
646
|
+
continue;
|
|
647
|
+
const pkg = packages.get(comp.packageRef);
|
|
648
|
+
if (!pkg)
|
|
649
|
+
continue;
|
|
650
|
+
const pin = pkg.get(np.pin);
|
|
651
|
+
if (pin && shapes.has(pin.shapeId))
|
|
652
|
+
resolvedPads++;
|
|
653
|
+
}
|
|
654
|
+
const svg = generateSvg({ profile, shapes, packages, viaPadSizes, components, net, netPins, factor }, matchedNetName);
|
|
655
|
+
const layersUsed = [...new Set(net.traces.map((t) => t.layer))].sort();
|
|
656
|
+
return {
|
|
657
|
+
netName: matchedNetName,
|
|
658
|
+
units: "MICRON",
|
|
659
|
+
svg,
|
|
660
|
+
stats: {
|
|
661
|
+
traceCount: net.traces.length,
|
|
662
|
+
viaCount: net.vias.length,
|
|
663
|
+
pinCount: netPins.length,
|
|
664
|
+
resolvedPads,
|
|
665
|
+
layersUsed,
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
};
|
|
669
|
+
export const register = (server) => {
|
|
670
|
+
server.registerTool("render_net", {
|
|
671
|
+
description: "Render a net's routing geometry as SVG from an IPC-2581 file. Returns an SVG showing board outline, trace paths by layer, exact SMD pad shapes, via annular rings, and pin labels.",
|
|
672
|
+
inputSchema: {
|
|
673
|
+
file: z.string().describe("Path to IPC-2581 XML file"),
|
|
674
|
+
pattern: z.string().describe("Regex pattern for net name (e.g., '^VDD_3V3B$', 'CLK')"),
|
|
675
|
+
},
|
|
676
|
+
}, async ({ file, pattern }) => {
|
|
677
|
+
const result = await renderNet(file, pattern);
|
|
678
|
+
if (isErrorResult(result))
|
|
679
|
+
return formatResult(result);
|
|
680
|
+
return await formatRenderResult(result);
|
|
681
|
+
});
|
|
682
|
+
};
|
|
683
|
+
//# sourceMappingURL=render-net.js.map
|