summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2023-09-26 16:44:40 +0200
committerjaseg <git@jaseg.de>2023-09-26 16:44:40 +0200
commitf711c1d91c3810283562957de86b4327cf97e7e0 (patch)
treef38370badb23bf87e51ee6f0a8ba0cdfd2bd5d69
parent61e591b5b811438ff36e4347f94c6c89f222b7b7 (diff)
downloadgerbonara-f711c1d91c3810283562957de86b4327cf97e7e0.tar.gz
gerbonara-f711c1d91c3810283562957de86b4327cf97e7e0.tar.bz2
gerbonara-f711c1d91c3810283562957de86b4327cf97e7e0.zip
cli: Add kicad schematic svg rendering
-rw-r--r--gerbonara/cad/kicad/__init__.py0
-rw-r--r--gerbonara/cad/kicad/pcb.py113
-rw-r--r--gerbonara/cad/kicad/primitives.py2
-rw-r--r--gerbonara/cad/kicad/schematic.py1
-rw-r--r--gerbonara/cad/kicad/symbols.py1
-rw-r--r--gerbonara/cli.py23
6 files changed, 98 insertions, 42 deletions
diff --git a/gerbonara/cad/kicad/__init__.py b/gerbonara/cad/kicad/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/gerbonara/cad/kicad/__init__.py
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)''')