# -*- coding: ISO-8859-1 -*-
#
#
# Copyright (C) 2002-2006 Jrg Lehmann <joergl@users.sourceforge.net>
# Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
# Copyright (C) 2002-2006 Andr Wobst <wobsta@users.sourceforge.net>
#
# This file is part of PyX (http://pyx.sourceforge.net/).
#
# PyX is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# PyX is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PyX; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
# TODO:
# - should we improve on the arc length -> arg parametrization routine or
# should we at least factor it out?
from __future__ import nested_scopes
import sys, math
import attr, canvas, color, path, normpath, style, trafo, unit
try:
from math import radians
except ImportError:
# fallback implementation for Python 2.1 and below
def radians(x): return x*math.pi/180
class _marker: pass
#
# Decorated path
#
class decoratedpath(canvas.canvasitem):
"""Decorated path
The main purpose of this class is during the drawing
(stroking/filling) of a path. It collects attributes for the
stroke and/or fill operations.
"""
def __init__(self, path, strokepath=None, fillpath=None,
styles=None, strokestyles=None, fillstyles=None,
ornaments=None):
self.path = path
# global style for stroking and filling and subdps
self.styles = styles
# styles which apply only for stroking and filling
self.strokestyles = strokestyles
self.fillstyles = fillstyles
# the decoratedpath can contain additional elements of the
# path (ornaments), e.g., arrowheads.
if ornaments is None:
self.ornaments = canvas.canvas()
else:
self.ornaments = ornaments
self.nostrokeranges = None
def ensurenormpath(self):
"""convert self.path into a normpath"""
assert self.nostrokeranges is None or isinstance(self.path, path.normpath), "you don't understand what you are doing"
self.path = self.path.normpath()
def excluderange(self, begin, end):
assert isinstance(self.path, path.normpath), "you don't understand what this is about"
if self.nostrokeranges is None:
self.nostrokeranges = [(begin, end)]
else:
ibegin = 0
while ibegin < len(self.nostrokeranges) and self.nostrokeranges[ibegin][1] < begin:
ibegin += 1
if ibegin == len(self.nostrokeranges):
self.nostrokeranges.append((begin, end))
return
iend = len(self.nostrokeranges) - 1
while 0 <= iend and end < self.nostrokeranges[iend][0]:
iend -= 1
if iend == -1:
self.nostrokeranges.insert(0, (begin, end))
return
if self.nostrokeranges[ibegin][0] < begin:
begin = self.nostrokeranges[ibegin][0]
if end < self.nostrokeranges[iend][1]:
end = self.nostrokeranges[iend][1]
self.nostrokeranges[ibegin:iend+1] = [(begin, end)]
def bbox(self):
pathbbox = self.path.bbox()
ornamentsbbox = self.ornaments.bbox()
if ornamentsbbox is not None:
return ornamentsbbox + pathbbox
else:
return pathbbox
def strokepath(self):
if self.nostrokeranges:
splitlist = []
for begin, end in self.nostrokeranges:
splitlist.append(begin)
splitlist.append(end)
split = self.path.split(splitlist)
# XXX properly handle closed paths?
result = split[0]
for i in range(2, len(split), 2):
result += split[i]
return result
else:
return self.path
def processPS(self, file, writer, context, registry, bbox):
# draw (stroke and/or fill) the decoratedpath on the canvas
# while trying to produce an efficient output, e.g., by
# not writing one path two times
# small helper
def _writestyles(styles, context, registry, bbox):
for style in styles:
style.processPS(file, writer, context, registry, bbox)
if self.strokestyles is None and self.fillstyles is None:
if not len(self.ornaments):
raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
# just draw additional elements of decoratedpath
self.ornaments.processPS(file, writer, context, registry, bbox)
return
strokepath = self.strokepath()
fillpath = self.path
# apply global styles
if self.styles:
file.write("gsave\n")
context = context()
_writestyles(self.styles, context, registry, bbox)
if self.fillstyles is not None:
file.write("newpath\n")
fillpath.outputPS(file, writer)
if self.strokestyles is not None and strokepath is fillpath:
# do efficient stroking + filling if respective paths are identical
file.write("gsave\n")
if self.fillstyles:
_writestyles(self.fillstyles, context(), registry, bbox)
file.write("fill\n")
file.write("grestore\n")
acontext = context()
if self.strokestyles:
file.write("gsave\n")
_writestyles(self.strokestyles, acontext, registry, bbox)
file.write("stroke\n")
# take linewidth into account for bbox when stroking a path
bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
if self.strokestyles:
file.write("grestore\n")
else:
# only fill fillpath - for the moment
if self.fillstyles:
file.write("gsave\n")
_writestyles(self.fillstyles, context(), registry, bbox)
file.write("fill\n")
bbox += fillpath.bbox()
if self.fillstyles:
file.write("grestore\n")
if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
# this is the only relevant case still left
# Note that a possible stroking has already been done.
acontext = context()
if self.strokestyles:
file.write("gsave\n")
_writestyles(self.strokestyles, acontext, registry, bbox)
file.write("newpath\n")
strokepath.outputPS(file, writer)
file.write("stroke\n")
# take linewidth into account for bbox when stroking a path
bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
if self.strokestyles:
file.write("grestore\n")
# now, draw additional elements of decoratedpath
self.ornaments.processPS(file, writer, context, registry, bbox)
# restore global styles
if self.styles:
file.write("grestore\n")
def processPDF(self, file, writer, context, registry, bbox):
# draw (stroke and/or fill) the decoratedpath on the canvas
def _writestyles(styles, context, registry, bbox):
for style in styles:
style.processPDF(file, writer, context, registry, bbox)
def _writestrokestyles(strokestyles, context, registry, bbox):
context.fillattr = 0
for style in strokestyles:
style.processPDF(file, writer, context, registry, bbox)
context.fillattr = 1
def _writefillstyles(fillstyles, context, registry, bbox):
context.strokeattr = 0
for style in fillstyles:
style.processPDF(file, writer, context, registry, bbox)
context.strokeattr = 1
if self.strokestyles is None and self.fillstyles is None:
if not len(self.ornaments):
raise RuntimeError("Path neither to be stroked nor filled nor decorated in another way")
# just draw additional elements of decoratedpath
self.ornaments.processPDF(file, writer, context, registry, bbox)
return
strokepath = self.strokepath()
fillpath = self.path
# apply global styles
if self.styles:
file.write("q\n") # gsave
context = context()
_writestyles(self.styles, context, registry, bbox)
if self.fillstyles is not None:
fillpath.outputPDF(file, writer)
if self.strokestyles is not None and strokepath is fillpath:
# do efficient stroking + filling
file.write("q\n") # gsave
acontext = context()
if self.fillstyles:
_writefillstyles(self.fillstyles, acontext, registry, bbox)
if self.strokestyles:
_writestrokestyles(self.strokestyles, acontext, registry, bbox)
file.write("B\n") # both stroke and fill
# take linewidth into account for bbox when stroking a path
bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
file.write("Q\n") # grestore
else:
# only fill fillpath - for the moment
if self.fillstyles:
file.write("q\n") # gsave
_writefillstyles(self.fillstyles, context(), registry, bbox)
file.write("f\n") # fill
bbox += fillpath.bbox()
if self.fillstyles:
file.write("Q\n") # grestore
if self.strokestyles is not None and (strokepath is not fillpath or self.fillstyles is None):
# this is the only relevant case still left
# Note that a possible stroking has already been done.
acontext = context()
if self.strokestyles:
file.write("q\n") # gsave
_writestrokestyles(self.strokestyles, acontext, registry, bbox)
strokepath.outputPDF(file, writer)
file.write("S\n") # stroke
# take linewidth into account for bbox when stroking a path
bbox += strokepath.bbox().enlarged_pt(0.5*acontext.linewidth_pt)
if self.strokestyles:
file.write("Q\n") # grestore
# now, draw additional elements of decoratedpath
self.ornaments.processPDF(file, writer, context, registry, bbox)
# restore global styles
if self.styles:
file.write("Q\n") # grestore
#
# Path decorators
#
class deco:
"""decorators
In contrast to path styles, path decorators depend on the concrete
path to which they are applied. In particular, they don't make
sense without any path and can thus not be used in canvas.set!
"""
def decorate(self, dp, texrunner):
"""apply a style to a given decoratedpath object dp
decorate accepts a decoratedpath object dp, applies PathStyle
by modifying dp in place.
"""
pass
#
# stroked and filled: basic decos which stroked and fill,
# respectively the path
#
class _stroked(deco, attr.exclusiveattr):
"""stroked is a decorator, which draws the outline of the path"""
def __init__(self, styles=[]):
attr.exclusiveattr.__init__(self, _stroked)
self.styles = attr.mergeattrs(styles)
attr.checkattrs(self.styles, [style.strokestyle])
def __call__(self, styles=[]):
# XXX or should we also merge self.styles
return _stroked(styles)
def decorate(self, dp, texrunner):
if dp.strokestyles is not None:
raise RuntimeError("Cannot stroke an already stroked path")
dp.strokestyles = self.styles
stroked = _stroked()
stroked.clear = attr.clearclass(_stroked)
class _filled(deco, attr.exclusiveattr):
"""filled is a decorator, which fills the interior of the path"""
def __init__(self, styles=[]):
attr.exclusiveattr.__init__(self, _filled)
self.styles = attr.mergeattrs(styles)
attr.checkattrs(self.styles, [style.fillstyle])
def __call__(self, styles=[]):
# XXX or should we also merge self.styles
return _filled(styles)
def decorate(self, dp, texrunner):
if dp.fillstyles is not None:
raise RuntimeError("Cannot fill an already filled path")
dp.fillstyles = self.styles
filled = _filled()
filled.clear = attr.clearclass(_filled)
#
# Arrows
#
# helper function which constructs the arrowhead
def _arrowhead(anormpath, arclenfrombegin, direction, size, angle, constrictionlen):
"""helper routine, which returns an arrowhead from a given anormpath
- arclenfrombegin: position of arrow in arc length from the start of the path
- direction: +1 for an arrow pointing along the direction of anormpath or
-1 for an arrow pointing opposite to the direction of normpath
- size: size of the arrow as arc length
- angle. opening angle
- constrictionlen: None (no constriction) or arc length of constriction.
"""
# arc length and coordinates of tip
tx, ty = anormpath.at(arclenfrombegin)
# construct the template for the arrow by cutting the path at the
# corresponding length
arrowtemplate = anormpath.split([arclenfrombegin, arclenfrombegin - direction * size])[1]
# from this template, we construct the two outer curves of the arrow
arrowl = arrowtemplate.transformed(trafo.rotate(-angle/2.0, tx, ty))
arrowr = arrowtemplate.transformed(trafo.rotate( angle/2.0, tx, ty))
# now come the joining backward parts
if constrictionlen is not None:
# constriction point (cx, cy) lies on path
cx, cy = anormpath.at(arclenfrombegin - direction * constrictionlen)
arrowcr= path.line(*(arrowr.atend() + (cx,cy)))
arrow = arrowl.reversed() << arrowr << arrowcr
else:
arrow = arrowl.reversed() << arrowr
arrow[-1].close()
return arrow
_base = 6 * unit.v_pt
class arrow(deco, attr.attr):
"""arrow is a decorator which adds an arrow to either side of the path"""
def __init__(self, attrs=[], pos=1, reversed=0, size=_base, angle=45, constriction=0.8):
self.attrs = attr.mergeattrs([style.linestyle.solid, filled] + attrs)
attr.checkattrs(self.attrs, [deco, style.fillstyle, style.strokestyle])
self.pos = pos
self.reversed = reversed
self.size = size
self.angle = angle
self.constriction = constriction
def __call__(self, attrs=None, pos=None, reversed=None, size=None, angle=None, constriction=_marker):
if attrs is None:
attrs = self.attrs
if pos is None:
pos = self.pos
if reversed is None:
reversed = self.reversed
if size is None:
size = self.size
if angle is None:
angle = self.angle
if constriction is _marker:
constriction = self.constriction
return arrow(attrs=attrs, pos=pos, reversed=reversed, size=size, angle=angle, constriction=constriction)
def decorate(self, dp, texrunner):
dp.ensurenormpath()
anormpath = dp.path
# calculate absolute arc length of constricition
# Note that we have to correct this length because the arrowtemplates are rotated
# by self.angle/2 to the left and right. Hence, if we want no constriction, i.e., for
# self.constriction = 1, we actually have a length which is approximately shorter
# by the given geometrical factor.
if self.constriction is not None:
constrictionlen = arrowheadconstrictionlen = self.size * self.constriction * math.cos(radians(self.angle/2.0))
else:
# if we do not want a constriction, i.e. constriction is None, we still
# need constrictionlen for cutting the path
constrictionlen = self.size * 1 * math.cos(radians(self.angle/2.0))
arrowheadconstrictionlen = None
arclenfrombegin = self.pos * anormpath.arclen()
direction = self.reversed and -1 or 1
arrowhead = _arrowhead(anormpath, arclenfrombegin, direction, self.size, self.angle, arrowheadconstrictionlen)
# add arrowhead to decoratedpath
dp.ornaments.draw(arrowhead, self.attrs)
# exlude part of the path from stroking when the arrow is strictly at the begin or the end
if self.pos == 0 and self.reversed:
dp.excluderange(0, min(self.size, constrictionlen))
elif self.pos == 1 and not self.reversed:
dp.excluderange(anormpath.end() - min(self.size, constrictionlen), anormpath.end())
arrow.clear = attr.clearclass(arrow)
# arrows at begin of path
barrow = arrow(pos=0, reversed=1)
barrow.SMALL = barrow(size=_base/math.sqrt(64))
barrow.SMALl = barrow(size=_base/math.sqrt(32))
barrow.SMAll = barrow(size=_base/math.sqrt(16))
barrow.SMall = barrow(size=_base/math.sqrt(8))
barrow.Small = barrow(size=_base/math.sqrt(4))
barrow.small = barrow(size=_base/math.sqrt(2))
barrow.normal = barrow(size=_base)
barrow.large = barrow(size=_base*math.sqrt(2))
barrow.Large = barrow(size=_base*math.sqrt(4))
barrow.LArge = barrow(size=_base*math.sqrt(8))
barrow.LARge = barrow(size=_base*math.sqrt(16))
barrow.LARGe = barrow(size=_base*math.sqrt(32))
barrow.LARGE = barrow(size=_base*math.sqrt(64))
# arrows at end of path
earrow = arrow()
earrow.SMALL = earrow(size=_base/math.sqrt(64))
earrow.SMALl = earrow(size=_base/math.sqrt(32))
earrow.SMAll = earrow(size=_base/math.sqrt(16))
earrow.SMall = earrow(size=_base/math.sqrt(8))
earrow.Small = earrow(size=_base/math.sqrt(4))
earrow.small = earrow(size=_base/math.sqrt(2))
earrow.normal = earrow(size=_base)
earrow.large = earrow(size=_base*math.sqrt(2))
earrow.Large = earrow(size=_base*math.sqrt(4))
earrow.LArge = earrow(size=_base*math.sqrt(8))
earrow.LARge = earrow(size=_base*math.sqrt(16))
earrow.LARGe = earrow(size=_base*math.sqrt(32))
earrow.LARGE = earrow(size=_base*math.sqrt(64))
class text(deco, attr.attr):
"""a simple text decorator"""
def __init__(self, text, textattrs=[], angle=0, textdist=0.2,
relarclenpos=0.5, arclenfrombegin=None, arclenfromend=None,
texrunner=None):
if arclenfrombegin is not None and arclenfromend is not None:
raise ValueError("either set arclenfrombegin or arclenfromend")
self.text = text
self.textattrs = textattrs
self.angle = angle
self.textdist = textdist
self.relarclenpos = relarclenpos
self.arclenfrombegin = arclenfrombegin
self.arclenfromend = arclenfromend
self.texrunner = texrunner
def decorate(self, dp, texrunner):
if self.texrunner:
texrunner = self.texrunner
import text as textmodule
textattrs = attr.mergeattrs([textmodule.halign.center, textmodule.vshift.mathaxis] + self.textattrs)
dp.ensurenormpath()
if self.arclenfrombegin is not None:
x, y = dp.path.at(dp.path.begin() + self.arclenfrombegin)
elif self.arclenfromend is not None:
x, y = dp.path.at(dp.path.end() - self.arclenfromend)
else:
# relarcpos is used, when neither arcfrombegin nor arcfromend is given
x, y = dp.path.at(self.relarclenpos * dp.path.arclen())
t = texrunner.text(x, y, self.text, textattrs)
t.linealign(self.textdist, math.cos(self.angle*math.pi/180), math.sin(self.angle*math.pi/180))
dp.ornaments.insert(t)
class shownormpath(deco, attr.attr):
def decorate(self, dp, texrunner):
r_pt = 2
dp.ensurenormpath()
for normsubpath in dp.path.normsubpaths:
for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
if isinstance(normsubpathitem, normpath.normcurve_pt):
dp.ornaments.stroke(normpath.normpath([normpath.normsubpath([normsubpathitem])]), [color.rgb.green])
else:
dp.ornaments.stroke(normpath.normpath([normpath.normsubpath([normsubpathitem])]), [color.rgb.blue])
for normsubpath in dp.path.normsubpaths:
for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
if isinstance(normsubpathitem, normpath.normcurve_pt):
dp.ornaments.stroke(path.line_pt(normsubpathitem.x0_pt, normsubpathitem.y0_pt, normsubpathitem.x1_pt, normsubpathitem.y1_pt), [style.linestyle.dashed, color.rgb.red])
dp.ornaments.stroke(path.line_pt(normsubpathitem.x2_pt, normsubpathitem.y2_pt, normsubpathitem.x3_pt, normsubpathitem.y3_pt), [style.linestyle.dashed, color.rgb.red])
dp.ornaments.draw(path.circle_pt(normsubpathitem.x1_pt, normsubpathitem.y1_pt, r_pt), [filled([color.rgb.red])])
dp.ornaments.draw(path.circle_pt(normsubpathitem.x2_pt, normsubpathitem.y2_pt, r_pt), [filled([color.rgb.red])])
for normsubpath in dp.path.normsubpaths:
for i, normsubpathitem in enumerate(normsubpath.normsubpathitems):
if not i:
x_pt, y_pt = normsubpathitem.atbegin_pt()
dp.ornaments.draw(path.circle_pt(x_pt, y_pt, r_pt), [filled])
x_pt, y_pt = normsubpathitem.atend_pt()
dp.ornaments.draw(path.circle_pt(x_pt, y_pt, r_pt), [filled])
|