Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bub.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

298 changes: 168 additions & 130 deletions bubbles/bubbles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('<svg xmlns="http://www.w3.org/2000/svg" '
def _format_head(self, xcanv, ycanv):
return ('<svg xmlns="http://www.w3.org/2000/svg" '
'xmlns:xlink="http://www.w3.org/1999/xlink" '
f'width="{xcanv}" height="{ycanv}">')

def print_tail(self):
print('</svg>')
def _format_tail(self):
return ('</svg>')

def print_def(self, dump=False, grad_offset=30.0, font_family='Latin Modern Sans', font_weight='normal'):
print(' <defs>')
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(' </defs>')
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 = ' <defs>\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 += ' </defs>\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:
Expand All @@ -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" <circle id='bubble{str(i)}' cx='0' cy='0' r='{bub['r']}' "
f"fill='#{cf:06x}' stroke='#{cs:06x}' stroke-width='{bub['stroke_w']}'/>")
ret += f" <circle id='bubble{str(i)}' cx='0' cy='0' r='{bub['r']}' "
ret += f"fill='#{cf:06x}' stroke='#{cs:06x}' stroke-width='{bub['stroke_w']}'/>\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:
Expand All @@ -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' <linearGradient id="myGradient{i}.{j}" x1="0" x2="0" y1="0" y2="1">'
f'<stop offset="{offset}%" stop-color="#{coli:06x}"/> '
f'<stop offset="{100-offset}%" stop-color="#{colj:06x}"/> '
f'</linearGradient>')

def print_def_font(self, family='Latin Modern Sans', weight='normal'):
ret += (f' <linearGradient id="myGradient{i}.{j}" x1="0" x2="0" y1="0" y2="1">'
f'<stop offset="{offset}%" stop-color="#{coli:06x}"/> '
f'<stop offset="{100-offset}%" stop-color="#{colj:06x}"/> '
f'</linearGradient>\n')

return ret

def _format_def_font(self, family='Latin Modern Sans', weight='normal'):
# font examples: 'Latin Modern Sans', 'Adobe Helvetica', 'monospace'
print(f''' <style>
.mytext {'{'}
return f''' <style>
.mytext {{
font-style:normal; font-variant:normal; font-weight:{weight}; font-stretch:normal;
line-height:125%;
font-family:"{family}"; -inkscape-font-specification:"{family}";
Expand All @@ -86,32 +79,14 @@ 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
{'}'}
</style>''')
}}
</style>\n'''

def put_bubble(self, a):
def _format_bubble(self, a):
t, x, y = a
print(f' <use x="{x}" y="{y}" xlink:href="#bubble{str(t)}" />')
return f' <use x="{x}" y="{y}" xlink:href="#bubble{str(t)}" />\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
Expand All @@ -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' <g transform="rotate({-angle},{x0},{y0})"> <path d=" '+\
path+\
f'" fill="url(\'#myGradient{col0}.{col1}\')" /> </g>\n'

def _format_text(self, x,y, text, fs=12, fc=0x000000):
print(f' <text x="{x}" y="{y}" class="mytext" font-size="{fs}px" fill="#{fc:06x}">')
l = (len(text)-1.5)/2
for i,line in enumerate(text):
print(f' <tspan x="{x}" dy="{-l if i==0 else 1}em"> {line} </tspan>')
print(' </text>')

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' <g transform="rotate({-angle},{x0},{y0})"> <path d=" '+\
path+\
f'" fill="url(\'#myGradient{col0}.{col1}\')" /> </g>')

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).
Expand All @@ -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}"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"bad" parameter actually works -- gives a weird looking bond but I thought it might be not useless

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).
Expand Down Expand Up @@ -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' <text x="{x}" y="{y}" class="mytext" font-size="{fs}px" fill="#{fc:06x}">')
l = (len(text)-1.5)/2
for i,line in enumerate(text):
print(f' <tspan x="{x}" dy="{-l if i==0 else 1}em"> {line} </tspan>')
print(' </text>')

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
Expand Down
Loading