[cairo] Correcting the 'wild miter' test for bug 7245
Keith Packard
keithp at keithp.com
Thu Jan 3 18:37:53 PST 2008
Bug 7245 identified a case where cairo drew a dramatically incorrect
miter join. The root cause of these 'wild miters' was two
nearlyparallel segments  slight imperfections in the positioning of
the ends of the two faces caused the miter position to move far from the
correct position.
Carl's original fix had a minor flaw  it was using the userspace line
width as if it were measured in devicespace units. When the transform
was far from the identity, his miter length computation would be
inaccurate enough to accidentally draw bevel joins on many simple lines.
Attempting to fix this uncovered a more subtle flaw  the underlying
algorithm assumed a square transformation where the line widths of the
two faces were exactly the same. Under a strongly rectangular transform,
when those line widths varied greatly, the computation wouldn't work at
all.
Here's a proposed patch that replaces the highlevel test with a simple
sanity check for the resulting miter point. If the computed miter point
does not lie between the two faces, the miter is replaced with a bevel.
I've just pushed a test which checks this case; this, in combination
with the existing path extents check should suffice to test both
negative and positive versions of the miter test.
commit a86fd00e0e10fa0ad564579a4ebd020375735b72
Author: Keith Packard <keithp at keithp.com>
Date: Thu Jan 3 18:22:16 2008 0800
Directly check the miter corner to detect wild miters.
The original test for wild miters would only work with a square transform
(and, in fact, the original code required an identity transform). Instead of
fixing that, I replaced it with a more obvious test which makes sure the
miter corner lies between the two faces and not out in space somewhere.
diff git a/src/cairopathstroke.c b/src/cairopathstroke.c
index fb12f15..773e9fd 100644
 a/src/cairopathstroke.c
+++ b/src/cairopathstroke.c
@@ 205,6 +205,22 @@ _cairo_stroker_face_clockwise (cairo_stroke_face_t *in, cairo_stroke_face_t *out
return _cairo_slope_clockwise (&in_slope, &out_slope);
}
+/**
+ * _cairo_slope_compare_sgn
+ *
+ * Return 1, 0 or 1 depending on the relative slopes of
+ * two lines.
+ */
+static int
+_cairo_slope_compare_sgn (double dx1, double dy1, double dx2, double dy2)
+{
+ double c = (dx1 * dy2  dx2 * dy1);
+
+ if (c > 0) return 1;
+ if (c < 0) return 1;
+ return 0;
+}
+
static cairo_status_t
_cairo_stroker_join (cairo_stroker_t *stroker, cairo_stroke_face_t *in, cairo_stroke_face_t *out)
{
@@ 272,9 +288,6 @@ _cairo_stroker_join (cairo_stroker_t *stroker, cairo_stroke_face_t *in, cairo_st
double in_dot_out = ((in>usr_vector.x * out>usr_vector.x)+
(in>usr_vector.y * out>usr_vector.y));
double ml = stroker>style>miter_limit;
 double tolerance_squared = stroker>tolerance * stroker>tolerance;
 double line_width_squared = (stroker>style>line_width *
 stroker>style>line_width);
/* Check the miter limit  lines meeting at an acute angle
* can generate long miters, the limit converts them to bevel
@@ 332,84 +345,17 @@ _cairo_stroker_join (cairo_stroker_t *stroker, cairo_stroke_face_t *in, cairo_st
*
* 2 <= ml² (1  in · out)
*
 *
 * That gives us the condition to avoid generating miters that
 * are too large from angles that are too large. But we also
 * need to avoid generating miters when the angle is very small.
 *
 * The miter formed from a tiny angle is also tiny, so the
 * miter limit is not a concern. But with a tiny angle we will
 * be computing the intersection of two lines that are very
 * near parallel. Also, the limits of the fixedpoint grid on
 * the input face coordinates mean that the resulting
 * intersection could be wildly wrong. (See the
 * getpathextents test case for a call to cairo_arc that
 * results in two problematic faces.)
 *
 * Fortunately we can also derive an expression for when using
 * a bevel join instead of a miter will introduce an error no
 * larger than the tolerance. Consider the same join from
 * before but with the miter now chopped off and replaced with
 * a bevel join. The drawing is zoomed in a bit again, the
 * point marked as '*' is the center of the strokethe point
 * where the two line segments of interest intersect:
 *
 *  .
 * ^ ..
 *  . .
 *  . .
 * 1/2 . .
 * miter . . 
 * length . . 
 *  .______. ___v___
 *   . \ 1/2 bevel
 * v  . \ width
 *  * \ 
 *  \ ^
 *
 *
 * The length of interest here is the vertical length of the
 * miter that is eliminated. It's length can be obtained by
 * starting with 1/2 the miter length and the subtracting off
 * the vertical length that is included by the bevel join,
 * (here termed 1/2 bevel width). To determine this new bevel
 * width, we have a small right triangle shown, the hypotenuse
 * of which has a length of 1/2 the line width, and the small
 * angle at the upper right of the figure is psi/2.
 *
 * So we have:
 *
 * sin (psi/2) = (bevel_width / 2) / (line_width / 2)
 *
 * And we can determine when the miter is required by
 * calculating when the eliminated portion of the miter is
 * greater than the tolerance:
 *
 * (miter_length / 2)  (bevel_width / 2) > tolerance
 *
 * Substituting in the above expressions for miter_length and
 * bevel_width:
 *
 * (line_width/2) / sin (psi/2)  (line_width/2) * sin (psi/2) > tolerance
 * 1 / sin(psi/2)  sin (psi/2) > 2 * tolerance / line_width
 * 1 / sin²(psi/2) 2 + sin²(psi/2) > 4 * (tolerance/line_width)²
 *
 * Use identity: sin²(psi/2) = (1cos(psi))/2

 * 2/(1  cos(psi))  2 + (1cos(psi))/2 > 4 * (tolerance/line_width)²
 * 4/(1  cos(psi))  4 + (1cos(psi)) > 8 * (tolerance/line_width)²
 * 4/(1  cos(psi)) + (1cos(psi)) > 8 * ((tolerance/line_width)² + 0.5)
*/
 if ((2 <= ml * ml * (1  in_dot_out)) &&
 ((8 * (tolerance_squared / line_width_squared + 0.5)) <
 4 / (1  in_dot_out) + (1  in_dot_out))
 )
+ if (2 <= ml * ml * (1  in_dot_out))
{
double x1, y1, x2, y2;
double mx, my;
double dx1, dx2, dy1, dy2;
cairo_point_t outer;
cairo_point_t quad[4];
+ double ix, iy;
+ double fdx1, fdy1, fdx2, fdy2;
+ double mdx, mdy;
/*
* we've got the points already transformed to device
@@ 447,17 +393,46 @@ _cairo_stroker_join (cairo_stroker_t *stroker, cairo_stroke_face_t *in, cairo_st
mx = (my  y2) * dx2 / dy2 + x2;
/*
 * Draw the quadrilateral
+ * When the two outer edges are nearly parallel, slight
+ * perturbations in the position of the outer points of the lines
+ * caused by representing them in fixed point form can cause the
+ * intersection point of the miter to move a large amount. If
+ * that moves the miter intersection from between the two faces,
+ * then draw a bevel instead.
*/
 outer.x = _cairo_fixed_from_double (mx);
 outer.y = _cairo_fixed_from_double (my);
 quad[0] = in>point;
 quad[1] = *inpt;
 quad[2] = outer;
 quad[3] = *outpt;
+ ix = _cairo_fixed_to_double (in>point.x);
+ iy = _cairo_fixed_to_double (in>point.y);
 return _cairo_traps_tessellate_convex_quad (stroker>traps, quad);
+ /* slope of one face */
+ fdx1 = x1  ix; fdy1 = y1  iy;
+
+ /* slope of the other face */
+ fdx2 = x2  ix; fdy2 = y2  iy;
+
+ /* slope from the intersection to the miter point */
+ mdx = mx  ix; mdy = my  iy;
+
+ /*
+ * Make sure the miter point line lies between the two
+ * faces by comparing the slopes
+ */
+ if (_cairo_slope_compare_sgn (fdx1, fdy1, mdx, mdy) !=
+ _cairo_slope_compare_sgn (fdx2, fdy2, mdx, mdy))
+ {
+ /*
+ * Draw the quadrilateral
+ */
+ outer.x = _cairo_fixed_from_double (mx);
+ outer.y = _cairo_fixed_from_double (my);
+
+ quad[0] = in>point;
+ quad[1] = *inpt;
+ quad[2] = outer;
+ quad[3] = *outpt;
+
+ return _cairo_traps_tessellate_convex_quad (stroker>traps, quad);
+ }
}
/* fall through ... */
}

keith.packard at intel.com
 next part 
A nontext attachment was scrubbed...
Name: not available
Type: application/pgpsignature
Size: 189 bytes
Desc: This is a digitally signed message part
Url : http://lists.cairographics.org/archives/cairo/attachments/20080103/f9cd9d4c/attachment0001.pgp
More information about the cairo
mailing list