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

Curved teardrops are now available.
The script is still available on my github page:

The script behaviour is identical to the previous version (0.2.0).
Curved style is used by default. The number of segments per curved is controlled by the parameter “segs” of the setTearDrops function (default is 10).
If segs is set to 2, straight lines are used.

I hope this will help.

Notice: I am not very good in geometry and the only solution I found is to use a bezier curve in order to keep the teardrop tangent to the track and the via. It works but it is quite expensive in processing time. If someone has a better method, please let me know.

The script version is now 0.3.0 (the thread title is now modified)

4 Likes

HI
thankyou for your help, I get one error:
File “C:\Program Files\KiCad\lib\python2.7\site-packages\pcbnew.py”, line 31, in
import _pcbnew
ImportError: DLL load failed: %1 不是有效的 Win32 应用程序。

Win10 64bit
python2.7.4 64bit

@Niluje

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.

Miles

2 Likes

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 !

3 Likes

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 !

1 Like

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):
1 Like

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

1 Like

Here is the code for nighty build,tested on r7987

#!/usr/bin/env python

# Teardrop for pcbnew using filled zones
# (c) Niluje 2016 thewireddoesntexist.org
#
# Based on Teardrops for PCBNEW by svofski, 2014 http://sensi.org/~svo

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

__version__ = "0.3.1"

ToUnits=ToMM
FromUnits=FromMM

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

def __List2File(thelist, filename):
    f = open(filename, 'w')
    for l in thelist:
        f.write(l+'\n')
    f.close()

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.SetLayer(track.GetLayer())
    z.SetNetCode(track.GetNetCode())
    z.SetZoneClearance(track.GetClearance())
    z.SetMinThickness(25400) #The minimum
    z.SetPadConnection(2) # 2 -> solid
    z.SetIsFilled(True)

    line=[]
    for p in points:
        z.AppendCorner(wxPoint(p.x, p.y))
        line.append(str(p))
    #z.Outline().CloseLastContour()

    line.sort()
    z.BuildFilledSolidAreasPolygons(board)

    #Save zone properties
    vialine = track.GetLayerName() + ':' + ''.join(line)
    if not vialine in viafile:
        viafile.append(vialine)
        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])
        pts.append(wxPoint(x,y))
    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
    else:
        # If a teardrop file is present AND no pad are selected,
        # remove all teardrops.
        if len(viasfile) > 0:
            RmTeardrops()

    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):
                            continue
                    coor = __ComputePoints(track, via, hpercent, vpercent, segs)
                    the_zone = __Zone(viasfile, pcb, coor, track)
                    if the_zone:
                        pcb.Add(the_zone)
                        count = count + 1

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

    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"""
    to_remove=[]
    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())]
            corners.sort()
            if line.rstrip() == z.GetLayerName() + ':' + ''.join(corners):
                to_remove.append(z)

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

    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)
    else:
        #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()) ]:
        pcb.Remove(z)
    
    print('{0} total zone removed'.format(pcb.GetAreaCount()))
1 Like

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.

1 Like

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…

Hi,

You can found these instructions in the readme.md 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.

1 Like

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