From 3e47e7c2dabc78650e228e5336da10d27bdf8dad Mon Sep 17 00:00:00 2001 From: jaseg Date: Sat, 22 Jul 2023 18:29:04 +0200 Subject: Add line wonkifier --- gerbonara/cad/kicad/schematic.py | 152 ++++++++++++++++++++++++++++++++++++++- gerbonara/cad/kicad/tmtheme.py | 56 ++++++++++----- 2 files changed, 187 insertions(+), 21 deletions(-) (limited to 'gerbonara/cad') 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())) -- cgit