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/fp_tidy.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""Tidy a converted footprint (EasyEDA today) to the rule book, the way KiCad's own footprints are drawn.
|
|
2
|
+
|
|
3
|
+
tidy(kicad_mod_text, notes) -> kicad_mod_text
|
|
4
|
+
|
|
5
|
+
EasyEDA's library footprints are drawn for its own editor: the reference sits on the fab layer at the centre (so
|
|
6
|
+
Altium's .Designator lands over the part body), SMD pads are sharp rectangles, the fab outline has no pin-1 corner,
|
|
7
|
+
and pin-1 dots are often drawn on silk under the part, where nobody sees them once it is placed. Each fix below
|
|
8
|
+
answers one rule of docs/footprint-rulebook.md and is noted, and nothing touches the copper position or size:
|
|
9
|
+
text the reference goes on F.SilkS (1.0 mm, 0.15 mm stroke) just above the courtyard, the value below it
|
|
10
|
+
(F.Fab), and a ${REFERENCE} sized to the body goes on F.Fab at its centre (KLC F5.1 / F5.2)
|
|
11
|
+
padshape sharp rectangular SMD pads become rounded rectangles: radius 25 % of the short side, at most 0.25 mm
|
|
12
|
+
(KLC F6.3, IPC-7351C)
|
|
13
|
+
silk silk drawn wholly inside an SMD body is removed (it is under the part)
|
|
14
|
+
pin1 a part with 3 or more pads and no silk mark near pad 1 (silk the same turned 180 degrees is no mark) gets a 0.3 mm silk dot beside pad 1, outside
|
|
15
|
+
the body and 0.2 mm clear of copper
|
|
16
|
+
polarity a rectangular fab outline gets its pin-1 corner chamfered (1 mm or 25 % of the body, KLC F5.2)
|
|
17
|
+
"""
|
|
18
|
+
import math
|
|
19
|
+
|
|
20
|
+
from . import sexpr
|
|
21
|
+
from .sexpr import Q
|
|
22
|
+
|
|
23
|
+
SILK_CLEAR = 0.2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _num(v):
|
|
27
|
+
return float(v)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _fmt(v):
|
|
31
|
+
s = f"{v:.4f}".rstrip("0").rstrip(".")
|
|
32
|
+
return "0" if s in ("-0", "") else s
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _layer(it):
|
|
36
|
+
lay = sexpr.first(it, "layer")
|
|
37
|
+
return str(lay[1]) if lay else ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _pts(it):
|
|
41
|
+
"""Points of a drawing item (lines, rects, circles as their bounding points, polys)."""
|
|
42
|
+
k = it[0]
|
|
43
|
+
if k in ("fp_line", "fp_rect"):
|
|
44
|
+
a, b = sexpr.first(it, "start"), sexpr.first(it, "end")
|
|
45
|
+
if k == "fp_rect":
|
|
46
|
+
x1, y1, x2, y2 = (_num(a[1]), _num(a[2]), _num(b[1]), _num(b[2]))
|
|
47
|
+
return [(x1, y1), (x2, y1), (x2, y2), (x1, y2)]
|
|
48
|
+
return [(_num(a[1]), _num(a[2])), (_num(b[1]), _num(b[2]))]
|
|
49
|
+
if k == "fp_circle":
|
|
50
|
+
c, e = sexpr.first(it, "center"), sexpr.first(it, "end")
|
|
51
|
+
cx, cy = _num(c[1]), _num(c[2])
|
|
52
|
+
r = math.hypot(_num(e[1]) - cx, _num(e[2]) - cy)
|
|
53
|
+
return [(cx - r, cy - r), (cx + r, cy + r), (cx - r, cy + r), (cx + r, cy - r)]
|
|
54
|
+
if k == "fp_arc":
|
|
55
|
+
return [(_num(p[1]), _num(p[2])) for p in (sexpr.first(it, "start"), sexpr.first(it, "mid"), sexpr.first(it, "end")) if p]
|
|
56
|
+
if k == "fp_poly":
|
|
57
|
+
return [(_num(p[1]), _num(p[2])) for p in sexpr.find(sexpr.first(it, "pts") or [], "xy")]
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _bbox(pts):
|
|
62
|
+
xs, ys = [p[0] for p in pts], [p[1] for p in pts]
|
|
63
|
+
return (min(xs), min(ys), max(xs), max(ys)) if xs else None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _pads(tree):
|
|
67
|
+
out = []
|
|
68
|
+
for p in sexpr.find(tree, "pad"):
|
|
69
|
+
at, size = sexpr.first(p, "at"), sexpr.first(p, "size")
|
|
70
|
+
if not at or not size:
|
|
71
|
+
continue
|
|
72
|
+
rot = _num(at[3]) if len(at) > 3 else 0.0
|
|
73
|
+
w, h = _num(size[1]), _num(size[2])
|
|
74
|
+
if round(rot) % 180 == 90:
|
|
75
|
+
w, h = h, w
|
|
76
|
+
out.append({"num": str(p[1]), "x": _num(at[1]), "y": _num(at[2]), "w": w, "h": h, "kind": str(p[2]),
|
|
77
|
+
"shape": str(p[3]), "node": p, "rot": rot})
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _pad_gap(x, y, p):
|
|
82
|
+
"""Distance from a point to a pad's bounding box (0 inside)."""
|
|
83
|
+
dx = max(abs(x - p["x"]) - p["w"] / 2, 0)
|
|
84
|
+
dy = max(abs(y - p["y"]) - p["h"] / 2, 0)
|
|
85
|
+
return math.hypot(dx, dy)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _fab_rect(tree):
|
|
89
|
+
"""The body: the F.Fab outline's bounding box when the outline is a plain axis-aligned rectangle, with its
|
|
90
|
+
items -> (bbox, items, is_rect)."""
|
|
91
|
+
items = [it for it in tree if isinstance(it, list) and it and it[0] in ("fp_line", "fp_rect", "fp_poly")
|
|
92
|
+
and _layer(it) == "F.Fab"]
|
|
93
|
+
pts = [p for it in items for p in _pts(it)]
|
|
94
|
+
if not pts:
|
|
95
|
+
return None, [], False
|
|
96
|
+
bb = _bbox(pts)
|
|
97
|
+
axis = all(it[0] == "fp_rect" or (it[0] == "fp_line" and (abs(_pts(it)[0][0] - _pts(it)[1][0]) < 1e-6
|
|
98
|
+
or abs(_pts(it)[0][1] - _pts(it)[1][1]) < 1e-6)) for it in items)
|
|
99
|
+
rect = axis and (len(items) == 1 and items[0][0] == "fp_rect" or len(items) == 4 and all(it[0] == "fp_line" for it in items)
|
|
100
|
+
and all(min(abs(x - bb[0]), abs(x - bb[2])) < 1e-6 and min(abs(y - bb[1]), abs(y - bb[3])) < 1e-6
|
|
101
|
+
for it in items for x, y in _pts(it)))
|
|
102
|
+
return bb, items, rect
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
SILK_KINDS = ("fp_line", "fp_circle", "fp_poly", "fp_arc", "fp_rect")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _width(it):
|
|
109
|
+
w = sexpr.first(sexpr.first(it, "stroke") or [], "width")
|
|
110
|
+
return _num(w[1]) if w else 0.0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _extent(it):
|
|
114
|
+
"""Largest printed size of a drawing item: its bounding box plus the line width."""
|
|
115
|
+
bb = _bbox(_pts(it))
|
|
116
|
+
return max(bb[2] - bb[0], bb[3] - bb[1]) + _width(it)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _covers(o, it, tol=1e-3):
|
|
120
|
+
"""True when line o lies along line it and spans all of it."""
|
|
121
|
+
(ax, ay), (bx, by) = _pts(o)
|
|
122
|
+
L = math.hypot(bx - ax, by - ay)
|
|
123
|
+
if L < tol:
|
|
124
|
+
return False
|
|
125
|
+
ux, uy = (bx - ax) / L, (by - ay) / L
|
|
126
|
+
ts = []
|
|
127
|
+
for px, py in _pts(it):
|
|
128
|
+
if abs((px - ax) * uy - (py - ay) * ux) > tol:
|
|
129
|
+
return False
|
|
130
|
+
ts.append((px - ax) * ux + (py - ay) * uy)
|
|
131
|
+
return min(ts) >= -tol and max(ts) <= L + tol
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _clear_rows(tree, pads, items):
|
|
135
|
+
"""Dual-row parts: a silk outline whose edges run between the two pad rows but closer than SILK_CLEAR to them
|
|
136
|
+
is pulled in (both edges, symmetric about the pads' centre) -> the distance moved, or 0. Copper is not touched."""
|
|
137
|
+
for ax in (1, 0): # y (rows above and below) first, then x
|
|
138
|
+
lo = [p for p in pads if p["kind"] == "smd"]
|
|
139
|
+
if len(lo) < 4:
|
|
140
|
+
return 0
|
|
141
|
+
c = (min(p[("x", "y")[ax]] for p in lo) + max(p[("x", "y")[ax]] for p in lo)) / 2
|
|
142
|
+
half = lambda p: (p["w"], p["h"])[ax] / 2
|
|
143
|
+
rows = {}
|
|
144
|
+
for p in lo:
|
|
145
|
+
rows.setdefault(round(p[("x", "y")[ax]] - c, 3), []).append(p)
|
|
146
|
+
if len(rows) != 2 or sorted(rows)[0] != -sorted(rows)[1] or sorted(rows)[1] <= 0:
|
|
147
|
+
continue
|
|
148
|
+
inner = min(abs(k) - max(half(p) for p in ps) for k, ps in rows.items())
|
|
149
|
+
coords = [q[ax] - c for it in items for q in _pts(it)]
|
|
150
|
+
if not coords:
|
|
151
|
+
return 0
|
|
152
|
+
edge = max(abs(v) for v in coords)
|
|
153
|
+
if not (0 < edge < inner) or not any(abs(v + edge) < 1e-3 for v in coords):
|
|
154
|
+
continue
|
|
155
|
+
w = max((_width(it) for it in items), default=0.12)
|
|
156
|
+
new = inner - SILK_CLEAR - w / 2
|
|
157
|
+
if new >= edge - 1e-3 or new < 0.3:
|
|
158
|
+
continue
|
|
159
|
+
key = ("start", "mid", "end", "center")
|
|
160
|
+
for it in items:
|
|
161
|
+
for k in key:
|
|
162
|
+
n = sexpr.first(it, k)
|
|
163
|
+
if n and abs(abs(_num(n[1 + ax]) - c) - edge) < 1e-3:
|
|
164
|
+
n[1 + ax] = _fmt(c + math.copysign(new, _num(n[1 + ax]) - c))
|
|
165
|
+
return round(edge - new, 3)
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _join_lines(tree, pads, lines, arc_pts=(), tol=1e-3):
|
|
170
|
+
"""Axis-aligned silk lines on one row with gaps between them: a gap is closed when the joined line stays
|
|
171
|
+
SILK_CLEAR (plus half its width) from every pad and no arc (a pin-1 notch) ends in it. Returns the gaps closed."""
|
|
172
|
+
rows = {}
|
|
173
|
+
for it in lines:
|
|
174
|
+
(ax, ay), (bx, by) = _pts(it)
|
|
175
|
+
if abs(ay - by) < tol and abs(ax - bx) > tol:
|
|
176
|
+
rows.setdefault((0, round(ay, 3), round(_width(it), 3)), []).append((min(ax, bx), max(ax, bx), it))
|
|
177
|
+
elif abs(ax - bx) < tol and abs(ay - by) > tol:
|
|
178
|
+
rows.setdefault((1, round(ax, 3), round(_width(it), 3)), []).append((min(ay, by), max(ay, by), it))
|
|
179
|
+
closed = 0
|
|
180
|
+
for (ax, c, w), segs in rows.items():
|
|
181
|
+
segs.sort(key=lambda t: t[0])
|
|
182
|
+
cur = list(segs[0])
|
|
183
|
+
for lo, hi, it in segs[1:]:
|
|
184
|
+
pts = [(v, c) if ax == 0 else (c, v) for v in (cur[1], lo, (cur[1] + lo) / 2)]
|
|
185
|
+
pts += [((cur[1] + (lo - cur[1]) * k / 20, c) if ax == 0 else (c, cur[1] + (lo - cur[1]) * k / 20)) for k in range(21)]
|
|
186
|
+
notch = any(abs((q[1] if ax == 0 else q[0]) - c) < 0.05 and cur[1] - tol <= (q[0] if ax == 0 else q[1]) <= lo + tol
|
|
187
|
+
for q in arc_pts)
|
|
188
|
+
if lo > cur[1] + tol and not notch and all(_pad_gap(x, y, p) >= SILK_CLEAR + w / 2 - tol for x, y in pts for p in pads):
|
|
189
|
+
closed += 1
|
|
190
|
+
elif lo > cur[1] + tol:
|
|
191
|
+
cur = [lo, hi, it]
|
|
192
|
+
continue
|
|
193
|
+
cur[1] = max(cur[1], hi)
|
|
194
|
+
tree.remove(it)
|
|
195
|
+
ends = ("start", "end")
|
|
196
|
+
a, b = (sexpr.first(cur[2], k) for k in ends)
|
|
197
|
+
lo_, hi_ = cur[0], cur[1]
|
|
198
|
+
a[1:3], b[1:3] = ([_fmt(lo_), _fmt(c)], [_fmt(hi_), _fmt(c)]) if ax == 0 else ([_fmt(c), _fmt(lo_)], [_fmt(c), _fmt(hi_)])
|
|
199
|
+
return closed
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _stroke(w):
|
|
203
|
+
return ["stroke", ["width", _fmt(w)], ["type", "solid"]]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def tidy(text, notes=None):
|
|
207
|
+
notes = [] if notes is None else notes
|
|
208
|
+
tree = sexpr.parse(text, keep_quotes=True)
|
|
209
|
+
if not isinstance(tree, list) or tree[0] not in ("footprint", "module"):
|
|
210
|
+
return text
|
|
211
|
+
pads = _pads(tree)
|
|
212
|
+
if not pads:
|
|
213
|
+
return text
|
|
214
|
+
smd = (sexpr.first(tree, "attr") or [None, ""])[1] == "smd" or all(p["kind"] == "smd" for p in pads)
|
|
215
|
+
body, fab_items, fab_rect = _fab_rect(tree)
|
|
216
|
+
copper = _bbox([(p["x"] + sx * p["w"] / 2, p["y"] + sy * p["h"] / 2) for p in pads for sx in (-1, 1) for sy in (-1, 1)])
|
|
217
|
+
crt = _bbox([q for it in tree if isinstance(it, list) and it and it[0] in ("fp_line", "fp_rect", "fp_poly")
|
|
218
|
+
and _layer(it) == "F.CrtYd" for q in _pts(it)])
|
|
219
|
+
outer = crt or _bbox([(copper[0], copper[1]), (copper[2], copper[3])] + ([(body[0], body[1]), (body[2], body[3])] if body else []))
|
|
220
|
+
numbered = [p for p in pads if p["num"]]
|
|
221
|
+
pin1 = next((p for p in numbered if p["num"] in ("1", "A1")), None)
|
|
222
|
+
|
|
223
|
+
# padshape: sharp SMD rectangles -> rounded rectangles (radius 25 % of the short side, at most 0.25 mm)
|
|
224
|
+
rounded = 0
|
|
225
|
+
for p in pads:
|
|
226
|
+
n = p["node"]
|
|
227
|
+
if p["kind"] == "smd" and p["shape"] == "rect" and not sexpr.first(n, "primitives"):
|
|
228
|
+
ratio = min(0.25, 0.25 / max(min(p["w"], p["h"]), 1e-6))
|
|
229
|
+
n[3] = "roundrect"
|
|
230
|
+
i = n.index(sexpr.first(n, "size")) + 1
|
|
231
|
+
n.insert(i, ["roundrect_rratio", _fmt(ratio)])
|
|
232
|
+
p["shape"] = "roundrect"
|
|
233
|
+
rounded += 1
|
|
234
|
+
if rounded:
|
|
235
|
+
notes.append(f"pads: {rounded} sharp SMD rectangle(s) given rounded corners (25 % of the short side, at most 0.25 mm; KLC F6.3)")
|
|
236
|
+
|
|
237
|
+
silk_items = lambda: [it for it in tree if isinstance(it, list) and it and it[0] in SILK_KINDS and _layer(it) == "F.SilkS" and _pts(it)]
|
|
238
|
+
|
|
239
|
+
# silk specks (EasyEDA's 0.06 mm "polarity" circles) are too small to print or to read as a mark
|
|
240
|
+
specks = [it for it in silk_items() if _extent(it) < 0.15]
|
|
241
|
+
for it in specks:
|
|
242
|
+
tree.remove(it)
|
|
243
|
+
if specks:
|
|
244
|
+
notes.append(f"silk: {len(specks)} speck(s) under 0.15 mm removed (too small to print)")
|
|
245
|
+
|
|
246
|
+
# a dot drawn as a thick ring (EasyEDA's CIRCLE, a 16-gon from the converter) -> the same dot, filled, with a KLC width
|
|
247
|
+
dots = 0
|
|
248
|
+
for it in silk_items():
|
|
249
|
+
if it[0] in ("fp_poly", "fp_circle") and _extent(it) <= 0.65 and not 0.1 - 1e-6 <= _width(it) <= 0.15 + 1e-6:
|
|
250
|
+
bb, R = _bbox(_pts(it)), _extent(it) / 2
|
|
251
|
+
x, y = (bb[0] + bb[2]) / 2, (bb[1] + bb[3]) / 2
|
|
252
|
+
tree[tree.index(it)] = ["fp_circle", ["center", _fmt(x), _fmt(y)], ["end", _fmt(x + max(R - 0.06, 0.03)), _fmt(y)],
|
|
253
|
+
_stroke(0.12), ["fill", "solid"], ["layer", Q("F.SilkS")]]
|
|
254
|
+
dots += 1
|
|
255
|
+
if dots:
|
|
256
|
+
notes.append(f"silk: {dots} dot(s) redrawn filled with a 0.12 mm line (same size)")
|
|
257
|
+
|
|
258
|
+
# silk line width to KLC's 0.10-0.15 mm
|
|
259
|
+
widths = 0
|
|
260
|
+
for it in silk_items():
|
|
261
|
+
w = sexpr.first(sexpr.first(it, "stroke") or [], "width")
|
|
262
|
+
if w and not 0.1 - 1e-6 <= _num(w[1]) <= 0.15 + 1e-6:
|
|
263
|
+
w[1] = "0.12"
|
|
264
|
+
widths += 1
|
|
265
|
+
if widths:
|
|
266
|
+
notes.append(f"silk: {widths} item(s) set to 0.12 mm line width (KLC: 0.10-0.15 mm)")
|
|
267
|
+
|
|
268
|
+
# a dot or other closed shape wholly under an SMD body is hidden by the part (its outline lines stay)
|
|
269
|
+
if smd and body:
|
|
270
|
+
x0, y0, x1, y1 = body
|
|
271
|
+
under = lambda q: x0 + 0.05 < q[0] < x1 - 0.05 and y0 + 0.05 < q[1] < y1 - 0.05
|
|
272
|
+
gone = [it for it in silk_items() if it[0] in ("fp_circle", "fp_poly") and _extent(it) < 1.5 and all(under(q) for q in _pts(it))]
|
|
273
|
+
for it in gone:
|
|
274
|
+
tree.remove(it)
|
|
275
|
+
if gone:
|
|
276
|
+
notes.append(f"silk: {len(gone)} mark(s) under the part body removed (hidden once the part is placed)")
|
|
277
|
+
|
|
278
|
+
# silk segments another silk line already covers (EasyEDA draws some outlines twice)
|
|
279
|
+
lines = [it for it in silk_items() if it[0] == "fp_line"]
|
|
280
|
+
dup = [it for it in lines if any(o is not it and _covers(o, it) and not (_covers(it, o) and lines.index(o) > lines.index(it))
|
|
281
|
+
for o in lines)]
|
|
282
|
+
for it in dup:
|
|
283
|
+
tree.remove(it)
|
|
284
|
+
if dup:
|
|
285
|
+
notes.append(f"silk: {len(dup)} duplicate segment(s) removed (drawn twice)")
|
|
286
|
+
|
|
287
|
+
# silk outline edges that run along a pad row too close to it are moved in to 0.2 mm clearance
|
|
288
|
+
moved = _clear_rows(tree, pads, [it for it in silk_items() if it[0] in ("fp_line", "fp_arc")])
|
|
289
|
+
if moved:
|
|
290
|
+
notes.append(f"silk: outline moved in {moved} mm to clear the pads by {SILK_CLEAR} mm")
|
|
291
|
+
|
|
292
|
+
# collinear silk pieces whose gap no longer needs to be there (cut back from pads before the outline moved in)
|
|
293
|
+
joined = _join_lines(tree, pads, [it for it in silk_items() if it[0] == "fp_line"],
|
|
294
|
+
[q for it in silk_items() if it[0] == "fp_arc" for q in _pts(it)])
|
|
295
|
+
if joined:
|
|
296
|
+
notes.append(f"silk: {joined} gap(s) in the outline closed (now clear of the pads)")
|
|
297
|
+
|
|
298
|
+
# pin 1 mark on silk, outside the body and clear of copper
|
|
299
|
+
if pin1 and len(numbered) >= 3:
|
|
300
|
+
silk = [q for it in tree if isinstance(it, list) and it and it[0] in ("fp_line", "fp_circle", "fp_poly", "fp_arc", "fp_rect")
|
|
301
|
+
and _layer(it) == "F.SilkS" for q in _pts(it)]
|
|
302
|
+
reach = max(pin1["w"], pin1["h"]) / 2 + 1.0
|
|
303
|
+
others = [p for p in pads if p is not pin1]
|
|
304
|
+
mx, my = (copper[0] + copper[2]) / 2, (copper[1] + copper[3]) / 2
|
|
305
|
+
twin = lambda q: any(abs(2 * mx - q[0] - s[0]) < 0.05 and abs(2 * my - q[1] - s[1]) < 0.05 for s in silk)
|
|
306
|
+
near1 = [q for q in silk if math.hypot(q[0] - pin1["x"], q[1] - pin1["y"]) <= reach and not twin(q) # a symmetric
|
|
307
|
+
and all(math.hypot(q[0] - pin1["x"], q[1] - pin1["y"]) < math.hypot(q[0] - o["x"], q[1] - o["y"]) for o in others)]
|
|
308
|
+
if not near1:
|
|
309
|
+
r, sw = 0.09, 0.12 # filled: 0.3 mm across, drawn with a KLC silk width
|
|
310
|
+
R = r + sw / 2
|
|
311
|
+
cx, cy = ((body[0] + body[2]) / 2, (body[1] + body[3]) / 2) if body else (0.0, 0.0)
|
|
312
|
+
ox, oy = (1 if pin1["x"] > cx else -1), (1 if pin1["y"] > cy else -1)
|
|
313
|
+
d = SILK_CLEAR + R + 0.02
|
|
314
|
+
cands = [(pin1["x"] + ox * (pin1["w"] / 2 + d), pin1["y"]), (pin1["x"], pin1["y"] + oy * (pin1["h"] / 2 + d)),
|
|
315
|
+
(pin1["x"] + ox * (pin1["w"] / 2 + d), pin1["y"] + oy * (pin1["h"] / 2 + d))]
|
|
316
|
+
inbody = lambda x, y: body and body[0] - R <= x <= body[2] + R and body[1] - R <= y <= body[3] + R
|
|
317
|
+
spot = next(((x, y) for x, y in cands if not inbody(x, y) and all(_pad_gap(x, y, p) >= SILK_CLEAR + R for p in pads)), None)
|
|
318
|
+
if spot:
|
|
319
|
+
x, y = spot
|
|
320
|
+
tree.append(["fp_circle", ["center", _fmt(x), _fmt(y)], ["end", _fmt(x + r), _fmt(y)], _stroke(sw),
|
|
321
|
+
["fill", "solid"], ["layer", Q("F.SilkS")]])
|
|
322
|
+
outer = (min(outer[0], x - R), min(outer[1], y - R), max(outer[2], x + R), max(outer[3], y + R))
|
|
323
|
+
notes.append(f"silk: pin 1 dot added beside pad {pin1['num']} (no pin 1 mark was visible on silk)")
|
|
324
|
+
|
|
325
|
+
# courtyard around the silk too: a pin-1 dot outside the body must not stick out of the part's keep-out
|
|
326
|
+
crts = [it for it in tree if isinstance(it, list) and it and it[0] == "fp_rect" and _layer(it) == "F.CrtYd"]
|
|
327
|
+
marks = [it for it in tree if isinstance(it, list) and it and it[0] in SILK_KINDS and _layer(it) == "F.SilkS" and _pts(it)]
|
|
328
|
+
if len(crts) == 1 and marks:
|
|
329
|
+
a, b = sexpr.first(crts[0], "start"), sexpr.first(crts[0], "end")
|
|
330
|
+
x0, y0, x1, y1 = min(_num(a[1]), _num(b[1])), min(_num(a[2]), _num(b[2])), max(_num(a[1]), _num(b[1])), max(_num(a[2]), _num(b[2]))
|
|
331
|
+
sx0 = min(_bbox(_pts(it))[0] - _width(it) / 2 for it in marks)
|
|
332
|
+
sy0 = min(_bbox(_pts(it))[1] - _width(it) / 2 for it in marks)
|
|
333
|
+
sx1 = max(_bbox(_pts(it))[2] + _width(it) / 2 for it in marks)
|
|
334
|
+
sy1 = max(_bbox(_pts(it))[3] + _width(it) / 2 for it in marks)
|
|
335
|
+
n0 = (math.floor(min(x0, sx0) * 100 + 1e-6) / 100, math.floor(min(y0, sy0) * 100 + 1e-6) / 100)
|
|
336
|
+
n1 = (math.ceil(max(x1, sx1) * 100 - 1e-6) / 100, math.ceil(max(y1, sy1) * 100 - 1e-6) / 100)
|
|
337
|
+
if n0[0] < x0 - 1e-6 or n0[1] < y0 - 1e-6 or n1[0] > x1 + 1e-6 or n1[1] > y1 + 1e-6:
|
|
338
|
+
a[1:3], b[1:3] = [_fmt(n0[0]), _fmt(n0[1])], [_fmt(n1[0]), _fmt(n1[1])]
|
|
339
|
+
notes.append("courtyard grown to take in the silkscreen (the pin-1 dot sat outside it)")
|
|
340
|
+
|
|
341
|
+
# polarity: chamfer the fab outline's pin-1 corner
|
|
342
|
+
if fab_rect and pin1 and len(numbered) >= 3:
|
|
343
|
+
x0, y0, x1, y1 = body
|
|
344
|
+
w = sexpr.first(sexpr.first(fab_items[0], "stroke") or [], "width")
|
|
345
|
+
lw = _num(w[1]) if w else 0.1
|
|
346
|
+
c = min(1.0, 0.25 * min(x1 - x0, y1 - y0))
|
|
347
|
+
cx, cy = (x0 if pin1["x"] <= (x0 + x1) / 2 else x1), (y0 if pin1["y"] <= (y0 + y1) / 2 else y1)
|
|
348
|
+
corners = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
|
|
349
|
+
k = corners.index((cx, cy))
|
|
350
|
+
nxt, opp, prv = corners[(k + 1) % 4], corners[(k + 2) % 4], corners[(k + 3) % 4]
|
|
351
|
+
toward = lambda q: (cx + c * (q[0] > cx) - c * (q[0] < cx), cy + c * (q[1] > cy) - c * (q[1] < cy))
|
|
352
|
+
pts = [toward(nxt), nxt, opp, prv, toward(prv)]
|
|
353
|
+
pts.append(pts[0])
|
|
354
|
+
for it in fab_items:
|
|
355
|
+
tree.remove(it)
|
|
356
|
+
for a, b in zip(pts, pts[1:]):
|
|
357
|
+
tree.append(["fp_line", ["start", _fmt(a[0]), _fmt(a[1])], ["end", _fmt(b[0]), _fmt(b[1])], _stroke(lw), ["layer", Q("F.Fab")]])
|
|
358
|
+
notes.append(f"fab: pin 1 corner of the body outline chamfered {_fmt(c)} mm (KLC F5.2)")
|
|
359
|
+
|
|
360
|
+
# text: reference on silk above the courtyard, ${REFERENCE} on fab inside the body
|
|
361
|
+
texts = [it for it in tree if isinstance(it, list) and it and it[0] in ("fp_text", "property")]
|
|
362
|
+
ref = next((it for it in texts if str(it[1]) in ("reference", "Reference")), None)
|
|
363
|
+
if ref is not None and _layer(ref) != "F.SilkS":
|
|
364
|
+
at = sexpr.first(ref, "at")
|
|
365
|
+
y = outer[1] - 1.2 # Altium writes the literal ".Designator", whose g hangs below the baseline: keep it clear
|
|
366
|
+
at[1:] = [_fmt((outer[0] + outer[2]) / 2), _fmt(y)]
|
|
367
|
+
ref[ref.index(sexpr.first(ref, "layer"))] = ["layer", Q("F.SilkS")]
|
|
368
|
+
eff = sexpr.first(ref, "effects")
|
|
369
|
+
if eff is not None:
|
|
370
|
+
eff[eff.index(sexpr.first(eff, "font"))] = ["font", ["size", "1", "1"], ["thickness", "0.15"]]
|
|
371
|
+
notes.append("text: reference moved to the silkscreen above the part (it was on the fab layer at the centre)")
|
|
372
|
+
val = next((it for it in texts if str(it[1]) in ("value", "Value")), None)
|
|
373
|
+
vat = sexpr.first(val, "at") if val is not None else None
|
|
374
|
+
if vat and outer[0] < _num(vat[1]) < outer[2] and outer[1] < _num(vat[2]) < outer[3]:
|
|
375
|
+
vat[1:] = [_fmt((outer[0] + outer[2]) / 2), _fmt(outer[3] + 1.0)]
|
|
376
|
+
if not _layer(val):
|
|
377
|
+
val.append(["layer", Q("F.Fab")])
|
|
378
|
+
notes.append("text: value moved below the part (it sat over the body, where Altium's .Comment crossed the outline)")
|
|
379
|
+
has_fab_ref = any(str(it[1]) == "user" and str(it[2]) == "${REFERENCE}" for it in texts if it[0] == "fp_text" and len(it) > 2)
|
|
380
|
+
if not has_fab_ref and body:
|
|
381
|
+
bw, bh = body[2] - body[0], body[3] - body[1]
|
|
382
|
+
fit = lambda along, across: min(1.0, 0.9 * along / (3 * 0.93), 0.8 * across) # "U12" at 0.93 x height per character
|
|
383
|
+
rot = fit(bh, bw) > fit(bw, bh)
|
|
384
|
+
size = max(0.5, fit(bh, bw) if rot else fit(bw, bh))
|
|
385
|
+
at = ["at", _fmt((body[0] + body[2]) / 2), _fmt((body[1] + body[3]) / 2)] + (["90"] if rot else [])
|
|
386
|
+
tree.append(["fp_text", "user", Q("${REFERENCE}"), at, ["layer", Q("F.Fab")],
|
|
387
|
+
["effects", ["font", ["size", _fmt(size), _fmt(size)], ["thickness", _fmt(round(0.15 * size, 3))]]]])
|
|
388
|
+
notes.append("text: ${REFERENCE} added on the fab layer inside the body (assembly drawing, KLC F5.2)")
|
|
389
|
+
return sexpr.dumps(tree) + "\n"
|
ecadlib/fsutil.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Crash-safe file writes for files that hold people's work (plan.json, librarian_state.json, batch state).
|
|
2
|
+
|
|
3
|
+
write_text_atomic writes UTF-8 to a temp file next to the target (unique per process and thread, so two writers never
|
|
4
|
+
share one), flushes it to disk and moves it over the target with os.replace: a crash or a full disk leaves the old file
|
|
5
|
+
whole instead of a truncated one that no later command can read.
|
|
6
|
+
|
|
7
|
+
On Windows a rename over a file that another process has open (the web UI reading the plan, a virus scanner, a
|
|
8
|
+
search indexer) fails with PermissionError for as long as it is open, where the old in-place write worked: replace()
|
|
9
|
+
retries for a few seconds before giving up."""
|
|
10
|
+
import os, threading, time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
RETRY = os.name == "nt" # only Windows refuses a rename over an open file; elsewhere it is a real error
|
|
14
|
+
DELAYS = (0.05, 0.1, 0.2, 0.4, 0.8, 1.6)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def replace(src, dst):
|
|
18
|
+
"""os.replace(src, dst), retried on Windows while dst (or src) is held open by another process."""
|
|
19
|
+
for delay in DELAYS + (None,):
|
|
20
|
+
try:
|
|
21
|
+
return os.replace(src, dst)
|
|
22
|
+
except PermissionError:
|
|
23
|
+
if not RETRY or delay is None:
|
|
24
|
+
raise
|
|
25
|
+
time.sleep(delay)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def write_text_atomic(path, text):
|
|
29
|
+
path = Path(path)
|
|
30
|
+
tmp = path.with_name(f".{path.name}.{os.getpid()}.{threading.get_ident()}.tmp")
|
|
31
|
+
try:
|
|
32
|
+
with open(tmp, "w", encoding="utf-8", newline="") as f:
|
|
33
|
+
f.write(text)
|
|
34
|
+
f.flush()
|
|
35
|
+
os.fsync(f.fileno())
|
|
36
|
+
replace(tmp, path)
|
|
37
|
+
except BaseException:
|
|
38
|
+
try:
|
|
39
|
+
os.unlink(tmp)
|
|
40
|
+
except OSError:
|
|
41
|
+
pass
|
|
42
|
+
raise
|
|
43
|
+
return path
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def write_bytes_atomic(path, data):
|
|
47
|
+
"""write_text_atomic for binary files (.PcbLib / .SchLib): a crash mid-write leaves the old file whole."""
|
|
48
|
+
path = Path(path)
|
|
49
|
+
tmp = path.with_name(f".{path.name}.{os.getpid()}.{threading.get_ident()}.tmp")
|
|
50
|
+
try:
|
|
51
|
+
with open(tmp, "wb") as f:
|
|
52
|
+
f.write(data)
|
|
53
|
+
f.flush()
|
|
54
|
+
os.fsync(f.fileno())
|
|
55
|
+
replace(tmp, path)
|
|
56
|
+
except BaseException:
|
|
57
|
+
try:
|
|
58
|
+
os.unlink(tmp)
|
|
59
|
+
except OSError:
|
|
60
|
+
pass
|
|
61
|
+
raise
|
|
62
|
+
return path
|