@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +474 -0
  3. package/package.json +48 -0
  4. package/src/api/easyeda-community.ts +259 -0
  5. package/src/api/easyeda.ts +153 -0
  6. package/src/api/index.ts +7 -0
  7. package/src/api/jlc.ts +185 -0
  8. package/src/constants/design-rules.ts +119 -0
  9. package/src/constants/footprints.ts +68 -0
  10. package/src/constants/index.ts +7 -0
  11. package/src/constants/kicad.ts +147 -0
  12. package/src/converter/category-router.ts +638 -0
  13. package/src/converter/footprint-mapper.ts +236 -0
  14. package/src/converter/footprint.ts +949 -0
  15. package/src/converter/global-lib-table.ts +394 -0
  16. package/src/converter/index.ts +46 -0
  17. package/src/converter/lib-table.ts +181 -0
  18. package/src/converter/svg-arc.ts +179 -0
  19. package/src/converter/symbol-templates.ts +214 -0
  20. package/src/converter/symbol.ts +1682 -0
  21. package/src/converter/value-normalizer.ts +262 -0
  22. package/src/index.ts +25 -0
  23. package/src/parsers/easyeda-shapes.ts +628 -0
  24. package/src/parsers/http-client.ts +96 -0
  25. package/src/parsers/index.ts +38 -0
  26. package/src/parsers/utils.ts +29 -0
  27. package/src/services/component-service.ts +100 -0
  28. package/src/services/fix-service.ts +50 -0
  29. package/src/services/index.ts +9 -0
  30. package/src/services/library-service.ts +696 -0
  31. package/src/types/component.ts +61 -0
  32. package/src/types/easyeda-community.ts +78 -0
  33. package/src/types/easyeda.ts +356 -0
  34. package/src/types/index.ts +12 -0
  35. package/src/types/jlc.ts +84 -0
  36. package/src/types/kicad.ts +136 -0
  37. package/src/types/mcp.ts +77 -0
  38. package/src/types/project.ts +60 -0
  39. package/src/utils/conversion.ts +104 -0
  40. package/src/utils/file-system.ts +143 -0
  41. package/src/utils/index.ts +8 -0
  42. package/src/utils/logger.ts +96 -0
  43. package/src/utils/validation.ts +110 -0
  44. 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
+ }