mirror of
https://github.com/whowechina/popn_pico.git
synced 2025-03-03 16:43:49 +01:00
260 lines
8.0 KiB
Python
260 lines
8.0 KiB
Python
|
"""
|
||
|
draw_mod.py
|
||
|
Copyright 2015 Adam Greig
|
||
|
Licensed under the MIT licence, see LICENSE file for details.
|
||
|
|
||
|
Render a .kicad_mod file to a PNG.
|
||
|
"""
|
||
|
|
||
|
from __future__ import print_function, division
|
||
|
|
||
|
import sys
|
||
|
import math
|
||
|
import cairo
|
||
|
|
||
|
from sexp import parse as sexp_parse
|
||
|
|
||
|
# Settings ====================================================================
|
||
|
|
||
|
# Gap between courtyard and image edge, in fraction of axis length
|
||
|
border_ratio = 0.02
|
||
|
|
||
|
# Image size in pixels
|
||
|
image_size = 512
|
||
|
|
||
|
# Background colour
|
||
|
bg_colour = (1, 1, 1, 1)
|
||
|
|
||
|
# Drill/hole colour
|
||
|
drill_colour = (0, 1, 1, .7)
|
||
|
|
||
|
# Colour mapping
|
||
|
colours = {
|
||
|
"F.CrtYd": (0, 0, 0, 1),
|
||
|
"F.SilkS": (1, 0, 1, 0.8),
|
||
|
"F.Fab": (0, 0, 1, 0.8),
|
||
|
"F.Cu": (1, 0, 0, 1),
|
||
|
"F.Mask": (0.5, 0, 1, 0.6),
|
||
|
"F.Paste": (0.6, 0.6, 0.6, 1),
|
||
|
}
|
||
|
|
||
|
# Layer stack (bottom first)
|
||
|
layer_stack = [
|
||
|
"F.Cu", "F.Mask", "F.Paste", "F.SilkS", "F.Fab", "F.CrtYd", "Drill"]
|
||
|
|
||
|
# End Settings ================================================================
|
||
|
|
||
|
|
||
|
def find_size(mod):
|
||
|
"""
|
||
|
Use the courtyard and a little padding to determine the footprint extent.
|
||
|
"""
|
||
|
left = right = top = bottom = 0
|
||
|
|
||
|
for line in (n for n in mod if n[0] == "fp_line"):
|
||
|
layer = [n for n in line if n[0] == "layer"][0]
|
||
|
if layer[1] in ("F.CrtYd", "B.CrtYd"):
|
||
|
start = [n for n in line if n[0] == "start"][0]
|
||
|
end = [n for n in line if n[0] == "end"][0]
|
||
|
for x, y in (start[1:], end[1:]):
|
||
|
x = float(x)
|
||
|
y = float(y)
|
||
|
left = min(x, left)
|
||
|
right = max(x, right)
|
||
|
top = min(y, top)
|
||
|
bottom = max(y, bottom)
|
||
|
|
||
|
width = right - left
|
||
|
height = bottom - top
|
||
|
|
||
|
left -= width * border_ratio
|
||
|
right += width * border_ratio
|
||
|
top -= height * border_ratio
|
||
|
bottom += height * border_ratio
|
||
|
|
||
|
return left, right, top, bottom
|
||
|
|
||
|
|
||
|
def draw_line(ctxs, draw):
|
||
|
layer = [n for n in draw if n[0] == "layer"][0][1]
|
||
|
if layer in ctxs:
|
||
|
ctx = ctxs[layer]
|
||
|
rgba = colours[layer]
|
||
|
width = [n for n in draw if n[0] == "width"][0][1]
|
||
|
ctx.set_source_rgba(*rgba)
|
||
|
ctx.set_line_width(float(width))
|
||
|
ctx.set_line_cap(cairo.LINE_CAP_ROUND)
|
||
|
if draw[0] == "fp_line":
|
||
|
start = [n for n in draw if n[0] == "start"][0]
|
||
|
end = [n for n in draw if n[0] == "end"][0]
|
||
|
ctx.move_to(float(start[1]), float(start[2]))
|
||
|
ctx.line_to(float(end[1]), float(end[2]))
|
||
|
elif draw[0] == "fp_circle":
|
||
|
center = [n for n in draw if n[0] == "center"][0]
|
||
|
end = [n for n in draw if n[0] == "end"][0]
|
||
|
dx = float(end[1]) - float(center[1])
|
||
|
dy = float(end[2]) - float(center[2])
|
||
|
r = math.sqrt(dx**2 + dy**2)
|
||
|
ctx.new_sub_path()
|
||
|
ctx.arc(float(center[1]), float(center[2]), r, 0, 2*math.pi)
|
||
|
elif draw[0] == "fp_arc":
|
||
|
start = [n for n in draw if n[0] == "start"][0]
|
||
|
end = [n for n in draw if n[0] == "end"][0]
|
||
|
angle = [n for n in draw if n[0] == "angle"][0]
|
||
|
dx = float(end[1]) - float(start[1])
|
||
|
dy = float(end[2]) - float(start[2])
|
||
|
r = math.sqrt(dx**2 + dy**2)
|
||
|
a_start = math.atan2(dy, dx)
|
||
|
a_end = a_start + float(angle[1]) * (math.pi / 180.0)
|
||
|
ctx.new_sub_path()
|
||
|
ctx.arc(float(start[1]), float(start[2]), r, a_start, a_end)
|
||
|
ctx.stroke()
|
||
|
|
||
|
|
||
|
def hatch(positive, rgba):
|
||
|
hs = 64
|
||
|
hatch = cairo.ImageSurface(cairo.FORMAT_ARGB32, hs, hs)
|
||
|
hctx = cairo.Context(hatch)
|
||
|
if positive:
|
||
|
hctx.move_to(0, 0)
|
||
|
hctx.line_to(hs, hs)
|
||
|
else:
|
||
|
hctx.move_to(0, hs)
|
||
|
hctx.line_to(hs, 0)
|
||
|
hctx.set_line_width(16)
|
||
|
hctx.set_source_rgba(*rgba)
|
||
|
hctx.stroke()
|
||
|
hpat = cairo.SurfacePattern(hatch)
|
||
|
hpat.set_extend(cairo.EXTEND_REPEAT)
|
||
|
hpat.set_matrix(cairo.Matrix(xx=image_size, yy=image_size))
|
||
|
return hpat
|
||
|
|
||
|
|
||
|
hatch_mask = hatch(True, colours["F.Mask"])
|
||
|
hatch_paste = hatch(False, colours["F.Paste"])
|
||
|
|
||
|
|
||
|
def pad_all_layers_front(layers):
|
||
|
for idx, layer in enumerate(layers):
|
||
|
if layer[0] == "*":
|
||
|
layers[idx] = "F" + layer[1:]
|
||
|
|
||
|
|
||
|
def pad_drill(drill, centre, ctx):
|
||
|
try:
|
||
|
drill_size = float(drill[0][1])
|
||
|
except ValueError:
|
||
|
pass
|
||
|
else:
|
||
|
ctx.arc(centre[0], centre[1], drill_size/2.0, 0, 2*math.pi)
|
||
|
ctx.set_source_rgba(*drill_colour)
|
||
|
ctx.fill()
|
||
|
offset = [n for n in drill[0] if n[0] == "offset"]
|
||
|
if offset:
|
||
|
centre[0] += float(offset[0][1])
|
||
|
centre[1] += float(offset[0][2])
|
||
|
|
||
|
|
||
|
def pad_margins(pad):
|
||
|
mask_margin = [n for n in pad if n[0] == "solder_mask_margin"]
|
||
|
paste_margin = [n for n in pad if n[0] == "solder_paste_margin"]
|
||
|
paste_ratio = [n for n in pad if n[0] == "solder_paste_ratio"]
|
||
|
mask_margin = float(mask_margin[0][1]) if mask_margin else 0
|
||
|
paste_margin = float(paste_margin[0][1]) if paste_margin else 0
|
||
|
paste_ratio = float(paste_ratio[0][1]) if paste_ratio else 0
|
||
|
return mask_margin, paste_margin, paste_ratio
|
||
|
|
||
|
|
||
|
def draw_pad(ctxs, pad):
|
||
|
shape = pad[3]
|
||
|
layers = [n for n in pad if n[0] == "layers"][0][1:]
|
||
|
pad_all_layers_front(layers)
|
||
|
centre = [float(v) for v in [n for n in pad if n[0] == "at"][0][1:]]
|
||
|
size = [float(v) for v in [n for n in pad if n[0] == "size"][0][1:]]
|
||
|
|
||
|
drill = [n for n in pad if n[0] == "drill"]
|
||
|
if drill:
|
||
|
pad_drill(drill, centre, ctxs['Drill'])
|
||
|
mask_margin, paste_margin, paste_ratio = pad_margins(pad)
|
||
|
|
||
|
for layer in ["F.Cu", "F.Mask", "F.Paste"]:
|
||
|
if layer in layers and layer in ctxs:
|
||
|
ctx = ctxs[layer]
|
||
|
rgba = colours[layer]
|
||
|
if layer.endswith("Mask"):
|
||
|
size[0] += 2*mask_margin
|
||
|
size[1] += 2*mask_margin
|
||
|
ctx.set_source(hatch_mask)
|
||
|
elif layer.endswith("Paste"):
|
||
|
size[0] += 2*paste_margin
|
||
|
size[1] += 2*paste_margin
|
||
|
size[0] += 2*paste_ratio*size[0]
|
||
|
size[1] += 2*paste_ratio*size[1]
|
||
|
ctx.set_source(hatch_paste)
|
||
|
elif layer.endswith("Cu"):
|
||
|
ctx.set_source_rgba(*rgba)
|
||
|
|
||
|
if shape == "rect":
|
||
|
x = centre[0] - size[0]/2.0
|
||
|
y = centre[1] - size[1]/2.0
|
||
|
ctx.rectangle(x, y, size[0], size[1])
|
||
|
elif shape == "circle":
|
||
|
ctx.arc(centre[0], centre[1], size[0]/2.0, 0, 2*math.pi)
|
||
|
else:
|
||
|
return
|
||
|
|
||
|
ctx.fill()
|
||
|
|
||
|
|
||
|
def draw(mod):
|
||
|
surf = cairo.ImageSurface(cairo.FORMAT_ARGB32, image_size, image_size)
|
||
|
ctx = cairo.Context(surf)
|
||
|
ctx.set_source_rgba(*bg_colour)
|
||
|
ctx.paint()
|
||
|
|
||
|
# Set up the context to map footprint coordinates to image coordinates
|
||
|
# Footprint is in mm, with the origin at the centre, x increasing right,
|
||
|
# and y increasing down.
|
||
|
# Image is in pixels, with the origin at the top left, x increasing right,
|
||
|
# and y increasing down.
|
||
|
left, right, top, bottom = find_size(mod)
|
||
|
length = float(max(right - left, bottom - top))
|
||
|
|
||
|
ctxs = {}
|
||
|
for layer in layer_stack:
|
||
|
lsurf = cairo.ImageSurface(cairo.FORMAT_ARGB32, image_size, image_size)
|
||
|
lctx = cairo.Context(lsurf)
|
||
|
lctx.scale(image_size/length, image_size/length)
|
||
|
lctx.translate(right, bottom)
|
||
|
ctxs[layer] = lctx
|
||
|
|
||
|
for pad in (n for n in mod if n[0] == "pad"):
|
||
|
draw_pad(ctxs, pad)
|
||
|
|
||
|
draw_types = ("fp_line", "fp_circle", "fp_arc")
|
||
|
for draw in (n for n in mod if n[0] in draw_types):
|
||
|
draw_line(ctxs, draw)
|
||
|
|
||
|
for layer in layer_stack:
|
||
|
lctx = ctxs[layer]
|
||
|
ctx.set_source_surface(lctx.get_target())
|
||
|
ctx.paint()
|
||
|
|
||
|
return surf
|
||
|
|
||
|
|
||
|
def main(modpath, outpath):
|
||
|
with open(modpath) as f:
|
||
|
sexp = sexp_parse(f.read())
|
||
|
img = draw(sexp)
|
||
|
img.write_to_png(outpath)
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
if len(sys.argv) == 3:
|
||
|
modpath = sys.argv[1]
|
||
|
outpath = sys.argv[2]
|
||
|
main(modpath, outpath)
|
||
|
else:
|
||
|
print("Usage: {} <.kicad_mod file> <output file>".format(sys.argv[0]))
|
||
|
sys.exit(0)
|