← Back to team overview

kicad-developers team mailing list archive

Re: Haskell Library (SVG drawing)

 

On Tue, 2014-12-02 at 16:40 +0000, Kaspar Emanuel wrote:
> 
> What I am working on is a stand-alone converter which maps various
> colours to layers (so red is F.Cu and green is B.Cu etc). I can
> already do rounded rectangles quite well. I made this PCB (a
> capacitive touch experiment) using it so I should probably clean it up
> and release it. I got caught up with trying to convert oval arcs and
> bezier curves in the mean time though.
> 
I hacked a biarc Bezier approximator from gcode-tools to mostly work.
See the attached patch which should work off the current tip. You do
need the svg.path package from pip.

The script can be run something like:

   python /path/to/test_svg.py /path/to/svg.svg

It will dump a kicad_pcb file to /tmp/svgtest.kicad_pcb and a module to
/tmp/svgtest/SVG.kicad_mod.

It still needs a lot of tweaks, but the code approximator seems to work
quite well. The attached spiral module is made up of 24 arcs (12 biarcs).

I have not yet worked out how best to draw elliptical arcs, though you
can always convert to a Bezier first and then draw by biarc
approximation.

Also attached is an ESD symbol, which I think turned out quite well.

This code is still a bit fragile, but the Bezier converter should be fairly
easy to integrate into your own scripts.

Cheers,

John








diff --git a/pcbnew/scripting/plugins/bezier_converter.py b/pcbnew/scripting/plugins/bezier_converter.py
new file mode 100644
index 0000000..b700051
--- /dev/null
+++ b/pcbnew/scripting/plugins/bezier_converter.py
@@ -0,0 +1,339 @@
+"""
+Copyright 2014 (C) John Beard, john.j.beard@xxxxxxxxx
+based on gcode_tools.py (C) 2009 Nick Drobchenko, nick@xxxxxxxxxxx
+based on gcode.py (C) 2007 hugomatic...
+based on addnodes.py (C) 2005,2007 Aaron Spike, aaron@xxxxxxxxx
+based on dots.py (C) 2005 Aaron Spike, aaron@xxxxxxxxx
+based on interp.py (C) 2005 Aaron Spike, aaron@xxxxxxxxx
+
+This program is free software; you can redistribute
+it and/or modify it under the terms of the GNU General Public
+License as published by the Free Software Foundation; either version
+2 of the License, or (at your option) any later version. This
+program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details. You should have received a
+copy of the GNU General Public License along with this program; if
+not, write to the Free Software Foundation, Inc., 59 Temple Place,
+Suite 330, Boston, MA 02111-1307 USA
+"""
+
+from __future__ import division
+
+import math
+
+def pointOnBezier(bez, t):
+
+    #Use the Bezier formula
+    x = (1-t)**3*bez[0][0] + 3*(1-t)**2*t*bez[1][0] + 3*(1-t)*t**2*bez[2][0] + t**3*bez[3][0]
+    y = (1-t)**3*bez[0][1] + 3*(1-t)**2*t*bez[1][1] + 3*(1-t)*t**2*bez[2][1] + t**3*bez[3][1]
+
+    return x, y
+
+def splitBezier(bez, t):
+    """
+    Split a bezier at t and return both halves
+    """
+    x1, y1 = bez[0]
+    x2, y2 = bez[1]
+    x3, y3 = bez[2]
+    x4, y4 = bez[3]
+
+    x12 = (x2 - x1) * t + x1
+    y12 = (y2 - y1) * t + y1
+
+    x23 = (x3 - x2) * t + x2
+    y23 = (y3 - y2) * t + y2
+
+    x34 = (x4 - x3) * t + x3
+    y34 = (y4 - y3) * t + y3
+
+    x123 = (x23 - x12) * t + x12
+    y123 = (y23 - y12) * t + y12
+
+    x234 = (x34 - x23) * t + x23
+    y234 = (y34 - y23) * t + y23
+
+    x1234 = (x234 - x123) * t + x123
+    y1234 = (y234 - y123) * t + y123
+
+    return [[(x1, y1), (x12, y12), (x123, y123), (x1234, y1234)],
+            [(x1234,y1234),(x234,y234),(x34,y34),(x4,y4)]]
+
+math.pi2 = math.pi/2
+straight_tolerance = 0.01
+straight_distance_tolerance = 0.01
+min_arc_radius = 0.1
+EMC_TOLERANCE_EQUAL = 0.01
+biarc_max_split_depth = 4
+biarc_tolerance = 100
+
+def between(c,x,y):
+    return x-straight_tolerance<=c<=y+straight_tolerance or y-straight_tolerance<=c<=x+straight_tolerance
+
+################################################################################
+###     Point (x,y) operations
+################################################################################
+class P:
+    def __init__(self, x, y=None):
+        if y is not None:
+            self.x, self.y = float(x), float(y)
+        else:
+            self.x, self.y = float(x[0]), float(x[1])
+
+    def __add__(self, other):
+        return P(self.x + other.x, self.y + other.y)
+
+    def __sub__(self, other):
+        return P(self.x - other.x, self.y - other.y)
+
+    def __neg__(self):
+        return P(-self.x, -self.y)
+
+    def __mul__(self, other):
+        if isinstance(other, P):
+            return self.x * other.x + self.y * other.y
+        return P(self.x * other, self.y * other)
+
+    __rmul__ = __mul__
+
+    def __truediv__(self, other):
+        return P(self.x / other, self.y / other)
+
+    def mag(self):
+        return math.hypot(self.x, self.y)
+
+    def unit(self):
+        h = self.mag()
+        if h:
+            return self / h
+        else:
+            return P(0,0)
+
+    def dot(self, other):
+        return self.x * other.x + self.y * other.y
+    def rot(self, theta):
+        c = math.cos(theta)
+        s = math.sin(theta)
+        return P(self.x * c - self.y * s,  self.x * s + self.y * c)
+
+    def angle(self):
+        return math.atan2(self.y, self.x)
+
+    def __repr__(self):
+        return '%f,%f' % (self.x, self.y)
+
+    def ccw(self):
+        return P(-self.y,self.x)
+
+    def l2(self):
+        return self.x*self.x + self.y*self.y
+
+def csp_at_t(bez,t):
+    ax,bx,cx,dx = bez[0][0], bez[1][0], bez[2][0], bez[3][0]
+    ay,by,cy,dy = bez[0][1], bez[1][1], bez[2][1], bez[3][1]
+
+    x1, y1 = ax+(bx-ax)*t, ay+(by-ay)*t
+    x2, y2 = bx+(cx-bx)*t, by+(cy-by)*t
+    x3, y3 = cx+(dx-cx)*t, cy+(dy-cy)*t
+
+    x4,y4 = x1+(x2-x1)*t, y1+(y2-y1)*t
+    x5,y5 = x2+(x3-x2)*t, y2+(y3-y2)*t
+
+    x,y = x4+(x5-x4)*t, y4+(y5-y4)*t
+    return [x,y]
+
+def point_to_arc_distance(p, arc):
+    """
+    Distance calculation from point to arc
+    """
+    P0,P2,c,a = arc
+    dist = None
+    p = P(p)
+    r = (P0 - c).mag()
+
+    if r > 0:
+        i = c + (p - c).unit() * r
+        alpha = ((i-c).angle() - (P0-c).angle())
+        if a*alpha<0:
+            if alpha>0:
+                alpha = alpha-math.pi2
+            else:
+                alpha = math.pi2+alpha
+        if between(alpha,0,a) or min(abs(alpha),abs(alpha-a))<straight_tolerance:
+            return (p-i).mag(), [i.x, i.y]
+        else:
+            d1, d2 = (p-P0).mag(), (p-P2).mag()
+            if d1<d2 :
+                return (d1, [P0.x,P0.y])
+            else :
+                return (d2, [P2.x,P2.y])
+
+def csp_to_arc_distance(bez, arc1, arc2, tolerance = 0.01 ): # arc = [start,end,center,alpha]
+    n, i = 10, 0
+    d, d1, dl = (0,(0,0)), (0,(0,0)), 0
+
+    while i<1 or (abs(d1[0]-dl[0])>tolerance and i<4):
+        i += 1
+        dl = d1*1
+        for j in range(n+1):
+            t = float(j)/n
+            p = csp_at_t(bez,t)
+            d = min(point_to_arc_distance(p,arc1), point_to_arc_distance(p,arc2))
+            d1 = max(d1,d)
+        n=n*2
+    return d1[0]
+
+################################################################################
+###
+###     Biarc function
+###
+###     Calculates biarc approximation of cubic super path segment
+###     splits segment if needed or approximates it with straight line
+###
+################################################################################
+def biarc(bez, depth=0):
+
+    def line_approx(bez):
+        return [ [bez[0], 'line', bez[3]] ]
+
+    def biarc_split(bez, depth):
+
+        if depth >= biarc_max_split_depth:
+            return line_approx(bez)
+
+        bez1, bez2 = splitBezier(bez, t=0.5)
+
+        return biarc(bez1, depth+1) + biarc(bez2, depth+1)
+
+    def biarc_curve_segment_length(seg):
+        if seg[1] == "arc" :
+            return math.sqrt((seg[0][0] - seg[2][0])**2 + (seg[0][1] - seg[2][1])**2) * seg[3]
+        elif seg[1] == "line" :
+            return math.sqrt((seg[0][0]-seg[4][0])**2+(seg[0][1]-seg[4][1])**2)
+        else:
+            return 0
+
+    def calculate_arc_params(P0,P1,P2):
+        D = (P0 + P2) / 2
+
+        if (D - P1).mag() == 0:
+            return None, None
+
+        R = D - ((D - P0).mag()**2 / (D - P1).mag() )*(P1 - D).unit()
+
+        p0a = (P0-R).angle()%(2*math.pi)
+        p1a = (P1-R).angle()%(2*math.pi)
+        p2a = (P2-R).angle()%(2*math.pi)
+        alpha = (p2a - p0a) % (2*math.pi)
+
+        if (p0a < p2a and (p1a < p0a or p2a < p1a)) or (p2a < p1a < p0a) :
+            alpha = -2*math.pi + alpha
+
+        if abs(R.x) > 1000000 or abs(R.y) > 1000000 or (R-P0).mag < min_arc_radius**2 :
+            return None, None
+        else :
+            return R, alpha
+
+    P0 = P(bez[0])
+    P4 = P(bez[3])
+
+    TS = (P(bez[1]) - P0)
+    TE = -(P(bez[2]) - P4)
+    v = P0 - P4
+
+    tsa = TS.angle()
+    tea = TE.angle()
+    va = v.angle()
+
+    if TE.mag() < straight_distance_tolerance and TS.mag() < straight_distance_tolerance:
+        # Both tangents are zero - line straight
+        return line_approx(bez)
+
+    if TE.mag() < straight_distance_tolerance:
+        TE = -(TS + v).unit()
+        r = TS.mag() / v.mag()*2
+    elif TS.mag() < straight_distance_tolerance:
+        TS = -(TE + v).unit()
+        r = 1 / ( TE.mag() / v.mag()*2 )
+    else:
+        r=TS.mag()/TE.mag()
+
+    TS = TS.unit()
+    TE = TE.unit()
+
+    tang_are_parallel = ((tsa - tea) % math.pi < straight_tolerance
+                            or math.pi-(tsa-tea)%math.pi<straight_tolerance)
+
+    if ( tang_are_parallel and
+                ((v.mag()<straight_distance_tolerance or TE.mag()<straight_distance_tolerance or TS.mag()<straight_distance_tolerance) or
+                    1-abs(TS*v/(TS.mag()*v.mag()))<straight_tolerance)  ):
+                # Both tangents are parallel and start and end are the same - line straight
+                # or one of tangents still smaller then tollerance
+
+                # Both tangents and v are parallel - line straight
+        line = line_approx(bez)
+        return line
+
+    c,b,a = v*v, 2*v*(r*TS+TE), 2*r*(TS*TE-1)
+
+    if v.mag() == 0:
+        return biarc_split(bez, depth)
+
+    asmall = abs(a) < 10**-10
+    bsmall = abs(b) < 10**-10
+    csmall = abs(c) < 10**-10
+
+    if asmall and b!=0:
+        beta = -c/b
+    elif csmall and a!=0:
+        beta = -b/a
+    elif not asmall:
+        discr = b*b-4*a*c
+        if discr < 0:
+            raise ValueError, (a,b,c,discr)
+        disq = discr**.5
+        beta1 = (-b - disq) / 2 / a
+        beta2 = (-b + disq) / 2 / a
+        if beta1*beta2 > 0 :
+            raise ValueError, (a,b,c,disq,beta1,beta2)
+        beta = max(beta1, beta2)
+    elif asmall and bsmall:
+        return biarc_split(bez, depth)
+
+    alpha = beta * r
+    ab = alpha + beta
+    P1 = P0 + alpha * TS
+    P3 = P4 - beta * TE
+    P2 = (beta / ab) * P1 + (alpha / ab) * P3
+
+    R1, a1 = calculate_arc_params(P0,P1,P2)
+    R2, a2 = calculate_arc_params(P2,P3,P4)
+
+    if R1 == None or R2 == None or (R1-P0).mag() < straight_tolerance or (R2-P2).mag() < straight_tolerance:
+        return line_approx(bez)
+
+    d = csp_to_arc_distance(bez, [P0,P2,R1,a1],[P2,P4,R2,a2])
+
+    if d > biarc_tolerance and depth < biarc_max_split_depth:
+        return biarc_split(bez, depth)
+
+    #otherwise construct a line or arc as needed
+
+    l = (P0-P2).l2()
+    if  l < EMC_TOLERANCE_EQUAL**2 or l<EMC_TOLERANCE_EQUAL**2 * R1.l2() /100 :
+        # arc should be straight otherwise it could be threated as full circle
+        arc1 = [ bez[0], 'line', [P2.x,P2.y] ]
+    else :
+        arc1 = [ bez[0], 'arc', [R1.x,R1.y], a1, [P2.x,P2.y] ]
+
+    l = (P4-P2).l2()
+    if  l < EMC_TOLERANCE_EQUAL**2 or l<EMC_TOLERANCE_EQUAL**2 * R2.l2() /100 :
+        # arc should be straight otherwise it could be threated as full circle
+        arc2 = [ [P2.x,P2.y], 'line', [P4.x,P4.y]]
+    else :
+        arc2 = [ [P2.x,P2.y], 'arc', [R2.x,R2.y], a2, [P4.x,P4.y] ]
+
+    return [ arc1, arc2 ]
+
diff --git a/pcbnew/scripting/plugins/svg_path.py b/pcbnew/scripting/plugins/svg_path.py
new file mode 100644
index 0000000..c56e356
--- /dev/null
+++ b/pcbnew/scripting/plugins/svg_path.py
@@ -0,0 +1,218 @@
+from __future__ import division
+
+import pcbnew
+from pcbnew import FromMM as fmm
+import math
+
+import HelpfulFootprintWizardPlugin as HFPW
+
+from lxml.etree import fromstring
+import svg.path
+from bezier_converter import biarc
+
+ns = "{http://www.w3.org/2000/svg}";
+
+class SVGPathConverter(HFPW.HelpfulFootprintWizardPlugin):
+
+    def GetName(self):
+        return "SVG Path"
+
+    def GetDescription(self):
+        return "Render an SVG's paths into a module"
+
+    def GenerateParameterList(self):
+
+        self.AddParam("SVG", "filename", self.uNatural, "")
+        self.AddParam("SVG", "biarc tolerance", self.uNatural, "0.1")
+        self.AddParam("SVG", "SVG units per mm", self.uNatural, "20")
+
+    def GetValue(self):
+        return "SVG"
+
+    def GetReferencePrefix(self):
+        return ""
+
+    def extractSvgStyle(self, e):
+
+        style = {}
+
+        if 'style' not in e.attrib:
+            return style
+
+        styles = [x.strip() for x in e.attrib['style'].split(';')]
+
+        for s in styles:
+            k, v = s.split(':', 1)
+
+            style[k] = v
+
+        return style
+
+    def SetStrokeWidth(self, e, scale, transform):
+        style = self.extractSvgStyle(e)
+
+        if 'stroke-width' in style:
+            w = float(style['stroke-width'])
+        else:
+            w = 0.2 #default
+
+        w = int(round(w * transform[0] * scale)) #assume isotropic scaling, there's not much we can do if not, that's up to the editor!
+
+        self.draw.SetWidth(w)
+
+    def BuildThisFootprint(self):
+
+        f = open(self.parameters["SVG"]["*filename"])
+        root = fromstring(f.read())
+
+        self.segments = 0
+        self.lines = 0
+        self.arcs = 0
+
+        # transform to internal units
+        scaleToMM = fmm(1)/self.parameters["SVG"]["*SVG units per mm"]
+        self.draw.TransformScaleOrigin(scaleToMM)
+
+        self.module.Reference().SetVisible(False)
+        self.module.Value().SetVisible(False)
+
+        docWidth = float(root.attrib['width'])
+        docHeight = float(root.attrib['height'])
+
+        # place the drawing page around the origin
+        self.draw.TransformTranslate(-docWidth/2, -docHeight/2)
+
+        for e in root.findall('.//%spath' % ns):
+            transform = self.collectTransforms(e)
+
+            d = svg.path.parse_path(e.attrib['d'])
+
+            self.SetStrokeWidth(e, scaleToMM, transform)
+
+            self.addPathToModule(d, transform)
+
+        for e in root.findall('.//%srect' % ns):
+            transform = self.collectTransforms(e)
+
+            x,y = float(e.attrib['x']), float(e.attrib['y'])
+            w,h = float(e.attrib['width']), float(e.attrib['height'])
+
+            self.draw.PushTransform(transform)
+            self.draw.TransformTranslate(w/2, h/2) #svg rects are referenced from the corner, not centre
+
+            self.SetStrokeWidth(e, scaleToMM, transform)
+
+            self.draw.Box(x,y,w,h)
+
+            self.draw.PopTransform(2)
+
+            self.lines += 4
+            self.segments += 4
+
+        self.draw.PopTransform(2) #pop the scale xfrm
+
+        #report
+        print "SVG had %d segments" % self.segments
+        print "%d arcs, %d lines, %d total segments" % (self.arcs, self.lines, self.arcs + self.lines)
+
+    def collectTransforms(self, e):
+        transforms = []
+
+        if 'transform' in e.attrib:
+            transforms.append(e.attrib['transform'])
+
+        for a in e.xpath('ancestor::*' ):
+
+            if 'transform' in a.attrib:
+                transforms.append(a.attrib['transform'])
+
+        transforms.reverse()
+
+        matrix = self.convertTransformsToMatrix(transforms)
+
+        return matrix
+
+    def convertTransformsToMatrix(self, ts):
+
+        m = [] # list of matrices
+        for t in ts:
+
+            s = t.index('(')
+            e = t.index(')')
+
+            c = [float(x) for x in t[s+1:e].split(',')]
+            trans = t[:s]
+
+            if trans == 'translate':
+                matrix = [1, 0, c[0], 0, 1, c[1]]
+            elif trans == 'scale':
+                matrix = [c[0], 0, 0, 0, c[1], 0]
+            elif trans == 'matrix':
+                matrix = [c[0], c[2], c[4], c[1], c[3], c[5]]
+            else:
+                raise KeyError("Unknown transform: %s" % t)
+
+            m.append(matrix)
+
+        combined = self.draw._ComposeMatricesWithIdentity(m)
+
+        return combined
+
+    def addPathToModule(self, d, t):
+
+        self.draw.PushTransform(t)
+
+        self.segments += len(d)
+
+        for seg in d:
+
+            if isinstance(seg, svg.path.CubicBezier):
+
+                p1 = (seg.start.real, seg.start.imag)
+                p2 = (seg.control1.real, seg.control1.imag)
+                p3 = (seg.control2.real, seg.control2.imag)
+                p4 = (seg.end.real, seg.end.imag)
+
+                bez = [p1, p2, p3, p4]
+
+                self.approximateAndDrawBezier(bez)
+
+            elif isinstance(seg, svg.path.Line):
+                self.lines += 1
+                self.draw.Line(seg.start.real, seg.start.imag, seg.end.real, seg.end.imag)
+
+            elif isinstance(seg, svg.path.Arc):
+                self.arcs += 1
+
+                start = (seg.start.real, seg.start.imag)
+                end = (seg.end.real, seg.end.imag)
+
+            else:
+                print "Skipping unknown segment: %s" % seg
+                continue;
+
+        self.draw.PopTransform()
+
+    def approximateAndDrawBezier(self, bez):
+
+        biarcs = biarc(bez)
+
+        for arc in biarcs:
+
+            if arc[1] == 'arc':
+
+                self.arcs += 1
+
+                angle = arc[3] * 1800 / math.pi
+
+                self.draw.Arc(arc[2][0], arc[2][1], arc[0][0], arc[0][1], angle)
+
+            elif arc[1] == 'line':
+
+                self.lines += 1
+
+                self.draw.Line(arc[0][0], arc[0][1], arc[2][0], arc[2][1])
+
+            else:
+                raise ValueError("Unknown arc type: %s" % arc)
+
diff --git a/pcbnew/scripting/tests/test_board.py b/pcbnew/scripting/tests/test_board.py
new file mode 100644
index 0000000..90f3f15
--- /dev/null
+++ b/pcbnew/scripting/tests/test_board.py
@@ -0,0 +1,210 @@
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation; either version 2 of the License, or
+#  (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software
+#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+#  MA 02110-1301, USA.
+#
+
+"""
+Simple helpers for automating generation of footprints with
+footprint wizards, either for testing or for producing footprints
+for libraries
+"""
+
+import pcbnew
+from pcbnew import FromMM as fmm
+
+import os
+import sys
+import re
+import argparse
+import tempfile
+
+# hack to avoid futzing with pythonpaths for the test
+sys.path.append(
+    os.path.join(
+        os.path.dirname(os.path.realpath(__file__)), os.pardir))
+
+
+class TestBoard(object):
+    """
+    Very basic and hacky test harness for stuffing modules into a PCB
+    file and then ripping them out into a directory
+    """
+
+    def __init__(self, output_board_file=None, output_mod_dir=None):
+        """
+        Set the board up, along with default geometries
+        """
+
+        # defualt geometry
+        self.margin = fmm(30)
+
+        self.pos = [0, 0]
+        self.sep = [fmm(5), fmm(5)]
+
+        # this is a hack until we can get a real bounding box
+        self.cell = [fmm(20), fmm(20)]
+
+        self.board = pcbnew.BOARD()
+
+        page = self.board.GetPageSettings()
+
+        self.lim = [page.GetWidthIU() - (self.margin),
+                    page.GetHeightIU() - (self.margin)]
+
+        self.filename = output_board_file
+        self.moddir = output_mod_dir
+
+    def set_grid_geometry(self, cell, sep):
+        """
+        Set a new layout geometry (cell sizes and separations, etc)
+
+        This will hopefully not be needed when we can work out the
+        actual bounding box
+        """
+        self.cell = cell
+        self.sep = sep
+
+    def new_row(self):
+        """
+        Start a new row of modules
+        """
+        self.pos = [0, self.pos[1] + self.cell[1] + self.sep[1]]
+
+    def add_module(self, mod):
+        """
+        Add a module to the board at the current position,
+        wrapping to the next line if we ran out of page
+        """
+
+        if self.pos[0] > self.lim[0] - self.cell[0]:
+            self.pos = [0, self.pos[1] + self.cell[1]]
+
+        mod.SetPosition(pcbnew.wxPoint(
+            self.margin + self.pos[0], self.margin + self.pos[1]))
+
+        # this segfaults :-(
+        # print mod.GetFootprintRect()
+        # self.x += mod.GetBoundingBox().GetWidth() + self.xsep
+
+        self.board.Add(mod)
+
+        self.pos[0] = self.pos[0] + self.cell[0] + self.sep[0]
+
+    def add_footprint(self, footprint, params):
+        """
+        Create a module from the given wizard and parameters
+        and then place it onto the board
+        """
+
+        for page in range(footprint.GetNumParameterPages()):
+
+            param_list = footprint.GetParameterNames(page)
+            val_list = footprint.GetParameterValues(page)
+
+            for key, val in params.iteritems():
+                if key in param_list:
+                    val_list[param_list.index(key)] = val
+
+            footprint.SetParameterValues(page, val_list)
+
+        module = footprint.GetModule()
+
+        self.add_module(module)
+
+    def save_board_and_modules(self):
+        """
+        Save the board to the given file
+        """
+        if not self.filename:
+            return
+
+        self.board.Save(self.filename)
+        self.rip_out_and_save_modules()
+
+    def rip_out_and_save_modules(self):
+        """
+        Hack to rip out modules from board.
+        Be very careful trusting this to treat modules nicely!
+        Surely there must be a better way to get a module into a file
+        without resorting to this?
+        """
+        if not self.filename or not self.moddir:
+            return
+
+        brd = open(self.filename, 'r')
+
+        if not os.path.isdir(self.moddir):
+            os.makedirs(self.moddir)
+
+        mod = ''
+        in_mod = False
+
+        for line in brd:
+            if line.startswith("  (module"):
+                in_mod = True
+
+                # remove unwanted elements
+                line = re.sub(r"\(t(stamp|edit).*?\)", "", line)
+
+                ref = line.split()[1]
+
+            if in_mod:
+                if not line.startswith("    (at "):
+                    mod += line[2:].rstrip() + "\n"
+
+                if line.startswith("  )"):
+                    in_mod = False
+
+                    mod_file = os.path.join(
+                        self.moddir, "%s.%s" % (ref, "kicad_mod"))
+
+                    ofile = open(mod_file, 'w')
+                    ofile.write(mod)
+                    ofile.close()
+
+                    mod = ''
+
+
+def set_up_simple_test_board():
+    """
+    Very simple board setup for basic "place modules onto PCB"-type tests
+    """
+
+    default_pcb_file = os.path.join(tempfile.gettempdir(), 'test.kicad_pcb')
+    default_mod_dir = os.path.join(tempfile.gettempdir(), 'kicad_test_mods')
+
+    parser = argparse.ArgumentParser(
+        description='Convert an SVG to a KiCad footprint.')
+
+    parser.add_argument(
+        '--board', '-b', metavar='B', type=str,
+        default=default_pcb_file,
+        help='Board file to output'
+    )
+    parser.add_argument(
+        '--moddir', '-d', metavar='D', type=str,
+        default=default_mod_dir,
+        help='Directory to output the modules to'
+    )
+
+    args = parser.parse_args()
+
+    print "Test started:\n"
+    print "\tOutputting file to: %s" % args.board
+    print "\tOutputting modules to: %s" % args.moddir
+    print "\n"
+
+    test_brd = TestBoard(args.board, args.moddir)
+
+    return test_brd
diff --git a/pcbnew/scripting/tests/test_svg.py b/pcbnew/scripting/tests/test_svg.py
new file mode 100644
index 0000000..eb39aa3
--- /dev/null
+++ b/pcbnew/scripting/tests/test_svg.py
@@ -0,0 +1,41 @@
+import pcbnew
+from pcbnew import FromMM as FMM
+
+import os, sys
+import argparse
+import re
+import itertools
+
+# hack to avoid futzing with pythonpaths for the test
+sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir))
+
+import plugins.svg_path as SVG
+import test_board as TB
+
+
+if __name__ == "__main__":
+
+
+    parser = argparse.ArgumentParser(description='Convert an SVG to a KiCad footprint.')
+    parser.add_argument('footprints', metavar='F', type=str, nargs='+',
+                   help='SVG files to convert')
+    parser.add_argument('--scale', '-s', metavar='S', type=float, default=10,
+                   help='SVG units per mm')
+
+    args = parser.parse_args()
+
+    filename = "/tmp/svgtest.kicad_pcb"
+    mod_path = "/tmp/svgtest"
+
+    tb = TB.TestBoard(filename, mod_path)
+
+    for fp in args.footprints:
+        tb.add_footprint(SVG.SVGPathConverter(), {
+            "*filename": fp,
+            "*SVG units per mm": args.scale
+        })
+
+    tb.new_row()
+
+    tb.save_board_and_modules()
+
(kicad_pcb (version 4) (host pcbnew "(2014-11-28 BZR 5279)-product")

  (general
    (links 0)
    (no_connects 0)
    (area 0 0 0 0)
    (thickness 1.6)
    (drawings 0)
    (tracks 0)
    (zones 0)
    (modules 1)
    (nets 1)
  )

  (page A4)
  (layers
    (0 F.Cu signal)
    (31 B.Cu signal)
    (32 B.Adhes user)
    (33 F.Adhes user)
    (34 B.Paste user)
    (35 F.Paste user)
    (36 B.SilkS user)
    (37 F.SilkS user)
    (38 B.Mask user)
    (39 F.Mask user)
    (40 Dwgs.User user)
    (41 Cmts.User user)
    (42 Eco1.User user)
    (43 Eco2.User user)
    (44 Edge.Cuts user)
    (45 Margin user)
    (46 B.CrtYd user)
    (47 F.CrtYd user)
    (48 B.Fab user)
    (49 F.Fab user)
  )

  (setup
    (last_trace_width 0.254)
    (trace_clearance 0.254)
    (zone_clearance 0.508)
    (zone_45_only no)
    (trace_min 0.254)
    (segment_width 0.2)
    (edge_width 0.15)
    (via_size 0.889)
    (via_drill 0.635)
    (via_min_size 0.889)
    (via_min_drill 0.508)
    (uvia_size 0.508)
    (uvia_drill 0.127)
    (uvias_allowed no)
    (uvia_min_size 0.508)
    (uvia_min_drill 0.127)
    (pcb_text_width 0.3)
    (pcb_text_size 1.5 1.5)
    (mod_edge_width 0.15)
    (mod_text_size 1.5 1.5)
    (mod_text_width 0.15)
    (pad_size 1.524 1.524)
    (pad_drill 0.762)
    (pad_to_mask_clearance 0.2)
    (aux_axis_origin 0 0)
    (visible_elements FFFFFF7F)
    (pcbplotparams
      (layerselection 0x00030_80000001)
      (usegerberextensions false)
      (excludeedgelayer true)
      (linewidth 0.150000)
      (plotframeref false)
      (viasonmask false)
      (mode 1)
      (useauxorigin false)
      (hpglpennumber 1)
      (hpglpenspeed 20)
      (hpglpendiameter 15)
      (hpglpenoverlay 2)
      (psnegative false)
      (psa4output false)
      (plotreference true)
      (plotvalue true)
      (plotinvisibletext false)
      (padsonsilk false)
      (subtractmaskfromsilk false)
      (outputformat 1)
      (mirror false)
      (drillshape 1)
      (scaleselection 1)
      (outputdirectory ""))
  )

  (net 0 "")

  (net_class Default "This is the default net class."
    (clearance 0.254)
    (trace_width 0.254)
    (via_dia 0.889)
    (via_drill 0.635)
    (uvia_dia 0.508)
    (uvia_drill 0.127)
  )

  (module SVG (layer F.Cu) (tedit 547E0A52) (tstamp 0)
    (at 30 30)
    (fp_text reference ** (at 0 0) (layer F.SilkS) hide
      (effects (font (thickness 0.2)))
    )
    (fp_text value SVG (at 0 0) (layer F.SilkS) hide
      (effects (font (thickness 0.2)))
    )
    (fp_arc (start -0.876447 -30.667606) (end -0.490439 -30.667606) (angle 108.643139) (layer F.SilkS) (width 0.2))
    (fp_arc (start -0.715016 -31.146096) (end -0.999843 -30.301852) (angle 38.87535782) (layer F.SilkS) (width 0.2))
    (fp_arc (start -0.516537 -31.272453) (end -1.466629 -30.667607) (angle 66.69387964) (layer F.SilkS) (width 0.2))
    (fp_arc (start 0.064171 -30.877622) (end -1.447928 -31.905718) (angle 38.13100318) (layer F.SilkS) (width 0.2))
    (fp_arc (start 0.144146 -30.626377) (end -0.490438 -32.619987) (angle 55.26731644) (layer F.SilkS) (width 0.2))
    (fp_arc (start -0.266205 -30.093727) (end 1.420979 -32.283747) (angle 40.40865593) (layer F.SilkS) (width 0.2))
    (fp_arc (start -0.568074 -30.02967) (end 2.438132 -30.667605) (angle 51.48686208) (layer F.SilkS) (width 0.2))
    (fp_arc (start -1.077179 -30.449434) (end 1.803034 -28.074655) (angle 41.45087625) (layer F.SilkS) (width 0.2))
    (fp_arc (start -1.127573 -30.766077) (end -0.490441 -26.762844) (angle 49.69751551) (layer F.SilkS) (width 0.2))
    (fp_arc (start -0.701534 -31.26219) (end -3.768489 -27.690792) (angle 42.08947081) (layer F.SilkS) (width 0.2))
    (fp_arc (start -0.378796 -31.303283) (end -5.371391 -30.667609) (angle 48.67002424) (layer F.SilkS) (width 0.2))
    (fp_arc (start 0.109249 -30.872802) (end -4.153219 -34.632521) (angle 42.52947405) (layer F.SilkS) (width 0.2))
    (fp_arc (start 0.143822 -30.546973) (end -0.490435 -36.524749) (angle 48.00718206) (layer F.SilkS) (width 0.2))
    (fp_arc (start -0.289956 -30.064376) (end 4.162335 -35.017728) (angle 42.85291525) (layer F.SilkS) (width 0.2))
    (fp_arc (start -0.617566 -30.034582) (end 6.342894 -30.667602) (angle 47.54543962) (layer F.SilkS) (width 0.2))
    (fp_arc (start -1.096248 -30.470897) (end 4.547831 -25.326355) (angle 43.10119747) (layer F.SilkS) (width 0.2))
    (fp_arc (start -1.122406 -30.799636) (end -0.490443 -22.858082) (angle 47.20579184) (layer F.SilkS) (width 0.2))
    (fp_arc (start -0.684089 -31.275369) (end -6.520572 -24.940671) (angle 43.2979689) (layer F.SilkS) (width 0.2))
    (fp_arc (start -0.354591 -31.298678) (end -9.276153 -30.667612) (angle 46.94572325) (layer F.SilkS) (width 0.2))
    (fp_arc (start 0.118853 -30.858732) (end -6.906385 -37.386906) (angle 43.45775311) (layer F.SilkS) (width 0.2))
    (fp_arc (start 0.139867 -30.528699) (end -0.490433 -40.429511) (angle 46.74027806) (layer F.SilkS) (width 0.2))
    (fp_arc (start -0.301428 -30.057082) (end 6.91823 -37.77281) (angle 43.5901024) (layer F.SilkS) (width 0.2))
    (fp_arc (start -0.631849 -30.03796) (end 10.247656 -30.6676) (angle 46.57393226) (layer F.SilkS) (width 0.2))
    (fp_arc (start -1.101971 -30.480387) (end 7.304202 -22.569414) (angle 43.70154119) (layer F.SilkS) (width 0.2))
  )

)

Attachment: ESD module.png
Description: PNG image


Follow ups

References