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

Reply via email to