Add line wonkifier

autoroute
jaseg 2023-07-22 18:29:04 +02:00
rodzic ba4cafa3a4
commit 3e47e7c2da
2 zmienionych plików z 187 dodań i 21 usunięć

Wyświetl plik

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

Wyświetl plik

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