From: <md...@us...> - 2007-12-12 00:15:30
|
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) #@classmethod - 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) #@classmethod - 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. |