Files
2021-11-24 17:00:32 +01:00

386 lines
16 KiB
Python

#!/usr/bin/env python
# Copyright 2017 Simon Kuppers https://github.com/skuep/
# Copyright 2019 Maurice https://github.com/easyw/
# original plugin https://github.com/skuep/kicad-plugins
# some source tips @
# https://github.com/MitjaNemec/Kicad_action_plugins
# https://github.com/jsreynaud/kicad-action-scripts
# GNU GENERAL PUBLIC LICENSE
# Version 3, 29 June 2007
#
# Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
# Everyone is permitted to copy and distribute verbatim copies
# of this license document, but changing it is not allowed.
#
# 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.
#
# 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 math
#import pyclipper
from bisect import bisect_left
import wx
import pcbnew
def verbose(object, *args, **kwargs):
global verboseFunc
verboseFunc(object, *args, **kwargs)
# Returns the slope of a line
def getLineSlope(line):
return math.atan2(line[0][1]-line[1][1], line[0][0]-line[1][0])
# Returns the length of a line
def getLineLength(line):
return math.hypot(line[0][0]-line[1][0], line[0][1]-line[1][1])
# Returns a sub path in a path with a path specification (startIdx, stopIdx)
def getSubPath(path, pathSpec):
listModulus = len(path)
if (pathSpec[1] < pathSpec[0]): pathSpec[1] += listModulus
return [path[i % listModulus] for i in range(pathSpec[0], pathSpec[1]+1)]
# Returns a list of subpaths with a list of path specifications
def getSubPaths(path, pathSpecList):
return [getSubPath(path, pathSpec) for pathSpec in pathSpecList if (pathSpec[0] != pathSpec[1])]
# Splits a path using a list of indices representing points on the path
def splitPathByPoints(path, splitList):
pathSpecList = [[splitList[item], splitList[item+1]] for item in range(0, len(splitList)-1)]
return getSubPaths(path, pathSpecList)
# Splits a path around a list of list of indices representing a subpath within the original path
def splitPathByPaths(path, splitList):
pathSpecList = [[splitList[item][-1], splitList[(item+1)%len(splitList)][0]] for item in range(0, len(splitList))]
return getSubPaths(path, pathSpecList)
# Return a cumulative distance vector representing the distance travelled along
# the path at each path vertex
def getPathCumDist(path):
cumDist = [0.0]
for vertexId in range(1, len(path)):
cumDist += [cumDist[-1] + getLineLength([path[vertexId], path[vertexId-1]])]
return cumDist
# Return a list of all vertex indices where the angle between
# the two lines connected to the vertex deviate from a straight
# path more by the tolerance angle in degrees
# This function is used to find bends that are larger than a certain angle
def getPathVertices(path, angleTolerance):
angleTolerance = angleTolerance * math.pi / 180
vertices = []
# Look through all vertices except start and end vertex
# Calculate by how much the lines before and after the vertex
# deviate from a straight path.
# If the deviation angle exceeds the specification, store it
for vertexIdx in range(1, len(path)-1):
prevSlope = getLineSlope([path[vertexIdx+1], path[vertexIdx]])
nextSlope = getLineSlope([path[vertexIdx-1], path[vertexIdx]])
deviationAngle = abs(prevSlope - nextSlope) - math.pi
if (abs(deviationAngle) > angleTolerance):
vertices += [vertexIdx]
return vertices
# Uses the cross product to check if a point is on a line defined by two other points
def isPointOnLine(point, line):
cross = (line[1][1] - point[1]) * (line[0][0] - point[0]) - (line[1][0] - point[0]) * (line[0][1] - point[1])
if ( ((line[0][0] <= point[0] <= line[1][0]) or (line[1][0] <= point[0] <= line[0][0]))
and ((line[0][1] <= point[1] <= line[1][1]) or (line[1][1] <= point[1] <= line[0][1]))
and (cross == 0) ):
return True
return False
# Returns a list of path indices touching any item in a list of points
def getPathsThroughPoints(path, pointList):
touchingPaths = []
for vertexIdx in range(0, len(path)):
fromIdx = vertexIdx
toIdx = (vertexIdx+1) % len(path)
# If a point in the pointList is located on this line, store the line
for point in pointList:
if isPointOnLine(point, [ path[fromIdx], path[toIdx] ]):
touchingPaths += [[fromIdx, toIdx]]
break
return touchingPaths
# A small linear interpolation class so we don't rely on scipy or numpy here
class LinearInterpolator(object):
def __init__(self, x_list, y_list):
self.x_list, self.y_list = x_list, y_list
intervals = zip(x_list, x_list[1:], y_list, y_list[1:])
self.slopes = [(y2 - y1)/(x2 - x1) for x1, x2, y1, y2 in intervals]
def __call__(self, x):
i = bisect_left(self.x_list, x) - 1
return self.y_list[i] + self.slopes[i] * (x - self.x_list[i])
# Interpolate a path with (x,y) vertices using a third parameter t
class PathInterpolator:
def __init__(self, t, path):
# Quick and dirty transpose path so we get two list with x and y coords
# And set up two separate interpolators for them
x = [vertex[0] for vertex in path]
y = [vertex[1] for vertex in path]
self.xInterp = LinearInterpolator(t, x)
self.yInterp = LinearInterpolator(t, y)
def __call__(self, t):
# Return interpolated coordinates on the original path
return [self.xInterp(t), self.yInterp(t)]
# A small pyclipper wrapper class to expand a line to a polygon with a given offset
def expandPathsToPolygons(pathList, offset):
import pyclipper
# Use PyclipperOffset to generate polygons that surround the original
# paths with a constant offset all around
co = pyclipper.PyclipperOffset()
for path in pathList: co.AddPath(path, pyclipper.JT_ROUND, pyclipper.ET_OPENROUND)
return co.Execute(offset)
# A small pyclipper wrapper to trim parts of a polygon using another polygon
def clipPolygonWithPolygons(path, clipPathList):
import pyclipper
pc = pyclipper.Pyclipper()
pc.AddPath(path, pyclipper.PT_SUBJECT, True)
for clipPath in clipPathList: pc.AddPath(clipPath, pyclipper.PT_CLIP, True)
return pc.Execute(pyclipper.CT_DIFFERENCE)
def unionPolygons(pathList):
import pyclipper
pc = pyclipper.Pyclipper()
for path in pathList: pc.AddPath(path, pyclipper.PT_SUBJECT, True)
return pc.Execute(pyclipper.CT_UNION, pyclipper.PFT_NONZERO)
def isPointInPolygon(point, path):
import pyclipper
return True if (pyclipper.PointInPolygon(point, path) == 1) else False
def getPathsInsidePolygon(pathList, polygon):
filteredPathList = []
for path in pathList:
allVerticesInside = True
for vertex in path:
if not isPointInPolygon(vertex, polygon):
allVerticesInside = False
break
if (allVerticesInside): filteredPathList += [path]
return filteredPathList
# Distribute Points along a path with equal spacing to each other
# When the path length is not evenly dividable by the minimumSpacing,
# the actual spacing will be larger, but still smaller than 2*minimumSpacing
# The function does not return the start and end vertex of the path
def distributeAlongPath(path, minimumSpacing):
# Get cumulated distance vector for the path
# and determine the number of points that can fit to the path
distList = getPathCumDist(path)
nPoints = int(math.floor(distList[-1] / minimumSpacing))
ptInterp = PathInterpolator(distList, path)
return [ptInterp(ptIdx * distList[-1]/nPoints) for ptIdx in range(1, nPoints)]
# Find the leaf vertices in a list of paths,
# additionally it calculates the slope of the line connected to the leaf vertex
def getLeafVertices(pathList):
allVertices = [vertex for path in pathList for vertex in path]
leafVertices = []
leafVertexSlopes = []
for path in pathList:
for vertexIdx in [0,-1]:
if (allVertices.count(path[vertexIdx]) == 1):
# vertex appears only once in entire path list, store away
# Get neighbour vertex and also calculate the slope
leafVertex = path[vertexIdx]
neighbourVertex = path[ [1,-2][vertexIdx] ]
leafVertices += [leafVertex]
leafVertexSlopes += [getLineSlope([neighbourVertex, leafVertex])]
return leafVertices, leafVertexSlopes
# Rotate and Translate a list of vertices using a given angle and offset
def transformVertices(vertexList, offset, angle):
return [ [ round(offset[0] + math.cos(angle) * vertex[0] - math.sin(angle) * vertex[1]),
round(offset[1] + math.sin(angle) * vertex[0] + math.cos(angle) * vertex[1]) ]
for vertex in vertexList]
# Trims a polygon flush around the given vertices
def trimFlushPolygonAtVertices(path, vertexList, vertexSlopes, radius):
const = 0.414
trimPoly = [ [0, -radius], [0, 0], [0, radius], [-const*radius, radius], [-radius, const*radius],
[-radius, -const*radius], [-const*radius, -radius] ]
trimPolys = [transformVertices(trimPoly, vertexPos, vertexSlope)
for vertexPos, vertexSlope in zip(vertexList, vertexSlopes)]
trimPolys = unionPolygons(trimPolys)
verbose(trimPolys, isPolygons=True)
return clipPolygonWithPolygons(path, trimPolys)
######################
def generateViaFence(pathList, viaOffset, viaPitch, vFunc = lambda *args,**kwargs:None):
global verboseFunc
verboseFunc = vFunc
viaPoints = []
# Remove zero length tracks
pathList = [path for path in pathList if getLineLength(path) > 0]
# Expand the paths given as a parameter into one or more polygons
# using the offset parameter
for offsetPoly in expandPathsToPolygons(pathList, viaOffset):
verbose([offsetPoly], isPolygons=True)
# Filter the input path to only include paths inside this polygon
# Find all leaf vertices and use them to trim the expanded polygon
# around the leaf vertices so that we get a flush, flat end
# These butt lines are then found using the leaf vertices
# and used to split open the polygon into multiple separate open
# paths that envelop the original path
localPathList = getPathsInsidePolygon(pathList, offsetPoly)
if len(localPathList) == 0: continue # This might happen with very bad input paths
leafVertexList, leafVertexAngles = getLeafVertices(localPathList)
offsetPoly = trimFlushPolygonAtVertices(offsetPoly, leafVertexList, leafVertexAngles, 1.1*viaOffset)[0]
buttLineIdxList = getPathsThroughPoints(offsetPoly, leafVertexList)
fencePaths = splitPathByPaths(offsetPoly, buttLineIdxList)
verbose([offsetPoly], isPolygons=True)
verbose([leafVertexList], isPoints=True)
verbose(fencePaths, isPaths=True)
# With the now separated open paths we perform via placement on each one of them
for fencePath in fencePaths:
# For a nice via fence placement, we identify vertices that differ from a straight
# line by more than 10 degrees so we find all non-arc edges
# We combine these points with the start and end point of the path and use
# them to place fixed vias on their positions
tolerance_degree = 10
fixPointIdxList = [0] + getPathVertices(fencePath, tolerance_degree) + [-1]
fixPointList = [fencePath[idx] for idx in fixPointIdxList]
verbose(fixPointList, isPoints=True)
viaPoints += fixPointList
# Then we autoplace vias between the fixed via locations by satisfying the
# minimum via pitch given by the user
for subPath in splitPathByPoints(fencePath, fixPointIdxList):
viaPoints += distributeAlongPath(subPath, viaPitch)
return viaPoints
def create_round_pts(sp,ep,cntr,rad,layer,width,Nn,N_SEGMENTS):
start_point = sp
end_point = ep
pos = sp
next_pos = ep
a1 = getAngleRadians(cntr,sp)
a2 = getAngleRadians(cntr,ep)
#wxLogDebug('a1:'+str(math.degrees(a1))+' a2:'+str(math.degrees(a2))+' a2-a1:'+str(math.degrees(a2-a1)),debug)
if (a2-a1) > 0 and abs(a2-a1) > math.radians(180):
deltaA = -(math.radians(360)-(a2-a1))/N_SEGMENTS
#wxLogDebug('deltaA reviewed:'+str(math.degrees(deltaA)),debug)
elif (a2-a1) < 0 and abs(a2-a1) > math.radians(180):
deltaA = (math.radians(360)-abs(a2-a1))/N_SEGMENTS
#wxLogDebug('deltaA reviewed2:'+str(math.degrees(deltaA)),debug)
else:
deltaA = (a2-a1)/N_SEGMENTS
delta=deltaA
#wxLogDebug('delta:'+str(math.degrees(deltaA))+' radius:'+str(ToMM(rad)),debug)
points = []
#import round_trk; import importlib; importlib.reload(round_trk)
for ii in range (N_SEGMENTS+1): #+1):
points.append(pos)
#t = create_Track(pos,pos)
prv_pos = pos
#pos = pos + fraction_delta
#posPolar = cmath.polar(pos)
#(rad) * cmath.exp(math.radians(deltaA)*1j) #cmath.rect(r, phi) : Return the complex number x with polar coordinates r and phi.
#pos = wxPoint(posPolar.real+sp.x,posPolar.imag+sp.y)
pos = rotatePoint(rad,a1,delta,cntr)
delta=delta+deltaA
#wxLogDebug("pos:"+str(ToUnits(prv_pos.x))+":"+str(ToUnits(prv_pos.y))+";"+str(ToUnits(pos.x))+":"+str(ToUnits(pos.y)),debug)
return points
#if 0:
# for i, p in enumerate(points):
# #if i < len (points)-1:
# if i < len (points)-2:
# t = create_Solder(pcb,p,points[i+1],layer,width,Nn,True,pcbGroup) #adding ts code to segments
# t = create_Solder(pcb,points[-2],ep,layer,width,Nn,True,pcbGroup) #avoiding rounding on last segment
#
# Function to find the circle on
# which the given three points lie
def getCircleCenterRadius(sp,ep,ip):
# findCircle(x1, y1, x2, y2, x3, y3) :
# NB add always set float even if values are pcb internal Units!!!
x1 = float(sp.x); y1 = float(sp.y)
x2 = float(ep.x); y2 = float(ep.y)
x3 = float(ip.x); y3 = float(ip.y)
x12 = x1 - x2;
x13 = x1 - x3;
y12 = y1 - y2;
y13 = y1 - y3;
y31 = y3 - y1;
y21 = y2 - y1;
x31 = x3 - x1;
x21 = x2 - x1;
# x1^2 - x3^2
sx13 = math.pow(x1, 2) - math.pow(x3, 2);
# y1^2 - y3^2
sy13 = math.pow(y1, 2) - math.pow(y3, 2);
sx21 = math.pow(x2, 2) - math.pow(x1, 2);
sy21 = math.pow(y2, 2) - math.pow(y1, 2);
f = (((sx13) * (x12) + (sy13) *
(x12) + (sx21) * (x13) +
(sy21) * (x13)) // (2 *
((y31) * (x12) - (y21) * (x13))));
g = (((sx13) * (y12) + (sy13) * (y12) +
(sx21) * (y13) + (sy21) * (y13)) //
(2 * ((x31) * (y12) - (x21) * (y13))));
c = (-math.pow(x1, 2) - math.pow(y1, 2) - 2 * g * x1 - 2 * f * y1);
# eqn of circle be x^2 + y^2 + 2*g*x + 2*f*y + c = 0
# where centre is (h = -g, k = -f) and
# radius r as r^2 = h^2 + k^2 - c
h = -g;
k = -f;
sqr_of_r = h * h + k * k - c;
# r is the radius
r = round(math.sqrt(sqr_of_r), 5);
Cx = h
Cy = k
radius = r
return wx.Point(Cx,Cy), radius
#
def getAngleRadians(p1,p2):
#return math.degrees(math.atan2((p1.y-p2.y),(p1.x-p2.x)))
return (math.atan2((p1.y-p2.y),(p1.x-p2.x)))
#
def rotatePoint(r,sa,da,c):
# sa, da in radians
x = c.x - math.cos(sa+da) * r
y = c.y - math.sin(sa+da) * r
return wx.Point(x,y)