From cc81019ebd3e8927f6139d04a7b69a9be9ecc393 Mon Sep 17 00:00:00 2001 From: Liam Marsh Date: Sat, 2 Sep 2023 11:31:01 +0200 Subject: [PATCH 1/3] more fixes for more recent inkscape versions, plus comments and asserts --- bubbles/bubbles.py | 13 +++++++++---- inkscape-ext/bubblebond.py | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/bubbles/bubbles.py b/bubbles/bubbles.py index e93528b..cfa8459 100644 --- a/bubbles/bubbles.py +++ b/bubbles/bubbles.py @@ -7,12 +7,11 @@ def __init__(self, grad_offset=30.0, font_family='Latin Modern Sans', font_weight='normal'): self.pars = locals(); self.pars.pop('self') - self.bubble = {} - self._bubbles = [] + self.bubble = {} # bubble definitions + self._bubbles = [] # bubble placements (id, x, y) self._liaisons = [] self._texts = [] - self._colors = [] - self._colorsid = [] + self._colors = [] # color definition def def_bubble(self, key, fill=0xFFFFFF, r=50, stroke=0x990000, stroke_w=10): @@ -140,12 +139,17 @@ def put_liaison_equal(self, r0, x, y0, y1, h): ''' l = abs(y1-y0) + assert 0.5*h <= r0 , f"bad h parameter, should be less or equal to {2*r0}" + if 0.5*l < r0: + assert 0.25*(l**2+h**2) >= r0**2, f"bad h parameter, should be greater or equal to {2*math.sqrt(r0**2-l**2/4)}" + R = (0.25*l**2 + 0.25*h**2 - r0**2) / (2.0*r0 - h) dx = 0.5*h + R dy = 0.5*l dr = math.hypot(dx, dy) + # position of a tangent point wrt C0's center xx = dx*r0 / dr yy = dy*r0 / dr return R, x+xx, y0+yy, 0, 2.0*(dy-yy), -2.0*xx @@ -270,6 +274,7 @@ def get_canvsize(self): def _gold(f, eps, bra, ket): + """Golden ratio optimisation: find a local minimum for f between bra and key, with tolerence eps""" phi = (math.sqrt(5.0)-1.0)*0.5 d = ket-bra x1 = ket-d*phi diff --git a/inkscape-ext/bubblebond.py b/inkscape-ext/bubblebond.py index 10fe5cd..1f888e3 100644 --- a/inkscape-ext/bubblebond.py +++ b/inkscape-ext/bubblebond.py @@ -3,7 +3,16 @@ from itertools import count import inkex from lxml import etree -import bubbles + +try: + import bubbles +except ImportError: + # try and detect the bubbles.py file next to this one + import sys, os + sys.path.append(os.path.split(__file__)[0]) + import bubbles + del sys.path[-1] + del sys,os class BubbleBond(inkex.Effect): @@ -36,7 +45,19 @@ def effect(self): raise inkex.AbortExtension("These are not circles.") h = width * 2.0*min(r0, r1) - colors = (st0['stroke'], st1['stroke']) + if 'stroke' in st0 and st0['stroke'] != 'none': + col0 = st0['stroke'] + elif 'fill' in st0: + col0 = st0['fill'] + else: + col0 = 'none' + if 'stroke' in st1 and st1['stroke'] != 'none': + col1 = st1['stroke'] + elif 'fill' in st0: + col1 = st1['fill'] + else: + col1 = 'none' + colors = (col0, col1) x0 = float(c0.attrib['cx']) y0 = float(c0.attrib['cy']) x1 = float(c1.attrib['cx']) @@ -98,14 +119,19 @@ def get_radius(self, c0, st0): r0 = c0.radius[0] else: return None - return r0 + float(st0['stroke-width'])/2 + if 'stroke' in st0 and st0['stroke'] != 'none': + return r0 + float(st0['stroke-width'])/2 + else: + return r0 def get_style_dict(self, c0): st0_str = c0.attrib['style'] st0_obj = inkex.styles.Style.parse_str(st0_str) - st0_dict = {} - for key, val in st0_obj: - st0_dict[key] = val + if hasattr(st0_obj, 'items'): + st0_dict = { k:v for k,v in st0_obj.items() } + else: + # fallback that only works on old versions of inkscape (before which one?) + st0_dict = { k:v for k,v in st0_obj } return st0_dict From 73278d31f990f934e57aefaa3396de292a41012a Mon Sep 17 00:00:00 2001 From: Liam Marsh Date: Sat, 2 Sep 2023 23:32:06 +0200 Subject: [PATCH 2/3] refactoring: splitting classes, changing inner interfaces for bond solving --- bubbles/bubbles.py | 210 ++++++++++++++++++++----------------- inkscape-ext/bubblebond.py | 5 +- 2 files changed, 117 insertions(+), 98 deletions(-) diff --git a/bubbles/bubbles.py b/bubbles/bubbles.py index cfa8459..81aee00 100644 --- a/bubbles/bubbles.py +++ b/bubbles/bubbles.py @@ -13,19 +13,6 @@ def __init__(self, self._texts = [] self._colors = [] # color definition - def def_bubble(self, key, fill=0xFFFFFF, r=50, stroke=0x990000, stroke_w=10): - - def findcolor(c): - if c not in self._colors: - self._colors.append(c) - return self._colors.index(c) - - self.bubble[key] = {'fill' : findcolor(fill), - 'r' : r, - 'stroke' : findcolor(stroke), - 'stroke_w' : stroke_w, - } - def print_head(self, xcanv, ycanv): print(' - .mytext {'{'} + .mytext {{ font-style:normal; font-variant:normal; font-weight:{weight}; font-stretch:normal; line-height:125%; font-family:"{family}"; -inkscape-font-specification:"{family}"; @@ -85,31 +72,13 @@ def print_def_font(self, family='Latin Modern Sans', weight='normal'): fill-opacity:1; stroke:#FFFFFF; stroke-width:0; stroke-linecap:butt; stroke-linejoin:miter; stroke-opacity:1; stroke-miterlimit:4; stroke-dasharray:none - {'}'} + }} ''') def put_bubble(self, a): t, x, y = a print(f' ') - def liaison_path(self, r0, r1, x0, y0, y11, alpha, h, auto, tol): - if h is None: - h = 0.666*r1 - if abs(r0-r1) > 1e-4: - R, dx0, dy0, dx1, dy1, dx2 = self.put_liaison_diffr(r0, r1, x0, y0, y11, alpha=alpha, h=h, auto=auto, tol=tol) - else: - R, dx0, dy0, dx1, dy1, dx2 = self.put_liaison_equal(r0, x0, y0, y11, h) - return f'M {dx0} {dy0} '\ - f'a {R}, {R} 0 0 0 {dx1} {+dy1} '\ - f'h {dx2} '\ - f'a {R}, {R} 0 0 0 {dx1} {-dy1} '\ - f'z' - - def liaison_angle(self, x0, x1, y0, y1): - angle = math.degrees(math.pi*0.5 - math.atan2(y1-y0, x1-x0)) - y11 = y0 + math.hypot(x0-x1, y0-y1) - return angle, y11 - def put_liaison(self, a0, a1, h=None, auto=True, alpha=None, tol=1e-4): t0, x0, y0 = a0 t1, x1, y1 = a1 @@ -119,14 +88,112 @@ def put_liaison(self, a0, a1, h=None, auto=True, alpha=None, tol=1e-4): col1 = self.bubble[t1]['stroke'] # virtually align the liaison with the y-axis - angle, y11 = self.liaison_angle(x0, x1, y0, y1) - path = self.liaison_path(r0, r1, x0, y0, y11, alpha, h, auto, tol) + angle, y11 = BondSolver.liaison_angle(x0, x1, y0, y1) + path = BondSolver.liaison_path(r0, r1, x0, y0, y11, alpha, h, auto, tol) print(f' ') - def put_liaison_equal(self, r0, x, y0, y1, h): + + def put_text(self, x,y, text, fs=12, fc=0x000000): + print(f' ') + l = (len(text)-1.5)/2 + for i,line in enumerate(text): + print(f' {line} ') + print(' ') + + def put_all_liaisons(self): + for [a,k] in self._liaisons: + self.put_liaison(*a, **k) + + def put_all_bubbles(self): + for [a,k] in self._bubbles: + self.put_bubble(*a, **k) + + def put_all_texts(self): + for [a,k] in self._texts: + self.put_text(*a, **k) + + def def_bubble(self, key, fill=0xFFFFFF, r=50, stroke=0x990000, stroke_w=10): + + def findcolor(c): + if c not in self._colors: + self._colors.append(c) + return self._colors.index(c) + + self.bubble[key] = {'fill' : findcolor(fill), + 'r' : r, + 'stroke' : findcolor(stroke), + 'stroke_w' : stroke_w, + } + + def add_liaison(self, *args, **kwargs): + self._liaisons.append((args, kwargs)) + + def add_bubble(self, *args, **kwargs): + self._bubbles.append((args, kwargs)) + + def add_text(self, *args, **kwargs): + self._texts.append((args, kwargs)) + + + def dump(self): + self.print_head(*self.get_canvsize()) + self.print_def(dump=True, grad_offset=self.pars['grad_offset'], font_family=self.pars['font_family'], font_weight=self.pars['font_weight']) + self.put_all_liaisons() + print() + self.put_all_bubbles() + print() + self.put_all_texts() + print() + self.print_tail() + + def get_canvsize(self): + xcanv = self.pars['xcanv'] + ycanv = self.pars['ycanv'] + if xcanv is None or ycanv is None: + xmax = ymax = 0 + rmean = 0 + for [a,k] in self._bubbles: + t,x,y = a[0] + r = self.bubble[t]['r']+self.bubble[t]['stroke_w']/2 + xmax = max(xmax, x+r) + ymax = max(ymax, y+r) + rmean += r + rmean /= len(self._bubbles) + if xcanv is None: xcanv = xmax+rmean/2 + if ycanv is None: ycanv = ymax+rmean/2 + return xcanv, ycanv + + +class BondSolver: + + @staticmethod + def liaison_path(r0, r1, x0, y0, y11, alpha, h, auto, tol): + if h is None: + h = 0.666*r1 + if abs(r0-r1) > 1e-4: + curv1,curv2, (x1,y1), dx0, dx1 = BondSolver._compute_circles_diffr(r0, r1, x0, y0, y11, alpha=alpha, h=h, auto=auto, tol=tol) + else: + curv1,curv2, (x1,y1), dx0, dx1 = BondSolver._compute_circles_equal(r0, x0, y0, y11, h) + + # move to C1, C1 -> C0 path, cross-C0 path, C0 -> C1 path, cross-C1 path, close. + return f'M {x1} {y1} '\ + f'{curv1} '\ + f'a {r0*1.01}, {r0*1.01} 0 0 0 {-dx0} 0'\ + f'{curv2} '\ + f'a {r1*1.01}, {r1*1.01} 0 0 0 {+dx1} 0'\ + f'z' + + @staticmethod + def liaison_angle(x0, x1, y0, y1): + angle = math.degrees(math.pi*0.5 - math.atan2(y1-y0, x1-x0)) + y11 = y0 + math.hypot(x0-x1, y0-y1) + return angle, y11 + + @staticmethod + def _compute_circles_equal(r0, x, y0, y1, h): ''' There are 2 circles C0, C1 with radius r0 centered at the points (x,y0) and (x,y1). @@ -152,9 +219,15 @@ def put_liaison_equal(self, r0, x, y0, y1, h): # position of a tangent point wrt C0's center xx = dx*r0 / dr yy = dy*r0 / dr - return R, x+xx, y0+yy, 0, 2.0*(dy-yy), -2.0*xx - def put_liaison_diffr(self, r0, r1, x, y0, y1, alpha, h, auto=True, tol=1e-4): + # R, R, rotation, flag,flag, travel_x, travel_y + curv1 = f'a {R}, {R} 0 0 0 0 {+2.0*(dy-yy)} ' + curv2 = f'a {R}, {R} 0 0 0 0 {-2.0*(dy-yy)} ' + return curv1,curv2, (x+xx, y0+yy), 2*xx, +2*xx + + + @staticmethod + def _compute_circles_diffr(r0, r1, x, y0, y1, alpha, h, auto=True, tol=1e-4): ''' There are 2 circles C0, C1 with radii r0!=r1 centered at the points (x,y0) and (x,y1). @@ -214,63 +287,10 @@ def get_coord(alpha): sinb, cosb, X12, X21, R, hh = get_coord(alpha) - return R, Cx+X12*cosb, Cy+X12*sinb, (X21-X12)*cosb, (X21-X12)*sinb, -2*X21*cosb - - def put_text(self, x,y, text, fs=12, fc=0x000000): - print(f' ') - l = (len(text)-1.5)/2 - for i,line in enumerate(text): - print(f' {line} ') - print(' ') - - def add_liaison(self, *args, **kwargs): - self._liaisons.append((args, kwargs)) - - def add_bubble(self, *args, **kwargs): - self._bubbles.append((args, kwargs)) - - def add_text(self, *args, **kwargs): - self._texts.append((args, kwargs)) - - def put_all_liaisons(self): - for [a,k] in self._liaisons: - self.put_liaison(*a, **k) - - def put_all_bubbles(self): - for [a,k] in self._bubbles: - self.put_bubble(*a, **k) - - def put_all_texts(self): - for [a,k] in self._texts: - self.put_text(*a, **k) - - def dump(self): - self.print_head(*self.get_canvsize()) - self.print_def(dump=True, grad_offset=self.pars['grad_offset'], font_family=self.pars['font_family'], font_weight=self.pars['font_weight']) - self.put_all_liaisons() - print() - self.put_all_bubbles() - print() - self.put_all_texts() - print() - self.print_tail() - - def get_canvsize(self): - xcanv = self.pars['xcanv'] - ycanv = self.pars['ycanv'] - if xcanv is None or ycanv is None: - xmax = ymax = 0 - rmean = 0 - for [a,k] in self._bubbles: - t,x,y = a[0] - r = self.bubble[t]['r']+self.bubble[t]['stroke_w']/2 - xmax = max(xmax, x+r) - ymax = max(ymax, y+r) - rmean += r - rmean /= len(self._bubbles) - if xcanv is None: xcanv = xmax+rmean/2 - if ycanv is None: ycanv = ymax+rmean/2 - return xcanv, ycanv + # R, R, rotation, flag,flag, travel_x, travel_y + curv1 = f'a {R}, {R} 0 0 0 {(X21-X12)*cosb} {+(X21-X12)*sinb}' + curv2 = f'a {R}, {R} 0 0 0 {(X21-X12)*cosb} {-(X21-X12)*sinb}' + return curv1,curv2, (Cx+X12*cosb, Cy+X12*sinb), 2*X21*cosb, 2*X12*cosb def _gold(f, eps, bra, ket): diff --git a/inkscape-ext/bubblebond.py b/inkscape-ext/bubblebond.py index 1f888e3..147cc74 100644 --- a/inkscape-ext/bubblebond.py +++ b/inkscape-ext/bubblebond.py @@ -100,9 +100,8 @@ def make_gradient(self, colors, offsets): return 'url(#'+gname+')' def make_liaison(self, r0, r1, x0, x1, y0, y1, h, gradient): - BW = bubbles.Bubble_World() - angle, y11 = BW.liaison_angle(x0, x1, y0, y1) - path = BW.liaison_path(r0, r1, x0, y0, y11, None, h, True, 1e-4) + angle, y11 = bubbles.BondSolver.liaison_angle(x0, x1, y0, y1) + path = bubbles.BondSolver.liaison_path(r0, r1, x0, y0, y11, None, h, True, 1e-4) liaison = inkex.elements.PathElement() liaison.path = path liaison.style = str(inkex.styles.Style({'stroke': 'none', 'stroke-width': '0', 'fill': gradient})) From 89215e9ca6bca6043123acfbf24bf82771d6cd72 Mon Sep 17 00:00:00 2001 From: Liam Marsh Date: Sun, 3 Sep 2023 10:46:41 +0200 Subject: [PATCH 3/3] refactor: do not use print() in 'library code' --- bub.py | 2 +- bubbles/bubbles.py | 109 +++++++++++++++++++++++++-------------------- 2 files changed, 62 insertions(+), 49 deletions(-) diff --git a/bub.py b/bub.py index 3630414..c315072 100755 --- a/bub.py +++ b/bub.py @@ -91,5 +91,5 @@ bubble_world.add_text(x11,y11, ['electronic', 'structure'], fs=22, fc=0x000000) bubble_world.add_text(x3, y3, ['SPAHM'], fs=25, fc=0x000000) -bubble_world.dump() +print(bubble_world.dump()) diff --git a/bubbles/bubbles.py b/bubbles/bubbles.py index 81aee00..d8e746d 100644 --- a/bubbles/bubbles.py +++ b/bubbles/bubbles.py @@ -13,26 +13,28 @@ def __init__(self, self._texts = [] self._colors = [] # color definition - def print_head(self, xcanv, ycanv): - print('') - def print_tail(self): - print('') + def _format_tail(self): + return ('') - def print_def(self, dump=False, grad_offset=30.0, font_family='Latin Modern Sans', font_weight='normal'): - print(' ') - self.print_def_bubble(dump=dump) - print() - self.print_def_gradient(dump=dump, offset=grad_offset) - print() - self.print_def_font(family=font_family, weight=font_weight) - print(' ') - print() - def print_def_bubble(self, dump=False): + def _format_defs(self, dump=False, grad_offset=30.0, font_family='Latin Modern Sans', font_weight='normal'): + ret = ' \n' + ret += self._format_def_bubbles(dump=dump) + ret += '\n' + ret += self._format_def_gradients(dump=dump, offset=grad_offset) + ret += '\n' + ret += self._format_def_fonts(family=font_family, weight=font_weight) + ret += ' \n\n' + return ret + def _format_def_bubbles(self, dump=False): + + ret = "" if dump is True: idx = set([a[0][0] for a,k in self._bubbles]) else: @@ -42,11 +44,14 @@ def print_def_bubble(self, dump=False): bub = self.bubble[i] cf = self._colors[bub['fill']] cs = self._colors[bub['stroke']] - print(f" ") + ret += f" \n" + + return ret - def print_def_gradient(self, offset=30.0, dump=False): + def _format_def_gradients(self, offset=30.0, dump=False): + ret = "" if dump is True: idx = [(self.bubble[a[0][0]]['stroke'], self.bubble[a[1][0]]['stroke']) for [a,k] in self._liaisons] else: @@ -55,14 +60,16 @@ def print_def_gradient(self, offset=30.0, dump=False): for i,j in set(idx): coli = self._colors[i] colj = self._colors[j] - print(f' ' - f' ' - f' ' - f'') - - def print_def_font(self, family='Latin Modern Sans', weight='normal'): + ret += (f' ' + f' ' + f' ' + f'\n') + + return ret + + def _format_def_font(self, family='Latin Modern Sans', weight='normal'): # font examples: 'Latin Modern Sans', 'Adobe Helvetica', 'monospace' - print(f''' ''') + \n''' - def put_bubble(self, a): + def _format_bubble(self, a): t, x, y = a - print(f' ') + return f' \n' - def put_liaison(self, a0, a1, h=None, auto=True, alpha=None, tol=1e-4): + def _format_liaison(self, a0, a1, h=None, auto=True, alpha=None, tol=1e-4): t0, x0, y0 = a0 t1, x1, y1 = a1 r0 = self.bubble[t0]['r']+self.bubble[t0]['stroke_w']/2 @@ -91,29 +98,34 @@ def put_liaison(self, a0, a1, h=None, auto=True, alpha=None, tol=1e-4): angle, y11 = BondSolver.liaison_angle(x0, x1, y0, y1) path = BondSolver.liaison_path(r0, r1, x0, y0, y11, alpha, h, auto, tol) - print(f' ') - + return f' \n' - def put_text(self, x,y, text, fs=12, fc=0x000000): + def _format_text(self, x,y, text, fs=12, fc=0x000000): print(f' ') l = (len(text)-1.5)/2 for i,line in enumerate(text): print(f' {line} ') print(' ') - def put_all_liaisons(self): + def _format_all_liaisons(self): + ret = '' for [a,k] in self._liaisons: - self.put_liaison(*a, **k) + ret += self._format_liaison(*a, **k) + return ret - def put_all_bubbles(self): + def _format_all_bubbles(self): + ret = '' for [a,k] in self._bubbles: - self.put_bubble(*a, **k) + ret += self._format_bubble(*a, **k) + return ret - def put_all_texts(self): + def _format_all_texts(self): + ret = '' for [a,k] in self._texts: - self.put_text(*a, **k) + ret += self._format_text(*a, **k) + return ret def def_bubble(self, key, fill=0xFFFFFF, r=50, stroke=0x990000, stroke_w=10): @@ -139,15 +151,16 @@ def add_text(self, *args, **kwargs): def dump(self): - self.print_head(*self.get_canvsize()) - self.print_def(dump=True, grad_offset=self.pars['grad_offset'], font_family=self.pars['font_family'], font_weight=self.pars['font_weight']) - self.put_all_liaisons() - print() - self.put_all_bubbles() - print() - self.put_all_texts() - print() - self.print_tail() + ret = self._format_head(*self.get_canvsize()) + ret += self._format_defs(dump=True, grad_offset=self.pars['grad_offset'], font_family=self.pars['font_family'], font_weight=self.pars['font_weight']) + ret += self._format_all_liaisons() + ret += '\n' + ret += self._format_all_bubbles() + ret += '\n' + ret += self._format_all_texts() + ret += '\n' + ret += self._format_tail() + return ret def get_canvsize(self): xcanv = self.pars['xcanv']