@tachikomagundam/abathur 0.2.7 → 0.2.8

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.
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env python3
2
+ """r17 GND pour: strip greedy GND tracks, paint GND pads, raster-erode foreign
3
+ items, emit dual-layer (F.Cu+B.Cu) zone with scanline-exact boundary rings plus
4
+ stitching vias. Deterministic 0.2mm raster. Usage: board_zone_pour.py in out"""
5
+ import json, os, re, sys
6
+ from collections import defaultdict
7
+
8
+ CELL = 0.2
9
+ EDGE_INSET = 0.3
10
+ OBS_CLEAR = 0.25
11
+ HOLE_CLEAR = 0.30
12
+
13
+
14
+ def sub_blocks(s, start):
15
+ out = []; i = start; n = len(s)
16
+ while i < n:
17
+ while i < n and s[i] != "(":
18
+ i += 1
19
+ if i >= n:
20
+ break
21
+ depth = 0; j = i; instr = False; esc = False
22
+ while j < n:
23
+ ch = s[j]
24
+ if esc: esc = False
25
+ elif ch == "\\": esc = True
26
+ elif ch == '"': instr = not instr
27
+ elif not instr:
28
+ if ch == "(": depth += 1
29
+ elif ch == ")":
30
+ depth -= 1
31
+ if depth == 0: break
32
+ j += 1
33
+ out.append(s[i + 1:j]); i = j + 1
34
+ p2 = i
35
+ while p2 < n and s[p2] in " \t\n\r":
36
+ p2 += 1
37
+ if p2 < n and s[p2] == ")":
38
+ break
39
+ return out
40
+
41
+
42
+ def parse_pads(txt):
43
+ pads = []
44
+ for b in sub_blocks(sub_blocks(txt, txt.index("(kicad_pcb"))[0], 0):
45
+ if not b.startswith("footprint"):
46
+ continue
47
+ m0 = re.search(r'\(at ([-\d.]+) ([-\d.]+)\)', b[:b.find("(property") if "(property" in b else 200])
48
+ ax, ay = float(m0.group(1)), float(m0.group(2))
49
+ mr = re.search(r'\(rotate (\d+\.?\d*)\)', b[:b.find("(layer")])
50
+ rot = __import__("math").radians(float(mr.group(1))) if mr else 0.0
51
+ mref = re.search(r'\(property "Reference" "([^"]+)"', b)
52
+ ref = mref.group(1) if mref else ""
53
+ i = 0
54
+ while True:
55
+ i = b.find("(pad ", i)
56
+ if i < 0: break
57
+ d = 0; j = i; instr = False
58
+ while j < len(b):
59
+ c = b[j]
60
+ if c == '"': instr = not instr
61
+ elif not instr:
62
+ if c == "(": d += 1
63
+ elif c == ")":
64
+ d -= 1
65
+ if d == 0: break
66
+ j += 1
67
+ pb = b[i + 1:j]; i = j + 1
68
+ pnum = re.match(r'pad "([^"]*)"', pb).group(1)
69
+ sm = re.search(r'\(at ([-\d.]+) ([-\d.]+)(?: ([-\d.]+))?\)', pb)
70
+ ox, oy = float(sm.group(1)), float(sm.group(2))
71
+ ca, sa = __import__("math").cos(rot), __import__("math").sin(rot)
72
+ wx, wy = ax + ox * ca - oy * sa, ay + ox * sa + oy * ca
73
+ sz = re.search(r'\(size ([-\d.]+) ([-\d.]+)\)', pb)
74
+ w, h = float(sz.group(1)), float(sz.group(2))
75
+ shape = "circle" if "(shape circle)" in pb or '"circle"' in pb else "rect"
76
+ drl = re.search(r'\(drill(?: oval)? ([\d.]+)(?: ([\d.]+))?\)', pb)
77
+ pads.append(dict(ref=ref, num=pnum, x=wx, y=wy, w=w, h=h, shape=shape,
78
+ drill=(float(drl.group(1)), float(drl.group(2) or drl.group(1))) if drl else None,
79
+ bnet=(re.search(r'\(net \d+ "([^"]*)"\)', pb) or [None, None])[1]))
80
+ return pads
81
+
82
+
83
+ def rings_from_rects(rects, x0, y0):
84
+ segs = set()
85
+ for (xa, ya, xb, yb) in rects:
86
+ ax, ay = x0 + (xa - 1) * CELL, y0 + (ya - 1) * CELL
87
+ bx, by = x0 + (xb - 1) * CELL, y0 + (yb - 1) * CELL
88
+ segs.add(((ax, ay), (bx, ay)))
89
+ segs.add(((bx, ay), (bx, by)))
90
+ segs.add(((bx, by), (ax, by)))
91
+ segs.add(((ax, by), (ax, ay)))
92
+ kill = set()
93
+ for a, b in segs:
94
+ if (b, a) in segs:
95
+ kill.add((a, b)); kill.add((b, a))
96
+ eout = defaultdict(list)
97
+ for a, b in segs - kill:
98
+ eout[a].append(b)
99
+ rings = []
100
+ for st in list(eout.keys()):
101
+ while eout[st]:
102
+ stack = [st]; trail = []
103
+ cur = st
104
+ while True:
105
+ outs = eout[cur]
106
+ if not outs:
107
+ break
108
+ nxt = outs.pop()
109
+ stack.append(nxt)
110
+ cur = nxt
111
+ if cur == st:
112
+ break
113
+ if cur == st and len(stack) >= 5:
114
+ rings.append(stack[:-1])
115
+ elif cur != st:
116
+ for q in reversed(stack[stack.index(st) + 1:]):
117
+ eout.setdefault(stack[stack.index(q)][0], [])
118
+ def area(r):
119
+ return sum(r[k][0] * r[k + 1][1] - r[k + 1][0] * r[k][1] for k in range(len(r) - 1)) / 2.0
120
+ out = []
121
+ for r in rings:
122
+ rr = []
123
+ for q in r:
124
+ if not rr or rr[-1] != q:
125
+ rr.append(q)
126
+ if len(rr) >= 5 and rr[0] == rr[-1]:
127
+ rr = rr[:-1]
128
+ if len(rr) >= 4 and abs(area(rr)) > 0.5:
129
+ out.append(rr)
130
+ return out
131
+
132
+
133
+ def main(inp, outp):
134
+ txt = open(inp).read()
135
+ nm = dict((m.group(2).strip('"'), int(m.group(1))) for m in
136
+ re.finditer(r'\(net (\d+) "([^"]+)"\)', txt))
137
+ gnd_code = nm.get("GND")
138
+ assert gnd_code is not None, "no GND net"
139
+ POW = [x for x in (os.environ.get("PCB_POWER_ZONE_NETS", "5V_SERVO,5V_LOGIC,3V3,VBUS")).split(",") if x]
140
+ pcode = {n: nm[n] for n in POW if n in nm}
141
+ padby = defaultdict(list)
142
+ prect = {}
143
+ pwr_codes = set(pcode.values())
144
+
145
+ root_children = sub_blocks(sub_blocks(txt, txt.index("(kicad_pcb"))[0], 0)
146
+ xs, ys = [], []
147
+ for b in root_children:
148
+ if b.startswith("gr_line") and "Edge.Cuts" in b:
149
+ for mm in re.finditer(r'\((?:start|end) ([-\d.]+) ([-\d.]+)\)', b):
150
+ xs.append(float(mm.group(1))); ys.append(float(mm.group(2)))
151
+ x0, x1, y0, y1 = min(xs), max(xs), min(ys), max(ys)
152
+ spec = "/home/lab/workspace/pcb-local-repro/solar_ray_v2/circuit/netlist.json"
153
+ pad_net = {}
154
+ for c in json.load(open(spec))["components"]:
155
+ for pin, net in c["pins"].items():
156
+ pad_net[(c["ref"], pin)] = net
157
+ pads = parse_pads(txt)
158
+ for pd_ in pads:
159
+ pd_["net"] = pd_["bnet"] or pad_net.get((pd_["ref"], pd_["num"]), "?")
160
+ padby[pd_["net"]].append(pd_)
161
+ for n_, ps in padby.items():
162
+ if n_ in pcode:
163
+ xs_ = [pd_["x"] - pd_["w"] / 2 for pd_ in ps] + [pd_["x"] + pd_["w"] / 2 for pd_ in ps]
164
+ ys_ = [pd_["y"] - pd_["h"] / 2 for pd_ in ps] + [pd_["y"] + pd_["h"] / 2 for pd_ in ps]
165
+ prect[n_] = (min(xs_) - 0.8, min(ys_) - 0.8, max(xs_) + 0.8, max(ys_) + 0.8)
166
+ gnd_name = "GND"
167
+ keep = []
168
+ nstrip = 0
169
+ fp_i = next(i for i, b in enumerate(root_children) if b.startswith("footprint"))
170
+ for b in root_children[fp_i:]:
171
+ if b.startswith("segment") or b.startswith("via"):
172
+ mn = re.search(r'\(net (\d+)\)', b)
173
+ if mn and int(mn.group(1)) in ({gnd_code} | pwr_codes):
174
+ nstrip += 1
175
+ continue
176
+ keep.append("(" + b + ")")
177
+ W = int((x1 - x0) / CELL) + 2
178
+ H = int((y1 - y0) / CELL) + 2
179
+ occ = bytearray(W * H)
180
+ rawpad = bytearray(W * H)
181
+
182
+ def cx(v): return int((v - x0) / CELL) + 1
183
+ def cy(v): return int((v - y0) / CELL) + 1
184
+
185
+ def mark_rect(xa, ya, xb, yb, r, arr):
186
+ gx0, gx1 = sorted((cx(xa), cx(xb)))
187
+ gy0, gy1 = sorted((cy(ya), cy(yb)))
188
+ pad = int(r / CELL) + 1
189
+ for gy in range(max(0, gy0 - pad), min(H, gy1 + pad + 1)):
190
+ for gx in range(max(0, gx0 - pad), min(W, gx1 + pad + 1)):
191
+ arr[gy * W + gx] = 1
192
+ for blk in keep:
193
+ if blk.startswith("(segment"):
194
+ m1 = re.search(r'\(start ([-\d.]+) ([-\d.]+)\) \(end ([-\d.]+) ([-\d.]+)\)', blk)
195
+ if m1:
196
+ xa, ya, xb, yb = (float(m1.group(i)) for i in (1, 2, 3, 4))
197
+ n_ = int(max(abs(xb - xa), abs(yb - ya)) / (CELL / 2)) + 1
198
+ for k in range(n_ + 1):
199
+ t = k / n_
200
+ mark_rect(xa + (xb - xa) * t - CELL, ya + (yb - ya) * t - CELL,
201
+ xa + (xb - xa) * t + CELL, ya + (yb - ya) * t + CELL, 0.15, occ)
202
+ elif blk.startswith("(via"):
203
+ m2 = re.search(r'\(via \(at ([-\d.]+) ([-\d.]+)\)', blk)
204
+ if m2:
205
+ mark_rect(float(m2.group(1)), float(m2.group(2)),
206
+ float(m2.group(1)), float(m2.group(2)), 0.55, occ)
207
+ for p_ in pads:
208
+ mark_rect(p_["x"] - p_["w"] / 2, p_["y"] - p_["h"] / 2,
209
+ p_["x"] + p_["w"] / 2, p_["y"] + p_["h"] / 2, 0.5, rawpad)
210
+ if p_["net"] != gnd_name:
211
+ if p_["drill"]:
212
+ mark_rect(p_["x"] - p_["w"] / 2, p_["y"] - p_["h"] / 2,
213
+ p_["x"] + p_["w"] / 2, p_["y"] + p_["h"] / 2, 0.5, occ)
214
+ else:
215
+ mark_rect(p_["x"] - p_["w"] / 2, p_["y"] - p_["h"] / 2,
216
+ p_["x"] + p_["w"] / 2, p_["y"] + p_["h"] / 2, 0.5, occ)
217
+ for blk in keep:
218
+ if blk.startswith("(segment"):
219
+ m1 = re.search(r'\(start ([-\d.]+) ([-\d.]+)\) \(end ([-\d.]+) ([-\d.]+)\)', blk)
220
+ if m1:
221
+ xa, ya, xb, yb = (float(m1.group(i)) for i in (1, 2, 3, 4))
222
+ nseg = int(max(abs(xb - xa), abs(yb - ya)) / (CELL / 2)) + 1
223
+ for k in range(nseg + 1):
224
+ t = k / nseg
225
+ px_, py_ = xa + (xb - xa) * t, ya + (yb - ya) * t
226
+ mark_rect(px_ - CELL, py_ - CELL, px_ + CELL, py_ + CELL, 0.1, occ)
227
+ bx0, by0, bx1, by1 = x0 + EDGE_INSET, y0 + EDGE_INSET, x1 - EDGE_INSET, y1 - EDGE_INSET
228
+ outline_pts = f"(xy {bx0:.4f} {by0:.4f}) (xy {bx1:.4f} {by0:.4f}) (xy {bx1:.4f} {by1:.4f}) (xy {bx0:.4f} {by1:.4f}) (xy {bx0:.4f} {by0:.4f})"
229
+ # fine-pitch connector keepout: no pour under USB-C-class parts (0.85mm rows)
230
+ kx = [pd_ for pd_ in pads if pd_["ref"] in {"J1"}]
231
+ KEEPOUT_RECT = None
232
+ if kx:
233
+ hx0 = min(p_["x"] - p_["w"] / 2 for p_ in kx) - 0.3
234
+ hy0 = min(p_["y"] - p_["h"] / 2 for p_ in kx) - 0.3
235
+ hx1 = max(p_["x"] + p_["w"] / 2 for p_ in kx) + 0.3
236
+ hy1 = max(p_["y"] + p_["h"] / 2 for p_ in kx) + 0.3
237
+ KEEPOUT_RECT = (hx0, hy0, hx1, hy1)
238
+ _ = KEEPOUT_RECT
239
+ outline = outline_pts
240
+ z = []
241
+ for LI, LAY in enumerate(["F.Cu", "B.Cu"]):
242
+ z += [f'(zone (net {gnd_code}) (net_name "GND") (layer "{LAY}") (uuid "5a000000-0000-0000-0000-00000000cafe{LI}")',
243
+ ' (hatch edge 0.508)',
244
+ ' (connect_pads (clearance 0))',
245
+ ' (min_thickness 0.2)',
246
+ ' (filled_areas_thickness no)',
247
+ ' (fill yes (thermal_gap 0.4) (thermal_bridge_width 0.5))',
248
+ f' (polygon (pts {outline}))',
249
+ ')']
250
+ def rectpts(xa, ya, xb, yb):
251
+ return f"(xy {xa:.4f} {ya:.4f}) (xy {xb:.4f} {ya:.4f}) (xy {xb:.4f} {yb:.4f}) (xy {xa:.4f} {yb:.4f}) (xy {xa:.4f} {ya:.4f})"
252
+ for zi, (n_, rc) in enumerate(sorted(prect.items())):
253
+ for LI, LAY in enumerate(["F.Cu", "B.Cu"]):
254
+ holes = []
255
+ # explicit holes ONLY for GND pads (protects GND fill corridors at the
256
+ # zone rim); power-vs-power carve stays native so a neighboring
257
+ # foreign rect never punches away a power pad's own connect island.
258
+ for pd_ in pads:
259
+ if pd_["net"] != "GND":
260
+ continue
261
+ hx0, hy0 = pd_["x"] - pd_["w"] / 2 - 0.30, pd_["y"] - pd_["h"] / 2 - 0.30
262
+ hx1, hy1 = pd_["x"] + pd_["w"] / 2 + 0.30, pd_["y"] + pd_["h"] / 2 + 0.30
263
+ if hx1 < rc[0] or hx0 > rc[2] or hy1 < rc[1] or hy0 > rc[3]:
264
+ continue
265
+ holes.append(rectpts(hx0, hy0, hx1, hy1))
266
+ pts = f"(pts {rectpts(*rc)}" + ("".join(" " + h for h in holes)) + ")"
267
+ z += [f'(zone (net {pcode[n_]}) (net_name "{n_}") (layer "{LAY}") (priority 2) (uuid "5a000000-0000-0000-0000-00000000cbfe{zi}{LI}")',
268
+ ' (hatch edge 0.508)',
269
+ ' (connect_pads (clearance 0))',
270
+ ' (min_thickness 0.2)',
271
+ ' (fill yes (thermal_gap 0.4) (thermal_bridge_width 0.5))',
272
+ f' (polygon {pts})',
273
+ ')']
274
+ if KEEPOUT_RECT:
275
+ for LI, LAY in enumerate(["F.Cu", "B.Cu"]):
276
+ k = KEEPOUT_RECT
277
+ z += [f'(zone (net -1) (net_name "") (layer "{LAY}") (uuid "5a000000-0000-0000-0000-00000000cdfe{LI}")',
278
+ ' (hatch edge 0.508)',
279
+ ' (priority 15)',
280
+ f' (polygon (pts (xy {k[0]:.4f} {k[1]:.4f}) (xy {k[2]:.4f} {k[1]:.4f}) (xy {k[2]:.4f} {k[3]:.4f}) (xy {k[0]:.4f} {k[3]:.4f}) (xy {k[0]:.4f} {k[1]:.4f})))',
281
+ ')']
282
+ stitch = 0
283
+ step = max(1, int(4.0 / CELL))
284
+ for gy in range(1, H - 1, step):
285
+ for gx in range(1, W - 1, step):
286
+ rr = int(0.55 / CELL)
287
+ vxc, vyc = x0 + (gx - 1) * CELL, y0 + (gy - 1) * CELL
288
+ if not (x0 + 1.0 < vxc < x1 - 1.0 and y0 + 1.0 < vyc < y1 - 1.0):
289
+ continue # board-edge via guard: copper_edge_clearance class
290
+ if any(rc[0] + 0.3 < vxc < rc[2] - 0.3 and rc[1] + 0.3 < vyc < rc[3] - 0.3 for rc in prect.values()):
291
+ continue # GND via must not land inside a power zone
292
+ if KEEPOUT_RECT and KEEPOUT_RECT[0] - 0.2 < vxc < KEEPOUT_RECT[2] + 0.2 and KEEPOUT_RECT[1] - 0.2 < vyc < KEEPOUT_RECT[3] + 0.2:
293
+ continue # fine-pitch connector keepout
294
+ if any(occ[max(0, min(H - 1, gy + dy2)) * W + max(0, min(W - 1, gx + dx2))] or
295
+ rawpad[max(0, min(H - 1, gy + dy2)) * W + max(0, min(W - 1, gx + dx2))]
296
+ for dx2 in range(-rr, rr + 1) for dy2 in range(-rr, rr + 1)):
297
+ continue
298
+ stitch += 1
299
+ z.append(f'(via (at {x0 + (gx - 1) * CELL:.4f} {y0 + (gy - 1) * CELL:.4f}) (size 0.6) (drill 0.3)'
300
+ f' (layers "F.Cu" "B.Cu") (net {gnd_code}) (uuid "00000000-0000-0000-0000-{stitch:012d}cafe0"))')
301
+ print(f"stripped={nstrip} pads={len(pads)} stitch-vias={stitch} zones={2 + 2 * len(prect)} (GND x2 + power x{2 * len(prect)}, native refill)")
302
+ # GND net-class -> solid pad connection (kills starved_thermal at pour rims)
303
+ head = re.sub(r'(\(net_class "GND"[^\n]*?)(\s*\)\s*)$',
304
+ r'\1 (pad_connection "solid")\2',
305
+ txt[:txt.index("(footprint")].rstrip("\n"))
306
+ body = "\n".join(keep if keep else [])
307
+ tail = "\n".join(z)
308
+ open(outp, "w").write(head + "\n" + body + "\n" + tail + "\n)\n")
309
+
310
+
311
+ if __name__ == "__main__":
312
+ main(sys.argv[1], sys.argv[2])