Last active
October 26, 2023 23:20
-
-
Save mcbridejc/86b6a5e57d9cf06f394ab0474bd5d158 to your computer and use it in GitHub Desktop.
Magnetic Train Track Kicad Plugin
This file contains 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
"""KiCad plugin to generate linear stepper track traces in KiCad""" | |
import pcbnew | |
import numpy as np | |
WIDTH=13.2 | |
PITCH=1.0 | |
WIDTH_MARGIN = 1.2 | |
LINE_WIDTH = 0.3 | |
VIA_DRILL = 0.3 | |
VIA_PAD = 0.6 | |
GUIDE_RAIL_SPACE = 9.0 | |
GUIDE_RAIL_WIDTH = 2.0 | |
CENTER_RAIL_WIDTH = 5.0 | |
def rotate(x, theta): | |
x = np.asarray(x) | |
R = np.array([ | |
[np.cos(theta), np.sin(theta)], | |
[-np.sin(theta), np.cos(theta)] | |
]) | |
return np.dot(R, x.T).T | |
def pcbpoint(p): | |
return pcbnew.wxPointMM(float(p[0]), float(p[1])) | |
def append_straight(board, start, cycles, angle): | |
start = np.array(start) | |
angle = np.deg2rad(angle) | |
for d in range(cycles): | |
a = np.array([ | |
(d*4 * PITCH, -(WIDTH / 2 + WIDTH_MARGIN)), | |
(d*4 * PITCH, WIDTH / 2 + WIDTH_MARGIN), | |
((d*4+2) * PITCH, WIDTH / 2 + WIDTH_MARGIN), | |
((d*4+2) * PITCH, -(WIDTH / 2 + WIDTH_MARGIN)), | |
((d*4+4) * PITCH, -(WIDTH / 2 + WIDTH_MARGIN)), | |
]) | |
b = np.array([ | |
((d*4 + 1) * PITCH, -WIDTH / 2), | |
((d*4 + 1) * PITCH, WIDTH / 2), | |
((d*4 + 3) * PITCH, WIDTH / 2), | |
((d*4 + 3) * PITCH, -WIDTH / 2), | |
((d*4 + 5) * PITCH, -WIDTH / 2), | |
]) | |
# Rotate points about (0, 0) by angle | |
a = rotate(a, angle) | |
b = rotate(b, angle) | |
a += start | |
b += start | |
for i in range(len(a)-1): | |
track = pcbnew.PCB_TRACK(board) | |
track.SetStart(pcbpoint(a[i])) | |
track.SetEnd(pcbpoint(a[i+1])) | |
track.SetWidth(int(LINE_WIDTH * 1e6)) | |
track.SetLayer(pcbnew.F_Cu) | |
board.Add(track) | |
for i in range(len(b) - 1): | |
if (i%2) == 0: | |
layer = pcbnew.F_Cu | |
else: | |
layer = pcbnew.B_Cu | |
track = pcbnew.PCB_TRACK(board) | |
track.SetStart(pcbpoint(b[i])) | |
track.SetEnd(pcbpoint(b[i+1])) | |
track.SetWidth(int(LINE_WIDTH * 1e6)) | |
track.SetLayer(layer) | |
board.Add(track) | |
via = pcbnew.PCB_VIA(board) | |
via.SetPosition(pcbpoint(b[i])) | |
via.SetDrill(int(VIA_DRILL * 1e6)) | |
via.SetWidth(int(VIA_PAD * 1e6)) | |
board.Add(via) | |
# Generate guide rails | |
def add_line(offset, thickness): | |
p0 = start + rotate((0, offset), angle) | |
p1 = start + rotate((cycles * PITCH * 4, offset), angle) | |
track = pcbnew.PCB_TRACK(board) | |
track.SetStart(pcbpoint(p0)) | |
track.SetEnd(pcbpoint(p1)) | |
track.SetWidth(int(thickness * 1e6)) | |
track.SetLayer(pcbnew.B_Cu) | |
board.Add(track) | |
add_line(-GUIDE_RAIL_SPACE / 2, GUIDE_RAIL_WIDTH) | |
add_line(GUIDE_RAIL_SPACE / 2, GUIDE_RAIL_WIDTH) | |
# return new starting point | |
return tuple(start + rotate(np.array((PITCH * cycles * 4, 0)), angle)) | |
def append_arc(board, start, radius, start_angle, arc_angle): | |
start = np.array(start) | |
arc_angle = np.deg2rad(arc_angle) | |
start_angle = np.deg2rad(start_angle) | |
# Pick an angle step so that the center pitch is maintained | |
# one PITCH step is 2*atan(PITCH / radius / 2), but we need to create a | |
# full 2-phase cycle, hence multiplied by 4, but after atan for better small | |
# angle approximation | |
ideal_angle_step = 8 * np.arctan(PITCH / radius / 2) | |
# Now we choose to compromise on pitch in order to achieve an exact angle | |
# Choose an angle step into which the total angle can divide evenly | |
n_angle_steps = int(np.round(arc_angle / ideal_angle_step)) | |
angle_step = arc_angle / n_angle_steps | |
# Compute the center of the arc | |
center = start + rotate(np.array([0, -radius]), start_angle) | |
# Define points as angle_step, radius offset (from center) | |
a = [ | |
(0, -WIDTH / 2 - WIDTH_MARGIN), | |
(0, WIDTH / 2 + WIDTH_MARGIN), | |
(2, WIDTH / 2 + WIDTH_MARGIN), | |
(2, -WIDTH / 2 - WIDTH_MARGIN), | |
(4, -WIDTH / 2 - WIDTH_MARGIN), | |
] | |
b = [ | |
(1, -WIDTH / 2), | |
(1, WIDTH / 2), | |
(3, WIDTH / 2), | |
(3, -WIDTH / 2), | |
(5, -WIDTH / 2), | |
] | |
def p2c(c, alpha, r): | |
return c + np.array([r * np.cos(alpha), -r * np.sin(alpha)]) | |
for n in range(n_angle_steps): | |
for i in range(len(a) - 1): | |
p1 = p2c(center, (n*4 + a[i][0]) * angle_step / 4 + start_angle - np.pi / 2, radius + a[i][1]) | |
p2 = p2c(center, (n*4 + a[i+1][0]) * angle_step / 4 + start_angle - np.pi / 2, radius + a[i+1][1]) | |
track = pcbnew.PCB_TRACK(board) | |
track.SetStart(pcbpoint(p1)) | |
track.SetEnd(pcbpoint(p2)) | |
track.SetWidth(int(LINE_WIDTH * 1e6)) | |
track.SetLayer(pcbnew.F_Cu) | |
board.Add(track) | |
for i in range(len(b) - 1): | |
p1 = p2c(center, (n*4 + b[i][0]) * angle_step / 4 + start_angle - np.pi / 2, radius + b[i][1]) | |
p2 = p2c(center, (n*4 + b[i+1][0]) * angle_step / 4 + start_angle - np.pi / 2, radius + b[i+1][1]) | |
if (i%2) == 0: | |
layer = pcbnew.F_Cu | |
else: | |
layer = pcbnew.B_Cu | |
track = pcbnew.PCB_TRACK(board) | |
track.SetStart(pcbpoint(p1)) | |
track.SetEnd(pcbpoint(p2)) | |
track.SetWidth(int(LINE_WIDTH * 1e6)) | |
track.SetLayer(layer) | |
board.Add(track) | |
via = pcbnew.PCB_VIA(board) | |
via.SetPosition(pcbpoint(p1)) | |
via.SetDrill(int(VIA_DRILL * 1e6)) | |
via.SetWidth(int(VIA_PAD * 1e6)) | |
board.Add(via) | |
# Add guard rails | |
def add_arc(offset, thickness): | |
arc_start = center + rotate((0, -radius + offset), start_angle - np.pi / 2) | |
arc_end = center + rotate(arc_start - center, -arc_angle) | |
arc_mid = center + rotate(arc_start - center, -arc_angle / 2) | |
track = pcbnew.PCB_ARC(board) | |
#track.SetPosition(pcbpoint(center)) | |
track.SetStart(pcbpoint(arc_start)) | |
track.SetMid(pcbpoint(arc_mid)) | |
track.SetEnd(pcbpoint(arc_end)) | |
track.SetWidth(int(thickness * 1e6)) | |
track.SetLayer(pcbnew.B_Cu) | |
board.Add(track) | |
add_arc(GUIDE_RAIL_SPACE / 2, GUIDE_RAIL_WIDTH) | |
add_arc(-GUIDE_RAIL_SPACE / 2, GUIDE_RAIL_WIDTH) | |
return tuple(center + rotate(start - center, arc_angle)) | |
class TrackLayout(pcbnew.ActionPlugin): | |
def defaults(self): | |
self.name = "Layout race track" | |
self.category = "Modify PCB" | |
self.description = "Layout race track" | |
self.show_toolbar_button = True | |
def Run(self): | |
board = pcbnew.GetBoard() | |
TURN_RADIUS = 30 | |
pos = (150, 150) | |
pos = append_straight(board, pos, 10, 0) | |
pos = append_arc(board, pos, TURN_RADIUS, 0, 90) | |
#pos = append_straight(board, pos, 5, 90) | |
pos = append_arc(board, pos, TURN_RADIUS, 90, 90) | |
pos = append_straight(board, pos, 10, 180) | |
pos = append_arc(board, pos, TURN_RADIUS, 180, 90) | |
#pos = append_straight(board, pos, 5, 270) | |
pos = append_arc(board, pos, TURN_RADIUS, 270, 90) | |
TrackLayout().register() |
This is simply brilliant! Thank you so much for sharing it :)
It seems, something has changed from Kicad 6 to 7. At least I couldn't get it to run anymore..
Hmm, that's not too surprising. I know there were a bunch of API changes in 7. I probably am not going to mess with this gist, but I guess I should figure out how to fix the other similar repos (e.g. CurvyCad) at some point...
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This uses numpy. I ran it on linux, where kicad uses the system python. On windows/mac, I believe kicad will be using its own python installation, which will probably not have numpy. See instructions here for installing new packages (i.e. numpy) in the kicad python env: https://github.com/mcbridejc/kicad_component_layout#how-to-install