def _draw_colored_line( self, start, end, line_width=1, capstyle=cairo.LINE_CAP_BUTT, start_color=(0,0,0), end_color=(0,0,0)): x1,y1 = start x2,y2 = end length = geometry.point_distance( x1,y1,x2,y2) xn2,yn2 = geometry.elongate_line( x1,y1,x2,y2, -0.5*length) line1 = [(x1,y1),(xn2,yn2)] xn1,yn1 = geometry.elongate_line( x2,y2,x1,y1, -0.5*length) line2 = [(xn1,yn1),(x2,y2)] self.context.set_line_cap( cairo.LINE_CAP_BUTT) # this is forced here self.context.set_line_width( line_width) for line,color in zip( [line1,line2], [start_color,end_color]): self.context.set_source_rgb( *color) self._create_cairo_path( line, closed=False) self.context.stroke()
def _draw_colored_line(self, start, end, line_width=1, capstyle=cairo.LINE_CAP_BUTT, start_color=(0, 0, 0), end_color=(0, 0, 0)): x1, y1 = start x2, y2 = end length = geometry.point_distance(x1, y1, x2, y2) xn2, yn2 = geometry.elongate_line(x1, y1, x2, y2, -0.5 * length) line1 = [(x1, y1), (xn2, yn2)] xn1, yn1 = geometry.elongate_line(x2, y2, x1, y1, -0.5 * length) line2 = [(xn1, yn1), (x2, y2)] self.context.set_line_cap(cairo.LINE_CAP_BUTT) # this is forced here self.context.set_line_width(line_width) for line, color in zip([line1, line2], [start_color, end_color]): self.context.set_source_rgb(*color) self._create_cairo_path(line, closed=False) self.context.stroke()
def _draw_vertex( self, v): pos = sum( [(a.x < v.x) and -1 or 1 for a in v.neighbors if abs(a.x-v.x)>0.2]) if 'show_symbol' in v.properties_: show_symbol = v.properties_['show_symbol'] else: show_symbol = (v.symbol != "C" or v.degree == 0 or self.show_carbon_symbol) if show_symbol: x = v.x y = v.y text = v.symbol hs = "" if self.show_hydrogens_on_hetero or v.properties_.get( 'show_hydrogens', False): if v.free_valency == 1: hs = "H" elif v.free_valency > 1: hs = "H<sub>%d</sub>" % v.free_valency if not hs: pos = -1 if pos <= 0: text += hs else: text = hs + text # charge charge = "" if v.charge == 1: charge = "<sup>+</sup>" elif v.charge == -1: charge = "<sup>−</sup>" elif v.charge > 1: charge = "<sup>%d+</sup>" % v.charge elif v.charge < -1: charge = "<sup>%d−</sup>" % abs( v.charge) if charge: if self._is_there_place( v, v.x+3, v.y-2) or v.charge < 0: # we place negative charge regardless of available place # otherwise minus might be mistaken for a bond text += charge charge = "" # coloring if self.color_atoms: color = self.atom_colors.get( v.symbol, (0,0,0)) else: color = (0,0,0) center_letter = pos <= 0 and 'first' or 'last' bbox = self._draw_text( (x,y), text, center_letter=center_letter, color=color) self._vertex_to_bbox[v] = ((x,y), geometry.expand_rectangle( bbox, self.space_around_atom)) # sometimes charge is done here, if it wasn't done before if charge: assert v.charge > 0 # if charge was not dealt with we change its appearance from 2+ to ++ charge = v.charge * "+" if self._is_there_place( v, v.x, v.y-10): angle = 1.5*math.pi elif self._is_there_place( v, v.x, v.y+10): angle = 0.5*math.pi else: angle = self._find_place_around_atom( v) self.context.set_font_size( self.subscript_size_ratio * self.font_size) xbearing, ybearing, width, height, x_advance, y_advance = self.context.text_extents( charge) x0 = v.x + 40*math.cos( angle) y0 = v.y + 40*math.sin( angle) line = (v.x,v.y,x0,y0) charge_bbox = [x0-0.5*width,y0-0.5*height,x0+0.5*width,y0+0.5*height] x1, y1 = geometry.intersection_of_line_and_rect( line, bbox, round_edges=0) x2, y2 = geometry.intersection_of_line_and_rect( line, charge_bbox, round_edges=0) x2, y2 = geometry.elongate_line( x1,y1,x2,y2, -self.space_around_atom) x = x0 + x1 - x2 y = y0 + y1 - y2 # draw self.context.set_source_rgb( *color) self.context.move_to( round( x-xbearing-0.5*width), round( y+0.5*height)) self.context.show_text( charge)
def _draw_edge( self, e): def draw_plain_or_colored_line( _start, _end, second=False): """second means if this is not the main line, drawing might be different""" if not has_shown_vertex or not self.color_bonds: if not second: self._draw_line( _start, _end, line_width=self.line_width, capstyle=cairo.LINE_CAP_ROUND) else: self._draw_line( _start, _end, line_width=self.line_width, capstyle=cairo.LINE_CAP_BUTT) else: self._draw_colored_line( _start, _end, line_width=self.line_width, start_color=color1, end_color=color2) def draw_plain_or_colored_wedge( _start, _end): x1, y1 = _start x2, y2 = _end x, y, x0, y0 = geometry.find_parallel( x1, y1, x2, y2, self.wedge_width/2.0) xa, ya, xb, yb = geometry.find_parallel( x1, y1, x2, y2, self.line_width/2.0) # no coloring now if not has_shown_vertex or not self.color_bonds: self._create_cairo_path( [(xa, ya), (x0, y0), (2*x2-x0, 2*y2-y0), (2*x1-xa, 2*y1-ya)], closed=True) self.context.set_source_rgb( 0,0,0) self.context.fill() else: # ratio 0.4 looks better than 0.5 because the area difference # is percieved more than length difference ratio = 0.4 xm1 = ratio*xa + (1-ratio)*x0 ym1 = ratio*ya + (1-ratio)*y0 xm2 = (1-ratio)*(2*x2-x0) + ratio*(2*x1-xa) ym2 = (1-ratio)*(2*y2-y0) + ratio*(2*y1-ya) self.context.set_source_rgb( *color1) self._create_cairo_path( [(xa,ya), (xm1,ym1), (xm2,ym2), (2*x1-xa, 2*y1-ya)], closed=True) self.context.fill() self.context.set_source_rgb( *color2) self._create_cairo_path( [(xm1,ym1), (x0, y0), (2*x2-x0, 2*y2-y0), (xm2,ym2)], closed=True) self.context.fill() def draw_plain_or_colored_hatch( _start, _end): x1, y1 = _start x2, y2 = _end # no coloring now x, y, x0, y0 = geometry.find_parallel( x1, y1, x2, y2, self.wedge_width/2.0) xa, ya, xb, yb = geometry.find_parallel( x1, y1, x2, y2, self.line_width/2.0) d = math.sqrt( (x1-x2)**2 + (y1-y2)**2) # length of the bond if d == 0: return # to prevent division by zero dx1 = (x0 - xa)/d dy1 = (y0 - ya)/d dx2 = (2*x2 -x0 -2*x1 +xa)/d dy2 = (2*y2 -y0 -2*y1 +ya)/d # we have to decide if the first line should be at the position of the first atom draw_start = 1 # is index not boolean if not v1 in self._vertex_to_bbox and v1.occupied_valency > 1: draw_start = 1 draw_end = 1 # is added to index not boolean if not v2 in self._vertex_to_bbox and v2.occupied_valency > 1: draw_end = 0 # adjust the step length step_size = 2*(self.line_width) ns = round( d / step_size) or 1 step_size = d / ns # now we finally draw self.context.set_line_cap( cairo.LINE_CAP_BUTT) self.context.set_source_rgb( *color1) middle = 0.5 * (draw_start + int( round( d/ step_size)) + draw_end - 2) for i in range( draw_start, int( round( d/ step_size)) +draw_end): coords = [xa+dx1*i*step_size, ya+dy1*i*step_size, 2*x1-xa+dx2*i*step_size, 2*y1-ya+dy2*i*step_size] if coords[0] == coords[2] and coords[1] == coords[3]: if (dx1+dx2) > (dy1+dy2): coords[0] += 1 else: coords[1] += 1 self._create_cairo_path( [coords[:2],coords[2:]]) if i >= middle: self.context.stroke() self.context.set_source_rgb( *color2) self.context.stroke() # code itself # at first detect the need to make 3D adjustments self._transform = transform3d.transform3d() self._invtransform = transform3d.transform3d() transform = None if e.order > 1: atom1,atom2 = e.vertices for n in atom1.neighbors + atom2.neighbors: # e.atom1 and e.atom2 are in this list as well if n.z != 0: # engage 3d transform prior to detection of where to draw transform = self._get_3dtransform_for_drawing( e) #transform = None break if transform: for n in atom1.neighbors + atom2.neighbors: n.coords = transform.transform_xyz( *n.coords) self._transform = transform self._invtransform = transform.get_inverse() # // end of 3D adjustments # now the code itself coords = self._where_to_draw_from_and_to( e) if coords: start = coords[:2] end = coords[2:] v1, v2 = e.vertices if self.color_bonds: color1 = self.atom_colors.get( v1.symbol, (0,0,0)) color2 = self.atom_colors.get( v2.symbol, (0,0,0)) else: color1 = color2 = (0,0,0) has_shown_vertex = bool( [1 for _v in e.vertices if _v in self._vertex_to_bbox]) if e.order == 1: if e.type == 'w': draw_plain_or_colored_wedge( start, end) elif e.type == 'h': draw_plain_or_colored_hatch( start, end) else: draw_plain_or_colored_line( start, end) if e.order == 2: side = 0 # find how to center the bonds # rings have higher priority in setting the positioning in_ring = False for ring in self.molecule.get_smallest_independent_cycles_dangerous_and_cached(): double_bonds = len( [b for b in self.molecule.vertex_subgraph_to_edge_subgraph(ring) if b.order == 2]) if v1 in ring and v2 in ring: in_ring = True side += double_bonds * reduce( operator.add, [geometry.on_which_side_is_point( start+end, (a.x,a.y)) for a in ring if a!=v1 and a!=v2]) # if rings did not decide, use the other neigbors if not side: for v in v1.neighbors + v2.neighbors: if v != v1 and v!= v2: side += geometry.on_which_side_is_point( start+end, (v.x, v.y)) # if neighbors did not decide either if not side and (in_ring or not has_shown_vertex): if in_ring: # we don't want centered bonds inside rings side = 1 # select arbitrary value else: # bond between two unshown atoms - we want to center them only in some cases if len( v1.neighbors) == 1 and len( v2.neighbors) == 1: # both atoms have only one neighbor side = 0 elif len( v1.neighbors) < 3 and len( v2.neighbors) < 3: # try to figure out which side is more towards the center of the molecule side = reduce( operator.add, [geometry.on_which_side_is_point( start+end, (a.x,a.y)) for a in self.molecule.vertices if a!=v1 and a!=v2], 0) if not side: side = 1 # we choose arbitrary value, we don't want centering if side: draw_plain_or_colored_line( start, end) x1, y1, x2, y2 = geometry.find_parallel( start[0], start[1], end[0], end[1], self.bond_width*misc.signum( side)) # shorten the second line length = geometry.point_distance( x1,y1,x2,y2) if v2 not in self._vertex_to_bbox: x2, y2 = geometry.elongate_line( x1, y1, x2, y2, -self.bond_second_line_shortening*length) if v1 not in self._vertex_to_bbox: x1, y1 = geometry.elongate_line( x2, y2, x1, y1, -self.bond_second_line_shortening*length) draw_plain_or_colored_line( (x1, y1), (x2, y2), second=True) else: for i in (1,-1): x1, y1, x2, y2 = geometry.find_parallel( start[0], start[1], end[0], end[1], i*self.bond_width*0.5) draw_plain_or_colored_line( (x1, y1), (x2, y2)) elif e.order == 3: self._draw_line( start, end, line_width=self.line_width) for i in (1,-1): x1, y1, x2, y2 = geometry.find_parallel( start[0], start[1], end[0], end[1], i*self.bond_width*0.7) draw_plain_or_colored_line( (x1, y1), (x2, y2), second=True) if transform: # if transform was used, we need to transform back for n in atom1.neighbors + atom2.neighbors: n.coords = self._invtransform.transform_xyz( *n.coords)
def _draw_vertex(self, v): pos = sum([(a.x < v.x) and -1 or 1 for a in v.neighbors if abs(a.x - v.x) > 0.2]) if 'show_symbol' in v.properties_: show_symbol = v.properties_['show_symbol'] else: show_symbol = (v.symbol != "C" or v.degree == 0 or self.show_carbon_symbol) if show_symbol: x = v.x y = v.y text = v.symbol hs = "" if self.show_hydrogens_on_hetero or v.properties_.get( 'show_hydrogens', False): if v.free_valency == 1: hs = "H" elif v.free_valency > 1: hs = "H<sub>%d</sub>" % v.free_valency if not hs: pos = -1 if pos <= 0: text += hs else: text = hs + text # charge charge = "" if v.charge == 1: charge = "<sup>+</sup>" elif v.charge == -1: charge = "<sup>−</sup>" elif v.charge > 1: charge = "<sup>%d+</sup>" % v.charge elif v.charge < -1: charge = "<sup>%d−</sup>" % abs(v.charge) if charge: if self._is_there_place(v, v.x + 3, v.y - 2) or v.charge < 0: # we place negative charge regardless of available place # otherwise minus might be mistaken for a bond text += charge charge = "" # coloring if self.color_atoms: color = self.atom_colors.get(v.symbol, (0, 0, 0)) else: color = (0, 0, 0) center_letter = pos <= 0 and 'first' or 'last' bbox = self._draw_text((x, y), text, center_letter=center_letter, color=color) self._vertex_to_bbox[v] = ((x, y), geometry.expand_rectangle( bbox, self.space_around_atom)) # sometimes charge is done here, if it wasn't done before if charge: assert v.charge > 0 # if charge was not dealt with we change its appearance from 2+ to ++ charge = v.charge * "+" if self._is_there_place(v, v.x, v.y - 10): angle = 1.5 * math.pi elif self._is_there_place(v, v.x, v.y + 10): angle = 0.5 * math.pi else: angle = self._find_place_around_atom(v) self.context.set_font_size(self.subscript_size_ratio * self.font_size) xbearing, ybearing, width, height, x_advance, y_advance = self.context.text_extents( charge) x0 = v.x + 40 * math.cos(angle) y0 = v.y + 40 * math.sin(angle) line = (v.x, v.y, x0, y0) charge_bbox = [ x0 - 0.5 * width, y0 - 0.5 * height, x0 + 0.5 * width, y0 + 0.5 * height ] x1, y1 = geometry.intersection_of_line_and_rect(line, bbox, round_edges=0) x2, y2 = geometry.intersection_of_line_and_rect(line, charge_bbox, round_edges=0) x2, y2 = geometry.elongate_line(x1, y1, x2, y2, -self.space_around_atom) x = x0 + x1 - x2 y = y0 + y1 - y2 # draw self.context.set_source_rgb(*color) self.context.move_to(round(x - xbearing - 0.5 * width), round(y + 0.5 * height)) self.context.show_text(charge)
def _draw_edge(self, e): def draw_plain_or_colored_line(_start, _end, second=False): """second means if this is not the main line, drawing might be different""" if not has_shown_vertex or not self.color_bonds: if not second: self._draw_line(_start, _end, line_width=self.line_width, capstyle=cairo.LINE_CAP_ROUND) else: self._draw_line(_start, _end, line_width=self.line_width, capstyle=cairo.LINE_CAP_BUTT) else: self._draw_colored_line(_start, _end, line_width=self.line_width, start_color=color1, end_color=color2) def draw_plain_or_colored_wedge(_start, _end): x1, y1 = _start x2, y2 = _end x, y, x0, y0 = geometry.find_parallel(x1, y1, x2, y2, self.wedge_width / 2.0) xa, ya, xb, yb = geometry.find_parallel(x1, y1, x2, y2, self.line_width / 2.0) # no coloring now if not has_shown_vertex or not self.color_bonds: self._create_cairo_path([(xa, ya), (x0, y0), (2 * x2 - x0, 2 * y2 - y0), (2 * x1 - xa, 2 * y1 - ya)], closed=True) self.context.set_source_rgb(0, 0, 0) self.context.fill() else: # ratio 0.4 looks better than 0.5 because the area difference # is percieved more than length difference ratio = 0.4 xm1 = ratio * xa + (1 - ratio) * x0 ym1 = ratio * ya + (1 - ratio) * y0 xm2 = (1 - ratio) * (2 * x2 - x0) + ratio * (2 * x1 - xa) ym2 = (1 - ratio) * (2 * y2 - y0) + ratio * (2 * y1 - ya) self.context.set_source_rgb(*color1) self._create_cairo_path([(xa, ya), (xm1, ym1), (xm2, ym2), (2 * x1 - xa, 2 * y1 - ya)], closed=True) self.context.fill() self.context.set_source_rgb(*color2) self._create_cairo_path([(xm1, ym1), (x0, y0), (2 * x2 - x0, 2 * y2 - y0), (xm2, ym2)], closed=True) self.context.fill() def draw_plain_or_colored_hatch(_start, _end): x1, y1 = _start x2, y2 = _end # no coloring now x, y, x0, y0 = geometry.find_parallel(x1, y1, x2, y2, self.wedge_width / 2.0) xa, ya, xb, yb = geometry.find_parallel(x1, y1, x2, y2, self.line_width / 2.0) d = math.sqrt((x1 - x2)**2 + (y1 - y2)**2) # length of the bond if d == 0: return # to prevent division by zero dx1 = (x0 - xa) / d dy1 = (y0 - ya) / d dx2 = (2 * x2 - x0 - 2 * x1 + xa) / d dy2 = (2 * y2 - y0 - 2 * y1 + ya) / d # we have to decide if the first line should be at the position of the first atom draw_start = 1 # is index not boolean if not v1 in self._vertex_to_bbox and v1.occupied_valency > 1: draw_start = 1 draw_end = 1 # is added to index not boolean if not v2 in self._vertex_to_bbox and v2.occupied_valency > 1: draw_end = 0 # adjust the step length step_size = 2 * (self.line_width) ns = round(d / step_size) or 1 step_size = d / ns # now we finally draw self.context.set_line_cap(cairo.LINE_CAP_BUTT) self.context.set_source_rgb(*color1) middle = 0.5 * (draw_start + int(round(d / step_size)) + draw_end - 2) for i in range(draw_start, int(round(d / step_size)) + draw_end): coords = [ xa + dx1 * i * step_size, ya + dy1 * i * step_size, 2 * x1 - xa + dx2 * i * step_size, 2 * y1 - ya + dy2 * i * step_size ] if coords[0] == coords[2] and coords[1] == coords[3]: if (dx1 + dx2) > (dy1 + dy2): coords[0] += 1 else: coords[1] += 1 self._create_cairo_path([coords[:2], coords[2:]]) if i >= middle: self.context.stroke() self.context.set_source_rgb(*color2) self.context.stroke() # code itself # at first detect the need to make 3D adjustments self._transform = transform3d.transform3d() self._invtransform = transform3d.transform3d() transform = None if e.order > 1: atom1, atom2 = e.vertices for n in atom1.neighbors + atom2.neighbors: # e.atom1 and e.atom2 are in this list as well if n.z != 0: # engage 3d transform prior to detection of where to draw transform = self._get_3dtransform_for_drawing(e) #transform = None break if transform: for n in atom1.neighbors + atom2.neighbors: n.coords = transform.transform_xyz(*n.coords) self._transform = transform self._invtransform = transform.get_inverse() # // end of 3D adjustments # now the code itself coords = self._where_to_draw_from_and_to(e) if coords: start = coords[:2] end = coords[2:] v1, v2 = e.vertices if self.color_bonds: color1 = self.atom_colors.get(v1.symbol, (0, 0, 0)) color2 = self.atom_colors.get(v2.symbol, (0, 0, 0)) else: color1 = color2 = (0, 0, 0) has_shown_vertex = bool( [1 for _v in e.vertices if _v in self._vertex_to_bbox]) if e.order == 1: if e.type == 'w': draw_plain_or_colored_wedge(start, end) elif e.type == 'h': draw_plain_or_colored_hatch(start, end) else: draw_plain_or_colored_line(start, end) if e.order == 2: side = 0 # find how to center the bonds # rings have higher priority in setting the positioning in_ring = False for ring in self.molecule.get_smallest_independent_cycles_dangerous_and_cached( ): double_bonds = len([ b for b in self.molecule.vertex_subgraph_to_edge_subgraph(ring) if b.order == 2 ]) if v1 in ring and v2 in ring: in_ring = True side += double_bonds * reduce(operator.add, [ geometry.on_which_side_is_point( start + end, (a.x, a.y)) for a in ring if a != v1 and a != v2 ]) # if rings did not decide, use the other neigbors if not side: for v in v1.neighbors + v2.neighbors: if v != v1 and v != v2: side += geometry.on_which_side_is_point( start + end, (v.x, v.y)) # if neighbors did not decide either if not side and (in_ring or not has_shown_vertex): if in_ring: # we don't want centered bonds inside rings side = 1 # select arbitrary value else: # bond between two unshown atoms - we want to center them only in some cases if len(v1.neighbors) == 1 and len(v2.neighbors) == 1: # both atoms have only one neighbor side = 0 elif len(v1.neighbors) < 3 and len(v2.neighbors) < 3: # try to figure out which side is more towards the center of the molecule side = reduce(operator.add, [ geometry.on_which_side_is_point( start + end, (a.x, a.y)) for a in self.molecule.vertices if a != v1 and a != v2 ], 0) if not side: side = 1 # we choose arbitrary value, we don't want centering if side: draw_plain_or_colored_line(start, end) x1, y1, x2, y2 = geometry.find_parallel( start[0], start[1], end[0], end[1], self.bond_width * misc.signum(side)) # shorten the second line length = geometry.point_distance(x1, y1, x2, y2) if v2 not in self._vertex_to_bbox: x2, y2 = geometry.elongate_line( x1, y1, x2, y2, -self.bond_second_line_shortening * length) if v1 not in self._vertex_to_bbox: x1, y1 = geometry.elongate_line( x2, y2, x1, y1, -self.bond_second_line_shortening * length) draw_plain_or_colored_line((x1, y1), (x2, y2), second=True) else: for i in (1, -1): x1, y1, x2, y2 = geometry.find_parallel( start[0], start[1], end[0], end[1], i * self.bond_width * 0.5) draw_plain_or_colored_line((x1, y1), (x2, y2)) elif e.order == 3: self._draw_line(start, end, line_width=self.line_width) for i in (1, -1): x1, y1, x2, y2 = geometry.find_parallel( start[0], start[1], end[0], end[1], i * self.bond_width * 0.7) draw_plain_or_colored_line((x1, y1), (x2, y2), second=True) if transform: # if transform was used, we need to transform back for n in atom1.neighbors + atom2.neighbors: n.coords = self._invtransform.transform_xyz(*n.coords)