Python scripting, classes and functions

Here, I document some of the peculiarities of python scripting I notice in KiCad version 4.06.
I’ll add to this thread as I discover more.

First up:

  1. pcbnew.Iu2Mils and pcbnew.Iu2DMils do not seem to work.
    Workaround: Using pcbnew.IU_PER_MILS and IU_PER_MM (not used: IU_PER_DECIMILS)
  2. I cannot seem to figure out LSET structure.
    Workaround: iterate over objects using IsOnLayer()
  3. Bounding box on text objects (one or both of TEXTE_MODULE and TEXTE_PCB)
    returns the upper left corner and the width/height. Not the Center, width, height as I expected.
    This is still true with Justification left/center/right, and rotation 0, 90, 180, 270.
    I have not dug into how non-orthogonal rotation affects the bounding box.
1 Like
  1. pcbnew.Iu2Mils and pcbnew.Iu2DMils do not seem to work.

I don’t see a function named Iu2DMils in the library. And my copy of Iu2Mils works as expected (see below). Can you say what version/OS you are using?

Python 2.7.13 (default, Jan 19 2017, 14:48:08) 
[GCC 6.3.0 20170118] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import pcbnew
>>> print pcbnew.IU_PER_MILS * pcbnew.Iu2Mils(10)
10.0
  1. I cannot seem to figure out LSET structure.

Can you say what you would like to do with LSET? Are you trying to iterate over all tracks on an LSET?

  1. Bounding box on text objects (one or both of TEXTE_MODULE and TEXTE_PCB)

The GetBoundingBox() method returns a rectangle. If you want the center of the rectangle, you call GetBoundingBox().GetCenter() like this:

>>> import pcbnew
>>> board = pcbnew.GetBoard()
>>> text = board.GetModules().begin().Reference()
>>> print text.GetBoundingBox().GetCenter()
(10000000, 43667500)
>>> print text.GetBoundingBox().GetWidth()
4435714
>>> print text.GetBoundingBox().GetHeight()
1650000
>>> text.Rotate(text.GetBoundingBox().GetCenter(),900)
>>> print text.GetBoundingBox().GetCenter()
(10000000, 43667500)
>>> print text.GetBoundingBox().GetWidth()
1650000
>>> print text.GetBoundingBox().GetHeight()
4435714

Thank you Seth_h,

PyCrust 0.9.8 - KiCAD Python Shell
Python 2.7.13 (default, Jan 17 2017, 13:56:44)  [GCC 6.3.0 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
import pcbnew
print pcbnew.IU_PER_MILS * pcbnew.Iu2Mils(10)
0.0

I’d like to iterate over an LSET of a series of objects to create a list of objects by layer.

BoundingBox returning a rectangle is fine, but returning the point as upper left is a bizarre choice. The center would be slightly more useful and unambiguous. But I guess that’s a fine design choice, if documented.

I’ll check if tracks and pads report a similar rect as BoundingBox.

Another peculiarity::

Via attribute is GetDrill() and GetDrillValue()
While Pad attribute is GetDrillSize()

These attributes variously return either one (radius?) or two (X and Y?) values.

What version of Kicad are you running? And Windows version?

There are a few ways to iterate over an LSET. I find that the easiest is to extract an LSEQ from the LSET. You can do this using the Seq() or SeqStackupBottom2Top() functions. The LSEQ structure will allow you to iterate over the layers because it has a defined sequence as opposed to the LSET that does not.

For VIAs, you should use GetDrillValue(). This will correctly return either the uViaDrill diameter or the ViaDrill diameter depending on the VIA type. GetDrill() will only return the local drill setting for the VIA, not the actual value. In other words, if your via is set to default, it will return 0.

For PADs, the hole might actually be a slot. Therefore, it needs to be a wxSize variable with x and y parameters.

KiCAD version 4.0.6 on Windows 7

Thanks for the pointers to LSET.

And the invaluable definition of GetDrill(). That could have been a difficult difference to determine!

OK, that makes sense. Kicad 4 has a bug where Iu2Mils returns an integer. So, everything gets rounded to 0 until you get over the 1e6 internal unit scaling. You can grab the nightly build to fix this. And the weird “decimils” (DMils) unit conversion is also removed in the newer versions.

Awesome. Thx! Right now, I feel safer on the stable versions. When I get further into development (or have a major reason to) I’ll figure out how to build KiCAD, and I’ll start grabbing the nightlies periodically.

Another peculiar scripting issue:

When getting the corners of the bounding boxes for pads and text, it’s a little strange. I’m not sure this is definitive, but seems to work for my layout. I haven’t specifically tried all different combinations of rotated module (footprint) and rotated text.

This procedure works for free text (i.e. at the top level) and for text/pads within modules. The idea here is to get a bounding box (which is always reported at orientation 0) and to get a rotation to apply to the bounding box. Applying the rotation to the bounding box gets the final region on the board.

Pad (only orthogonal tested): always take the bounding box. It is oriented correctly regardless of footprint orientation. That is, rotation applied is zero.
I would expect that a non-orthogonal rotation applied to either pad or footprint would require different code.

Text:
bounding_box = textobject.GetTextBox()
textobject.GetOrientation() + textobject.GetParent().GetOrientation()

If the textobject is free text (i.e. it’s at the top level instead of in a footprint), the parent will not have a GetOrientation attribute. Here’s the code:

    try:
        m=object.GetParent().Cast_to_MODULE()
        parentorientation = m.GetOrientation()
    except:
        parentorientation = 0.0
    orientation = object.GetOrientation()
    return self.GetCornersRotatedRect(
        object.GetTextBox().getWxRect(),
        object.GetCenter(),
        orientation+parentorientation)

And the GetCornersRotatedRect function:

def GetCornersRotatedRect(self,rect,center,orientation):
    # rect is entered as a KiCAD boundingbox in a wxRect format (indexable)
    # rect format is (upper left x, upper left y, width, height)
    # orientation for this algorithm is -(kicad orientation).
    # This is because algorithm orientation is in the opposite direction
    # of kicad orientation.
    # (note that orientation is returned as tenths of a degree,
    #  so this also converts to floating point degrees)
    orientation = -orientation/10.0
    
    # the center is the rotation point
    ox,oy = center
    
    rpoints = []
    #initial points
    for px,py in (
        (rect[0]        ,rect[1]),
        (rect[0]+rect[2],rect[1]),
        (rect[0]+rect[2],rect[1]+rect[3]),
        (rect[0]        ,rect[1]+rect[3]),
        ):
        
        # rotate point around center
        cos= math.cos(math.radians(orientation))
        sin= math.sin(math.radians(orientation))
        pox = px-ox
        poy = py-oy
        
        nx = cos * (pox) - sin * (poy) + ox
        ny = sin * (pox) + cos * (poy) + oy
        
        rpoints.append(pcbnew.wxPoint(int(nx),int(ny)))

    # add the start point to form a closed polygon
    rpoints.append(rpoints[0]) 
    
    return rpoints

Are you trying to find the minimum rectangle of any orientation that encompasses an object?

Yes I the code above gets the minimum enclosing rectangle (well, really just the properly rotated and sized Bounding Box–or more precisely: the TextBox) that surrounds the text. The Bounding Box does take rotation into account, but reports the minimum enclosing orthogonal rectangle.

I’ve finally gotten the correct orientation on text strokes for text within modules.

The code above seems to get the correct Text Box orientation. But it is necessary to use TransormTextShapeToSegmentList() to get the individual DRAWSEGMENT objects (line segments) that make up the text strokes.

I’ve finally decoded how to get the properly rotated segment list. The difficulty is that it is different whether you are getting the text from TEXTE_PCB and TEXTE_MODULE. This seems an odd design choice, but this is what I have figured out:

# Excerpt of python code for pcbnew that creates line
# segments ('DRAWSEGMENT') from the individual strokes of text.
#
# 'object' here is a text object of type TEXTE_MODULE or TEXTE_PCB
#
# The following 'try' differentiates TEXTE_PCB from TEXTE_MODULE
# There is probably a more efficient way to determine the type :-)
# perhaps using if type(object) == pcbnew.TEXTE_MODULE:

# TEXTE_MODULE: oddly, the combination of draw rotation and 
# orientation is what's needed to determine the correct
# segments transformation only for TEXTE_MODULE object.
try:
    orientation = object.GetDrawRotation() - object.GetOrientation() # ccw from x-axis
except:
    # TEXTE_PCB:
    # Already returns segments that are rotated for display.
    orientation = 0

self.DrawVector(self.GetRotatedVector(vector,center,orientation),
            layer=pcbnew.Eco1_User, #USER_draw_outlines_layer,
            thickness=USER_draw_stroke_thickness)


def DrawVector(self,vector,layer=pcbnew.Eco2_User,thickness=0.015):
    '''wxPoint_vector'''
    #print type(vector)
    for i in range(0,len(vector)-1,2):
        self.DrawSeg(
            vector[i][0],
            vector[i][1],
            vector[i+1][0],
            vector[i+1][1],
            layer=layer,
            thickness=thickness)
def DrawSeg(self,x1,y1,x2,y2,layer=pcbnew.Dwgs_User,thickness=0.15):
    b=self._board or pcbnew.GetBoard()
    ds=pcbnew.DRAWSEGMENT(b)
    b.Add(ds)
    ds.SetStart(pcbnew.wxPoint(x1,y1))
    ds.SetEnd(pcbnew.wxPoint(x2,y2))
    ds.SetLayer(layer)#B.SilkS"])
    ds.SetWidth(int(thickness*pcbnew.IU_PER_MM))

    
def GetRotatedVector(self,vector,center,orientation):
    # return immediately if there's nothing to rotate
    if len(vector) == 0:
        return vector

    # the center is the rotation point
    # common equations for all points in this vector
    ox,oy = center
    orientation = -orientation/10.0
    cos= math.cos(math.radians(orientation))
    sin= math.sin(math.radians(orientation))
    
    rpoints = pcbnew.wxPoint_Vector(0)

    for v in vector:
        px=v[0]; py=v[1]
        # rotate point around center
        pox = px-ox
        poy = py-oy
        
        nx = cos * (pox) - sin * (poy) + ox
        ny = sin * (pox) + cos * (poy) + oy
        
        rpoints.append(pcbnew.wxPoint(int(nx),int(ny)))

    # add the start point to form a closed polygon
    rpoints.append(rpoints[0]) 
    
    return rpoints

why kicad 6.0.9

pcbnew.Iu2Mils(10) always return 0.0 ?