Skip to content

Instantly share code, notes, and snippets.

@mcbridejc
Last active October 26, 2023 23:20
Show Gist options
  • Save mcbridejc/86b6a5e57d9cf06f394ab0474bd5d158 to your computer and use it in GitHub Desktop.
Save mcbridejc/86b6a5e57d9cf06f394ab0474bd5d158 to your computer and use it in GitHub Desktop.
Magnetic Train Track Kicad Plugin
"""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()
@mcbridejc
Copy link
Author

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

@beniroquai
Copy link

beniroquai commented Oct 26, 2023

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..

@mcbridejc
Copy link
Author

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