cli: Add kicad schematic svg rendering
This commit is contained in:
parent
61e591b5b8
commit
f711c1d91c
6 changed files with 97 additions and 41 deletions
0
gerbonara/cad/kicad/__init__.py
Normal file
0
gerbonara/cad/kicad/__init__.py
Normal file
|
|
@ -2,10 +2,12 @@
|
|||
Library for handling KiCad's PCB files (`*.kicad_mod`).
|
||||
"""
|
||||
|
||||
import sys
|
||||
import math
|
||||
from pathlib import Path
|
||||
from dataclasses import field, KW_ONLY, fields
|
||||
from itertools import chain
|
||||
from collections import defaultdict
|
||||
import re
|
||||
import fnmatch
|
||||
import functools
|
||||
|
|
@ -428,6 +430,7 @@ class Board:
|
|||
@_require_trace_index
|
||||
def query_trace_index_tolerance(self, point, layers='*.Cu', tol=10e-6):
|
||||
layers = layer_mask(layers)
|
||||
print(f'query {layers:08x}', file=sys.stderr)
|
||||
|
||||
x, y = point
|
||||
for obj_id in self._trace_index.intersection((x-tol, y-tol, x+tol, y+tol)):
|
||||
|
|
@ -466,66 +469,94 @@ class Board:
|
|||
coord, size, layers = search_frontier.pop()
|
||||
x, y = coord.x, coord.y
|
||||
|
||||
# First, find all bounding box intersections
|
||||
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:
|
||||
for cand in self.find_conductors_at(x, y, layers & filter_layers, size):
|
||||
if id(cand) not in visited:
|
||||
enqueue(cand)
|
||||
yield cand
|
||||
|
||||
def track_skeleton(self, start, tol=10e-6):
|
||||
search_frontier = []
|
||||
visited = set()
|
||||
def enqueue(obj):
|
||||
visited.add(id(obj))
|
||||
|
||||
def enqueue(obj, arr):
|
||||
if isinstance(obj, (TrackSegment, TrackArc)):
|
||||
search_frontier.append((obj.start, obj.width, obj.layer_mask))
|
||||
search_frontier.append((obj.end, obj.width, obj.layer_mask))
|
||||
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((obj.at, obj.size, obj.layer_mask))
|
||||
search_frontier.append((id(obj), arr, True, obj.at, obj.size, obj.layer_mask))
|
||||
|
||||
elif isinstance(obj, Pad):
|
||||
search_frontier.append((obj.at, max(obj.size.x, obj.size.y), obj.layer_mask))
|
||||
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.')
|
||||
|
||||
enqueue(start)
|
||||
first_edge = []
|
||||
enqueue(start, first_edge)
|
||||
nodes = {id(start): 1}
|
||||
edges = {1: [first_edge]}
|
||||
|
||||
i = 0
|
||||
while search_frontier:
|
||||
coord, size, layers = search_frontier.pop()
|
||||
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
|
||||
|
||||
# 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.append((dist, cand))
|
||||
candidates = [cand for cand in self.find_conductors_at(x, y, layers, size) if id(cand) != obj_id]
|
||||
|
||||
if not found:
|
||||
continue
|
||||
if force_node or len(candidates) > 1:
|
||||
for cand in candidates:
|
||||
if node_id := nodes.get(id(cand)):
|
||||
edge.append(node_id)
|
||||
break
|
||||
|
||||
# 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)
|
||||
yield cand
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ def layer_mask(layers):
|
|||
for layer in layers:
|
||||
match layer:
|
||||
case '*.Cu':
|
||||
return 0xff
|
||||
return 0xffffffff
|
||||
case 'F.Cu':
|
||||
mask |= 1<<0
|
||||
case 'B.Cu':
|
||||
|
|
|
|||
|
|
@ -343,6 +343,7 @@ class SymbolInstance:
|
|||
at: AtPos = field(default_factory=AtPos)
|
||||
mirror: OmitDefault(MirrorFlags) = field(default_factory=MirrorFlags)
|
||||
unit: Named(int) = 1
|
||||
exclude_from_sim: Named(YesNoAtom()) = False
|
||||
in_bom: Named(YesNoAtom()) = True
|
||||
on_board: Named(YesNoAtom()) = True
|
||||
dnp: Named(YesNoAtom()) = True
|
||||
|
|
|
|||
|
|
@ -501,6 +501,7 @@ class Symbol:
|
|||
power: Wrap(Flag()) = False
|
||||
pin_numbers: OmitDefault(PinNumberSpec) = field(default_factory=PinNumberSpec)
|
||||
pin_names: OmitDefault(PinNameSpec) = field(default_factory=PinNameSpec)
|
||||
exclude_from_sim: Named(YesNoAtom()) = False
|
||||
in_bom: Named(YesNoAtom()) = True
|
||||
on_board: Named(YesNoAtom()) = True
|
||||
properties: List(Property) = field(default_factory=list)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ from .cam import FileSettings
|
|||
from .rs274x import GerberFile
|
||||
from . import layers as lyr
|
||||
from . import __version__
|
||||
from .cad.kicad import schematic as kc_schematic
|
||||
from .cad.kicad import tmtheme
|
||||
|
||||
|
||||
def _print_version(ctx, param, value):
|
||||
|
|
@ -129,6 +131,27 @@ def cli():
|
|||
pass
|
||||
|
||||
|
||||
@cli.group('kicad')
|
||||
def kicad_group():
|
||||
pass
|
||||
|
||||
|
||||
@kicad_group.group('schematic')
|
||||
def schematic_group():
|
||||
pass
|
||||
|
||||
|
||||
@schematic_group.command()
|
||||
@click.argument('inpath', type=click.Path(exists=True))
|
||||
@click.argument('theme', type=click.Path(exists=True))
|
||||
@click.argument('outfile', type=click.File('w'), default='-')
|
||||
def render(inpath, theme, outfile):
|
||||
sch = kc_schematic.Schematic.open(inpath)
|
||||
cs = tmtheme.TmThemeSchematic(Path(theme).read_text())
|
||||
with outfile as f:
|
||||
f.write(str(sch.to_svg(cs)))
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option('--warnings', 'format_warnings', type=click.Choice(['default', 'ignore', 'once']), default='default',
|
||||
help='''Enable or disable file format warnings during parsing (default: on)''')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue