@intelligentelectron/pcb-lens 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +4 -0
  4. package/README.md +156 -0
  5. package/dist/cli/commands.d.ts +23 -0
  6. package/dist/cli/commands.d.ts.map +1 -0
  7. package/dist/cli/commands.js +113 -0
  8. package/dist/cli/commands.js.map +1 -0
  9. package/dist/cli/prompts.d.ts +10 -0
  10. package/dist/cli/prompts.d.ts.map +1 -0
  11. package/dist/cli/prompts.js +25 -0
  12. package/dist/cli/prompts.js.map +1 -0
  13. package/dist/cli/shell.d.ts +15 -0
  14. package/dist/cli/shell.d.ts.map +1 -0
  15. package/dist/cli/shell.js +66 -0
  16. package/dist/cli/shell.js.map +1 -0
  17. package/dist/cli/updater.d.ts +50 -0
  18. package/dist/cli/updater.d.ts.map +1 -0
  19. package/dist/cli/updater.js +305 -0
  20. package/dist/cli/updater.js.map +1 -0
  21. package/dist/index.d.ts +15 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +62 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/server.d.ts +16 -0
  26. package/dist/server.d.ts.map +1 -0
  27. package/dist/server.js +175 -0
  28. package/dist/server.js.map +1 -0
  29. package/dist/service.d.ts +13 -0
  30. package/dist/service.d.ts.map +1 -0
  31. package/dist/service.js +1139 -0
  32. package/dist/service.js.map +1 -0
  33. package/dist/types.d.ts +128 -0
  34. package/dist/types.d.ts.map +1 -0
  35. package/dist/types.js +8 -0
  36. package/dist/types.js.map +1 -0
  37. package/dist/version.d.ts +10 -0
  38. package/dist/version.d.ts.map +1 -0
  39. package/dist/version.js +25 -0
  40. package/dist/version.js.map +1 -0
  41. package/dist/wasm-embed.d.ts +3 -0
  42. package/dist/wasm-embed.d.ts.map +1 -0
  43. package/dist/wasm-embed.js +5 -0
  44. package/dist/wasm-embed.js.map +1 -0
  45. package/dist/xml-utils.d.ts +41 -0
  46. package/dist/xml-utils.d.ts.map +1 -0
  47. package/dist/xml-utils.js +81 -0
  48. package/dist/xml-utils.js.map +1 -0
  49. package/package.json +78 -0
@@ -0,0 +1,1139 @@
1
+ /**
2
+ * PCB Layout Service
3
+ *
4
+ * IPC-2581 XML streaming query methods.
5
+ * All methods take a file path to an IPC-2581 XML as input.
6
+ * All physical values (coordinates, trace widths) are normalized to microns.
7
+ */
8
+ import { stat } from "node:fs/promises";
9
+ import path from "node:path";
10
+ import { attr, numAttr, streamAllLines, loadAllLines, scanLines } from "./xml-utils.js";
11
+ // =============================================================================
12
+ // File Validation
13
+ // =============================================================================
14
+ const validateFile = async (filePath) => {
15
+ try {
16
+ const s = await stat(filePath);
17
+ if (!s.isFile()) {
18
+ return { error: `'${filePath}' is not a file` };
19
+ }
20
+ if (!filePath.endsWith(".xml")) {
21
+ return { error: `'${filePath}' is not an XML file` };
22
+ }
23
+ }
24
+ catch {
25
+ return { error: `File not found: '${filePath}'` };
26
+ }
27
+ return null;
28
+ };
29
+ // =============================================================================
30
+ // Unit Conversion
31
+ // =============================================================================
32
+ /**
33
+ * Conversion factors from IPC-2581 unit values to microns.
34
+ */
35
+ const UNIT_TO_MICRON = {
36
+ MICRON: 1,
37
+ MILLIMETER: 1_000,
38
+ MM: 1_000,
39
+ INCH: 25_400,
40
+ };
41
+ /**
42
+ * Extract the micron conversion factor from the CadHeader element.
43
+ * Returns 1 if the file already uses MICRON or the unit is unrecognized.
44
+ */
45
+ const extractMicronFactor = async (filePath) => {
46
+ let factor = 1;
47
+ await streamAllLines(filePath, (line) => {
48
+ if (line.includes("<CadHeader ")) {
49
+ const units = attr(line, "units")?.toUpperCase();
50
+ if (units && units in UNIT_TO_MICRON) {
51
+ factor = UNIT_TO_MICRON[units];
52
+ }
53
+ return false;
54
+ }
55
+ });
56
+ return factor;
57
+ };
58
+ /** Extract micron factor from in-memory lines. */
59
+ const extractMicronFactorFromLines = (lines) => {
60
+ let factor = 1;
61
+ scanLines(lines, (line) => {
62
+ if (line.includes("<CadHeader ")) {
63
+ const units = attr(line, "units")?.toUpperCase();
64
+ if (units && units in UNIT_TO_MICRON) {
65
+ factor = UNIT_TO_MICRON[units];
66
+ }
67
+ return false;
68
+ }
69
+ });
70
+ return factor;
71
+ };
72
+ // =============================================================================
73
+ // LineDesc Dictionary
74
+ // =============================================================================
75
+ /**
76
+ * Build a map of LineDesc IDs to their lineWidth values.
77
+ * These are defined in the Content/DictionaryLineDesc section.
78
+ */
79
+ const buildLineDescDict = async (filePath) => {
80
+ const dict = new Map();
81
+ let currentId;
82
+ await streamAllLines(filePath, (line) => {
83
+ if (line.includes("<EntryLineDesc ")) {
84
+ currentId = attr(line, "id");
85
+ }
86
+ if (line.includes("<LineDesc ") && currentId) {
87
+ const width = numAttr(line, "lineWidth");
88
+ if (width !== undefined) {
89
+ dict.set(currentId, width);
90
+ }
91
+ currentId = undefined;
92
+ }
93
+ // Stop after Content section ends (LineDesc is always in Content)
94
+ if (line.includes("</Content>")) {
95
+ return false;
96
+ }
97
+ });
98
+ return dict;
99
+ };
100
+ // =============================================================================
101
+ // get_design_overview
102
+ // =============================================================================
103
+ export const getDesignOverview = async (filePath) => {
104
+ const err = await validateFile(filePath);
105
+ if (err)
106
+ return err;
107
+ const fileStats = await stat(filePath);
108
+ let ipc2581Revision;
109
+ let stepName;
110
+ const layers = [];
111
+ const seenLayers = new Set();
112
+ let componentCount = 0;
113
+ const netNames = new Set();
114
+ // Track sections by top-level elements
115
+ const topLevelTags = [
116
+ "Content",
117
+ "LogicalNet",
118
+ "LogisticHeader",
119
+ "Bom",
120
+ "Ecad",
121
+ "PhyNet",
122
+ "LayerFeature",
123
+ ];
124
+ const tagPatterns = topLevelTags.map((tag) => [tag, new RegExp(`^\\s*<${tag}[\\s>]`)]);
125
+ const sectionMap = new Map();
126
+ let currentSection = null;
127
+ let currentSectionStart = 0;
128
+ let totalLineCount = 0;
129
+ await streamAllLines(filePath, (line, lineNumber) => {
130
+ totalLineCount = lineNumber;
131
+ // Detect IPC-2581 revision from root element
132
+ if (ipc2581Revision === undefined && line.includes("<IPC-2581")) {
133
+ ipc2581Revision = attr(line, "revision");
134
+ }
135
+ // Detect step name
136
+ if (stepName === undefined && line.includes("<Step ")) {
137
+ stepName = attr(line, "name");
138
+ }
139
+ // Collect layer definitions
140
+ if (line.includes("<LayerRef ") || line.includes("<Layer ")) {
141
+ const name = attr(line, "layerOrGroupRef") ?? attr(line, "name");
142
+ if (name && !seenLayers.has(name)) {
143
+ seenLayers.add(name);
144
+ const side = attr(line, "side");
145
+ const layerFunction = attr(line, "layerFunction");
146
+ const layerInfo = { name };
147
+ if (side)
148
+ layerInfo.side = side;
149
+ if (layerFunction)
150
+ layerInfo.layerFunction = layerFunction;
151
+ layers.push(layerInfo);
152
+ }
153
+ }
154
+ // Count components
155
+ if (line.includes("<Component ") && line.includes("refDes=")) {
156
+ componentCount++;
157
+ }
158
+ // Count unique nets
159
+ if (line.includes("<PhyNet ")) {
160
+ const netName = attr(line, "name");
161
+ if (netName)
162
+ netNames.add(netName);
163
+ }
164
+ // Track major sections
165
+ for (const [tag, pattern] of tagPatterns) {
166
+ if (pattern.test(line)) {
167
+ if (currentSection && currentSectionStart > 0) {
168
+ sectionMap.set(currentSection, (sectionMap.get(currentSection) ?? 0) + (lineNumber - currentSectionStart));
169
+ }
170
+ currentSection = tag;
171
+ currentSectionStart = lineNumber;
172
+ break;
173
+ }
174
+ }
175
+ });
176
+ // Close last section
177
+ if (currentSection && currentSectionStart > 0) {
178
+ sectionMap.set(currentSection, (sectionMap.get(currentSection) ?? 0) + (totalLineCount - currentSectionStart + 1));
179
+ }
180
+ const sections = [...sectionMap.entries()].map(([name, lineCount]) => ({
181
+ name,
182
+ lineCount,
183
+ }));
184
+ return {
185
+ fileName: path.basename(filePath),
186
+ fileSizeBytes: fileStats.size,
187
+ totalLines: totalLineCount,
188
+ units: "MICRON",
189
+ ipc2581Revision,
190
+ stepName,
191
+ layers,
192
+ componentCount,
193
+ netCount: netNames.size,
194
+ sections: sections.sort((a, b) => b.lineCount - a.lineCount),
195
+ };
196
+ };
197
+ // =============================================================================
198
+ // query_components
199
+ // =============================================================================
200
+ export const queryComponents = async (filePath, pattern) => {
201
+ const err = await validateFile(filePath);
202
+ if (err)
203
+ return err;
204
+ if (pattern.length > 200) {
205
+ return { error: "Regex pattern too long (max 200 characters)" };
206
+ }
207
+ let regex;
208
+ try {
209
+ regex = new RegExp(pattern, "i");
210
+ }
211
+ catch {
212
+ return { error: `Invalid regex pattern: '${pattern}'` };
213
+ }
214
+ const factor = await extractMicronFactor(filePath);
215
+ // Pass 1: Collect matching component placements from Component section.
216
+ // Structure:
217
+ // <Component refDes="P10" packageRef="..." layerRef="BOTTOM" part="..." mountType="SMT">
218
+ // <NonstandardAttribute name="VALUE" value="YL004-030-001" type="STRING"/>
219
+ // <Xform rotation="90.000" mirror="true"/>
220
+ // <Location x="70609.968" y="31259.780"/>
221
+ // </Component>
222
+ const placements = new Map();
223
+ let currentRefdes = null;
224
+ await streamAllLines(filePath, (line) => {
225
+ if (line.includes("<Component ") && line.includes("refDes=")) {
226
+ const refdes = attr(line, "refDes");
227
+ if (refdes && regex.test(refdes)) {
228
+ currentRefdes = refdes;
229
+ placements.set(refdes, {
230
+ refdes,
231
+ packageRef: attr(line, "packageRef") ?? "",
232
+ x: 0,
233
+ y: 0,
234
+ rotation: 0,
235
+ layer: attr(line, "layerRef") ?? "",
236
+ mountType: attr(line, "mountType"),
237
+ });
238
+ }
239
+ else {
240
+ currentRefdes = null;
241
+ }
242
+ }
243
+ if (currentRefdes && placements.has(currentRefdes)) {
244
+ const comp = placements.get(currentRefdes);
245
+ if (line.includes("<Location ")) {
246
+ const x = numAttr(line, "x");
247
+ const y = numAttr(line, "y");
248
+ if (x !== undefined)
249
+ comp.x = x * factor;
250
+ if (y !== undefined)
251
+ comp.y = y * factor;
252
+ }
253
+ if (line.includes("<Xform ")) {
254
+ const rotation = numAttr(line, "rotation");
255
+ if (rotation !== undefined)
256
+ comp.rotation = rotation;
257
+ }
258
+ if (line.includes("</Component>")) {
259
+ currentRefdes = null;
260
+ }
261
+ }
262
+ // Stop after Placement section (components come before PhyNet)
263
+ if (line.includes("<PhyNetGroup ") || line.includes("</Step>")) {
264
+ return false;
265
+ }
266
+ });
267
+ if (placements.size === 0) {
268
+ return { pattern, units: "MICRON", matches: [] };
269
+ }
270
+ // Pass 2: Collect BOM data for matched refdes.
271
+ // Structure:
272
+ // <BomItem OEMDesignNumberRef="..." quantity="1" pinCount="14" category="ELECTRICAL">
273
+ // <RefDes name="P10" packageRef="..." populate="true" layerRef="BOTTOM"/>
274
+ // <RefDes name="P9" .../>
275
+ // <Characteristics category="ELECTRICAL">
276
+ // <Textual ... textualCharacteristicName="DEVICE_TYPE" textualCharacteristicValue="..."/>
277
+ // <Textual ... textualCharacteristicName="COMP_VALUE" textualCharacteristicValue="..."/>
278
+ // </Characteristics>
279
+ // </BomItem>
280
+ const bomCharacteristics = new Map();
281
+ const bomDescriptions = new Map();
282
+ let currentBomRefdes = [];
283
+ let currentBomChars = {};
284
+ let currentBomDesc;
285
+ await streamAllLines(filePath, (line) => {
286
+ if (line.includes("<BomItem ")) {
287
+ currentBomRefdes = [];
288
+ currentBomChars = {};
289
+ currentBomDesc = attr(line, "OEMDesignNumberRef");
290
+ }
291
+ if (line.includes("<RefDes ")) {
292
+ const name = attr(line, "name");
293
+ if (name && placements.has(name)) {
294
+ currentBomRefdes.push(name);
295
+ }
296
+ }
297
+ if (line.includes("<Textual ")) {
298
+ const charName = attr(line, "textualCharacteristicName");
299
+ const charValue = attr(line, "textualCharacteristicValue");
300
+ if (charName && charValue) {
301
+ currentBomChars[charName] = charValue;
302
+ }
303
+ }
304
+ if (line.includes("</BomItem>")) {
305
+ for (const refdes of currentBomRefdes) {
306
+ bomCharacteristics.set(refdes, { ...currentBomChars });
307
+ if (currentBomDesc) {
308
+ bomDescriptions.set(refdes, currentBomDesc);
309
+ }
310
+ }
311
+ currentBomRefdes = [];
312
+ currentBomChars = {};
313
+ currentBomDesc = undefined;
314
+ }
315
+ if (line.includes("</Bom>")) {
316
+ return false;
317
+ }
318
+ });
319
+ // Merge placement + BOM
320
+ const matches = [];
321
+ for (const [refdes, placement] of placements) {
322
+ matches.push({
323
+ ...placement,
324
+ description: bomDescriptions.get(refdes),
325
+ characteristics: bomCharacteristics.get(refdes) ?? {},
326
+ });
327
+ }
328
+ matches.sort((a, b) => a.refdes.localeCompare(b.refdes));
329
+ return { pattern, units: "MICRON", matches };
330
+ };
331
+ // =============================================================================
332
+ // query_net
333
+ // =============================================================================
334
+ export const queryNet = async (filePath, pattern) => {
335
+ const err = await validateFile(filePath);
336
+ if (err)
337
+ return err;
338
+ if (pattern.length > 200) {
339
+ return { error: "Regex pattern too long (max 200 characters)" };
340
+ }
341
+ let regex;
342
+ try {
343
+ regex = new RegExp(pattern, "i");
344
+ }
345
+ catch {
346
+ return { error: `Invalid regex pattern: '${pattern}'` };
347
+ }
348
+ const factor = await extractMicronFactor(filePath);
349
+ // Pass 1: Find matching net name from PhyNet section
350
+ let matchedNetName = null;
351
+ await streamAllLines(filePath, (line) => {
352
+ if (line.includes("<PhyNet ")) {
353
+ const name = attr(line, "name");
354
+ if (name && regex.test(name)) {
355
+ if (!matchedNetName)
356
+ matchedNetName = name;
357
+ return false; // Found it, stop
358
+ }
359
+ }
360
+ });
361
+ if (!matchedNetName) {
362
+ return { error: `No net matching pattern '${pattern}' found` };
363
+ }
364
+ // Pass 2: Build LineDesc dictionary (for resolving trace widths)
365
+ const lineDescDict = await buildLineDescDict(filePath);
366
+ // Pass 3: Single pass through LayerFeature sections.
367
+ // Collect pins, routing, and vias for the matched net.
368
+ //
369
+ // Structure in LayerFeature:
370
+ // <LayerFeature layerRef="TOP">
371
+ // <Set net="VDD_3V3B"> ← routing Set
372
+ // <Features>
373
+ // <Polyline>
374
+ // <LineDescRef id="ROUND_1500"/> ← trace width ref
375
+ // </Polyline>
376
+ // </Features>
377
+ // </Set>
378
+ // <Set net="DGND" testPoint="false" plate="true"> ← pad Set
379
+ // <Pad padstackDefRef="60C32D">
380
+ // <PinRef pin="43" componentRef="P9"/> ← pin connection
381
+ // </Pad>
382
+ // </Set>
383
+ const pins = [];
384
+ const pinsSeen = new Set();
385
+ const routeMap = new Map();
386
+ const viaMap = new Map();
387
+ const skipLayers = new Set(["REF-route", "REF-both"]);
388
+ let currentLayerName = "";
389
+ let insideMatchedSet = false;
390
+ let currentSetHasPolyline = false;
391
+ let currentSetLineDescId;
392
+ let currentSetInlineWidth;
393
+ let currentSetGeometry;
394
+ await streamAllLines(filePath, (line) => {
395
+ // Track which LayerFeature we're in
396
+ if (line.includes("<LayerFeature ")) {
397
+ currentLayerName = attr(line, "layerRef") ?? "";
398
+ }
399
+ // Detect <Set net="..."> matching our target net (skip phantom REF layers)
400
+ if (line.includes("<Set ")) {
401
+ const netName = attr(line, "net");
402
+ insideMatchedSet = netName === matchedNetName && !skipLayers.has(currentLayerName);
403
+ currentSetHasPolyline = false;
404
+ currentSetLineDescId = undefined;
405
+ currentSetInlineWidth = undefined;
406
+ currentSetGeometry = attr(line, "geometry");
407
+ }
408
+ if (insideMatchedSet) {
409
+ // Collect pin references
410
+ if (line.includes("<PinRef ")) {
411
+ const compRef = attr(line, "componentRef");
412
+ const pin = attr(line, "pin");
413
+ if (compRef && pin) {
414
+ const key = `${compRef}.${pin}`;
415
+ if (!pinsSeen.has(key)) {
416
+ pinsSeen.add(key);
417
+ pins.push({ refdes: compRef, pin });
418
+ }
419
+ }
420
+ }
421
+ // Track polyline (trace) segments
422
+ if (line.includes("<Polyline")) {
423
+ currentSetHasPolyline = true;
424
+ }
425
+ // Capture LineDescRef for width resolution (dictionary reference)
426
+ if (line.includes("<LineDescRef ")) {
427
+ currentSetLineDescId = attr(line, "id");
428
+ }
429
+ // Capture inline LineDesc for width (direct definition inside Polyline)
430
+ if (line.includes("<LineDesc ") && !line.includes("<EntryLineDesc ")) {
431
+ const inlineWidth = numAttr(line, "lineWidth");
432
+ if (inlineWidth !== undefined) {
433
+ currentSetInlineWidth = inlineWidth;
434
+ }
435
+ }
436
+ // Track vias: <Hole platingStatus="VIA"> in DRILL layers
437
+ if (line.includes("<Hole ")) {
438
+ const platingStatus = attr(line, "platingStatus");
439
+ if (platingStatus === "VIA") {
440
+ const diameter = numAttr(line, "diameter");
441
+ const key = currentSetGeometry ?? `dia_${diameter ?? "unknown"}`;
442
+ viaMap.set(key, (viaMap.get(key) ?? 0) + 1);
443
+ }
444
+ }
445
+ if (line.includes("</Set>")) {
446
+ // Finalize the Set: if it had a polyline, record the route segment
447
+ if (currentSetHasPolyline && currentLayerName) {
448
+ if (!routeMap.has(currentLayerName)) {
449
+ routeMap.set(currentLayerName, { widths: new Set(), segments: 0 });
450
+ }
451
+ const layerRoute = routeMap.get(currentLayerName);
452
+ layerRoute.segments++;
453
+ // Resolve width: prefer LineDescRef dictionary lookup, fall back to inline LineDesc
454
+ if (currentSetLineDescId) {
455
+ const width = lineDescDict.get(currentSetLineDescId);
456
+ if (width !== undefined) {
457
+ layerRoute.widths.add(width * factor);
458
+ }
459
+ }
460
+ else if (currentSetInlineWidth !== undefined) {
461
+ layerRoute.widths.add(currentSetInlineWidth * factor);
462
+ }
463
+ }
464
+ insideMatchedSet = false;
465
+ }
466
+ }
467
+ });
468
+ const routing = [];
469
+ for (const [layerName, data] of routeMap) {
470
+ routing.push({
471
+ layerName,
472
+ traceWidths: [...data.widths].sort((a, b) => a - b),
473
+ segmentCount: data.segments,
474
+ });
475
+ }
476
+ const vias = [];
477
+ for (const [padstackRef, count] of viaMap) {
478
+ vias.push({ padstackRef, count });
479
+ }
480
+ const totalSegments = routing.reduce((sum, r) => sum + r.segmentCount, 0);
481
+ const totalVias = vias.reduce((sum, v) => sum + v.count, 0);
482
+ const layersUsed = routing.map((r) => r.layerName).sort();
483
+ return {
484
+ netName: matchedNetName,
485
+ units: "MICRON",
486
+ pins,
487
+ routing,
488
+ vias,
489
+ totalSegments,
490
+ totalVias,
491
+ layersUsed,
492
+ };
493
+ };
494
+ // =============================================================================
495
+ // render_net
496
+ // =============================================================================
497
+ /**
498
+ * Fixed colors for well-known layers; inner layers get dynamic colors.
499
+ */
500
+ const FIXED_LAYER_COLORS = {
501
+ TOP: "#e74c3c",
502
+ BOTTOM: "#3498db",
503
+ };
504
+ const INNER_LAYER_PALETTE = [
505
+ "#2ecc71", // green
506
+ "#9b59b6", // purple
507
+ "#f39c12", // orange
508
+ "#1abc9c", // teal
509
+ "#e67e22", // dark orange
510
+ "#3498db", // (fallback blue variant — won't collide since BOTTOM is already assigned)
511
+ "#e84393", // pink
512
+ "#00cec9", // cyan
513
+ ];
514
+ /**
515
+ * Build a color map for layers actually present in the rendered data.
516
+ * TOP and BOTTOM get fixed colors; inner layers get assigned from the palette.
517
+ */
518
+ const buildLayerColors = (layers) => {
519
+ const colorMap = new Map();
520
+ let paletteIdx = 0;
521
+ for (const layer of layers) {
522
+ const upper = layer.toUpperCase();
523
+ if (upper === "TOP" || upper === "BOTTOM") {
524
+ colorMap.set(layer, FIXED_LAYER_COLORS[upper]);
525
+ }
526
+ else {
527
+ colorMap.set(layer, INNER_LAYER_PALETTE[paletteIdx % INNER_LAYER_PALETTE.length]);
528
+ paletteIdx++;
529
+ }
530
+ }
531
+ return colorMap;
532
+ };
533
+ // ---------------------------------------------------------------------------
534
+ // Extraction passes (in-memory line scanning)
535
+ // ---------------------------------------------------------------------------
536
+ const extractShapes = (lines, f) => {
537
+ const shapes = new Map();
538
+ let currentId = "";
539
+ scanLines(lines, (line) => {
540
+ if (line.includes("<EntryStandard ")) {
541
+ currentId = attr(line, "id") ?? "";
542
+ }
543
+ if (currentId && line.includes("<RectCenter ")) {
544
+ const w = numAttr(line, "width");
545
+ const h = numAttr(line, "height");
546
+ if (w !== undefined && h !== undefined) {
547
+ shapes.set(currentId, { type: "rect", width: w * f, height: h * f });
548
+ }
549
+ currentId = "";
550
+ }
551
+ if (currentId && line.includes("<Circle ")) {
552
+ const d = numAttr(line, "diameter");
553
+ if (d !== undefined) {
554
+ shapes.set(currentId, { type: "circle", width: d * f, height: d * f });
555
+ }
556
+ currentId = "";
557
+ }
558
+ if (line.includes("</Content>"))
559
+ return false;
560
+ });
561
+ return shapes;
562
+ };
563
+ const extractPackages = (lines) => {
564
+ const packages = new Map();
565
+ let currentPkg = "";
566
+ let inPad = false;
567
+ let inPin = false;
568
+ let padPin = "";
569
+ let padOffset = { x: 0, y: 0 };
570
+ let padShapeId = "";
571
+ const commitPad = () => {
572
+ if (currentPkg && padPin && padShapeId) {
573
+ packages.get(currentPkg).set(padPin, {
574
+ offsetX: padOffset.x,
575
+ offsetY: padOffset.y,
576
+ shapeId: padShapeId,
577
+ });
578
+ }
579
+ padPin = "";
580
+ padOffset = { x: 0, y: 0 };
581
+ padShapeId = "";
582
+ };
583
+ scanLines(lines, (line) => {
584
+ if (line.includes("<Package ")) {
585
+ currentPkg = attr(line, "name") ?? "";
586
+ if (currentPkg && !packages.has(currentPkg)) {
587
+ packages.set(currentPkg, new Map());
588
+ }
589
+ }
590
+ if (line.includes("</Package>")) {
591
+ currentPkg = "";
592
+ }
593
+ if (!currentPkg)
594
+ return;
595
+ // RevA/B: <LandPattern> → <Pad> → <PinRef>/<Location>/<StandardPrimitiveRef> → </Pad>
596
+ if (line.includes("<Pad") && (line.includes("<Pad>") || line.includes("<Pad "))) {
597
+ inPad = true;
598
+ padPin = "";
599
+ padOffset = { x: 0, y: 0 };
600
+ padShapeId = "";
601
+ }
602
+ if (inPad) {
603
+ if (line.includes("<PinRef ")) {
604
+ padPin = attr(line, "pin") ?? "";
605
+ }
606
+ if (line.includes("<Location ")) {
607
+ const x = numAttr(line, "x");
608
+ const y = numAttr(line, "y");
609
+ if (x !== undefined)
610
+ padOffset.x = x;
611
+ if (y !== undefined)
612
+ padOffset.y = y;
613
+ }
614
+ if (line.includes("<StandardPrimitiveRef ")) {
615
+ padShapeId = attr(line, "id") ?? "";
616
+ }
617
+ if (line.includes("</Pad>")) {
618
+ commitPad();
619
+ inPad = false;
620
+ }
621
+ }
622
+ // RevC: <Pin number="..." ...> → <Location>/<StandardPrimitiveRef> → </Pin>
623
+ if (!inPad && line.includes("<Pin ") && line.includes("number=")) {
624
+ inPin = true;
625
+ padPin = attr(line, "number") ?? "";
626
+ padOffset = { x: 0, y: 0 };
627
+ padShapeId = "";
628
+ }
629
+ if (inPin) {
630
+ if (line.includes("<Location ")) {
631
+ const x = numAttr(line, "x");
632
+ const y = numAttr(line, "y");
633
+ if (x !== undefined)
634
+ padOffset.x = x;
635
+ if (y !== undefined)
636
+ padOffset.y = y;
637
+ }
638
+ if (line.includes("<StandardPrimitiveRef ")) {
639
+ padShapeId = attr(line, "id") ?? "";
640
+ }
641
+ if (line.includes("</Pin>")) {
642
+ commitPad();
643
+ inPin = false;
644
+ }
645
+ }
646
+ if (line.includes("<Component ") && line.includes("refDes="))
647
+ return false;
648
+ });
649
+ return packages;
650
+ };
651
+ const extractViaPadSizes = (lines) => {
652
+ const viaPads = new Map();
653
+ let currentName = "";
654
+ let currentDrill = 0;
655
+ let foundRegular = false;
656
+ scanLines(lines, (line) => {
657
+ if (line.includes("<PadStackDef ")) {
658
+ currentName = attr(line, "name") ?? "";
659
+ currentDrill = 0;
660
+ foundRegular = false;
661
+ }
662
+ if (currentName && line.includes("<PadstackHoleDef ")) {
663
+ currentDrill = numAttr(line, "diameter") ?? 0;
664
+ }
665
+ if (currentName && !foundRegular && line.includes('padUse="REGULAR"')) {
666
+ foundRegular = true;
667
+ }
668
+ if (currentName && foundRegular && line.includes("<StandardPrimitiveRef ")) {
669
+ const id = attr(line, "id") ?? "";
670
+ if (id && currentDrill > 0) {
671
+ viaPads.set(currentName, { padShapeId: id, drillDiameter: currentDrill });
672
+ }
673
+ foundRegular = false;
674
+ }
675
+ if (line.includes("</PadStackDef>")) {
676
+ currentName = "";
677
+ }
678
+ if (line.includes("<Package "))
679
+ return false;
680
+ });
681
+ return viaPads;
682
+ };
683
+ const extractComponents = (lines, f) => {
684
+ const components = [];
685
+ let current = null;
686
+ scanLines(lines, (line) => {
687
+ if (line.includes("<Component ") && line.includes("refDes=")) {
688
+ const refdes = attr(line, "refDes");
689
+ if (refdes) {
690
+ current = {
691
+ refdes,
692
+ packageRef: attr(line, "packageRef") ?? "",
693
+ x: 0,
694
+ y: 0,
695
+ rotation: 0,
696
+ mirror: false,
697
+ layer: attr(line, "layerRef") ?? "",
698
+ };
699
+ }
700
+ }
701
+ if (current) {
702
+ if (line.includes("<Location ")) {
703
+ const x = numAttr(line, "x");
704
+ const y = numAttr(line, "y");
705
+ if (x !== undefined)
706
+ current.x = x * f;
707
+ if (y !== undefined)
708
+ current.y = y * f;
709
+ }
710
+ if (line.includes("<Xform ")) {
711
+ current.rotation = numAttr(line, "rotation") ?? 0;
712
+ current.mirror = attr(line, "mirror") === "true";
713
+ }
714
+ if (line.includes("</Component>")) {
715
+ components.push(current);
716
+ current = null;
717
+ }
718
+ }
719
+ if (line.includes("<PhyNetGroup ") || line.includes("</Step>"))
720
+ return false;
721
+ });
722
+ return components;
723
+ };
724
+ const extractProfile = (lines, f) => {
725
+ const points = [];
726
+ const arcs = new Map();
727
+ let inProfile = false;
728
+ scanLines(lines, (line) => {
729
+ if (line.includes("<Profile>")) {
730
+ inProfile = true;
731
+ return;
732
+ }
733
+ if (line.includes("</Profile>"))
734
+ return false;
735
+ if (!inProfile)
736
+ return;
737
+ if (line.includes("<PolyBegin ")) {
738
+ const x = numAttr(line, "x");
739
+ const y = numAttr(line, "y");
740
+ if (x !== undefined && y !== undefined)
741
+ points.push({ x: x * f, y: y * f });
742
+ }
743
+ if (line.includes("<PolyStepSegment ")) {
744
+ const x = numAttr(line, "x");
745
+ const y = numAttr(line, "y");
746
+ if (x !== undefined && y !== undefined)
747
+ points.push({ x: x * f, y: y * f });
748
+ }
749
+ if (line.includes("<PolyStepCurve ")) {
750
+ const x = numAttr(line, "x");
751
+ const y = numAttr(line, "y");
752
+ const cx = numAttr(line, "centerX");
753
+ const cy = numAttr(line, "centerY");
754
+ const cw = attr(line, "clockwise") === "true";
755
+ if (x !== undefined && y !== undefined && cx !== undefined && cy !== undefined) {
756
+ points.push({ x: x * f, y: y * f });
757
+ arcs.set(points.length - 1, {
758
+ x: x * f,
759
+ y: y * f,
760
+ centerX: cx * f,
761
+ centerY: cy * f,
762
+ clockwise: cw,
763
+ });
764
+ }
765
+ }
766
+ });
767
+ return { points, arcs };
768
+ };
769
+ const extractNetGeometry = (lines, netName, f) => {
770
+ const traces = [];
771
+ const vias = [];
772
+ let currentLayer = "";
773
+ let insideMatchedSet = false;
774
+ let currentPoints = [];
775
+ let currentWidth = 0;
776
+ let inPolyline = false;
777
+ let currentGeometry = "";
778
+ const skipLayers = new Set(["REF-route", "REF-both"]);
779
+ scanLines(lines, (line) => {
780
+ if (line.includes("<LayerFeature ")) {
781
+ currentLayer = attr(line, "layerRef") ?? "";
782
+ }
783
+ if (line.includes("<Set ")) {
784
+ const net = attr(line, "net");
785
+ insideMatchedSet = net === netName && !skipLayers.has(currentLayer);
786
+ currentPoints = [];
787
+ currentWidth = 0;
788
+ inPolyline = false;
789
+ currentGeometry = attr(line, "geometry") ?? "";
790
+ }
791
+ if (!insideMatchedSet)
792
+ return;
793
+ if (line.includes("<Polyline")) {
794
+ inPolyline = true;
795
+ currentPoints = [];
796
+ currentWidth = 0;
797
+ }
798
+ if (inPolyline) {
799
+ if (line.includes("<PolyBegin ")) {
800
+ const x = numAttr(line, "x");
801
+ const y = numAttr(line, "y");
802
+ if (x !== undefined && y !== undefined)
803
+ currentPoints.push({ x: x * f, y: y * f });
804
+ }
805
+ if (line.includes("<PolyStepSegment ")) {
806
+ const x = numAttr(line, "x");
807
+ const y = numAttr(line, "y");
808
+ if (x !== undefined && y !== undefined)
809
+ currentPoints.push({ x: x * f, y: y * f });
810
+ }
811
+ if (line.includes("<LineDesc ")) {
812
+ const w = numAttr(line, "lineWidth");
813
+ if (w !== undefined)
814
+ currentWidth = w * f;
815
+ }
816
+ if (line.includes("</Polyline>")) {
817
+ if (currentPoints.length >= 2) {
818
+ traces.push({ layer: currentLayer, points: [...currentPoints], width: currentWidth });
819
+ }
820
+ inPolyline = false;
821
+ }
822
+ }
823
+ if (line.includes("<Hole ")) {
824
+ const status = attr(line, "platingStatus");
825
+ if (status === "VIA") {
826
+ const x = numAttr(line, "x");
827
+ const y = numAttr(line, "y");
828
+ const d = (numAttr(line, "diameter") ?? 0.008) * f;
829
+ if (x !== undefined && y !== undefined) {
830
+ vias.push({ x: x * f, y: y * f, drillDiameter: d, padstackRef: currentGeometry });
831
+ }
832
+ }
833
+ }
834
+ if (line.includes("</Set>"))
835
+ insideMatchedSet = false;
836
+ });
837
+ return { traces, vias };
838
+ };
839
+ const extractNetPins = (lines, netName) => {
840
+ const pins = [];
841
+ const seen = new Set();
842
+ let insideMatchedSet = false;
843
+ scanLines(lines, (line) => {
844
+ if (line.includes("<Set ")) {
845
+ insideMatchedSet = attr(line, "net") === netName;
846
+ }
847
+ if (insideMatchedSet && line.includes("<PinRef ")) {
848
+ const compRef = attr(line, "componentRef");
849
+ const pin = attr(line, "pin");
850
+ if (compRef && pin) {
851
+ const key = `${compRef}.${pin}`;
852
+ if (!seen.has(key)) {
853
+ seen.add(key);
854
+ pins.push({ refdes: compRef, pin });
855
+ }
856
+ }
857
+ }
858
+ if (line.includes("</Set>"))
859
+ insideMatchedSet = false;
860
+ });
861
+ return pins;
862
+ };
863
+ // ---------------------------------------------------------------------------
864
+ // Geometry helpers
865
+ // ---------------------------------------------------------------------------
866
+ const transformPin = (comp, pinDef, f) => {
867
+ const rad = (comp.rotation * Math.PI) / 180;
868
+ const cos = Math.cos(rad);
869
+ const sin = Math.sin(rad);
870
+ let dx = pinDef.offsetX * f;
871
+ const dy = pinDef.offsetY * f;
872
+ if (comp.mirror)
873
+ dx = -dx;
874
+ return {
875
+ x: comp.x + dx * cos - dy * sin,
876
+ y: comp.y + dx * sin + dy * cos,
877
+ };
878
+ };
879
+ const svgArc = (prev, arc) => {
880
+ const r = Math.sqrt((arc.centerX - prev.x) ** 2 + (arc.centerY - prev.y) ** 2);
881
+ const sweepFlag = arc.clockwise ? 0 : 1;
882
+ return `A${r},${r} 0 0 ${sweepFlag} ${arc.x},${arc.y}`;
883
+ };
884
+ const generateSvg = (data, netName) => {
885
+ const { profile, shapes, packages, viaPadSizes, components, net, netPins, factor } = data;
886
+ // Build dynamic layer color map from layers actually used
887
+ const allLayers = [
888
+ ...new Set([
889
+ ...net.traces.map((t) => t.layer),
890
+ ...netPins
891
+ .map((np) => {
892
+ const comp = components.find((c) => c.refdes === np.refdes);
893
+ return comp?.layer ?? "";
894
+ })
895
+ .filter(Boolean),
896
+ ]),
897
+ ];
898
+ const layerColors = buildLayerColors(allLayers);
899
+ // Board bounds
900
+ const allPx = profile.points.map((p) => p.x);
901
+ const allPy = profile.points.map((p) => p.y);
902
+ const boardMinX = Math.min(...allPx);
903
+ const boardMaxX = Math.max(...allPx);
904
+ const boardMinY = Math.min(...allPy);
905
+ const boardMaxY = Math.max(...allPy);
906
+ const boardW = boardMaxX - boardMinX;
907
+ const boardH = boardMaxY - boardMinY;
908
+ const margin = Math.max(boardW, boardH) * 0.08;
909
+ const vbX = boardMinX - margin;
910
+ const vbY = boardMinY - margin;
911
+ const vbW = boardW + 2 * margin;
912
+ const vbH = boardH + 2 * margin;
913
+ const svgWidth = 1200;
914
+ const svgHeight = Math.round(svgWidth * (vbH / vbW));
915
+ const fontSize = boardW * 0.008;
916
+ const pinFontSize = boardW * 0.005;
917
+ const L = [];
918
+ L.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${svgWidth}" height="${svgHeight}" viewBox="${vbX} ${vbY} ${vbW} ${vbH}">`);
919
+ L.push(` <title>Net: ${netName}</title>`);
920
+ L.push(` <rect x="${vbX}" y="${vbY}" width="${vbW}" height="${vbH}" fill="#1a1a2e"/>`);
921
+ L.push(` <g transform="scale(1,-1) translate(0,${-(boardMinY + boardMaxY)})">`);
922
+ // Board outline
923
+ if (profile.points.length > 0) {
924
+ let d = `M${profile.points[0].x},${profile.points[0].y}`;
925
+ for (let i = 1; i < profile.points.length; i++) {
926
+ const arc = profile.arcs.get(i);
927
+ d += arc
928
+ ? ` ${svgArc(profile.points[i - 1], arc)}`
929
+ : ` L${profile.points[i].x},${profile.points[i].y}`;
930
+ }
931
+ d += " Z";
932
+ L.push(` <path d="${d}" fill="#16213e" stroke="#e0e0e0" stroke-width="${boardW * 0.003}"/>`);
933
+ }
934
+ // Component dots
935
+ const compMap = new Map();
936
+ for (const c of components)
937
+ compMap.set(c.refdes, c);
938
+ const netRefdes = new Set(netPins.map((p) => p.refdes));
939
+ L.push(` <!-- Components -->`);
940
+ L.push(` <g>`);
941
+ for (const c of components) {
942
+ const onNet = netRefdes.has(c.refdes);
943
+ L.push(` <circle cx="${c.x}" cy="${c.y}" r="${boardW * 0.0012}" fill="${onNet ? "#ffffff" : "#334155"}" opacity="${onNet ? 0.5 : 0.2}"/>`);
944
+ }
945
+ L.push(` </g>`);
946
+ // Refdes labels for net components
947
+ L.push(` <!-- Refdes labels -->`);
948
+ L.push(` <g font-family="monospace" font-size="${fontSize}" fill="#ffffff">`);
949
+ const labeled = new Set();
950
+ for (const p of netPins) {
951
+ if (labeled.has(p.refdes))
952
+ continue;
953
+ labeled.add(p.refdes);
954
+ const comp = compMap.get(p.refdes);
955
+ if (!comp)
956
+ continue;
957
+ L.push(` <text x="${comp.x}" y="${comp.y}" transform="scale(1,-1) translate(0,${-2 * comp.y})" dy="${-fontSize}">${comp.refdes}</text>`);
958
+ }
959
+ L.push(` </g>`);
960
+ // Traces by layer
961
+ const byLayer = new Map();
962
+ for (const t of net.traces) {
963
+ const arr = byLayer.get(t.layer) ?? [];
964
+ arr.push(t);
965
+ byLayer.set(t.layer, arr);
966
+ }
967
+ for (const [layer, layerTraces] of byLayer) {
968
+ const color = layerColors.get(layer) ?? "#7f8c8d";
969
+ L.push(` <!-- ${netName} — ${layer} -->`);
970
+ L.push(` <g stroke="${color}" fill="none" stroke-linecap="round" stroke-linejoin="round" opacity="0.9">`);
971
+ for (const t of layerTraces) {
972
+ const sw = t.width > 0 ? t.width : boardW * 0.003;
973
+ const d = t.points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x},${p.y}`).join(" ");
974
+ L.push(` <path d="${d}" stroke-width="${sw}"/>`);
975
+ }
976
+ L.push(` </g>`);
977
+ }
978
+ // SMD pads at pin locations
979
+ L.push(` <!-- SMD Pads -->`);
980
+ L.push(` <g>`);
981
+ for (const np of netPins) {
982
+ const comp = compMap.get(np.refdes);
983
+ if (!comp)
984
+ continue;
985
+ const pkg = packages.get(comp.packageRef);
986
+ if (!pkg)
987
+ continue;
988
+ const pinDef = pkg.get(np.pin);
989
+ if (!pinDef)
990
+ continue;
991
+ const pos = transformPin(comp, pinDef, factor);
992
+ const shape = shapes.get(pinDef.shapeId);
993
+ if (!shape)
994
+ continue;
995
+ const layer = comp.layer || "TOP";
996
+ const color = layerColors.get(layer) ?? "#7f8c8d";
997
+ if (shape.type === "rect") {
998
+ const hw = shape.width / 2;
999
+ const hh = shape.height / 2;
1000
+ if (Math.abs(comp.rotation % 180) < 0.1) {
1001
+ L.push(` <rect x="${pos.x - hw}" y="${pos.y - hh}" width="${shape.width}" height="${shape.height}" fill="${color}" opacity="0.8"/>`);
1002
+ }
1003
+ else if (Math.abs((comp.rotation - 90) % 180) < 0.1) {
1004
+ L.push(` <rect x="${pos.x - hh}" y="${pos.y - hw}" width="${shape.height}" height="${shape.width}" fill="${color}" opacity="0.8"/>`);
1005
+ }
1006
+ else {
1007
+ const rad = (comp.rotation * Math.PI) / 180;
1008
+ 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)})"/>`);
1009
+ }
1010
+ }
1011
+ else {
1012
+ const r = shape.width / 2;
1013
+ L.push(` <circle cx="${pos.x}" cy="${pos.y}" r="${r}" fill="${color}" opacity="0.8"/>`);
1014
+ }
1015
+ }
1016
+ L.push(` </g>`);
1017
+ // Vias
1018
+ if (net.vias.length > 0) {
1019
+ L.push(` <!-- Vias -->`);
1020
+ L.push(` <g>`);
1021
+ for (const v of net.vias) {
1022
+ const viaDef = viaPadSizes.get(v.padstackRef);
1023
+ let padDiameter;
1024
+ if (viaDef) {
1025
+ const padShape = shapes.get(viaDef.padShapeId);
1026
+ padDiameter = padShape ? padShape.width : v.drillDiameter * 2.25;
1027
+ }
1028
+ else {
1029
+ padDiameter = v.drillDiameter * 2.25;
1030
+ }
1031
+ const padR = padDiameter / 2;
1032
+ const drillR = v.drillDiameter / 2;
1033
+ L.push(` <circle cx="${v.x}" cy="${v.y}" r="${padR}" fill="#c8a415" stroke="#a08410" stroke-width="${boardW * 0.0008}"/>`);
1034
+ L.push(` <circle cx="${v.x}" cy="${v.y}" r="${drillR}" fill="#1a1a2e"/>`);
1035
+ }
1036
+ L.push(` </g>`);
1037
+ }
1038
+ // Pin labels
1039
+ L.push(` <!-- Pin labels -->`);
1040
+ L.push(` <g font-family="monospace" font-size="${pinFontSize}" fill="#f1c40f" text-anchor="middle">`);
1041
+ for (const np of netPins) {
1042
+ const comp = compMap.get(np.refdes);
1043
+ if (!comp)
1044
+ continue;
1045
+ const pkg = packages.get(comp.packageRef);
1046
+ const pinDef = pkg?.get(np.pin);
1047
+ if (pinDef) {
1048
+ const pos = transformPin(comp, pinDef, factor);
1049
+ 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>`);
1050
+ }
1051
+ }
1052
+ L.push(` </g>`);
1053
+ // Legend
1054
+ const legendX = vbX + margin * 0.3;
1055
+ const legendY = boardMaxY + margin * 0.5;
1056
+ const legendFS = boardW * 0.01;
1057
+ L.push(` <!-- Legend -->`);
1058
+ L.push(` <g font-family="monospace" font-size="${legendFS}" transform="scale(1,-1) translate(0,${-2 * legendY})">`);
1059
+ let ly = legendY;
1060
+ for (const [layer, color] of layerColors) {
1061
+ L.push(` <rect x="${legendX}" y="${ly}" width="${legendFS}" height="${legendFS}" fill="${color}"/>`);
1062
+ L.push(` <text x="${legendX + legendFS * 1.5}" y="${ly + legendFS * 0.85}" fill="#e0e0e0">${layer}</text>`);
1063
+ ly += legendFS * 1.4;
1064
+ }
1065
+ L.push(` </g>`);
1066
+ L.push(` </g>`);
1067
+ L.push(`</svg>`);
1068
+ return L.join("\n");
1069
+ };
1070
+ // ---------------------------------------------------------------------------
1071
+ // Public API
1072
+ // ---------------------------------------------------------------------------
1073
+ export const renderNet = async (filePath, pattern) => {
1074
+ const err = await validateFile(filePath);
1075
+ if (err)
1076
+ return err;
1077
+ if (pattern.length > 200) {
1078
+ return { error: "Regex pattern too long (max 200 characters)" };
1079
+ }
1080
+ let regex;
1081
+ try {
1082
+ regex = new RegExp(pattern, "i");
1083
+ }
1084
+ catch {
1085
+ return { error: `Invalid regex pattern: '${pattern}'` };
1086
+ }
1087
+ const lines = await loadAllLines(filePath);
1088
+ // Find matching net name
1089
+ let matchedNetName = null;
1090
+ scanLines(lines, (line) => {
1091
+ if (line.includes("<PhyNet ")) {
1092
+ const name = attr(line, "name");
1093
+ if (name && regex.test(name)) {
1094
+ matchedNetName = name;
1095
+ return false;
1096
+ }
1097
+ }
1098
+ });
1099
+ if (!matchedNetName) {
1100
+ return { error: `No net matching pattern '${pattern}' found` };
1101
+ }
1102
+ const factor = extractMicronFactorFromLines(lines);
1103
+ const shapes = extractShapes(lines, factor);
1104
+ const packages = extractPackages(lines);
1105
+ const viaPadSizes = extractViaPadSizes(lines);
1106
+ const components = extractComponents(lines, factor);
1107
+ const profile = extractProfile(lines, factor);
1108
+ const net = extractNetGeometry(lines, matchedNetName, factor);
1109
+ const netPins = extractNetPins(lines, matchedNetName);
1110
+ // Count resolved pads
1111
+ const compMap = new Map(components.map((c) => [c.refdes, c]));
1112
+ let resolvedPads = 0;
1113
+ for (const np of netPins) {
1114
+ const comp = compMap.get(np.refdes);
1115
+ if (!comp)
1116
+ continue;
1117
+ const pkg = packages.get(comp.packageRef);
1118
+ if (!pkg)
1119
+ continue;
1120
+ const pin = pkg.get(np.pin);
1121
+ if (pin && shapes.has(pin.shapeId))
1122
+ resolvedPads++;
1123
+ }
1124
+ const svg = generateSvg({ profile, shapes, packages, viaPadSizes, components, net, netPins, factor }, matchedNetName);
1125
+ const layersUsed = [...new Set(net.traces.map((t) => t.layer))].sort();
1126
+ return {
1127
+ netName: matchedNetName,
1128
+ units: "MICRON",
1129
+ svg,
1130
+ stats: {
1131
+ traceCount: net.traces.length,
1132
+ viaCount: net.vias.length,
1133
+ pinCount: netPins.length,
1134
+ resolvedPads,
1135
+ layersUsed,
1136
+ },
1137
+ };
1138
+ };
1139
+ //# sourceMappingURL=service.js.map