kopia lustrzana https://gitlab.com/gerbolyze/gerbonara
autoroute
rodzic
f711c1d91c
commit
cff22b9e08
|
@ -2,12 +2,10 @@
|
||||||
Library for handling KiCad's PCB files (`*.kicad_mod`).
|
Library for handling KiCad's PCB files (`*.kicad_mod`).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import math
|
import math
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dataclasses import field, KW_ONLY, fields
|
from dataclasses import field, KW_ONLY, fields
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from collections import defaultdict
|
|
||||||
import re
|
import re
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import functools
|
import functools
|
||||||
|
@ -352,6 +350,7 @@ class Board:
|
||||||
b.__after_parse__(None)
|
b.__after_parse__(None)
|
||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
def init_default_layers(self, inner_layers=0):
|
def init_default_layers(self, inner_layers=0):
|
||||||
inner = [(i, f'In{i}.Cu', 'signal', None) for i in range(1, inner_layers+1)]
|
inner = [(i, f'In{i}.Cu', 'signal', None) for i in range(1, inner_layers+1)]
|
||||||
self.layers = [LayerSettings(idx, name, Atom(ltype)) for idx, name, ltype, cname in [
|
self.layers = [LayerSettings(idx, name, Atom(ltype)) for idx, name, ltype, cname in [
|
||||||
|
@ -386,12 +385,13 @@ class Board:
|
||||||
(57, 'User.8', 'user', None),
|
(57, 'User.8', 'user', None),
|
||||||
(58, 'User.9', 'user', None)]]
|
(58, 'User.9', 'user', None)]]
|
||||||
|
|
||||||
|
|
||||||
def rebuild_trace_index(self):
|
def rebuild_trace_index(self):
|
||||||
idx = self._trace_index = rtree.index.Index()
|
idx = self._trace_index = rtree.index.Index()
|
||||||
id_map = self._trace_index_map = {}
|
id_map = self._trace_index_map = {}
|
||||||
for obj in chain(self.track_segments, self.track_arcs):
|
for obj in chain(self.track_segments, self.track_arcs):
|
||||||
for field in ('start', 'end'):
|
for i, field in enumerate(('start', 'end')):
|
||||||
obj_id = id(obj)
|
obj_id = id(obj) + i
|
||||||
coord = getattr(obj, field)
|
coord = getattr(obj, field)
|
||||||
id_map[obj_id] = obj, field, obj.width, obj.layer_mask
|
id_map[obj_id] = obj, field, obj.width, obj.layer_mask
|
||||||
idx.insert(obj_id, (coord.x, coord.y, coord.x, coord.y))
|
idx.insert(obj_id, (coord.x, coord.y, coord.x, coord.y))
|
||||||
|
@ -407,6 +407,7 @@ class Board:
|
||||||
id_map[obj_id] = via, 'at', via.size, via.layer_mask
|
id_map[obj_id] = via, 'at', via.size, via.layer_mask
|
||||||
idx.insert(obj_id, (via.at.x, via.at.y, via.at.x, via.at.y))
|
idx.insert(obj_id, (via.at.x, via.at.y, via.at.x, via.at.y))
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _require_trace_index(fun):
|
def _require_trace_index(fun):
|
||||||
@functools.wraps(fun)
|
@functools.wraps(fun)
|
||||||
|
@ -417,6 +418,7 @@ class Board:
|
||||||
return fun(self, *args, **kwargs)
|
return fun(self, *args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
@_require_trace_index
|
@_require_trace_index
|
||||||
def query_trace_index_nearest(self, point, layers='*.Cu', n=1):
|
def query_trace_index_nearest(self, point, layers='*.Cu', n=1):
|
||||||
layers = layer_mask(layers)
|
layers = layer_mask(layers)
|
||||||
|
@ -427,10 +429,10 @@ class Board:
|
||||||
if layers & mask:
|
if layers & mask:
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
|
|
||||||
@_require_trace_index
|
@_require_trace_index
|
||||||
def query_trace_index_tolerance(self, point, layers='*.Cu', tol=10e-6):
|
def query_trace_index_tolerance(self, point, layers='*.Cu', tol=10e-6):
|
||||||
layers = layer_mask(layers)
|
layers = layer_mask(layers)
|
||||||
print(f'query {layers:08x}', file=sys.stderr)
|
|
||||||
|
|
||||||
x, y = point
|
x, y = point
|
||||||
for obj_id in self._trace_index.intersection((x-tol, y-tol, x+tol, y+tol)):
|
for obj_id in self._trace_index.intersection((x-tol, y-tol, x+tol, y+tol)):
|
||||||
|
@ -439,6 +441,7 @@ class Board:
|
||||||
if layers & mask and math.dist((attr.x, attr.y), (x, y)) <= tol:
|
if layers & mask and math.dist((attr.x, attr.y), (x, y)) <= tol:
|
||||||
yield entry
|
yield entry
|
||||||
|
|
||||||
|
|
||||||
def find_connected_traces(self, obj, layers='*.Cu', tol=10e-6):
|
def find_connected_traces(self, obj, layers='*.Cu', tol=10e-6):
|
||||||
search_frontier = []
|
search_frontier = []
|
||||||
visited = set()
|
visited = set()
|
||||||
|
@ -463,101 +466,31 @@ class Board:
|
||||||
raise TypeError(f'Finding connected traces for {type(obj)} objects is not (yet) supported.')
|
raise TypeError(f'Finding connected traces for {type(obj)} objects is not (yet) supported.')
|
||||||
|
|
||||||
enqueue(obj)
|
enqueue(obj)
|
||||||
|
yield obj
|
||||||
|
|
||||||
filter_layers = layer_mask(layers)
|
filter_layers = layer_mask(layers)
|
||||||
while search_frontier:
|
while search_frontier:
|
||||||
coord, size, layers = search_frontier.pop()
|
coord, size, layers = search_frontier.pop()
|
||||||
x, y = coord.x, coord.y
|
x, y = coord.x, coord.y
|
||||||
|
|
||||||
for cand in self.find_conductors_at(x, y, layers & filter_layers, size):
|
# First, find all bounding box intersections
|
||||||
if id(cand) not in visited:
|
found = []
|
||||||
|
for cand, attr, cand_size, cand_mask in self.query_trace_index_tolerance((x, y), layers&filter_layers, size):
|
||||||
|
cand_coord = getattr(cand, attr)
|
||||||
|
dist = math.dist((x, y), (cand_coord.x, cand_coord.y))
|
||||||
|
if dist <= size/2 + cand_size/2 and layers&cand_mask:
|
||||||
|
found.append((dist, cand))
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Second, filter to match only objects that are within tolerance of closest
|
||||||
|
min_dist = min(e[0] for e in found)
|
||||||
|
for dist, cand in found:
|
||||||
|
if dist < min_dist+tol and id(cand) not in visited:
|
||||||
enqueue(cand)
|
enqueue(cand)
|
||||||
yield cand
|
yield cand
|
||||||
|
|
||||||
def track_skeleton(self, start, tol=10e-6):
|
|
||||||
search_frontier = []
|
|
||||||
def enqueue(obj, arr):
|
|
||||||
if isinstance(obj, (TrackSegment, TrackArc)):
|
|
||||||
search_frontier.append((id(obj), arr, False, obj.start, obj.width, obj.layer_mask))
|
|
||||||
search_frontier.append((id(obj), arr, False, obj.end, obj.width, obj.layer_mask))
|
|
||||||
|
|
||||||
elif isinstance(obj, Via):
|
|
||||||
search_frontier.append((id(obj), arr, True, obj.at, obj.size, obj.layer_mask))
|
|
||||||
|
|
||||||
elif isinstance(obj, Pad):
|
|
||||||
search_frontier.append((id(obj), arr, True, obj.at, max(obj.size.x, obj.size.y), obj.layer_mask))
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise TypeError(f'Track skeleton starting at {type(obj)} objects is not (yet) supported.')
|
|
||||||
|
|
||||||
first_edge = []
|
|
||||||
enqueue(start, first_edge)
|
|
||||||
nodes = {id(start): 1}
|
|
||||||
edges = {1: [first_edge]}
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
while search_frontier:
|
|
||||||
obj_id, edge, force_node, coord, size, layers = search_frontier.pop()
|
|
||||||
print(f'current entry {obj_id} {force_node} {coord} {size} {layers:08x}', file=sys.stderr)
|
|
||||||
x, y = coord.x, coord.y
|
|
||||||
|
|
||||||
candidates = [cand for cand in self.find_conductors_at(x, y, layers, size) if id(cand) != obj_id]
|
|
||||||
|
|
||||||
if force_node or len(candidates) > 1:
|
|
||||||
for cand in candidates:
|
|
||||||
if node_id := nodes.get(id(cand)):
|
|
||||||
edge.append(node_id)
|
|
||||||
break
|
|
||||||
|
|
||||||
else:
|
|
||||||
node_id = nodes[obj_id] = len(nodes) + 1
|
|
||||||
edge.append(node_id)
|
|
||||||
edges[node_id] = arrs = []
|
|
||||||
for cand in candidates:
|
|
||||||
a = [cand]
|
|
||||||
arrs.append(a)
|
|
||||||
enqueue(cand, a)
|
|
||||||
|
|
||||||
elif len(candidates) == 1:
|
|
||||||
next_obj, = candidates
|
|
||||||
edge.append(next_obj)
|
|
||||||
if id(next_obj) not in nodes:
|
|
||||||
enqueue(next_obj, edge)
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
print(f'~ Step {i}', file=sys.stderr)
|
|
||||||
print(f'~ Candidates:', file=sys.stderr)
|
|
||||||
for e in candidates:
|
|
||||||
print(f'~ {e}', file=sys.stderr)
|
|
||||||
|
|
||||||
print(f'~ Nodes:', file=sys.stderr)
|
|
||||||
for k, v in nodes.items():
|
|
||||||
print(f'~ {k} = {v}', file=sys.stderr)
|
|
||||||
|
|
||||||
print(f'~ Current edge:', file=sys.stderr)
|
|
||||||
for e in edge:
|
|
||||||
print(f'~ {e}', file=sys.stderr)
|
|
||||||
|
|
||||||
return nodes, edges
|
|
||||||
|
|
||||||
def find_conductors_at(self, x, y, layers, size, tol=1e-6):
|
|
||||||
# First, find all bounding box intersections
|
|
||||||
found = {}
|
|
||||||
for cand, attr, cand_size, cand_mask in self.query_trace_index_tolerance((x, y), layers, size):
|
|
||||||
cand_coord = getattr(cand, attr)
|
|
||||||
dist = math.dist((x, y), (cand_coord.x, cand_coord.y))
|
|
||||||
if dist <= size/2 + cand_size/2 and layers&cand_mask:
|
|
||||||
found[id(cand)] = dist, cand
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Second, filter to match only objects that are within tolerance of closest
|
|
||||||
min_dist = min(e[0] for e in found.values())
|
|
||||||
for dist, cand in found.values():
|
|
||||||
if dist < min_dist+tol:
|
|
||||||
yield cand
|
|
||||||
|
|
||||||
|
|
||||||
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}
|
||||||
|
@ -567,10 +500,12 @@ class Board:
|
||||||
|
|
||||||
self.nets = {net.index: net.name for net in self.nets}
|
self.nets = {net.index: net.name for net in self.nets}
|
||||||
|
|
||||||
|
|
||||||
def __before_sexp__(self):
|
def __before_sexp__(self):
|
||||||
self.properties = [Property(key, value) for key, value in self.properties.items()]
|
self.properties = [Property(key, value) for key, value in self.properties.items()]
|
||||||
self.nets = [Net(index, name) for index, name in self.nets.items()]
|
self.nets = [Net(index, name) for index, name in self.nets.items()]
|
||||||
|
|
||||||
|
|
||||||
def remove(self, obj):
|
def remove(self, obj):
|
||||||
match obj:
|
match obj:
|
||||||
case gr.Text():
|
case gr.Text():
|
||||||
|
@ -608,12 +543,14 @@ 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):
|
def remove_many(self, iterable):
|
||||||
iterable = {id(obj) for obj in iterable}
|
iterable = {id(obj) for obj in iterable}
|
||||||
for field in fields(self):
|
for field in fields(self):
|
||||||
if field.default_factory is list and field.name not in ('nets', 'properties'):
|
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])
|
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():
|
||||||
|
@ -652,6 +589,7 @@ class Board:
|
||||||
for elem in self.map_gn_cad(obj):
|
for elem in self.map_gn_cad(obj):
|
||||||
self.add(elem)
|
self.add(elem)
|
||||||
|
|
||||||
|
|
||||||
def map_gn_cad(self, obj, locked=False, net_name=None):
|
def map_gn_cad(self, obj, locked=False, net_name=None):
|
||||||
match obj:
|
match obj:
|
||||||
case cad_pr.Trace():
|
case cad_pr.Trace():
|
||||||
|
@ -703,10 +641,12 @@ class Board:
|
||||||
v=Atom(v_align) if v_align != 'middle' else None,
|
v=Atom(v_align) if v_align != 'middle' else None,
|
||||||
mirror=flip)))
|
mirror=flip)))
|
||||||
|
|
||||||
|
|
||||||
def unfill_zones(self):
|
def unfill_zones(self):
|
||||||
for zone in self.zones:
|
for zone in self.zones:
|
||||||
zone.unfill()
|
zone.unfill()
|
||||||
|
|
||||||
|
|
||||||
def find_pads(self, net=None):
|
def find_pads(self, net=None):
|
||||||
for fp in self.footprints:
|
for fp in self.footprints:
|
||||||
for pad in fp.pads:
|
for pad in fp.pads:
|
||||||
|
@ -714,6 +654,7 @@ class Board:
|
||||||
continue
|
continue
|
||||||
yield pad
|
yield pad
|
||||||
|
|
||||||
|
|
||||||
def find_footprints(self, value=None, reference=None, name=None, net=None, sheetname=None, sheetfile=None):
|
def find_footprints(self, value=None, reference=None, name=None, net=None, sheetname=None, sheetfile=None):
|
||||||
for fp in self.footprints:
|
for fp in self.footprints:
|
||||||
if name and not match_filter(name, fp.name):
|
if name and not match_filter(name, fp.name):
|
||||||
|
@ -730,6 +671,7 @@ class Board:
|
||||||
continue
|
continue
|
||||||
yield fp
|
yield fp
|
||||||
|
|
||||||
|
|
||||||
def find_traces(self, net=None, include_vias=True):
|
def find_traces(self, net=None, include_vias=True):
|
||||||
net_id = self.net_id(net, create=False)
|
net_id = self.net_id(net, create=False)
|
||||||
match = lambda obj: obj.net == net_id
|
match = lambda obj: obj.net == net_id
|
||||||
|
@ -737,34 +679,42 @@ class Board:
|
||||||
if obj.net == net_id:
|
if obj.net == net_id:
|
||||||
yield obj
|
yield obj
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self):
|
def version(self):
|
||||||
return self._version
|
return self._version
|
||||||
|
|
||||||
|
|
||||||
@version.setter
|
@version.setter
|
||||||
def version(self, value):
|
def version(self, value):
|
||||||
if value not in SUPPORTED_FILE_FORMAT_VERSIONS:
|
if value not in SUPPORTED_FILE_FORMAT_VERSIONS:
|
||||||
raise FormatError(f'File format version {value} is not supported. Supported versions are {", ".join(map(str, SUPPORTED_FILE_FORMAT_VERSIONS))}.')
|
raise FormatError(f'File format version {value} is not supported. Supported versions are {", ".join(map(str, SUPPORTED_FILE_FORMAT_VERSIONS))}.')
|
||||||
|
|
||||||
|
|
||||||
def write(self, filename=None):
|
def write(self, filename=None):
|
||||||
with open(filename or self.original_filename, 'w') as f:
|
with open(filename or self.original_filename, 'w') as f:
|
||||||
f.write(self.serialize())
|
f.write(self.serialize())
|
||||||
|
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
return build_sexp(sexp(type(self), self)[0])
|
return build_sexp(sexp(type(self), self)[0])
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def open(kls, pcb_file, *args, **kwargs):
|
def open(kls, pcb_file, *args, **kwargs):
|
||||||
return kls.load(Path(pcb_file).read_text(), *args, **kwargs, original_filename=pcb_file)
|
return kls.load(Path(pcb_file).read_text(), *args, **kwargs, original_filename=pcb_file)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(kls, data, *args, **kwargs):
|
def load(kls, data, *args, **kwargs):
|
||||||
return kls.parse(data, *args, **kwargs)
|
return kls.parse(data, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def single_sided(self):
|
def single_sided(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def net_id(self, name, create=True):
|
def net_id(self, name, create=True):
|
||||||
if name is None:
|
if name is None:
|
||||||
return None
|
return None
|
||||||
|
@ -781,6 +731,7 @@ class Board:
|
||||||
else:
|
else:
|
||||||
raise IndexError(f'No such net: "{name}"')
|
raise IndexError(f'No such net: "{name}"')
|
||||||
|
|
||||||
|
|
||||||
# FIXME vvv
|
# FIXME vvv
|
||||||
def graphic_objects(self, text=False, images=False):
|
def graphic_objects(self, text=False, images=False):
|
||||||
return chain(
|
return chain(
|
||||||
|
|
|
@ -14,6 +14,7 @@ from gerbonara.cad.kicad import footprints as kicad_fp
|
||||||
from gerbonara.cad.kicad import graphical_primitives as kicad_gr
|
from gerbonara.cad.kicad import graphical_primitives as kicad_gr
|
||||||
from gerbonara.cad.kicad import primitives as kicad_pr
|
from gerbonara.cad.kicad import primitives as kicad_pr
|
||||||
from gerbonara.utils import Tag
|
from gerbonara.utils import Tag
|
||||||
|
from gerbonara import graphic_primitives as gp
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,6 +47,95 @@ def angle_between_vectors(va, vb):
|
||||||
angle += 2*pi
|
angle += 2*pi
|
||||||
return angle
|
return angle
|
||||||
|
|
||||||
|
|
||||||
|
def traces_to_gmsh(traces, mesh_out, bbox, model_name='gerbonara_board', log=True, copper_thickness=35e-6, board_thickness=0.8, air_box_margin=5.0):
|
||||||
|
import gmsh
|
||||||
|
occ = gmsh.model.occ
|
||||||
|
eps = 1e-6
|
||||||
|
|
||||||
|
board_thickness -= 2*copper_thickness
|
||||||
|
|
||||||
|
gmsh.initialize()
|
||||||
|
gmsh.model.add('gerbonara_board')
|
||||||
|
if log:
|
||||||
|
gmsh.logger.start()
|
||||||
|
|
||||||
|
trace_tags = {}
|
||||||
|
trace_ends = set()
|
||||||
|
render_cache = {}
|
||||||
|
for i, tr in enumerate(traces):
|
||||||
|
layer = tr[1].layer
|
||||||
|
z0 = 0 if layer == 'F.Cu' else -(board_thickness+copper_thickness)
|
||||||
|
|
||||||
|
prims = [prim
|
||||||
|
for elem in tr
|
||||||
|
for obj in elem.render(cache=render_cache)
|
||||||
|
for prim in obj.to_primitives()]
|
||||||
|
|
||||||
|
tags = []
|
||||||
|
for prim in prims:
|
||||||
|
if isinstance(prim, gp.Line):
|
||||||
|
length = dist((prim.x1, prim.y1), (prim.x2, prim.y2))
|
||||||
|
box_tag = occ.addBox(0, -prim.width/2, 0, length, prim.width, copper_thickness)
|
||||||
|
angle = atan2(prim.y2 - prim.y1, prim.x2 - prim.x1)
|
||||||
|
occ.rotate([(3, box_tag)], 0, 0, 0, 0, 0, 1, angle)
|
||||||
|
occ.translate([(3, box_tag)], prim.x1, prim.y1, z0)
|
||||||
|
tags.append(box_tag)
|
||||||
|
|
||||||
|
for x, y in ((prim.x1, prim.y1), (prim.x2, prim.y2)):
|
||||||
|
disc_id = (round(x, 3), round(y, 3), round(z0, 3), round(prim.width, 3))
|
||||||
|
if disc_id in trace_ends:
|
||||||
|
continue
|
||||||
|
|
||||||
|
trace_ends.add(disc_id)
|
||||||
|
cylinder_tag = occ.addCylinder(x, y, z0, 0, 0, copper_thickness, prim.width/2)
|
||||||
|
tags.append(cylinder_tag)
|
||||||
|
print('fusing', tags)
|
||||||
|
tags, tag_map = occ.fuse([(3, tags[0])], [(3, tag) for tag in tags[1:]])
|
||||||
|
print(tags)
|
||||||
|
assert len(tags) == 1
|
||||||
|
(_dim, tag), = tags
|
||||||
|
trace_tags[i] = tag
|
||||||
|
|
||||||
|
(x1, y1), (x2, y2) = bbox
|
||||||
|
substrate = occ.addBox(x1, y1, -board_thickness, x2-x1, y2-y1, board_thickness)
|
||||||
|
|
||||||
|
x1, y1 = x1-air_box_margin, y1-air_box_margin
|
||||||
|
x2, y2 = x2+air_box_margin, y2+air_box_margin
|
||||||
|
w, d = x2-x1, y2-y1
|
||||||
|
z0 = -board_thickness-air_box_margin
|
||||||
|
ab_h = board_thickness + 2*air_box_margin
|
||||||
|
airbox = occ.addBox(x1, y1, z0, w, d, ab_h)
|
||||||
|
|
||||||
|
print('Cutting airbox')
|
||||||
|
occ.cut([(3, airbox)], [(3, tag) for tag in trace_tags.values()], removeObject=True, removeTool=False)
|
||||||
|
print('Fragmenting')
|
||||||
|
fragment_tags, fragment_hierarchy = occ.fragment([(3, airbox)], [(3, substrate)] + [(3, tag) for tag in trace_tags.values()])
|
||||||
|
|
||||||
|
print('Synchronizing')
|
||||||
|
occ.synchronize()
|
||||||
|
substrate_physical = gmsh.model.add_physical_group(3, [substrate], name='substrate')
|
||||||
|
airbox_physical = gmsh.model.add_physical_group(3, [airbox], name='airbox')
|
||||||
|
trace_physical_surfaces = [
|
||||||
|
gmsh.model.add_physical_group(2, list(gmsh.model.getAdjacencies(3, tag)[1]), name=f'trace{i}')
|
||||||
|
for i, tag in trace_tags.items()]
|
||||||
|
|
||||||
|
airbox_adjacent = set(gmsh.model.getAdjacencies(3, airbox)[1])
|
||||||
|
in_bbox = {tag for _dim, tag in gmsh.model.getEntitiesInBoundingBox(x1+eps, y1+eps, z0+eps, x1+w-eps, y1+d-eps, z0+ab_h-eps, dim=22)}
|
||||||
|
airbox_physical_surface = gmsh.model.add_physical_group(2, list(airbox_adjacent - in_bbox), name='airbox_surface')
|
||||||
|
|
||||||
|
gmsh.option.setNumber('Mesh.MeshSizeFromCurvature', 90)
|
||||||
|
gmsh.option.setNumber('Mesh.Smoothing', 10)
|
||||||
|
gmsh.option.setNumber('Mesh.Algorithm3D', 10)
|
||||||
|
gmsh.option.setNumber('Mesh.MeshSizeMax', 0.2e-3)
|
||||||
|
gmsh.option.setNumber('General.NumThreads', 12)
|
||||||
|
|
||||||
|
print('Meshing')
|
||||||
|
gmsh.model.mesh.generate(dim=3)
|
||||||
|
print('Writing')
|
||||||
|
gmsh.write(str(mesh_out))
|
||||||
|
|
||||||
|
|
||||||
class SVGPath:
|
class SVGPath:
|
||||||
def __init__(self, **attrs):
|
def __init__(self, **attrs):
|
||||||
self.d = ''
|
self.d = ''
|
||||||
|
@ -143,12 +233,13 @@ def print_valid_twists(ctx, param, value):
|
||||||
@click.option('--show-twists', callback=print_valid_twists, expose_value=False, type=int, is_eager=True, help='Calculate and show valid --twists counts for the given number of turns. Takes the number of turns as a value.')
|
@click.option('--show-twists', callback=print_valid_twists, expose_value=False, type=int, is_eager=True, help='Calculate and show valid --twists counts for the given number of turns. Takes the number of turns as a value.')
|
||||||
@click.option('--clearance', type=float, default=None)
|
@click.option('--clearance', type=float, default=None)
|
||||||
@click.option('--arc-tolerance', type=float, default=0.02)
|
@click.option('--arc-tolerance', type=float, default=0.02)
|
||||||
|
@click.option('--mesh-out', type=click.Path(writable=True, dir_okay=False, path_type=Path))
|
||||||
@click.option('--clipboard/--no-clipboard', help='Use clipboard integration (requires wl-clipboard)')
|
@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.')
|
@click.option('--counter-clockwise/--clockwise', help='Direction of generated spiral. Default: clockwise when wound from the inside.')
|
||||||
@click.version_option()
|
@click.version_option()
|
||||||
def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_drill, via_offset, trace_width, clearance,
|
def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_drill, via_offset, trace_width, clearance,
|
||||||
footprint_name, layer_pair, twists, clipboard, counter_clockwise, keepout_zone, keepout_margin,
|
footprint_name, layer_pair, twists, clipboard, counter_clockwise, keepout_zone, keepout_margin,
|
||||||
arc_tolerance, pcb):
|
arc_tolerance, pcb, mesh_out):
|
||||||
if 'WAYLAND_DISPLAY' in os.environ:
|
if 'WAYLAND_DISPLAY' in os.environ:
|
||||||
copy, paste, cliputil = ['wl-copy'], ['wl-paste'], 'xclip'
|
copy, paste, cliputil = ['wl-copy'], ['wl-paste'], 'xclip'
|
||||||
else:
|
else:
|
||||||
|
@ -157,6 +248,9 @@ def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_d
|
||||||
if gcd(twists, turns) != 1:
|
if gcd(twists, turns) != 1:
|
||||||
raise click.ClickException('For the geometry to work out, the --twists parameter must be co-prime to --turns, i.e. the two must have 1 as their greatest common divisor. You can print valid values for --twists by running this command with --show-twists [turns number].')
|
raise click.ClickException('For the geometry to work out, the --twists parameter must be co-prime to --turns, i.e. the two must have 1 as their greatest common divisor. You can print valid values for --twists by running this command with --show-twists [turns number].')
|
||||||
|
|
||||||
|
if mesh_out and not pcb:
|
||||||
|
raise click.ClickException('--pcb is required when --mesh-out is used.')
|
||||||
|
|
||||||
outer_radius = outer_diameter/2
|
outer_radius = outer_diameter/2
|
||||||
inner_radius = inner_diameter/2
|
inner_radius = inner_diameter/2
|
||||||
turns_per_layer = turns/2
|
turns_per_layer = turns/2
|
||||||
|
@ -326,13 +420,13 @@ def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_d
|
||||||
xn, yn = x0, y0
|
xn, yn = x0, y0
|
||||||
points = [(x0, y0)]
|
points = [(x0, y0)]
|
||||||
dists = []
|
dists = []
|
||||||
for i in range(fn+1):
|
for i in range(fn):
|
||||||
r, g, b, _a = mpl.cm.plasma(start_frac + (end_frac - start_frac)/fn * (i + 0.5))
|
r, g, b, _a = mpl.cm.plasma(start_frac + (end_frac - start_frac)/fn * (i + 0.5))
|
||||||
path = SVGPath(fill='none', stroke=f'#{round(r*255):02x}{round(g*255):02x}{round(b*255):02x}', stroke_width=trace_width, stroke_linejoin='round', stroke_linecap='round')
|
path = SVGPath(fill='none', stroke=f'#{round(r*255):02x}{round(g*255):02x}{round(b*255):02x}', stroke_width=trace_width, stroke_linejoin='round', stroke_linecap='round')
|
||||||
svg_stuff.append(path)
|
svg_stuff.append(path)
|
||||||
xp, yp = xn, yn
|
xp, yp = xn, yn
|
||||||
r = r1 + i*(r2-r1)/fn
|
r = r1 + (i+1)*(r2-r1)/fn
|
||||||
a = a1 + i*(a2-a1)/fn
|
a = a1 + (i+1)*(a2-a1)/fn
|
||||||
xn, yn = cos(a)*r, sin(a)*r
|
xn, yn = cos(a)*r, sin(a)*r
|
||||||
path.move(xp, yp)
|
path.move(xp, yp)
|
||||||
path.line(xn, yn)
|
path.line(xn, yn)
|
||||||
|
@ -358,7 +452,6 @@ def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_d
|
||||||
|
|
||||||
inverse = {}
|
inverse = {}
|
||||||
for i in range(twists):
|
for i in range(twists):
|
||||||
#print(i, i*turns % twists, file=sys.stderr)
|
|
||||||
inverse[i*turns%twists] = i
|
inverse[i*turns%twists] = i
|
||||||
|
|
||||||
svg_vias = []
|
svg_vias = []
|
||||||
|
@ -373,7 +466,7 @@ def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_d
|
||||||
|
|
||||||
xv, yv = inner_via_ring_radius*cos(fold_angle), inner_via_ring_radius*sin(fold_angle)
|
xv, yv = inner_via_ring_radius*cos(fold_angle), inner_via_ring_radius*sin(fold_angle)
|
||||||
pads.append(make_via(xv, yv, layer_pair))
|
pads.append(make_via(xv, yv, layer_pair))
|
||||||
if via_offset > 0:
|
if not isclose(via_offset, 0, abs_tol=1e-6):
|
||||||
lines.append(make_line(xn, yn, xv, yv, layer_pair[0]))
|
lines.append(make_line(xn, yn, xv, yv, layer_pair[0]))
|
||||||
lines.append(make_line(xn, yn, xv, yv, layer_pair[1]))
|
lines.append(make_line(xn, yn, xv, yv, layer_pair[1]))
|
||||||
svg_vias.append(Tag('circle', cx=xv, cy=yv, r=via_diameter/2, stroke='none', fill='white'))
|
svg_vias.append(Tag('circle', cx=xv, cy=yv, r=via_diameter/2, stroke='none', fill='white'))
|
||||||
|
@ -382,7 +475,7 @@ def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_d
|
||||||
if i > 0:
|
if i > 0:
|
||||||
xv, yv = outer_via_ring_radius*cos(start_angle), outer_via_ring_radius*sin(start_angle)
|
xv, yv = outer_via_ring_radius*cos(start_angle), outer_via_ring_radius*sin(start_angle)
|
||||||
pads.append(make_via(xv, yv, layer_pair))
|
pads.append(make_via(xv, yv, layer_pair))
|
||||||
if via_offset > 0:
|
if not isclose(via_offset, 0, abs_tol=1e-6):
|
||||||
lines.append(make_line(x0, y0, xv, yv, layer_pair[0]))
|
lines.append(make_line(x0, y0, xv, yv, layer_pair[0]))
|
||||||
lines.append(make_line(x0, y0, xv, yv, layer_pair[1]))
|
lines.append(make_line(x0, y0, xv, yv, layer_pair[1]))
|
||||||
svg_vias.append(Tag('circle', cx=xv, cy=yv, r=via_diameter/2, stroke='none', fill='white'))
|
svg_vias.append(Tag('circle', cx=xv, cy=yv, r=via_diameter/2, stroke='none', fill='white'))
|
||||||
|
@ -390,8 +483,10 @@ def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_d
|
||||||
|
|
||||||
print(f'Approximate track length: {clen*twists*2:.2f} mm', file=sys.stderr)
|
print(f'Approximate track length: {clen*twists*2:.2f} mm', file=sys.stderr)
|
||||||
|
|
||||||
pads.append(make_pad(1, [layer_pair[0]], outer_radius, 0))
|
top_pad = make_pad(1, [layer_pair[0]], outer_radius, 0)
|
||||||
pads.append(make_pad(2, [layer_pair[1]], outer_radius, 0))
|
pads.append(top_pad)
|
||||||
|
bottom_pad = make_pad(2, [layer_pair[1]], outer_radius, 0)
|
||||||
|
pads.append(bottom_pad)
|
||||||
|
|
||||||
svg_stuff += svg_vias
|
svg_stuff += svg_vias
|
||||||
|
|
||||||
|
@ -461,8 +556,38 @@ def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_d
|
||||||
vias=[kicad_pcb.Via.from_pad(pad) for pad in pads if pad.type == kicad_pcb.Atom.thru_hole])
|
vias=[kicad_pcb.Via.from_pad(pad) for pad in pads if pad.type == kicad_pcb.Atom.thru_hole])
|
||||||
obj.rebuild_trace_index()
|
obj.rebuild_trace_index()
|
||||||
seg = obj.track_segments[-1]
|
seg = obj.track_segments[-1]
|
||||||
for e in obj.find_connected_traces(seg, layers=seg.layer_mask):
|
traces = []
|
||||||
print(getattr(e, 'layer', ''), str(e)[:80], file=sys.stderr)
|
end = top_pad
|
||||||
|
layer = 'F.Cu'
|
||||||
|
while True:
|
||||||
|
tr = list(obj.find_connected_traces(end, layers=[layer]))
|
||||||
|
traces.append(tr)
|
||||||
|
if not isinstance(tr[-1], kicad_pcb.Via):
|
||||||
|
break
|
||||||
|
layer = 'B.Cu' if layer == 'F.Cu' else 'F.Cu'
|
||||||
|
end = tr[-1]
|
||||||
|
# remove start pad
|
||||||
|
traces[0] = traces[0][1:]
|
||||||
|
|
||||||
|
r = outer_diameter/2 + 20
|
||||||
|
traces_to_gmsh(traces, mesh_out, ((-r, -r), (r, r)))
|
||||||
|
|
||||||
|
# for trace in traces:
|
||||||
|
# print(f'Trace {i}', file=sys.stderr)
|
||||||
|
# print(f' Length: {len(trace)}', file=sys.stderr)
|
||||||
|
# print(f' Start: {trace[0]}', file=sys.stderr)
|
||||||
|
# print(f' End: {trace[-1]}', file=sys.stderr)
|
||||||
|
# print(f' Layer: {trace[1].layer}', file=sys.stderr)
|
||||||
|
|
||||||
|
#for e in obj.find_connected_traces(seg, layers=seg.layer_mask):
|
||||||
|
# print(getattr(e, 'layer', ''), str(e)[:80], file=sys.stderr)
|
||||||
|
#nodes, edges = obj.track_skeleton(pads[-1])
|
||||||
|
#for node, node_edges in edges.items():
|
||||||
|
# print(f'Node {node} with {len(node_edges)} edges', file=sys.stderr)
|
||||||
|
# for i, e in enumerate(node_edges):
|
||||||
|
# print(f' Edge {i}', file=sys.stderr)
|
||||||
|
# for elem in e:
|
||||||
|
# print(' ', elem, file=sys.stderr)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
obj = kicad_fp.Footprint(
|
obj = kicad_fp.Footprint(
|
||||||
|
|
Ładowanie…
Reference in New Issue