← Back to team overview

kicad-developers team mailing list archive

PATCH: Footprint wizard helpers

 

Hi,

I have a patch (fpw-overhaul.diff) for the Python footprint wizard
helpers. This adds a few more drawing functions, such as for circles,
and also uses a matrix-based transform stack with greatly simplifies
constructing footprints consisting of regularly spaced elements (e.g. in
lines, grids, circles, or some list of points that you specify).

This fixes #1366299. It seems FootprintWizardDrawingAids.py has an
old-style layer define that was not patched in r4969 (see #1333268).

I also attach a follow-up patch (fpw-tests.diff) containing a really
basic test harness to run though a few footprints with pre-defined
parameters, save them to a PCB board and also optionally "rip" the
modules out to a .pretty directory. I use something like this to
generate whole .pretties programmatically.

This patch contains a few nasty hacks as some things don't work through
the Python binding, so it may not be time to put it in. Though putting
it in would at least allow Jenkins to make sure the footprint wizards
can execute without dying. Attached for comment, nevertheless.

Thanks,

John
diff --git a/pcbnew/scripting/plugins/FootprintWizardDrawingAids.py b/pcbnew/scripting/plugins/FootprintWizardDrawingAids.py
index d1046bd..ba82ebe 100644
--- a/pcbnew/scripting/plugins/FootprintWizardDrawingAids.py
+++ b/pcbnew/scripting/plugins/FootprintWizardDrawingAids.py
@@ -14,7 +14,11 @@
 #  MA 02110-1301, USA.
 #
 
+from __future__ import division
+
 import pcbnew
+import math
+
 
 class FootprintWizardDrawingAids:
     """
@@ -24,31 +28,291 @@ class FootprintWizardDrawingAids:
     A "drawing context" is provided which can be used to set and retain
     settings such as line width and layer
     """
+
+    # directions (in degrees, compass-like)
+    dirN = 0
+    dirNE = 45
+    dirE = 90
+    dirSE = 135
+    dirS = 180
+    dirSW = 225
+    dirW = 270
+    dirNW = 315
+
+    # flip constants
+    flipNone = 0
+    flipX = 1  # flip X values, i.e. about Y
+    flipY = 2  # flip Y valuersabout X
+    flipBoth = 3
+
+    xfrmIDENTITY = [1, 0, 0, 0, 1, 0]  # no transform
+
     def __init__(self, module):
         self.module = module
-        #drawing context defaults
+        # drawing context defaults
         self.dc = {
-            'layer': pcbnew.SILKSCREEN_N_FRONT,
-            'width': pcbnew.FromMM(0.2)
+            'layer': pcbnew.F_SilkS,
+            'width': pcbnew.FromMM(0.2),
+            'transforms': [],
+            'transform': self.xfrmIDENTITY
         }
 
+    def PushTransform(self, mat):
+        """
+        Add a transform to the top of the stack and recompute the
+        overall transform
+        """
+        self.dc['transforms'].append(mat)
+        self.RecomputeTransforms()
+
+    def PopTransform(self, num=1):
+        """
+        Remove a transform from the top of the stack and recompute the
+        overall transform
+        """
+
+        for i in range(num):
+            mat = self.dc['transforms'].pop()
+        self.RecomputeTransforms()
+        return mat
+
+    def ResetTransform(self):
+        """
+        Reset the transform stack to the identity matrix
+        """
+        self.dc['transforms'] = []
+        self.RecomputeTransforms()
+
+    def _ComposeMatricesWithIdentity(self, mats):
+        """
+        Compose a sequence of matrices together by sequential
+        pre-mutiplciation with the identity matrix
+        """
+
+        x = self.xfrmIDENTITY
+
+        for mat in mats:
+            #precompose with each transform in turn
+            x = [
+                x[0] * mat[0] + x[1] * mat[3],
+                x[0] * mat[1] + x[1] * mat[4],
+                x[0] * mat[2] + x[1] * mat[5] + x[2],
+                x[3] * mat[0] + x[4] * mat[3],
+                x[3] * mat[1] + x[4] * mat[4],
+                x[3] * mat[2] + x[4] * mat[5] + x[5]]
+
+        return x
+
+    def RecomputeTransforms(self):
+        """
+        Re-compute the transform stack into a single transform and
+        store in the DC
+        """
+        self.dc['transform'] = self._ComposeMatricesWithIdentity(
+            self.dc['transforms'])
+
+    def TransformTranslate(self, x, y, push=True):
+        """
+        Set up and return a transform matrix representing a translartion
+        optionally pushing onto the stack
+
+        (   1  0   x  )
+        (   0  1   y  )
+        """
+        mat = [1, 0, x, 0, 1, y]
+
+        if push:
+            self.PushTransform(mat)
+        return mat
+
+    def TransformFlipOrigin(self, flip, push=True):
+        """
+        Set up and return a transform matrix representing a horizontal,
+        vertical or both flip about the origin
+        """
+        mat = None
+        if flip == self.flipX:
+            mat = [-1, 0, 0, 0, 1, 0]
+        elif flip == self.flipY:
+            mat = [1, 0, 0, 0, -1, 0]
+        elif flip == self.flipBoth:
+            mat = [-1, 0, 0, 0, -1, 0]
+        elif flip == self.flipNone:
+            mat = self.xfrmIDENTITY
+        else:
+            raise ValueError
+
+        if push:
+            self.PushTransform(mat)
+        return mat
+
+    def TransformFlip(self, x, y, flip=flipNone, push=True):
+        """
+        Set up and return a transform matrix representing a horizontal,
+        vertical or both flip about a point (x,y)
+
+        This is performed by a translate-to-origin, flip, translate-
+        back sequence
+        """
+        mats = [self.TransformTranslate(x, y, push=False),
+                self.TransformFlipOrigin(flip, push=False),
+                self.TransformTranslate(-x, -y, push=False)]
+
+        #distill into a single matrix
+        mat = self._ComposeMatricesWithIdentity(mats)
+
+        if push:
+            self.PushTransform(mat)
+        return mat
+
+    def TransformRotationOrigin(self, rot, push=True):
+        """
+        Set up and return a transform matrix representing a rotation
+        about the origin, and optionally push onto the stack
+
+        (   cos(t)  -sin(t)   0  )
+        (   sin(t)   cos(t)   0  )
+        """
+        rads = rot * math.pi / 180
+        mat = [math.cos(rads), -math.sin(rads), 0,
+               math.sin(rads), math.cos(rads), 0]
+
+        if push:
+            self.PushTransform(mat)
+        return mat
+
+    def TransformRotation(self, x, y, rot, push=True):
+        """
+        Set up and return a transform matrix representing a rotation
+        about the pooint (x,y), and optionally push onto the stack
+
+        This is performed by a translate-to-origin, rotate, translate-
+        back sequence
+        """
+
+        mats = [self.TransformTranslate(x, y, push=False),
+                self.TransformRotationOrigin(rot, push=False),
+                self.TransformTranslate(-x, -y, push=False)]
+
+        #distill into a single matrix
+        mat = self._ComposeMatricesWithIdentity(mats)
+
+        if push:
+            self.PushTransform(mat)
+        return mat
+
+    def TransformScaleOrigin(self, sx, sy=None, push=True):
+        """
+        Set up and return a transform matrix representing a scale about
+        the origin, and optionally push onto the stack
+
+        (   sx   0   0  )
+        (    0  sy   0  )
+        """
+
+        if sy is None:
+            sy = sx
+
+        mat = [sx, 0, 0, 0, sy, 0]
+
+        if push:
+            self.PushTransform(mat)
+        return mat
+
+    def TransformPoint(self, x, y, mat=None):
+        """
+        Return a point (x, y) transformed by the given matrix, or if
+        that is not given, the drawing context transform
+        """
+
+        if not mat:
+            mat = self.dc['transform']
+
+        return pcbnew.wxPoint(x * mat[0] + y * mat[1] + mat[2],
+                              x * mat[3] + y * mat[4] + mat[5])
+
     def SetWidth(self, width):
+        """
+        Set the current pen width used for subsequent drawing
+        operations
+        """
         self.dc['width'] = width
 
+    def GetWidth(self):
+        """
+        Get the current drawing context width
+        """
+        return self.dc['width']
+
     def SetLayer(self, layer):
+        """
+        Set the current drawing layer, used for subsequent drawing
+        operations
+        """
         self.dc['layer'] = layer
 
     def Line(self, x1, y1, x2, y2):
+        """
+        Draw a line from (x1, y1) to (x2, y2)
+        """
 
         outline = pcbnew.EDGE_MODULE(self.module)
         outline.SetWidth(self.dc['width'])
         outline.SetLayer(self.dc['layer'])
         outline.SetShape(pcbnew.S_SEGMENT)
-        start = pcbnew.wxPoint(x1, y1)
-        end = pcbnew.wxPoint(x2, y2)
+        start = self.TransformPoint(x1, y1)
+        end = self.TransformPoint(x2, y2)
         outline.SetStartEnd(start, end)
         self.module.Add(outline)
 
+    def Circle(self, x, y, r, filled=False):
+        """
+        Draw a circle at (x,y) of radius r
+
+        If filled is true, the width and radius of the line will be set
+        such that the circle appears filled
+        """
+        circle = pcbnew.EDGE_MODULE(self.module)
+        start = self.TransformPoint(x, y)
+
+        if filled:
+            circle.SetWidth(r)
+            end = self.TransformPoint(x, y + r/2)
+        else:
+            circle.SetWidth(self.dc['width'])
+            end = self.TransformPoint(x, y + r)
+
+        circle.SetLayer(self.dc['layer'])
+        circle.SetShape(pcbnew.S_CIRCLE)
+        circle.SetStartEnd(start, end)
+        self.module.Add(circle)
+
+    def Arc(self, cx, cy, sx, sy, a):
+        """
+        Draw an arc based on centre, start and angle
+
+        The transform matrix is applied
+
+        Note that this won't work properly if the result is not a
+        circular arc (eg a horzontal scale)
+        """
+        circle = pcbnew.EDGE_MODULE(self.module)
+        circle.SetWidth(self.dc['width'])
+
+        center = self.TransformPoint(cx, cy)
+        start = self.TransformPoint(sx, sy)
+
+        circle.SetLayer(self.dc['layer'])
+        circle.SetShape(pcbnew.S_ARC)
+
+        # check if the angle needs to be reverse (a flip scaling)
+        if cmp(self.dc['transform'][0], 0) != cmp(self.dc['transform'][4], 0):
+            a = -a
+
+        circle.SetAngle(a)
+        circle.SetStartEnd(center, start)
+        self.module.Add(circle)
+
     # extends from (x1,y1) right
     def HLine(self, x, y, l):
         """
@@ -62,13 +326,34 @@ class FootprintWizardDrawingAids:
         """
         self.Line(x, y, x, y + l)
 
-    def Polyline(self, pts):
+    def Polyline(self, pts, mirrorX=None, mirrorY=None):
+        """
+        Draw a polyline, optinally mirroring around the given points
+        """
+
+        def _PolyLineInternal(pts):
+            if len(pts) < 2:
+                return
 
-        if len(pts) < 2:
-            return
+            for i in range(0, len(pts) - 1):
+                self.Line(pts[i][0], pts[i][1], pts[i+1][0], pts[i+1][1])
 
-        for i in range(0, len(pts) - 1):
-            self.Line(pts[i][0], pts[i][1], pts[i+1][0], pts[i+1][1])
+        _PolyLineInternal(pts)  # original
+
+        if mirrorX is not None:
+            self.TransformFlip(mirrorX, 0, self.flipX)
+            _PolyLineInternal(pts)
+            self.PopTransform()
+
+        if mirrorY is not None:
+            self.TransformFlipOrigin(0, mirrorY, self.flipY)
+            _PolyLineInternal(pts)
+            self.PopTransform()
+
+        if mirrorX is not None and mirrorY is not None:
+            self.TransformFlip(mirrorX, mirrorY, self.flipBoth)  # both
+            _PolyLineInternal(pts)
+            self.PopTransform()
 
     def Reference(self, x, y, size):
         """
@@ -80,8 +365,9 @@ class FootprintWizardDrawingAids:
 
         text_size = pcbnew.wxSize(size, size)
 
-        self.module.Reference().SetPos0(pcbnew.wxPoint(x, y))
-        self.module.Reference().SetTextPosition(self.module.Reference().GetPos0())
+        self.module.Reference().SetPos0(self.TransformPoint(x, y))
+        self.module.Reference().SetTextPosition(
+            self.module.Reference().GetPos0())
         self.module.Reference().SetSize(text_size)
 
     def Value(self, x, y, size):
@@ -90,7 +376,7 @@ class FootprintWizardDrawingAids:
         """
         text_size = pcbnew.wxSize(size, size)
 
-        self.module.Value().SetPos0(pcbnew.wxPoint(x, y))
+        self.module.Value().SetPos0(self.TransformPoint(x, y))
         self.module.Value().SetTextPosition(self.module.Value().GetPos0())
         self.module.Value().SetSize(text_size)
 
@@ -99,36 +385,115 @@ class FootprintWizardDrawingAids:
         Draw a rectangular box, centred at (x,y), with given width and
         height
         """
-        self.VLine(x - w/2, y - h/2, h) # left
-        self.VLine(x + w/2, y - h/2, h) # right
-        self.HLine(x - w/2, y + h/2, w) # bottom
-        self.HLine(x - w/2, y - h/2, w) # top
+
+        pts = [[x - w/2, y - h/2],  # left
+               [x + w/2, y - h/2],  # right
+               [x + w/2, y + h/2],  # bottom
+               [x - w/2, y + h/2],  # top
+               [x - w/2, y - h/2]]  # close
+
+        self.Polyline(pts)
+
+    def NotchedCircle(self, x, y, r, notch_w, notch_h):
+        """
+        Circle radus r centred at (x, y) with a raised or depressed notch
+        at the top
+
+        Notch height is measured from the top of the circle radius
+        """
+        # find the angle where the notch vertical meets the circle
+        angle_intercept = math.asin(notch_w/(2 * r))
+
+        # and find the co-ords of this point
+        sx = math.sin(angle_intercept) * r
+        sy = -math.cos(angle_intercept) * r
+
+        # NOTE: this may be out by a factor of ten one day
+        arc_angle = (math.pi * 2 - angle_intercept * 2) * (1800/math.pi)
+
+        self.Arc(x,y, sx, sy, arc_angle)
+
+        pts = [[sx,  sy],
+               [sx,  -r - notch_h],
+               [-sx, -r - notch_h],
+               [-sx, sy]]
+
+        self.Polyline(pts)
 
     def NotchedBox(self, x, y, w, h, notchW, notchH):
         """
         Draw a box with a notch in the top edge
         """
-        #limit to half the overall width
+        # limit to half the overall width
         notchW = min(x + w/2, notchW)
 
         # draw notch
-        self.Polyline([ #three sides of box
-                        (x - w/2, y - h/2),
-                        (x - w/2, y + h/2),
-                        (x + w/2, y + h/2),
-                        (x + w/2, y - h/2),
-                        #the notch
-                        (notchW/2, y - h/2),
-                        (notchW/2, y - h/2 + notchH),
-                        (-notchW/2, y - h/2 + notchH),
-                        (-notchW/2, y - h/2),
-                        (x - w/2, y - h/2)
-                    ])
-
-    def BoxWithDiagonalAtCorner(self, x, y, w, h, diagSetback):
-
-        self.Box(x, y, w, h)
-
-        #diagonal corner
-        self.Line(x - w/2 + diagSetback, x - h/2, x - w/2,
-                x - h/2 + diagSetback)
+        self.Polyline([  # three sides of box
+            (x - w/2, y - h/2),
+            (x - w/2, y + h/2),
+            (x + w/2, y + h/2),
+            (x + w/2, y - h/2),
+            # the notch
+            (notchW/2, y - h/2),
+            (notchW/2, y - h/2 + notchH),
+            (-notchW/2, y - h/2 + notchH),
+            (-notchW/2, y - h/2),
+            (x - w/2, y - h/2)
+        ])
+
+    def BoxWithDiagonalAtCorner(self, x, y, w, h,
+                                setback=pcbnew.FromMM(1.27), flip=flipNone):
+        """
+        Draw a box with a diagonal at the top left corner
+        """
+
+        self.TransformFlip(x, y, flip, push=True)
+
+        pts = [[x - w/2 + setback, y - h/2],
+               [x - w/2,           y - h/2 + setback],
+               [x - w/2,           y + h/2],
+               [x + w/2,           y + h/2],
+               [x + w/2,           y - h/2],
+               [x - w/2 + setback, y - h/2]]
+
+        self.Polyline(pts)
+
+        self.PopTransform()
+
+    def BoxWithOpenCorner(self, x, y, w, h,
+                          setback=pcbnew.FromMM(1.27), flip=flipNone):
+        """
+        Draw a box with an opening at the top left corner
+        """
+
+        self.TransformTranslate(x, y)
+        self.TransformFlipOrigin(flip)
+
+        pts = [[- w/2,           - h/2 + setback],
+               [- w/2,           + h/2],
+               [+ w/2,           + h/2],
+               [+ w/2,           - h/2],
+               [- w/2 + setback, - h/2]]
+
+        self.Polyline(pts)
+
+        self.PopTransform(num=2)
+
+    def MarkerArrow(self, x, y, direction=dirN, width=pcbnew.FromMM(1)):
+        """
+        Draw a marker arrow facing in the given direction, with the
+        point at (x,y)
+
+        Direction of 0 is north
+        """
+
+        self.TransformTranslate(x, y)
+        self.TransformRotationOrigin(direction)
+
+        pts = [[0,          0],
+               [width / 2,  width / 2],
+               [-width / 2, width / 2],
+               [0,          0]]
+
+        self.Polyline(pts)
+        self.PopTransform(2)
diff --git a/pcbnew/scripting/plugins/HelpfulFootprintWizardPlugin.py b/pcbnew/scripting/plugins/HelpfulFootprintWizardPlugin.py
index de93cfd..4b69ef0 100644
--- a/pcbnew/scripting/plugins/HelpfulFootprintWizardPlugin.py
+++ b/pcbnew/scripting/plugins/HelpfulFootprintWizardPlugin.py
@@ -15,8 +15,10 @@
 #
 
 import pcbnew
+import math
 import FootprintWizardDrawingAids
 
+
 class FootprintWizardParameterManager:
     """
     Functions for helpfully managing parameters to a KiCAD Footprint
@@ -50,8 +52,9 @@ class FootprintWizardParameterManager:
     uMils = 2
     uNatural = 3
     uBool = 4
+    uString = 5
 
-    def AddParam(self, section, param, unit, default, hint = ''):
+    def AddParam(self, section, param, unit, default, hint=''):
         """
         Add a parameter with some properties.
 
@@ -66,14 +69,16 @@ class FootprintWizardParameterManager:
             val = pcbnew.FromMils(default)
         elif unit == self.uNatural:
             val = default
+        elif unit == self.uString:
+            val = str(default)
         elif unit == self.uBool:
-            val = "True" if default else "False" #ugly stringing
+            val = "True" if default else "False"  # ugly stringing
         else:
             print "Warning: Unknown unit type: %s" % unit
             return
 
-        if unit in [self.uNatural, self.uBool]:
-            param = "*%s" % param #star prefix for natural
+        if unit in [self.uNatural, self.uBool, self.uString]:
+            param = "*%s" % param  # star prefix for natural
 
         if section not in self.parameters:
             self.parameters[section] = {}
@@ -89,7 +94,8 @@ class FootprintWizardParameterManager:
 
             for key, value in section.iteritems():
                 unit = ""
-                if (type(value) is int or type(value) is float) and not "*" in key:
+                if ((type(value) is int or type(value) is float)
+                        and not "*" in key):
                     unit = "mm"
 
                 if "*" in key:
@@ -101,7 +107,7 @@ class FootprintWizardParameterManager:
 
     def _ParametersHaveErrors(self):
         """
-        Return true if we discovered errors suring parameter processing
+        Return true if we discovered errors during parameter processing
         """
 
         for name, section in self.parameter_errors.iteritems():
@@ -124,8 +130,8 @@ class FootprintWizardParameterManager:
                     if not printed_section:
                         print "  %s:" % name
 
-                    print "       %s: %s (have %s)" % (key, value,
-                                        self.parameters[name][key])
+                    print "       %s: %s (have %s)" % (
+                        key, value, self.parameters[name][key])
 
     def ProcessParameters(self):
         """
@@ -134,14 +140,15 @@ class FootprintWizardParameterManager:
         """
 
         self.ClearErrors()
-        self.CheckParameters();
+        self.CheckParameters()
 
         if self._ParametersHaveErrors():
             print "Cannot build footprint: Parameters have errors:"
             self._PrintParameterErrors()
             return False
 
-        print "Building new %s footprint with the following parameters:" % self.name
+        print ("Building new %s footprint with the following parameters:"
+               % self.name)
 
         self._PrintParameterTable()
         return True
@@ -150,29 +157,37 @@ class FootprintWizardParameterManager:
     # PARAMETER CHECKERS
     #################################################################
 
-    def CheckParamPositiveInt(self, section, param, min_value = 1,
-                                max_value = None, is_multiple_of = 1):
+    def CheckParamInt(self, section, param, min_value=1,
+                      max_value=None, is_multiple_of=1):
         """
         Make sure a parameter can be made into an int, and enforce
         limits if required
         """
 
         try:
-            self.parameters[section][param] = int(self.parameters[section][param])
+            self.parameters[section][param] = (
+                int(self.parameters[section][param]))
         except ValueError:
-            self.parameter_errors[section][param] = "Must be a valid integer"
+            self.parameter_errors[section][param] = (
+                "Must be a valid integer")
             return
 
-        if min_value is not None and (self.parameters[section][param] < min_value):
-            self.parameter_errors[section][param] = "Must be greater than or equal to %d" % (min_value)
+        if min_value is not None and (
+                self.parameters[section][param] < min_value):
+            self.parameter_errors[section][param] = (
+                "Must be greater than or equal to %d" % (min_value))
             return
 
-        if max_value is not None and (self.parameters[section][param] > min_value):
-            self.parameter_errors[section][param] = "Must be less than or equal to %d" % (max_value)
+        if max_value is not None and (
+                self.parameters[section][param] > min_value):
+            self.parameter_errors[section][param] = (
+                "Must be less than or equal to %d" % (max_value))
             return
 
-        if is_multiple_of > 1 and (self.parameters[section][param] % is_multiple_of) > 0:
-            self.parameter_errors[section][param] = "Must be a multiple of %d" % is_multiple_of
+        if is_multiple_of > 1 and (
+                self.parameters[section][param] % is_multiple_of) > 0:
+            self.parameter_errors[section][param] = (
+                "Must be a multiple of %d" % is_multiple_of)
             return
 
         return
@@ -182,11 +197,13 @@ class FootprintWizardParameterManager:
         Make sure a parameter looks like a boolean, convert to native
         boolean type if so
         """
-        if str(self.parameters[section][param]).lower() in ["true", "t", "y", "yes", "on", "1", "1.0"]:
-            self.parameters[section][param] = True;
+        if str(self.parameters[section][param]).lower() in [
+                "true", "t", "y", "yes", "on", "1", "1.0"]:
+            self.parameters[section][param] = True
             return
-        elif str(self.parameters[section][param]).lower() in ["false", "f", "n", "no", "off", "0", "0.0"]:
-            self.parameters[section][param] = False;
+        elif str(self.parameters[section][param]).lower() in [
+                "false", "f", "n", "no", "off", "0", "0.0"]:
+            self.parameters[section][param] = False
             return
 
         self.parameter_errors[section][param] = "Must be boolean (true/false)"
@@ -194,7 +211,7 @@ class FootprintWizardParameterManager:
 
 
 class HelpfulFootprintWizardPlugin(pcbnew.FootprintWizardPlugin,
-                                    FootprintWizardParameterManager):
+                                   FootprintWizardParameterManager):
     """
     A class to simplify many aspects of footprint creation, leaving only
     the foot-print specific routines to the wizards themselves
@@ -216,16 +233,46 @@ class HelpfulFootprintWizardPlugin(pcbnew.FootprintWizardPlugin,
         self.decription = self.GetDescription()
         self.image = self.GetImage()
 
-    def GetReference(self):
+    def GetValue(self):
         raise NotImplementedError
 
-    def GetValuePrefix(self):
-        return "U" # footprints needing wizards of often ICs
+    def GetReferencePrefix(self):
+        return "U"  # footprints needing wizards of often ICs
 
     def GetImage(self):
         return ""
 
+    def GetTextSize(self):
+        """
+        IPC nominal
+        """
+        return pcbnew.FromMM(1.2)
+
+    def GetTextThickness(self):
+        """
+        Thicker than IPC guidelines (10% of text height = 0.12mm)
+        as 5 wires/mm is a common silk screen limitation
+        """
+        return pcbnew.FromMM(0.2)
+
+    def SetModule3DModel(self):
+        """
+        Set a 3D model for the module
+
+        Default is to do nothing, you need to implement this if you have
+        a model to set
+
+        FIXME: This doesn't seem to be enabled yet?
+        """
+        pass
+
     def BuildThisFootprint(self):
+        """
+        Draw the footprint.
+
+        This is specific to each footprint class, you need to implment
+        this to draw what you want
+        """
         raise NotImplementedError
 
     def BuildFootprint(self):
@@ -234,17 +281,26 @@ class HelpfulFootprintWizardPlugin(pcbnew.FootprintWizardPlugin,
         the implmenting class
         """
 
+        self.module = pcbnew.MODULE(None)  # create a new module
+        # do it first, so if we return early, we don't segfault KiCad
+
         if not self.ProcessParameters():
             return
 
-        self.module = pcbnew.MODULE(None) # create a new module
+        self.draw = FootprintWizardDrawingAids.FootprintWizardDrawingAids(
+            self.module)
+
+        self.module.SetValue(self.GetValue())
+        self.module.SetReference("%s**" % self.GetReferencePrefix())
+
+        fpid = pcbnew.FPID(self.module.GetValue())  # the name in library
+        self.module.SetFPID(fpid)
 
-        self.draw = FootprintWizardDrawingAids.FootprintWizardDrawingAids(self.module)
+        self.BuildThisFootprint()  # implementer's build function
 
-        self.module.SetReference(self.GetReference())
-        self.module.SetValue("%s**" % self.GetValuePrefix())
+        self.SetModule3DModel()  # add a 3d module if specified
 
-        fpid = pcbnew.FPID(self.module.GetReference())   #the name in library
-        self.module.SetFPID( fpid )
+        thick = self.GetTextThickness()
 
-        self.BuildThisFootprint() # implementer's build function
+        self.module.Reference().SetThickness(thick)
+        self.module.Value().SetThickness(thick)
diff --git a/pcbnew/scripting/plugins/PadArray.py b/pcbnew/scripting/plugins/PadArray.py
index 9e4d9b3..97b93c2 100644
--- a/pcbnew/scripting/plugins/PadArray.py
+++ b/pcbnew/scripting/plugins/PadArray.py
@@ -1,4 +1,27 @@
-
+#  PadArray.py
+#
+#  Copyright 2014 john <john@johndev>
+#
+#  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.
+#
+#
+
+from __future__ import division
+
+import math
 import pcbnew
 
 class PadMaker:
@@ -9,7 +32,7 @@ class PadMaker:
     def __init__(self, module):
         self.module = module
 
-    def THPad(self, w, l, drill, shape = pcbnew.PAD_OVAL):
+    def THPad(self, w, l, drill, shape=pcbnew.PAD_OVAL):
         pad = pcbnew.D_PAD(self.module)
 
         pad.SetSize(pcbnew.wxSize(l, w))
@@ -22,7 +45,23 @@ class PadMaker:
 
         return pad
 
-    def SMDPad(self, w, l, shape = pcbnew.PAD_RECT):
+    def THRoundPad(self, size, drill):
+        pad = self.THPad(size, size, drill, shape=pcbnew.PAD_CIRCLE)
+        return pad
+
+    def NPTHRoundPad(self, drill):
+        pad = pcbnew.D_PAD(self.module)
+
+        pad.SetSize(pcbnew.wxSize(drill, drill))
+
+        pad.SetShape(pcbnew.PAD_CIRCLE)
+
+        pad.SetAttribute(pcbnew.PAD_HOLE_NOT_PLATED)
+        pad.SetLayerSet(pad.UnplatedHoleMask())
+        pad.SetDrillSize(pcbnew.wxSize(drill, drill))
+        return pad
+
+    def SMDPad(self, w, l, shape=pcbnew.PAD_RECT):
         pad = pcbnew.D_PAD(self.module)
         pad.SetSize(pcbnew.wxSize(l, w))
 
@@ -34,23 +73,65 @@ class PadMaker:
         return pad
 
     def SMTRoundPad(self, size):
-        pad = self.SMDPad(size, size, shape = pcbnew.PAD_CIRCLE)
+        pad = self.SMDPad(size, size, shape=pcbnew.PAD_CIRCLE)
         return pad
 
+
 class PadArray:
 
     def __init__(self):
-        self.firstPad = 1;
+        self.firstPadNum = 1
+        self.pinNames = None
+        self.firstPad = None
+
+    def SetPinNames(self, pinNames):
+        """
+        Set a name for all the pins
+        """
+        self.pinNames = pinNames
+
+    def SetFirstPadType(self, firstPad):
+        self.firstPad = firstPad
 
     def SetFirstPadInArray(self, fpNum):
-        self.firstPad = fpNum
+        self.firstPadNum = fpNum
 
     def AddPad(self, pad):
         self.pad.GetParent().Add(pad)
 
+    def GetPad(self, is_first_pad, pos):
+
+        if (self.firstPad and is_first_pad):
+            pad = self.firstPad
+        else:
+            pad = self.pad
+
+        # create a new pad with same characteristics
+        pad = pad.Duplicate()
+
+        pad.SetPos0(pos)
+        pad.SetPosition(pos)
+
+        return pad
+
+    def GetName(self, *args, **kwargs):
+
+        if self.pinNames is None:
+            return self.NamingFunction(*args, **kwargs)
+
+        return self.pinNames
+
+    def NamingFunction(self, *args, **kwargs):
+        """
+        Implement this as needed for each array type
+        """
+        raise NotImplementedError;
+
+
 class PadGridArray(PadArray):
 
-    def __init__(self, pad, nx, ny, px, py, pin1Pos):
+    def __init__(self, pad, nx, ny, px, py, centre=pcbnew.wxPoint(0, 0)):
+        PadArray.__init__(self)
         # this pad is more of a "context", we will use it as a source of
         # pad data, but not actually add it
         self.pad = pad
@@ -58,78 +139,121 @@ class PadGridArray(PadArray):
         self.ny = int(ny)
         self.px = px
         self.py = py
-        self.pin1Pos = pin1Pos
+        self.centre = centre
 
     # handy utility function 1 - A, 2 - B, 26 - AA, etc
     # aIndex = 0 for 0 - A
-    # alphabet = set of allowable chars if not A-Z, eg ABCDEFGHJKLMNPRTUVWY for BGA
-    def AlphaNameFromNumber(self, n, aIndex = 1, alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
+    # alphabet = set of allowable chars if not A-Z,
+    #            eg ABCDEFGHJKLMNPRTUVWY for BGA
+    def AlphaNameFromNumber(self, n, aIndex=1,
+                            alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
 
         div, mod = divmod(n - aIndex, len(alphabet))
         alpha = alphabet[mod]
 
         if div > 0:
-            return self.AlphaNameFromNumber(div, aIndex, alphabet) + alpha;
+            return self.AlphaNameFromNumber(div, aIndex, alphabet) + alpha
 
-        return alpha;
+        return alpha
 
     # right to left, top to bottom
     def NamingFunction(self, x, y):
-        return self.firstPad + (self.nx * y + x)
+        return self.firstPadNum + (self.nx * y + x)
 
     #relocate the pad and add it as many times as we need
-    def AddPadsToModule(self):
+    def AddPadsToModule(self, dc):
+
+        pin1posX = self.centre.x - self.px * (self.nx - 1) / 2
+        pin1posY = self.centre.y - self.py * (self.ny - 1) / 2
 
         for x in range(0, self.nx):
-            for y in range(self.ny):
-                posX = self.pin1Pos.x + (self.px * x)
-                posY = self.pin1Pos.y + (self.py * y)
 
-                pos = pcbnew.wxPoint(posX, posY)
+            posX = pin1posX + (x * self.px)
 
-                # create a new pad with same characteristics
-                pad = self.pad.Duplicate()
+            for y in range(self.ny):
+                posY = pin1posY + (self.py * y)
+
+                pos = dc.TransformPoint(posX, posY)
 
-                pad.SetPos0(pos)
-                pad.SetPosition(pos)
+                pad = self.GetPad(x == 0 and y == 0, pos)
 
-                pad.SetPadName(str(self.NamingFunction(x,y)))
+                pad.SetPadName(self.GetName(x,y))
 
                 self.AddPad(pad)
 
+
 class PadLineArray(PadGridArray):
 
-    def __init__(self, pad, n, pitch, isVertical, pin1Pos):
+    def __init__(self, pad, n, pitch, isVertical,
+                 centre=pcbnew.wxPoint(0, 0)):
 
         if isVertical:
-            PadGridArray.__init__(self, pad, 1, n, 0, pitch, pin1Pos)
+            PadGridArray.__init__(self, pad, 1, n, 0, pitch, centre)
         else:
-            PadGridArray.__init__(self, pad, n, 1, pitch, 0, pin1Pos)
-
-class RectPadArray(PadArray):
-
-    def __init__(self, nx, ny, pitch, xpitch, ypitch, pin1Pos):
-
-        #left row
-        pin1Pos = pcbnew.wxPoint(-h_pitch / 2, -row_len / 2)
-        array = PadLineArray(h_pad, pads_per_row, pad_pitch, True, pin1Pos)
-        array.SetFirstPadInArray(1)
-        array.AddPadsToModule()
-
-        #bottom row
-        pin1Pos = pcbnew.wxPoint(-row_len / 2, v_pitch / 2)
-        array = PA.PadLineArray(v_pad, pads_per_row, pad_pitch, False, pin1Pos)
-        array.SetFirstPadInArray(pads_per_row + 1)
-        array.AddPadsToModule()
-
-        #right row
-        pin1Pos = pcbnew.wxPoint(h_pitch / 2, row_len / 2)
-        array = PadLineArray(h_pad, pads_per_row, -pad_pitch, True, pin1Pos)
-        array.SetFirstPadInArray(2*pads_per_row + 1)
-        array.AddPadsToModule()
-
-        #top row
-        pin1Pos = pcbnew.wxPoint(row_len / 2, -v_pitch / 2)
-        array = PadLineArray(v_pad, pads_per_row, -pad_pitch, False, pin1Pos)
-        array.SetFirstPadInArray(3*pads_per_row + 1)
-        array.AddPadsToModule()
+            PadGridArray.__init__(self, pad, n, 1, pitch, 0, centre)
+
+class PadCircleArray(PadArray):
+
+    def __init__(self, pad, n, r, angle_offset=0, centre=pcbnew.wxPoint(0, 0),
+                 clockwise=True):
+        PadArray.__init__(self)
+        # this pad is more of a "context", we will use it as a source of
+        # pad data, but not actually add it
+        self.pad = pad
+        self.n = int(n)
+        self.r = r
+        self.angle_offset = angle_offset
+        self.centre = centre
+        self.clockwise = clockwise
+
+    # around the circle, CW or CCW according to the flag
+    def NamingFunction(self, n):
+        return str(self.firstPadNum + n)
+
+    #relocate the pad and add it as many times as we need
+    def AddPadsToModule(self, dc):
+
+        for pin in range(0, self.n):
+
+            angle = self.angle_offset + (360 / self.n) * pin
+
+            if not self.clockwise:
+                angle = -angle
+
+            pos_x = math.sin(angle * math.pi / 180) * self.r
+            pos_y = -math.cos(angle  * math.pi / 180) * self.r
+
+            pos = dc.TransformPoint(pos_x, pos_y)
+
+            pad = self.GetPad(pin == 0, pos)
+
+            pad.SetPadName(self.GetName(pin))
+
+            self.AddPad(pad)
+
+class PadCustomArray(PadArray):
+    """
+    Layout pads according to a custom array of [x,y] data
+    """
+
+    def __init__(self, pad, array):
+        PadArray.__init__(self)
+        self.pad = pad
+
+        self.array = array
+
+    def NamingFunction(self, n):
+        return str(self.firstPadNum + n)
+
+    #relocate the pad and add it as many times as we need
+    def AddPadsToModule(self, dc):
+
+        for i in range(len(self.array)):
+
+            pos = dc.TransformPoint(self.array[i][0], self.array[i][1])
+
+            pad = self.GetPad(i == 0, pos)
+
+            pad.SetPadName(self.GetName(i))
+
+            self.AddPad(pad)
diff --git a/pcbnew/scripting/plugins/__init__.py b/pcbnew/scripting/plugins/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/pcbnew/scripting/plugins/__init__.py
@@ -0,0 +1 @@
+
diff --git a/pcbnew/scripting/plugins/bga_wizard.py b/pcbnew/scripting/plugins/bga_wizard.py
index 9b68b48..2ca044a 100644
--- a/pcbnew/scripting/plugins/bga_wizard.py
+++ b/pcbnew/scripting/plugins/bga_wizard.py
@@ -23,8 +23,10 @@ import PadArray as PA
 
 class BGAPadGridArray(PA.PadGridArray):
 
-    def NamingFunction(self, x, y):
-        return "%s%d" % (self.AlphaNameFromNumber(y + 1, alphabet="ABCDEFGHJKLMNPRTUVWY"), x + 1)
+    def NamingFunction(self, n_x, n_y):
+        return "%s%d" % (
+            self.AlphaNameFromNumber(n_y + 1, alphabet="ABCDEFGHJKLMNPRTUVWY"),
+            n_x + 1)
 
 
 class BGAWizard(HFPW.HelpfulFootprintWizardPlugin):
@@ -46,21 +48,19 @@ class BGAWizard(HFPW.HelpfulFootprintWizardPlugin):
 
     def CheckParameters(self):
 
-        self.CheckParamPositiveInt("Pads", "*row count")
-        self.CheckParamPositiveInt("Pads", "*column count")
+        self.CheckParamInt("Pads", "*row count")
+        self.CheckParamInt("Pads", "*column count")
 
+    def GetValue(self):
 
-    def GetReference(self):
-
-        pins = self.parameters["Pads"]["*row count"] * self.parameters["Pads"]["*column count"]
+        pins = (self.parameters["Pads"]["*row count"]
+                * self.parameters["Pads"]["*column count"])
 
         return "BGA %d" % pins
 
-
-    def GetValuePrefix(self):
+    def GetReferencePrefix(self):
         return "U"
 
-
     def BuildThisFootprint(self):
 
         pads = self.parameters["Pads"]
@@ -76,23 +76,24 @@ class BGAWizard(HFPW.HelpfulFootprintWizardPlugin):
         # add in the pads
         pad = PA.PadMaker(self.module).SMTRoundPad(pads["pad size"])
 
-        pin1Pos = pcbnew.wxPoint(-((cols - 1) * pad_pitch) / 2,
-                                 -((rows - 1) * pad_pitch) / 2)
+        pin1_pos = pcbnew.wxPoint(-((cols - 1) * pad_pitch) / 2,
+                                  -((rows - 1) * pad_pitch) / 2)
 
-        array = BGAPadGridArray(pad, cols, rows, pad_pitch, pad_pitch, pin1Pos)
-        array.AddPadsToModule()
+        array = BGAPadGridArray(pad, cols, rows, pad_pitch, pad_pitch)
+        array.AddPadsToModule(self.draw)
 
         #box
-        ssX = -pin1Pos.x + pads["outline x margin"]
-        ssY = -pin1Pos.y + pads["outline y margin"]
+        ssx = -pin1_pos.x + pads["outline x margin"]
+        ssy = -pin1_pos.y + pads["outline y margin"]
 
-        self.draw.BoxWithDiagonalAtCorner(0, 0, ssX*2, ssY*2, pads["outline x margin"])
+        self.draw.BoxWithDiagonalAtCorner(0, 0, ssx*2, ssy*2,
+                                          pads["outline x margin"])
 
         #reference and value
-        textSize = pcbnew.FromMM(0.8)
+        text_size = pcbnew.FromMM(1.2)  # IPC nominal
 
-        self.draw.Value(0, - ssY - textSize, textSize)
-        self.draw.Reference(0, ssY + textSize, textSize)
+        self.draw.Value(0, - ssy - text_size, text_size)
+        self.draw.Reference(0, ssy + text_size, text_size)
 
 
 BGAWizard().register()
diff --git a/pcbnew/scripting/plugins/pad_arrays.py b/pcbnew/scripting/plugins/pad_arrays.py
new file mode 100644
index 0000000..b498b36
--- /dev/null
+++ b/pcbnew/scripting/plugins/pad_arrays.py
@@ -0,0 +1,77 @@
+#  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.
+#
+from __future__ import division
+
+import math
+
+import pcbnew
+import HelpfulFootprintWizardPlugin as HFPW
+import PadArray as PA
+
+
+class circular_array_wizard(HFPW.HelpfulFootprintWizardPlugin):
+
+    def GetName(self):
+        return "Circular Array"
+
+    def GetDescription(self):
+        return "Circular array of pads"
+
+    def GenerateParameterList(self):
+
+        self.AddParam("Pads", "n", self.uNatural, 6)
+        self.AddParam("Pads", "pad width", self.uMM, 1.5)
+        self.AddParam("Pads", "drill", self.uMM, 1)
+        self.AddParam("Pads", "circle diameter", self.uMM, 5)
+        self.AddParam("Pads", "first pad angle", self.uNatural, 0)
+        self.AddParam("Pads", "number clockwise", self.uBool, True)
+        self.AddParam("Pads", "first pad number", self.uNatural, 1)
+
+    def CheckParameters(self):
+
+        self.CheckParamInt("Pads", "*n")
+        self.CheckParamInt("Pads", "*first pad number")
+        self.CheckParamBool("Pads", "*number clockwise")
+
+    def GetValue(self):
+        return "A"
+
+    def GetReference(self):
+        return ""
+
+    def BuildThisFootprint(self):
+
+        prm = self.parameters['Pads']
+
+        pad_size = prm['pad width']
+
+        pad = PA.PadMaker(self.module).THPad(
+            prm['pad width'],
+            prm['pad width'],
+            prm['drill'])
+
+        array = PA.PadCircleArray(
+            pad, prm['*n'], prm['circle diameter'] / 2,
+            angle_offset=prm["*first pad angle"],
+            centre=pcbnew.wxPoint(0, 0),
+            clockwise=prm["*number clockwise"])
+
+        array.SetFirstPadInArray(prm["*first pad number"])
+
+        array.AddPadsToModule(self.draw)
+
+
+circular_array_wizard().register()
diff --git a/pcbnew/scripting/plugins/qfp_wizard.py b/pcbnew/scripting/plugins/qfp_wizard.py
index 4964bcd..76fac37 100644
--- a/pcbnew/scripting/plugins/qfp_wizard.py
+++ b/pcbnew/scripting/plugins/qfp_wizard.py
@@ -1,9 +1,26 @@
+#  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.
+#
+
 from __future__ import division
 import pcbnew
 
 import HelpfulFootprintWizardPlugin
 import PadArray as PA
 
+
 class QFPWizard(HelpfulFootprintWizardPlugin.HelpfulFootprintWizardPlugin):
 
     def GetName(self):
@@ -26,10 +43,10 @@ class QFPWizard(HelpfulFootprintWizardPlugin.HelpfulFootprintWizardPlugin):
 
     def CheckParameters(self):
 
-        self.CheckParamPositiveInt("Pads", "*n", is_multiple_of = 4)
+        self.CheckParamInt("Pads", "*n", is_multiple_of=4)
         self.CheckParamBool("Pads", "*oval")
 
-    def GetReference(self):
+    def GetValue(self):
         return "QFP %d" % self.parameters["Pads"]["*n"]
 
     def BuildThisFootprint(self):
@@ -49,44 +66,58 @@ class QFPWizard(HelpfulFootprintWizardPlugin.HelpfulFootprintWizardPlugin):
 
         pad_shape = pcbnew.PAD_OVAL if pads["*oval"] else pcbnew.PAD_RECT
 
-        h_pad = PA.PadMaker(self.module).SMDPad(pad_width, pad_length, shape = pad_shape)
-        v_pad = PA.PadMaker(self.module).SMDPad(pad_length, pad_width, shape = pad_shape)
+        h_pad = PA.PadMaker(self.module).SMDPad(
+            pad_width, pad_length, shape=pad_shape)
+        v_pad = PA.PadMaker(self.module).SMDPad(
+            pad_length, pad_width, shape=pad_shape)
 
         #left row
-        pin1Pos = pcbnew.wxPoint(-h_pitch / 2, -row_len / 2)
-        array = PA.PadLineArray(h_pad, pads_per_row, pad_pitch, True, pin1Pos)
+        pin1Pos = pcbnew.wxPoint(-h_pitch / 2, 0)
+        array = PA.PadLineArray(h_pad, pads_per_row, pad_pitch, True,
+                                pin1Pos)
         array.SetFirstPadInArray(1)
-        array.AddPadsToModule()
+        array.AddPadsToModule(self.draw)
 
         #bottom row
-        pin1Pos = pcbnew.wxPoint(-row_len / 2, v_pitch / 2)
-        array = PA.PadLineArray(v_pad, pads_per_row, pad_pitch, False, pin1Pos)
+        pin1Pos = pcbnew.wxPoint(0, v_pitch / 2)
+        array = PA.PadLineArray(v_pad, pads_per_row, pad_pitch, False,
+                                pin1Pos)
         array.SetFirstPadInArray(pads_per_row + 1)
-        array.AddPadsToModule()
+        array.AddPadsToModule(self.draw)
 
         #right row
-        pin1Pos = pcbnew.wxPoint(h_pitch / 2, row_len / 2)
-        array = PA.PadLineArray(h_pad, pads_per_row, -pad_pitch, True, pin1Pos)
+        pin1Pos = pcbnew.wxPoint(h_pitch / 2, 0)
+        array = PA.PadLineArray(h_pad, pads_per_row, -pad_pitch, True,
+                                pin1Pos)
         array.SetFirstPadInArray(2*pads_per_row + 1)
-        array.AddPadsToModule()
+        array.AddPadsToModule(self.draw)
 
         #top row
-        pin1Pos = pcbnew.wxPoint(row_len / 2, -v_pitch / 2)
-        array = PA.PadLineArray(v_pad, pads_per_row, -pad_pitch, False, pin1Pos)
+        pin1Pos = pcbnew.wxPoint(0, -v_pitch / 2)
+        array = PA.PadLineArray(v_pad, pads_per_row, -pad_pitch, False,
+                                pin1Pos)
         array.SetFirstPadInArray(3*pads_per_row + 1)
-        array.AddPadsToModule()
+        array.AddPadsToModule(self.draw)
 
-        limX = pads["package width"] / 2
-        limY = pads["package height"] / 2
+        lim_x = pads["package width"] / 2
+        lim_y = pads["package height"] / 2
         inner = (row_len / 2) + pad_pitch
 
         #top left - diagonal
-        self.draw.Line(-limX, -inner, -inner, -limY)
+        self.draw.Line(-lim_x, -inner, -inner, -lim_y)
         # top right
-        self.draw.Polyline([(inner, -limY), (limX, -limY), (limX, -inner)])
+        self.draw.Polyline([(inner, -lim_y), (lim_x, -lim_y), (lim_x, -inner)])
         # bottom left
-        self.draw.Polyline([(-inner, limY), (-limX, limY), (-limX, inner)])
+        self.draw.Polyline([(-inner, lim_y), (-lim_x, lim_y), (-lim_x, inner)])
         # bottom right
-        self.draw.Polyline([(inner, limY), (limX, limY), (limX, inner)])
+        self.draw.Polyline([(inner, lim_y), (lim_x, lim_y), (lim_x, inner)])
+
+        #reference and value
+        text_size = pcbnew.FromMM(1.2)  # IPC nominal
+
+        text_offset = v_pitch / 2 + text_size + pad_length / 2
+
+        self.draw.Value(0, -text_offset, text_size)
+        self.draw.Reference(0, text_offset, text_size)
 
 QFPWizard().register()
diff --git a/pcbnew/scripting/plugins/sdip_wizard.py b/pcbnew/scripting/plugins/sdip_wizard.py
index c2a3765..876fa2f 100644
--- a/pcbnew/scripting/plugins/sdip_wizard.py
+++ b/pcbnew/scripting/plugins/sdip_wizard.py
@@ -24,10 +24,11 @@ import PadArray as PA
 class RowedGridArray(PA.PadGridArray):
 
     def NamingFunction(self, x, y):
-        if (x % 2) == 0: # even row, count up
-            return (x * self.ny) + y + 1;
-        else: # odd row, count down
-            return (self.ny * (x + 1)) - y;
+        if (x % 2) == 0:  # even row, count up
+            return (x * self.ny) + y + 1
+        else:  # odd row, count down
+            return (self.ny * (x + 1)) - y
+
 
 class RowedFootprint(HFPW.HelpfulFootprintWizardPlugin):
 
@@ -39,9 +40,13 @@ class RowedFootprint(HFPW.HelpfulFootprintWizardPlugin):
         self.AddParam("Pads", "row count", self.uNatural, 2)
 
     def CheckParameters(self):
-        self.CheckParamPositiveInt("Pads", "*row count")
-        self.CheckParamPositiveInt("Pads", "*n", is_multiple_of = self.parameters["Pads"]["*row count"])
-        self.CheckParamBool("Pads", "*silk screen inside") #can do this internally to parameter manager?
+        self.CheckParamInt("Pads", "*row count")
+        self.CheckParamInt(
+            "Pads", "*n",
+            is_multiple_of=self.parameters["Pads"]["*row count"])
+
+        # can do this internally to parameter manager?
+        self.CheckParamBool("Pads", "*silk screen inside")
 
     def BuildThisFootprint(self):
 
@@ -57,39 +62,40 @@ class RowedFootprint(HFPW.HelpfulFootprintWizardPlugin):
 
         pads_per_row = num_pads // num_rows
 
-        row_length = pad_pitch * (pads_per_row - 1) #fenceposts
+        row_length = pad_pitch * (pads_per_row - 1)  # fenceposts
 
         # add in the pads
         pad = self.GetPad()
 
-        pin1Pos = pcbnew.wxPoint(-((num_rows - 1) * row_pitch) / 2, -row_length / 2)
+        pin1_pos = pcbnew.wxPoint(
+            -((num_rows - 1) * row_pitch) / 2,
+            -row_length / 2)
 
-        array = RowedGridArray(pad, num_rows, pads_per_row, row_pitch, pad_pitch, pin1Pos)
-        array.AddPadsToModule()
+        array = RowedGridArray(pad, num_rows, pads_per_row, row_pitch,
+                               pad_pitch)
+        array.AddPadsToModule(self.draw)
 
         # draw the Silk Screen
 
         pad_length = pads["pad length"]
         pad_width = pads["pad width"]
 
-        ssXOffset = -pad_length / 2 - pads["outline x margin"]
-        ssYOffset = -pad_width / 2 - pads["outline y margin"]
-
+        ssx_offset = -pad_length / 2 - pads["outline x margin"]
+        ssy_offset = -pad_width / 2 - pads["outline y margin"]
 
         if pads["*silk screen inside"]:
-            ssXOffset *= -1
-
-        ssX = -pin1Pos.x - ssXOffset
-        ssY = -pin1Pos.y - ssYOffset
+            ssx_offset *= -1
 
+        ssx = -pin1_pos.x - ssx_offset
+        ssy = -pin1_pos.y - ssy_offset
 
-        self.DrawBox(ssX, ssY)
+        self.DrawBox(ssx, ssy)
 
         #reference and value
-        textSize = pcbnew.FromMM(0.8)
+        text_size = pcbnew.FromMM(1.2)  # IPC nominal
 
-        self.draw.Value(0, - ssY - textSize, textSize)
-        self.draw.Reference(0, ssY + textSize, textSize)
+        self.draw.Value(0, - ssy - text_size, text_size)
+        self.draw.Reference(0, ssy + text_size, text_size)
 
 
 class SDIPWizard(RowedFootprint):
@@ -111,15 +117,15 @@ class SDIPWizard(RowedFootprint):
         self.AddParam("Pads", "outline x margin", self.uMM, 0.5)
         self.AddParam("Pads", "outline y margin", self.uMM, 1)
 
-    def GetReference(self):
+    def GetValue(self):
 
         rows = self.parameters["Pads"]["*row count"]
 
-        if  rows == 1:
+        if rows == 1:
             name = "SIP"
         elif rows == 2:
             name = "DIP"
-        else: # triple and up aren't really a thing, but call it something!
+        else:  # triple and up aren't really a thing, but call it something!
             name = "xIP"
 
         return "%s %d" % (name, self.parameters["Pads"]["*n"])
@@ -128,9 +134,10 @@ class SDIPWizard(RowedFootprint):
         pad_length = self.parameters["Pads"]["pad length"]
         pad_width = self.parameters["Pads"]["pad width"]
         drill = self.parameters["Pads"]["drill size"]
-        return PA.PadMaker(self.module).THPad(pad_width, pad_length, drill, shape = pcbnew.PAD_OVAL)
+        return PA.PadMaker(self.module).THPad(
+            pad_width, pad_length, drill, shape=pcbnew.PAD_OVAL)
 
-    def DrawBox(self, ssX, ssY):
+    def DrawBox(self, ssx, ssy):
 
         if self.parameters["Pads"]["*row count"] == 2:
 
@@ -144,18 +151,19 @@ class SDIPWizard(RowedFootprint):
             notchWidth = pcbnew.FromMM(3)
             notchHeight = pcbnew.FromMM(1)
 
-            self.draw.NotchedBox(0, 0, ssX*2, ssY*2, notchWidth, notchHeight)
+            self.draw.NotchedBox(0, 0, ssx*2, ssy*2, notchWidth, notchHeight)
         else:
             #  -----------------
             #  |1|2 3 4 5 6 7 8|
             #  -----------------
-            self.draw.Box(ssX*2, ssY*2)
+            self.draw.Box(0, 0, ssx*2, ssy*2)
 
             #line between pin1 and pin2
-            pad_pitch = self.parameters["Pads"]["pad pitch"];
-            self.draw.HLine(-ssX, pin1Pos.y + pad_pitch/2, ssX * 2)
+            pad_pitch = self.parameters["Pads"]["pad pitch"]
+            line_y = - (self.parameters["Pads"]["*n"] - 2) * pad_pitch / 2
+            self.draw.HLine(-ssx, line_y, ssx * 2)
 
-        return ssX, ssY
+        return ssx, ssy
 
 SDIPWizard().register()
 
@@ -168,7 +176,7 @@ class SOICWizard(RowedFootprint):
     def GetDescription(self):
         return "SOIC, MSOP, SSOP, TSSOP, etc, footprint wizard"
 
-    def GetReference(self):
+    def GetValue(self):
         return "%s %d" % ("SOIC", self.parameters["Pads"]["*n"])
 
     def GenerateParameterList(self):
@@ -186,15 +194,16 @@ class SOICWizard(RowedFootprint):
     def GetPad(self):
         pad_length = self.parameters["Pads"]["pad length"]
         pad_width = self.parameters["Pads"]["pad width"]
-        return PA.PadMaker(self.module).SMDPad(pad_width, pad_length, shape = pcbnew.PAD_RECT)
+        return PA.PadMaker(self.module).SMDPad(
+            pad_width, pad_length, shape=pcbnew.PAD_RECT)
 
-    def DrawBox(self, ssX, ssY):
+    def DrawBox(self, ssx, ssy):
 
         #  ----------
         #  |8 7 6 5 |
         #  |1 2 3 4 |
         #  \---------
 
-        self.draw.BoxWithDiagonalAtCorner(0, 0, ssX*2, ssY*2, pcbnew.FromMM(1))
+        self.draw.BoxWithDiagonalAtCorner(0, 0, ssx*2, ssy*2, pcbnew.FromMM(1))
 
 SOICWizard().register()
diff --git a/pcbnew/scripting/tests/test_board.py b/pcbnew/scripting/tests/test_board.py
new file mode 100644
index 0000000..acd719d
--- /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, pcbnew.IO_MGR.KICAD)
+        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_fpw.py b/pcbnew/scripting/tests/test_fpw.py
new file mode 100644
index 0000000..48ea182
--- /dev/null
+++ b/pcbnew/scripting/tests/test_fpw.py
@@ -0,0 +1,78 @@
+#  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.
+#
+
+"""
+Test BGA, QFP and SDIP footprints
+"""
+
+from pcbnew import FromMM as fmm
+
+import test_board as TB
+
+import plugins.bga_wizard as BGA
+import plugins.qfp_wizard as QFP
+import plugins.sdip_wizard as SDIP
+
+def test_footprints():
+    """
+    Perform the test
+    """
+
+    test_brd = TB.set_up_simple_test_board()
+
+    test_brd.add_footprint(BGA.BGAWizard(), {
+        "pad pitch": fmm(1),
+        "pad size": fmm(0.5),
+        "*row count": 5,
+        "*column count": 5,
+        "outline x margin": fmm(1),
+        "outline y margin": fmm(1)
+    })
+
+    for oval in [True, False]:
+        test_brd.add_footprint(QFP.QFPWizard(), {
+            "*n": 64,
+            "pad pitch": fmm(0.5),
+            "pad width": fmm(0.25),
+            "pad length": fmm(1.5),
+            "vertical pitch": fmm(15),
+            "horizontal pitch": fmm(15),
+            "*oval": oval,
+            "package width": fmm(14),
+            "package height": fmm(14)
+        })
+
+    test_brd.new_row()
+
+    for rows in [1, 2]:
+        for ssi in [True, False]:
+            test_brd.add_footprint(SDIP.SDIPWizard(), {
+                "*n": 6,
+                "*silk screen inside": ssi,
+                "*row count": rows,
+                "pad pitch": fmm(2.54),
+                "pad width": fmm(1.5),
+                "pad length": fmm(3.8),
+                "row spacing": fmm(7.62),
+                "drill size": fmm(1),
+                "outline x margin": fmm(0.5),
+                "outline y margin": fmm(1)
+            })
+
+    test_brd.save_board_and_modules()
+
+if __name__ == "__main__":
+    test_footprints()

Follow ups