Revision: 4783
http://matplotlib.svn.sourceforge.net/matplotlib/?rev=4783&view=rev
Author: mdboom
Date: 2007-12-21 07:08:38 -0800 (Fri, 21 Dec 2007)
Log Message:
-----------
Add size-dependent highly-accurate arc drawing.
Modified Paths:
--------------
trunk/matplotlib/lib/matplotlib/patches.py
Modified: trunk/matplotlib/lib/matplotlib/patches.py
===================================================================
--- trunk/matplotlib/lib/matplotlib/patches.py 2007-12-20 17:18:12 UTC (rev
4782)
+++ trunk/matplotlib/lib/matplotlib/patches.py 2007-12-21 15:08:38 UTC (rev
4783)
@@ -973,6 +973,337 @@
__init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd
+class Arc(Ellipse):
+ """
+ An elliptical arc. Because it performs various optimizations, it
+ can 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), which should be adequate when the number of pixels across
+ the image is less than 5e5. 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 each
+ pair of intersections are drawn using the bezier arc
+ approximation technique implemented in arc().
+ """
+ # Do the usual GC handling stuff
+ if not self.get_visible(): return
+ gc = renderer.new_gc()
+ gc.set_foreground(self._edgecolor)
+ gc.set_linewidth(self._linewidth)
+ gc.set_alpha(self._alpha)
+ gc.set_antialiased(self._antialiased)
+ self._set_gc_clip(gc)
+ gc.set_capstyle('projecting')
+ if not self.fill or self._facecolor is None: rgbFace = None
+ else: rgbFace = colors.colorConverter.to_rgb(self._facecolor)
+ if self._hatch:
+ gc.set_hatch(self._hatch )
+
+ def iter_circle_intersect_on_line(x0, y0, x1, y1):
+ dx = x1 - x0
+ dy = y1 - y0
+ dr2 = dx*dx + dy*dy
+ 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:
+ # The definition of "sign" here is different from
+ # npy.sign: we never want to get 0.0
+ if dy < 0.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
+
+ def arc(theta1, theta2, trans, n=None):
+ """
+ Returns an arc on the unit circle from angle theta1 to
+ angle theta2 (in degrees). The returned arc is already
+ transformed using the affine transformation matrix trans.
+ The arc is returned as an agg::path_storage object.
+
+ If n is provided, it is the number of spline segments to make.
+ If n is not provided, the number of spline segments is determined
+ based on the delta between theta1 and theta2.
+ """
+ # From Masionobe, L. 2003. "Drawing an elliptical arc using
+ # polylines, quadratic or cubic Bezier curves".
+ #
+ # http://www.spaceroots.org/documents/ellipse/index.html
+
+ # degrees to radians
+ theta1 *= npy.pi / 180.0
+ theta2 *= npy.pi / 180.0
+
+ twopi = npy.pi * 2.0
+ halfpi = npy.pi * 0.5
+
+ eta1 = npy.arctan2(npy.sin(theta1), npy.cos(theta1))
+ eta2 = npy.arctan2(npy.sin(theta2), npy.cos(theta2))
+ eta2 -= twopi * npy.floor((eta2 - eta1) / twopi)
+ if (theta2 - theta1 > npy.pi) and (eta2 - eta1 < npy.pi):
+ eta2 += twopi
+
+ # number of curve segments to make
+ if n is None:
+ n = int(2 ** npy.ceil((eta2 - eta1) / halfpi))
+
+ deta = (eta2 - eta1) / n
+ t = npy.tan(0.5 * deta)
+ alpha = npy.sin(deta) * (npy.sqrt(4.0 + 3.0 * t * t) - 1) / 3.0
+
+ steps = npy.linspace(eta1, eta2, n + 1, True)
+ cos_eta = npy.cos(steps)
+ sin_eta = npy.sin(steps)
+
+ xA = cos_eta[:-1]
+ yA = sin_eta[:-1]
+ xA_dot = -yA
+ yA_dot = xA
+
+ xB = cos_eta[1:]
+ yB = sin_eta[1:]
+ xB_dot = -yB
+ yB_dot = xB
+
+ length = n * 3 + 1
+ vertices = npy.zeros((length, 2), npy.float_)
+ vertices[0] = [xA[0], yA[0]]
+ end = length
+
+ vertices[1::3, 0] = xA + alpha * xA_dot
+ vertices[1::3, 1] = yA + alpha * yA_dot
+ vertices[2::3, 0] = xB - alpha * xB_dot
+ vertices[2::3, 1] = yB - alpha * yB_dot
+ vertices[3::3, 0] = xB
+ vertices[3::3, 1] = yB
+
+ vertices = affine_transform(vertices, trans)
+
+ path = agg.path_storage()
+ path.move_to(*vertices[0])
+ for i in range(1, length, 3):
+ path.curve4(*vertices[i:i+3].flat)
+ return path
+
+ def point_in_polygon(x, y, poly):
+ inside = False
+ for i in range(len(poly) - 1):
+ p1x, p1y = poly[i]
+ p2x, p2y = poly[i+1]
+ if p1x < p2x:
+ xmin, xmax = p1x, p2x
+ else:
+ xmin, xmax = p2x, p1x
+ if p1y < p2y:
+ ymin, ymax = p1y, p2y
+ else:
+ ymin, ymax = p2y, p1y
+ if (y > ymin and
+ y <= ymax and
+ x <= xmax):
+ xinters = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x
+ if p1x == p2x or x <= xinters:
+ inside = not inside
+ return inside
+
+ def affine_transform(vertices, transform):
+ # This may seem silly, but it's faster than expanding the
+ # vertices array to Nx3 and then back to Nx2
+ transform = transform.copy()
+ transform[0, 1], transform[1, 0] = transform[1, 0], transform[0, 1]
+ vertices = npy.dot(vertices, transform[0:2, 0:2])
+ vertices += transform[0:2, 2:].flat
+ return vertices
+
+ # Set up the master transform from unit circle, all the way to
+ # display space.
+ trans = self.get_transform()
+ scale = npy.array(
+ [[self.width * 0.5, 0.0, 0.0],
+ [0.0, self.height * 0.5, 0.0],
+ [0.0, 0.0, 1.0]], npy.float_)
+ theta = (self.angle / 180.0) * npy.pi
+ rotate = npy.array(
+ [[npy.cos(theta), -npy.sin(theta), 0.0],
+ [npy.sin(theta), npy.cos(theta), 0.0],
+ [0.0, 0.0, 1.0]], npy.float_)
+ translate = npy.array(
+ [[1.0, 0.0, self.center[0]],
+ [0.0, 1.0, self.center[1]],
+ [0.0, 0.0, 1.0]], npy.float_)
+ sx, b, c, sy, tx, ty = trans.as_vec6_val()
+ dataTrans = npy.array(
+ [[sx, b, tx],
+ [c, sy, ty],
+ [0, 0, 1]], npy.float_)
+ mainTrans = \
+ npy.dot(
+ npy.dot(
+ npy.dot(dataTrans, translate), rotate), scale)
+
+ # Determine the size of the ellipse in pixels, and use
+ # that as a threshold to use the fast (whole ellipse)
+ # technique or accurate (partial arcs) technique.
+ size = affine_transform(
+ npy.array([[self.width, self.height]], npy.float_),
+ mainTrans)
+ width = size[0,0]
+ height = size[0,1]
+ # We divide the error in half, to just be *really*
+ # conservative
+ inv_error = (1.0 / 1.89818e-6) * 0.5
+
+ if width < inv_error and height < inv_error:
+ path = arc(self._theta1, self._theta2, mainTrans)
+ renderer.draw_path(gc, rgbFace, path)
+ return
+
+ # 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.
+ axes_bbox = self.axes.bbox
+ box_path = npy.array(
+ [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]],
+ npy.float_)
+ axesTrans = npy.array(
+ [[axes_bbox.width(), 0.0, axes_bbox.xmin()],
+ [0.0, axes_bbox.height(), axes_bbox.ymin()],
+ [0.0, 0.0, 1.0]], npy.float_)
+ boxTrans = npy.dot(npy.linalg.inv(mainTrans), axesTrans)
+ box_path = affine_transform(box_path, boxTrans)
+
+ 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[:-1], box_path[1:]):
+ x0, y0 = p0
+ x1, y1 = p1
+ for x, y in iter_circle_intersect_on_line_seg(x0, y0, x1, y1):
+ theta = npy.arccos(x)
+ if y < 0:
+ theta = TWOPI - theta
+ # Convert radians to angles
+ 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 = point_in_polygon(npy.cos(theta1_rad), npy.sin(theta1_rad),
box_path)
+ for theta in thetas:
+ if inside:
+ path = arc(last_theta, theta, mainTrans, 8)
+ renderer.draw_path(gc, rgbFace, path)
+ inside = False
+ else:
+ inside = True
+ last_theta = theta
+
+
class PolygonInteractor:
"""
An polygon editor.
This was sent by the SourceForge.net collaborative development platform, the
world's largest Open Source development site.
-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2005.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Matplotlib-checkins mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/matplotlib-checkins