# Copyright (C) 2002-2006 Alexei Gilchrist and Paul Cochrane
#
# This program 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.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# $Id: path.py,v 1.25 2006/05/16 05:38:31 aalexei Exp $
"""
The Path module
"""
__revision__ = '$Revision: 1.25 $'
#from pyscript.defaults import defaults
from math import sqrt,pi,sin,cos
from pyscript.vectors import P,Bbox,U,Identity,R
from pyscript.base import Color
from pyscript.objects import AffineObj
from pyscript.arrowheads import ArrowHead
import cStringIO
# -------------------------------------------------------------------------
# Pathlettes ... components of path, not used by themselves
# -------------------------------------------------------------------------
class _line(object):
'''
A line pathlette
'''
s = None
e = None
def __init__(self, s, e):
object.__init__(self)
self.s = s
self.e = e
def _get_start(self):
"""
return start point
"""
return self.s
start = property(_get_start)
def _get_end(self):
"""
return end point
"""
return self.e
end = property(_get_end)
def _get_length(self):
"""
Get the length of the pathlette
"""
return (self.e-self.s).length
length = property(_get_length)
def P(self, f):
'''
return point at fraction f of length
'''
return (self.s+(self.e-self.s)*f)
def tangent(self, f):
'''
return angle of tangent of curve at fraction f of length
'''
return (self.e-self.s).arg
def body(self):
"""
Return the postscript body
"""
return '%s lineto\n' % self.e
def bbox(self, itoe = Identity):
"""
Return the bounding box
"""
p0 = itoe(self.s)
p1 = itoe(self.e)
x0 = min(p0[0], p1[0])
x1 = max(p0[0], p1[0])
y0 = min(p0[1], p1[1])
y1 = max(p0[1], p1[1])
return Bbox(sw = P(x0, y0), width = x1-x0,
height = y1-y0)
# -------------------------------------------------------------------------
class _bezier(object):
'''
A Bezier pathlette
'''
s = None
e = None
cs = None
ce = None
length = None
TOL = None #tolerance for linearising
def __init__(self, s, cs, ce, e, TOL = 2e-3, temporary = False):
object.__init__(self)
self.s = s # start
self.e = e # end
self.cs = cs # start control
self.ce = ce # end control
self.TOL = TOL
# for efficiency don't do this unless we intend to
# keep this pathlette
if not temporary:
self._points = self.straighten()
self.set_length()
def _is_straight(self):
'''
is this curve straight?
'''
L1 = (self.cs-self.s).length+\
(self.ce-self.cs).length+\
(self.e-self.ce).length
L2 = (self.e-self.s).length
if abs(L1-L2)/float(L1) <= self.TOL:
return True
else:
return False
def straighten(self):
"""
Straighten the bezier curve
"""
if self._is_straight():
return (self.s, self.e)
else:
c1, c2 = self._bisect(temporary = True)
return (c1.straighten()+c2.straighten())
def _bisect(self, t = .5, temporary = False):
'''
Divide this bezier into two
'''
p01 = self.s * (1-t) + self.cs * t
p12 = self.cs * (1-t) + self.ce * t
p23 = self.ce * (1-t) + self.e * t
p012 = p01 * (1-t) + p12 * t
p123 = p12 * (1-t) + p23 * t
p0123 = p012 * (1-t) + p123 * t
return (_bezier(self.s.copy(),
p01, p012, p0123, temporary = temporary),
_bezier(p0123.copy(),
p123, p23, self.e.copy(), temporary = temporary))
def set_length(self):
"""
Set the length of the bezier curve
"""
L = 0
p0 = self.s
for p in self._points:
L += (p-p0).length
p0 = p
self.length = L
def body(self):
"""
Return the postscript body of the object
"""
return '%s %s %s curveto\n' % (self.cs, self.ce, self.e)
def _t(self, t):
'''
Return point on curve parametrised by t [0-1]
This is exact
'''
a1 = 3*(self.cs-self.s)
a2 = 3*(self.s-2*self.cs+self.ce)
a3 = -self.s+3*self.cs-3*self.ce+self.e
return a3*t**3+a2*t**2+a1*t+self.s
def _get_start(self):
"""
return start point
"""
return self.s
start = property(_get_start)
def _get_end(self):
"""
return end point
"""
return self.e
end = property(_get_end)
def P(self, f):
'''
return point on curve at fraction f of length
'''
assert 0 <= f <= 1
#if self.length is None:
# self._cache()
if f == 0:
return self.s
elif f == 1:
return self.e
Lf = self.length*f
L = 0
p0 = self.s
for p in self._points:
l = (p-p0).length
if L+l >= Lf:
break
L += l
p0 = p
# XXX Add a correction here so it's actually on the curve!
# Newton Rapson?
return (p-p0).U*(Lf-L) +p0
def tangent(self, f):
'''
return angle of tangent of curve at fraction f of length
'''
assert 0 <= f <= 1
if f == 0:
return (self.cs-self.s).arg
elif f == 1:
return (self.e-self.ce).arg
Lf = self.length*f
L = 0
p0 = self.s
for p in self._points:
l = (p-p0).length
if L+l >= Lf:
break
L += l
p0 = p
# XXX Add a correction here so it's actually on the curve!
# Newton Rapson?
return (p-p0).arg
def bbox(self, itoe = Identity):
"""
Return the bounding box of the object
"""
# run through the list of points to get the bounding box
#if self.length is None:
# self._cache()
p0 = itoe(self.s)
x0, y0 = p0
x1, y1 = p0
for p in self._points:
p1 = itoe(p)
x0 = min(x0, p1[0])
x1 = max(x1, p1[0])
y0 = min(y0, p1[1])
y1 = max(y1, p1[1])
return Bbox(sw = P(x0, y0), width = x1-x0,
height = y1-y0)
# -------------------------------------------------------------------------
# Curve specifier
# -------------------------------------------------------------------------
class C(object):
"""
Specifier and generator for curves
"""
# these params control the natural bezier
# (they are set to the MetaPost defaults)
_a = sqrt(2)
_b = 1/16.
_c = (3-sqrt(5))/2.
# user parameters for curve:
c0 = None
c1 = None
t0 = 1
t1 = 1
#curl = 1
# this for specifing an arc
arc = None
def __init__(self, *args, **options):
'''
store curve parameters
'''
if len(args) == 1:
raise ValueError, "C takes two arguments"
#self.c0 = args[0]
#self.c1 = args[0]
elif len(args) == 2:
self.c0 = args[0]
self.c1 = args[1]
# anything supplied in keywords will override
# the above points eg C(P(0, 0), c1=45)
object.__init__(self)
self(**options)
def __call__(self, **options):
'''
Set a whole lot of attributes in one go
eg::
obj.set(bg=Color(.3), linewidth=2)
@return: self
@rtype: self
'''
# first do non-property ones
# this will raise an exception if class doesn't have attribute
# I think this is good.
prop = []
for key, value in options.items():
if isinstance(eval('self.__class__.%s'%key), property):
prop.append((key, value))
else:
self.__class__.__setattr__(self, key, value)
# now the property ones
# (which are functions of the non-property ones)
for key, value in prop:
self.__class__.__setattr__(self, key, value)
# for convenience return a reference to us
return self
def _get_fullyspecified(self):
'''
Is this curve fully specified (all control points)
'''
if self.arc is not None:
# an arc is already fully specified
return 1
elif isinstance(self.c0, P) and isinstance(self.c1, P):
# both points set
return 1
else:
return 0
fullyspecified = property(_get_fullyspecified, None)
def curve(self, p0, p1 = None):
'''
return pathlette object corresponding to curve
'''
if self.arc is not None:
# an arc
return self.create_arc(p0)
else:
# a bezier
if not self.fullyspecified:
# fit natural curve...
self.fit_curve(p0, p1)
return self.create_bezier(p0, p1)
def fit_curve(self, p0, p1):
'''
fit a natural looking spline to end slopes
'''
# first get the angles ...
if type(self.c0) in [type(10), type(10.0)]:
# turn this into a unit vector in that direction
w0 = U(self.c0)
elif isinstance(self.c0, R):
# already have unit vectior
w0 = self.c0
elif isinstance(self.c0, P):
# non-unit vector giving direction
w0 = (self.c0-p0)
else:
raise ValueError, "Unknown control type c0"
if type(self.c1) in [type(10), type(10.0)]:
# turn this into a unit vector in that direction
w1 = U(self.c1)
elif isinstance(self.c1, R):
# already have unit vectior
w1 = self.c1
elif isinstance(self.c1, P):
# non-unit vector giving direction
w1 = (self.c1-p1)
else:
raise ValueError, "Unknown control type c1"
t = ((p1-p0).arg-w0.arg)*pi/180.
p = -((-p1+p0).arg-w1.arg)*pi/180.
a = self._a
b = self._b
c = self._c
alpha = a*(sin(t)-b*sin(p))*(sin(p)-b*sin(t))*(cos(t)-cos(p))
rho = (2+alpha)/(1+(1-c)*cos(t)+c*cos(p))
sigma = (2-alpha)/(1+(1-c)*cos(p)+c*cos(t))
c0 = P( p0.x + rho*( (p1.x-p0.x)*cos(t)-(p1.y-p0.y)*sin(t))/(3*self.t0) ,
p0.y + rho*( (p1.y-p0.y)*cos(t)+(p1.x-p0.x)*sin(t))/(3*self.t0) )
c1 = P(
p0.x + (p1.x-p0.x)*(1-sigma*cos(p)/(3*self.t1)) -
(p1.y-p0.y)*sigma*sin(p)/(3*self.t1) ,
p0.y + (p1.y-p0.y)*(1-sigma*cos(p)/(3*self.t1)) +
(p1.x-p0.x)*sigma*sin(p)/(3*self.t1)
)
# only change if we were given an angle
if type(self.c0) in [type(10), type(10.0)]:
self.c0 = c0
if type(self.c1) in [type(10), type(10.0)]:
self.c1 = c1
def create_arc(self, centre):
"""
Create an arc
"""
return None
def create_bezier(self, p0, p1):
"""
Create a bezier curve
"""
c0 = self.c0
c1 = self.c1
# fix up relative points:
if isinstance(c0, R):
c0 = p0+c0
if isinstance(c1, R):
c1 = p1+c1
return _bezier(p0, c0, c1, p1)
# -------------------------------------------------------------------------
# Path object
# -------------------------------------------------------------------------
class Path(AffineObj):
"""
A Path
"""
fg = Color(0)
bg = None
linewidth = None
linecap = None
linejoin = None
miterlimit = None
dash = None
closed = 0
#ArrowHead instances:
heads = []
#_pathlettes=[]
def __init__(self, *path, **options):
self._pathlettes = []
AffineObj.__init__(self, **options)
path = list(path) # so we can use pop
# first point must be, well a point
assert isinstance(path[0], P)
# if the last point of a closed path has been
# skipped, add it now
if not isinstance(path[-1], P) and self.closed:
path.append(path[0])
cp = path.pop(0) # current point
while 1:
if len(path) == 0:
break
p = path.pop(0)
if isinstance(p, R):
p = cp+p
self._pathlettes.append(_line(cp, p))
cp = p
elif isinstance(p, P):
self._pathlettes.append(_line(cp, p))
cp = p
elif isinstance(p, C):
c = p
# Get the next point
p = path.pop(0)
if isinstance(p, R):
p = cp+p
self._pathlettes.append(c.curve(cp, p))
cp = p
else:
raise ValueError, "Unknown path control"
# now add arrowheads
heads = []
for head in self.heads:
# make a copy so this class has it's own instance
h=head.copy()
# line colors overide arrow they blend
# (how would a user overide this?)
if options.has_key('fg'):
h(fg=options['fg'])
h(bg=options['fg'])
# position it appropriately:
h.__init__(tip=self.P(head.pos), angle=self.tangent(head.pos).arg)
heads.append(h)
self.heads = heads
def bbox(self):
"""
Return the bounding box of the Path
"""
b = Bbox()
for pl in self._pathlettes:
b.union(pl.bbox(self.itoe))
# take into account extent of arrowheads
for ar in self.heads:
b.union(ar.bbox())
return b
def _get_start(self):
"""
return start point
"""
return self.itoe(self._pathlettes[0].start)
start = property(_get_start)
def _get_end(self):
"""
return end point
"""
return self.itoe(self._pathlettes[-1].end)
end = property(_get_end)
def _get_length(self):
"""
Get the length of the path
"""
l = 0
for pl in self._pathlettes:
l += pl.length
return l
length = property(_get_length)
def P(self, f):
'''
Return the point at fraction f along the path
'''
assert 0 <= f <= 1
Lf = self.length*f
L = 0
for pl in self._pathlettes:
l = pl.length
if L+l >= Lf:
break
L += l
return self.itoe(pl.P((Lf-L)/float(l)))
def tangent(self, f):
'''
return tangent (unit vector) of curve at fraction f of length
'''
assert 0 <= f <= 1
Lf = self.length*f
L = 0
for pl in self._pathlettes:
l = pl.length
if L+l >= Lf:
break
L += l
return U(self.itoe(U(pl.tangent((Lf-L)/float(l)))).arg)
def body(self):
"""
Return the postscript body of the Path
"""
out = cStringIO.StringIO()
if self.linewidth is not None:
out.write("%g setlinewidth "%self.linewidth)
if self.linecap is not None:
out.write("%d setlinecap "%self.linecap)
if self.linejoin is not None:
out.write("%d setlinejoin "%self.linejoin)
if self.miterlimit is not None:
out.write("%f setmiterlimit "%self.miterlimit)
if self.dash is not None:
out.write(str(self.dash))
out.write('newpath %s moveto\n'%self._pathlettes[0].start)
for pl in self._pathlettes:
out.write(pl.body())
if self.closed:
out.write(' closepath ')
if self.bg is not None:
out.write("gsave %s fill grestore\n"%self.bg)
if self.fg is not None:
out.write("%s stroke\n"%self.fg)
for head in self.heads:
out.write(str(head))
return out.getvalue()
# -------------------------------------------------------------------------
# Arrow objects
# -------------------------------------------------------------------------
class Arrow(Path):
'''
Path object with arrow at end ... just for convenience
'''
#heads = [defaults.arrowhead(pos=1)]
heads = [ArrowHead(1)]
class DoubleArrow(Path):
"""
Path object with arrow at both ends ... just for convenience
"""
#heads = [defaults.arrowhead(pos=1),defaults.arrowhead(pos=1,reverse=1)]
heads = [ArrowHead(1), ArrowHead(0, reverse=1)]
# vim: expandtab shiftwidth=4:
|