Yet another Python teardrop script. Adds and deletes teardrops to a PCB. V0.3.3



The reason you’re getting corrupt output files is because you need to call z.CloseLastContour() after z.AppendCorner§

I see this as a bug in kicad; AppendCorner should call it for you. (I’ll suggest it on the developer maillist)

Sadly, the only way to know this is to step through the c code in the debugger.



Thank you very much!

I will correct it as soon as possible. This will be a huge enhancement!


I have filed a bug on this:

I probably should have done this when I first noticed the problem some weeks ago.


Thanks to your debug work, the non closing zone bug is now fixed.

I released a 0.3.1 version that does not require to patch the pcb file at all.
The scripts is updated on my github repo:

Thanks again !

Kicad Some Update Need

It’s not work on nightybuild,PAD_STANDARD should replaced by PAD_ATTRIB_STANDARD。
and I made some change to code:

in def __GetAllPads(board, filters=[]):
drill = min(pad.GetSize())

It’s better for round pad

in def SetTeardrops(hpercent=30, vpercent=70, segs=10)
if (track.GetLength() < via[1]) or (track.GetWidth() >= via[1] * vpercent / 100):

ingore track width bigger than via dim



I will include your changes in the script.
Thank you for your work !


Changes applied in lasted release V0.3.2.
Thanks again!


That great!
I found some minor issue like this:

adjust some parameter affer:


here is the code:

added a funtion to caculate teardrop length depend on via size, track width and hpercent, select the best value

def TeardropLength(track, via, hpercent):

n = min(track.GetLength(), (via[1] - track.GetWidth()) * 1.2071)
n = max(via[1]*(0.5+hpercent/200.0), n)
return n

in def __ComputePoints(track, via, hpercent, vpercent, segs):

if sqrt(d.x * d.x + d.y * d.y) < via[1]:

change to:

if sqrt(d.x * d.x + d.y * d.y) < via[1]/2:

n = radius*(1+hpercent/100.0)

change to:

n = TeardropLength(track, via, hpercent)

in def SetTeardrops(hpercent=30, vpercent=70, segs=10):

if (track.GetLength() < via[1])if (track.GetLength() < via[1])

change to:

if (track.GetLength() < TeardropLength(track, via, hpercent)) or (track.GetWidth() >= via[1] * vpercent / 100):


It would also be nice to apply this sort of technique when necking from one trace width to another.


Here is the code for nighty build,tested on r7987

#!/usr/bin/env python

# Teardrop for pcbnew using filled zones
# (c) Niluje 2016
# Based on Teardrops for PCBNEW by svofski, 2014

import os, sys
import argparse
import fileinput
from math import cos, acos, sin, asin, tan, atan2, degrees
from pcbnew import *

__version__ = "0.3.1"


def __File2List(filename):
        f = open(filename, 'r')
        listfile = [ l.rstrip() for l in f ]
    except IOError:
        return []
    return listfile

def __List2File(thelist, filename):
    f = open(filename, 'w')
    for l in thelist:

def __GetAllVias(board):
    """Just retreive all via from the given board"""
    vias = []
    vias_selected =[]
    for item in board.GetTracks():
        if type(item) == VIA:
            pos = item.GetPosition()
            width = item.GetWidth()
            drill = item.GetDrillValue()
            vias.append((pos, width, drill))
            if item.IsSelected():
                vias_selected.append((pos, width, drill))
    return vias, vias_selected

def __GetAllPads(board, filters=[]):
    """Just retreive all pads from the given board"""
    pads = []
    pads_selected = []
    for i in xrange(board.GetPadCount()):
        pad = board.GetPad(i)
        if pad.GetAttribute() in filters:
            pos = pad.GetPosition()
            #drill = pad.GetDrillSize().x + FromUnits(0.2 * 2)
            drill = min(pad.GetSize())
            pads.append((pos, drill ))
            if pad.IsSelected():
                pads_selected.append((pos, drill))
    return pads, pads_selected

def __Zone(viafile, board, points, track):
    """Add a zone to the board"""
    z = ZONE_CONTAINER(board)

    #Add zone properties
    z.SetMinThickness(25400) #The minimum
    z.SetPadConnection(2) # 2 -> solid

    for p in points:
        z.AppendCorner(wxPoint(p.x, p.y))


    #Save zone properties
    vialine = track.GetLayerName() + ':' + ''.join(line)
    if not vialine in viafile:
        return z

    return None

def __Bezier(p1, p2, p3, n=20.0):
    n = float(n)
    pts = []
    for i in range(int(n)+1):
        t = i/n
        a = (1.0 - t) ** 2
        b = 2.0*t*(1.0-t)
        c = t**2

        x = int(a * p1[0] + b * p2[0] + c * p3[0])
        y = int(a * p1[1] + b * p2[1] + c * p3[1])
    return pts

def __ComputeCurved(vpercent, w, vec, via, pts, segs):
    """Compute the curves part points"""

    radius = via[1]/2.0

    #Compute the bezier middle points
    req_angle = asin(vpercent/100.0);
    oppside = tan(req_angle)*(radius-(w/sin(req_angle)))
    length = sqrt(radius*radius + oppside*oppside)
    d = req_angle - acos(radius/length)
    vecBC = [vec[0]*cos(d)+vec[1]*sin(d) , -vec[0]*sin(d)+vec[1]*cos(d)]
    pointBC = via[0] + wxPoint(int(vecBC[0] * length), int(vecBC[1] * length))
    d = -d
    vecAE = [vec[0]*cos(d)+vec[1]*sin(d) , -vec[0]*sin(d)+vec[1]*cos(d)]
    pointAE = via[0] + wxPoint(int(vecAE[0] * length), int(vecAE[1] * length))

    curve1 = __Bezier(pts[1], pointBC, pts[2], n=segs)
    curve2 = __Bezier(pts[4], pointAE, pts[0], n=segs)

    return curve1 + [pts[3]] + curve2

def TeardropLength(track, via, hpercent):

    n = min(track.GetLength(), (via[1] - track.GetWidth()) * 1.2071)
    n = max(via[1]*(0.5+hpercent/200.0), n)
    return n

def __ComputePoints(track, via, hpercent, vpercent, segs):
    """Compute all teardrop points"""
    start = track.GetStart()
    end = track.GetEnd()
    if (segs>2) and (vpercent>70.0):
        #If curved via are selected, max angle is 45 degres --> 70%
        vpercent = 70.0

    # ensure that start is at the via/pad end
    d = end - via[0]
    if sqrt(d.x * d.x + d.y * d.y) < via[1]/2:
        start, end = end, start

    # get normalized track vector
    # it will be used a base vector pointing in the track direction
    pt = end - start
    norm = sqrt(pt.x * pt.x + pt.y * pt.y)
    vec = [t / norm for t in pt]

    # find point on the track, sharp end of the teardrop
    w = track.GetWidth()/2
    radius = via[1]/2
    n = TeardropLength(track, via, hpercent)
    dist = sqrt(n*n + w*w)
    d = atan2(w, n)
    vecB = [vec[0]*cos(d)+vec[1]*sin(d) , -vec[0]*sin(d)+vec[1]*cos(d)]
    pointB = start + wxPoint(int(vecB[0] * dist), int(vecB[1] * dist))
    vecA = [vec[0]*cos(-d)+vec[1]*sin(-d) , -vec[0]*sin(-d)+vec[1]*cos(-d)]
    pointA = start + wxPoint(int(vecA[0] * dist), int(vecA[1] * dist))

    # via side points
    radius = via[1] / 2
    d = asin(vpercent/100.0);
    vecC = [vec[0]*cos(d)+vec[1]*sin(d) , -vec[0]*sin(d)+vec[1]*cos(d)]
    d = asin(-vpercent/100.0);
    vecE = [vec[0]*cos(d)+vec[1]*sin(d) , -vec[0]*sin(d)+vec[1]*cos(d)]
    pointC = via[0] + wxPoint(int(vecC[0] * radius), int(vecC[1] * radius))
    pointE = via[0] + wxPoint(int(vecE[0] * radius), int(vecE[1] * radius))

    # Introduce a last point in order to cover the via centre.
    # If not, the zone won't be filled
    vecD = [-vec[0], -vec[1]]
    radius = (via[1]/2)*0.5 #50% of via radius is enough to include
    pointD = via[0] + wxPoint(int(vecD[0] * radius), int(vecD[1] * radius))

    pts = [pointA, pointB, pointC, pointD, pointE]
    if segs > 2:
        pts = __ComputeCurved(vpercent, w, vec, via, pts, segs)

    return pts

def SetTeardrops(hpercent=30, vpercent=70, segs=5):
    """Set teardrops on a teardrop free board"""

    pcb = GetBoard()
    td_filename = pcb.GetFileName() + '_td'

    vias = __GetAllVias(pcb)[0] + __GetAllPads(pcb, [PAD_ATTRIB_STANDARD])[0]
    vias_selected = __GetAllVias(pcb)[1] + __GetAllPads(pcb, [PAD_ATTRIB_STANDARD])[1]
    viasfile = __File2List(td_filename)
    if len(vias_selected) > 0:
        print('Using selected pads/vias')
        vias = vias_selected
        # If a teardrop file is present AND no pad are selected,
        # remove all teardrops.
        if len(viasfile) > 0:

    count = 0
    for track in pcb.GetTracks():
        if type(track) == TRACK:
            for via in vias:
                if track.IsPointOnEnds(via[0], via[1]/2):
                    if (track.GetLength() < TeardropLength(track, via, hpercent)) or (track.GetWidth() >= via[1] * vpercent / 100):
                    coor = __ComputePoints(track, via, hpercent, vpercent, segs)
                    the_zone = __Zone(viasfile, pcb, coor, track)
                    if the_zone:
                        count = count + 1

    if len(viasfile) > 0:
        __List2File(viasfile, td_filename)
        #Just remove the file
        except IOError:
            #There was no file at startup and no teardrop to add

    print('{0} teardrops inserted'.format(count))

def __RemoveTeardropsInList(pcb, tdlist):
    """Remove all teardrops mentioned in the list if available in current PCB.
       Returns number of deleted pads"""
    for line in tdlist:
        for z in [ pcb.GetArea(i) for i in range(pcb.GetAreaCount()) ]:
            corners = [str(z.GetCornerPosition(i)) for i in range(z.GetNumCorners())]
            if line.rstrip() == z.GetLayerName() + ':' + ''.join(corners):

    count = len(to_remove)
    for tbr in to_remove:
    #Remove the td file
        os.remove(pcb.GetFileName() + '_td')
    except OSError:

    return count

def __RemoveSelectedTeardrops(pcb, tdlist, sel):
    """Remove only the selected teardrops if mentionned in teardrops file.
       Also update the teardrops file"""
    print('Not implemented yet')
    return 0

def RmTeardrops():
    """Remove teardrops according to teardrops definition file"""

    pcb = GetBoard()
    td_filename = pcb.GetFileName() + '_td'
    viasfile = __File2List(td_filename)
    vias_selected = __GetAllVias(pcb)[1] + __GetAllPads(pcb, [PAD_ATTRIB_STANDARD])[1]

    if len(vias_selected) > 0:
        #Only delete selected teardrops. We need to recompute the via structure
        #in order to found it in the viasfile and delete it
        count = __RemoveSelectedTeardrops(pcb, viasfile, vias_selected)
        #Delete every teardrops mentionned in the teardrops file
        count = __RemoveTeardropsInList(pcb, viasfile)

    print('{0} teardrops removed'.format(count))

def RmAllZones():
    pcb = GetBoard()
    for z in [ pcb.GetArea(i) for i in range(pcb.GetAreaCount()) ]:
    print('{0} total zone removed'.format(pcb.GetAreaCount()))


This function just for debug. because of the nighty build changed some python API, the RmTeardrops() no longer working. So I add this function to remove all the zone in the board.


Thank you very much for this contribution.
This is a really great improvement.
I will include this in the trunk as soon as I can.


You’re right. This is usually a feature that comes with teardrops.
I’ll make some tests in order to see if it possible to include it in this script.


Hi Can someone say how to Run this Python Code in Kicad…
Please Tell step by step i am New to kicad/
Thanks advance…



You can found these instructions in the delivered with the git repository:

I will try to add pcbnew menu entries to help with this. But I can’t say if this will be a success.


Sorry for troubling
Can you Explain me for the Above teardrop script Running Steps, one by one
thanks advance


copy to kicad script plugin folder

  1. open pcbnew
  2. click python term icon on the toolbar
  3. input command:
    import td


Hi yut,

Sorry for this late answer.
I have (at last) implemented your modifications to the github repo.
I also included a workaround to the rmteardrops bug you found.

The new version is 0.3.3 and is available, as usual, on my github repo.

Thanks again for you work

PS: I am not able to edit this thread title. I used to edit the first message to change the title but seems it is not working anymore. Does someone knows how to do it ?