Index: Headers/AppKit/NSBezierPath.h =================================================================== RCS file: /cvsroot/gnustep/gnustep/core/gui/Headers/AppKit/NSBezierPath.h,v retrieving revision 1.3 diff -u -r1.3 NSBezierPath.h --- Headers/AppKit/NSBezierPath.h 12 Jan 2005 15:38:30 -0000 1.3 +++ Headers/AppKit/NSBezierPath.h 29 Jan 2005 15:16:20 -0000 @@ -233,6 +233,15 @@ */ - (BOOL)containsPoint:(NSPoint)point; +#ifndef NO_GNUSTEP +/**
Returns YES iff the receiver, when stroked using the current line
+ width, line cap and line join styles, contains point.
+
+ Note that the current implementation only handles round line joins. +
*/ +-(BOOL) strokeContainsPoint:(NSPoint)point; +#endif + // // Caching // Index: Source/NSBezierPath.m =================================================================== RCS file: /cvsroot/gnustep/gnustep/core/gui/Source/NSBezierPath.m,v retrieving revision 1.34 diff -u -r1.34 NSBezierPath.m --- Source/NSBezierPath.m 21 Jan 2005 20:39:18 -0000 1.34 +++ Source/NSBezierPath.m 29 Jan 2005 15:16:21 -0000 @@ -907,7 +907,7 @@ p = NSMakePoint(originx + width / 2, originy); p1 = NSMakePoint(originx, originy + height / 2 - vdiff); p2 = NSMakePoint(originx + width / 2 - hdiff, originy); - [self curveToPoint: p controlPoint1: p1 controlPoint2: p2]; + [self curveToPoint: p controlPoint1: p1 controlPoint2: p2]; p = NSMakePoint(originx + width, originy + height / 2); p1 = NSMakePoint(originx + width / 2 + hdiff, originy); @@ -1514,6 +1514,446 @@ } } + +/* Helpers for -strokeContainsPoint:. */ + +/* For a fifth degree polynomial p(t), returns an upper bound on the number + of sign changes (and thus roots) where t>x. Uses Descartes' rule of + signs. */ +static int sign_changes_right_of(double p[6], double x) +{ + double np[6]; + int i, j, l; + double x2 = x * x; + double x3 = x2 * x; + double x4 = x2 * x2; + double x5 = x4 * x; + + /* Descartes' rule of signs gives us (a bound on) the number of sign + changes for t > 0. Thus, we compute the coefficients for a new + polynomial np(t) = p(t + x) and use Descartes' on np(t) instead. */ + np[0] = p[0]; + np[1] = p[1] + 5 * x * p[0]; + np[2] = p[2] + 4 * x * p[1] + 10 * x2 * p[0]; + np[3] = p[3] + 3 * x * p[2] + 6 * x2 * p[1] + 10 * x3 * p[0]; + np[4] = p[4] + 2 * x * p[3] + 3 * x2 * p[2] + 4 * x3 * p[1] + 5 * x4 * p[0]; + np[5] = p[5] + x * p[4] + x2 * p[3] + x3 * p[2] + x4 * p[1] + x5 * p[0]; + + l = 0; + for (i = 1, j = 0; i < 6; i++) + { + if (!np[l]) + l = i; + else if (np[l] * np[i] < 0) + { + j++; + l = i; + } + } + + return j; +} + +/* Evaluate a fifth degree polynomial at some point. */ +static double eval(double p[6], double x) +{ + double a; + int i; + for (i = 0, a = 0; i < 6; i++) + a = a * x + p[i]; + return a; +} + +#define CUTOFF 1e-8 +#define CUTOFF_DEPTH 40 + +static int solve_r(double p[6],double *solutions,double x0,double x1,int depth) +{ + int n; + double m; + n = sign_changes_right_of(p, x0) - sign_changes_right_of(p, x1); + + if (!n) + return 0; + + m = (x1 + x0) / 2.0; + + if (x1 - x0 < CUTOFF || depth >= CUTOFF_DEPTH) + { + double e0, e1; + e0 = eval(p, x0); + e1 = eval(p, x1); + if (e0 == 0) + m = x0; + else if (e1 == 0) + m = x1; + else if (eval(p, x0) * eval(p, x1) > 0) + return 0; + *solutions = m; + return 1; + } + + n = solve_r(p, solutions , x0, m, depth + 1); + n += solve_r(p, solutions + n, m, x1, depth + 1); + + return n; +} + +/* Finds all solutions of p(t) = 0 for 0<=t<=1. Solutions are stored + in the solutions array, and the number of solutions (no more than 5 + for a fifth degree polynomial) is returned. */ +static int solve_5(double p[6], double solutions[5]) +{ + return solve_r(p, solutions, 0.0, 1.0, 1); +} + + +/* + +Given a parameterized curve C(t)={x(t),y(t)}, 0<=t<=1, we want to know if +a given point would be "inside" its stroked outline. + +The definition of stroking we use here (the postscript definition, +basically) is that stroking is done by sweeping the normal along the curve. +In other words, each point C(t) covers a set of points that +lies on a line through the point, parallel to the curve's normal at C(t), +and within lineWidth / 2 of p. + +The (unnormalized) tangent N(t) = {x'(t),y'(t)} is perpendicular to the +normal, so a point p is on the line for a point C(t) iff + N(t).p = N(t).C(t) +<=> + N(t) . (p - {x(t),y(t)}) = 0 +<=> + {x'(t), y'(t)} . {p.x-x(t), p.y-y(t)} = 0 +<=> +(1) x'(t) * (p.x-x(t)) + y'(t) * (p.y-y(t)) = 0 + +and if |p - C(t)| <= lineWidth/2. + +Thus, to find out if a given point p is in the stroked curve, we solve (1) +and check for each solution if |p - C(t)| <= lineWidth/2. + + +For a line, we have: + x(t) = cx*t + dx + y(t) = cy*t + dy + +and (1) becomes: + + (-cy^2-cx^2)*t + cy*py + cx*px - cy*dy - cx*dx = 0 + +point_is_in_stroked_line() handles this. + + +For a bezier curve, we have: + x(t) = bax*(1-t)^3 + 3*bbx*(1-t)^2*t + 3*bcx*(1-t)*t^2 + bdx*t^3 + y(t) = bay*(1-t)^3 + 3*bby*(1-t)^2*t + 3*bcy*(1-t)*t^2 + bdy*t^3 + +First, we rewrite this is a normal third degree polynomial: + + a = bd - 3*bc + 3*bb - ba + b = 3*bc - 6*bb + 3*ba + c = 3*bb - 3*ba + d = ba + + x(t) = ax*t^3 + bx*t^2 + cx*t + dx + y(t) = ay*t^3 + by*t^2 + cy*t + dy + +and (1) becomes (using maxima, since we're lazy): + + t^5 * (-3*ay^2 - 3*ax^2) ++ t^4 * (-5*ay*by - 5*ax*bx) ++ t^3 * (-4*ay*cy - 4*ax*cx - 2*by^2 - 2*bx^2) ++ t^2 * ( 3*ay*py + 3*ax*px - 3*ay*dy - 3*ax*dx - 3*by*cy - 3*bx*cx) ++ t * ( 2*by*py + 2*bx*px - 2*by*dy - 2*bx*dx - cy^2 - cx^2) ++ cy*py + cx*px - cy*dy - cx*dx += 0 + +points_on_stroked_curve() and point_is_in_stroked_curve() handle this. + +*/ + +static int points_on_stroked_curve( + double bax, double bbx, double bcx, double bdx, + double bay, double bby, double bcy, double bdy, + double px, double py, double ts[5]) +{ + double ax, bx, cx, dx; + double ay, by, cy, dy; + double p[6]; + + ax = bdx - 3 * bcx + 3 * bbx - bax; + bx = 3 * bcx - 6 * bbx + 3 * bax; + cx = 3 * bbx - 3 * bax; + dx = bax; + + ay = bdy - 3 * bcy + 3 * bby - bay; + by = 3 * bcy - 6 * bby + 3 * bay; + cy = 3 * bby - 3 * bay; + dy = bay; + + p[0] = (-3 * ay * ay - 3 * ax * ax); + p[1] = (-5 * ay * by - 5 * ax * bx); + p[2] = (-4 * ay * cy - 4 * ax * cx - 2 * by * by - 2 * bx * bx); + p[3] = ( 3 * ay * py + 3 * ax * px - 3 * ay * dy - 3 * ax * dx - 3 * by * cy - 3 * bx * cx); + p[4] = ( 2 * by * py + 2 * bx * px - 2 * by * dy - 2 * bx * dx - cy * cy - cx * cx); + p[5] = cy * py + cx * px - cy * dy - cx * dx; + + return solve_5(p, ts); +} + +/* Returns YES if the point px,py is in the stroked curve given by the + b* coordinates. */ +static BOOL point_is_in_stroked_curve( + double bax, double bbx, double bcx, double bdx, + double bay, double bby, double bcy, double bdy, + double px, double py, + double lineWidth) +{ + double ts[5], t; + int i, j; + double x, y; + + lineWidth /= 2; + + /* Get the points of the curve for which px,py lie on the normal. */ + j = points_on_stroked_curve( + bax, bbx, bcx, bdx, + bay, bby, bcy, bdy, + px, py, ts); + + /* For each such point, see if it is within the line width. */ + for (i = 0; i < j; i++) + { + double it, t0, t1, t2, t3; + double dist; + + t = ts[i]; + it = 1 - t; + t0 = it * it * it; + t1 = it * it * t; + t2 = it * t * t; + t3 = t * t * t; + x = bax * t0 + 3 * bbx * t1 + 3 * bcx * t2 + bdx * t3; + y = bay * t0 + 3 * bby * t1 + 3 * bcy * t2 + bdy * t3; + + x -= px; + y -= py; + dist = sqrt(x * x + y * y); + if (dist <= lineWidth) + return 1; + } + return 0; +} + +static BOOL point_is_in_stroked_line( + double x0, double y0, double x1, double y1, + double px, double py, + double lineWidth) +{ + double cx, cy, dx, dy; + double x, y, dist; + double t; + + dx = x0; + dy = y0; + cx = x1 - x0; + cy = y1 - y0; + + /* Handle degenerate lines. */ + if (!cx && !cy) + return NO; + + t = (cy * py + cx * px - cy * dy - cx * dx) / (cx * cx + cy * cy); + + if (t < 0.0 || t > 1.0) + return NO; + + x = dx + cx * t - px; + y = dy + cy * t - py; + dist = sqrt(x * x + y * y); + return dist <= lineWidth / 2; +} + + +/* Return YES iff p is in the line cap formed at p0, with the tangent going + towards p1. */ +static BOOL point_is_in_line_cap(NSPoint p0, NSPoint p1, NSPoint p, + float lineWidth, int style) +{ + if (style == NSButtLineCapStyle) + return NO; + else if (style == NSRoundLineCapStyle) + { + float dx, dy; + dx = p0.x - p.x; + dy = p0.y - p.y; + lineWidth /= 2; + return dx * dx + dy * dy <= lineWidth * lineWidth; + } + else if (style == NSSquareLineCapStyle) + { + float dx, dy, d; + dx = p0.x - p1.x; + dy = p0.y - p1.y; + d = sqrt(dx * dx + dy * dy); + d = lineWidth / 2 / d; + dx *= d; + dy *= d; + return point_is_in_stroked_line(p0.x + dx, p0.y + dy, p1.x, p1.y, + p.x, p.y, lineWidth); + } + return NO; +} + +/* Return YES iff p is in the line join formed at p1, with the tangent + to p1 coming from p0, and tangent from p1 going towards p2. */ +static BOOL point_is_in_line_join(NSPoint p0, NSPoint p1, NSPoint p2, + NSPoint p, float lineWidth, int style) +{ + /* TODO: handle all join styles. Currently, we pretend that + everything is a round join. */ + float dx, dy; + dx = p1.x - p.x; + dy = p1.y - p.y; + lineWidth /= 2; + return dx * dx + dy * dy <= lineWidth * lineWidth; +} + + +-(BOOL) strokeContainsPoint:(NSPoint)point +{ + NSBezierPathElement type; + int count; + int subCount; + NSPoint pts[3]; + NSPoint first_p, last_p; + NSPoint first_tangent_p, last_tangent_p; + NSPoint next_p; + int i; + float lineWidth = [self lineWidth]; + int capStyle = [self lineCapStyle]; + int joinStyle = [self lineJoinStyle]; + + count = [self elementCount]; + + /* 'Unroll' the first element to avoid compiler warnings. It has to be + a MoveTo, anyway. */ + type = [self elementAtIndex: 0 associatedPoints: pts]; + if (type != NSMoveToBezierPathElement) + { + NSWarnLog(@"Invalid path, first element isn't MoveTo."); + return NO; + } + last_p = first_p = pts[0]; + first_tangent_p = last_tangent_p = NSZeroPoint; + subCount = 1; + + for (i = 1; i < count; i++) + { + type = [self elementAtIndex: i associatedPoints: pts]; + switch(type) + { + case NSMoveToBezierPathElement: + if (subCount >= 2) + { + if (point_is_in_line_cap(last_p, last_tangent_p, point, + lineWidth, capStyle)) + return YES; + if (point_is_in_line_cap(first_p, first_tangent_p, point, + lineWidth, capStyle)) + return YES; + } + last_p = first_p = pts[0]; + subCount = 1; + break; + + case NSLineToBezierPathElement: + if (!subCount) + { + NSWarnLog(@"Invalid path, LineTo without MoveTo."); + return NO; + } + if (point_is_in_stroked_line(last_p.x, last_p.y, pts[0].x, + pts[0].y, point.x, point.y, + lineWidth)) + { + return YES; + } + next_p = pts[0]; + goto curve_line_common; + + case NSCurveToBezierPathElement: + if (!subCount) + { + NSWarnLog(@"Invalid path, CurveTo without MoveTo."); + return NO; + } + if (point_is_in_stroked_curve( + last_p.x, pts[0].x, pts[1].x, pts[2].x, + last_p.y, pts[0].y, pts[1].y, pts[2].y, + point.x, point.y, lineWidth)) + { + return YES; + } + next_p = pts[2]; + /* Fall-through. */ + + curve_line_common: + if (subCount >= 2) + if (point_is_in_line_join(last_tangent_p, last_p, next_p, + point, lineWidth, joinStyle)) + return YES; + + if (subCount == 1) + first_tangent_p = next_p; + last_tangent_p = last_p; + last_p = next_p; + subCount++; + break; + + case NSClosePathBezierPathElement: + if (!subCount) + { + NSWarnLog(@"Invalid path, ClosePath with no open subpath."); + return NO; + } + if (subCount >= 2) + { + if (point_is_in_line_join(last_tangent_p, last_p, first_p, + point, lineWidth, joinStyle)) + return YES; + if (point_is_in_line_join(last_p, first_p, first_tangent_p, + point, lineWidth, joinStyle)) + return YES; + } + if (point_is_in_stroked_line(last_p.x, last_p.y, first_p.x, + first_p.y, point.x, point.y, + lineWidth)) + { + return YES; + } + subCount = 0; + break; + default: + NSWarnLog(@"Invalid element in path."); + return NO; + } + } + if (subCount >= 2) + { + if (point_is_in_line_cap(last_p, last_tangent_p, point, + lineWidth, capStyle)) + return YES; + if (point_is_in_line_cap(first_p, first_tangent_p, point, + lineWidth, capStyle)) + return YES; + } + return NO; +} + + // // Caching paths //