Revision: 4700
http://matplotlib.svn.sourceforge.net/matplotlib/?rev=4700&view=rev
Author: mdboom
Date: 2007-12-11 16:15:23 -0800 (Tue, 11 Dec 2007)
Log Message:
-----------
Added (experimental) support for large arcs
Modified Paths:
--------------
branches/transforms/lib/matplotlib/patches.py
branches/transforms/lib/matplotlib/path.py
branches/transforms/src/_path.cpp
branches/transforms/unit/ellipse_compare.py
branches/transforms/unit/ellipse_large.py
Modified: branches/transforms/lib/matplotlib/patches.py
===================================================================
--- branches/transforms/lib/matplotlib/patches.py 2007-12-11 22:03:58 UTC
(rev 4699)
+++ branches/transforms/lib/matplotlib/patches.py 2007-12-12 00:15:23 UTC
(rev 4700)
@@ -839,6 +839,7 @@
self._width, self._height = width, height
self._angle = angle
self._recompute_transform()
+ self._path = Path.unit_circle()
def _recompute_transform(self):
self._patch_transform = transforms.Affine2D() \
@@ -850,7 +851,7 @@
"""
Return the vertices of the rectangle
"""
- return Path.unit_circle()
+ return self._path
def get_patch_transform(self):
return self._patch_transform
@@ -881,7 +882,6 @@
self._recompute_transform()
angle = property(_get_angle, _set_angle)
-
class Circle(Ellipse):
"""
A circle patch
@@ -908,7 +908,180 @@
Ellipse.__init__(self, xy, radius*2, radius*2, **kwargs)
__init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd
+class Arc(Ellipse):
+ """
+ An elliptical arc. Because it performs various optimizations, it may not
be
+ filled.
+ """
+ def __str__(self):
+ return
"Arc(%d,%d;%dx%d)"%(self.center[0],self.center[1],self.width,self.height)
+ def __init__(self, xy, width, height, angle=0.0, theta1=0.0, theta2=360.0,
**kwargs):
+ """
+ xy - center of ellipse
+ width - length of horizontal axis
+ height - length of vertical axis
+ angle - rotation in degrees (anti-clockwise)
+ theta1 - starting angle of the arc in degrees
+ theta2 - ending angle of the arc in degrees
+
+ If theta1 and theta2 are not provided, the arc will form a
+ complete ellipse.
+
+ Valid kwargs are:
+ %(Patch)s
+ """
+ fill = kwargs.pop('fill')
+ if fill:
+ raise ValueError("Arc objects can not be filled")
+ kwargs['fill'] = False
+
+ Ellipse.__init__(self, xy, width, height, angle, **kwargs)
+
+ self._theta1 = theta1
+ self._theta2 = theta2
+
+ def draw(self, renderer):
+ """
+ Ellipses are normally drawn using an approximation that uses
+ eight cubic bezier splines. The error of this approximation
+ is 1.89818e-6, according to this unverified source:
+
+ Lancaster, Don. Approximating a Circle or an Ellipse Using
+ Four Bezier Cubic Splines.
+
+ http://www.tinaja.com/glib/ellipse4.pdf
+
+ There is a use case where very large ellipses must be drawn
+ with very high accuracy, and it is too expensive to render the
+ entire ellipse with enough segments (either splines or line
+ segments). Therefore, in the case where either radius of the
+ ellipse is large enough that the error of the spline
+ approximation will be visible (greater than one pixel offset
+ from the ideal), a different technique is used.
+
+ In that case, only the visible parts of the ellipse are drawn,
+ with each visible arc using a fixed number of spline segments
+ (8). The algorithm proceeds as follows:
+
+ 1. The points where the ellipse intersects the axes bounding
+ box are located. (This is done be performing an inverse
+ transformation on the axes bbox such that it is relative to
+ the unit circle -- this makes the intersection calculation
+ much easier than doing rotated ellipse intersection
+ directly).
+
+ This uses the "line intersecting a circle" algorithm from:
+
+ Vince, John. Geometry for Computer Graphics: Formulae,
+ Examples & Proofs. London: Springer-Verlag, 2005.
+
+ 2. The angles of each of the intersection points are
+ calculated.
+
+ 3. Proceeding counterclockwise starting in the positive
+ x-direction, each of the visible arc-segments between the
+ pairs of vertices are drawn using the bezier arc
+ approximation technique implemented in Path.arc().
+ """
+ # Get the width and height in pixels
+ width, height = self.get_transform().transform_point(
+ (self._width, self._height))
+ inv_error = (1.0 / 1.89818e-6)
+
+ if width < inv_error and height < inv_error and False:
+ self._path = Path.arc(self._theta1, self._theta2)
+ return Patch.draw(self, renderer)
+
+ # Transforms the axes box_path so that it is relative to the unit
+ # circle in the same way that it is relative to the desired
+ # ellipse.
+ box_path = Path.unit_rectangle()
+ box_path_transform = transforms.BboxTransformTo(self.axes.bbox) + \
+ self.get_transform().inverted()
+ box_path = box_path.transformed(box_path_transform)
+ vertices = []
+
+ def iter_circle_intersect_on_line(x0, y0, x1, y1):
+ dx = x1 - x0
+ dy = y1 - y0
+ dr2 = dx*dx + dy*dy
+ dr = npy.sqrt(dr2)
+ D = x0*y1 - x1*y0
+ D2 = D*D
+ discrim = dr2 - D2
+
+ # Single (tangential) intersection
+ if discrim == 0.0:
+ x = (D*dy) / dr2
+ y = (-D*dx) / dr2
+ yield x, y
+ elif discrim > 0.0:
+ if dy < 0:
+ sign_dy = -1.0
+ else:
+ sign_dy = 1.0
+ sqrt_discrim = npy.sqrt(discrim)
+ for sign in (1., -1.):
+ x = (D*dy + sign * sign_dy * dx * sqrt_discrim) / dr2
+ y = (-D*dx + sign * npy.abs(dy) * sqrt_discrim) / dr2
+ yield x, y
+
+ def iter_circle_intersect_on_line_seg(x0, y0, x1, y1):
+ epsilon = 1e-9
+ if x1 < x0:
+ x0e, x1e = x1, x0
+ else:
+ x0e, x1e = x0, x1
+ if y1 < y0:
+ y0e, y1e = y1, y0
+ else:
+ y0e, y1e = y0, y1
+ x0e -= epsilon
+ y0e -= epsilon
+ x1e += epsilon
+ y1e += epsilon
+ for x, y in iter_circle_intersect_on_line(x0, y0, x1, y1):
+ if x >= x0e and x <= x1e and y >= y0e and y <= y1e:
+ yield x, y
+
+ PI = npy.pi
+ TWOPI = PI * 2.0
+ RAD2DEG = 180.0 / PI
+ DEG2RAD = PI / 180.0
+ theta1 = self._theta1
+ theta2 = self._theta2
+ thetas = {}
+ # For each of the point pairs, there is a line segment
+ for p0, p1 in zip(box_path.vertices[:-1], box_path.vertices[1:]):
+ x0, y0 = p0
+ x1, y1 = p1
+ for x, y in iter_circle_intersect_on_line_seg(x0, y0, x1, y1):
+ # Convert radians to angles
+ theta = npy.arccos(x)
+ if y < 0:
+ theta = TWOPI - theta
+ theta *= RAD2DEG
+ if theta > theta1 and theta < theta2:
+ thetas[theta] = None
+
+ thetas = thetas.keys()
+ thetas.sort()
+ thetas.append(theta2)
+
+ last_theta = theta1
+ theta1_rad = theta1 * DEG2RAD
+ inside = box_path.contains_point((npy.cos(theta1_rad),
npy.sin(theta1_rad)))
+
+ for theta in thetas:
+ if inside:
+ self._path = Path.arc(last_theta, theta, 8)
+ Patch.draw(self, renderer)
+ inside = False
+ else:
+ inside = True
+ last_theta = theta
+
def bbox_artist(artist, renderer, props=None, fill=True):
"""
This is a debug function to draw a rectangle around the bounding
Modified: branches/transforms/lib/matplotlib/path.py
===================================================================
--- branches/transforms/lib/matplotlib/path.py 2007-12-11 22:03:58 UTC (rev
4699)
+++ branches/transforms/lib/matplotlib/path.py 2007-12-12 00:15:23 UTC (rev
4700)
@@ -408,7 +408,7 @@
unit_circle = classmethod(unit_circle)
[EMAIL PROTECTED]
- def arc(cls, theta1, theta2, is_wedge=False, n=None):
+ def arc(cls, theta1, theta2, n=None, is_wedge=False):
"""
Returns an arc on the unit circle from angle theta1 to angle
theta2 (in degrees).
@@ -486,12 +486,12 @@
arc = classmethod(arc)
[EMAIL PROTECTED]
- def wedge(cls, theta1, theta2):
+ def wedge(cls, theta1, theta2, n=None):
"""
Returns a wedge of the unit circle from angle theta1 to angle
theta2 (in degrees).
"""
- return cls.arc(theta1, theta2, True)
+ return cls.arc(theta1, theta2, True, n)
wedge = classmethod(wedge)
_get_path_collection_extents = get_path_collection_extents
Modified: branches/transforms/src/_path.cpp
===================================================================
--- branches/transforms/src/_path.cpp 2007-12-11 22:03:58 UTC (rev 4699)
+++ branches/transforms/src/_path.cpp 2007-12-12 00:15:23 UTC (rev 4700)
@@ -109,7 +109,7 @@
// Input 2D polygon _pgon_ with _numverts_ number of vertices and test point
// _point_, returns 1 if inside, 0 if outside.
template<class T>
-bool point_in_path_impl(double tx, double ty, T& path)
+bool point_in_path_impl(const double tx, const double ty, T& path)
{
int yflag0, yflag1, inside_flag;
double vtx0, vty0, vtx1, vty1, sx, sy;
@@ -132,7 +132,7 @@
yflag0 = (vty0 >= ty);
vtx1 = x;
- vty1 = x;
+ vty1 = y;
inside_flag = 0;
do
@@ -141,7 +141,7 @@
// The following cases denote the beginning on a new subpath
if (code == agg::path_cmd_stop ||
- (code & agg::path_cmd_end_poly) == agg::path_cmd_end_poly)
+ (code & agg::path_cmd_end_poly) == agg::path_cmd_end_poly)
{
x = sx;
y = sy;
@@ -169,7 +169,7 @@
// by Joseph Samosky's and Mark Haigh-Hutchinson's different
// polygon inclusion tests.
if ( ((vty1-ty) * (vtx0-vtx1) >=
- (vtx1-tx) * (vty0-vty1)) == yflag1 )
+ (vtx1-tx) * (vty0-vty1)) == yflag1 )
{
inside_flag ^= 1;
}
@@ -184,7 +184,7 @@
vty1 = y;
}
while (code != agg::path_cmd_stop &&
- (code & agg::path_cmd_end_poly) != agg::path_cmd_end_poly);
+ (code & agg::path_cmd_end_poly) != agg::path_cmd_end_poly);
yflag1 = (vty1 >= ty);
if (yflag0 != yflag1)
Modified: branches/transforms/unit/ellipse_compare.py
===================================================================
--- branches/transforms/unit/ellipse_compare.py 2007-12-11 22:03:58 UTC (rev
4699)
+++ branches/transforms/unit/ellipse_compare.py 2007-12-12 00:15:23 UTC (rev
4700)
@@ -1,5 +1,5 @@
"""
-Compare the ellipse generated with arcs versus a polygonal approximation
+Compare the ellipse generated with arcs versus a polygonal approximation
"""
import numpy as npy
from matplotlib import patches
@@ -29,7 +29,7 @@
ax = fig.add_subplot(211, aspect='auto')
ax.fill(x, y, alpha=0.2, facecolor='yellow', edgecolor='yellow', linewidth=1,
zorder=1)
-e1 = patches.Ellipse((xcenter, ycenter), width, height,
+e1 = patches.Arc((xcenter, ycenter), width, height,
angle=angle, linewidth=2, fill=False, zorder=2)
ax.add_patch(e1)
Modified: branches/transforms/unit/ellipse_large.py
===================================================================
--- branches/transforms/unit/ellipse_large.py 2007-12-11 22:03:58 UTC (rev
4699)
+++ branches/transforms/unit/ellipse_large.py 2007-12-12 00:15:23 UTC (rev
4700)
@@ -6,7 +6,7 @@
import math
from pylab import *
-from matplotlib.patches import Ellipse
+from matplotlib.patches import Arc
# given a point x, y
x = 2692.440
@@ -54,22 +54,22 @@
# make the lower-bound ellipse
diam = (r - delta) * 2.0
-lower_ellipse = Ellipse( (0.0, 0.0), diam, diam, 0.0, fill=False,
edgecolor="darkgreen" )
+lower_ellipse = Arc( (0.0, 0.0), diam, diam, 0.0, fill=False,
edgecolor="darkgreen" )
ax.add_patch( lower_ellipse )
# make the target ellipse
diam = r * 2.0
-target_ellipse = Ellipse( (0.0, 0.0), diam, diam, 0.0, fill=False,
edgecolor="darkred" )
+target_ellipse = Arc( (0.0, 0.0), diam, diam, 0.0, fill=False,
edgecolor="darkred" )
ax.add_patch( target_ellipse )
# make the upper-bound ellipse
diam = (r + delta) * 2.0
-upper_ellipse = Ellipse( (0.0, 0.0), diam, diam, 0.0, fill=False,
edgecolor="darkblue" )
+upper_ellipse = Arc( (0.0, 0.0), diam, diam, 0.0, fill=False,
edgecolor="darkblue" )
ax.add_patch( upper_ellipse )
# make the target
diam = delta * 2.0
-target = Ellipse( (x, y), diam, diam, 0.0, fill=False, edgecolor="#DD1208" )
+target = Arc( (x, y), diam, diam, 0.0, fill=False, edgecolor="#DD1208" )
ax.add_patch( target )
# give it a big marker
@@ -104,4 +104,4 @@
ax.set_ylim(6705, 6735)
show()
-savefig("ellipse")
+# savefig("ellipse")
This was sent by the SourceForge.net collaborative development platform, the
world's largest Open Source development site.
-------------------------------------------------------------------------
SF.Net email is sponsored by:
Check out the new SourceForge.net Marketplace.
It's the best place to buy or sell services for
just about anything Open Source.
http://sourceforge.net/services/buy/index.php
_______________________________________________
Matplotlib-checkins mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/matplotlib-checkins