ecadlib 0.3.0__py3-none-any.whl

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 (199) hide show
  1. ecadlib/__init__.py +3 -0
  2. ecadlib/__main__.py +5 -0
  3. ecadlib/a365_sync.py +637 -0
  4. ecadlib/agent.py +1407 -0
  5. ecadlib/agent_queue.py +780 -0
  6. ecadlib/agent_watch.py +732 -0
  7. ecadlib/altium365.py +385 -0
  8. ecadlib/altium_dblib_sync.py +530 -0
  9. ecadlib/altium_export.py +209 -0
  10. ecadlib/altium_forms.py +768 -0
  11. ecadlib/altium_lib.py +1089 -0
  12. ecadlib/altium_menu.py +1751 -0
  13. ecadlib/altium_pkg.py +396 -0
  14. ecadlib/altium_project.py +1095 -0
  15. ecadlib/app.py +1458 -0
  16. ecadlib/approvals.py +395 -0
  17. ecadlib/assets/demo_bom.csv +8 -0
  18. ecadlib/assets/kicad_action_plugin.py +607 -0
  19. ecadlib/assets/kicad_reread.py +60 -0
  20. ecadlib/assets/pcblib_template.json.gz +0 -0
  21. ecadlib/bom_inputs.py +339 -0
  22. ecadlib/bom_triage.py +519 -0
  23. ecadlib/cad_model.py +528 -0
  24. ecadlib/cfb.py +171 -0
  25. ecadlib/cli.py +185 -0
  26. ecadlib/clihelp.py +149 -0
  27. ecadlib/cliutil.py +119 -0
  28. ecadlib/completion.py +173 -0
  29. ecadlib/costs.py +276 -0
  30. ecadlib/crosscheck.py +412 -0
  31. ecadlib/datasheet.py +2168 -0
  32. ecadlib/dedupe.py +572 -0
  33. ecadlib/detect.py +170 -0
  34. ecadlib/doctor.py +450 -0
  35. ecadlib/ds_route.py +541 -0
  36. ecadlib/eagle_export.py +1409 -0
  37. ecadlib/easy_setup.py +338 -0
  38. ecadlib/easyeda_convert.py +386 -0
  39. ecadlib/easyeda_pro_export.py +972 -0
  40. ecadlib/eval/__init__.py +0 -0
  41. ecadlib/eval/baselines/kicad-v1.json +404 -0
  42. ecadlib/eval/build_set.py +299 -0
  43. ecadlib/eval/cli.py +119 -0
  44. ecadlib/eval/gate.py +358 -0
  45. ecadlib/eval/golden/demo.txt +379 -0
  46. ecadlib/eval/golden/specs.txt +3120 -0
  47. ecadlib/eval/golden/testpack.txt +463 -0
  48. ecadlib/eval/golden.py +446 -0
  49. ecadlib/eval/replay/kicad-v1.json.gz +0 -0
  50. ecadlib/eval/runner.py +456 -0
  51. ecadlib/eval/score.py +179 -0
  52. ecadlib/eval/sets/kicad-v1.json +630 -0
  53. ecadlib/firstrun.py +400 -0
  54. ecadlib/footprint_adv.py +1006 -0
  55. ecadlib/footprint_bench.py +218 -0
  56. ecadlib/footprint_check.py +562 -0
  57. ecadlib/footprint_gen.py +582 -0
  58. ecadlib/footprint_rules.py +1092 -0
  59. ecadlib/forge.py +267 -0
  60. ecadlib/fp_share.py +95 -0
  61. ecadlib/fp_tidy.py +389 -0
  62. ecadlib/fsutil.py +62 -0
  63. ecadlib/house_style.py +1051 -0
  64. ecadlib/httpcache.py +124 -0
  65. ecadlib/hub.py +1490 -0
  66. ecadlib/hub_copy.py +161 -0
  67. ecadlib/hub_tools.py +79 -0
  68. ecadlib/kicad_dblib.py +257 -0
  69. ecadlib/kicad_export.py +837 -0
  70. ecadlib/kicad_import.py +913 -0
  71. ecadlib/kicad_index.py +160 -0
  72. ecadlib/kicad_menu.py +193 -0
  73. ecadlib/kicad_pcm.py +475 -0
  74. ecadlib/launcher.py +156 -0
  75. ecadlib/librarian.py +963 -0
  76. ecadlib/lifecycle.py +691 -0
  77. ecadlib/llm/__init__.py +276 -0
  78. ecadlib/llm/cli.py +70 -0
  79. ecadlib/llm/openai_compat.py +214 -0
  80. ecadlib/llm/profiles.py +245 -0
  81. ecadlib/llm/rails.py +570 -0
  82. ecadlib/llm/run.py +126 -0
  83. ecadlib/llm/sampling.py +49 -0
  84. ecadlib/mcp_server.py +723 -0
  85. ecadlib/messages.py +419 -0
  86. ecadlib/migrate.py +407 -0
  87. ecadlib/notify.py +329 -0
  88. ecadlib/package_schema.json +1238 -0
  89. ecadlib/package_specs/CP_EIA-3216-18_Kemet-A.json +27 -0
  90. ecadlib/package_specs/C_0402_1005Metric.json +22 -0
  91. ecadlib/package_specs/C_0805_2012Metric.json +22 -0
  92. ecadlib/package_specs/DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm.json +40 -0
  93. ecadlib/package_specs/D_SMA.json +27 -0
  94. ecadlib/package_specs/D_SMB.json +27 -0
  95. ecadlib/package_specs/D_SOD-123.json +43 -0
  96. ecadlib/package_specs/D_SOD-323.json +43 -0
  97. ecadlib/package_specs/LED_0603_1608Metric.json +23 -0
  98. ecadlib/package_specs/LED_0805_2012Metric.json +23 -0
  99. ecadlib/package_specs/LQFP-48_7x7mm_P0.5mm.json +31 -0
  100. ecadlib/package_specs/L_0805_2012Metric.json +22 -0
  101. ecadlib/package_specs/MSOP-8_3x3mm_P0.65mm.json +31 -0
  102. ecadlib/package_specs/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm.json +39 -0
  103. ecadlib/package_specs/QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm.json +37 -0
  104. ecadlib/package_specs/R_0402_1005Metric.json +22 -0
  105. ecadlib/package_specs/R_0603_1608Metric.json +22 -0
  106. ecadlib/package_specs/R_0805_2012Metric.json +22 -0
  107. ecadlib/package_specs/R_1206_3216Metric.json +22 -0
  108. ecadlib/package_specs/SOIC-16_3.9x9.9mm_P1.27mm.json +32 -0
  109. ecadlib/package_specs/SOIC-8_3.9x4.9mm_P1.27mm.json +32 -0
  110. ecadlib/package_specs/SOT-223-3_TabPin2.json +56 -0
  111. ecadlib/package_specs/SOT-23-5.json +57 -0
  112. ecadlib/package_specs/SOT-23-6.json +62 -0
  113. ecadlib/package_specs/SOT-23.json +48 -0
  114. ecadlib/package_specs/TSSOP-16_4.4x5mm_P0.65mm.json +31 -0
  115. ecadlib/package_specs/VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm.json +39 -0
  116. ecadlib/package_specs/advanced/BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  117. ecadlib/package_specs/advanced/BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD.json +26 -0
  118. ecadlib/package_specs/advanced/BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +29 -0
  119. ecadlib/package_specs/advanced/BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +23 -0
  120. ecadlib/package_specs/advanced/BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +26 -0
  121. ecadlib/package_specs/advanced/BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  122. ecadlib/package_specs/advanced/BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  123. ecadlib/package_specs/advanced/BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD.json +24 -0
  124. ecadlib/package_specs/advanced/BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD.json +32 -0
  125. ecadlib/package_specs/advanced/BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +30 -0
  126. ecadlib/package_specs/advanced/BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm.json +26 -0
  127. ecadlib/package_specs/advanced/DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias.json +60 -0
  128. ecadlib/package_specs/advanced/Example_Custom_LPad_Slot.json +24 -0
  129. ecadlib/package_specs/advanced/JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal.json +49 -0
  130. ecadlib/package_specs/advanced/JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical.json +33 -0
  131. ecadlib/package_specs/advanced/JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal.json +55 -0
  132. ecadlib/package_specs/advanced/JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal.json +49 -0
  133. ecadlib/package_specs/advanced/JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical.json +34 -0
  134. ecadlib/package_specs/advanced/JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical.json +34 -0
  135. ecadlib/package_specs/advanced/JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical.json +33 -0
  136. ecadlib/package_specs/advanced/LFBGA-144_10x10mm_Layout12x12_P0.8mm.json +23 -0
  137. ecadlib/package_specs/advanced/Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical.json +34 -0
  138. ecadlib/package_specs/advanced/Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical.json +54 -0
  139. ecadlib/package_specs/advanced/PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left.json +14 -0
  140. ecadlib/package_specs/advanced/PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left.json +14 -0
  141. ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical.json +12 -0
  142. ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left.json +14 -0
  143. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical.json +12 -0
  144. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left.json +25 -0
  145. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right.json +14 -0
  146. ecadlib/package_specs/advanced/PinHeader_1x10_P1.00mm_Vertical.json +15 -0
  147. ecadlib/package_specs/advanced/PinHeader_2x05_P1.00mm_Vertical_SMD.json +13 -0
  148. ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical.json +18 -0
  149. ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical_SMD.json +24 -0
  150. ecadlib/package_specs/advanced/PinHeader_2x05_P2.00mm_Vertical_SMD.json +13 -0
  151. ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical.json +12 -0
  152. ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical_SMD.json +24 -0
  153. ecadlib/package_specs/advanced/PinHeader_2x20_P2.54mm_Vertical.json +12 -0
  154. ecadlib/package_specs/advanced/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias.json +87 -0
  155. ecadlib/package_specs/advanced/UFBGA-132_7x7mm_Layout12x12_P0.5mm.json +43 -0
  156. ecadlib/package_specs/advanced/USB_C_Receptacle_HRO_TYPE-C-31-M-12.json +218 -0
  157. ecadlib/package_specs/standard.json.gz +0 -0
  158. ecadlib/parity.py +1204 -0
  159. ecadlib/parts_export.py +154 -0
  160. ecadlib/partview.py +215 -0
  161. ecadlib/plugins.py +231 -0
  162. ecadlib/providers.py +226 -0
  163. ecadlib/rebuild.py +471 -0
  164. ecadlib/release_check.py +437 -0
  165. ecadlib/reports.py +274 -0
  166. ecadlib/resolve.py +556 -0
  167. ecadlib/review_cli.py +292 -0
  168. ecadlib/rules.py +979 -0
  169. ecadlib/rules_infer.py +1205 -0
  170. ecadlib/sexpr.py +149 -0
  171. ecadlib/sheetsafe.py +65 -0
  172. ecadlib/sim_models.py +1087 -0
  173. ecadlib/sourcing.py +1161 -0
  174. ecadlib/stdparts.py +755 -0
  175. ecadlib/step_gen.py +929 -0
  176. ecadlib/symbol_layout.py +798 -0
  177. ecadlib/team.py +558 -0
  178. ecadlib/team_setup.py +288 -0
  179. ecadlib/term.py +420 -0
  180. ecadlib/term_views.py +64 -0
  181. ecadlib/testpack.py +334 -0
  182. ecadlib/ui/__init__.py +1 -0
  183. ecadlib/ui/__main__.py +3 -0
  184. ecadlib/ui/server.py +1029 -0
  185. ecadlib/ui/static/hairline.js +2 -0
  186. ecadlib/ui/static/index.html +2098 -0
  187. ecadlib/ui/static/setup.html +294 -0
  188. ecadlib/upkeep.py +717 -0
  189. ecadlib/upstream.py +573 -0
  190. ecadlib/userconfig.py +257 -0
  191. ecadlib/visual_check.py +1603 -0
  192. ecadlib/ws_status.py +203 -0
  193. ecadlib-0.3.0.dist-info/METADATA +2245 -0
  194. ecadlib-0.3.0.dist-info/RECORD +199 -0
  195. ecadlib-0.3.0.dist-info/WHEEL +5 -0
  196. ecadlib-0.3.0.dist-info/entry_points.txt +2 -0
  197. ecadlib-0.3.0.dist-info/licenses/LICENSE +21 -0
  198. ecadlib-0.3.0.dist-info/licenses/NOTICE +53 -0
  199. ecadlib-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,386 @@
1
+ """Minimal native EasyEDA (std) shape-string converter: footprint -> .kicad_mod text, symbol -> .kicad_sym text.
2
+
3
+ The KiCad text then goes through kicad_import (convert_footprint / convert_symbol) to Altium, and footprint_check
4
+ can read the .kicad_mod. Only what a land pattern and a pin map need is converted:
5
+ footprint: PAD (RECT / ELLIPSE / OVAL; POLYGON as its bounding rectangle), SMD and through-hole (round or slot,
6
+ plated or not), HOLE (NPTH), TRACK, RECT, CIRCLE (as a 16-gon) and circular ARC on silk / fab layers, the body
7
+ region (SOLIDREGION on layer 99) as a fab outline; a courtyard is drawn around pads and fab (EasyEDA
8
+ has no courtyard layer); TEXT, VIA and other regions (layer-100 lead shapes) are dropped with a note;
9
+ REF** goes on silk above the courtyard and the value on fab below it (1 mm, 0.15 mm stroke); a pin-1 dot
10
+ inside the body is moved beside pad 1
11
+ symbol: pins (number, name, electrical type, position, length), rectangles, polylines, polygons, circles;
12
+ arcs, paths, text and multi-unit `subparts` are not converted
13
+
14
+ FORMAT ASSUMPTIONS (easyeda.com is blocked here, so this was written against hand-made fixtures shaped after
15
+ easyeda2kicad 1.0.1's parameter dataclasses, not real payloads):
16
+ * every shape is "TYPE~f1~f2~..." with fields in easyeda2kicad's order:
17
+ PAD~shape~cx~cy~w~h~layer~net~number~hole_r~points~rot~id~hole_len~slot_outline~plated~locked
18
+ TRACK~width~layer~net~"x1 y1 x2 y2 ..."~id~locked HOLE~cx~cy~r~id~locked
19
+ CIRCLE~cx~cy~r~width~layer~id~locked RECT~x~y~w~h~layer~id~locked~width
20
+ symbol pin "P~show~type~spice_num~x~y~rot~id~locked^^dot^^path~color^^name seg^^num seg^^..." with the
21
+ KiCad pin number at segment 4 field 4 and the name at segment 3 field 4
22
+ R~x~y~rx~ry~w~h~... (rx/ry may be empty) PL / PG~"x1 y1 x2 y2 ..."~... C~cx~cy~r~...
23
+ * footprint units are 10 mil (x 0.254 mm), relative to dataStr.head x/y, y down like KiCad
24
+ * symbol units are 10 mil too, relative to head x/y, y down (flipped for KiCad, whose symbols are y up)
25
+ * layers: 1 top copper, 2 bottom, 11 multi-layer (through-hole), 3 top silk, 13 top assembly -> F.Fab,
26
+ 100 lead shapes -> F.Fab, 101 polarity marks -> silk; 99 (component shape) -> F.Fab here (easyeda2kicad
27
+ puts it on the courtyard, but a body shape is not an IPC courtyard)
28
+ * pin electrical type: 0 unspecified, 1 input, 2 output, 3 bidirectional, 4 power
29
+ * a pin's connection point is (x, y); its body end is the far end of its path; pad rotation is used as given
30
+ * a pad is plated unless its plated field says N / no / false / 0"""
31
+ import math, re
32
+
33
+ U = 0.254 # one EasyEDA unit (10 mil) in mm
34
+ SILK_CLEAR, SILK_HARD = 0.2, 0.1 # KLC F5.1 silk-to-pad gap; below SILK_HARD footprint_check errors
35
+ TEXT_H, TEXT_W, TEXT_GAP = 1.0, 0.15, 0.2 # reference / value text: height, stroke, gap to courtyard / silk (mm)
36
+ DOT_R = 0.5 # a silk circle this small (radius, mm) is a pin-1 dot
37
+ FP_LAYERS = {3: "F.SilkS", 101: "F.SilkS", 13: "F.Fab", 99: "F.Fab", 100: "F.Fab"}
38
+ # EasyEDA's library data asks anyone who reuses it to credit the source; every converted part carries this line
39
+ CREDIT = "From the JLCEDA/EasyEDA Official Library (https://easyeda.com, https://lceda.cn)"
40
+ PIN_TYPE = {"0": "unspecified", "1": "input", "2": "output", "3": "bidirectional", "4": "power_in"}
41
+
42
+
43
+ def _f(v, d=0.0):
44
+ try:
45
+ return float(v)
46
+ except (TypeError, ValueError):
47
+ return d
48
+
49
+
50
+ def _q(s):
51
+ return '"' + str(s).replace("\\", "\\\\").replace('"', '\\"') + '"'
52
+
53
+
54
+ def _n(v):
55
+ return f"{round(v, 4) + 0:g}"
56
+
57
+
58
+ def _nums(s):
59
+ return [float(v) for v in re.findall(r"-?\d+(?:\.\d+)?(?:[eE]-?\d+)?", s or "")]
60
+
61
+
62
+ def _no(v):
63
+ return str(v).strip().lower() in ("n", "no", "false", "0")
64
+
65
+
66
+ def _grid(v, up):
67
+ return (math.ceil if up else math.floor)(round(v * 100, 6)) / 100
68
+
69
+
70
+ # ---------------------------------------------------------------- footprint
71
+ def footprint_to_kicad(data, name=None, notes=None):
72
+ """packageDetail.dataStr ({head, shape}) -> .kicad_mod text (KiCad 7 format)."""
73
+ notes = [] if notes is None else notes
74
+ head = data.get("head") or {}
75
+ ox, oy = _f(head.get("x")), _f(head.get("y"))
76
+ name = name or (head.get("c_para") or {}).get("package") or "EasyEDA"
77
+ X, Y = (lambda x: (_f(x) - ox) * U), (lambda y: (_f(y) - oy) * U)
78
+ pads, lines, circles, arcs, dropped, extent, copper = [], [], [], [], {}, [], []
79
+ for s in data.get("shape") or []:
80
+ if not isinstance(s, str):
81
+ continue
82
+ f = s.split("~")
83
+ kind = f[0]
84
+ if kind == "PAD":
85
+ f += [""] * (17 - len(f))
86
+ shape, cx, cy, w, h, layer, num, hole_r = f[1], X(f[2]), Y(f[3]), _f(f[4]) * U, _f(f[5]) * U, f[6], f[8], _f(f[9]) * U
87
+ rot, hole_len = _f(f[11]) % 360, _f(f[13]) * U
88
+ if shape == "POLYGON":
89
+ pts = _nums(f[10])
90
+ xs, ys = [X(v) for v in pts[0::2]], [Y(v) for v in pts[1::2]]
91
+ if xs:
92
+ cx, cy, w, h, rot = (min(xs) + max(xs)) / 2, (min(ys) + max(ys)) / 2, max(xs) - min(xs), max(ys) - min(ys), 0
93
+ notes.append(f"pad {num}: polygon pad written as its bounding rectangle {w:.3f}x{h:.3f} mm")
94
+ kshape = {"RECT": "rect", "OVAL": "oval", "ELLIPSE": "circle" if abs(w - h) < 1e-6 else "oval"}.get(shape, "rect")
95
+ at = f"(at {_n(cx)} {_n(cy)}{' ' + _n(rot) if rot else ''})"
96
+ if hole_r > 0:
97
+ d = 2 * hole_r
98
+ drill = f"(drill {_n(d)})"
99
+ if hole_len > d + 1e-6: # slot along the pad's long side (easyeda2kicad's rule)
100
+ drill = f"(drill oval {_n(hole_len)} {_n(d)})" if w >= h else f"(drill oval {_n(d)} {_n(hole_len)})"
101
+ kind_ = "np_thru_hole" if _no(f[15]) else "thru_hole"
102
+ pads.append(f' (pad {_q(num)} {kind_} {kshape} {at} (size {_n(w)} {_n(h)}) {drill} (layers "*.Cu" "*.Mask"))')
103
+ else:
104
+ side = "B" if str(layer) == "2" else "F"
105
+ pads.append(f' (pad {_q(num)} smd {kshape} {at} (size {_n(w)} {_n(h)}) (layers "{side}.Cu" "{side}.Paste" "{side}.Mask"))')
106
+ extent += _rect(cx, cy, w, h, rot)
107
+ copper.append({"num": num, "x": cx, "y": cy, "w": w, "h": h, "rot": rot, "shape": kshape})
108
+ elif kind == "HOLE":
109
+ cx, cy, d = X(f[1]), Y(f[2]), 2 * _f(f[3]) * U
110
+ pads.append(f' (pad "" np_thru_hole circle (at {_n(cx)} {_n(cy)}) (size {_n(d)} {_n(d)}) (drill {_n(d)}) (layers "*.Cu" "*.Mask"))')
111
+ extent += [(cx - d / 2, cy - d / 2), (cx + d / 2, cy + d / 2)]
112
+ copper.append({"num": "", "x": cx, "y": cy, "w": d, "h": d, "rot": 0, "shape": "circle"})
113
+ elif kind in ("TRACK", "RECT", "CIRCLE"):
114
+ lid = int(_f(f[2] if kind == "TRACK" else f[5] if kind in ("RECT", "CIRCLE") else 0))
115
+ layer = FP_LAYERS.get(lid)
116
+ if not layer:
117
+ dropped[f"{kind} on layer {lid}"] = dropped.get(f"{kind} on layer {lid}", 0) + 1
118
+ continue
119
+ if kind == "TRACK":
120
+ w, p = _f(f[1]) * U, _nums(f[4])
121
+ pts = [(X(x), Y(y)) for x, y in zip(p[0::2], p[1::2])]
122
+ lines += [(a, b, w, layer) for a, b in zip(pts, pts[1:])]
123
+ elif kind == "RECT":
124
+ x, y, w, h, sw = X(f[1]), Y(f[2]), _f(f[3]) * U, _f(f[4]) * U, _f(f[8] if len(f) > 8 else 0) * U or 0.1
125
+ c = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
126
+ lines += [(a, b, sw, layer) for a, b in zip(c, c[1:] + c[:1])]
127
+ else:
128
+ circles.append((X(f[1]), Y(f[2]), _f(f[3]) * U, _f(f[4]) * U, layer))
129
+ elif kind == "ARC" and FP_LAYERS.get(int(_f(f[2]))) and _svg_arc(f[4]):
130
+ arcs.append((*_svg_arc(f[4], X, Y), _f(f[1]) * U, FP_LAYERS[int(_f(f[2]))]))
131
+ elif kind == "SOLIDREGION" and int(_f(f[1])) == 99 and _region(f[3]):
132
+ pts = [(X(x), Y(y)) for x, y in _region(f[3])] # the part body: its outline on F.Fab
133
+ lines += [(a, b, 0.1, "F.Fab") for a, b in zip(pts, pts[1:] + pts[:1])]
134
+ elif kind in ("ARC", "TEXT", "SOLIDREGION", "VIA", "SVGNODE"):
135
+ what = f"SOLIDREGION on layer {int(_f(f[1]))}" if kind == "SOLIDREGION" else kind
136
+ if kind != "SVGNODE":
137
+ dropped[what] = dropped.get(what, 0) + 1
138
+ else:
139
+ dropped[kind] = dropped.get(kind, 0) + 1
140
+ lines, circles = _clear_silk(lines, circles, copper, notes)
141
+ circles = _pin1_dot(circles, lines, copper, notes)
142
+ if dropped:
143
+ notes.append("EasyEDA shapes not converted: " + ", ".join(f"{k} x{v}" for k, v in sorted(dropped.items())))
144
+ if not pads:
145
+ raise ValueError("EasyEDA footprint has no pads")
146
+ extent += [pt for a, b, _, layer in lines if layer == "F.Fab" for pt in (a, b)]
147
+ extent += [(cx + s * r, cy + s * r) for cx, cy, r, _, layer in circles if layer == "F.Fab" for s in (-1, 1)]
148
+ xs, ys = [p[0] for p in extent], [p[1] for p in extent]
149
+ c = 0.25 # IPC-7351B nominal courtyard excess, on the 0.01 mm grid
150
+ crt = (_grid(min(xs) - c, False), _grid(min(ys) - c, False), _grid(max(xs) + c, True), _grid(max(ys) + c, True))
151
+ # Reference on silk just above everything (courtyard and silk), Value on fab just below: 1 mm text, 0.15 mm stroke,
152
+ # clear of the pads, as footprint_gen and KiCad's own library place them (EasyEDA's TEXT shapes are not converted)
153
+ silk_y = [v for a, b, _, layer in lines if layer == "F.SilkS" for v in (a[1], b[1])] + \
154
+ [cy + s * (r + w / 2) for cx, cy, r, w, layer in circles if layer == "F.SilkS" for s in (-1, 1)]
155
+ ref_y = round(min([crt[1]] + silk_y) - TEXT_GAP - TEXT_H / 2, 4)
156
+ val_y = round(max([crt[3]] + silk_y) + TEXT_GAP + TEXT_H / 2, 4)
157
+ cx_ = round((crt[0] + crt[2]) / 2, 4)
158
+ font = f"(effects (font (size {_n(TEXT_H)} {_n(TEXT_H)}) (thickness {_n(TEXT_W)})))"
159
+ out = [f"(footprint {_q(name)} (version 20221018) (generator ecadlib_easyeda) (layer \"F.Cu\")",
160
+ f" (descr {_q(CREDIT + '; converted by ecadlib.easyeda_convert; check against the datasheet')})",
161
+ " (attr smd)" if all(" smd " in p for p in pads) else " (attr through_hole)",
162
+ f' (fp_text reference "REF**" (at {_n(cx_)} {_n(ref_y)}) (layer "F.SilkS") {font})',
163
+ f' (fp_text value {_q(name)} (at {_n(cx_)} {_n(val_y)}) (layer "F.Fab") {font})']
164
+ # ${REFERENCE} on the assembly drawing (KLC F5.2), in the middle of the body, 0.5 .. 1.0 mm to fit it
165
+ bpts = [pt for a, b, _, layer in lines if layer in ("F.Fab", "F.SilkS") for pt in (a, b)] or [pt for pt in extent]
166
+ bw, bh = max(p[0] for p in bpts) - min(p[0] for p in bpts), max(p[1] for p in bpts) - min(p[1] for p in bpts)
167
+ fs = max(0.5, min(1.0, math.floor(100 * min(bw / (len("REF**") * 0.9), bh * 0.6)) / 100))
168
+ bc = (round((max(p[0] for p in bpts) + min(p[0] for p in bpts)) / 2, 4), round((max(p[1] for p in bpts) + min(p[1] for p in bpts)) / 2, 4))
169
+ out.append(f' (fp_text user "${{REFERENCE}}" (at {_n(bc[0])} {_n(bc[1])}) (layer "F.Fab") '
170
+ f'(effects (font (size {_n(fs)} {_n(fs)}) (thickness {_n(round(0.15 * fs, 3))}))))')
171
+ for (x1, y1), (x2, y2), w, layer in lines:
172
+ out.append(f' (fp_line (start {_n(x1)} {_n(y1)}) (end {_n(x2)} {_n(y2)}) (stroke (width {_n(w or 0.1)}) (type solid)) (layer "{layer}"))')
173
+ for cx, cy, r, w, layer in circles: # as a 16-gon: footprint_check reads fp_circle's centre from (start)
174
+ xy = " ".join(f"(xy {_n(cx + r * math.cos(k * math.pi / 8))} {_n(cy + r * math.sin(k * math.pi / 8))})" for k in range(16))
175
+ out.append(f' (fp_poly (pts {xy}) (stroke (width {_n(w or 0.1)}) (type solid)) (fill none) (layer "{layer}"))')
176
+ for a, m, b, w, layer in arcs:
177
+ out.append(f' (fp_arc (start {_n(a[0])} {_n(a[1])}) (mid {_n(m[0])} {_n(m[1])}) (end {_n(b[0])} {_n(b[1])}) '
178
+ f'(stroke (width {_n(w or 0.1)}) (type solid)) (layer "{layer}"))')
179
+ out.append(f' (fp_rect (start {_n(crt[0])} {_n(crt[1])}) '
180
+ f'(end {_n(crt[2])} {_n(crt[3])}) (stroke (width 0.05) (type solid)) (fill none) (layer "F.CrtYd"))')
181
+ notes.append("courtyard drawn by ecadlib: pads and fab outline + 0.25 mm (EasyEDA has no courtyard layer)")
182
+ return "\n".join(out + pads) + "\n)\n"
183
+
184
+
185
+ def _svg_arc(path, X=lambda v: v, Y=lambda v: v):
186
+ """'M x1,y1 A r,r rot large sweep x2,y2' (a circular SVG arc, y down) -> (start, mid, end) through X / Y, else None."""
187
+ v = _nums(path)
188
+ if not re.match(r"\s*M[^A]*A", path or "") or len(v) != 9 or abs(v[2] - v[3]) > 1e-6 * max(1.0, abs(v[2])):
189
+ return None
190
+ (x1, y1), r, large, sweep, (x2, y2) = v[0:2], v[2], v[5], v[6], v[7:9]
191
+ d = math.hypot(x2 - x1, y2 - y1) / 2
192
+ if d < 1e-9:
193
+ return None
194
+ h = math.sqrt(max(r * r - d * d, 0.0))
195
+ sign = 1 if bool(large) != bool(sweep) else -1 # SVG 1.1 F.6.5, no x-axis rotation for a circle
196
+ cx, cy = (x1 + x2) / 2 + sign * h / d * (y1 - y2) / 2, (y1 + y2) / 2 + sign * h / d * (x2 - x1) / 2
197
+ a1, a2, rr = math.atan2(y1 - cy, x1 - cx), math.atan2(y2 - cy, x2 - cx), max(r, d)
198
+ delta = (a2 - a1) % (2 * math.pi) if sweep else -((a1 - a2) % (2 * math.pi))
199
+ am = a1 + delta / 2
200
+ mid = (cx + rr * math.cos(am), cy + rr * math.sin(am))
201
+ return (X(x1), Y(y1)), (X(mid[0]), Y(mid[1])), (X(x2), Y(y2))
202
+
203
+
204
+ def _region(path):
205
+ """A SOLIDREGION path of straight segments 'M x y L x y ... Z' -> its points, else None (curves are not read)."""
206
+ if not path or re.search(r"[AaCcQqSsTtHhVv]", path):
207
+ return None
208
+ v = _nums(path)
209
+ pts = list(zip(v[0::2], v[1::2]))
210
+ return pts if len(pts) >= 3 and len(v) % 2 == 0 else None
211
+
212
+
213
+ def _clear_silk(lines, circles, copper, notes):
214
+ """Silk kept off the copper, as KiCad's own footprints and footprint_check want it (KLC F5.1): EasyEDA's library
215
+ draws silk outlines up to, or over, the pads (the fab cuts silk from copper when it makes the board). Silk lines are
216
+ cut back to SILK_CLEAR from every pad's outline (pieces shorter than 0.2 mm go); a silk circle (a pin-1 dot) is kept
217
+ whole unless it comes within SILK_HARD of a pad, and then left out. Returns (lines, circles)."""
218
+ from .footprint_adv import _clip_any, _pad_dist
219
+ if not copper:
220
+ return lines, circles
221
+ near = lambda pts, c: min(_pad_dist(x, y, p) for x, y in pts for p in copper) < c
222
+ out, cut = [], 0
223
+ for a, b, w, layer in lines:
224
+ sw = w or 0.1
225
+ c = SILK_CLEAR + sw / 2 + 0.005
226
+ L = math.hypot(b[0] - a[0], b[1] - a[1])
227
+ n = max(2, int(L / 0.01))
228
+ if layer != "F.SilkS" or not near([(a[0] + (b[0] - a[0]) * k / n, a[1] + (b[1] - a[1]) * k / n) for k in range(n + 1)], c):
229
+ out.append((a, b, w, layer))
230
+ continue
231
+ cut += 1
232
+ out += [(p, q, w, layer) for p, q in _clip_any(a, b, copper, c, min_len=min(0.2, L))]
233
+ keep, gone = [], 0
234
+ for cx, cy, r, w, layer in circles:
235
+ ring = [(cx + r * math.cos(k * math.pi / 16), cy + r * math.sin(k * math.pi / 16)) for k in range(32)] + [(cx, cy)]
236
+ if layer == "F.SilkS" and near(ring, SILK_HARD + (w or 0.1) / 2 + 0.005):
237
+ gone += 1
238
+ continue
239
+ keep.append((cx, cy, r, w, layer))
240
+ if cut:
241
+ notes.append(f"silk: {cut} line(s) cut back to {SILK_CLEAR} mm from the pads (the EasyEDA outline ran up to or over the copper)")
242
+ if gone:
243
+ notes.append(f"silk: {gone} circle(s) left out: within {SILK_HARD} mm of a pad")
244
+ return out, keep
245
+
246
+
247
+ def _pin1_dot(circles, lines, copper, notes):
248
+ """EasyEDA's pin-1 dot usually sits inside the body outline, where the part covers it once placed. A small silk
249
+ circle (a dot, r <= DOT_R) inside the body is moved next to pad 1, outside the body and the pads: the spot beside
250
+ pad 1 (left / right / above / below) closest to it that keeps SILK_CLEAR from every pad and from the body outline."""
251
+ p1 = next((p for p in copper if p["num"] == "1"), None)
252
+ if p1 is None or len([p for p in copper if p["num"]]) < 3:
253
+ return circles
254
+ outline = [pt for a, b, _, layer in lines if layer in ("F.SilkS", "F.Fab") for pt in (a, b)]
255
+ pads = [pt for p in copper for pt in _rect(p["x"], p["y"], p["w"], p["h"], p["rot"])]
256
+ pts = outline or pads
257
+ body = (min(x for x, _ in pts), min(y for _, y in pts), max(x for x, _ in pts), max(y for _, y in pts))
258
+ pb = (min(x for x, _ in pads), min(y for _, y in pads), max(x for x, _ in pads), max(y for _, y in pads))
259
+ from .footprint_adv import _pad_dist
260
+ out = []
261
+ for cx, cy, r, w, layer in circles:
262
+ inside = body[0] < cx < body[2] and body[1] < cy < body[3] or pb[0] < cx < pb[2] and pb[1] < cy < pb[3]
263
+ if layer != "F.SilkS" or r > DOT_R or not inside:
264
+ out.append((cx, cy, r, w, layer))
265
+ continue
266
+ ext = r + (w or 0.1) / 2
267
+ gap = SILK_CLEAR + ext
268
+ x1 = _rect(p1["x"], p1["y"], p1["w"], p1["h"], p1["rot"])
269
+ hx, hy = (max(x for x, _ in x1) - min(x for x, _ in x1)) / 2, (max(y for _, y in x1) - min(y for _, y in x1)) / 2
270
+ best = None
271
+ for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)):
272
+ x, y = p1["x"] + dx * (hx + gap), p1["y"] + dy * (hy + gap)
273
+ if dx and body[0] - gap < x < body[2] + gap and body[1] < y < body[3]: # still on the body: past its edge
274
+ x = body[0] - gap if dx < 0 else body[2] + gap
275
+ if dy and body[1] - gap < y < body[3] + gap and body[0] < x < body[2]:
276
+ y = body[1] - gap if dy < 0 else body[3] + gap
277
+ if min(_pad_dist(x, y, p) for p in copper) < gap - 1e-6:
278
+ continue
279
+ d = math.hypot(x - p1["x"], y - p1["y"])
280
+ if best is None or d < best[0]:
281
+ best = (d, round(x, 4), round(y, 4))
282
+ if best is None:
283
+ out.append((cx, cy, r, w, layer))
284
+ continue
285
+ out.append((best[1], best[2], r, w, layer))
286
+ notes.append(f"silk: pin-1 dot moved from ({cx:.2f}, {cy:.2f}) inside the body to ({best[1]:.2f}, {best[2]:.2f}) "
287
+ f"beside pad 1, {SILK_CLEAR} mm clear of the pads")
288
+ return out
289
+
290
+
291
+ def _rect(cx, cy, w, h, rot):
292
+ a = math.radians(rot)
293
+ return [(cx + x * math.cos(a) - y * math.sin(a), cy + x * math.sin(a) + y * math.cos(a))
294
+ for x, y in ((-w / 2, -h / 2), (w / 2, -h / 2), (w / 2, h / 2), (-w / 2, h / 2))]
295
+
296
+
297
+ # ---------------------------------------------------------------- symbol
298
+ def _path_ends(path):
299
+ """'M 370 300 h 10' / 'M370,300L380,300' / 'v -10' -> (start, end) in canvas units."""
300
+ toks = re.findall(r"[MLHVmlhv]|-?\d+(?:\.\d+)?", path or "")
301
+ x = y = 0.0
302
+ start, cmd, i = None, None, 0
303
+ while i < len(toks):
304
+ t = toks[i]
305
+ if t.isalpha():
306
+ cmd, i = t, i + 1
307
+ continue
308
+ if cmd in ("M", "L", "m", "l"):
309
+ dx, dy = float(toks[i]), float(toks[i + 1]); i += 2
310
+ x, y = (x + dx, y + dy) if cmd.islower() else (dx, dy)
311
+ elif cmd in ("H", "h"):
312
+ x = x + float(t) if cmd == "h" else float(t); i += 1
313
+ elif cmd in ("V", "v"):
314
+ y = y + float(t) if cmd == "v" else float(t); i += 1
315
+ else:
316
+ i += 1
317
+ if start is None:
318
+ start = (x, y)
319
+ return start, (x, y)
320
+
321
+
322
+ def symbol_to_kicad(data, name, footprint="", props=None, notes=None):
323
+ """dataStr ({head, shape}) of an EasyEDA symbol -> .kicad_sym library text with one symbol `name`."""
324
+ notes = [] if notes is None else notes
325
+ head = data.get("head") or {}
326
+ cp = head.get("c_para") or {}
327
+ ox, oy = _f(head.get("x")), _f(head.get("y"))
328
+ P = lambda x, y: (_n((_f(x) - ox) * U), _n(-(_f(y) - oy) * U))
329
+ body, pins, dropped = [], [], {}
330
+ for s in data.get("shape") or []:
331
+ if not isinstance(s, str):
332
+ continue
333
+ kind = s.split("~", 1)[0]
334
+ if kind == "P":
335
+ seg = [g.split("~") for g in s.split("^^")]
336
+ st = seg[0] + [""] * (9 - len(seg[0]))
337
+ num = (seg[4][4] if len(seg) > 4 and len(seg[4]) > 4 else "") or st[3]
338
+ nm = seg[3][4] if len(seg) > 3 and len(seg[3]) > 4 else ""
339
+ cx, cy = _f(st[4]), _f(st[5])
340
+ a, b = _path_ends(seg[2][0] if len(seg) > 2 else "")
341
+ if a is None:
342
+ notes.append(f"pin {num}: no pin path; length 10 assumed"); a = b = (cx - 10, cy)
343
+ far = max((a, b), key=lambda p: math.hypot(p[0] - cx, p[1] - cy))
344
+ dx, dy = far[0] - cx, -(far[1] - cy) # connection point -> body, KiCad y up
345
+ raw = math.degrees(math.atan2(dy, dx)) if (dx or dy) else 0.0
346
+ ang = round(raw / 90) % 4 * 90 # KiCad only takes 0, 90, 180, 270: anything else and
347
+ if abs((raw - ang + 180) % 360 - 180) > 0.5: # the whole library fails to load
348
+ notes.append(f"pin {num}: slanted pin path ({raw % 360:.0f} deg) drawn at {ang} deg")
349
+ length = (abs(dx) if ang in (0, 180) else abs(dy)) * U or 2.54
350
+ x, y = P(cx, cy)
351
+ pins.append(f' (pin {PIN_TYPE.get(st[2], "unspecified")} line (at {x} {y} {ang}) (length {_n(length)})'
352
+ f' (name {_q(nm or "~")} (effects (font (size 1.27 1.27)))) (number {_q(num)} (effects (font (size 1.27 1.27)))))')
353
+ elif kind == "R":
354
+ f = s.split("~") + [""] * 8
355
+ x, y, w, h = _f(f[1]), _f(f[2]), _f(f[5]), _f(f[6])
356
+ (x1, y1), (x2, y2) = P(x, y), P(x + w, y + h)
357
+ body.append(f" (rectangle (start {x1} {y1}) (end {x2} {y2}) (stroke (width 0.254) (type default)) (fill (type background)))")
358
+ elif kind in ("PL", "PG"):
359
+ f = s.split("~")
360
+ p = _nums(f[1] if len(f) > 1 else "")
361
+ pts = [P(x, y) for x, y in zip(p[0::2], p[1::2])]
362
+ if kind == "PG" and pts:
363
+ pts.append(pts[0])
364
+ if len(pts) >= 2:
365
+ xy = " ".join(f"(xy {x} {y})" for x, y in pts)
366
+ body.append(f" (polyline (pts {xy}) (stroke (width 0.254) (type default)) (fill (type {'background' if kind == 'PG' else 'none'})))")
367
+ elif kind in ("C", "E"):
368
+ f = s.split("~") + [""] * 5
369
+ rx, ry = _f(f[3]), _f(f[4]) if kind == "E" else _f(f[3])
370
+ if kind == "E" and abs(rx - ry) > 1e-6:
371
+ dropped["E (ellipse)"] = dropped.get("E (ellipse)", 0) + 1; continue
372
+ x, y = P(f[1], f[2])
373
+ body.append(f" (circle (center {x} {y}) (radius {_n(rx * U)}) (stroke (width 0.254) (type default)) (fill (type none)))")
374
+ else:
375
+ dropped[kind] = dropped.get(kind, 0) + 1
376
+ if dropped:
377
+ notes.append("EasyEDA symbol shapes not converted: " + ", ".join(f"{k} x{v}" for k, v in sorted(dropped.items())))
378
+ if not pins:
379
+ raise ValueError("EasyEDA symbol has no pins")
380
+ pr = {"Reference": (cp.get("pre") or "U?").rstrip("?") or "U", "Value": name, "Footprint": footprint,
381
+ "Datasheet": "~", "Description": "converted from EasyEDA by ecadlib.easyeda_convert", "Credit": CREDIT}
382
+ pr.update({k: v for k, v in (props or {}).items() if v})
383
+ out = ["(kicad_symbol_lib (version 20231120) (generator ecadlib_easyeda)", f" (symbol {_q(name)} (in_bom yes) (on_board yes)"]
384
+ out += [f" (property {_q(k)} {_q(v)} (at 0 0 0) (effects (font (size 1.27 1.27)){'' if k in ('Reference', 'Value') else ' hide'}))" for k, v in pr.items()]
385
+ out += [f" (symbol {_q(name + '_1_1')}"] + body + pins + [" )", " )", ")"]
386
+ return "\n".join(out) + "\n"