386 lines
16 KiB
Python
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) |