summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2023-07-22 18:29:04 +0200
committerjaseg <git@jaseg.de>2023-07-22 18:29:04 +0200
commit3e47e7c2dabc78650e228e5336da10d27bdf8dad (patch)
tree908c7c7e6defbce68f3be5f7b18502f0d1f3d74c
parentba4cafa3a43f3b6655e2cbfbc35d2505f0efccb1 (diff)
downloadgerbonara-3e47e7c2dabc78650e228e5336da10d27bdf8dad.tar.gz
gerbonara-3e47e7c2dabc78650e228e5336da10d27bdf8dad.tar.bz2
gerbonara-3e47e7c2dabc78650e228e5336da10d27bdf8dad.zip
Add line wonkifier
-rw-r--r--gerbonara/cad/kicad/schematic.py152
-rw-r--r--gerbonara/cad/kicad/tmtheme.py56
2 files changed, 187 insertions, 21 deletions
diff --git a/gerbonara/cad/kicad/schematic.py b/gerbonara/cad/kicad/schematic.py
index b267743..5f4e920 100644
--- a/gerbonara/cad/kicad/schematic.py
+++ b/gerbonara/cad/kicad/schematic.py
@@ -630,10 +630,153 @@ class Schematic:
return setup_svg(children, ((0, 0), (w, h)), pagecolor=colorscheme.background)
+
+# From: https://jakevdp.github.io/blog/2012/10/07/xkcd-style-plots-in-matplotlib/
+#def xkcd_line(x, y, xlim=None, ylim=None, mag=1.0, f1=30, f2=0.05, f3=15):
+def xkcd_line(x, y, xlim=None, ylim=None, mag=1.0, f1=10, f2=0.10, f3=5):
+ """
+ Mimic a hand-drawn line from (x, y) data
+
+ Parameters
+ ----------
+ x, y : array_like
+ arrays to be modified
+ xlim, ylim : data range
+ the assumed plot range for the modification. If not specified,
+ they will be guessed from the data
+ mag : float
+ magnitude of distortions
+ f1, f2, f3 : int, float, int
+ filtering parameters. f1 gives the size of the window, f2 gives
+ the high-frequency cutoff, f3 gives the size of the filter
+
+ Returns
+ -------
+ x, y : ndarrays
+ The modified lines
+ """
+ import numpy as np
+ from scipy import interpolate, signal
+
+ x = np.asarray(x)
+ y = np.asarray(y)
+
+ # get limits for rescaling
+ if xlim is None:
+ xlim = (x.min(), x.max())
+ if ylim is None:
+ ylim = (y.min(), y.max())
+
+ if xlim[1] == xlim[0]:
+ xlim = ylim
+
+ if ylim[1] == ylim[0]:
+ ylim = xlim
+
+ # scale the data
+ x_scaled = (x - xlim[0]) * 1. / (xlim[1] - xlim[0])
+ y_scaled = (y - ylim[0]) * 1. / (ylim[1] - ylim[0])
+
+ # compute the total distance along the path
+ dx = x_scaled[1:] - x_scaled[:-1]
+ dy = y_scaled[1:] - y_scaled[:-1]
+ dist_tot = np.sum(np.sqrt(dx * dx + dy * dy))
+
+ # number of interpolated points is proportional to the distance
+ Nu = int(50 * dist_tot)
+ u = np.arange(-1, Nu + 1) * 1. / (Nu - 1)
+
+ # interpolate curve at sampled points
+ k = min(3, len(x) - 1)
+ res = interpolate.splprep([x_scaled, y_scaled], s=0, k=k)
+ x_int, y_int = interpolate.splev(u, res[0])
+
+ # we'll perturb perpendicular to the drawn line
+ dx = x_int[2:] - x_int[:-2]
+ dy = y_int[2:] - y_int[:-2]
+ dist = np.sqrt(dx * dx + dy * dy)
+
+ # create a filtered perturbation
+ coeffs = mag * np.random.normal(0, 0.01, len(x_int) - 2)
+ b = signal.firwin(f1, f2 * dist_tot, window=('kaiser', f3))
+ response = signal.lfilter(b, 1, coeffs)
+
+ x_int[1:-1] += response * dy / dist
+ y_int[1:-1] += response * dx / dist
+
+ # un-scale data
+ x_int = x_int[1:-1] * (xlim[1] - xlim[0]) + xlim[0]
+ y_int = y_int[1:-1] * (ylim[1] - ylim[0]) + ylim[0]
+
+ return x_int, y_int
+
+def wonkify(path):
+ out = []
+ for segment in path.attrs['d'].split('M')[1:]:
+ if 'A' in segment:
+ out.append(segment)
+ continue
+
+ points = segment.split('L')
+ if points[-1].rstrip().endswith('Z'):
+ closed = True
+ points[-1] = points[-1].rstrip()[:-1].rstrip()
+ points.append(points[0])
+ else:
+ closed = False
+
+ pts = []
+ lx, ly = None, None
+ for pt in points:
+ x, y = pt.strip().split()
+ x, y = float(x), float(y)
+ if (x, y) == (lx, ly):
+ continue
+
+ lx, ly = x, y
+ pts.append((x, y))
+
+ if len(pts) == 2:
+ segs = [pts]
+
+ else:
+ seg = [pts[0]]
+ segs = [seg]
+ for p0, p1, p2 in zip(pts[0::], pts[1::], pts[2::]):
+ dx1, dy1 = p1[0] - p0[0], p1[1] - p0[1]
+ dx2, dy2 = p2[0] - p1[0], p2[1] - p1[1]
+ a1, a2 = math.atan2(dy1, dx1), math.atan2(dy2, dx2)
+ da = (a2 - a1 + math.pi) % (2*math.pi) - math.pi
+ if abs(da) > math.pi/4:
+ seg.append(p1)
+ seg = [p1, p2]
+ segs.append(seg)
+ seg.append(p1)
+ seg.append(p2)
+
+ for seg in segs:
+ xs, ys = [x for x, y in seg], [y for x, y in seg]
+ xs, ys = xkcd_line(xs, ys)
+ d = ' L '.join(f'{x:.3f} {y:.3f}' for x, y in zip(xs, ys))
+ if closed:
+ d += ' Z'
+ out.append(d)
+
+ path.attrs['d'] = ' '.join(f'M {seg}' for seg in out)
+
+
+def postprocess(tag):
+ if tag.name == 'path':
+ wonkify(tag)
+ else:
+ for child in tag.children:
+ postprocess(child)
+ return tag
+
if __name__ == '__main__':
import sys
from ...layers import LayerStack
- from .tmtheme import TmThemeSchematic
+ from .tmtheme import *
sch = Schematic.open(sys.argv[1])
print('Loaded schematic with', len(sch.wires), 'wires and', len(sch.symbols), 'symbols.')
for subsh in sch.subsheets:
@@ -641,5 +784,10 @@ if __name__ == '__main__':
print('Loaded sub-sheet with', len(subsh.wires), 'wires and', len(subsh.symbols), 'symbols.')
sch.write('/tmp/test.kicad_sch')
- Path('/tmp/test.svg').write_text(str(sch.to_svg(TmThemeSchematic(Path('/tmp/witchhazelhypercolor.tmTheme').read_text()))))
+ for p in Path('/tmp').glob('*.tmTheme'):
+ cs = TmThemeSchematic(p.read_text())
+ Path(f'/tmp/test-{p.stem}.svg').write_text(str(postprocess(sch.to_svg(cs))))
+ for p in Path('/tmp').glob('*.sublime-color-scheme'):
+ cs = SublimeSchematic(p.read_text())
+ Path(f'/tmp/test-{p.stem}.svg').write_text(str(postprocess(sch.to_svg(cs))))
diff --git a/gerbonara/cad/kicad/tmtheme.py b/gerbonara/cad/kicad/tmtheme.py
index 52d9afd..2a6ff92 100644
--- a/gerbonara/cad/kicad/tmtheme.py
+++ b/gerbonara/cad/kicad/tmtheme.py
@@ -1,6 +1,7 @@
from xml.etree import ElementTree
import base64
+import json
from pathlib import Path
def _map_primitive(element):
@@ -31,17 +32,9 @@ def parse_shitty_json(data):
root = ElementTree.fromstring(data)
return _map_primitive(root[0])
-class TmThemeSchematic:
- def __init__(self, data):
- self.theme = parse_shitty_json(data)
- s = self.theme['settings'][0]['settings']
- by_scope = {}
- for elem in self.theme['settings']:
- if 'scope' not in elem:
- continue
- for scope in elem['scope'].split(','):
- by_scope[scope.strip()] = elem.get('settings', {})
+class _SublimeColorschemeSuper:
+ def __init__(self, s, by_scope):
def lookup(default, *scopes):
for scope in scopes:
if not (elem := by_scope.get(scope)):
@@ -54,16 +47,41 @@ class TmThemeSchematic:
return default
self.background = s.get('background', 'white')
- self.bus = lookup('black', 'constant.other', 'storage.type')
- self.wire = self.lines = lookup('black', 'constant.other')
- self.no_connect = lookup('black', 'constant.language', 'variable')
- self.text = lookup('black', 'constant.numeric', 'constant.numeric.hex', 'storage.type.number')
- self.pin_numbers = lookup('black', 'constant.character', 'constant.other')
- self.pin_names = lookup('black', 'constant.character.format.placeholder', 'constant.other.placeholder')
- self.values = lookup('black', 'constant.character.format.placeholder', 'constant.other.placeholder')
- self.labels = lookup('black', 'constant.numeric', 'constant.numeric.hex', 'storage.type.number')
+ fg = s.get('foreground', 'black')
+ self.bus = lookup(fg, 'constant.other', 'storage.type')
+ self.wire = self.lines = lookup(fg, 'constant.other')
+ self.no_connect = lookup(fg, 'constant.language', 'variable')
+ self.text = lookup(fg, 'constant.numeric', 'constant.numeric.hex', 'storage.type.number')
+ self.pin_names = lookup(fg, 'constant.character', 'constant.other')
+ self.pin_numbers = fg
+ self.values = lookup(fg, 'constant.character.format.placeholder', 'constant.other.placeholder', 'entity.name.tag', 'support.type', 'support.class', 'entity.other.inherited-class')
+ self.labels = lookup(fg, 'constant.numeric', 'constant.numeric.hex', 'storage.type.number')
self.fill = s.get('background')
- print(f'{self.background=} {self.wire=} {self.bus=} {self.lines=} {self.no_connect=} {self.labels=} {self.fill=}')
+
+
+class TmThemeSchematic(_SublimeColorschemeSuper):
+ def __init__(self, data):
+ self.theme = parse_shitty_json(data)
+ s = self.theme['settings'][0]['settings']
+ by_scope = {}
+ for elem in self.theme['settings']:
+ if 'scope' not in elem:
+ continue
+ for scope in elem['scope'].split(','):
+ by_scope[scope.strip()] = elem.get('settings', {})
+ super().__init__(s, by_scope)
+
+
+class SublimeSchematic(_SublimeColorschemeSuper):
+ def __init__(self, data):
+ self.theme = json.loads(data)
+ s = self.theme['globals']
+ by_scope = {}
+ for elem in self.theme['rules']:
+ for scope in elem['scope'].split(','):
+ by_scope[scope.strip()] = elem
+ super().__init__(s, by_scope)
+
if __name__ == '__main__':
print(parse_shitty_json(Path('/tmp/witchhazelhypercolor.tmTheme').read_text()))