def fillet(self, point, radius): """ Add a fillet to the substrate at given point. If the point does not lie on an inner corner or the surrounding geometry does not allow for fillet, does nothing. Return true if the fillet was created, else otherwise """ if radius == 0: return LEN_LIMIT = 40 # Should be roughly equal to half the circular segments, # may prevent from finding a solution cut1, cut2 = cutOutline(point, self.substrates.boundary, LEN_LIMIT, tolerance=pcbnew.FromMM(0.02)) if not cut1 or not cut2: # The point does not intersect any outline return False offset1 = cut1.parallel_offset(radius, 'right', join_style=2) offset2 = cut2.parallel_offset(radius, 'right', join_style=2) filletCenter = extractPoint(offset1.intersection(offset2)) if not filletCenter: return False a, _ = shapely.ops.nearest_points(cut1, filletCenter) b, _ = shapely.ops.nearest_points(cut2, filletCenter) if not a or not b: return False patch = Polygon([a, b, point]).buffer(pcbnew.FromMM(0.001)).difference( filletCenter.buffer(radius)) self.union(patch) return True
def splitLine(linestring, point, tolerance=pcbnew.FromMM(0.01)): splitted = split(linestring, point.buffer(tolerance, resolution=1)) if len(splitted) != 3: print(splitted) raise RuntimeError("Expected 3 segments in line spitting") p1 = LineString(list(splitted[0].coords) + [point]) p2 = LineString([point] + list(splitted[2].coords)) return shapely.geometry.collection.GeometryCollection([p1, p2])
def tab(self, origin, direction, width, partitionLine=None, maxHeight=pcbnew.FromMM(50)): """ Create a tab for the substrate. The tab starts at the specified origin (2D point) and tries to penetrate existing substrate in direction (a 2D vector). The tab is constructed with given width. If the substrate is not penetrated within maxHeight, exception is raised. Returns a pair tab and cut outline. Add the tab it via uion - batch adding of geometry is more efficient. """ origin = np.array(origin) direction = normalize(direction) sideOriginA = origin + makePerpendicular(direction) * width / 2 sideOriginB = origin - makePerpendicular(direction) * width / 2 boundary = self.substrates.boundary splitPointA = closestIntersectionPoint(sideOriginA, direction, boundary, maxHeight) splitPointB = closestIntersectionPoint(sideOriginB, direction, boundary, maxHeight) if isinstance(boundary, MultiLineString): shiftedOutline = cutOutline(splitPointB, boundary) tabFace = splitLine(shiftedOutline, splitPointA)[0] tab = Polygon(list(tabFace.coords) + [sideOriginA, sideOriginB]) return tab, tabFace else: tabFace = biteBoundary(boundary, splitPointB, splitPointA) if partitionLine is None: # There is nothing else to do, return the tab tab = Polygon(list(tabFace.coords) + [sideOriginA, sideOriginB]) return tab, tabFace # Span the tab towwards the partition line # There might be multiple geometries in the partition line, so try them # individually. direction = -direction for p in listGeometries(partitionLine): try: partitionSplitPointA = closestIntersectionPoint(splitPointA.coords[0], direction, p, maxHeight) partitionSplitPointB = closestIntersectionPoint(splitPointB.coords[0], direction, p, maxHeight) except NoIntersectionError: # We cannot span towards the partition line continue if isLinestringCyclic(p): candidates = [(partitionSplitPointA, partitionSplitPointB)] else: candidates = [(partitionSplitPointA, partitionSplitPointB), (partitionSplitPointB, partitionSplitPointA)] for i, (spa, spb) in enumerate(candidates): partitionFace = biteBoundary(p, spa, spb) if partitionFace is None: continue partitionFaceCoord = list(partitionFace.coords) if i == 1: partitionFaceCoord = partitionFaceCoord[::-1] tab = Polygon(list(tabFace.coords) + partitionFaceCoord) return tab, tabFace return None, None
def approximateArc(arc, endWith): """ Take DRAWINGITEM and approximate it using lines """ SEGMENTS_PER_FULL = 4 * 16 # To Be consistent with default shapely settings startAngle = arc.GetArcAngleStart() / 10 if arc.GetShape() == STROKE_T.S_CIRCLE: endAngle = startAngle + 360 segments = SEGMENTS_PER_FULL else: endAngle = startAngle + arc.GetAngle() / 10 segments = abs(int((endAngle - startAngle) * SEGMENTS_PER_FULL // 360)) theta = np.radians(np.linspace(startAngle, endAngle, segments)) x = arc.GetCenter()[0] + arc.GetRadius() * np.cos(theta) y = arc.GetCenter()[1] + arc.GetRadius() * np.sin(theta) outline = list(np.column_stack([x, y])) last = outline[-1][0], outline[-1][1] if (not np.isclose(last[0], endWith[0], atol=pcbnew.FromMM(0.001)) or not np.isclose(last[1], endWith[1], atol=pcbnew.FromMM(0.001))): outline.reverse() return outline
def _serializeRing(self, ring): coords = list(ring.simplify(pcbnew.FromMM(0.001)).coords) segments = [] # ToDo: Reconstruct arcs if coords[0] != coords[-1]: raise RuntimeError("Ring is incomplete") for a, b in zip(coords, coords[1:]): segment = pcbnew.PCB_SHAPE() segment.SetShape(STROKE_T.S_SEGMENT) segment.SetLayer(Layer.Edge_Cuts) segment.SetStart(roundPoint(a)) segment.SetEnd(roundPoint(b)) segments.append(segment) return segments
def tab(self, origin, direction, width, maxHeight=pcbnew.FromMM(50)): """ Create a tab for the substrate. The tab starts at the specified origin (2D point) and tries to penetrate existing substrate in direction (a 2D vector). The tab is constructed with given width. If the substrate is not penetrated within maxHeight, exception is raised. Returns a pair tab and cut outline. Add the tab it via uion - batch adding of geometry is more efficient. origin = np.array(origin) direction = normalize(direction) sideOriginA = origin + makePerpendicular(direction) * width / 2 sideOriginB = origin - makePerpendicular(direction) * width / 2 boundary = self.substrates.boundary splitPointA = closestIntersectionPoint(sideOriginA, direction, boundary, maxHeight) splitPointB = closestIntersectionPoint(sideOriginB, direction, boundary, maxHeight) shiftedOutline = cutOutline(splitPointB, boundary) tabFace = splitLine(shiftedOutline, splitPointA)[0] tab = Polygon(list(tabFace.coords) + [sideOriginA, sideOriginB]) return tab, tabFace """ origin = np.array(origin) direction = normalize(direction) sideOriginA = origin + makePerpendicular(direction) * width / 2 sideOriginB = origin - makePerpendicular(direction) * width / 2 boundary = self.substrates.boundary splitPointA1 = closestIntersectionPoint(np.array(sideOriginA), direction, boundary, maxHeight) splitPointB1 = closestIntersectionPoint(np.array(sideOriginB), direction, boundary, maxHeight) splitPointA2 = closestIntersectionPoint(np.array(splitPointA1), direction, boundary, maxHeight) splitPointB2 = closestIntersectionPoint(np.array(splitPointB1), direction, boundary, maxHeight) shiftedOutline = cutOutline(splitPointB1, boundary) tabFace1 = splitLine(shiftedOutline, splitPointA1)[0] shiftedOutline = cutOutline(splitPointB2, boundary) tabFace2 = splitLine(shiftedOutline, splitPointA2)[0] #tabFace1 = shapely.geometry.collection.GeometryCollection([splitPointA1, splitPointB1]) #tabFace2 = shapely.geometry.collection.GeometryCollection([splitPointA2, splitPointB2]) tab = Polygon([splitPointA1, splitPointB1, splitPointB2, splitPointA2]) #print("Found a tab: "+tab) return tab, tabFace1, tabFace2
def cutOutline(point, linestring, segmentLimit=None, tolerance=pcbnew.FromMM(0.001)): """ Given a point finds an entity in (multi)linestring which goes through the point. It is possible to restrict the number of segments used by specifying segmentLimit. When segment limit is passed, return a tuple of the string starting and ending in that point. When no limit is passed returns a single string. """ if not isinstance(point, Point): point = Point(point[0], point[1]) if isinstance(linestring, LineString): geom = [linestring] elif isinstance(linestring, MultiLineString): geom = linestring.geoms else: raise RuntimeError("Unknown geometry '{}' passed".format( type(linestring))) for string in geom: bufferedPoint = point.buffer(tolerance, resolution=1) splitted = split(string, bufferedPoint) if len(splitted) == 1 and not Point( splitted[0].coords[0]).intersects(bufferedPoint): continue if len(splitted) == 3: string = LineString([point] + list(splitted[2].coords) + splitted[0].coords[1:]) elif len(splitted) == 2: string = LineString( list(splitted[1].coords) + splitted[0].coords[1:]) else: string = splitted[0] if segmentLimit is None: return string limit1 = max(1, len(string.coords) - segmentLimit) limit2 = min(segmentLimit, len(string.coords) - 1) return LineString(string.coords[limit1:]), LineString( string.coords[:limit2]) return None, None
def fromMm(mm): """Convert millimeters to KiCAD internal units""" return pcbnew.FromMM(mm)
from kikit.pcbnew_compatibility import pcbnew from kikit.intervals import Interval, AxialLine from pcbnew import wxPoint, wxRect import os from itertools import product, chain, islice import numpy as np from shapely.geometry import LinearRing PKG_BASE = os.path.dirname(__file__) KIKIT_LIB = os.path.join(PKG_BASE, "resources/kikit.pretty") SHP_EPSILON = pcbnew.FromMM(0.01) # Common factor of enlarging substrates to # cover up numerical imprecisions of Shapely def fromDegrees(angle): return angle * 10 def fromKicadAngle(angle): return angle / 10 def fromMm(mm): """Convert millimeters to KiCAD internal units""" return pcbnew.FromMM(mm) def toMm(kiUnits): """Convert KiCAD internal units to millimeters""" return pcbnew.ToMM(kiUnits)