@jlcpcb/core 0.1.0
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 +10 -0
- package/README.md +474 -0
- package/package.json +48 -0
- package/src/api/easyeda-community.ts +259 -0
- package/src/api/easyeda.ts +153 -0
- package/src/api/index.ts +7 -0
- package/src/api/jlc.ts +185 -0
- package/src/constants/design-rules.ts +119 -0
- package/src/constants/footprints.ts +68 -0
- package/src/constants/index.ts +7 -0
- package/src/constants/kicad.ts +147 -0
- package/src/converter/category-router.ts +638 -0
- package/src/converter/footprint-mapper.ts +236 -0
- package/src/converter/footprint.ts +949 -0
- package/src/converter/global-lib-table.ts +394 -0
- package/src/converter/index.ts +46 -0
- package/src/converter/lib-table.ts +181 -0
- package/src/converter/svg-arc.ts +179 -0
- package/src/converter/symbol-templates.ts +214 -0
- package/src/converter/symbol.ts +1682 -0
- package/src/converter/value-normalizer.ts +262 -0
- package/src/index.ts +25 -0
- package/src/parsers/easyeda-shapes.ts +628 -0
- package/src/parsers/http-client.ts +96 -0
- package/src/parsers/index.ts +38 -0
- package/src/parsers/utils.ts +29 -0
- package/src/services/component-service.ts +100 -0
- package/src/services/fix-service.ts +50 -0
- package/src/services/index.ts +9 -0
- package/src/services/library-service.ts +696 -0
- package/src/types/component.ts +61 -0
- package/src/types/easyeda-community.ts +78 -0
- package/src/types/easyeda.ts +356 -0
- package/src/types/index.ts +12 -0
- package/src/types/jlc.ts +84 -0
- package/src/types/kicad.ts +136 -0
- package/src/types/mcp.ts +77 -0
- package/src/types/project.ts +60 -0
- package/src/utils/conversion.ts +104 -0
- package/src/utils/file-system.ts +143 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +96 -0
- package/src/utils/validation.ts +110 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified EasyEDA shape parser
|
|
3
|
+
* Parses both symbol and footprint shapes from EasyEDA format
|
|
4
|
+
*
|
|
5
|
+
* Shape format reference (from easyeda2kicad):
|
|
6
|
+
* - All shapes use tilde (~) as field delimiter
|
|
7
|
+
* - Coordinates are in 10mil units (0.254mm per unit)
|
|
8
|
+
* - Symbol shapes have Y-axis flipped relative to footprint
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
// Symbol shape types
|
|
13
|
+
EasyEDAPin,
|
|
14
|
+
EasyEDASymbolRect,
|
|
15
|
+
EasyEDASymbolCircle,
|
|
16
|
+
EasyEDASymbolEllipse,
|
|
17
|
+
EasyEDASymbolArc,
|
|
18
|
+
EasyEDASymbolPolyline,
|
|
19
|
+
EasyEDASymbolPolygon,
|
|
20
|
+
EasyEDASymbolPath,
|
|
21
|
+
ParsedSymbolData,
|
|
22
|
+
// Footprint shape types
|
|
23
|
+
EasyEDAPad,
|
|
24
|
+
EasyEDATrack,
|
|
25
|
+
EasyEDAHole,
|
|
26
|
+
EasyEDACircle,
|
|
27
|
+
EasyEDAArc,
|
|
28
|
+
EasyEDARect,
|
|
29
|
+
EasyEDAVia,
|
|
30
|
+
EasyEDAText,
|
|
31
|
+
EasyEDASolidRegion,
|
|
32
|
+
ParsedFootprintData,
|
|
33
|
+
} from '../types/easyeda.js';
|
|
34
|
+
|
|
35
|
+
import { parseBool, safeParseFloat, safeParseInt } from './utils.js';
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Symbol Shape Parsers
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse EasyEDA symbol pin format
|
|
43
|
+
* Format: P~show~type~number~x~y~rotation~id~locked^^dotData^^pathData^^nameData^^numData^^dot^^clock
|
|
44
|
+
*
|
|
45
|
+
* Segments:
|
|
46
|
+
* [0] = P~show~type~number~x~y~rotation~id~locked (main settings)
|
|
47
|
+
* [1] = dot display data (visual dot, not inversion)
|
|
48
|
+
* [2] = SVG path for pin line (M x y h LENGTH or v LENGTH)
|
|
49
|
+
* [3] = name text data (1~x~y~rotation~name~fontSize~...)
|
|
50
|
+
* [4] = number text data
|
|
51
|
+
* [5] = inverted bubble indicator - "1" if inverted
|
|
52
|
+
* [6] = clock triangle indicator - non-empty if clock
|
|
53
|
+
*/
|
|
54
|
+
export function parseSymbolPin(pinData: string): EasyEDAPin | null {
|
|
55
|
+
try {
|
|
56
|
+
const segments = pinData.split('^^');
|
|
57
|
+
const settings = segments[0].split('~');
|
|
58
|
+
const nameSegment = segments[3]?.split('~') || [];
|
|
59
|
+
|
|
60
|
+
// Extract pin length from SVG path segment (segment 2)
|
|
61
|
+
// Path format: "M x y h LENGTH" (horizontal) or "M x y v LENGTH" (vertical)
|
|
62
|
+
let pinLength = 100; // default 100 EasyEDA units
|
|
63
|
+
if (segments[2]) {
|
|
64
|
+
const pathMatch = segments[2].match(/[hv]\s*(-?[\d.]+)/i);
|
|
65
|
+
if (pathMatch) {
|
|
66
|
+
pinLength = Math.abs(parseFloat(pathMatch[1]));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check for inverted (dot/bubble) indicator - segment 5
|
|
71
|
+
// Format: "is_displayed~x~y" where is_displayed=1 means bubble is shown
|
|
72
|
+
const dotFields = segments[5]?.split('~') || [];
|
|
73
|
+
const hasDot = dotFields[0] === '1';
|
|
74
|
+
|
|
75
|
+
// Check for clock indicator - segment 6
|
|
76
|
+
// Format: "is_displayed~path" where is_displayed=1 means clock triangle is shown
|
|
77
|
+
const clockFields = segments[6]?.split('~') || [];
|
|
78
|
+
const hasClock = clockFields[0] === '1';
|
|
79
|
+
|
|
80
|
+
// Extract rotation from settings
|
|
81
|
+
const rotation = safeParseFloat(settings[6]);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
number: settings[3] || '',
|
|
85
|
+
name: nameSegment[4] || '',
|
|
86
|
+
electricalType: settings[2] || '0',
|
|
87
|
+
x: safeParseFloat(settings[4]),
|
|
88
|
+
y: safeParseFloat(settings[5]),
|
|
89
|
+
rotation,
|
|
90
|
+
hasDot,
|
|
91
|
+
hasClock,
|
|
92
|
+
pinLength,
|
|
93
|
+
};
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse symbol Rectangle shape
|
|
101
|
+
* Format: R~x~y~rx~ry~width~height~strokeColor~strokeWidth~strokeStyle~fillColor~id~locked
|
|
102
|
+
*/
|
|
103
|
+
export function parseSymbolRect(data: string): EasyEDASymbolRect | null {
|
|
104
|
+
try {
|
|
105
|
+
const f = data.split('~');
|
|
106
|
+
return {
|
|
107
|
+
x: safeParseFloat(f[1]),
|
|
108
|
+
y: safeParseFloat(f[2]),
|
|
109
|
+
rx: safeParseFloat(f[3]), // corner radius X
|
|
110
|
+
ry: safeParseFloat(f[4]), // corner radius Y
|
|
111
|
+
width: safeParseFloat(f[5]),
|
|
112
|
+
height: safeParseFloat(f[6]),
|
|
113
|
+
strokeColor: f[7] || '#000000',
|
|
114
|
+
strokeWidth: safeParseFloat(f[8], 1),
|
|
115
|
+
fillColor: f[10] || 'none',
|
|
116
|
+
};
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Parse symbol Circle shape
|
|
124
|
+
* Format: C~cx~cy~radius~strokeColor~strokeWidth~strokeStyle~fillColor~id~locked
|
|
125
|
+
*/
|
|
126
|
+
export function parseSymbolCircle(data: string): EasyEDASymbolCircle | null {
|
|
127
|
+
try {
|
|
128
|
+
const f = data.split('~');
|
|
129
|
+
return {
|
|
130
|
+
cx: safeParseFloat(f[1]),
|
|
131
|
+
cy: safeParseFloat(f[2]),
|
|
132
|
+
radius: safeParseFloat(f[3]),
|
|
133
|
+
strokeColor: f[4] || '#000000',
|
|
134
|
+
strokeWidth: safeParseFloat(f[5], 1),
|
|
135
|
+
fillColor: f[7] || 'none',
|
|
136
|
+
};
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Parse symbol Ellipse shape
|
|
144
|
+
* Format: E~cx~cy~rx~ry~strokeColor~strokeWidth~strokeStyle~fillColor~id~locked
|
|
145
|
+
*/
|
|
146
|
+
export function parseSymbolEllipse(data: string): EasyEDASymbolEllipse | null {
|
|
147
|
+
try {
|
|
148
|
+
const f = data.split('~');
|
|
149
|
+
return {
|
|
150
|
+
cx: safeParseFloat(f[1]),
|
|
151
|
+
cy: safeParseFloat(f[2]),
|
|
152
|
+
radiusX: safeParseFloat(f[3]),
|
|
153
|
+
radiusY: safeParseFloat(f[4]),
|
|
154
|
+
strokeColor: f[5] || '#000000',
|
|
155
|
+
strokeWidth: safeParseFloat(f[6], 1),
|
|
156
|
+
fillColor: f[8] || 'none',
|
|
157
|
+
};
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parse symbol Arc shape (SVG path format)
|
|
165
|
+
* Format: A~path~strokeColor~strokeWidth~strokeStyle~fillColor~id~locked
|
|
166
|
+
*/
|
|
167
|
+
export function parseSymbolArc(data: string): EasyEDASymbolArc | null {
|
|
168
|
+
try {
|
|
169
|
+
const f = data.split('~');
|
|
170
|
+
return {
|
|
171
|
+
path: f[1] || '', // SVG arc path "M x1 y1 A rx ry rotation largeArc sweep x2 y2"
|
|
172
|
+
strokeColor: f[2] || '#000000',
|
|
173
|
+
strokeWidth: safeParseFloat(f[3], 1),
|
|
174
|
+
fillColor: f[5] || 'none',
|
|
175
|
+
};
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Parse symbol Polyline shape (open path)
|
|
183
|
+
* Format: PL~points~strokeColor~strokeWidth~strokeStyle~fillColor~id~locked
|
|
184
|
+
*/
|
|
185
|
+
export function parseSymbolPolyline(data: string): EasyEDASymbolPolyline | null {
|
|
186
|
+
try {
|
|
187
|
+
const f = data.split('~');
|
|
188
|
+
return {
|
|
189
|
+
points: f[1] || '', // Space-separated "x1 y1 x2 y2 ..."
|
|
190
|
+
strokeColor: f[2] || '#000000',
|
|
191
|
+
strokeWidth: safeParseFloat(f[3], 1),
|
|
192
|
+
fillColor: f[5] || 'none',
|
|
193
|
+
};
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Parse symbol Polygon shape (closed filled path)
|
|
201
|
+
* Format: PG~points~strokeColor~strokeWidth~strokeStyle~fillColor~id~locked
|
|
202
|
+
*/
|
|
203
|
+
export function parseSymbolPolygon(data: string): EasyEDASymbolPolygon | null {
|
|
204
|
+
try {
|
|
205
|
+
const f = data.split('~');
|
|
206
|
+
return {
|
|
207
|
+
points: f[1] || '', // Space-separated "x1 y1 x2 y2 ..."
|
|
208
|
+
strokeColor: f[2] || '#000000',
|
|
209
|
+
strokeWidth: safeParseFloat(f[3], 1),
|
|
210
|
+
fillColor: f[5] || 'none',
|
|
211
|
+
};
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Parse symbol SVG Path shape
|
|
219
|
+
* Format: PT~path~strokeColor~strokeWidth~strokeStyle~fillColor~id~locked
|
|
220
|
+
*/
|
|
221
|
+
export function parseSymbolPath(data: string): EasyEDASymbolPath | null {
|
|
222
|
+
try {
|
|
223
|
+
const f = data.split('~');
|
|
224
|
+
return {
|
|
225
|
+
path: f[1] || '', // SVG path commands (M/L/Z/C/A)
|
|
226
|
+
strokeColor: f[2] || '#000000',
|
|
227
|
+
strokeWidth: safeParseFloat(f[3], 1),
|
|
228
|
+
fillColor: f[5] || 'none',
|
|
229
|
+
};
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// =============================================================================
|
|
236
|
+
// Footprint Shape Parsers
|
|
237
|
+
// =============================================================================
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Parse PAD element - 18 fields
|
|
241
|
+
* Format: PAD~shape~cx~cy~width~height~layerId~net~number~holeRadius~points~rotation~id~holeLength~holePoint~isPlated~isLocked
|
|
242
|
+
*/
|
|
243
|
+
export function parsePad(data: string): EasyEDAPad | null {
|
|
244
|
+
try {
|
|
245
|
+
const f = data.split('~');
|
|
246
|
+
return {
|
|
247
|
+
shape: f[1] || 'RECT',
|
|
248
|
+
centerX: safeParseFloat(f[2]),
|
|
249
|
+
centerY: safeParseFloat(f[3]),
|
|
250
|
+
width: safeParseFloat(f[4]),
|
|
251
|
+
height: safeParseFloat(f[5]),
|
|
252
|
+
layerId: safeParseInt(f[6], 1),
|
|
253
|
+
net: f[7] || '',
|
|
254
|
+
number: f[8] || '',
|
|
255
|
+
holeRadius: safeParseFloat(f[9]),
|
|
256
|
+
points: f[10] || '',
|
|
257
|
+
rotation: safeParseFloat(f[11]),
|
|
258
|
+
id: f[12] || '',
|
|
259
|
+
holeLength: safeParseFloat(f[13]),
|
|
260
|
+
holePoint: f[14] || '',
|
|
261
|
+
isPlated: parseBool(f[15]),
|
|
262
|
+
isLocked: parseBool(f[16]),
|
|
263
|
+
};
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Parse TRACK element - silkscreen/fab lines
|
|
271
|
+
* Format: TRACK~strokeWidth~layerId~net~points~id~isLocked
|
|
272
|
+
*/
|
|
273
|
+
export function parseTrack(data: string): EasyEDATrack | null {
|
|
274
|
+
try {
|
|
275
|
+
const f = data.split('~');
|
|
276
|
+
return {
|
|
277
|
+
strokeWidth: safeParseFloat(f[1]),
|
|
278
|
+
layerId: safeParseInt(f[2], 1),
|
|
279
|
+
net: f[3] || '',
|
|
280
|
+
points: f[4] || '',
|
|
281
|
+
id: f[5] || '',
|
|
282
|
+
isLocked: parseBool(f[6]),
|
|
283
|
+
};
|
|
284
|
+
} catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Parse HOLE element - NPTH
|
|
291
|
+
* Format: HOLE~cx~cy~radius~id~isLocked
|
|
292
|
+
*/
|
|
293
|
+
export function parseHole(data: string): EasyEDAHole | null {
|
|
294
|
+
try {
|
|
295
|
+
const f = data.split('~');
|
|
296
|
+
return {
|
|
297
|
+
centerX: safeParseFloat(f[1]),
|
|
298
|
+
centerY: safeParseFloat(f[2]),
|
|
299
|
+
radius: safeParseFloat(f[3]),
|
|
300
|
+
id: f[4] || '',
|
|
301
|
+
isLocked: parseBool(f[5]),
|
|
302
|
+
};
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Parse CIRCLE element (footprint)
|
|
310
|
+
* Format: CIRCLE~cx~cy~radius~strokeWidth~layerId~id~isLocked
|
|
311
|
+
*/
|
|
312
|
+
export function parseCircle(data: string): EasyEDACircle | null {
|
|
313
|
+
try {
|
|
314
|
+
const f = data.split('~');
|
|
315
|
+
return {
|
|
316
|
+
cx: safeParseFloat(f[1]),
|
|
317
|
+
cy: safeParseFloat(f[2]),
|
|
318
|
+
radius: safeParseFloat(f[3]),
|
|
319
|
+
strokeWidth: safeParseFloat(f[4]),
|
|
320
|
+
layerId: safeParseInt(f[5], 1),
|
|
321
|
+
id: f[6] || '',
|
|
322
|
+
isLocked: parseBool(f[7]),
|
|
323
|
+
};
|
|
324
|
+
} catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Parse ARC element (footprint) with SVG path
|
|
331
|
+
* Format: ARC~strokeWidth~layerId~net~path~helperDots~id~isLocked
|
|
332
|
+
*/
|
|
333
|
+
export function parseArc(data: string): EasyEDAArc | null {
|
|
334
|
+
try {
|
|
335
|
+
const f = data.split('~');
|
|
336
|
+
return {
|
|
337
|
+
strokeWidth: safeParseFloat(f[1]),
|
|
338
|
+
layerId: safeParseInt(f[2], 1),
|
|
339
|
+
net: f[3] || '',
|
|
340
|
+
path: f[4] || '',
|
|
341
|
+
helperDots: f[5] || '',
|
|
342
|
+
id: f[6] || '',
|
|
343
|
+
isLocked: parseBool(f[7]),
|
|
344
|
+
};
|
|
345
|
+
} catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Parse RECT element (footprint)
|
|
352
|
+
* Format: RECT~x~y~width~height~strokeWidth~id~layerId~isLocked
|
|
353
|
+
*/
|
|
354
|
+
export function parseRect(data: string): EasyEDARect | null {
|
|
355
|
+
try {
|
|
356
|
+
const f = data.split('~');
|
|
357
|
+
return {
|
|
358
|
+
x: safeParseFloat(f[1]),
|
|
359
|
+
y: safeParseFloat(f[2]),
|
|
360
|
+
width: safeParseFloat(f[3]),
|
|
361
|
+
height: safeParseFloat(f[4]),
|
|
362
|
+
strokeWidth: safeParseFloat(f[5]),
|
|
363
|
+
id: f[6] || '',
|
|
364
|
+
layerId: safeParseInt(f[7], 1),
|
|
365
|
+
isLocked: parseBool(f[8]),
|
|
366
|
+
};
|
|
367
|
+
} catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Parse VIA element
|
|
374
|
+
* Format: VIA~cx~cy~diameter~net~radius~id~isLocked
|
|
375
|
+
*/
|
|
376
|
+
export function parseVia(data: string): EasyEDAVia | null {
|
|
377
|
+
try {
|
|
378
|
+
const f = data.split('~');
|
|
379
|
+
return {
|
|
380
|
+
centerX: safeParseFloat(f[1]),
|
|
381
|
+
centerY: safeParseFloat(f[2]),
|
|
382
|
+
diameter: safeParseFloat(f[3]),
|
|
383
|
+
net: f[4] || '',
|
|
384
|
+
radius: safeParseFloat(f[5]),
|
|
385
|
+
id: f[6] || '',
|
|
386
|
+
isLocked: parseBool(f[7]),
|
|
387
|
+
};
|
|
388
|
+
} catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Parse TEXT element
|
|
395
|
+
* Format: TEXT~type~cx~cy~strokeWidth~rotation~mirror~layerId~net~fontSize~text~textPath~isDisplayed~id~isLocked
|
|
396
|
+
*/
|
|
397
|
+
export function parseText(data: string): EasyEDAText | null {
|
|
398
|
+
try {
|
|
399
|
+
const f = data.split('~');
|
|
400
|
+
return {
|
|
401
|
+
type: f[1] || '',
|
|
402
|
+
centerX: safeParseFloat(f[2]),
|
|
403
|
+
centerY: safeParseFloat(f[3]),
|
|
404
|
+
strokeWidth: safeParseFloat(f[4]),
|
|
405
|
+
rotation: safeParseFloat(f[5]),
|
|
406
|
+
mirror: f[6] || '',
|
|
407
|
+
layerId: safeParseInt(f[7], 1),
|
|
408
|
+
net: f[8] || '',
|
|
409
|
+
fontSize: safeParseFloat(f[9]),
|
|
410
|
+
text: f[10] || '',
|
|
411
|
+
textPath: f[11] || '',
|
|
412
|
+
// Default to true if not specified - EasyEDA texts are displayed by default
|
|
413
|
+
isDisplayed: f[12] === undefined || f[12] === '' ? true : parseBool(f[12]),
|
|
414
|
+
id: f[13] || '',
|
|
415
|
+
isLocked: parseBool(f[14]),
|
|
416
|
+
};
|
|
417
|
+
} catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Parse SOLIDREGION element - filled polygon region
|
|
424
|
+
* Format: SOLIDREGION~layerId~~path~fillType~id~~~~
|
|
425
|
+
*/
|
|
426
|
+
export function parseSolidRegion(data: string): EasyEDASolidRegion | null {
|
|
427
|
+
try {
|
|
428
|
+
const f = data.split('~');
|
|
429
|
+
const path = f[3] || '';
|
|
430
|
+
// Skip empty paths
|
|
431
|
+
if (!path || path.length < 3) return null;
|
|
432
|
+
return {
|
|
433
|
+
layerId: safeParseInt(f[1], 1),
|
|
434
|
+
path,
|
|
435
|
+
fillType: f[4] || 'solid',
|
|
436
|
+
id: f[5] || '',
|
|
437
|
+
};
|
|
438
|
+
} catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// =============================================================================
|
|
444
|
+
// High-Level Dispatch Functions
|
|
445
|
+
// =============================================================================
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Parse all symbol shapes from raw shape strings
|
|
449
|
+
* Returns parsed data without origin - origin is added by client after parsing.
|
|
450
|
+
*/
|
|
451
|
+
export function parseSymbolShapes(shapes: string[]): ParsedSymbolData {
|
|
452
|
+
const pins: EasyEDAPin[] = [];
|
|
453
|
+
const rectangles: EasyEDASymbolRect[] = [];
|
|
454
|
+
const circles: EasyEDASymbolCircle[] = [];
|
|
455
|
+
const ellipses: EasyEDASymbolEllipse[] = [];
|
|
456
|
+
const arcs: EasyEDASymbolArc[] = [];
|
|
457
|
+
const polylines: EasyEDASymbolPolyline[] = [];
|
|
458
|
+
const polygons: EasyEDASymbolPolygon[] = [];
|
|
459
|
+
const paths: EasyEDASymbolPath[] = [];
|
|
460
|
+
|
|
461
|
+
for (const line of shapes) {
|
|
462
|
+
if (typeof line !== 'string') continue;
|
|
463
|
+
|
|
464
|
+
const designator = line.split('~')[0];
|
|
465
|
+
|
|
466
|
+
switch (designator) {
|
|
467
|
+
case 'P': {
|
|
468
|
+
const pin = parseSymbolPin(line);
|
|
469
|
+
if (pin) pins.push(pin);
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
case 'R': {
|
|
473
|
+
const rect = parseSymbolRect(line);
|
|
474
|
+
if (rect) rectangles.push(rect);
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
case 'C': {
|
|
478
|
+
const circle = parseSymbolCircle(line);
|
|
479
|
+
if (circle) circles.push(circle);
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
case 'E': {
|
|
483
|
+
const ellipse = parseSymbolEllipse(line);
|
|
484
|
+
if (ellipse) ellipses.push(ellipse);
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
case 'A': {
|
|
488
|
+
const arc = parseSymbolArc(line);
|
|
489
|
+
if (arc) arcs.push(arc);
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
case 'PL': {
|
|
493
|
+
const polyline = parseSymbolPolyline(line);
|
|
494
|
+
if (polyline) polylines.push(polyline);
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
case 'PG': {
|
|
498
|
+
const polygon = parseSymbolPolygon(line);
|
|
499
|
+
if (polygon) polygons.push(polygon);
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
case 'PT': {
|
|
503
|
+
const path = parseSymbolPath(line);
|
|
504
|
+
if (path) paths.push(path);
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case 'T':
|
|
508
|
+
// Text elements - skip (we have our own text placement)
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
pins,
|
|
515
|
+
rectangles,
|
|
516
|
+
circles,
|
|
517
|
+
ellipses,
|
|
518
|
+
arcs,
|
|
519
|
+
polylines,
|
|
520
|
+
polygons,
|
|
521
|
+
paths,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Parse all footprint shapes from raw shape strings
|
|
527
|
+
* Returns parsed data without origin - origin is added by client after parsing.
|
|
528
|
+
*/
|
|
529
|
+
export function parseFootprintShapes(shapes: string[]): ParsedFootprintData {
|
|
530
|
+
const pads: EasyEDAPad[] = [];
|
|
531
|
+
const tracks: EasyEDATrack[] = [];
|
|
532
|
+
const holes: EasyEDAHole[] = [];
|
|
533
|
+
const circles: EasyEDACircle[] = [];
|
|
534
|
+
const arcs: EasyEDAArc[] = [];
|
|
535
|
+
const rects: EasyEDARect[] = [];
|
|
536
|
+
const texts: EasyEDAText[] = [];
|
|
537
|
+
const vias: EasyEDAVia[] = [];
|
|
538
|
+
const solidRegions: EasyEDASolidRegion[] = [];
|
|
539
|
+
let model3d: { name: string; uuid: string } | undefined;
|
|
540
|
+
|
|
541
|
+
for (const line of shapes) {
|
|
542
|
+
if (typeof line !== 'string') continue;
|
|
543
|
+
|
|
544
|
+
const designator = line.split('~')[0];
|
|
545
|
+
|
|
546
|
+
switch (designator) {
|
|
547
|
+
case 'PAD': {
|
|
548
|
+
const pad = parsePad(line);
|
|
549
|
+
if (pad) pads.push(pad);
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
case 'TRACK': {
|
|
553
|
+
const track = parseTrack(line);
|
|
554
|
+
if (track) tracks.push(track);
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
case 'HOLE': {
|
|
558
|
+
const hole = parseHole(line);
|
|
559
|
+
if (hole) holes.push(hole);
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
case 'CIRCLE': {
|
|
563
|
+
const circle = parseCircle(line);
|
|
564
|
+
if (circle) circles.push(circle);
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
case 'ARC': {
|
|
568
|
+
const arc = parseArc(line);
|
|
569
|
+
if (arc) arcs.push(arc);
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
case 'RECT': {
|
|
573
|
+
const rect = parseRect(line);
|
|
574
|
+
if (rect) rects.push(rect);
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
case 'VIA': {
|
|
578
|
+
const via = parseVia(line);
|
|
579
|
+
if (via) vias.push(via);
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
case 'TEXT': {
|
|
583
|
+
const text = parseText(line);
|
|
584
|
+
if (text) texts.push(text);
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case 'SVGNODE': {
|
|
588
|
+
// Extract 3D model info
|
|
589
|
+
try {
|
|
590
|
+
const jsonStr = line.split('~')[1];
|
|
591
|
+
const svgData = JSON.parse(jsonStr);
|
|
592
|
+
if (svgData?.attrs?.uuid) {
|
|
593
|
+
model3d = {
|
|
594
|
+
name: svgData.attrs.title || '3D Model',
|
|
595
|
+
uuid: svgData.attrs.uuid,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
} catch {
|
|
599
|
+
// Ignore parse errors
|
|
600
|
+
}
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
case 'SOLIDREGION': {
|
|
604
|
+
const solidRegion = parseSolidRegion(line);
|
|
605
|
+
if (solidRegion) solidRegions.push(solidRegion);
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Determine type based on pads
|
|
612
|
+
const type = pads.some(p => p.holeRadius > 0) ? 'tht' : 'smd';
|
|
613
|
+
|
|
614
|
+
return {
|
|
615
|
+
name: 'Unknown', // Will be set by caller
|
|
616
|
+
type,
|
|
617
|
+
pads,
|
|
618
|
+
tracks,
|
|
619
|
+
holes,
|
|
620
|
+
circles,
|
|
621
|
+
arcs,
|
|
622
|
+
rects,
|
|
623
|
+
texts,
|
|
624
|
+
vias,
|
|
625
|
+
solidRegions,
|
|
626
|
+
model3d,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP client for EasyEDA API calls
|
|
3
|
+
* Used by both LCSC and Community API clients
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { createLogger } from '../utils/index.js';
|
|
8
|
+
|
|
9
|
+
const logger = createLogger('http-client');
|
|
10
|
+
|
|
11
|
+
export interface FetchOptions {
|
|
12
|
+
method?: 'GET' | 'POST';
|
|
13
|
+
body?: string;
|
|
14
|
+
contentType?: string;
|
|
15
|
+
binary?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fetch URL with curl fallback for reliability
|
|
20
|
+
* Falls back to curl when Node fetch fails (proxy issues, etc.)
|
|
21
|
+
*/
|
|
22
|
+
export async function fetchWithCurlFallback(
|
|
23
|
+
url: string,
|
|
24
|
+
options: FetchOptions = {}
|
|
25
|
+
): Promise<string | Buffer> {
|
|
26
|
+
const method = options.method || 'GET';
|
|
27
|
+
const headers: Record<string, string> = {
|
|
28
|
+
Accept: 'application/json',
|
|
29
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (options.contentType) {
|
|
33
|
+
headers['Content-Type'] = options.contentType;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Try native fetch first
|
|
37
|
+
try {
|
|
38
|
+
const fetchOptions: RequestInit = {
|
|
39
|
+
method,
|
|
40
|
+
headers,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (options.body) {
|
|
44
|
+
fetchOptions.body = options.body;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const response = await fetch(url, fetchOptions);
|
|
48
|
+
|
|
49
|
+
if (response.ok) {
|
|
50
|
+
if (options.binary) {
|
|
51
|
+
return Buffer.from(await response.arrayBuffer());
|
|
52
|
+
}
|
|
53
|
+
return await response.text();
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger.debug(`Native fetch failed, falling back to curl: ${error}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fallback to curl
|
|
60
|
+
try {
|
|
61
|
+
const curlArgs = ['curl', '-s'];
|
|
62
|
+
|
|
63
|
+
if (method === 'POST') {
|
|
64
|
+
curlArgs.push('-X', 'POST');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
curlArgs.push('-H', '"Accept: application/json"');
|
|
68
|
+
curlArgs.push(
|
|
69
|
+
'-H',
|
|
70
|
+
'"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"'
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (options.contentType) {
|
|
74
|
+
curlArgs.push('-H', `"Content-Type: ${options.contentType}"`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (options.body) {
|
|
78
|
+
curlArgs.push('-d', `'${options.body}'`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
curlArgs.push(`"${url}"`);
|
|
82
|
+
|
|
83
|
+
if (options.binary) {
|
|
84
|
+
const result = execSync(curlArgs.join(' '), { maxBuffer: 50 * 1024 * 1024 });
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result = execSync(curlArgs.join(' '), {
|
|
89
|
+
encoding: 'utf-8',
|
|
90
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
91
|
+
});
|
|
92
|
+
return result;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw new Error(`Both fetch and curl failed for URL: ${url}`);
|
|
95
|
+
}
|
|
96
|
+
}
|