@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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +190 -0
- package/NOTICE +4 -0
- package/README.md +156 -0
- package/dist/cli/commands.d.ts +23 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +113 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/prompts.d.ts +10 -0
- package/dist/cli/prompts.d.ts.map +1 -0
- package/dist/cli/prompts.js +25 -0
- package/dist/cli/prompts.js.map +1 -0
- package/dist/cli/shell.d.ts +15 -0
- package/dist/cli/shell.d.ts.map +1 -0
- package/dist/cli/shell.js +66 -0
- package/dist/cli/shell.js.map +1 -0
- package/dist/cli/updater.d.ts +50 -0
- package/dist/cli/updater.d.ts.map +1 -0
- package/dist/cli/updater.js +305 -0
- package/dist/cli/updater.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +175 -0
- package/dist/server.js.map +1 -0
- package/dist/service.d.ts +13 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +1139 -0
- package/dist/service.js.map +1 -0
- package/dist/types.d.ts +128 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +25 -0
- package/dist/version.js.map +1 -0
- package/dist/wasm-embed.d.ts +3 -0
- package/dist/wasm-embed.d.ts.map +1 -0
- package/dist/wasm-embed.js +5 -0
- package/dist/wasm-embed.js.map +1 -0
- package/dist/xml-utils.d.ts +41 -0
- package/dist/xml-utils.d.ts.map +1 -0
- package/dist/xml-utils.js +81 -0
- package/dist/xml-utils.js.map +1 -0
- package/package.json +78 -0
package/dist/service.js
ADDED
|
@@ -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
|