← Back to team overview

kicad-developers team mailing list archive

Footprint wizard changes

 

Hi,

I have made some changes to the helpers for the footprint wizards. There
is now a matrix-based transform stack that helps simplify calculations
that footprint wizards need to do. The patch attached is based on BZR rev 4975.

There are also some tests in the pcbnew/scripting/tests directory
(test_board.py and test_fpw.py). These use a method I'm not very happy
with: they construct a kicad_pcb file with the relevant modules in it,
and then disassemble it into modules. I couldn't see a way to directly
export modules to .kicad_mod files using the Python interface.

It also uses fixed sized "cells" as the call to module.GetBoundingBox()
segfaults.

I am very open to suggestions for this, but as it stands, at least it's
a fast-turnaround way to write new wizards and make sure the existing
ones aren't broken. I use it like this:

$ pcbnew/scripting/tests/test_fpw.py && pcbnew /tmp/test.kicad_pcb

Thanks,

John
diff --git a/pcbnew/scripting/plugins/FootprintWizardDrawingAids.py b/pcbnew/scripting/plugins/FootprintWizardDrawingAids.py
index d1046bd..fcd893a 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,285 @@ 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 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 +320,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
+
+            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 len(pts) < 2:
-            return
+        if mirrorX is not None:
+            self.TransformFlip(mirrorX, 0, self.flipX)
+            _PolyLineInternal(pts)
+            self.PopTransform()
 
-        for i in range(0, len(pts) - 1):
-            self.Line(pts[i][0], pts[i][1], pts[i+1][0], pts[i+1][1])
+        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 +359,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 +370,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 +379,89 @@ 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 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..23c4c9f 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
@@ -51,7 +53,7 @@ class FootprintWizardParameterManager:
     uNatural = 3
     uBool = 4
 
-    def AddParam(self, section, param, unit, default, hint = ''):
+    def AddParam(self, section, param, unit, default, hint=''):
         """
         Add a parameter with some properties.
 
@@ -67,13 +69,13 @@ class FootprintWizardParameterManager:
         elif unit == self.uNatural:
             val = 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
+            param = "*%s" % param  # star prefix for natural
 
         if section not in self.parameters:
             self.parameters[section] = {}
@@ -89,7 +91,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 +104,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 +127,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 +137,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 +154,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 +194,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 +208,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 +230,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 +278,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..38e282f 100644
--- a/pcbnew/scripting/plugins/PadArray.py
+++ b/pcbnew/scripting/plugins/PadArray.py
@@ -1,6 +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.
+#
+#
 
 import pcbnew
 
+
 class PadMaker:
     """
     Useful construction functions for common types of pads
@@ -9,7 +30,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 +43,7 @@ class PadMaker:
 
         return pad
 
-    def SMDPad(self, w, l, shape = pcbnew.PAD_RECT):
+    def SMDPad(self, w, l, shape=pcbnew.PAD_RECT):
         pad = pcbnew.D_PAD(self.module)
         pad.SetSize(pcbnew.wxSize(l, w))
 
@@ -34,58 +55,76 @@ 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;
+    def __init__(self, pinNames=None):
+        self.firstPadNum = 1
+        self.pinNames = pinNames
 
     def SetFirstPadInArray(self, fpNum):
-        self.firstPad = fpNum
+        self.firstPadNum = fpNum
 
     def AddPad(self, pad):
         self.pad.GetParent().Add(pad)
 
+
 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),
+                 pinNames=None, firstPad=None):
+        PadArray.__init__(self, pinNames)
         # 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.firstPad = firstPad
         self.nx = int(nx)
         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):
+
+            posX = pin1posX + (x * self.px)
+
             for y in range(self.ny):
-                posX = self.pin1Pos.x + (self.px * x)
-                posY = self.pin1Pos.y + (self.py * y)
+                posY = pin1posY + (self.py * y)
 
-                pos = pcbnew.wxPoint(posX, posY)
+                pos = dc.TransformPoint(posX, posY)
+
+                if (self.firstPad and x == 0 and y == 0):
+                    pad = self.firstPad
+                else:
+                    pad = self.pad
 
                 # create a new pad with same characteristics
                 pad = self.pad.Duplicate()
@@ -93,43 +132,22 @@ class PadGridArray(PadArray):
                 pad.SetPos0(pos)
                 pad.SetPosition(pos)
 
-                pad.SetPadName(str(self.NamingFunction(x,y)))
+                if self.pinNames is None:
+                    pad.SetPadName(str(self.NamingFunction(x, y)))
+                else:
+                    pad.SetPadName(self.pinNames)
 
                 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), pinNames=None, firstPad=None):
 
         if isVertical:
-            PadGridArray.__init__(self, pad, 1, n, 0, pitch, pin1Pos)
+            PadGridArray.__init__(self, pad, 1, n, 0, pitch, centre,
+                                  pinNames, firstPad)
         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,
+                                  pinNames, firstPad)
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/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..f32e51f
--- /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.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