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 e93528b..d8e746d 100644
--- a/bubbles/bubbles.py
+++ b/bubbles/bubbles.py
@@ -7,46 +7,34 @@ 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):
-
- 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('')
+ 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:
@@ -56,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:
@@ -69,15 +60,17 @@ 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 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):
+ 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
@@ -120,14 +95,118 @@ 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)
+
+ return f' \n'
+
+ 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 _format_all_liaisons(self):
+ ret = ''
+ for [a,k] in self._liaisons:
+ ret += self._format_liaison(*a, **k)
+ return ret
+
+ def _format_all_bubbles(self):
+ ret = ''
+ for [a,k] in self._bubbles:
+ ret += self._format_bubble(*a, **k)
+ return ret
+
+ def _format_all_texts(self):
+ ret = ''
+ for [a,k] in self._texts:
+ ret += self._format_text(*a, **k)
+ return ret
+
+ 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):
+ 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']
+ 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
- print(f' ')
- def put_liaison_equal(self, r0, x, y0, y1, h):
+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).
@@ -140,17 +219,28 @@ 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
- 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).
@@ -210,66 +300,14 @@ 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):
+ """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..147cc74 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'])
@@ -79,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}))
@@ -98,14 +118,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