#! /usr/bin/python3 -t
MODULE_NAME = "mformula_svg"
MODULE_DESC = "Generates an SVG picture of a 'roadkill' molecular diagram"
MODULE_VERS = "1.0"
MODULE_COPYRIGHT = "Copyright © 2009-07-06 by the State University of Campinas (UNICAMP)"
import sys
import os
import copy
import math
from math import sqrt,sin,cos,pi,ceil,trunc
# sys.path[1:0] = [ sys.path[0] + '/../lib', os.path.expandvars('${STOLFIHOME}/lib'), '.' ]
# sys.stderr.write("%s.py: path = %r\n" % (MODULE_NAME, sys.path));
import mformula
import rn
import argparser # Just for the "no warranty" statements.
from datetime import date
class obj:
"""A device for plotting an {mformula.obj} instance."""
def __init__(svg, wr,style,back):
svg.wr = wr # Output file
svg.style = style # Formula render style: 'B1','B2' for balls, 'S1' for symbols.
svg.back = back # True to plot a backing plane.
# Note that atom and label coordinates in formula are interpreted as multiples of the {bondlength},
# whereas the atom radii are interpreted as multiples of the {atomradius},
# and font sizes are multiples of {atomftheight}
# Initialize the element tables:
svg.nelems = 0 # Number of distinct elements.
svg.elem_symbol = [] # Symbol of each element (string).
svg.elem_radius = [] # Radius of each element's atom/label (float), rel. to {atomradius}.
# Relevant for 'B' style only:
svg.elem_color = [] # Color of each atom/label (RGB tuple, 0-1 range).
# Relevant for 'S' style only:
svg.elem_label = [] # Label to print inside atom.
svg.elem_fsize = [] # Font size reduction factor.
svg.elem_labdx = [] # Horizontal adjustment for label.
svg.elem_labdy = [] # Vertical adjustment for label.
# Bounding box of the formula (in pixels) not including {margin_wx,margin_wy}.
# Initialize the minimum formula extents to always contain the origin.
# The client may change these parameters later to enforce a prescribed minimum size.
svg.molec_min_x = 0
svg.molec_max_x = 0
svg.molec_min_y = 0
svg.molec_max_y = 0
svg.set_style_params()
#----------------------------------------------------------------------
def set_style_params(svg):
"""Assumes {style} is "{genstyle}{substyle}" where the {genstyle} is 'B' for
balls, 'S' for symbols, and {substyle} is an integer that defines minor
variations such as atom sizes etc."""
if svg.style == 'B1':
svg.set_style_params_B()
svg.set_elem_style_B1()
elif svg.style == 'B2':
svg.set_style_params_B()
svg.set_elem_style_B2()
elif svg.style == 'S1':
svg.set_style_params_S()
svg.set_elem_style_S1()
elif svg.style == 'S2':
svg.set_style_params_S()
svg.set_elem_style_S2()
else:
assert False, "invalid style \"" + svg.style + "\""
def set_style_params_B(svg):
"""General style parameters for 'B1' and 'B2' styles."""
svg.bondlength = 30.00 # Bond length (float, in pixels).
svg.dashlength = 2.50 # Nominal dash length for 1.5 bonds (float, in pixels).
svg.bondwidth = 1.25 # Width of bond lines (float, in pixels).
svg.dotsdist = 3.00 # Distance from atom edge to unfilled-bond dots (float, in pixels).
svg.dotwidth = 2.50 # Width of each unfilled-bond dot (float, in pixels).
svg.chargedist = None # Distance from atom edge to charge symbol (float, in pixels).
svg.ringwidth = 0.50 # Width of atom outlines (float, in pixels).
svg.maxdepth = 5 # Max layer depth (0 = atoms, 1=bonds, 2,..=charge).
#----------------------------------------------------------------------
def set_style_params_S(svg):
"""General style parameters for 'S1' style."""
svg.bondlength = 18.00 # Bond length (float, in pixels).
svg.dashlength = 1.50 # Nominal dash length for 1.5 bonds (float, in pixels).
svg.bondwidth = 0.90 # Width of bond lines (float, in pixels).
svg.dotsdist = 2.00 # Distance from atom edge to unfilled-bond dots (float, in pixels).
svg.dotwidth = 1.50 # Width of each unfilled-bond dot (float, in pixels).
svg.chargedist = 2.00 # Distance from atom edge to charge symbol (float, in pixels).
svg.ringwidth = 0.0 # Width of atom outlines (float, in pixels).
svg.maxdepth = 3 # Max layer depth (0 = atoms, 1=bonds, 2,..=charge).
#----------------------------------------------------------------------
def set_elem_style_B1(svg):
"""Sizes and colors of atoms for 'B1' style. Atoms are represented by
big colored balls, of distinctly variable sizes.
Multibonds are shorter."""
svg.atomradius = 8.00 # Max atom radius (float, in pixels).
svg.atomftheight = 10.00 # Max font height for atom symbols (float, in pixels).
svg.add_elem("C", 1.00, [0.000, 0.000, 0.000], None, 1.00, None, None)
svg.add_elem("O", 0.90, [0.900, 0.200, 0.100], None, 0.90, None, None)
svg.add_elem("S", 0.90, [0.950, 1.000, 0.100], None, 0.90, None, None)
svg.add_elem("P", 0.90, [1.000, 0.700, 0.100], None, 0.90, None, None)
svg.add_elem("N", 0.90, [0.250, 0.500, 1.000], None, 0.90, None, None)
svg.add_elem("H", 0.50, [0.800, 0.800, 0.800], None, 0.50, None, None)
svg.add_elem(".", 0.00, [1.000, 1.000, 1.000], None, 0.00, None, None) # Invisible atom (bond break).
svg.add_elem("+", 0.00, [1.000, 1.000, 1.000], None, 0.00, None, None) # Disembodied charge (+ or -)
svg.doublebondlength = 0.85 # Length of double bonds relative to single bonds.
svg.CHbondlength = 0.60 # Rel. length of single C-H bonds.
svg.CObondlength = 0.80 # Rel. length of single C-O bonds (also C-N, C-F, etc.)
svg.OObondlength = 0.80 # Rel. length of single O-O bonds (also O-N, S-O, etc.)
svg.OHbondlength = 0.60 # Rel. length of single O-H bonds (also N-H, S-H, etc.)
#----------------------------------------------------------------------
def set_elem_style_B2(svg):
"""Sizes and colors of atoms for 'B2' style. Atoms are represented by
colored balls, but with smaller atoms of mostly uniform sizes.
Multibonds are shorter."""
svg.atomradius = 6.50 # Max atom radius (float, in pixels).
svg.atomftheight = 8.00 # Max font height for atom symbols (float, in pixels).
svg.add_elem("C", 1.00, [0.000, 0.000, 0.000], None, 1.00, None, None)
svg.add_elem("O", 1.00, [0.900, 0.200, 0.100], None, 1.00, None, None)
svg.add_elem("S", 1.00, [0.950, 1.000, 0.100], None, 1.00, None, None)
svg.add_elem("P", 1.00, [1.000, 0.700, 0.100], None, 1.00, None, None)
svg.add_elem("N", 1.00, [0.250, 0.500, 1.000], None, 1.00, None, None)
svg.add_elem("H", 0.65, [0.800, 0.800, 0.800], None, 0.65, None, None)
svg.add_elem(".", 0.00, [1.000, 1.000, 1.000], None, 0.00, None, None) # Invisible atom (bond break).
svg.add_elem("+", 0.00, [1.000, 1.000, 1.000], None, 0.00, None, None) # Disembodied charge (+ or -)
svg.doublebondlength = 0.85 # Length of double bonds relative to single bonds.
svg.CHbondlength = 0.60 # Rel length of single C-H bonds.
svg.CObondlength = 0.80 # Rel. length of single C-O bonds (also C-N, C-F, etc.)
svg.OObondlength = 0.80 # Rel. length of single O-O bonds (also O-N, S-O, etc.)
svg.OHbondlength = 0.60 # Rel. length of single O-H bonds (also N-H, S-H, etc.)
#----------------------------------------------------------------------
def set_elem_style_S1(svg):
"""Sizes, labels, and label position adjustments of atoms for 'S' style.
Atoms are represented by their symbols.
Multiple bonds have the same length as single bonds."""
svg.atomradius = 4.50 # Max atom radius (float, in pixels).
svg.atomftheight = 9.00 # Max font height for atom symbols (float, in pixels).
svg.add_elem("C", 1.00, None, "C", 1.00, -0.004, -0.350)
svg.add_elem("O", 1.00, None, "O", 1.00, -0.004, -0.350)
svg.add_elem("S", 1.00, None, "S", 1.00, -0.004, -0.350)
svg.add_elem("P", 1.00, None, "P", 1.00, -0.004, -0.350)
svg.add_elem("N", 1.00, None, "N", 1.00, -0.004, -0.350)
svg.add_elem("H", 0.85, None, "H", 0.80, -0.004, -0.350)
svg.add_elem(".", 0.00, None, "", 0.00, 00.000, 00.000) # Invisible atom (invisible bond break).
svg.add_elem("+", 0.00, None, "+", 0.75, -0.004, -0.300) # Disembodied charge (+ or -)
svg.doublebondlength = 1.00 # Length of double bonds relative to single bonds.
svg.CHbondlength = 0.70 # Length of single bonds with hydrogen.
svg.CObondlength = 0.90 # Rel. length of single C-O bonds (also C-N, C-F, etc.)
svg.OObondlength = 0.90 # Rel. length of single O-O bonds (also O-N, S-O, etc.)
svg.OHbondlength = 0.70 # Rel. length of single O-H bonds (also N-H, S-H, etc.)
#----------------------------------------------------------------------
def set_elem_style_S2(svg):
"""Sizes, labels, and label position adjustments of atoms for 'S' style.
Atoms are represented by their symbols, except C that are represented by kinks.
Multiple bonds have the same length as single bonds."""
svg.atomradius = 4.50 # Max atom radius (float, in pixels).
svg.atomftheight = 9.00 # Max font height for atom symbols (float, in pixels).
svg.add_elem("C", 0.00, None, "", 0.00, 00.000, 00.000)
svg.add_elem("O", 1.00, None, "O", 1.00, -0.004, -0.350)
svg.add_elem("S", 1.00, None, "S", 1.00, -0.004, -0.350)
svg.add_elem("P", 1.00, None, "P", 1.00, -0.004, -0.350)
svg.add_elem("N", 1.00, None, "N", 1.00, -0.004, -0.350)
svg.add_elem("H", 0.85, None, "H", 0.80, -0.004, -0.350)
svg.add_elem(".", 0.00, None, "", 0.00, 00.000, 00.000) # Invisible atom (invisible bond break).
svg.add_elem("+", 0.00, None, "+", 0.75, -0.004, -0.300) # Disembodied charge (+ or -)
svg.doublebondlength = 1.00 # Length of double bonds relative to single bonds.
svg.CHbondlength = 0.70 # Length of single bonds with hydrogen.
svg.CObondlength = 0.90 # Rel. length of single C-O bonds (also C-N, C-F, etc.)
svg.OObondlength = 0.90 # Rel. length of single O-O bonds (also O-N, S-O, etc.)
svg.OHbondlength = 0.70 # Rel. length of single O-H bonds (also N-H, S-H, etc.)
#----------------------------------------------------------------------
def add_elem(svg, sym,rad,col,lab,fntsz,labdx,labdy):
"""Defines the appearance of an element for use in formula {svg}.
The element has the identifying symbol {sym}, and will be assumed
to occupy circle or sphere with radius {rad}.
If style is 'B', the atom will be plotted as a solid circle
of that radius and color {col}.
If {style} is 'S', that cicle will not be painted; instead, the string {lab},
if not empty, will be written over it, with font size reduced by factor {fntsz},
centered but displaced by {labdx,labdy} times the nominal font size. The circle
will still be used to define the endpoints of bonds."""
sys.stderr.write("! adding elem symbol = %r radius = %r color = %r" % (sym,rad,col))
sys.stderr.write(" label = '%s' labdx = %r labdy = %r\n" % (lab,labdx,labdy))
svg.elem_symbol.append(sym)
svg.elem_radius.append(rad)
svg.elem_color.append(copy.copy(col))
svg.elem_label.append(lab)
svg.elem_fsize.append(fntsz)
svg.elem_labdx.append(labdx)
svg.elem_labdy.append(labdy)
svg.nelems += 1
assert len(svg.elem_symbol) == svg.nelems
#----------------------------------------------------------------------
def compute_derived_dims(svg, fm):
"""(Re)computes derived dimensions.
Recomputes several dimensions that are derived from the formula
{fm} and the main style parameters. Must be called just before
plotting the formula."""
# {svg.bindatradius} is the distance from atom center to start of bond lines.
# It needs to be scaled by the atom's radius.
if svg.style[0] == 'B':
svg.bindatradius = 0.0
elif svg.style[0] == 'S':
svg.bindatradius = 1.0
# {svg.totbondwidth} is the Width of nominal rectangle enclosing all bond lines.
if svg.style[0] == 'B':
svg.totbondwidth = 0.40*svg.bondlength
elif svg.style[0] == 'S':
svg.totbondwidth = 1.75*svg.atomradius
# Plot displacement from edges:
svg.margin_wx = 0.5*svg.atomradius # Horiz. margin.
svg.margin_wy = 0.5*svg.atomradius # Vert. margin.
# Update the coordinate range of molecule's plot:
svg.output_figure_body(fm,True)
# Figure dimensions (unscaled, including margins):
svg.fig_wx = 2*svg.margin_wx + (svg.molec_max_x - svg.molec_min_x) # Total figure width.
svg.fig_wy = 2*svg.margin_wy + (svg.molec_max_y - svg.molec_min_y) # Total figure height.
# Overall scale factor (can be changed without changing any other parameter).
svg.scale = 2.0
#----------------------------------------------------------------------
def include_in_bounds (svg, ctrx,ctry,radx,rady):
"""Includes {ctrx,ctry} plus/minus {radx,rady} in the molecule's plot
coordinate range.
Expands {svg.molec_min_x,svg.molec_min_y,svg.molec_max_x,svg.molec_max_y} so
as to include the box from {(ctrx-radx,ctry-rady)} to
{(ctrx+radx,ctry+rady)}."""
if ctrx - radx < svg.molec_min_x: svg.molec_min_x = ctrx - radx
if ctrx + radx > svg.molec_max_x: svg.molec_max_x = ctrx + radx
if ctry - rady < svg.molec_min_y: svg.molec_min_y = ctry - rady
if ctry + rady > svg.molec_max_y: svg.molec_max_y = ctry + rady
#----------------------------------------------------------------------
def output_figure(svg, fm):
"""Writes the figure {fm} to {svg.wr}."""
# Recompute derived dims,in case the client has changed the main params:
svg.compute_derived_dims(fm)
sys.stderr.write("molecule ranges (pixels) = {%d .. %d} × {%d .. %d}\n" % \
(svg.molec_min_x, svg.molec_max_x, svg.molec_min_y, svg.molec_max_y))
# Output the figure:
svg.output_figure_preamble(fm)
svg.wr.write('\n')
svg.output_figure_obj_defs(fm)
svg.wr.write('\n')
# DEBUG:
# b = 0.55*svg.bondlength
# c = 0.10*svg.bondlength
# svg.draw_segment(-b,0*c,+b,0*c,1.00000,svg.bondwidth)
# svg.draw_segment(-b,1*c,+b,1*c,0.25000,svg.bondwidth)
# svg.draw_segment(-b,2*c,+b,2*c,0.50000,svg.bondwidth)
# svg.draw_segment(-b,3*c,+b,3*c,0.75000,svg.bondwidth)
svg.output_figure_body(fm,False)
svg.wr.write('\n')
svg.output_figure_postamble(fm)
sys.stderr.write("done.\n")
#----------------------------------------------------------------------
def output_figure_preamble(svg, fm):
"""Writes the SVG preamble for {fm} to {svg.wr}."""
svg.wr.write( \
'\n' \
'\n' \
'\n' \
'\n' \
'\n' \
'\n' \
)
def output_bond(svg, fm,j,update_bounds):
"""Draws bond number {j} of formula {fm} (which may be unfilled orbitals).
If {update_bounds} is true, does not draw anything and updates
{svg}'s bounding box to enclose the bond. """
i1 = fm.bond_atom_1_index[j]
i2 = fm.bond_atom_2_index[j]
val = fm.bond_valency[j]
if (i2 != None):
svg.output_bond_as_lines(fm,i1,i2,val,update_bounds)
else:
ang = fm.bond_angle[j]
svg.output_bond_as_dots(fm,i1,ang,val,update_bounds)
#----------------------------------------------------------------------
def output_atom(svg, fm,i,depth,update_bounds):
"""Draws atom number {i} of formula {fm}.
If {depth} is 2 or more, outputs a layer of the atom's charge cloud
(if any). If {depth} is 1, does nothing. If {depth} is zero, outputs
only the atom without the charge cloud.
If {update_bounds} is true, does not draw anything and updates
{svg}'s bounding box to enclose the entire atom."""
if (svg.style[0] == 'B'):
svg.output_atom_style_B(fm,i,depth,update_bounds)
elif (svg.style[0] == 'S'):
svg.output_atom_style_S(fm,i,depth,update_bounds)
else:
assert False
#----------------------------------------------------------------------
def output_atom_style_B(svg, fm,i,depth,update_bounds):
"""Draws atom number {i} of formula {fm} in style 'B'.
If {depth} is 2 or more, outputs a layer of the atom's charge cloud
(if any). If {depth} is 1, does nothing. If {depth} is zero, outputs
only the atom without the charge cloud. If {update_bounds} is true,
does not draw anything and updates {svg}'s bounding box."""
# Get the element index {k}:
sym = fm.atom_symbol[i]
k = svg.elem_symbol.index(sym)
# Get the atom's center SVG coordinates {(cx,cy)}:
cx, cy = rn.scale(svg.bondlength, fm.atom_ctr[i][0:2])
# Get the atom's relative radius {rrad}:
rrad = svg.elem_radius[k] # Radius relative to {svg.atomradius}
# Get the atom's color {R,G,B} and label {lab}:
if (depth >= 2):
svg.output_atom_style_B_charge(fm,i,depth,update_bounds)
elif (depth == 1):
# Bond layer, do nothing
return
elif (depth == 0):
# Draw and fill the element's atom proper:
# Color is that of the element:
RGB = svg.elem_color[k]
# Get atom radius:
rad = svg.atomradius * rrad
# Draw ball if appropriate, and note its color {fillRGB}:
if update_bounds:
svg.include_in_bounds (cx,cy,rad,rad)
# Assume that the label is inside the atom.
else:
if rad > 0:
# svg.draw_rectangle(cx,cy,rad,rad,1.0,[1,0,1],False,True) # DEBUG
svg.draw_solid_ball(cx,cy,rad,svg.ringwidth,RGB,True,True)
fillRGB = RGB
else:
fillRGB = [1,1,1]
else:
assert False
#----------------------------------------------------------------------
def output_atom_style_B_charge(svg, fm,i,depth,update_bounds):
"""Draws layer {depth} of the charge cloud of atom number {i}
of formula {fm} in style 'B'.
If {update_bounds} is true, does not draw anything and updates
{svg}'s bounding box."""
# Get the element index {k}:
sym = fm.atom_symbol[i]
k = svg.elem_symbol.index(sym)
# Get the atom's SVG center coords {(cx,cy)}:
cx, cy = rn.scale(svg.bondlength, fm.atom_ctr[i][0:2])
# Get the atom's relative radius {rrad} and center {(x,y)}:w
rrad = svg.elem_radius[k] # Radius relative to {svg.atomradius}
# Paint the charge cloud:
if (fm.atom_chnum[i] == 0): return
# Compute fractional charge {q}:
q = (fm.atom_chnum[i] + 0.0)/(fm.atom_chden[i] + 0.0)
# Compute the color mixing factor {h}:
qabs = math.fabs(q)
if (qabs < 1.0):
h = 0.25 + 0.75*qabs # Fainter color.
else:
h = 1.0 # Full color.
# Compute color {RGB} of charge cloud:
RGB_v = [1.000, 1.000, 1.000] # Color of vacuum.
if q < 0.0:
RGB_n = [0.400, 0.700, 1.000] # Color of one electron.
RGB = rn.mix(h, RGB_n, 1-h, RGB_v)
else:
RGB_p = [1.000, 0.700, 0.400] # Color of one proton.
RGB = rn.mix(h, RGB_p, 1-h, RGB_v)
# Compute inner radius {rad0} and outer radius {rad1} from {area}:
rad0 = svg.atomradius * rrad
rad1 = svg.charge_radius(rrad,q)
if update_bounds:
radm = max(max(rad0,rad1), svg.atomradius)
svg.include_in_bounds (cx,cy,radm,radm)
else:
# Draw fuzzy ball appropriate:
if rad1 > 0:
# svg.draw_rectangle(cx,cy,rad1,rad1,1.0,[1,0,1],False,True) # DEBUG
svg.draw_fuzzy_ball(cx,cy,rad0,rad1,RGB,depth)
#----------------------------------------------------------------------
def output_atom_style_S(svg, fm,i,depth,update_bounds):
"""Draws atom number {i} of formula {fm} in style 'S'.
If {depth} is 1 or more, does nothing. If {depth} is zero, outputs
only the atom without the charge cloud. If {update_bounds} is true,
does not draw anything and updates {svg}'s bounding box."""
# Get the element index {k}:
sym = fm.atom_symbol[i]
k = svg.elem_symbol.index(sym)
# Get the atom's center {(cx,cy)}:
cx, cy = rn.scale(svg.bondlength, fm.atom_ctr[i][0:2])
if (depth >= 2):
# Charge cloud layer, do nothing for now.
return
elif (depth == 1):
# Bond layer, do nothing
return
elif (depth == 0):
# Define {fh} the nominal font height to use to draw the label, {fwx,fwy} the assumed letter dims.
fh = svg.atomftheight
# Get the label {lab} (either the element's symbol or the charge) and its visible length {labLen}.
lab = svg.elem_label[k]
fntsz = svg.elem_fsize[k] # Relative font size to use.
fh = fntsz*svg.atomftheight
if (lab == '+'):
# Make up the label from the charge,reduce font size:
lab = svg.atom_charge_string(fm,i)
# The circle-signs are 7 bytes in HTML enity code so:
labLen = max(1, len(lab)-6)
else:
labLen = len(lab)
# Estimated letter width and height for bounding box:
fwx = 0.75*fh # Est letter width in pixels.
fwy = 1.00*fh # Est letter height in pixels.
# Get the estimated radii {radx,rady} of the label and {radc} of the circle (pixels).
if (lab == ''):
# No label, no disk:
radx = 0
rady = 0
radc = 0
else:
# Assume that the letters have aspect {3:4}:
radx = 0.75*labLen*0.5*fwx
rady = 0.5*fwy
# Assume all atoms except '.' have the same ghost radius:
radc = svg.atomradius
sys.stderr.write(" atom %d at [%.2f %.2f] label radii [%.2f %.2f]" % (i,cx,cy,radx,rady))
sys.stderr.write(" disk radius %.2f\n" % radc)
if update_bounds:
svg.include_in_bounds (cx,cy,max(radc,radx),max(radc,rady))
else:
# Draw label:
if lab != '':
# Print label string with proper font formatting:
txt = make_roman_style(lab)
tx = cx + fh*svg.elem_labdx[k]
ty = cy + fh*svg.elem_labdy[k]
bold = False;
svg.draw_label(tx,ty,txt,fh,bold,[0,0,0])
else:
assert False
#----------------------------------------------------------------------
def atom_charge_string(svg, fm,i):
"""Converts the charge of atom number {i} of formula {fm} to a string for style 'S'."""
# Get the element index {k}:
sym = fm.atom_symbol[i]
k = svg.elem_symbol.index(sym)
# Get the atom's charge {qn/qd}:
qn = fm.atom_chnum[i]
qd = fm.atom_chden[i]
if (qn == 0): return ''
assert (qd > 0)
# Format charge as string:
if (qn > 0):
chs = "⊕" # Unicode circle plus, U+2295
else:
chs = "⊖" # Unicode circle minus, U+2296
qn = abs(qn)
if ((qn % qd) == 0):
# Charge is integer.
if (qn > qd): chs = "%d%s" % (qn,chs)
else:
# Charge is fractional:
# Reduce fraction to lowest terms
qo = gcd(qn,qd)
qn = qn/qo
qd = qd/qo
chs = "%d/%d%s" % (qn,qd,chs)
return chs
#----------------------------------------------------------------------
def output_label(svg, fm,i,depth,update_bounds):
"""Draws label number {i} of formula {fm}.
If {depth} is 1 or more, does nothing. If {depth} is zero, outputs
the label. If {update_bounds} is true, does not draw anything and
updates {svg}'s bounding box to enclose the entire label."""
# Get the text {txt}.
txt = fm.label_text[i]
if (txt == ''):
# Empty label, nothing to do:
return
elif (depth >= 1):
# Charge or bond layer, do nothing
return
elif (depth == 0):
# Font size:
fh = fm.label_ftsize[i]*svg.atomftheight
bold = fm.label_bold[i]
fwy = fh # Assumed font height in pixels.
fwx = 0.75*fwy # Est font width in pixels, assuming aspect ratio {3:4}.
fRGB = fm.label_color[i]
# Get the label's center {(x,y)}:
cx, cy = rn.scale(svg.bondlength, fm.label_ctr[i])
# Compute the visible length {labLen}.
# Also define {fh} the nominal font height to use
# to draw the text, {fwx,fwy} the assumed letter dims.
labLen = len(txt)
# Get the estimated radii {radx,rady} of the text (pixels).
# Assume that the letters have aspect {3:4}:
radx = 0.5*labLen*fwx
rady = 0.5*fwy
sys.stderr.write(" text %d at [%.2f %.2f] text radii [%.2f %.2f]\n" % (i,cx,cy,radx,rady))
if update_bounds:
svg.include_in_bounds (cx,cy,radx,rady)
else:
# Print text string with proper font formatting:
stxt = make_roman_style(txt)
tx = cx
ty = cy - 0.35*fwy
svg.draw_label(tx,ty,stxt,fh,bold,fRGB)
else:
assert False
#----------------------------------------------------------------------
def output_bond_as_lines(svg, fm,i1,i2,val,update_bounds):
"""Draws a set of bond lines from atom {i1} to atom {i2} with valence {val}.
The integer part of {val} specifies the number of solid lines. If
the fractional part of {val} is not zero, adds a dashed line. If
{update_bounds} is true, does not draw anything and updates {svg}'s
bounding box. """
# Assume that bond lines are always inside the
# bounding box of the two atoms:
if val > 0 and not update_bounds:
# sys.stderr.write("drawing %r bond lines from atom %r to atom %r\n" % (val,i1,i2))
# Get the atom centers {c1,c2} (relative to {svg.bondlength}) and {a1,a2} (scaled):
c1 = rn.scale(svg.bondlength, fm.atom_ctr[i1])
c2 = rn.scale(svg.bondlength, fm.atom_ctr[i2])
a1 = c1[0:2]
a2 = c2[0:2]
# Get the atom's radii {r1,r2} (relative to {svg.atomradius}) and {s1,s2} (scaled):
sym1 = fm.atom_symbol[i1]
k1 = svg.elem_symbol.index(sym1)
r1 = svg.elem_radius[k1]
sym2 = fm.atom_symbol[i2]
k2 = svg.elem_symbol.index(sym2)
r2 = svg.elem_radius[k2]
s1 = r1*svg.atomradius
s2 = r2*svg.atomradius
# Number of lines, total and solid:
nlines = int(val + 0.999)
nsolid = int(val)
assert(nlines > 0)
# Fraction of last line:
frline = val - nsolid
# Compute longitudinal {X,Y} unit vector {dir12} from atom 1 to atom 2:
dir12, dist12 = rn.dir(rn.sub(a2, a1))
# Compute perpendicular {X,Y} unit vector {dirp}:
dirp = [+dir12[1],-dir12[0]]
# Compute "perspective" bond width scaling at each end:
rwdmax = 3.0*3/(nlines+2);
rwdmin = 0.6;
if c1[2] > c2[2]:
# Atom 1 is closer:
twd1 = 1.2*svg.totbondwidth; wd1 = rwdmax*svg.bondwidth
twd2 = 0.4*svg.totbondwidth; wd2 = rwdmin*svg.bondwidth
elif c1[2] < c2[2]:
# Atom 2 is closer:
twd1 = 0.4*svg.totbondwidth; wd1 = rwdmin*svg.bondwidth
twd2 = 1.2*svg.totbondwidth; wd2 = rwdmax*svg.bondwidth
else:
# Same depth:
twd1 = svg.totbondwidth; wd1 = svg.bondwidth
twd2 = svg.totbondwidth; wd2 = svg.bondwidth
# Compute end points {p1,p2} of bond axis:
bdist1 = max(svg.bindatradius*s1, svg.bondwidth) # Safe
bdist2 = max(svg.bindatradius*s2, svg.bondwidth) # Safe
if svg.style == "S2" and nlines == 1:
# In style "S2", single bonds to a carbon atom extend
# all the way to the atom's center. Triple bonds
# and double bonds for now will stop at some distance
# from it, as usual
if sym1 == "C": bdist1 = 0
if sym2 == "C": bdist2 = 0
p1 = rn.add(a1, rn.scale(+bdist, dir12))
p2 = rn.add(a2, rn.scale(-bdist, dir12))
for j in range(nlines):
# Draw line {j}:
if (j >= nsolid):
frdash = frline
else:
frdash = 1.0
fr = (j + 1)/(nlines+1) - 0.5
# Get {XYZ} coordinates of line endpoints. Use same {Z} as the atom centers.
q1 = rn.add(p1, rn.scale(fr*twd1, dirp))
q2 = rn.add(p2, rn.scale(fr*twd2, dirp))
svg.draw_bond_line(q1,q2,wd1,wd2,frdash)
#----------------------------------------------------------------------
def draw_bond_line(svg, q1,q2,wd1,wd2,frdash):
"""Draws a bond line between 2D points {q1} and {q2} with width {wd1} at {q1}
and width {wd2} at {q2}. The {frdash} parameter
is passed to {svg.segment}."""
assert len(q1) == 2 and len(q2) == 2, "invalid coords"
if wd1 == wd2:
svg.draw_segment(q1[0],q1[1],q2[0],q2[1],frdash,wd1)
else:
dxy, dlen = rn.dir([ q2[1]-q1[1], q1[0]-q2[0] ]) # Perp direction vector.
wdmin = min(wd1,wd2)
wdmax = max(wd1,wd2)
n = int(math.ceil(1.1*wdmax/wdmin))
assert n >= 2
d1 = wd1 - wdmin
d2 = wd2 - wdmin
for k in range(n):
r = k/(n-1) - 0.5;
x1 = q1[0] + r*d1*dxy[0]
y1 = q1[1] + r*d1*dxy[1]
x2 = q2[0] + r*d2*dxy[0]
y2 = q2[1] + r*d2*dxy[1]
svg.draw_segment(x1,y1,x2,y2,frdash,wdmin)
#----------------------------------------------------------------------
def output_bond_as_dots(svg, fm,i,ang,nf,update_bounds):
"""Draws a set of {nf} dots towards angle {ang} on atom at {(x1,y1)} (px, user axes).
The parameter {nf} is rounded up to integer.
If {update_bounds} is true, does not draw anything and updates {svg}'s bounding box."""
n = trunc(ceil(nf))
# sys.stderr.write("drawing %r bond dots on atom %r at angle %r\n" % (n,i,ang))
a1 = rn.scale(svg.bondlength, fm.atom_ctr[i])[0:2]
# sys.stderr.write(" atom center = %r\n" % p1)
# Compute unit direction vector {ex,ey}:
ex = math.cos(ang)
ey = math.sin(ang)
e = [ex, ey]
# Compute lateral displacement vector {d} between dots:
d = rn.scale(svg.totbondwidth/(n+1), [+ey,-ex])
# Compute end points {p1,p2} of dots (actually fat segments):
p1 = rn.add(a1, rn.scale(svg.atomradius + svg.dotsdist, e))
p2 = rn.add(p1, rn.scale(0.05*svg.dotwidth, e))
hn = 0.5*(n - 1.0)
if update_bounds:
ctrx = 0.5*(p1[0] + p2[0])
ctry = 0.5*(p1[1] + p2[1])
radx = abs(hn*d[0]) + abs(0.5*(p1[0] - p2[0])) + 0.5*svg.dotwidth
rady = abs(hn*d[1]) + abs(0.5*(p1[1] - p2[1])) + 0.5*svg.dotwidth
svg.include_in_bounds (ctrx,ctry,radx,rady)
else:
sys.stderr.write(" @@ output_bond_as_dots: %r --> %r x %d\n" % (p1,p2,n))
for j in range(n):
# Draw dot {j}:
fr = (j - hn)
q1 = rn.add(p1, rn.scale(fr, d))
q2 = rn.add(p2, rn.scale(fr, d))
svg.draw_big_dot(q1[0],q1[1],q2[0],q2[1])
#----------------------------------------------------------------------
def draw_solid_ball(svg, cx,cy,rad,penWd,RGB,fill,draw):
"""Paints a disk with color {RGB}, center {(cx,cy)}, and
radius {rad} (in px, user axes).
The color {RGB} is a triplet of floats in [0_1].
If {fill} is 1 the disk will be filled with color {RGB}, otherwise
it will be left unfilled. Then, if {draw} is 1 the disk's outline
will be stroked with a burned-out version of {RGB} and pen width
{penWd}, otherwise it will be left unstroked. The stroked outline
will be on the inside of the disk so that the overall radius will be
{rad}."""
# Compute fill option {fillOp}:
if fill:
# Fill with solid color:
fillRGB = RGB
fillOp = 'fill="rgb(' + rgbF(fillRGB) + ')"'
else:
# No filling:
fillOp = 'fill="none"'
# Compute stroke option {drawOp} and adjusted radius {radp}:
if draw:
# Solid color, with border:
drawRGB = rn.mix(0.3, [0,0,0], 0.7, RGB)
drawOp = 'stroke-width="' + wdF(penWd) + 'px" stroke="rgb(' + rgbF(drawRGB) + ')"'
radp = rad - 0.5*penWd
else:
# Solid color, no border:
drawOp = 'stroke="none"'
radp = rad
# Convert to image axes:
cxp = (cx - svg.molec_min_x) + svg.margin_wx
cyp = (svg.molec_max_y - cy) + svg.margin_wy
if rad > 0:
svg.wr.write( \
' \n' \
' \n' \
' \n' \
)
#----------------------------------------------------------------------
def draw_rectangle(svg, cx,cy,rx,ry,penWd,RGB,fill,draw):
"""Paints a rectangle with color {RGB}, center {(cx,cy)},
and radiii {rx,ry} (in px, user axes).
The color {RGB} is a triplet of floats in [0_1].
If {fill} is 1 the rectangle will be filled with color {RGB},
otherwise it will be left unfilled. Then, if {draw} is 1 the
rectangle's outline will be stroked with a burned-out version of
{RGB} and pen width {penWd}, otherwise it will be left unstroked.
The stroked outline will be on the inside of the rectangle so that
the overall radii will be {rx,ry}."""
# Compute fill option {fillOp}:
if fill:
# Fill with solid color:
fillRGB = RGB
fillOp = 'fill="rgb(' + rgbF(fillRGB) + ')"'
else:
# No filling:
fillOp = 'fill="none"'
# Compute draw option {drawOp}:
if draw:
# Solid color, with border:
drawRGB = rn.mix(0.3, [0,0,0], 0.7, RGB)
drawOp = 'stroke-width="' + wdF(penWd) + 'px" stroke="rgb(' + rgbF(drawRGB) + ')"'
rxp = rx - 0.5*penWd
ryp = ry - 0.5*penWd
else:
# Solid color, no border:
drawOp = 'stroke="none"'
rxp = rx
ryp = ry
# Convert center coords to image axes:
cxp = (cx - svg.molec_min_x) + svg.margin_wx
cyp = (svg.molec_max_y - cy) + svg.margin_wy
if (rx > 0) and (ry > 0):
svg.wr.write( \
' \n' \
' \n' \
' \n' \
)
#----------------------------------------------------------------------
def draw_fuzzy_ball(svg, cx,cy,rad0,rad1,RGB,depth):
"""Paints layer {depth} of a fuzzy disk with color {RGB},
center {(cx,cy)} and radius {rad1} (in px, user axes).
The color will be {RGB} inside a circle with radius {rad0} and will
fade to white at radius {rad1}. The {depth} depth should decrease
from 2 to {svg.maxdepth}."""
if rad1 <= 0: return
# Convert to image axes:
cxp = (cx - svg.molec_min_x) + svg.margin_wx
cyp = (svg.molec_max_y - cy) + svg.margin_wy
# Get the number {n} of charge layers:
n = svg.maxdepth - 1
# Get the index {i} of the layer, from bottom (0) to top {n-1}:
i = svg.maxdepth - depth
# Compute the disk radius {rad} so that the annuli have the same area:
frad = (i + 0.0)/(n + 0.0)
rad = sqrt(rad1*rad1 - frad*(rad1*rad1 - rad0*rad0))
# Compute the color of the disk:
fcol = (i + 1.0)/(n + 0.0)
dkRGB = rn.mix(1-fcol, [1,1,1], fcol, RGB)
if rad > 0:
svg.wr.write( \
' ' \
' \n' \
' \n' \
)
#----------------------------------------------------------------------
def draw_label(svg, cx,cy,txt,fh,bold,fRGB):
"""Ouputs a label {txt} at {(cx,cy)} (in px, user axes)
with font height {fh} and color {fRGB}.
The reference point is at bottom center of text."""
if (txt != ''):
# Convert to image axes:
cxp = (cx - svg.molec_min_x) + svg.margin_wx
cyp = (svg.molec_max_y - cy) + svg.margin_wy
# text fill and outline color:
fill_color = "rgb(" + rgbF(fRGB) + ")"
svg.wr.write( \
' ' + txt + '\n' \
)
#----------------------------------------------------------------------
def draw_segment(svg, x1,y1,x2,y2,frdash,bwid):
"""Draws a straight line from {(x1,y1)} to {(x2,y2)} (in px, user axes).
The {frdash} parameter is the fraction of dashes in line: 0.0 is
blank, 1.0 is solid. The line width is {bwid}."""
# Convert to pixel coordinates:
x1p = (x1 - svg.molec_min_x) + svg.margin_wx
y1p = (svg.molec_max_y - y1) + svg.margin_wy
x2p = (x2 - svg.molec_min_x) + svg.margin_wx
y2p = (svg.molec_max_y - y2) + svg.margin_wy
# Fix dash fraction:
if (frdash > 0.99):
# Practically solid, use solid:
frdash = 1.0
elif (frdash < 0.01):
# Practically blank, use blank:
frdash = 0.0
else:
# Round slightly awy from 0 and 1:
frdash = 0.05 + 0.90*frdash
# Decide line width {bwid}, line cap style {bcap} and lengthening of dashes due to it.
if svg.style[0] == 'B':
bcap = "round"
ebut = 0.7*bwid
elif svg.style[0] == 'S':
bcap = "butt"
ebut = 0
else:
assert False
if frdash > 0.0:
# Define dash pattern:
if (frdash == 1.0):
dash_style = ''
else:
peri = 2*svg.dashlength # Length of dash + gap.
# Adjust {peri} so that there is an odd integer number of periods in line:
dist = math.hypot(x1p-x2p, y1p-y2p)
nper = 2*int(dist/peri/2 - 0.5) + 1
# There must be at least two dashes:
if (nper < 2): nper = 2
peri = dist/(nper + 0.0)
# Compute lengths {dash} of dashes and {gapp} of gaps:
dash = peri*frdash - ebut # Nominal length of dash.
if dash <= 0.001: dash = 0.001
gapp = peri-dash # Length of gap.
# Compute offset so that the dashes are centered in the periods
doff = peri-(gapp + dist - nper*peri)/2.0 # Dash offset.
# doff = 0.0
dash_style = 'stroke-dasharray="' + dsF(dash) + ',' + dsF(gapp) + '" stroke-dashoffset="' + dsF(doff) + '"'
svg.wr.write( \
' \n' \
)
#----------------------------------------------------------------------
def draw_big_dot(svg, x1,y1,x2,y2):
"""Draws a big dot, actually a fat segment from {(x1,y1)} to {(x2,y2)}
(in px, user axes)."""
# Convert to pixel coordinates:
x1p = (x1 - svg.molec_min_x) + svg.margin_wx
y1p = (svg.molec_max_y - y1) + svg.margin_wy
x2p = (x2 - svg.molec_min_x) + svg.margin_wx
y2p = (svg.molec_max_y - y2) + svg.margin_wy
# Decide line width {bwid}, line cap style {bcap} and lengthening of dashes due to it.
bwid = svg.dotwidth
bcap = "round"
svg.wr.write( \
' \n' \
)
#----------------------------------------------------------------------
def charge_radius(svg, rrad,q):
"""Computes the radius of the cloud charge for an atom with
relative radius {rrad} and charge {q}.
The radius {rrar} is relative to {svg.atomradius}.
If the {charge} is zero, returns {rrad*svg.atomradius}."""
if q == 0: return rrad*svg.atomradius
qabs = math.fabs(q)
# Compute the relative area {area} of the cloud:
wmin = 0.75 # Min annulus width rel {atomradius} if {rad = atomradius}.
amin = (1 + wmin)*(1 + wmin) - 1 # Min area of annulus rel. {pi*atomradius^2}
if (qabs < 1.0):
# Minimum area:
area = amin
else:
# Area proportional to qabs:
area = amin*qabs
return svg.atomradius * sqrt(amin + rrad*rrad)
#----------------------------------------------------------------------
def rel_bond_length(svg, v) :
"""Computes the relative bond length for a bond with valency {v}."""
return math.pow(svg.doublebondlength, v-1)
#----------------------------------------------------------------------
#----------------------------------------------------------------------
def rgbF(RGB):
"""Formats {RGB} as an SVG color triplet '{R},{G},{B}' in 0..255 range."""
return "%d,%d,%d" % (int(255*RGB[0]+0.5),int(255*RGB[1]+0.5),int(255*RGB[2]+0.5))
#----------------------------------------------------------------------
def ptF(x,y):
"""Formats {x,y} as two coordinates separated by a comma."""
return "%+06.1f,%+06.1f" % (x,y)
#----------------------------------------------------------------------
def radF(r):
"""Formats {r} as a radius."""
return "%06.1f" % r
#----------------------------------------------------------------------
def xyF(x,y,tag):
"""Formats {x,y} as '{tag}x=\"{x}\" {tag}y=\"{y}\"'."""
return "%sx=\"%+06.1f\" %sy=\"%+06.1f\"" % (tag,x,tag,y)
#----------------------------------------------------------------------
def wdF(wd):
"""Formats {wd} as a line width."""
return "%.2f" % wd
#----------------------------------------------------------------------
def dsF(wd):
"""Formats {wd} as a dash/gap length."""
return "%.2f" % wd
#----------------------------------------------------------------------
def intF(x):
"""Converts the decimal number {x} to string."""
return ("%r" % x)
#----------------------------------------------------------------------
def make_italic_style(txt):
"""Packages a string with italic style markup."""
if (txt != ''):
return '' + txt + ''
else:
return ''
#----------------------------------------------------------------------
def make_roman_style(txt):
"""Packages a string with normal style (non-italic) markup."""
if (txt != ''):
return '' + txt + ''
else:
return ''
#----------------------------------------------------------------------
def gcd(x,y):
"""Greatest common divisor of integers {x} and {y}."""
while y != 0:
r = x % y; x = y; y = r
return x
#----------------------------------------------------------------------
def luminance(RGB):
"""CIE luminance of color {RGB}."""
return 0.298911*RGB[0] + 0.586611*RGB[1] + 0.114478*RGB[2]
#----------------------------------------------------------------------
def style_descr(style):
"""Long description of style {style}."""
if style == 'B1':
return "'Large balls' style"
elif style == 'B2':
return "'Smaller balls' style"
elif style == 'S1':
return "Letter symbol style"
else:
assert False
#----------------------------------------------------------------------