#!/usr/bin/python from math import pi, sin, cos, sqrt, ceil import pyparsing as PYP import re from lxml import etree as et import pcbmode.config as config # import pcbmode modules from . import utils from .point import Point def svg_grammar(): """ Returns the grammar used to parse SVG paths """ # pyparsing grammar comma = PYP.Literal(",").suppress() # supress removes the ',' when captured dot = PYP.Literal(".") space = PYP.Literal(' ') coord = PYP.Regex(r"-?\d+(\.\d*)?([Ee][+-]?\d+)?") one_coord = PYP.Group(coord) xycoords = PYP.Group(coord + PYP.Optional(comma) + coord) two_xycoords = xycoords + PYP.Optional(comma) + xycoords three_xycoords = xycoords + PYP.Optional(comma) + xycoords + PYP.Optional(comma)+xycoords # TODO optimise this; there has to be a more efficient way to describe this c_M = PYP.Literal('M') + PYP.OneOrMore(xycoords) c_m = PYP.Literal('m') + PYP.OneOrMore(xycoords) c_C = PYP.Literal('C') + PYP.OneOrMore(three_xycoords) c_c = PYP.Literal('c') + PYP.OneOrMore(three_xycoords) c_Q = PYP.Literal('Q') + PYP.OneOrMore(two_xycoords) c_q = PYP.Literal('q') + PYP.OneOrMore(two_xycoords) c_T = PYP.Literal('T') + PYP.OneOrMore(xycoords) c_t = PYP.Literal('t') + PYP.OneOrMore(xycoords) c_L = PYP.Literal('L') + PYP.OneOrMore(xycoords) c_l = PYP.Literal('l') + PYP.OneOrMore(xycoords) c_V = PYP.Literal('V') + PYP.OneOrMore(one_coord) c_v = PYP.Literal('v') + PYP.OneOrMore(one_coord) c_H = PYP.Literal('H') + PYP.OneOrMore(one_coord) c_h = PYP.Literal('h') + PYP.OneOrMore(one_coord) c_S = PYP.Literal('S') + PYP.OneOrMore(two_xycoords) c_s = PYP.Literal('s') + PYP.OneOrMore(two_xycoords) c_z = PYP.Literal('z') c_Z = PYP.Literal('Z') path_cmd = c_M | c_m | c_C | c_c | c_Q | c_q | c_T | c_t | c_L | c_l | c_V | c_v | c_H | c_h | c_S | c_s | c_Z | c_z # return the format we're after return PYP.OneOrMore(PYP.Group(path_cmd)) def absolute_to_relative_path(path): """ Converts an SVG path into a path that has only relative commands. This basically allows taking paths from anywhere and placing them in a new SVG. """ # TODO: add support all commands # TODO: optimise 'switch' logic # check to see if path is empty or doesn't exist if (path == None) or (path == ''): return # get SVG path grammar look_for = svg_grammar() # parse the input based on this grammar pd = look_for.parseString(path) p = '' # This variable stores the absolute coordinates as the path is converted; abspos = Point() patho = Point() for i in range(0, len(pd)): # 'move to' command if re.match('M', pd[i][0], re.I): # TODO: write this code more concisely coord = Point(pd[i][1][0], pd[i][1][1]) p += 'm ' # if this is the start of the path, the first M/m coordinate is # always absolute if i == 0: abspos.assign(coord.x, coord.y) p += str(abspos.x) + ',' + str(abspos.y) + ' ' patho.assign(coord.x, coord.y) else: if pd[i][0] == 'm': p += str(coord.x) + ',' + str(coord.y) + ' ' abspos += coord patho = abspos else: p += str(coord.x - abspos.x) + ',' + str(coord.y - abspos.y) + ' ' abspos.assign(coord.x, coord.y) patho.assign(coord.x, coord.y) # do the rest of the coordinates for coord_tmp in pd[i][2:]: coord.assign(coord_tmp[0], coord_tmp[1]) if pd[i][0] == 'm': p += str(coord.x) + ',' + str(coord.y) + ' ' abspos += coord else: p += str(coord.x - abspos.x) + ',' + str(coord.y - abspos.y) + ' ' abspos.assign(coord.x, coord.y) # cubic Bezier (PCCP) curve command elif re.match('C', pd[i][0], re.I): p += pd[i][0].lower()+' ' if pd[i][0] == 'c': for coord_tmp in pd[i][1:]: coord.assign(coord_tmp[0], coord_tmp[1]) p += str(coord.x) + ',' + str(coord.y) +' ' # for keeping track of the absolute position, we need to add up every # *third* coordinate of the cubic Bezier curve for coord_tmp in pd[i][3::3]: coord.assign(coord_tmp[0], coord_tmp[1]) abspos += coord if pd[i][0] == 'C': for n in range(1, len(pd[i])-1, 3): for m in range(0, 3): coord.assign(pd[i][n+m][0], pd[i][n+m][1]) p += str(coord.x - abspos.x) + ',' + str(coord.y - abspos.y)+' ' abspos.assign(coord.x, coord.y) # quadratic Bezier (PCP) curve command elif re.match('Q', pd[i][0], re.I): p += pd[i][0].lower() + ' ' if pd[i][0] == 'q': for coord_tmp in pd[i][1:]: coord.assign(coord_tmp[0], coord_tmp[1]) p += str(coord.x) + ',' + str(coord.y) +' ' # for keeping track of the absolute position, we need to add up every # *third* coordinate of the cubic Bezier curve for coord_tmp in pd[i][2::2]: coord.assign(coord_tmp[0], coord_tmp[1]) abspos += coord if pd[i][0] == 'Q': for coord_tmp in pd[i][1:]: coord.assign(coord_tmp[0], coord_tmp[1]) p += str(coord.x - abspos.x) + ',' + str(coord.y - abspos.y) + ' ' abspos.assign(coord.x, coord.y) # simple cubic Bezier curve command elif re.match('T', pd[i][0], re.I): p += pd[i][0].lower()+' ' if pd[i][0] == 't': for coord_tmp in pd[i][1:]: coord.assign(coord_tmp[0], coord_tmp[1]) p += str(coord.x) + ',' + str(coord.y) + ' ' # for keeping track of the absolute position, we need to add up every # *third* coordinate of the cubic Bezier curve #for coord in pd[i][2::2]: abspos += coord if pd[i][0] == 'T': for coord_tmp in pd[i][1:]: coord.assign(coord_tmp[0], coord_tmp[1]) p += str(float(coord[0]) - abspos['x']) + ',' + str(float(coord[1]) - abspos['y']) + ' ' abspos.assign(coord.x, coord.y) elif re.match('S', pd[i][0], re.I): p += pd[i][0].lower()+' ' if pd[i][0] == 's': for coord_tmp in pd[i][1:]: coord.assign(coord_tmp[0], coord_tmp[1]) p += str(coord.x)+','+str(coord.y)+' ' abspos += coord if pd[i][0] == 'S': for coord_tmp in pd[i][1:]: coord.assign(coord_tmp[0], coord_tmp[1]) p += str(coord.x - abspos.x) + ',' + str(coord.y - abspos.y) + ' ' abspos.assign(coord.x, coord.y) # 'line to' command elif re.match('L', pd[i][0], re.I): p += pd[i][0].lower()+' ' if pd[i][0] == 'l': for coord_tmp in pd[i][1:]: coord.assign(coord_tmp[0], coord_tmp[1]) p += str(coord.x) + ',' + str(coord.y) + ' ' abspos += coord if pd[i][0] == 'L': for coord_tmp in pd[i][1:]: coord.assign(coord_tmp[0], coord_tmp[1]) p += str(coord.x - abspos.x) + ',' + str(coord.y - abspos.y) + ' ' abspos.assign(coord.x, coord.y) # 'horizontal line' command elif re.match('H', pd[i][0], re.I): p += pd[i][0].lower()+' ' if pd[i][0] == 'h': for coord_tmp in pd[i][1:]: coord.assign(coord_tmp[0], 0) p += str(coord.x) + ' ' abspos.x += coord.x if pd[i][0] == 'H': for coord_tmp in pd[i][1:]: coord.assign(coord_tmp[0], coord_tmp[1]) p += str(coord.x - abspos.x) + ' ' abspos.x = coord.x # 'vertical line' command elif re.match('V', pd[i][0], re.I): p += pd[i][0].lower() + ' ' if pd[i][0] == 'v': for coord_tmp in pd[i][1:]: coord.assign(0, coord_tmp[0]) p += str(coord.y) + ' ' abspos.y += coord.y if pd[i][0] == 'V': for coord_tmp in pd[i][1:]: coord.assign(0, coord_tmp[0]) p += str(coord.y - abspos.y) + ' ' abspos.y = coord.y # 'close shape' command elif re.match('Z', pd[i][0], re.I): p += pd[i][0].lower() + ' ' abspos = abspos + (patho - abspos) else: print("ERROR: found an unsupported SVG path command " + str(pd[i][0])) # return the relative path return p def relative_svg_path_to_absolute_coord_list(path, bezier_steps=100, segment_length=0.05): """ return a list of absolute coordinates from an SVG *relative* path """ # get SVG path grammar look_for = svg_grammar() # parse the input based on this grammar pd = look_for.parseString(path) # absolute position ap = Point() # path origin po = Point() points = [] p = [] last_bezier_control_point = Point() for i in range(0, len(pd)): cmd = pd[i][0] # 'move to' command if re.match('m', cmd): if i == 0: coord = Point(pd[i][1][0], pd[i][1][1]) ap.assign(coord.x, coord.y) p.append(ap) po.assign(coord.x, coord.y) else: coord_tmp = Point(pd[i][1][0], pd[i][1][1]) ap += coord_tmp # a marker that a new path is starting after a previous one closed points.append(p) p = [] p.append(ap) po = ap for coord_tmp in pd[i][2:]: coord = Point(coord_tmp[0], coord_tmp[1]) ap += coord p.append(ap) # cubic (two control points) Bezier curve command elif re.match('c', cmd): bezier_curve_path = [] for n in range(1, len(pd[i])-1, 3): bezier_curve_path.append(ap) for m in range(0, 3): coord = pd[i][n+m] point = Point(coord[0], coord[1]) bezier_curve_path.append(ap + point) new_point = Point(pd[i][n+m][0], pd[i][n+m][1]) ap += new_point for n in range(0, len(bezier_curve_path), 4): # clear bezier point arrays bezier_points_x = [] bezier_points_y = [] # split points of bezier into 'x' and 'y' coordinate arrays # as this is what the point array function expects for m in range(0, 4): bezier_points_x.append(bezier_curve_path[n+m].x) bezier_points_y.append(bezier_curve_path[n+m].y) # caluclate the individual points along the bezier curve for 'x' # and 'y' points_x = calculate_points_of_cubic_bezier(bezier_points_x, bezier_steps) points_y = calculate_points_of_cubic_bezier(bezier_points_y, bezier_steps) path_length = calculate_cubic_bezier_length(points_x, points_y) if path_length == 0: steps = 1 else: steps = ceil(path_length / segment_length) skip = int(ceil(bezier_steps / steps)) bezier_point_array = [] # put thos points back into a Point type array for n in range(0, len(points_x), skip): bezier_point_array.append(Point(points_x[n], points_y[n])) bezier_point_array.append(Point(points_x[len(points_x)-1], points_y[len(points_x)-1])) p += bezier_point_array # quadratic (single control point) Bezier curve command elif re.match('q', cmd): bezier_curve_path = [] for n in range(1, len(pd[i])-1, 2): bezier_curve_path.append(ap) for m in range(0, 2): coord = pd[i][n+m] point = Point(coord[0], coord[1]) bezier_curve_path.append(ap + point) # inject a second, identical control point so this quadratic # bezier looks like a cubic one if m == 1: bezier_curve_path.append(ap+point) if m == 0: last_bezier_control_point = ap + point new_point = Point(pd[i][n+m][0], pd[i][n+m][1]) ap += new_point for n in range(0, len(bezier_curve_path), 4): # clear bezier point arrays bezier_points_x = [] bezier_points_y = [] # split points of bezier into 'x' and 'y' coordinate arrays # as this is what the point array function expects for m in range(0, 4): bezier_points_x.append(bezier_curve_path[n+m].x) bezier_points_y.append(bezier_curve_path[n+m].y) # caluclate the individual points along the bezier curve for 'x' # and 'y' points_x = calculate_points_of_cubic_bezier(bezier_points_x, bezier_steps) points_y = calculate_points_of_cubic_bezier(bezier_points_y, bezier_steps) path_length = calculate_cubic_bezier_length(points_x, points_y) skip = int(ceil(bezier_steps / (path_length / segment_length))) bezier_point_array = [] # put those points back into a Point type array for n in range(0, len(points_x), skip): bezier_point_array.append(Point(points_x[n], points_y[n])) bezier_point_array.append(Point(points_x[len(points_x)-1], points_y[len(points_x)-1])) p += bezier_point_array # simple cubic Bezier curve command elif re.match('t', cmd): bezier_curve_path = [] for n in range(1, len(pd[i])): bezier_curve_path.append(ap) coord = pd[i][n] point = Point(coord[0], coord[1]) end_point = ap + point diff = Point(ap.x - last_bezier_control_point.x, ap.y - last_bezier_control_point.y) control_point = ap + diff bezier_curve_path.append(control_point) bezier_curve_path.append(end_point) bezier_curve_path.append(end_point) last_bezier_control_point = control_point new_point = Point(pd[i][n][0], pd[i][n][1]) ap += new_point for n in range(0, len(bezier_curve_path), 4): # clear bezier point arrays bezier_points_x = [] bezier_points_y = [] # split points of bezier into 'x' and 'y' coordinate arrays # as this is what the point array function expects for m in range(0, 4): bezier_points_x.append(bezier_curve_path[n+m].x) bezier_points_y.append(bezier_curve_path[n+m].y) # caluclate the individual points along the bezier curve for 'x' # and 'y' points_x = calculate_points_of_cubic_bezier(bezier_points_x, bezier_steps) points_y = calculate_points_of_cubic_bezier(bezier_points_y, bezier_steps) path_length = calculate_cubic_bezier_length(points_x, points_y) skip = int(ceil(bezier_steps / (path_length / segment_length))) bezier_point_array = [] # put those points back into a Point type array for m in range(0, len(points_x), skip): bezier_point_array.append(Point(points_x[m], points_y[m])) bezier_point_array.append(Point(points_x[len(points_x)-1], points_y[len(points_x)-1])) p += bezier_point_array # elif re.match('s', cmd): # pass # 'line to' command elif re.match('l', cmd): for coord_tmp in pd[i][1:]: coord = Point(coord_tmp[0], coord_tmp[1]) ap += coord p.append(ap) # 'horizontal line' command elif re.match('h', cmd): for coord_tmp in pd[i][1:]: coord = Point(coord_tmp[0], 0) ap += coord p.append(ap) # 'vertical line' command elif re.match('v', cmd): for coord_tmp in pd[i][1:]: coord = Point(0, coord_tmp[0]) ap += coord p.append(ap) # 'close shape' command elif re.match('z', cmd): ap = ap + (po - ap) else: print("ERROR: found an unsupported SVG path command "+ str(cmd)) points.append(p) return points def mirror_path_over_axis(path, axis, width): """ mirrors a path horizontally by first converting it to a relative path and then mirrors it either horizontally or vertically by negating the x or y axis coordinates """ # TODO: add vertical flipping ;) # check to see if path is empty or doesn't exist if (path == None) or (path == ''): return # convert path to relative coordinates; this simplifies the mirroring relative_path = absolute_to_relative_path(path) # get SVG path grammar look_for = svg_grammar() # parse the input based on this grammar pd = look_for.parseString(relative_path) p = '' for i in range(0, len(pd)): pcmd = pd[i][0] if re.match('m', pcmd): if i == 0: p += 'm ' + str(width - float(pd[i][1][0])) + ',' + str(pd[i][1][1]) + ' ' else: p += 'm ' + str(-float(pd[i][1][0])) + ',' + str(pd[i][1][1]) + ' ' for coord in pd[i][2:]: p += str(-float(coord[0])) + ',' + coord[1] + ' ' else: p += pd[i][0]+' ' for coord in pd[i][1:]: if len(coord) > 1: p += str(-float(coord[0])) + ',' + str(float(coord[1])) + ' ' else: if pd[i][0] == 'h': p += str(-float(coord[0])) + ' ' else: p += str(float(coord[0])) + ' ' return p def boundary_box_check(tl, br, p): new_tl = Point(tl.x, tl.y) new_br = Point(br.x, br.y) if p.x > br.x: new_br.x = p.x if p.x < tl.x: new_tl.x = p.x if p.y > tl.y: new_tl.y = p.y if p.y < br.y: new_br.y = p.y return new_tl, new_br def calculate_bounding_box_of_path(path): """ Calculates the bounding box of an SVG path """ # convert path to relative relative_path = absolute_to_relative_path(path) # get SVG path grammar look_for = svg_grammar() # parse the input based on this grammar pd = look_for.parseString(relative_path) last_point = Point() abs_point = Point() bbox_top_left = Point() bbox_bot_right = Point() # for the t/T (shorthand bezier) command, we need to keep track # of the last bezier control point from previous Q/q/T/t command last_bezier_control_point = Point() for i in range(0, len(pd)): # 'move to' command if re.match('m', pd[i][0]): if i == 0: # the first coordinate is the start of both top left and bottom right abs_point.assign(pd[i][1][0], pd[i][1][1]) bbox_top_left.assign(pd[i][1][0], pd[i][1][1]) bbox_bot_right.assign(pd[i][1][0], pd[i][1][1]) else: new_point = Point(pd[i][1][0], pd[i][1][1]) abs_point += new_point bbox_top_left, bbox_bot_right = boundary_box_check(bbox_top_left, bbox_bot_right, abs_point) # for the rest of the coordinates for coord in pd[i][2:]: new_point = Point(coord[0], coord[1]) abs_point += new_point bbox_top_left, bbox_bot_right = boundary_box_check(bbox_top_left, bbox_bot_right, abs_point) # cubic Bezier curve command elif re.match('c', pd[i][0]): bezier_curve_path = [] for n in range(1, len(pd[i])-1, 3): bezier_curve_path.append(abs_point) for m in range(0, 3): coord = pd[i][n+m] point = Point(coord[0], coord[1]) bezier_curve_path.append(abs_point + point) new_point = Point(pd[i][n+m][0], pd[i][n+m][1]) abs_point += new_point for n in range(0, len(bezier_curve_path), 4): # clear bezier point arrays bezier_points_x = [] bezier_points_y = [] # split points of bezier into 'x' and 'y' coordinate arrays # as this is what the point array function expects for m in range(0, 4): bezier_points_x.append(bezier_curve_path[n+m].x) bezier_points_y.append(bezier_curve_path[n+m].y) # caluclate the individual points along the bezier curve for 'x' # and 'y' points_x = calculate_points_of_cubic_bezier(bezier_points_x, 100) points_y = calculate_points_of_cubic_bezier(bezier_points_y, 100) bezier_point_array = [] # put those points back into a Point type array for n in range(0, len(points_x)): bezier_point_array.append(Point(points_x[n], points_y[n])) # check each point if it extends the boundary box for n in range(0, len(bezier_point_array)): bbox_top_left, bbox_bot_right = boundary_box_check( bbox_top_left, bbox_bot_right, bezier_point_array[n]) # quadratic Bezier curve command elif re.match('q', pd[i][0]): bezier_curve_path = [] for n in range(1, len(pd[i])-1, 2): bezier_curve_path.append(abs_point) for m in range(0, 2): coord = pd[i][n+m] point = Point(coord[0], coord[1]) bezier_curve_path.append(abs_point + point) # inject a second, identical control point so this quadratic # bezier looks like a cubic one if m == 1: bezier_curve_path.append(abs_point + point) if m == 0: last_bezier_control_point = abs_point + point new_point = Point(pd[i][n+m][0], pd[i][n+m][1]) abs_point += new_point for n in range(0, len(bezier_curve_path), 4): # clear bezier point arrays bezier_points_x = [] bezier_points_y = [] # split points of bezier into 'x' and 'y' coordinate arrays # as this is what the point array function expects for m in range(0, 4): bezier_points_x.append(bezier_curve_path[n+m].x) bezier_points_y.append(bezier_curve_path[n+m].y) # caluclate the individual points along the bezier curve for 'x' # and 'y' points_x = calculate_points_of_cubic_bezier(bezier_points_x, 100) points_y = calculate_points_of_cubic_bezier(bezier_points_y, 100) bezier_point_array = [] # put those points back into a Point type array for n in range(0, len(points_x)): bezier_point_array.append(Point(points_x[n], points_y[n])) # check each point if it extends the boundary box for n in range(0, len(bezier_point_array)): bbox_top_left, bbox_bot_right = boundary_box_check( bbox_top_left, bbox_bot_right, bezier_point_array[n]) # simple cubic Bezier curve command elif re.match('t', pd[i][0]): bezier_curve_path = [] for n in range(1, len(pd[i])): bezier_curve_path.append(abs_point) coord = pd[i][n] point = Point(coord[0], coord[1]) end_point = abs_point + point diff = Point(abs_point.x - last_bezier_control_point.x, abs_point.y - last_bezier_control_point.y) control_point = abs_point + diff bezier_curve_path.append(control_point) bezier_curve_path.append(end_point) bezier_curve_path.append(end_point) last_bezier_control_point = control_point new_point = Point(pd[i][n][0], pd[i][n][1]) abs_point += new_point for n in range(0, len(bezier_curve_path), 4): # clear bezier point arrays bezier_points_x = [] bezier_points_y = [] # split points of bezier into 'x' and 'y' coordinate arrays # as this is what the point array function expects for m in range(0, 4): bezier_points_x.append(bezier_curve_path[n+m].x) bezier_points_y.append(bezier_curve_path[n+m].y) # caluclate the individual points along the bezier curve for 'x' # and 'y' points_x = calculate_points_of_cubic_bezier(bezier_points_x, 100) points_y = calculate_points_of_cubic_bezier(bezier_points_y, 100) bezier_point_array = [] # put those points back into a Point type array for n in range(0, len(points_x)): bezier_point_array.append(Point(points_x[n], points_y[n])) # check each point if it extends the boundary box for m in range(0, len(bezier_point_array)): bbox_top_left, bbox_bot_right = boundary_box_check( bbox_top_left, bbox_bot_right, bezier_point_array[m]) # elif re.match('S', pd[i][0], re.I): # pass # 'line to' command elif re.match('l', pd[i][0]): for coord in pd[i][1:]: new_point = Point(coord[0], coord[1]) abs_point += new_point bbox_top_left, bbox_bot_right = boundary_box_check(bbox_top_left, bbox_bot_right, abs_point) # 'horizontal line' command elif re.match('h', pd[i][0]): for coord in pd[i][1:]: new_point = Point(coord[0], 0) abs_point += new_point bbox_top_left, bbox_bot_right = boundary_box_check(bbox_top_left, bbox_bot_right, abs_point) # 'vertical line' command elif re.match('v', pd[i][0]): for coord in pd[i][1:]: new_point = Point(0, coord[0]) abs_point += new_point bbox_top_left, bbox_bot_right = boundary_box_check(bbox_top_left, bbox_bot_right, abs_point) # 'close shape' command elif re.match('Z', pd[i][0], re.I): pass else: print("ERROR: found an unsupported SVG path command " + str(pd[i][0])) return bbox_top_left, bbox_bot_right def calculate_points_of_cubic_bezier(p, steps = 10): """ This function receives four points [start, control, control, end] and returns points on the cubic Bezier curve that they define. As 'steps' decreases, so do the amount of points that are returned, making the curve less, well, curvey. The code for this function was adapted/copied from: http://www.niksula.cs.hut.fi/~hkankaan/Homepages/bezierfast.html http://www.pygame.org/wiki/BezierCurve """ t = 1.0 / steps temp = t*t f = p[0] fd = 3 * (p[1] - p[0]) * t fdd_per_2 = 3 * (p[0] - 2 * p[1] + p[2]) * temp fddd_per_2 = 3 * (3 * (p[1] - p[2]) + p[3] - p[0]) * temp * t fddd = 2 * fddd_per_2 fdd = 2 * fdd_per_2 fddd_per_6 = fddd_per_2 / 3.0 points = [] for x in range(steps): points.append(f) f += fd + fdd_per_2 + fddd_per_6 fd += fdd + fddd_per_2 fdd += fddd fdd_per_2 += fddd_per_2 points.append(f) return points def transform_path(p, center=False, scale=1, rotate_angle=0, rotate_point=Point()): """ transforms a path """ p_tl, p_br = calculate_bounding_box_of_path(p) width, height = get_width_and_height_of_shape_from_two_points(p_tl, p_br) # get SVG path grammar look_for = svg_grammar() # parse the input based on this grammar pd = look_for.parseString(p) # first point of path first_point = Point(pd[0][1][0], pd[0][1][1]) if center is True: # center point of path origin_point = Point(p_tl.x+width/2, p_tl.y-height/2) # caluclate what's the new starting point of path based on the new origin new_first_point = Point(first_point.x - origin_point.x, first_point.y - origin_point.y) else: new_first_point = Point(first_point.x, first_point.y) new_first_point.rotate(rotate_angle, rotate_point) new_first_point.mult(scale) new_p = "m %f,%f " % (new_first_point.x, new_first_point.y) tmpp = Point() origin = Point() for n in range(0, len(pd)): if pd[n][0] == 'm' and n == 0: for m in range(2, len(pd[n])): tmpp.assign(pd[n][m][0], pd[n][m][1]) tmpp.rotate(rotate_angle, rotate_point) tmpp.mult(scale) new_p += str(tmpp.x) + "," + str(tmpp.y) + " " else: if pd[n][0] == 'h' or pd[n][0] == 'v': new_p += "l " else: new_p += pd[n][0] + " " for m in range(1, len(pd[n])): if pd[n][0] == 'h': tmpp.assign(pd[n][m][0], 0) elif pd[n][0] == 'v': tmpp.assign(0, pd[n][m][0]) else: tmpp.assign(pd[n][m][0], pd[n][m][1]) tmpp.rotate(rotate_angle, rotate_point) tmpp.mult(scale) new_p += str(tmpp.x) + "," + str(tmpp.y) + " " return width, height, new_p def get_width_and_height_of_shape_from_two_points(tl, br): """ SVG's origin is top left so we need to take the absolute value, otherwise the length will be negative (alternatively, we can do tl.y - br.y) """ return (br.x - tl.x), abs(br.y - tl.y) # width, height def width_and_height_to_path(width, height, radii=None): """ Returns a centered path based on width and height; smooth corners can be defined with radii """ width = float(width) height = float(height) # The calculation to obtain the 'k' coefficient can be found here: # http://itc.ktu.lt/itc354/Riskus354.pdf # "APPROXIMATION OF A CUBIC BEZIER CURVE BY CIRCULAR ARCS AND VICE VERSA" # by Aleksas Riskus k = 0.5522847498 all_zeros = True if radii is not None: # check if all values are equal to '0' for value in radii.values(): if value != 0: all_zeros = False if all_zeros is True: path = "m %f,%f h %f v %f h %f v %f z" % (-width/2, -height/2, width, height, -width, -height) else: top_left = float(radii.get('tl') or radii.get('top_left') or 0) top_right = float(radii.get('tr') or radii.get('top_right') or 0) bot_right = float(radii.get('br') or radii.get('bot_right') or radii.get('bottom_right') or 0) bot_left = float(radii.get('bl') or radii.get('bot_left') or radii.get('bottom_left') or 0) path = "m %f,%f " % (-width/2, 0) if top_left == 0: path += "v %f h %f " % (-height/2, width/2) else: r = top_left path += "v %f c %f,%f %f,%f %f,%f h %f " % (-(height/2-r), 0,-k*r, -r*(k-1),-r, r,-r, width/2-r) if top_right == 0: path += "h %f v %f " % (width/2, height/2) else: r = top_right path += "h %f c %f,%f %f,%f %f,%f v %f " % (width/2-r, k*r,0, r,-r*(k-1), r,r, height/2-r) if bot_right == 0: path += "v %f h %f " % (height/2, -width/2) else: r = bot_right path += "v %f c %f,%f %f,%f %f,%f h %f " % (height/2-r, 0,k*r, r*(k-1),r, -r,r, -(width/2-r)) if bot_left == 0: path += "h %f v %f " % (-width/2, -height/2) else: r = bot_left path += "h %f c %f,%f %f,%f %f,%f v %f " % (-(width/2-r), -k*r,0, -r,r*(k-1), -r,-r, -(height/2-r)) path += "z" else: path = "m %f,%f h %f v %f h %f v %f z" % (-width/2, -height/2, width, height, -width, -height) return path def ring_diameters_to_path(d1, d2): """ Returns a path for a ring based on two diameters; the function automatically determines which diameter is the inner and which is the outer diameter """ path = None if d1 == d2: path = circle_diameter_to_path(d1) else: if d1 > d2: outer = d1 inner = d2 else: outer = d2 inner = d1 path = circle_diameter_to_path(outer) path += circle_diameter_to_path(inner, Point(0, outer/2)) return path def circle_diameter_to_path(d, offset=Point()): """ Returns an SVG path of a circle of diameter 'diameter' """ r = d/2.0 # The calculation to obtain the 'k' coefficient can be found here: # http://itc.ktu.lt/itc354/Riskus354.pdf # "APPROXIMATION OF A CUBIC BEZIER CURVE BY CIRCULAR ARCS AND VICE VERSA" # by Aleksas Riskus k = 0.5522847498 return "m %s,%s c %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s z" % (0,r-offset.y, k*r,0, r,-r*(1-k), r,-r, 0,-r*k, -r*(1-k),-r, -r,-r, -r*k,0, -r,r*(1-k), -r,r, 0,r*k, r*(1-k),r, r,r) def drillPath(diameter): """ Returns an SVG path for a drill symbol of diameter 'diameter' """ r = diameter/2.0 # The calculation to obtain the 'k' coefficient can be found here: # http://itc.ktu.lt/itc354/Riskus354.pdf # "APPROXIMATION OF A CUBIC BEZIER CURVE BY CIRCULAR ARCS AND VICE VERSA" # by Aleksas Riskus k = 0.5522847498 # internal circle b = r*0.9 return "m %s,%s c %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s z m %s,%s %s,%s c %s,%s %s,%s %s,%s l %s,%s %s,%s c %s,%s %s,%s %s,%s z" % (0,r, k*r,0, r,-r*(1-k), r,-r, 0,-r*k, -r*(1-k),-r, -r,-r, -r*k,0, -r,r*(1-k), -r,r, 0,r*k, r*(1-k),r, r,r, 0,-(r-b), 0,-2*b, -b*k,0, -b,b*(1-k), -b,b, b,0, b,0, 0,k*b, -b*(1-k),b, -b,b) def placementMarkerPath(): """ Returns a path for the placement marker """ diameter = 0.2 r = diameter/2.0 # The calculation to obtain the 'k' coefficient can be found here: # http://itc.ktu.lt/itc354/Riskus354.pdf # "APPROXIMATION OF A CUBIC BEZIER CURVE BY CIRCULAR ARCS AND VICE VERSA" # by Aleksas Riskus k = 0.5522847498 # extension b = r*1.8 # return "m %s,%s c %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s m %s,%s %s,%s m %s,%s %s,%s z" % (0,r, k*r,0, r,-r*(1-k), r,-r, 0,-r*k, -r*(1-k),-r, -r,-r, -r*k,0, -r,r*(1-k), -r,r, 0,r*k, r*(1-k),r, r,r, 0,-(r-b), 0,-2*b, -b,b, 2*b,0) return "m %s,%s c %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s %s,%s m %s,%s %s,%s z" % (0,r, k*r,0, r,-r*(1-k), r,-r, 0,-r*k, -r*(1-k),-r, -r,-r, -r*k,0, -r,r*(1-k), -r,r, 0,r*k, r*(1-k),r, r,r, -b,-r, 2*b,0) def mirror_transform(transform, axis='y'): """ Returns a mirrored transfrom """ mirrored_transform = transform regex = r"(?P<before>.*?)translate\s?\(\s?(?P<x>-?[0-9]*\.?[0-9]+)\s+(?P<y>-?[0-9]*\.?[0-9]+\s?)\s?\)(?P<after>.*)" capture = re.match(regex, transform) if capture is not None: mirrored_transform = "%s translate(%g %s) %s" % ( capture.group('before'), -float(capture.group('x')), capture.group('y'), capture.group('after')) return mirrored_transform def makeSvgLayers(top_layer, transform=None, refdef=None): """ Creates Inkscape SVG layers that correspond to a board's layers. Includes the default style definition from the stylesheet. Returns a dictionary of layer instantiations. """ layer_control = config.brd['layer-control'] # Holds SVG layers layers = {} # Create layers for top and bottom PCB layers for layer_dict in reversed(config.stk['layers-dict']): layer_type = layer_dict['type'] layer_name = layer_dict['name'] # create SVG layer for PCB layer layers[layer_name] = {} element = layers[layer_name]['layer'] = makeSvgLayer(top_layer, layer_name, transform, None, refdef) element.set('{'+config.cfg['ns']['pcbmode']+'}%s' % ('pcb-layer'), layer_name) sheets = layer_dict['stack'] if layer_type == 'signal-layer-surface': placement_dict = [{"name": "placement", "type": "placement"}] assembly_dict = [{"name": "assembly", "type": "assembly"}] solderpaste_dict = [{"name": "solderpaste", "type": "solderpaste"}] # Layer appear in Inkscape first/top to bottom/last sheets = placement_dict + assembly_dict + solderpaste_dict + sheets for sheet in reversed(sheets): sheet_type = sheet['type'] sheet_name = sheet['name'] # Set default style for this sheet try: style = utils.dictToStyleText(config.stl['layout'][sheet_type]['default'][layer_name]) except: # A stylesheet may define one style for any sheet type # or a specific style for multiple layers of the same # type. If, for example, a specific style for # 'internal-2' cannot be found, PCBmodE will default # to the general definition for this type of sheet style = utils.dictToStyleText(config.stl['layout'][sheet_type]['default'][layer_name.split('-')[0]]) if layer_control[sheet_type]['hide'] == True: style += 'display:none;' tmp = layers[layer_name] tmp[sheet_type] = {} element = tmp[sheet_type]['layer'] = makeSvgLayer(parent_layer=tmp['layer'], layer_name=sheet_name, transform=None, style=style, refdef=refdef) element.set('{'+config.cfg['ns']['pcbmode']+'}%s' % ('sheet'), sheet_type) if layer_control[sheet_type]['lock'] == True: element.set('{'+config.cfg['ns']['sodipodi']+'}insensitive', 'true') # A PCB layer of type 'conductor' is best presented in # seperate sub-layers of 'pours', 'pads', and # 'routing'. The following generates those sub-layers if sheet_type == 'conductor': tmp2 = layers[layer_name]['conductor'] conductor_types = ['routing', 'pads', 'pours'] for cond_type in conductor_types: try: style = utils.dictToStyle(config.stl['layout']['conductor'][cond_type].get(layer_name)) except: # See comment above for rationalle style = utils.dictToStyleText(config.stl['layout']['conductor'][cond_type][layer_name.split('-')[0]]) if layer_control['conductor'][cond_type]['hide'] == True: style += 'display:none;' tmp2[cond_type] = {} element = tmp2[cond_type]['layer'] = makeSvgLayer(parent_layer=tmp2['layer'], layer_name=cond_type, transform=None, style=style, refdef=refdef) element.set('{'+config.cfg['ns']['pcbmode']+'}%s' % ('sheet'), cond_type) if layer_control['conductor'][cond_type]['lock'] == True: element.set('{'+config.cfg['ns']['sodipodi']+'}insensitive', 'true') for info_layer in ['origin','dimensions','outline','drills','documentation']: style = utils.dictToStyleText(config.stl['layout'][info_layer].get('default')) if layer_control[info_layer]['hide'] == True: style += 'display:none;' layers[info_layer] = {} element = layers[info_layer]['layer'] = makeSvgLayer(top_layer, info_layer, transform, style, refdef) element.set('{'+config.cfg['ns']['pcbmode']+'}%s' % ('sheet'), info_layer) if layer_control[info_layer]['lock'] == True: element.set('{'+config.cfg['ns']['sodipodi']+'}insensitive', 'true') return layers def makeSvgLayer(parent_layer, layer_name, transform=None, style=None, refdef=None): """ Create and return an Inkscape SVG layer """ new_layer = et.SubElement(parent_layer, 'g') new_layer.set('{'+config.cfg['ns']['inkscape']+'}groupmode', 'layer') new_layer.set('{'+config.cfg['ns']['inkscape']+'}label', layer_name) if transform is not None: new_layer.set('transform', transform) if style is not None: new_layer.set('style', style) if refdef is not None: new_layer.set('refdef', refdef) return new_layer def create_layers_for_gerber_svg(cfg, top_layer, transform=None, refdef=None): """ Creates a dictionary of SVG layers 'top_layer' is the parent layer element """ # holds all SVG layers for the board board_svg_layers = {} # create layers for top and bottom PCB layers for pcb_layer_name in reversed(utils.get_surface_layers(cfg)): # create SVG layer for PCB layer layer_id = pcb_layer_name board_svg_layers[pcb_layer_name] = {} board_svg_layers[pcb_layer_name]['layer'] = create_svg_layer(cfg, top_layer, pcb_layer_name, layer_id, transform, None, refdef) # create the following PCB sheets for the PCB layer pcb_sheets = ['copper', 'silkscreen', 'soldermask'] for pcb_sheet in pcb_sheets: # define a layer id layer_id = "%s_%s" % (pcb_layer_name, pcb_sheet) if refdef is not None: layer_id += "_%s" % refdef style = utils.dict_to_style(cfg['layout_style'][pcb_sheet]['default'].get(pcb_layer_name)) tmp = board_svg_layers[pcb_layer_name] tmp[pcb_sheet] = {} tmp[pcb_sheet]['layer'] = create_svg_layer(cfg, tmp['layer'], pcb_sheet, layer_id, None, style, refdef) # create dimensions layer layer_name = 'outline' # define a layer id layer_id = "%s" % (layer_name) if refdef is not None: layer_id += "_%s" % refdef style = '' #utils.dict_to_style(cfg['layout_style'][layer_name].get('default')) board_svg_layers[layer_name] = {} board_svg_layers[layer_name]['layer'] = create_svg_layer(cfg, top_layer, layer_name, layer_id, transform, style, refdef) # create drills layer layer_name = 'drills' # define a layer id layer_id = "%s" % (layer_name) if refdef is not None: layer_id += "_%s" % refdef style = utils.dict_to_style(cfg['layout_style'][layer_name].get('default')) board_svg_layers[layer_name] = {} board_svg_layers[layer_name]['layer'] = create_svg_layer(cfg, top_layer, layer_name, layer_id, transform, style, refdef) # create drills layer layer_name = 'documentation' # define a layer id layer_id = "%s" % (layer_name) if refdef is not None: layer_id += "_%s" % refdef style = utils.dict_to_style(cfg['layout_style'][layer_name].get('default')) board_svg_layers[layer_name] = {} board_svg_layers[layer_name]['layer'] = create_svg_layer(cfg, top_layer, layer_name, layer_id, transform, style, refdef) return board_svg_layers def rect_to_path(shape): """ Takes a 'rect' definition and returns a corresponding path """ width = float(shape['width']) height = float(shape['height']) radii = shape.get('radii') path = width_and_height_to_path(width, height, radii) return path def create_meandering_path(params): """ Returns a meander path based on input parameters """ deg_to_rad = 2 * pi / 360 radius = params.get('radius') theta = params.get('theta') width = params.get('trace-width') number = params.get('bus-width') or 1 pitch = params.get('pitch') or 0 coords = [] coords.append(Point(0, -(number-1)*pitch/2)) for n in range(1, int(number)): coords.append(Point(2*radius*cos(theta*deg_to_rad), pitch)) path = '' for coord in coords: path += create_round_meander(radius, theta, coord) # calculate the reduction of bounding box width to be used in # pattern spacing setting spacing = radius - radius*cos(theta*deg_to_rad) return path, spacing def create_round_meander(radius, theta=0, offset=Point()): """ Returns a single period of a meandering path based on radius and angle theta """ deg_to_rad = 2 * pi / 360 r = radius t = theta * deg_to_rad # The calculation to obtain the 'k' coefficient can be found here: # http://itc.ktu.lt/itc354/Riskus354.pdf # "APPROXIMATION OF A CUBIC BEZIER CURVE BY CIRCULAR ARCS AND VICE VERSA" # by Aleksas Riskus k = 0.5522847498 # the control points need to be shortened relative to the angle by this factor j = 2*t/pi path = "m %s,%s " % (-2*r*cos(t)-offset.x, -offset.y) path += "c %s,%s %s,%s %s,%s " % (-k*r*j*sin(t),-k*r*j*cos(t), -(r-r*cos(t)),-r*sin(t)+r*k*j, -(r-r*cos(t)),-r*sin(t)) path += "c %s,%s %s,%s %s,%s " % (0,-k*r, r-k*r,-r, r,-r) path += "c %s,%s %s,%s %s,%s " % (k*r,0, r,r-k*r, r,r) path += "c %s,%s %s,%s %s,%s " % (0,k*r*j, -(r-r*cos(t)-k*r*j*sin(t)),r*sin(t)-r*k*j*cos(t), -r+r*cos(t),r*sin(t)) path += "c %s,%s %s,%s %s,%s " % (-k*r*j*sin(t),k*r*j*cos(t), -(r-r*cos(t)),r*sin(t)-r*k*j, -(r-r*cos(t)),r*sin(t)) path += "c %s,%s %s,%s %s,%s " % (0,k*r, r-k*r,r, r,r) path += "c %s,%s %s,%s %s,%s " % (k*r,0, r,-r+k*r, r,-r) path += "c %s,%s %s,%s %s,%s " % (0,-k*r*j, -(r-r*cos(t)-k*r*j*sin(t)),-r*sin(t)+r*k*j*cos(t), -r+r*cos(t),-r*sin(t)) return path def calculate_cubic_bezier_length(px, py): """ Return the length of a cubic bezier """ length = 0.0; prev = Point(px[0], py[0]) for i in range(1, len(px)): length += sqrt((px[i] - prev.x)**2 + (py[i] - prev.y)**2) prev = Point(px[i], py[i]) return length def coord_list_to_svg_path(coord_list): """ Turn a list of points into an SVG path """ path = '' #'M 0,0 '# % (coord_list[0]['coord'].x, coord_list[0]['coord'].y) last_action_type = '' for action in coord_list: if action['type'] == 'move': if last_action_type != 'M': path += 'M ' path += '%s,%s ' % (action['coord'].x, -action['coord'].y) last_action_type = 'M' if action['type'] == 'draw': if last_action_type != 'L': path += 'L ' path += '%s,%s ' % (action['coord'].x, -action['coord'].y) last_action_type = 'L' return path