kopia lustrzana https://gitlab.com/gerbolyze/gerbonara
Multilayer coil WIP
rodzic
3e47e7c2da
commit
301601e81d
|
@ -208,6 +208,9 @@ class XYCoord:
|
||||||
else:
|
else:
|
||||||
self.x, self.y = x, y
|
self.x, self.y = x, y
|
||||||
|
|
||||||
|
def within_distance(self, x, y, dist):
|
||||||
|
return math.dist((x, y), (self.x, self.y)) < dist
|
||||||
|
|
||||||
def isclose(self, other, tol=1e-3):
|
def isclose(self, other, tol=1e-3):
|
||||||
return math.isclose(self.x, other.x, tol) and math.isclose(self.y, other.y, tol)
|
return math.isclose(self.x, other.x, tol) and math.isclose(self.y, other.y, tol)
|
||||||
|
|
||||||
|
|
|
@ -409,12 +409,47 @@ class Pad:
|
||||||
x, y = rotate_point(self.at.x, self.at.y, math.radians(pr))
|
x, y = rotate_point(self.at.x, self.at.y, math.radians(pr))
|
||||||
return x+px, y+py, self.at.rotation, False
|
return x+px, y+py, self.at.rotation, False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def layer_mask(self):
|
||||||
|
return layer_mask(self.layers)
|
||||||
|
|
||||||
def offset(self, x=0, y=0):
|
def offset(self, x=0, y=0):
|
||||||
self.at = self.at.with_offset(x, y)
|
self.at = self.at.with_offset(x, y)
|
||||||
|
|
||||||
def find_connected(self, **filters):
|
def find_connected_footprints(self, **filters):
|
||||||
""" Find footprints connected to the same net as this pad """
|
""" Find footprints connected to the same net as this pad """
|
||||||
return self.footprint.board.find_footprints(net=self.net.name, **filters)
|
return self.footprint.board.find_footprints(net=self.net.name, **filters)
|
||||||
|
|
||||||
|
def find_same_net(self, include_vias=True):
|
||||||
|
""" Find traces and vias of the same net as this pad. """
|
||||||
|
return self.footprint.board.find_traces(self.net.name, include_vias=include_vias)
|
||||||
|
|
||||||
|
def find_connected_traces(self, consider_candidates=5):
|
||||||
|
board = self.footprint.board
|
||||||
|
|
||||||
|
found = set()
|
||||||
|
search_frontier = [(self.at, 0, self.layer_mask)]
|
||||||
|
while search_frontier:
|
||||||
|
coord, size, layers = search_frontier.pop()
|
||||||
|
x, y = coord.x, coord.y
|
||||||
|
|
||||||
|
for cand, attr, cand_size in self.footprint.board.query_trace_index((x, x, y, y), layers,
|
||||||
|
n=consider_candidates):
|
||||||
|
if cand in found:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cand_coord = getattr(cand, attr)
|
||||||
|
cand_x, cand_y = cand_coord.x, cand_coord.y
|
||||||
|
if math.dist((x, y), (cand_x, cand_y)) <= size/2 + cand_size/2:
|
||||||
|
found.add(cand)
|
||||||
|
yield cand
|
||||||
|
|
||||||
|
if hasattr(cand, 'at'): # via or pad
|
||||||
|
search_frontier.append((cand.at, getattr(cand, 'size', 0), cand.layer_mask))
|
||||||
|
else:
|
||||||
|
mask = cand.layer_mask
|
||||||
|
search_frontier.append((cand.start, cand.width, mask))
|
||||||
|
search_frontier.append((cand.end, cand.width, mask))
|
||||||
|
|
||||||
def render(self, variables=None, margin=None, cache=None):
|
def render(self, variables=None, margin=None, cache=None):
|
||||||
#if self.type in (Atom.connect, Atom.np_thru_hole):
|
#if self.type in (Atom.connect, Atom.np_thru_hole):
|
||||||
|
|
|
@ -78,6 +78,10 @@ class Line:
|
||||||
stroke: Stroke = field(default_factory=Stroke)
|
stroke: Stroke = field(default_factory=Stroke)
|
||||||
tstamp: Timestamp = None
|
tstamp: Timestamp = None
|
||||||
|
|
||||||
|
def rotate(self, angle, cx=None, cy=None):
|
||||||
|
self.start.x, self.start.y = rotate_point(self.start.x, self.start.y, angle, cx, cy)
|
||||||
|
self.end.x, self.end.y = rotate_point(self.end.x, self.end.y, angle, cx, cy)
|
||||||
|
|
||||||
def render(self, variables=None):
|
def render(self, variables=None):
|
||||||
if self.angle:
|
if self.angle:
|
||||||
raise NotImplementedError('Angles on lines are not implemented. Please raise an issue and provide an example file.')
|
raise NotImplementedError('Angles on lines are not implemented. Please raise an issue and provide an example file.')
|
||||||
|
@ -176,6 +180,19 @@ class Arc:
|
||||||
width: Named(float) = None
|
width: Named(float) = None
|
||||||
stroke: Stroke = field(default_factory=Stroke)
|
stroke: Stroke = field(default_factory=Stroke)
|
||||||
tstamp: Timestamp = None
|
tstamp: Timestamp = None
|
||||||
|
_: SEXP_END = None
|
||||||
|
center: XYCoord = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.start = XYCoord(self.start)
|
||||||
|
self.end = XYCoord(self.end)
|
||||||
|
self.mid = XYCoord(self.mid) if self.mid else center_arc_to_kicad_mid(XYCoord(self.center), self.start, self.end)
|
||||||
|
self.center = None
|
||||||
|
|
||||||
|
def rotate(self, angle, cx=None, cy=None):
|
||||||
|
self.start.x, self.start.y = rotate_point(self.start.x, self.start.y, angle, cx, cy)
|
||||||
|
self.mid.x, self.mid.y = rotate_point(self.mid.x, self.mid.y, angle, cx, cy)
|
||||||
|
self.end.x, self.end.y = rotate_point(self.end.x, self.end.y, angle, cx, cy)
|
||||||
|
|
||||||
def render(self, variables=None):
|
def render(self, variables=None):
|
||||||
# FIXME stroke support
|
# FIXME stroke support
|
||||||
|
|
|
@ -4,7 +4,7 @@ Library for handling KiCad's PCB files (`*.kicad_mod`).
|
||||||
|
|
||||||
import math
|
import math
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dataclasses import field, KW_ONLY
|
from dataclasses import field, KW_ONLY, fields
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
import re
|
import re
|
||||||
import fnmatch
|
import fnmatch
|
||||||
|
@ -14,6 +14,7 @@ from .base_types import *
|
||||||
from .primitives import *
|
from .primitives import *
|
||||||
from .footprints import Footprint
|
from .footprints import Footprint
|
||||||
from . import graphical_primitives as gr
|
from . import graphical_primitives as gr
|
||||||
|
import rtree.index
|
||||||
|
|
||||||
from .. import primitives as cad_pr
|
from .. import primitives as cad_pr
|
||||||
|
|
||||||
|
@ -164,6 +165,10 @@ class TrackSegment:
|
||||||
self.start = XYCoord(self.start)
|
self.start = XYCoord(self.start)
|
||||||
self.end = XYCoord(self.end)
|
self.end = XYCoord(self.end)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def layer_mask(self):
|
||||||
|
return layer_mask([self.layer])
|
||||||
|
|
||||||
def render(self, variables=None, cache=None):
|
def render(self, variables=None, cache=None):
|
||||||
if not self.width:
|
if not self.width:
|
||||||
return
|
return
|
||||||
|
@ -193,29 +198,18 @@ class TrackArc:
|
||||||
locked: Flag() = False
|
locked: Flag() = False
|
||||||
net: Named(int) = 0
|
net: Named(int) = 0
|
||||||
tstamp: Timestamp = field(default_factory=Timestamp)
|
tstamp: Timestamp = field(default_factory=Timestamp)
|
||||||
_: KW_ONLY
|
_: SEXP_END = None
|
||||||
center: XYCoord = None
|
center: XYCoord = None
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
self.start = XYCoord(self.start)
|
self.start = XYCoord(self.start)
|
||||||
self.end = XYCoord(self.end)
|
self.end = XYCoord(self.end)
|
||||||
if self.center is not None:
|
self.mid = XYCoord(self.mid) if self.mid else center_arc_to_kicad_mid(XYCoord(self.center), self.start, self.end)
|
||||||
# Convert normal p1/p2/center notation to the insanity that is kicad's midpoint notation
|
self.center = None
|
||||||
center = XYCoord(self.center)
|
|
||||||
cx, cy = center.x, center.y
|
@property
|
||||||
x1, y1 = self.start.x - cx, self.start.y - cy
|
def layer_mask(self):
|
||||||
x2, y2 = self.end.x - cx, self.end.y - cy
|
return layer_mask([self.layer])
|
||||||
# Get a vector pointing towards the middle between "start" and "end"
|
|
||||||
dx, dy = (x1 + x2)/2, (y1 + y2)/2
|
|
||||||
# normalize vector, and multiply by radius to get final point
|
|
||||||
r = math.hypot(x1, y1)
|
|
||||||
l = math.hypot(dx, dy)
|
|
||||||
mx = cx + dx / l * r
|
|
||||||
my = cy + dy / l * r
|
|
||||||
self.mid = XYCoord(mx, my)
|
|
||||||
self.center = None
|
|
||||||
else:
|
|
||||||
self.mid = XYCoord(self.mid)
|
|
||||||
|
|
||||||
def render(self, variables=None, cache=None):
|
def render(self, variables=None, cache=None):
|
||||||
if not self.width:
|
if not self.width:
|
||||||
|
@ -228,9 +222,6 @@ class TrackArc:
|
||||||
yield go.Arc(x1, y1, x2, y2, cx-x1, cy-y1, aperture=aperture, clockwise=True, unit=MM)
|
yield go.Arc(x1, y1, x2, y2, cx-x1, cy-y1, aperture=aperture, clockwise=True, unit=MM)
|
||||||
|
|
||||||
def rotate(self, angle, cx=None, cy=None):
|
def rotate(self, angle, cx=None, cy=None):
|
||||||
if cx is None or cy is None:
|
|
||||||
cx, cy = self.mid.x, self.mid.y
|
|
||||||
|
|
||||||
self.start.x, self.start.y = rotate_point(self.start.x, self.start.y, angle, cx, cy)
|
self.start.x, self.start.y = rotate_point(self.start.x, self.start.y, angle, cx, cy)
|
||||||
self.mid.x, self.mid.y = rotate_point(self.mid.x, self.mid.y, angle, cx, cy)
|
self.mid.x, self.mid.y = rotate_point(self.mid.x, self.mid.y, angle, cx, cy)
|
||||||
self.end.x, self.end.y = rotate_point(self.end.x, self.end.y, angle, cx, cy)
|
self.end.x, self.end.y = rotate_point(self.end.x, self.end.y, angle, cx, cy)
|
||||||
|
@ -259,6 +250,14 @@ class Via:
|
||||||
def abs_pos(self):
|
def abs_pos(self):
|
||||||
return self.at.x, self.at.y, 0, False
|
return self.at.x, self.at.y, 0, False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def layer_mask(self):
|
||||||
|
return layer_mask(self.layers)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self):
|
||||||
|
return self.size
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
self.at = XYCoord(self.at)
|
self.at = XYCoord(self.at)
|
||||||
|
|
||||||
|
@ -314,8 +313,47 @@ class Board:
|
||||||
_ : SEXP_END = None
|
_ : SEXP_END = None
|
||||||
original_filename: str = None
|
original_filename: str = None
|
||||||
_bounding_box: tuple = None
|
_bounding_box: tuple = None
|
||||||
|
_trace_index: rtree.index.Index = None
|
||||||
|
_trace_index_map: dict = None
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild_trace_index(self):
|
||||||
|
idx = self._trace_index = rtree.index.Index()
|
||||||
|
id_map = self._trace_index_map = {}
|
||||||
|
for obj in chain(self.track_segments, self.track_arcs):
|
||||||
|
for field in ('start', 'end'):
|
||||||
|
obj_id = id(obj)
|
||||||
|
coord = getattr(obj, field)
|
||||||
|
_trace_index_map[obj_id] = obj, field, obj.width, obj.layer_mask
|
||||||
|
idx.insert(obj_id, (coord.x, coord.x, coord.y, coord.y))
|
||||||
|
|
||||||
|
for fp in self.footprints:
|
||||||
|
for pad in fp.pads:
|
||||||
|
obj_id = id(pad)
|
||||||
|
_trace_index_map[obj_id] = pad, 'at', 0, pad.layer_mask
|
||||||
|
idx.insert(obj_id, (pad.at.x, pad.at.x, pad.at.y, pad.at.y))
|
||||||
|
|
||||||
|
for via in self.vias:
|
||||||
|
obj_id = id(via)
|
||||||
|
_trace_index_map[obj_id] = via, 'at', via.size, via.layer_mask
|
||||||
|
idx.insert(obj_id, (via.at.x, via.at.x, via.at.y, via.at.y))
|
||||||
|
|
||||||
|
def query_trace_index(self, point, layers='*.Cu', n=5):
|
||||||
|
if self._trace_index is None:
|
||||||
|
self.rebuild_trace_index()
|
||||||
|
|
||||||
|
if isinstance(layers, str):
|
||||||
|
layers = [l.strip() for l in layers.split(',')]
|
||||||
|
|
||||||
|
if not isinstance(layers, int):
|
||||||
|
layers = layer_mask(layers)
|
||||||
|
|
||||||
|
x, y = point
|
||||||
|
for obj_id in self._trace_index.nearest((x, x, y, y), n):
|
||||||
|
entry = obj, attr, size, mask = _trace_index_map[obj_id]
|
||||||
|
if layers & mask:
|
||||||
|
yield entry
|
||||||
|
|
||||||
def __after_parse__(self, parent):
|
def __after_parse__(self, parent):
|
||||||
self.properties = {prop.key: prop.value for prop in self.properties}
|
self.properties = {prop.key: prop.value for prop in self.properties}
|
||||||
|
|
||||||
|
@ -365,6 +403,12 @@ class Board:
|
||||||
case _:
|
case _:
|
||||||
raise TypeError('Can only remove KiCad objects, cannot map generic gerbonara.cad objects for removal')
|
raise TypeError('Can only remove KiCad objects, cannot map generic gerbonara.cad objects for removal')
|
||||||
|
|
||||||
|
def remove_many(self, iterable):
|
||||||
|
iterable = {id(obj) for obj in iterable}
|
||||||
|
for field in fields(self):
|
||||||
|
if field.default_factory is list and field.name not in ('nets', 'properties'):
|
||||||
|
setattr(self, field.name, [obj for obj in getattr(self, field.name) if id(obj) not in iterable])
|
||||||
|
|
||||||
def add(self, obj):
|
def add(self, obj):
|
||||||
match obj:
|
match obj:
|
||||||
case gr.Text():
|
case gr.Text():
|
||||||
|
@ -481,6 +525,13 @@ class Board:
|
||||||
continue
|
continue
|
||||||
yield fp
|
yield fp
|
||||||
|
|
||||||
|
def find_traces(self, net=None, include_vias=True):
|
||||||
|
net_id = self.net_id(net, create=False)
|
||||||
|
match = lambda obj: obj.net == net_id
|
||||||
|
for obj in chain(self.track_segments, self.track_arcs, self.vias):
|
||||||
|
if obj.net == net_id:
|
||||||
|
yield obj
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self):
|
def version(self):
|
||||||
return self._version
|
return self._version
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
|
import math
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .sexp import *
|
from .sexp import *
|
||||||
|
@ -12,6 +13,7 @@ def unfuck_layers(layers):
|
||||||
else:
|
else:
|
||||||
return layers
|
return layers
|
||||||
|
|
||||||
|
|
||||||
def fuck_layers(layers):
|
def fuck_layers(layers):
|
||||||
if layers and 'F.Cu' in layers and 'B.Cu' in layers and not any(re.match(r'^In[0-9]+\.Cu$', l) for l in layers):
|
if layers and 'F.Cu' in layers and 'B.Cu' in layers and not any(re.match(r'^In[0-9]+\.Cu$', l) for l in layers):
|
||||||
return ['F&B.Cu', *(l for l in layers if l not in ('F.Cu', 'B.Cu'))]
|
return ['F&B.Cu', *(l for l in layers if l not in ('F.Cu', 'B.Cu'))]
|
||||||
|
@ -19,6 +21,38 @@ def fuck_layers(layers):
|
||||||
return layers
|
return layers
|
||||||
|
|
||||||
|
|
||||||
|
def layer_mask(layers):
|
||||||
|
mask = 0
|
||||||
|
for layer in layers:
|
||||||
|
match layer:
|
||||||
|
case '*.Cu':
|
||||||
|
return 0xff
|
||||||
|
case 'F.Cu':
|
||||||
|
mask |= 1<<0
|
||||||
|
case 'B.Cu':
|
||||||
|
mask |= 1<<31
|
||||||
|
case _:
|
||||||
|
if (m := re.match(f'In([0-9]+)\.Cu', layer)):
|
||||||
|
mask |= 1<<int(m.group(1))
|
||||||
|
return mask
|
||||||
|
|
||||||
|
|
||||||
|
def center_arc_to_kicad_mid(center, start, end):
|
||||||
|
# Convert normal p1/p2/center notation to the insanity that is kicad's midpoint notation
|
||||||
|
cx, cy = center.x, center.y
|
||||||
|
x1, y1 = start.x - cx, start.y - cy
|
||||||
|
x2, y2 = end.x - cx, end.y - cy
|
||||||
|
# Get a vector pointing from the center to the "mid" point.
|
||||||
|
dx, dy = x1 - x2, y1 - y2 # Get a vector pointing from "end" to "start"
|
||||||
|
dx, dy = -dy, dx # rotate by 90 degrees counter-clockwise
|
||||||
|
# normalize vector, and multiply by radius to get final point
|
||||||
|
r = math.hypot(x1, y1)
|
||||||
|
l = math.hypot(dx, dy)
|
||||||
|
mx = cx + dx / l * r
|
||||||
|
my = cy + dy / l * r
|
||||||
|
return XYCoord(mx, my)
|
||||||
|
|
||||||
|
|
||||||
@sexp_type('hatch')
|
@sexp_type('hatch')
|
||||||
class Hatch:
|
class Hatch:
|
||||||
style: AtomChoice(Atom.none, Atom.edge, Atom.full) = Atom.edge
|
style: AtomChoice(Atom.none, Atom.edge, Atom.full) = Atom.edge
|
||||||
|
|
|
@ -633,7 +633,7 @@ class Schematic:
|
||||||
|
|
||||||
# From: https://jakevdp.github.io/blog/2012/10/07/xkcd-style-plots-in-matplotlib/
|
# From: https://jakevdp.github.io/blog/2012/10/07/xkcd-style-plots-in-matplotlib/
|
||||||
#def xkcd_line(x, y, xlim=None, ylim=None, mag=1.0, f1=30, f2=0.05, f3=15):
|
#def xkcd_line(x, y, xlim=None, ylim=None, mag=1.0, f1=30, f2=0.05, f3=15):
|
||||||
def xkcd_line(x, y, xlim=None, ylim=None, mag=1.0, f1=10, f2=0.10, f3=5):
|
def xkcd_line(x, y, xlim=None, ylim=None, mag=1.0, f1=10, f2=0.05, f3=5):
|
||||||
"""
|
"""
|
||||||
Mimic a hand-drawn line from (x, y) data
|
Mimic a hand-drawn line from (x, y) data
|
||||||
|
|
||||||
|
@ -745,9 +745,10 @@ def wonkify(path):
|
||||||
for p0, p1, p2 in zip(pts[0::], pts[1::], pts[2::]):
|
for p0, p1, p2 in zip(pts[0::], pts[1::], pts[2::]):
|
||||||
dx1, dy1 = p1[0] - p0[0], p1[1] - p0[1]
|
dx1, dy1 = p1[0] - p0[0], p1[1] - p0[1]
|
||||||
dx2, dy2 = p2[0] - p1[0], p2[1] - p1[1]
|
dx2, dy2 = p2[0] - p1[0], p2[1] - p1[1]
|
||||||
|
l1, l2 = math.hypot(dx1, dy1), math.hypot(dx2, dy2)
|
||||||
a1, a2 = math.atan2(dy1, dx1), math.atan2(dy2, dx2)
|
a1, a2 = math.atan2(dy1, dx1), math.atan2(dy2, dx2)
|
||||||
da = (a2 - a1 + math.pi) % (2*math.pi) - math.pi
|
da = (a2 - a1 + math.pi) % (2*math.pi) - math.pi
|
||||||
if abs(da) > math.pi/4:
|
if abs(da) > math.pi/4 and l1+l2 > 3:
|
||||||
seg.append(p1)
|
seg.append(p1)
|
||||||
seg = [p1, p2]
|
seg = [p1, p2]
|
||||||
segs.append(seg)
|
segs.append(seg)
|
||||||
|
|
|
@ -142,12 +142,17 @@ def cli():
|
||||||
rules and use only rules given by --input-map''')
|
rules and use only rules given by --input-map''')
|
||||||
@click.option('--force-zip', is_flag=True, help='''Force treating input path as a zip file (default: guess file type
|
@click.option('--force-zip', is_flag=True, help='''Force treating input path as a zip file (default: guess file type
|
||||||
from extension and contents)''')
|
from extension and contents)''')
|
||||||
@click.option('--top/--bottom', default=True, help='Which side of the board to render')
|
@click.option('--top', 'side', flag_value='top', help='Render top side')
|
||||||
|
@click.option('--bottom', 'side', flag_value='bottom', help='Render top side')
|
||||||
@click.option('--command-line-units', type=Unit(), help='''Units for values given in other options. Default:
|
@click.option('--command-line-units', type=Unit(), help='''Units for values given in other options. Default:
|
||||||
millimeter''')
|
millimeter''')
|
||||||
@click.option('--margin', type=float, default=0.0, help='Add space around the board inside the viewport')
|
@click.option('--margin', type=float, default=0.0, help='Add space around the board inside the viewport')
|
||||||
@click.option('--force-bounds', help='Force SVG bounding box to value given as "min_x,min_y,max_x,max_y"')
|
@click.option('--force-bounds', help='Force SVG bounding box to value given as "min_x,min_y,max_x,max_y"')
|
||||||
@click.option('--inkscape/--standard-svg', default=True, help='Export in Inkscape SVG format with layers and stuff.')
|
@click.option('--inkscape/--standard-svg', default=True, help='Export in Inkscape SVG format with layers and stuff.')
|
||||||
|
@click.option('--pretty/--no-filters', default=True, help='''Export pseudo-realistic render using filters (default) or
|
||||||
|
just stack up layers using given colorscheme. In "--no-filters" mode, by default all layers are exported
|
||||||
|
unless either "--top" or "--bottom" is given.''')
|
||||||
|
@click.option('--drills/--no-drills', default=True, help='''Include (default) or exclude drills ("--no-filters" only!)''')
|
||||||
@click.option('--colorscheme', type=click.Path(exists=True, path_type=Path), help='''Load colorscheme from given JSON
|
@click.option('--colorscheme', type=click.Path(exists=True, path_type=Path), help='''Load colorscheme from given JSON
|
||||||
file. The JSON file must contain a single dict with keys copper, silk, mask, paste, drill and outline.
|
file. The JSON file must contain a single dict with keys copper, silk, mask, paste, drill and outline.
|
||||||
Each key must map to a string containing either a normal 6-digit hex color with leading hash sign, or an
|
Each key must map to a string containing either a normal 6-digit hex color with leading hash sign, or an
|
||||||
|
@ -155,8 +160,8 @@ def cli():
|
||||||
with FF being completely opaque, and 00 being invisibly transparent.''')
|
with FF being completely opaque, and 00 being invisibly transparent.''')
|
||||||
@click.argument('inpath', type=click.Path(exists=True))
|
@click.argument('inpath', type=click.Path(exists=True))
|
||||||
@click.argument('outfile', type=click.File('w'), default='-')
|
@click.argument('outfile', type=click.File('w'), default='-')
|
||||||
def render(inpath, outfile, format_warnings, input_map, use_builtin_name_rules, force_zip, top, command_line_units,
|
def render(inpath, outfile, format_warnings, input_map, use_builtin_name_rules, force_zip, side, drills,
|
||||||
margin, force_bounds, inkscape, colorscheme):
|
command_line_units, margin, force_bounds, inkscape, pretty, colorscheme):
|
||||||
""" Render a gerber file, or a directory or zip of gerber files into an SVG file. """
|
""" Render a gerber file, or a directory or zip of gerber files into an SVG file. """
|
||||||
|
|
||||||
overrides = json.loads(input_map.read_bytes()) if input_map else None
|
overrides = json.loads(input_map.read_bytes()) if input_map else None
|
||||||
|
@ -174,9 +179,14 @@ def render(inpath, outfile, format_warnings, input_map, use_builtin_name_rules,
|
||||||
if colorscheme:
|
if colorscheme:
|
||||||
colorscheme = json.loads(colorscheme.read_text())
|
colorscheme = json.loads(colorscheme.read_text())
|
||||||
|
|
||||||
outfile.write(str(stack.to_pretty_svg(side='top' if top else 'bottom', margin=margin,
|
if pretty:
|
||||||
arg_unit=(command_line_units or MM),
|
svg = stack.to_pretty_svg(side='bottom' if side == 'bottom' else 'top', margin=margin,
|
||||||
svg_unit=MM, force_bounds=force_bounds, inkscape=inkscape, colors=colorscheme)))
|
arg_unit=(command_line_units or MM),
|
||||||
|
svg_unit=MM, force_bounds=force_bounds, inkscape=inkscape, colors=colorscheme)
|
||||||
|
else:
|
||||||
|
svg = stack.to_svg(side_re=side or '.*', margin=margin, drills=drills, arg_unit=(command_line_units or MM),
|
||||||
|
svg_unit=MM, force_bounds=force_bounds, colors=colorscheme)
|
||||||
|
outfile.write(str(svg))
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
|
|
@ -699,7 +699,7 @@ class LayerStack:
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self)
|
return str(self)
|
||||||
|
|
||||||
def to_svg(self, margin=0, arg_unit=MM, svg_unit=MM, force_bounds=None, color_map=None, tag=Tag):
|
def to_svg(self, margin=0, side_re='.*', drills=True, arg_unit=MM, svg_unit=MM, force_bounds=None, colors=None, tag=Tag):
|
||||||
""" Convert this layer stack to a plain SVG string. This is intended for use cases where the resulting SVG will
|
""" Convert this layer stack to a plain SVG string. This is intended for use cases where the resulting SVG will
|
||||||
be processed by other tools, and thus styling with colors or extra markup like Inkscape layer information are
|
be processed by other tools, and thus styling with colors or extra markup like Inkscape layer information are
|
||||||
unwanted. If you want to instead generate a nice-looking preview image for display or graphical editing in tools
|
unwanted. If you want to instead generate a nice-looking preview image for display or graphical editing in tools
|
||||||
|
@ -709,6 +709,9 @@ class LayerStack:
|
||||||
mirrored vertically.
|
mirrored vertically.
|
||||||
|
|
||||||
:param margin: Export SVG file with given margin around the board's bounding box.
|
:param margin: Export SVG file with given margin around the board's bounding box.
|
||||||
|
:param side_re: A regex, such as ``'top'``, ``'bottom'``, or ``'.*'`` (default). Selects which layers to export.
|
||||||
|
The default includes inner layers.
|
||||||
|
:param drills: :py:obj:`bool` setting if drills are included (default) or not.
|
||||||
:param arg_unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). Which unit ``margin`` and
|
:param arg_unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). Which unit ``margin`` and
|
||||||
``force_bounds`` are specified in. Default: mm
|
``force_bounds`` are specified in. Default: mm
|
||||||
:param svg_unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). Which unit to use inside the SVG file.
|
:param svg_unit: :py:class:`.LengthUnit` or str (``'mm'`` or ``'inch'``). Which unit to use inside the SVG file.
|
||||||
|
@ -716,6 +719,7 @@ class LayerStack:
|
||||||
:param force_bounds: Use bounds given as :py:obj:`((min_x, min_y), (max_x, max_y))` tuple for the output SVG
|
:param force_bounds: Use bounds given as :py:obj:`((min_x, min_y), (max_x, max_y))` tuple for the output SVG
|
||||||
file instead of deriving them from this board's bounding box and ``margin``. Note that this
|
file instead of deriving them from this board's bounding box and ``margin``. Note that this
|
||||||
will not scale or move the board, but instead will only crop the viewport.
|
will not scale or move the board, but instead will only crop the viewport.
|
||||||
|
:param colors: Dict mapping ``f'{side} {use}'`` strings to SVG colors.
|
||||||
:param tag: Extension point to support alternative XML serializers in addition to the built-in one.
|
:param tag: Extension point to support alternative XML serializers in addition to the built-in one.
|
||||||
:rtype: :py:obj:`str`
|
:rtype: :py:obj:`str`
|
||||||
"""
|
"""
|
||||||
|
@ -726,29 +730,29 @@ class LayerStack:
|
||||||
|
|
||||||
stroke_attrs = {'stroke_linejoin': 'round', 'stroke_linecap': 'round'}
|
stroke_attrs = {'stroke_linejoin': 'round', 'stroke_linecap': 'round'}
|
||||||
|
|
||||||
if color_map is None:
|
if colors is None:
|
||||||
color_map = default_dict(lambda: 'black')
|
colors = defaultdict(lambda: 'black')
|
||||||
|
|
||||||
tags = []
|
tags = []
|
||||||
|
layer_transform = f'translate(0 {bounds[0][1] + bounds[1][1]}) scale(1 -1)'
|
||||||
for (side, use), layer in reversed(self.graphic_layers.items()):
|
for (side, use), layer in reversed(self.graphic_layers.items()):
|
||||||
fg = color_map[(side, use)]
|
if re.match(side_re, side) and (fg := colors.get(f'{side} {use}')):
|
||||||
tags.append(tag('g', list(layer.svg_objects(svg_unit=svg_unit, fg=fg, bg="white", tag=Tag)),
|
tags.append(tag('g', list(layer.svg_objects(svg_unit=svg_unit, fg=fg, bg="white", tag=Tag)),
|
||||||
**stroke_attrs, id=f'l-{side}-{use}'))
|
**stroke_attrs, id=f'l-{side}-{use}', transform=layer_transform))
|
||||||
|
|
||||||
if self.drill_pth:
|
if drills:
|
||||||
fg = color_map[('drill', 'pth')]
|
if self.drill_pth and (fg := colors.get('drill pth')):
|
||||||
tags.append(tag('g', list(self.drill_pth.svg_objects(svg_unit=svg_unit, fg=fg, bg="white", tag=Tag)),
|
tags.append(tag('g', list(self.drill_pth.svg_objects(svg_unit=svg_unit, fg=fg, bg="white", tag=Tag)),
|
||||||
**stroke_attrs, id=f'l-drill-pth'))
|
**stroke_attrs, id=f'l-drill-pth', transform=layer_transform))
|
||||||
|
|
||||||
if self.drill_npth:
|
if self.drill_npth and (fg := colors.get('drill npth')):
|
||||||
fg = color_map[('drill', 'npth')]
|
tags.append(tag('g', list(self.drill_npth.svg_objects(svg_unit=svg_unit, fg=fg, bg="white", tag=Tag)),
|
||||||
tags.append(tag('g', list(self.drill_npth.svg_objects(svg_unit=svg_unit, fg=fg, bg="white", tag=Tag)),
|
**stroke_attrs, id=f'l-drill-npth', transform=layer_transform))
|
||||||
**stroke_attrs, id=f'l-drill-npth'))
|
|
||||||
|
|
||||||
for i, layer in enumerate(self._drill_layers):
|
if (fg := colors.get('drill unknown')):
|
||||||
fg = color_map[('drill', 'unknown')]
|
for i, layer in enumerate(self._drill_layers):
|
||||||
tags.append(tag('g', list(layer.svg_objects(svg_unit=svg_unit, fg=fg, bg="white", tag=Tag)),
|
tags.append(tag('g', list(layer.svg_objects(svg_unit=svg_unit, fg=fg, bg="white", tag=Tag)),
|
||||||
**stroke_attrs, id=f'l-drill-{i}'))
|
**stroke_attrs, id=f'l-drill-{i}', transform=layer_transform))
|
||||||
|
|
||||||
return setup_svg(tags, bounds, margin=margin, arg_unit=arg_unit, svg_unit=svg_unit, tag=tag)
|
return setup_svg(tags, bounds, margin=margin, arg_unit=arg_unit, svg_unit=svg_unit, tag=tag)
|
||||||
|
|
||||||
|
@ -819,6 +823,7 @@ class LayerStack:
|
||||||
|
|
||||||
inkscape_attrs = lambda label: dict(inkscape__groupmode='layer', inkscape__label=label) if inkscape else {}
|
inkscape_attrs = lambda label: dict(inkscape__groupmode='layer', inkscape__label=label) if inkscape else {}
|
||||||
stroke_attrs = {'stroke_linejoin': 'round', 'stroke_linecap': 'round'}
|
stroke_attrs = {'stroke_linejoin': 'round', 'stroke_linecap': 'round'}
|
||||||
|
layer_transform=f'translate(0 {bounds[0][1] + bounds[1][1]}) scale(1 -1)'
|
||||||
|
|
||||||
use_defs = []
|
use_defs = []
|
||||||
|
|
||||||
|
@ -862,18 +867,19 @@ class LayerStack:
|
||||||
objects.insert(0, tag('path', id='outline-path', d=self.outline_svg_d(unit=svg_unit), fill='white'))
|
objects.insert(0, tag('path', id='outline-path', d=self.outline_svg_d(unit=svg_unit), fill='white'))
|
||||||
layers.append(tag('g', objects, id=f'l-{side}-{use}', filter=f'url(#f-{use})',
|
layers.append(tag('g', objects, id=f'l-{side}-{use}', filter=f'url(#f-{use})',
|
||||||
fill=default_fill, stroke=default_stroke, **stroke_attrs,
|
fill=default_fill, stroke=default_stroke, **stroke_attrs,
|
||||||
**inkscape_attrs(f'{side} {use}')))
|
**inkscape_attrs(f'{side} {use}'), transform=layer_transform))
|
||||||
|
|
||||||
for i, layer in enumerate(self.drill_layers):
|
for i, layer in enumerate(self.drill_layers):
|
||||||
layers.append(tag('g', list(layer.instance.svg_objects(svg_unit=svg_unit, fg='white', bg='black', tag=Tag)),
|
layers.append(tag('g', list(layer.instance.svg_objects(svg_unit=svg_unit, fg='white', bg='black', tag=Tag)),
|
||||||
id=f'l-drill-{i}', filter=f'url(#f-drill)', **stroke_attrs, **inkscape_attrs(f'drill-{i}')))
|
id=f'l-drill-{i}', filter=f'url(#f-drill)', **stroke_attrs, **inkscape_attrs(f'drill-{i}'),
|
||||||
|
transform=layer_transform))
|
||||||
|
|
||||||
if self.outline:
|
if self.outline:
|
||||||
layers.append(tag('g', list(self.outline.instance.svg_objects(svg_unit=svg_unit, fg='white', bg='black', tag=Tag)),
|
layers.append(tag('g', list(self.outline.instance.svg_objects(svg_unit=svg_unit, fg='white', bg='black', tag=Tag)),
|
||||||
id=f'l-mechanical-outline', **stroke_attrs, **inkscape_attrs(f'outline')))
|
id=f'l-mechanical-outline', **stroke_attrs, **inkscape_attrs(f'outline'),
|
||||||
|
transform=layer_transform))
|
||||||
|
|
||||||
layer_group = tag('g', layers, transform=f'translate(0 {bounds[0][1] + bounds[1][1]}) scale(1 -1)')
|
tags = [tag('defs', filter_defs + use_defs), *layers]
|
||||||
tags = [tag('defs', filter_defs + use_defs), layer_group]
|
|
||||||
return setup_svg(tags, bounds, margin=margin, arg_unit=arg_unit, svg_unit=svg_unit, pagecolor="white", tag=tag, inkscape=inkscape)
|
return setup_svg(tags, bounds, margin=margin, arg_unit=arg_unit, svg_unit=svg_unit, pagecolor="white", tag=tag, inkscape=inkscape)
|
||||||
|
|
||||||
def bounding_box(self, unit=MM, default=None):
|
def bounding_box(self, unit=MM, default=None):
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -30,7 +30,7 @@ setup(
|
||||||
'Tracker': 'https://gitlab.com/gerbolyze/gerbonara/issues',
|
'Tracker': 'https://gitlab.com/gerbolyze/gerbonara/issues',
|
||||||
},
|
},
|
||||||
packages=find_packages(exclude=['tests']),
|
packages=find_packages(exclude=['tests']),
|
||||||
install_requires=['click'],
|
install_requires=['click', 'rtree'],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'gerbonara = gerbonara.cli:cli',
|
'gerbonara = gerbonara.cli:cli',
|
||||||
|
|
|
@ -0,0 +1,294 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from math import *
|
||||||
|
from pathlib import Path
|
||||||
|
from itertools import cycle
|
||||||
|
from scipy.constants import mu_0
|
||||||
|
|
||||||
|
from gerbonara.cad.kicad import pcb as kicad_pcb
|
||||||
|
from gerbonara.cad.kicad import footprints as kicad_fp
|
||||||
|
from gerbonara.cad.kicad import graphical_primitives as kicad_gr
|
||||||
|
from gerbonara.cad.kicad import primitives as kicad_pr
|
||||||
|
from gerbonara.utils import Tag
|
||||||
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = '1.0.0'
|
||||||
|
|
||||||
|
|
||||||
|
def point_line_distance(p, l1, l2):
|
||||||
|
x0, y0 = p
|
||||||
|
x1, y1 = l1
|
||||||
|
x2, y2 = l2
|
||||||
|
# https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
|
||||||
|
return abs((x2-x1)*(y1-y0) - (x1-x0)*(y2-y1)) / sqrt((x2-x1)**2 + (y2-y1)**2)
|
||||||
|
|
||||||
|
def line_line_intersection(l1, l2):
|
||||||
|
p1, p2 = l1
|
||||||
|
p3, p4 = l2
|
||||||
|
x1, y1 = p1
|
||||||
|
x2, y2 = p2
|
||||||
|
x3, y3 = p3
|
||||||
|
x4, y4 = p4
|
||||||
|
|
||||||
|
# https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
|
||||||
|
px = ((x1*y2-y1*x2)*(x3-x4)-(x1-x2)*(x3*y4-y3*x4))/((x1-x2)*(y3-y4)-(y1-y2)*(x3-x4))
|
||||||
|
py = ((x1*y2-y1*x2)*(y3-y4)-(y1-y2)*(x3*y4-y3*x4))/((x1-x2)*(y3-y4)-(y1-y2)*(x3-x4))
|
||||||
|
return px, py
|
||||||
|
|
||||||
|
def angle_between_vectors(va, vb):
|
||||||
|
angle = atan2(vb[1], vb[0]) - atan2(va[1], va[0])
|
||||||
|
if angle < 0:
|
||||||
|
angle += 2*pi
|
||||||
|
return angle
|
||||||
|
|
||||||
|
class SVGPath:
|
||||||
|
def __init__(self, **attrs):
|
||||||
|
self.d = ''
|
||||||
|
self.attrs = attrs
|
||||||
|
|
||||||
|
def line(self, x, y):
|
||||||
|
self.d += f'L {x} {y} '
|
||||||
|
|
||||||
|
def move(self, x, y):
|
||||||
|
self.d += f'M {x} {y} '
|
||||||
|
|
||||||
|
def arc(self, x, y, r, large, sweep):
|
||||||
|
self.d += f'A {r} {r} 0 {int(large)} {int(sweep)} {x} {y} '
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.d += 'Z '
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
attrs = ' '.join(f'{key.replace("_", "-")}="{value}"' for key, value in self.attrs.items())
|
||||||
|
return f'<path {attrs} d="{self.d.rstrip()}"/>'
|
||||||
|
|
||||||
|
class SVGCircle:
|
||||||
|
def __init__(self, r, cx, cy, **attrs):
|
||||||
|
self.r = r
|
||||||
|
self.cx, self.cy = cx, cy
|
||||||
|
self.attrs = attrs
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
attrs = ' '.join(f'{key.replace("_", "-")}="{value}"' for key, value in self.attrs.items())
|
||||||
|
return f'<circle {attrs} r="{self.r}" cx="{self.cx}" cy="{self.cy}"/>'
|
||||||
|
|
||||||
|
def svg_file(fn, stuff, vbw, vbh, vbx=0, vby=0):
|
||||||
|
with open(fn, 'w') as f:
|
||||||
|
f.write('<?xml version="1.0" standalone="no"?>\n')
|
||||||
|
f.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
|
||||||
|
f.write(f'<svg version="1.1" width="{vbw*4}mm" height="{vbh*4}mm" viewBox="{vbx} {vby} {vbw} {vbh}" style="background-color: #333" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">>\n')
|
||||||
|
|
||||||
|
for foo in stuff:
|
||||||
|
f.write(str(foo))
|
||||||
|
|
||||||
|
f.write('</svg>\n')
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path))
|
||||||
|
@click.option('--footprint-name', help="Name for the generated footprint. Default: Output file name sans extension.")
|
||||||
|
@click.option('--target-layers', default='F.Cu,B.Cu', help="Target KiCad layers for the generated footprint. Default: F.Cu,B.Cu.")
|
||||||
|
@click.option('--turns', type=int, default=5, help='Number of turns')
|
||||||
|
@click.option('--diameter', type=float, default=50, help='Outer diameter [mm]')
|
||||||
|
@click.option('--trace-width', type=float, default=0.15)
|
||||||
|
@click.option('--via-diameter', type=float, default=0.6)
|
||||||
|
@click.option('--via-drill', type=float, default=0.3)
|
||||||
|
@click.option('--keepout-zone/--no-keepout-zone', default=True, help='Add a keepout are to the footprint (default: yes)')
|
||||||
|
@click.option('--keepout-margin', type=float, default=5, help='Margin between outside of coil and keepout area (mm, default: 5)')
|
||||||
|
@click.option('--num-twists', type=int, default=1, help='Number of twists per revolution (default: 1)')
|
||||||
|
@click.option('--clearance', type=float, default=0.15)
|
||||||
|
@click.option('--clipboard/--no-clipboard', help='Use clipboard integration (requires wl-clipboard)')
|
||||||
|
@click.option('--counter-clockwise/--clockwise', help='Direction of generated spiral. Default: clockwise when wound from the inside.')
|
||||||
|
def generate(outfile, turns, diameter, via_diameter, via_drill, trace_width, clearance, footprint_name, target_layers,
|
||||||
|
num_twists, clipboard, counter_clockwise, keepout_zone, keepout_margin):
|
||||||
|
if 'WAYLAND_DISPLAY' in os.environ:
|
||||||
|
copy, paste, cliputil = ['wl-copy'], ['wl-paste'], 'xclip'
|
||||||
|
else:
|
||||||
|
copy, paste, cliputil = ['xclip', '-i', '-sel', 'clipboard'], ['xclip', '-o', '-sel' 'clipboard'], 'wl-clipboard'
|
||||||
|
|
||||||
|
|
||||||
|
pitch = clearance + trace_width
|
||||||
|
target_layers = [name.strip() for name in target_layers.split(',')]
|
||||||
|
via_diameter = max(trace_width, via_diameter)
|
||||||
|
rainbow = '#817 #a35 #c66 #e94 #ed0 #9d5 #4d8 #2cb #0bc #09c #36b #639'.split()
|
||||||
|
rainbow = rainbow[2::3] + rainbow[1::3] + rainbow[0::3]
|
||||||
|
out_paths = [SVGPath(fill='none', stroke=rainbow[i%len(rainbow)], stroke_width=trace_width, stroke_linejoin='round', stroke_linecap='round') for i in range(len(target_layers))]
|
||||||
|
svg_stuff = [*out_paths]
|
||||||
|
|
||||||
|
|
||||||
|
# See https://coil32.net/pcb-coil.html for details
|
||||||
|
|
||||||
|
d_inside = diameter - 2*(pitch*turns - clearance)
|
||||||
|
d_avg = (diameter + d_inside)/2
|
||||||
|
phi = (diameter - d_inside) / (diameter + d_inside)
|
||||||
|
c1, c2, c3, c4 = 1.00, 2.46, 0.00, 0.20
|
||||||
|
L = mu_0 * turns**2 * d_avg*1e3 * c1 / 2 * (log(c2/phi) + c3*phi + c4*phi**2)
|
||||||
|
print(f'Outer diameter: {diameter:g} mm', file=sys.stderr)
|
||||||
|
print(f'Average diameter: {d_avg:g} mm', file=sys.stderr)
|
||||||
|
print(f'Inner diameter: {d_inside:g} mm', file=sys.stderr)
|
||||||
|
print(f'Fill factor: {phi:g}', file=sys.stderr)
|
||||||
|
print(f'Approximate inductance: {L:g} µH', file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
make_pad = lambda num, x, y: kicad_fp.Pad(
|
||||||
|
number=str(num),
|
||||||
|
type=kicad_fp.Atom.smd,
|
||||||
|
shape=kicad_fp.Atom.circle,
|
||||||
|
at=kicad_fp.AtPos(x=x, y=y),
|
||||||
|
size=kicad_fp.XYCoord(x=trace_width, y=trace_width),
|
||||||
|
layers=[target_layer],
|
||||||
|
clearance=clearance,
|
||||||
|
zone_connect=0)
|
||||||
|
|
||||||
|
make_line = lambda x1, y1, x2, y2, layer: kicad_fp.Line(
|
||||||
|
start=kicad_fp.XYCoord(x=x1, y=y1),
|
||||||
|
end=kicad_fp.XYCoord(x=x2, y=y2),
|
||||||
|
layer=layer,
|
||||||
|
stroke=kicad_fp.Stroke(width=trace_width))
|
||||||
|
|
||||||
|
make_arc = lambda x1, y1, x2, y2, xc, yc, layer: kicad_fp.Arc(
|
||||||
|
start=kicad_fp.XYCoord(x=x1, y=y1),
|
||||||
|
mid=kicad_fp.XYCoord(x=xc, y=yc),
|
||||||
|
end=kicad_fp.XYCoord(x=x2, y=y2),
|
||||||
|
layer=layer,
|
||||||
|
stroke=kicad_fp.Stroke(width=trace_width))
|
||||||
|
|
||||||
|
|
||||||
|
make_via = lambda x, y, layers: kicad_fp.Pad(number="NC",
|
||||||
|
type=kicad_fp.Atom.thru_hole,
|
||||||
|
shape=kicad_fp.Atom.circle,
|
||||||
|
at=kicad_fp.AtPos(x=x, y=y),
|
||||||
|
size=kicad_fp.XYCoord(x=via_diameter, y=via_diameter),
|
||||||
|
drill=kicad_fp.Drill(diameter=via_drill),
|
||||||
|
layers=layers,
|
||||||
|
clearance=clearance,
|
||||||
|
zone_connect=0)
|
||||||
|
|
||||||
|
pads = []
|
||||||
|
lines = []
|
||||||
|
arcs = []
|
||||||
|
turns_per_layer = ceil((turns-1) / len(target_layers))
|
||||||
|
print(f'Splitting {turns} turns into {len(target_layers)} layers using {turns_per_layer} turns per layer plus one weaving turn.', file=sys.stderr)
|
||||||
|
sector_angle = 2*pi / turns_per_layer
|
||||||
|
### DELETE THIS:
|
||||||
|
d_inside = diameter/2 # FIXME DEBUG
|
||||||
|
###
|
||||||
|
|
||||||
|
def do_spiral(path, r1, r2, a1, a2, layer, fn=64):
|
||||||
|
x0, y0 = cos(a1)*r1, sin(a1)*r1
|
||||||
|
path.move(x0, y0)
|
||||||
|
direction = '↓' if r2 < r1 else '↑'
|
||||||
|
dr = 3 if r2 < r1 else -3
|
||||||
|
label = f'{direction} {degrees(a1):.0f}'
|
||||||
|
svg_stuff.append(Tag('text',
|
||||||
|
[label],
|
||||||
|
x=str(x0 + cos(a1)*dr),
|
||||||
|
y=str(y0 + sin(a1)*dr),
|
||||||
|
style=f'font: 1px bold sans-serif; fill: {path.attrs["stroke"]}'))
|
||||||
|
|
||||||
|
for i in range(fn+1):
|
||||||
|
r = r1 + i*(r2-r1)/fn
|
||||||
|
a = a1 + i*(a2-a1)/fn
|
||||||
|
xn, yn = cos(a)*r, sin(a)*r
|
||||||
|
path.line(xn, yn)
|
||||||
|
|
||||||
|
svg_stuff.append(Tag('text',
|
||||||
|
[label],
|
||||||
|
x=str(xn + cos(a2)*-dr),
|
||||||
|
y=str(yn + sin(a2)*-dr + 1.2),
|
||||||
|
style=f'font: 1px bold sans-serif; fill: {path.attrs["stroke"]}'))
|
||||||
|
|
||||||
|
|
||||||
|
print(f'{turns=} {turns_per_layer=} {len(target_layers)=}', file=sys.stderr)
|
||||||
|
|
||||||
|
start_radius = d_inside/2
|
||||||
|
end_radius = diameter/2
|
||||||
|
|
||||||
|
inner_via_ring_radius = start_radius - via_diameter/2
|
||||||
|
inner_via_angle = 2*asin(via_diameter/2 / inner_via_ring_radius)
|
||||||
|
|
||||||
|
outer_via_ring_radius = end_radius + via_diameter/2
|
||||||
|
outer_via_angle = 2*asin(via_diameter/2 / outer_via_ring_radius)
|
||||||
|
print(f'inner via ring @ {inner_via_ring_radius:.2f} mm (from {start_radius:.2f} mm)', file=sys.stderr)
|
||||||
|
print(f' {degrees(inner_via_angle):.1f} deg / via', file=sys.stderr)
|
||||||
|
print(f'outer via ring @ {outer_via_ring_radius:.2f} mm (from {end_radius:.2f} mm)', file=sys.stderr)
|
||||||
|
print(f' {degrees(outer_via_angle):.1f} deg / via', file=sys.stderr)
|
||||||
|
|
||||||
|
for n in range(turns-1):
|
||||||
|
layer_n = n % len(target_layers)
|
||||||
|
layer = target_layers[layer_n]
|
||||||
|
layer_turn = floor(n / len(target_layers))
|
||||||
|
print(f' {layer_n=} {layer_turn=}', file=sys.stderr)
|
||||||
|
|
||||||
|
start_angle = sector_angle * (layer_turn - layer_n / len(target_layers))
|
||||||
|
end_angle = start_angle + (turns_per_layer + 1/len(target_layers)) * sector_angle
|
||||||
|
|
||||||
|
if layer_n % 2 == 1:
|
||||||
|
start_radius, end_radius = end_radius, start_radius
|
||||||
|
|
||||||
|
do_spiral(out_paths[layer_n], start_radius, end_radius, start_angle, end_angle, layer_n)
|
||||||
|
|
||||||
|
svg_file('/tmp/test.svg', svg_stuff, 100, 100, -50, -50)
|
||||||
|
|
||||||
|
if counter_clockwise:
|
||||||
|
for p in pads:
|
||||||
|
p.at.y = -p.at.y
|
||||||
|
|
||||||
|
for l in lines:
|
||||||
|
l.start.y = -l.start.y
|
||||||
|
l.end.y = -l.end.y
|
||||||
|
|
||||||
|
for a in arcs:
|
||||||
|
a.start.y = -a.start.y
|
||||||
|
a.end.y = -a.end.y
|
||||||
|
|
||||||
|
if footprint_name:
|
||||||
|
name = footprint_name
|
||||||
|
elif outfile:
|
||||||
|
name = outfile.stem,
|
||||||
|
else:
|
||||||
|
name = 'generated_coil'
|
||||||
|
|
||||||
|
if keepout_zone:
|
||||||
|
r = diameter/2 + keepout_margin
|
||||||
|
tol = 0.05 # mm
|
||||||
|
n = ceil(pi / acos(1 - tol/r))
|
||||||
|
pts = [(r*cos(a*2*pi/n), r*sin(a*2*pi/n)) for a in range(n)]
|
||||||
|
zones = [kicad_pr.Zone(layers=['*.Cu'],
|
||||||
|
hatch=kicad_pr.Hatch(),
|
||||||
|
filled_areas_thickness=False,
|
||||||
|
keepout=kicad_pr.ZoneKeepout(copperpour_allowed=False),
|
||||||
|
polygon=kicad_pr.ZonePolygon(pts=kicad_pr.PointList(xy=[kicad_pr.XYCoord(x=x, y=y) for x, y in pts])))]
|
||||||
|
else:
|
||||||
|
zones = []
|
||||||
|
|
||||||
|
fp = kicad_fp.Footprint(
|
||||||
|
name=name,
|
||||||
|
generator=kicad_fp.Atom('GerbonaraTwistedCoilGenV1'),
|
||||||
|
layer='F.Cu',
|
||||||
|
descr=f"{turns} turn {diameter:.2f} mm diameter twisted coil footprint, inductance approximately {L:.6f} µH. Generated by gerbonara'c Twisted Coil generator, version {__version__}.",
|
||||||
|
clearance=clearance,
|
||||||
|
zone_connect=0,
|
||||||
|
lines=lines,
|
||||||
|
arcs=arcs,
|
||||||
|
pads=pads,
|
||||||
|
zones=zones,
|
||||||
|
)
|
||||||
|
|
||||||
|
if clipboard:
|
||||||
|
try:
|
||||||
|
print(f'Running {copy[0]}.', file=sys.stderr)
|
||||||
|
proc = subprocess.Popen(copy, stdin=subprocess.PIPE, text=True)
|
||||||
|
proc.communicate(fp.serialize())
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f'Error: --clipboard requires the {copy[0]} and {paste[0]} utilities from {cliputil} to be installed.', file=sys.stderr)
|
||||||
|
elif not outfile:
|
||||||
|
print(fp.serialize())
|
||||||
|
else:
|
||||||
|
fp.write(outfile)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
generate()
|
Ładowanie…
Reference in New Issue