cli: Add kicad schematic svg rendering

This commit is contained in:
jaseg 2023-09-26 16:44:40 +02:00
parent 61e591b5b8
commit f711c1d91c
6 changed files with 97 additions and 41 deletions

View file

View 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):

View file

@ -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':

View file

@ -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

View file

@ -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)

View file

@ -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)''')