From f711c1d91c3810283562957de86b4327cf97e7e0 Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 26 Sep 2023 16:44:40 +0200 Subject: cli: Add kicad schematic svg rendering --- gerbonara/cad/kicad/__init__.py | 0 gerbonara/cad/kicad/pcb.py | 113 ++++++++++++++++++++++++-------------- gerbonara/cad/kicad/primitives.py | 2 +- gerbonara/cad/kicad/schematic.py | 1 + gerbonara/cad/kicad/symbols.py | 1 + gerbonara/cli.py | 23 ++++++++ 6 files changed, 98 insertions(+), 42 deletions(-) create mode 100644 gerbonara/cad/kicad/__init__.py (limited to 'gerbonara') diff --git a/gerbonara/cad/kicad/__init__.py b/gerbonara/cad/kicad/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gerbonara/cad/kicad/pcb.py b/gerbonara/cad/kicad/pcb.py index 06023a5..0c018cf 100644 --- a/gerbonara/cad/kicad/pcb.py +++ b/gerbonara/cad/kicad/pcb.py @@ -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)) - - if not found: - continue + 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) - for dist, cand in found: - if dist < min_dist+tol and id(cand) not in visited: - enqueue(cand) - yield cand + # 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): diff --git a/gerbonara/cad/kicad/primitives.py b/gerbonara/cad/kicad/primitives.py index 2f24fb5..74ce4e4 100644 --- a/gerbonara/cad/kicad/primitives.py +++ b/gerbonara/cad/kicad/primitives.py @@ -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': diff --git a/gerbonara/cad/kicad/schematic.py b/gerbonara/cad/kicad/schematic.py index 390716e..45a022e 100644 --- a/gerbonara/cad/kicad/schematic.py +++ b/gerbonara/cad/kicad/schematic.py @@ -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 diff --git a/gerbonara/cad/kicad/symbols.py b/gerbonara/cad/kicad/symbols.py index 972dd55..a98fbeb 100644 --- a/gerbonara/cad/kicad/symbols.py +++ b/gerbonara/cad/kicad/symbols.py @@ -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) diff --git a/gerbonara/cli.py b/gerbonara/cli.py index 95be4db..d6b93f2 100644 --- a/gerbonara/cli.py +++ b/gerbonara/cli.py @@ -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)''') -- cgit