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.
- ecadlib/__init__.py +3 -0
- ecadlib/__main__.py +5 -0
- ecadlib/a365_sync.py +637 -0
- ecadlib/agent.py +1407 -0
- ecadlib/agent_queue.py +780 -0
- ecadlib/agent_watch.py +732 -0
- ecadlib/altium365.py +385 -0
- ecadlib/altium_dblib_sync.py +530 -0
- ecadlib/altium_export.py +209 -0
- ecadlib/altium_forms.py +768 -0
- ecadlib/altium_lib.py +1089 -0
- ecadlib/altium_menu.py +1751 -0
- ecadlib/altium_pkg.py +396 -0
- ecadlib/altium_project.py +1095 -0
- ecadlib/app.py +1458 -0
- ecadlib/approvals.py +395 -0
- ecadlib/assets/demo_bom.csv +8 -0
- ecadlib/assets/kicad_action_plugin.py +607 -0
- ecadlib/assets/kicad_reread.py +60 -0
- ecadlib/assets/pcblib_template.json.gz +0 -0
- ecadlib/bom_inputs.py +339 -0
- ecadlib/bom_triage.py +519 -0
- ecadlib/cad_model.py +528 -0
- ecadlib/cfb.py +171 -0
- ecadlib/cli.py +185 -0
- ecadlib/clihelp.py +149 -0
- ecadlib/cliutil.py +119 -0
- ecadlib/completion.py +173 -0
- ecadlib/costs.py +276 -0
- ecadlib/crosscheck.py +412 -0
- ecadlib/datasheet.py +2168 -0
- ecadlib/dedupe.py +572 -0
- ecadlib/detect.py +170 -0
- ecadlib/doctor.py +450 -0
- ecadlib/ds_route.py +541 -0
- ecadlib/eagle_export.py +1409 -0
- ecadlib/easy_setup.py +338 -0
- ecadlib/easyeda_convert.py +386 -0
- ecadlib/easyeda_pro_export.py +972 -0
- ecadlib/eval/__init__.py +0 -0
- ecadlib/eval/baselines/kicad-v1.json +404 -0
- ecadlib/eval/build_set.py +299 -0
- ecadlib/eval/cli.py +119 -0
- ecadlib/eval/gate.py +358 -0
- ecadlib/eval/golden/demo.txt +379 -0
- ecadlib/eval/golden/specs.txt +3120 -0
- ecadlib/eval/golden/testpack.txt +463 -0
- ecadlib/eval/golden.py +446 -0
- ecadlib/eval/replay/kicad-v1.json.gz +0 -0
- ecadlib/eval/runner.py +456 -0
- ecadlib/eval/score.py +179 -0
- ecadlib/eval/sets/kicad-v1.json +630 -0
- ecadlib/firstrun.py +400 -0
- ecadlib/footprint_adv.py +1006 -0
- ecadlib/footprint_bench.py +218 -0
- ecadlib/footprint_check.py +562 -0
- ecadlib/footprint_gen.py +582 -0
- ecadlib/footprint_rules.py +1092 -0
- ecadlib/forge.py +267 -0
- ecadlib/fp_share.py +95 -0
- ecadlib/fp_tidy.py +389 -0
- ecadlib/fsutil.py +62 -0
- ecadlib/house_style.py +1051 -0
- ecadlib/httpcache.py +124 -0
- ecadlib/hub.py +1490 -0
- ecadlib/hub_copy.py +161 -0
- ecadlib/hub_tools.py +79 -0
- ecadlib/kicad_dblib.py +257 -0
- ecadlib/kicad_export.py +837 -0
- ecadlib/kicad_import.py +913 -0
- ecadlib/kicad_index.py +160 -0
- ecadlib/kicad_menu.py +193 -0
- ecadlib/kicad_pcm.py +475 -0
- ecadlib/launcher.py +156 -0
- ecadlib/librarian.py +963 -0
- ecadlib/lifecycle.py +691 -0
- ecadlib/llm/__init__.py +276 -0
- ecadlib/llm/cli.py +70 -0
- ecadlib/llm/openai_compat.py +214 -0
- ecadlib/llm/profiles.py +245 -0
- ecadlib/llm/rails.py +570 -0
- ecadlib/llm/run.py +126 -0
- ecadlib/llm/sampling.py +49 -0
- ecadlib/mcp_server.py +723 -0
- ecadlib/messages.py +419 -0
- ecadlib/migrate.py +407 -0
- ecadlib/notify.py +329 -0
- ecadlib/package_schema.json +1238 -0
- ecadlib/package_specs/CP_EIA-3216-18_Kemet-A.json +27 -0
- ecadlib/package_specs/C_0402_1005Metric.json +22 -0
- ecadlib/package_specs/C_0805_2012Metric.json +22 -0
- ecadlib/package_specs/DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm.json +40 -0
- ecadlib/package_specs/D_SMA.json +27 -0
- ecadlib/package_specs/D_SMB.json +27 -0
- ecadlib/package_specs/D_SOD-123.json +43 -0
- ecadlib/package_specs/D_SOD-323.json +43 -0
- ecadlib/package_specs/LED_0603_1608Metric.json +23 -0
- ecadlib/package_specs/LED_0805_2012Metric.json +23 -0
- ecadlib/package_specs/LQFP-48_7x7mm_P0.5mm.json +31 -0
- ecadlib/package_specs/L_0805_2012Metric.json +22 -0
- ecadlib/package_specs/MSOP-8_3x3mm_P0.65mm.json +31 -0
- ecadlib/package_specs/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm.json +39 -0
- ecadlib/package_specs/QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm.json +37 -0
- ecadlib/package_specs/R_0402_1005Metric.json +22 -0
- ecadlib/package_specs/R_0603_1608Metric.json +22 -0
- ecadlib/package_specs/R_0805_2012Metric.json +22 -0
- ecadlib/package_specs/R_1206_3216Metric.json +22 -0
- ecadlib/package_specs/SOIC-16_3.9x9.9mm_P1.27mm.json +32 -0
- ecadlib/package_specs/SOIC-8_3.9x4.9mm_P1.27mm.json +32 -0
- ecadlib/package_specs/SOT-223-3_TabPin2.json +56 -0
- ecadlib/package_specs/SOT-23-5.json +57 -0
- ecadlib/package_specs/SOT-23-6.json +62 -0
- ecadlib/package_specs/SOT-23.json +48 -0
- ecadlib/package_specs/TSSOP-16_4.4x5mm_P0.65mm.json +31 -0
- ecadlib/package_specs/VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm.json +39 -0
- ecadlib/package_specs/advanced/BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +29 -0
- ecadlib/package_specs/advanced/BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +23 -0
- ecadlib/package_specs/advanced/BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD.json +24 -0
- ecadlib/package_specs/advanced/BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD.json +32 -0
- ecadlib/package_specs/advanced/BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +30 -0
- ecadlib/package_specs/advanced/BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm.json +26 -0
- ecadlib/package_specs/advanced/DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias.json +60 -0
- ecadlib/package_specs/advanced/Example_Custom_LPad_Slot.json +24 -0
- ecadlib/package_specs/advanced/JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal.json +49 -0
- ecadlib/package_specs/advanced/JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical.json +33 -0
- ecadlib/package_specs/advanced/JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal.json +55 -0
- ecadlib/package_specs/advanced/JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal.json +49 -0
- ecadlib/package_specs/advanced/JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical.json +33 -0
- ecadlib/package_specs/advanced/LFBGA-144_10x10mm_Layout12x12_P0.8mm.json +23 -0
- ecadlib/package_specs/advanced/Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical.json +54 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left.json +25 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x10_P1.00mm_Vertical.json +15 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.00mm_Vertical_SMD.json +13 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical.json +18 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical_SMD.json +24 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.00mm_Vertical_SMD.json +13 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical_SMD.json +24 -0
- ecadlib/package_specs/advanced/PinHeader_2x20_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias.json +87 -0
- ecadlib/package_specs/advanced/UFBGA-132_7x7mm_Layout12x12_P0.5mm.json +43 -0
- ecadlib/package_specs/advanced/USB_C_Receptacle_HRO_TYPE-C-31-M-12.json +218 -0
- ecadlib/package_specs/standard.json.gz +0 -0
- ecadlib/parity.py +1204 -0
- ecadlib/parts_export.py +154 -0
- ecadlib/partview.py +215 -0
- ecadlib/plugins.py +231 -0
- ecadlib/providers.py +226 -0
- ecadlib/rebuild.py +471 -0
- ecadlib/release_check.py +437 -0
- ecadlib/reports.py +274 -0
- ecadlib/resolve.py +556 -0
- ecadlib/review_cli.py +292 -0
- ecadlib/rules.py +979 -0
- ecadlib/rules_infer.py +1205 -0
- ecadlib/sexpr.py +149 -0
- ecadlib/sheetsafe.py +65 -0
- ecadlib/sim_models.py +1087 -0
- ecadlib/sourcing.py +1161 -0
- ecadlib/stdparts.py +755 -0
- ecadlib/step_gen.py +929 -0
- ecadlib/symbol_layout.py +798 -0
- ecadlib/team.py +558 -0
- ecadlib/team_setup.py +288 -0
- ecadlib/term.py +420 -0
- ecadlib/term_views.py +64 -0
- ecadlib/testpack.py +334 -0
- ecadlib/ui/__init__.py +1 -0
- ecadlib/ui/__main__.py +3 -0
- ecadlib/ui/server.py +1029 -0
- ecadlib/ui/static/hairline.js +2 -0
- ecadlib/ui/static/index.html +2098 -0
- ecadlib/ui/static/setup.html +294 -0
- ecadlib/upkeep.py +717 -0
- ecadlib/upstream.py +573 -0
- ecadlib/userconfig.py +257 -0
- ecadlib/visual_check.py +1603 -0
- ecadlib/ws_status.py +203 -0
- ecadlib-0.3.0.dist-info/METADATA +2245 -0
- ecadlib-0.3.0.dist-info/RECORD +199 -0
- ecadlib-0.3.0.dist-info/WHEEL +5 -0
- ecadlib-0.3.0.dist-info/entry_points.txt +2 -0
- ecadlib-0.3.0.dist-info/licenses/LICENSE +21 -0
- ecadlib-0.3.0.dist-info/licenses/NOTICE +53 -0
- ecadlib-0.3.0.dist-info/top_level.txt +1 -0
ecadlib/cad_model.py
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"""KiCad footprints and symbols read into plain shapes, for the writers of other tools (eagle_export, easyeda_pro_export).
|
|
2
|
+
|
|
3
|
+
Both Eagle / Fusion Electronics and EasyEDA Pro draw with y UP, so everything here is millimetres, y up, angles
|
|
4
|
+
counter-clockwise in degrees (KiCad footprints are y down; KiCad symbols are already y up).
|
|
5
|
+
|
|
6
|
+
footprint(text) -> {"name", "descr", "pads": [...], "graphics": [...], "texts": {"reference": (x, y), "value": (x, y)},
|
|
7
|
+
"models": [path, ...], "notes": [...]}
|
|
8
|
+
pad {"num", "kind": smd | tht | npth | window, "side": F | B (smd / window), "x", "y", "rot", "w", "h",
|
|
9
|
+
"shape": circle | rect | oval | roundrect | custom, "rratio" (roundrect: radius / shorter side),
|
|
10
|
+
"drill": (dx, dy) in the pad frame or None, "offset": (ox, oy) copper centre - hole in the pad frame,
|
|
11
|
+
"paste", "mask" (bool), "mask_margin", "paste_margin" (mm or None = the board's rule),
|
|
12
|
+
"parts": custom pads' copper ([("poly", [(x, y), ...], outline width) | ("circle", (cx, cy), r)] in the footprint frame),
|
|
13
|
+
"layers": a window's layers (paste_f, mask_f, ...), "approx": [what could not be kept exactly]}
|
|
14
|
+
graphic {"layer": role, "kind": line | arc | circle | poly, "w": stroke width, "fill": bool,
|
|
15
|
+
line: "a", "b"; arc: "a" (start), "b" (end), "c" (centre), "sweep" (degrees, + = counter-clockwise);
|
|
16
|
+
circle: "c", "r"; poly: "pts" (closed)}
|
|
17
|
+
roles cu_f cu_b silk_f silk_b fab_f fab_b crtyd_f crtyd_b mask_f mask_b paste_f paste_b edge doc
|
|
18
|
+
|
|
19
|
+
symbol(tree) -> {"name", "props": {...}, "units": {n: {"pins": [...], "shapes": [...]}}, "unit_count",
|
|
20
|
+
"pin_names_hidden", "pin_numbers_hidden", "name_offset", "notes"}
|
|
21
|
+
unit 0 holds what KiCad draws in every unit; De Morgan alternates (body style 2) are left out (noted).
|
|
22
|
+
pin {"num", "name", "type" (KiCad electrical type), "shape" (KiCad graphic style), "x", "y" (connection point),
|
|
23
|
+
"angle" (0 = the pin runs right from its connection point to the body), "length", "hidden"}
|
|
24
|
+
shape {"kind": rect | poly | circle | arc | text, "w", "fill": none | outline | background, rect: "a", "b";
|
|
25
|
+
poly: "pts", "closed"; circle: "c", "r"; arc: "a", "b", "c", "sweep"; text: "at", "text", "size", "angle"}"""
|
|
26
|
+
import math, re
|
|
27
|
+
|
|
28
|
+
from . import sexpr
|
|
29
|
+
|
|
30
|
+
LAYER_ROLE = {"F.Cu": "cu_f", "B.Cu": "cu_b", "F.SilkS": "silk_f", "B.SilkS": "silk_b", "F.Silkscreen": "silk_f",
|
|
31
|
+
"B.Silkscreen": "silk_b", "F.Fab": "fab_f", "B.Fab": "fab_b", "F.CrtYd": "crtyd_f", "B.CrtYd": "crtyd_b",
|
|
32
|
+
"F.Courtyard": "crtyd_f", "B.Courtyard": "crtyd_b", "F.Mask": "mask_f", "B.Mask": "mask_b",
|
|
33
|
+
"F.Paste": "paste_f", "B.Paste": "paste_b", "Edge.Cuts": "edge", "Dwgs.User": "doc", "Cmts.User": "doc",
|
|
34
|
+
"User.Drawings": "doc", "User.Comments": "doc"}
|
|
35
|
+
DEFAULT_STROKE = 0.1524 # KiCad draws a symbol stroke of 0 with the 6 mil default
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _f(v):
|
|
39
|
+
return float(v)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def yup(x, y):
|
|
43
|
+
"""KiCad footprint point (y down) -> y up."""
|
|
44
|
+
return (_f(x), -_f(y) + 0.0)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def rot(u, v, a):
|
|
48
|
+
"""(u, v) turned counter-clockwise by a degrees (y up)."""
|
|
49
|
+
r = math.radians(a)
|
|
50
|
+
c, s = math.cos(r), math.sin(r)
|
|
51
|
+
return (u * c - v * s, u * s + v * c)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def arc3(s, m, e):
|
|
55
|
+
"""Arc through start, mid, end (y up) -> (centre, radius, sweep from start to end, + = counter-clockwise)."""
|
|
56
|
+
(ax, ay), (bx, by), (cx, cy) = s, m, e
|
|
57
|
+
d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))
|
|
58
|
+
if abs(d) < 1e-12:
|
|
59
|
+
raise ValueError("degenerate arc")
|
|
60
|
+
ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d
|
|
61
|
+
uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d
|
|
62
|
+
a0, am, a1 = (math.degrees(math.atan2(py - uy, px - ux)) for px, py in (s, m, e))
|
|
63
|
+
ccw = (bx - ax) * (cy - by) - (by - ay) * (cx - bx) > 0
|
|
64
|
+
sweep = (a1 - a0) % 360 if ccw else -((a0 - a1) % 360)
|
|
65
|
+
if abs(sweep) < 1e-9:
|
|
66
|
+
sweep = 360.0 if ccw else -360.0
|
|
67
|
+
return (ux, uy), math.hypot(ax - ux, ay - uy), sweep
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def arc_mid(a, c, sweep):
|
|
71
|
+
"""The middle point of an arc from a around c by sweep degrees."""
|
|
72
|
+
return tuple(p + q for p, q in zip(c, rot(a[0] - c[0], a[1] - c[1], sweep / 2)))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def arc_points(a, c, sweep, step=10.0):
|
|
76
|
+
"""Points along an arc (a included, end included), for shapes a format cannot draw as an arc."""
|
|
77
|
+
n = max(2, int(math.ceil(abs(sweep) / step)))
|
|
78
|
+
return [tuple(p + q for p, q in zip(c, rot(a[0] - c[0], a[1] - c[1], sweep * k / n))) for k in range(n + 1)]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _stroke(g):
|
|
82
|
+
w = sexpr.first(sexpr.first(g, "stroke") or [], "width") or sexpr.first(g, "width")
|
|
83
|
+
return _f(w[1]) if w else 0.0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _filled(g):
|
|
87
|
+
fl = sexpr.first(g, "fill")
|
|
88
|
+
if not fl or len(fl) < 2:
|
|
89
|
+
return g[0] in ("fp_poly", "gr_poly") and fl is None
|
|
90
|
+
v = fl[1] if isinstance(fl[1], str) else (sexpr.first(fl, "type") or [None, "none"])[1]
|
|
91
|
+
return v in ("yes", "solid", "outline", "background", "color")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _xy(node):
|
|
95
|
+
return (_f(node[1]), _f(node[2]))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def capsule(a, b, w, step=15.0):
|
|
99
|
+
"""A line of width w with round ends as one polygon (y up)."""
|
|
100
|
+
(ax, ay), (bx, by), r = a, b, w / 2
|
|
101
|
+
ang = math.degrees(math.atan2(by - ay, bx - ax))
|
|
102
|
+
pts = [(bx + r * math.cos(math.radians(ang + t)), by + r * math.sin(math.radians(ang + t)))
|
|
103
|
+
for t in [k * step - 90 for k in range(int(180 / step) + 1)]]
|
|
104
|
+
pts += [(ax + r * math.cos(math.radians(ang + t)), ay + r * math.sin(math.radians(ang + t)))
|
|
105
|
+
for t in [k * step + 90 for k in range(int(180 / step) + 1)]]
|
|
106
|
+
return pts
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def round_rect(cx, cy, w, h, r, a=0.0, step=3.0):
|
|
110
|
+
"""Outline of a w x h rectangle with corner radius r centred on (cx, cy), turned by a (y up)."""
|
|
111
|
+
r = max(0.0, min(r, w / 2, h / 2))
|
|
112
|
+
pts = []
|
|
113
|
+
for (sx, sy), start in (((1, 1), 0), ((-1, 1), 90), ((-1, -1), 180), ((1, -1), 270)):
|
|
114
|
+
ox, oy = sx * (w / 2 - r), sy * (h / 2 - r)
|
|
115
|
+
if r <= 1e-9:
|
|
116
|
+
pts.append((sx * w / 2, sy * h / 2))
|
|
117
|
+
continue
|
|
118
|
+
for k in range(int(90 / step) + 1):
|
|
119
|
+
t = math.radians(start + k * step)
|
|
120
|
+
pts.append((ox + r * math.cos(t), oy + r * math.sin(t)))
|
|
121
|
+
return [(cx + p[0], cy + p[1]) for p in (rot(u, v, a) for u, v in pts)]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------- footprints
|
|
125
|
+
def _graphic(g, layer, notes, frame=None):
|
|
126
|
+
"""One fp_line / fp_rect / fp_circle / fp_arc / fp_poly (or a custom pad's gr_*) -> a graphic dict (y up).
|
|
127
|
+
frame: (x, y, rot) of a pad, for primitives in the pad's frame."""
|
|
128
|
+
def P(node):
|
|
129
|
+
x, y = yup(*node[1:3])
|
|
130
|
+
if frame:
|
|
131
|
+
u, v = rot(x, y, frame[2])
|
|
132
|
+
return (frame[0] + u, frame[1] + v)
|
|
133
|
+
return (x, y)
|
|
134
|
+
kind, w, fill = g[0][3:], _stroke(g), _filled(g)
|
|
135
|
+
if kind == "line":
|
|
136
|
+
return {"layer": layer, "kind": "line", "a": P(sexpr.first(g, "start")), "b": P(sexpr.first(g, "end")), "w": w, "fill": False}
|
|
137
|
+
if kind == "rect":
|
|
138
|
+
(x1, y1), (x2, y2) = _xy(sexpr.first(g, "start")), _xy(sexpr.first(g, "end"))
|
|
139
|
+
pts = [P(["xy", x, y]) for x, y in ((x1, y1), (x2, y1), (x2, y2), (x1, y2))]
|
|
140
|
+
return {"layer": layer, "kind": "poly", "pts": pts, "w": w, "fill": fill}
|
|
141
|
+
if kind == "circle":
|
|
142
|
+
c, e = P(sexpr.first(g, "center")), P(sexpr.first(g, "end"))
|
|
143
|
+
return {"layer": layer, "kind": "circle", "c": c, "r": math.hypot(e[0] - c[0], e[1] - c[1]), "w": w, "fill": fill}
|
|
144
|
+
if kind == "arc":
|
|
145
|
+
if sexpr.first(g, "mid"):
|
|
146
|
+
s, m, e = (P(sexpr.first(g, k)) for k in ("start", "mid", "end"))
|
|
147
|
+
c, r, sweep = arc3(s, m, e)
|
|
148
|
+
else: # KiCad 5 / 6 arcs: (start centre) (end arc start) (angle, clockwise on screen)
|
|
149
|
+
c, s = P(sexpr.first(g, "start")), P(sexpr.first(g, "end"))
|
|
150
|
+
sweep = -_f(sexpr.first(g, "angle")[1])
|
|
151
|
+
e = tuple(p + q for p, q in zip(c, rot(s[0] - c[0], s[1] - c[1], sweep)))
|
|
152
|
+
return {"layer": layer, "kind": "arc", "a": s, "b": e, "c": c, "sweep": sweep, "w": w, "fill": False}
|
|
153
|
+
if kind == "poly":
|
|
154
|
+
pts = []
|
|
155
|
+
for p in (sexpr.first(g, "pts") or [])[1:]:
|
|
156
|
+
if p[0] == "xy":
|
|
157
|
+
pts.append(P(p))
|
|
158
|
+
elif p[0] == "arc": # KiCad 7+: an arc inside a polygon outline
|
|
159
|
+
s, m, e = (P(sexpr.first(p, k)) for k in ("start", "mid", "end"))
|
|
160
|
+
c, _, sweep = arc3(s, m, e)
|
|
161
|
+
pts += arc_points(s, c, sweep)
|
|
162
|
+
return {"layer": layer, "kind": "poly", "pts": pts, "w": w, "fill": fill}
|
|
163
|
+
if kind == "curve": # Bezier: drawn through its control points
|
|
164
|
+
pts = [P(p) for p in (sexpr.first(g, "pts") or [])[1:] if p[0] == "xy"]
|
|
165
|
+
notes.append("Bezier curve drawn as lines through its control points")
|
|
166
|
+
return {"layer": layer, "kind": "poly", "pts": pts, "w": w, "fill": False, "open": True}
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _custom_parts(p, x, y, a, notes):
|
|
171
|
+
"""A custom pad's primitives -> copper parts in the footprint frame (y up)."""
|
|
172
|
+
parts = []
|
|
173
|
+
for g in (sexpr.first(p, "primitives") or [])[1:]:
|
|
174
|
+
if not isinstance(g, list):
|
|
175
|
+
continue
|
|
176
|
+
d = _graphic(["fp_" + g[0][3:]] + g[1:], "cu", notes, (x, y, a))
|
|
177
|
+
if not d:
|
|
178
|
+
continue
|
|
179
|
+
w = d["w"]
|
|
180
|
+
if d["kind"] == "poly" and len(d["pts"]) >= 3:
|
|
181
|
+
parts.append(("poly", d["pts"], w)) # w: the outline width, which grows the shape by w / 2
|
|
182
|
+
elif d["kind"] == "circle":
|
|
183
|
+
if d["fill"]:
|
|
184
|
+
parts.append(("circle", d["c"], d["r"] + w / 2))
|
|
185
|
+
else: # a ring: its outer edge
|
|
186
|
+
parts.append(("circle", d["c"], d["r"] + w / 2))
|
|
187
|
+
notes.append("custom pad ring drawn filled")
|
|
188
|
+
elif d["kind"] == "line" and w > 0:
|
|
189
|
+
parts.append(("poly", capsule(d["a"], d["b"], w), 0.0))
|
|
190
|
+
elif d["kind"] == "arc" and w > 0:
|
|
191
|
+
pts = arc_points(d["a"], d["c"], d["sweep"], 7.5)
|
|
192
|
+
parts += [("poly", capsule(p1, p2, w), 0.0) for p1, p2 in zip(pts, pts[1:])]
|
|
193
|
+
return parts
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def footprint(text):
|
|
197
|
+
t = sexpr.parse(text) if isinstance(text, str) else text
|
|
198
|
+
notes = []
|
|
199
|
+
out = {"name": str(t[1]), "descr": str((sexpr.first(t, "descr") or [None, ""])[1]), "pads": [], "graphics": [],
|
|
200
|
+
"texts": {}, "models": [], "notes": notes, "attr": [str(a) for a in (sexpr.first(t, "attr") or [None])[1:]]}
|
|
201
|
+
fp_mask = sexpr.first(t, "solder_mask_margin")
|
|
202
|
+
fp_paste = sexpr.first(t, "solder_paste_margin")
|
|
203
|
+
for p in sexpr.find(t, "pad"):
|
|
204
|
+
num, kind, shape = str(p[1]), str(p[2]), str(p[3])
|
|
205
|
+
at, size = sexpr.first(p, "at"), sexpr.first(p, "size")
|
|
206
|
+
x, y = yup(at[1], at[2])
|
|
207
|
+
a = _f(at[3]) if len(at) > 3 else 0.0
|
|
208
|
+
w, h = _f(size[1]), _f(size[2])
|
|
209
|
+
layers = [str(l) for l in (sexpr.first(p, "layers") or ["layers"])[1:]]
|
|
210
|
+
cu = [l for l in layers if l.endswith(".Cu")]
|
|
211
|
+
pad = {"num": num, "x": x, "y": y, "rot": a % 360, "w": w, "h": h, "shape": shape, "rratio": 0.0, "drill": None,
|
|
212
|
+
"offset": (0.0, 0.0), "approx": [], "parts": [], "layers": []}
|
|
213
|
+
if kind in ("smd", "connect") and not cu:
|
|
214
|
+
pad["kind"] = "window"
|
|
215
|
+
for l in layers:
|
|
216
|
+
for side in ("F", "B"):
|
|
217
|
+
for what in ("Paste", "Mask"):
|
|
218
|
+
if l in (f"{side}.{what}", f"*.{what}", f"F&B.{what}"):
|
|
219
|
+
pad["layers"].append(f"{what.lower()}_{side.lower()}")
|
|
220
|
+
pad["layers"] = sorted(set(pad["layers"]))
|
|
221
|
+
else:
|
|
222
|
+
pad["kind"] = {"thru_hole": "tht", "np_thru_hole": "npth"}.get(kind, "smd")
|
|
223
|
+
pad["side"] = "B" if cu and all(l.startswith("B.") for l in cu) else "F"
|
|
224
|
+
pad["copper"] = bool(cu)
|
|
225
|
+
if shape == "roundrect":
|
|
226
|
+
rr = sexpr.first(p, "roundrect_rratio")
|
|
227
|
+
pad["rratio"] = min(0.5, _f(rr[1])) if rr else 0.0
|
|
228
|
+
if pad["rratio"] < 0.0025:
|
|
229
|
+
pad["shape"] = "rect"
|
|
230
|
+
if shape == "circle":
|
|
231
|
+
pad["h"] = pad["w"]
|
|
232
|
+
cr = sexpr.first(p, "chamfer_ratio")
|
|
233
|
+
if sexpr.first(p, "chamfer") and cr and _f(cr[1]) > 0:
|
|
234
|
+
pad["approx"].append("chamfered corners written square")
|
|
235
|
+
if shape == "trapezoid":
|
|
236
|
+
pad["shape"] = "rect"
|
|
237
|
+
pad["approx"].append("trapezoid written as a rectangle")
|
|
238
|
+
if shape == "custom":
|
|
239
|
+
pad["anchor"] = str((sexpr.first(sexpr.first(p, "options") or [], "anchor") or [None, "circle"])[1])
|
|
240
|
+
pad["parts"] = _custom_parts(p, x, y, a, pad["approx"])
|
|
241
|
+
d = sexpr.first(p, "drill")
|
|
242
|
+
if d:
|
|
243
|
+
vals = [_f(v) for v in d[1:] if isinstance(v, str) and v != "oval"]
|
|
244
|
+
if vals:
|
|
245
|
+
pad["drill"] = (vals[0], vals[1] if len(vals) > 1 else vals[0])
|
|
246
|
+
off = sexpr.first(d, "offset")
|
|
247
|
+
if off:
|
|
248
|
+
pad["offset"] = (_f(off[1]), -_f(off[2]) + 0.0)
|
|
249
|
+
if pad["kind"] == "smd" and pad["offset"] != (0.0, 0.0): # no hole: the offset only moves the copper
|
|
250
|
+
dx, dy = rot(*pad["offset"], a)
|
|
251
|
+
pad["x"], pad["y"], pad["offset"] = x + dx, y + dy, (0.0, 0.0)
|
|
252
|
+
pad["paste"] = any("Paste" in l for l in layers) and pad["kind"] in ("smd", "window")
|
|
253
|
+
pad["mask"] = any("Mask" in l for l in layers)
|
|
254
|
+
mm_ = sexpr.first(p, "solder_mask_margin") or fp_mask
|
|
255
|
+
pm = sexpr.first(p, "solder_paste_margin") or fp_paste
|
|
256
|
+
pad["mask_margin"] = _f(mm_[1]) if mm_ else None
|
|
257
|
+
pad["paste_margin"] = _f(pm[1]) if pm and pad["paste"] else None
|
|
258
|
+
if sexpr.first(p, "solder_paste_margin_ratio") and _f(sexpr.first(p, "solder_paste_margin_ratio")[1]):
|
|
259
|
+
pad["approx"].append("paste margin ratio not kept")
|
|
260
|
+
out["pads"].append(pad)
|
|
261
|
+
for kind in ("fp_line", "fp_rect", "fp_circle", "fp_arc", "fp_poly", "fp_curve"):
|
|
262
|
+
for g in sexpr.find(t, kind):
|
|
263
|
+
lay = str((sexpr.first(g, "layer") or [None, ""])[1])
|
|
264
|
+
role = LAYER_ROLE.get(lay)
|
|
265
|
+
if not role:
|
|
266
|
+
if lay:
|
|
267
|
+
notes.append(f"graphics on {lay} not written")
|
|
268
|
+
continue
|
|
269
|
+
try:
|
|
270
|
+
d = _graphic(g, role, notes)
|
|
271
|
+
except ValueError as e:
|
|
272
|
+
notes.append(f"{kind} on {lay} skipped: {e}")
|
|
273
|
+
continue
|
|
274
|
+
if d:
|
|
275
|
+
out["graphics"].append(d)
|
|
276
|
+
for f in list(sexpr.find(t, "fp_text")) + list(sexpr.find(t, "property")):
|
|
277
|
+
key = str(f[1]).lower()
|
|
278
|
+
at = sexpr.first(f, "at")
|
|
279
|
+
if key in ("reference", "value") and at:
|
|
280
|
+
out["texts"].setdefault(key, yup(at[1], at[2]))
|
|
281
|
+
if sexpr.find(t, "zone") and next(sexpr.find(t, "zone"), None):
|
|
282
|
+
notes.append("keep-out / copper zones not written")
|
|
283
|
+
out["models"] = [str(m[1]) for m in sexpr.find(t, "model")]
|
|
284
|
+
return out
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ---------------------------------------------------------------- symbols
|
|
288
|
+
def _hidden(node):
|
|
289
|
+
return "hide" in node[1:] or bool(sexpr.first(node, "hide") and (len(sexpr.first(node, "hide")) < 2 or sexpr.first(node, "hide")[1] == "yes"))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _sym_fill(g):
|
|
293
|
+
fl = sexpr.first(g, "fill")
|
|
294
|
+
return str((sexpr.first(fl, "type") or [None, "none"])[1]) if fl else "none"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _sym_shape(g, notes):
|
|
298
|
+
w = _stroke(g) or DEFAULT_STROKE
|
|
299
|
+
fill = _sym_fill(g)
|
|
300
|
+
if g[0] == "rectangle":
|
|
301
|
+
return {"kind": "rect", "a": _xy(sexpr.first(g, "start")), "b": _xy(sexpr.first(g, "end")), "w": w, "fill": fill}
|
|
302
|
+
if g[0] == "polyline":
|
|
303
|
+
pts = [_xy(p) for p in (sexpr.first(g, "pts") or [])[1:] if p[0] == "xy"]
|
|
304
|
+
return {"kind": "poly", "pts": pts, "closed": len(pts) > 2 and pts[0] == pts[-1], "w": w, "fill": fill}
|
|
305
|
+
if g[0] == "bezier":
|
|
306
|
+
pts = [_xy(p) for p in (sexpr.first(g, "pts") or [])[1:] if p[0] == "xy"]
|
|
307
|
+
notes.append("symbol Bezier drawn as lines through its control points")
|
|
308
|
+
return {"kind": "poly", "pts": pts, "closed": False, "w": w, "fill": "none"}
|
|
309
|
+
if g[0] == "circle":
|
|
310
|
+
return {"kind": "circle", "c": _xy(sexpr.first(g, "center")), "r": _f(sexpr.first(g, "radius")[1]), "w": w, "fill": fill}
|
|
311
|
+
if g[0] == "arc":
|
|
312
|
+
if sexpr.first(g, "mid"):
|
|
313
|
+
s, m, e = (_xy(sexpr.first(g, k)) for k in ("start", "mid", "end"))
|
|
314
|
+
c, r, sweep = arc3(s, m, e)
|
|
315
|
+
else: # KiCad 6: (radius (at) (length) (angles a0 a1)), counter-clockwise
|
|
316
|
+
rad = sexpr.first(g, "radius")
|
|
317
|
+
c, r = _xy(sexpr.first(rad, "at")), _f(sexpr.first(rad, "length")[1])
|
|
318
|
+
a0, a1 = (_f(v) for v in sexpr.first(rad, "angles")[1:3])
|
|
319
|
+
s = (c[0] + r * math.cos(math.radians(a0)), c[1] + r * math.sin(math.radians(a0)))
|
|
320
|
+
e = (c[0] + r * math.cos(math.radians(a1)), c[1] + r * math.sin(math.radians(a1)))
|
|
321
|
+
sweep = (a1 - a0) % 360 or 360.0
|
|
322
|
+
if sweep > 180: # KiCad 6 arcs are the short way round
|
|
323
|
+
sweep -= 360
|
|
324
|
+
return {"kind": "arc", "a": s, "b": e, "c": c, "sweep": sweep, "w": w, "fill": fill}
|
|
325
|
+
if g[0] == "text":
|
|
326
|
+
at = sexpr.first(g, "at")
|
|
327
|
+
size = sexpr.first(sexpr.first(sexpr.first(g, "effects") or [], "font") or [], "size")
|
|
328
|
+
return {"kind": "text", "at": (_f(at[1]), _f(at[2])), "angle": _f(at[3]) / (10 if _f(at[3]) >= 360 else 1) if len(at) > 3 else 0.0,
|
|
329
|
+
"text": str(g[1]), "size": _f(size[1]) if size else 1.27}
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def symbol(sym):
|
|
334
|
+
"""A KiCad symbol tree (one `symbol` node, `extends` already flattened, as kicad_export writes it)."""
|
|
335
|
+
notes = []
|
|
336
|
+
props = {str(p[1]): str(p[2]) for p in sexpr.find(sym, "property")}
|
|
337
|
+
pos = {}
|
|
338
|
+
for p in sexpr.find(sym, "property"):
|
|
339
|
+
at = sexpr.first(p, "at")
|
|
340
|
+
if at:
|
|
341
|
+
pos[str(p[1])] = (_f(at[1]), _f(at[2]), _f(at[3]) if len(at) > 3 else 0.0, _hidden(sexpr.first(p, "effects") or []) or _hidden(p))
|
|
342
|
+
pn, pnum = sexpr.first(sym, "pin_names"), sexpr.first(sym, "pin_numbers")
|
|
343
|
+
off = sexpr.first(pn or [], "offset")
|
|
344
|
+
out = {"name": str(sym[1]), "props": props, "prop_at": pos, "units": {}, "notes": notes,
|
|
345
|
+
"pin_names_hidden": bool(pn and _hidden(pn)), "pin_numbers_hidden": bool(pnum and _hidden(pnum)),
|
|
346
|
+
"name_offset": _f(off[1]) if off else 0.508, "power": bool(sexpr.first(sym, "power"))}
|
|
347
|
+
alt = False
|
|
348
|
+
for u in sexpr.find(sym, "symbol"):
|
|
349
|
+
m = re.search(r"_(\d+)_(\d+)$", str(u[1]))
|
|
350
|
+
unit, style = (int(m.group(1)), int(m.group(2))) if m else (1, 1)
|
|
351
|
+
if style > 1:
|
|
352
|
+
alt = True
|
|
353
|
+
continue
|
|
354
|
+
d = out["units"].setdefault(unit, {"pins": [], "shapes": []})
|
|
355
|
+
for g in u[1:]:
|
|
356
|
+
if not isinstance(g, list) or not g:
|
|
357
|
+
continue
|
|
358
|
+
if g[0] == "pin":
|
|
359
|
+
at = sexpr.first(g, "at")
|
|
360
|
+
ln = sexpr.first(g, "length")
|
|
361
|
+
nm, nu = sexpr.first(g, "name") or ["name", ""], sexpr.first(g, "number") or ["number", ""]
|
|
362
|
+
d["pins"].append({"num": str(nu[1]), "name": str(nm[1]), "type": str(g[1]), "shape": str(g[2]),
|
|
363
|
+
"x": _f(at[1]), "y": _f(at[2]), "angle": (_f(at[3]) if len(at) > 3 else 0.0) % 360,
|
|
364
|
+
"length": _f(ln[1]) if ln else 2.54, "hidden": _hidden(g)})
|
|
365
|
+
else:
|
|
366
|
+
try:
|
|
367
|
+
s = _sym_shape(g, notes)
|
|
368
|
+
except (ValueError, TypeError, IndexError) as e:
|
|
369
|
+
notes.append(f"symbol {g[0]} skipped: {e}")
|
|
370
|
+
continue
|
|
371
|
+
if s:
|
|
372
|
+
d["shapes"].append(s)
|
|
373
|
+
if alt:
|
|
374
|
+
notes.append("De Morgan alternate body style not written (the normal style is)")
|
|
375
|
+
out["unit_count"] = max([k for k in out["units"] if k > 0] or [1])
|
|
376
|
+
return out
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def pin_text(name, overbar):
|
|
380
|
+
"""KiCad pin name with ~{...} overbars -> the target's overbar spelling (overbar(text) wraps one span)."""
|
|
381
|
+
if name in ("~", ""):
|
|
382
|
+
return ""
|
|
383
|
+
if "~{" not in name: # KiCad 6 '~RESET~' / '~RESET'
|
|
384
|
+
name = re.sub(r"~([^~]*)~?", lambda m: "~{" + m.group(1) + "}" if m.group(1) else "", name)
|
|
385
|
+
return re.sub(r"~\{([^}]*)\}", lambda m: overbar(m.group(1)), name)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# ---------------------------------------------------------------- layer coverage
|
|
389
|
+
def grown(pts, d):
|
|
390
|
+
"""A polygon grown by d on every side, as a filled shape with an outline of width 2d is (round corners):
|
|
391
|
+
[(point, sweep)] where sweep is the arc from this point to the next (+ = counter-clockwise) or None for a line."""
|
|
392
|
+
if d <= 1e-12:
|
|
393
|
+
return [(p, None) for p in pts]
|
|
394
|
+
q = [p for i, p in enumerate(pts) if math.hypot(p[0] - pts[i - 1][0], p[1] - pts[i - 1][1]) > 1e-9]
|
|
395
|
+
if sum(a[0] * b[1] - b[0] * a[1] for a, b in zip(q, q[1:] + q[:1])) < 0:
|
|
396
|
+
q = q[::-1] # counter-clockwise: the outside is on the right of each edge
|
|
397
|
+
n = len(q)
|
|
398
|
+
normal = []
|
|
399
|
+
for i in range(n):
|
|
400
|
+
(x1, y1), (x2, y2) = q[i], q[(i + 1) % n]
|
|
401
|
+
L = math.hypot(x2 - x1, y2 - y1)
|
|
402
|
+
normal.append(((y2 - y1) / L, -(x2 - x1) / L))
|
|
403
|
+
out = []
|
|
404
|
+
for i in range(n):
|
|
405
|
+
a, b = normal[i - 1], normal[i]
|
|
406
|
+
p = q[i]
|
|
407
|
+
turn = math.degrees(math.atan2(a[0] * b[1] - a[1] * b[0], a[0] * b[0] + a[1] * b[1]))
|
|
408
|
+
if turn > 1e-6: # convex corner: an arc round the vertex
|
|
409
|
+
out.append(((p[0] + a[0] * d, p[1] + a[1] * d), turn))
|
|
410
|
+
out.append(((p[0] + b[0] * d, p[1] + b[1] * d), None))
|
|
411
|
+
else: # straight on or concave: where the two offset edges meet
|
|
412
|
+
k = d / max(1e-9, math.cos(math.radians(turn) / 2))
|
|
413
|
+
bx, by = a[0] + b[0], a[1] + b[1]
|
|
414
|
+
bl = math.hypot(bx, by) or 1.0
|
|
415
|
+
out.append(((p[0] + bx / bl * k, p[1] + by / bl * k), None))
|
|
416
|
+
return out
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def grown_points(pts, d, step=5.0):
|
|
420
|
+
seg = grown(pts, d)
|
|
421
|
+
out = []
|
|
422
|
+
for i, (p, sweep) in enumerate(seg):
|
|
423
|
+
if sweep:
|
|
424
|
+
nxt = seg[(i + 1) % len(seg)][0]
|
|
425
|
+
c = _arc_c(p, nxt, sweep)
|
|
426
|
+
out += arc_points(p, c, sweep, step)[:-1]
|
|
427
|
+
else:
|
|
428
|
+
out.append(p)
|
|
429
|
+
return out
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _arc_c(a, b, sweep):
|
|
433
|
+
dx, dy = b[0] - a[0], b[1] - a[1]
|
|
434
|
+
dd = math.hypot(dx, dy)
|
|
435
|
+
h = dd / 2 / math.tan(math.radians(sweep) / 2)
|
|
436
|
+
return (a[0] + b[0]) / 2 - dy / dd * h, (a[1] + b[1]) / 2 + dx / dd * h
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def disc(c, r, step=3.0):
|
|
440
|
+
k = max(8, int(round(360 / step)))
|
|
441
|
+
return [(c[0] + r * math.cos(2 * math.pi * i / k), c[1] + r * math.sin(2 * math.pi * i / k)) for i in range(k)]
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def pad_polys(p, grow=0.0):
|
|
445
|
+
"""A cad_model pad's copper (custom: anchor and parts) grown by `grow`, as polygons (y up)."""
|
|
446
|
+
x, y, w, h, a = p["x"], p["y"], p["w"], p["h"], p["rot"]
|
|
447
|
+
if p["kind"] == "tht" and p.get("offset") != (0.0, 0.0):
|
|
448
|
+
dx, dy = rot(*p["offset"], a)
|
|
449
|
+
x, y = x + dx, y + dy
|
|
450
|
+
if p["shape"] == "custom":
|
|
451
|
+
out = [disc((x, y), w / 2 + grow)] if p.get("anchor", "circle") == "circle" else [round_rect(x, y, w + 2 * grow, h + 2 * grow, grow, a)]
|
|
452
|
+
for part in p.get("parts", []):
|
|
453
|
+
if part[0] == "circle":
|
|
454
|
+
out.append(disc(part[1], part[2] + grow))
|
|
455
|
+
else:
|
|
456
|
+
d = (part[2] / 2 if len(part) > 2 else 0.0) + grow
|
|
457
|
+
out.append(grown_points(part[1], d) if d > 0 else part[1])
|
|
458
|
+
return out
|
|
459
|
+
r = {"circle": min(w, h) / 2, "oval": min(w, h) / 2, "roundrect": p.get("rratio", 0.0) * min(w, h)}.get(p["shape"], 0.0)
|
|
460
|
+
if p["kind"] == "npth" and p.get("drill"):
|
|
461
|
+
w = h = max(w, h)
|
|
462
|
+
r = w / 2
|
|
463
|
+
return [round_rect(x, y, w + 2 * grow, h + 2 * grow, r + grow if r or grow else 0.0, a)]
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def openings(fp, frame=0.0):
|
|
467
|
+
"""Mask and paste openings of a cad_model footprint per layer {"mask_F": [polys], ...}: every pad that opens the
|
|
468
|
+
layer (grown by its margin; mask openings with the board rule grown by `frame`) and every window. A pad-for-pad
|
|
469
|
+
independent picture of the layers, for checking a writer that splits openings between pads differently."""
|
|
470
|
+
out = {k: [] for k in ("mask_F", "mask_B", "paste_F", "paste_B")}
|
|
471
|
+
for p in fp["pads"]:
|
|
472
|
+
if p["kind"] == "window":
|
|
473
|
+
for lay in p["layers"]:
|
|
474
|
+
kind, side = lay.split("_")
|
|
475
|
+
out[f"{kind}_{side.upper()}"] += pad_polys(p)
|
|
476
|
+
continue
|
|
477
|
+
sides = ("F", "B") if p["kind"] in ("tht", "npth") else (p["side"],)
|
|
478
|
+
if p.get("mask"):
|
|
479
|
+
m = p.get("mask_margin")
|
|
480
|
+
for s in sides:
|
|
481
|
+
out[f"mask_{s}"] += pad_polys(p, frame if m is None else m)
|
|
482
|
+
if p.get("paste") and p["kind"] == "smd":
|
|
483
|
+
out[f"paste_{p['side']}"] += pad_polys(p, p.get("paste_margin") or 0.0)
|
|
484
|
+
for g in fp["graphics"]: # filled drawings on mask / paste open them too
|
|
485
|
+
lay = {"mask_f": "mask_F", "mask_b": "mask_B", "paste_f": "paste_F", "paste_b": "paste_B"}.get(g["layer"])
|
|
486
|
+
if lay and g.get("fill"):
|
|
487
|
+
if g["kind"] == "poly":
|
|
488
|
+
out[lay].append(grown_points(g["pts"], g["w"] / 2) if g["w"] > 0 else g["pts"])
|
|
489
|
+
elif g["kind"] == "circle":
|
|
490
|
+
out[lay].append(disc(g["c"], g["r"] + g["w"] / 2))
|
|
491
|
+
return out
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def raster(polys, res=0.02):
|
|
495
|
+
"""Cells (i, j) of a res grid covered by the union of the polygons (even-odd per polygon), as a set."""
|
|
496
|
+
cells = set()
|
|
497
|
+
for pts in polys:
|
|
498
|
+
if len(pts) < 3:
|
|
499
|
+
continue
|
|
500
|
+
ys = [p[1] for p in pts]
|
|
501
|
+
j0, j1 = math.floor(min(ys) / res), math.ceil(max(ys) / res)
|
|
502
|
+
n = len(pts)
|
|
503
|
+
for j in range(j0, j1 + 1):
|
|
504
|
+
yc = (j + 0.5) * res
|
|
505
|
+
xs = []
|
|
506
|
+
for k in range(n):
|
|
507
|
+
(x1, y1), (x2, y2) = pts[k], pts[(k + 1) % n]
|
|
508
|
+
if (y1 <= yc < y2) or (y2 <= yc < y1):
|
|
509
|
+
xs.append(x1 + (yc - y1) * (x2 - x1) / (y2 - y1))
|
|
510
|
+
xs.sort()
|
|
511
|
+
for a, b in zip(xs[::2], xs[1::2]):
|
|
512
|
+
for i in range(math.ceil(a / res - 0.5), math.floor(b / res - 0.5) + 1):
|
|
513
|
+
cells.add((i, j))
|
|
514
|
+
return cells
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def same_cover(a, b, res=0.02, rel=0.03, floor=0.02):
|
|
518
|
+
"""Whether two polygon sets cover the same area: the area in one and not the other is under rel of the
|
|
519
|
+
larger area (and over `floor` mm2). -> (same, differing mm2, total mm2)."""
|
|
520
|
+
# cells are only kept where a polygon is, so the grid is capped by the polygons' own area (~360000 cells), not
|
|
521
|
+
# by their spread: two small holes far apart keep a fine grid
|
|
522
|
+
area = sum((max(p[0] for p in q) - min(p[0] for p in q)) * (max(p[1] for p in q) - min(p[1] for p in q))
|
|
523
|
+
for q in list(a) + list(b) if q)
|
|
524
|
+
res = max(res, math.sqrt(area / 360000))
|
|
525
|
+
ca, cb = raster(a, res), raster(b, res)
|
|
526
|
+
diff = len(ca ^ cb) * res * res
|
|
527
|
+
total = max(len(ca), len(cb)) * res * res
|
|
528
|
+
return diff <= max(floor, rel * total), round(diff, 4), round(total, 4)
|