Created
December 27, 2025 11:14
-
-
Save akirayou/b6a170f7a5e8c66885c4cc9ef421e353 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import math | |
| import pcbnew | |
| # ----------------- KiCad9 helpers ----------------- | |
| def v2i_mm(x_mm: float, y_mm: float) -> "pcbnew.VECTOR2I": | |
| return pcbnew.VECTOR2I(pcbnew.FromMM(x_mm), pcbnew.FromMM(y_mm)) | |
| def mm_to_iu(mm: float) -> int: | |
| return int(pcbnew.FromMM(mm)) | |
| def ensure_net(board, net_name): | |
| if not net_name: | |
| return None | |
| net = board.FindNet(net_name) | |
| if net is None: | |
| net = pcbnew.NETINFO_ITEM(board, net_name) | |
| board.Add(net) | |
| return net | |
| # ----------------- primitives (return end cursor) ----------------- | |
| def add_track(board, start_mm, diff_mm, width_iu, layer, netcode): | |
| """Draw straight segment. Returns end_mm.""" | |
| end_mm = (start_mm[0] + diff_mm[0], start_mm[1] + diff_mm[1]) | |
| if start_mm == end_mm: | |
| return end_mm | |
| t = pcbnew.PCB_TRACK(board) | |
| t.SetStart(v2i_mm(*start_mm)) | |
| t.SetEnd(v2i_mm(*end_mm)) | |
| t.SetWidth(width_iu) | |
| t.SetLayer(layer) | |
| if netcode is not None: | |
| t.SetNetCode(netcode) | |
| board.Add(t) | |
| return end_mm | |
| def add_arc_90_ccw(board, start_mm, dir_in, r_mm, width_iu, layer, netcode): | |
| """ | |
| Draw a 90-degree CCW fillet arc around the sharp corner, using only: | |
| - start_mm: expected to be the tangent point on the incoming edg | |
| - dir_in: incoming direction (0:R, 1:U, 2:L, 3:D) | |
| - r_mm: radius | |
| Returns arc end point (tangent on outgoing edge). | |
| """ | |
| if r_mm <= 0: | |
| # No arc: just return the corner itself (caller can decide) | |
| return start_mm | |
| start = start_mm | |
| sx, sy = start_mm | |
| r = r_mm | |
| # CCW turn: dir_out = (dir_in + 1) % 4 | |
| # Define start/end tangent points and arc center for each incoming dir. | |
| # right is positive X, up is positive Y | |
| if dir_in == 0: # Right -> Up at (cx,cy) | |
| center = (sx, sy + r) | |
| end = (sx + r, sy + r) | |
| elif dir_in == 1: # Up -> Left | |
| center = (sx - r, sy ) | |
| end = (sx - r, sy + r) | |
| elif dir_in == 2: # Left -> Down | |
| center = (sx, sy - r) | |
| end = (sx - r , sy - r) | |
| elif dir_in == 3: # Down -> Right | |
| center = (sx + r, sy ) | |
| end = (sx + r, sy - r) | |
| else: | |
| raise ValueError("dir_in must be 0(R),1(U),2(L),3(D)") | |
| # Compute mid-point for KiCad ARC (start/center/end -> 45deg point) | |
| sx, sy = start | |
| ccx, ccy = center | |
| ex, ey = end | |
| vs = (sx - ccx, sy - ccy) | |
| ve = (ex - ccx, ey - ccy) | |
| v = (vs[0] + ve[0], vs[1] + ve[1]) | |
| nv = math.hypot(v[0], v[1]) | |
| if nv == 0: | |
| raise ValueError("Degenerate arc geometry (check r/pitch/size).") | |
| rr = math.hypot(vs[0], vs[1]) | |
| mx = ccx + (v[0] / nv) * rr | |
| my = ccy + (v[1] / nv) * rr | |
| a = pcbnew.PCB_ARC(board) | |
| a.SetStart(v2i_mm(sx, sy)) | |
| a.SetMid(v2i_mm(mx, my)) | |
| a.SetEnd(v2i_mm(ex, ey)) | |
| a.SetWidth(width_iu) | |
| a.SetLayer(layer) | |
| if netcode is not None: | |
| a.SetNetCode(netcode) | |
| board.Add(a) | |
| return end | |
| # ----------------- validation ----------------- | |
| def validate_params(outer_w, outer_h, turns, pitch, r, track_w): | |
| if int(turns) != turns or turns < 1: | |
| raise ValueError("turns must be a positive integer.") | |
| if pitch <= 0: | |
| raise ValueError("pitch_mm must be > 0.") | |
| if track_w <= 0: | |
| raise ValueError("track_w_mm must be > 0.") | |
| if r < 0: | |
| raise ValueError("corner_r_mm must be >= 0.") | |
| if outer_w <= 0 or outer_h <= 0: | |
| raise ValueError("outer_w_mm / outer_h_mm must be > 0.") | |
| if outer_w <= 2*r or outer_h <= 2*r: | |
| raise ValueError("outer size too small for chosen corner radius.") | |
| # practical guard (keeps adjacent turns from kissing at corners) | |
| if r > 0.5 * pitch: | |
| raise ValueError("corner_r_mm too large; recommend corner_r_mm <= pitch_mm/2.") | |
| # Need positive width/height at the last turn BEFORE drawing edges | |
| # width_i = outer_w - 2*i*pitch, i=turns-1 must satisfy width_i > 2r (same for height) | |
| min_w = outer_w - 2.0 * (turns - 1) * pitch | |
| min_h = outer_h - 2.0 * (turns - 1) * pitch | |
| if min_w <= 2.0 * r or min_h <= 2.0 * r: | |
| raise ValueError( | |
| f"Spiral collapses: min_w={min_w:.3f}mm, min_h={min_h:.3f}mm, need > 2r={2*r:.3f}mm. " | |
| f"Increase outer size or reduce turns/pitch/r." | |
| ) | |
| # ----------------- main ----------------- | |
| def draw_rect_spiral_rounded_ccw( | |
| start_x_mm=100.0, | |
| start_y_mm=100.0, | |
| outer_w_mm=30.0, | |
| outer_h_mm=18.0, | |
| turns=8, | |
| pitch_mm=0.7, | |
| track_w_mm=0.35, | |
| corner_r_mm=None, # None -> pitch/2 | |
| layer=pcbnew.F_Cu, | |
| net_name=None, | |
| ): | |
| """ | |
| Rectangular spiral, CCW fixed: | |
| Right -> Up -> Left -> Down ... (repeat) | |
| Boundary shrink rules (per full turn): | |
| bottom += pitch, right -= pitch, top -= pitch, left += pitch | |
| Each corner is a 90° CCW arc with radius r. | |
| Pads/vias are NOT touched. | |
| """ | |
| turns = int(turns) | |
| r = (0.5 * pitch_mm) if corner_r_mm is None else float(corner_r_mm) | |
| validate_params(outer_w_mm, outer_h_mm, turns, pitch_mm, r, track_w_mm) | |
| board = pcbnew.GetBoard() | |
| net = ensure_net(board, net_name) | |
| netcode = net.GetNetCode() if net else None | |
| wiu = mm_to_iu(track_w_mm) | |
| left = start_x_mm | |
| bottom = start_y_mm | |
| # Start on bottom edge heading right: (left+r, bottom) | |
| cur = (left + r, bottom) | |
| w=outer_w_mm - 2*r | |
| h=outer_h_mm - 2*r | |
| for _ in range(turns): | |
| # ---- corner at (right, bottom): dir_in=Right(0) ---- | |
| cur = add_track(board, cur, (w,0), wiu, layer, netcode) | |
| cur = add_arc_90_ccw(board, cur, dir_in=0, r_mm=r, width_iu=wiu, layer=layer, netcode=netcode) | |
| w-= pitch_mm | |
| # ---- corner at (right, top): dir_in=Up(1) ---- | |
| cur = add_track(board, cur, (0,h), wiu, layer, netcode) | |
| cur = add_arc_90_ccw(board, cur, dir_in=1, r_mm=r, width_iu=wiu, layer=layer, netcode=netcode) | |
| h-= pitch_mm | |
| # ---- corner at (left, top): dir_in=Left(2) ---- | |
| cur = add_track(board, cur, (-w, 0), wiu, layer, netcode) | |
| cur = add_arc_90_ccw(board, cur, dir_in=2, r_mm=r, width_iu=wiu, layer=layer, netcode=netcode) | |
| w-= pitch_mm | |
| # ---- corner at (left, bottom): dir_in=Down(3) ---- | |
| cur = add_track(board, cur, (0,-h), wiu, layer, netcode) | |
| cur = add_arc_90_ccw(board, cur, dir_in=3, r_mm=r, width_iu=wiu, layer=layer, netcode=netcode) | |
| h-= pitch_mm | |
| # Now we are at (left_old + r, bottom). Update left boundary for next loop. | |
| # IMPORTANT: connect to next loop's start (left_new + r, bottom) to keep continuity | |
| #cur = add_track(board, cur, (left + r, bottom), wiu, layer, netcode) | |
| pcbnew.Refresh() | |
| # ----------------- example ----------------- | |
| W=60 | |
| H=40 | |
| T=10 | |
| LW=0.5 | |
| D=LW+0.2 | |
| draw_rect_spiral_rounded_ccw( | |
| start_x_mm=100.0, | |
| start_y_mm=100.0, | |
| outer_w_mm=W, | |
| outer_h_mm=H, | |
| turns=T, | |
| pitch_mm=D*2, | |
| track_w_mm=LW, | |
| corner_r_mm=None, # None -> pitch/2 | |
| layer=pcbnew.F_Cu, | |
| net_name="PCB_heater" | |
| ) | |
| draw_rect_spiral_rounded_ccw( | |
| start_x_mm=100.0+D, | |
| start_y_mm=100.0+D, | |
| outer_w_mm=W-D*2, | |
| outer_h_mm=H-D*2, | |
| turns=T, | |
| pitch_mm=D*2, | |
| track_w_mm=LW, | |
| corner_r_mm=None, # None -> pitch/2 | |
| layer=pcbnew.F_Cu, | |
| net_name="PCB_heater" | |
| ) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment