Files
gtk/gsk/gskcurveintersect.c
Matthias Clasen e5c97cc7fb curve: Add gsk_curve_intersect
Add a way to find the intersections of two curves.
We can handle some curve-line intersections directly,
the general case is handled via bisecting.

This will be used in stroking and path ops.
2023-07-08 20:44:37 -04:00

880 lines
22 KiB
C

/*
* Copyright © 2020 Red Hat, Inc
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors: Matthias Clasen <mclasen@redhat.com>
*/
#include "config.h"
#include <math.h>
#include "gskcurveprivate.h"
/* {{{ Utilities */
static void
get_tangent (const graphene_point_t *p0,
const graphene_point_t *p1,
graphene_vec2_t *t)
{
graphene_vec2_init (t, p1->x - p0->x, p1->y - p0->y);
graphene_vec2_normalize (t, t);
}
static inline gboolean
acceptable (float t)
{
return 0 - FLT_EPSILON <= t && t <= 1 + FLT_EPSILON;
}
static inline void
_sincosf (float angle,
float *out_s,
float *out_c)
{
#ifdef HAVE_SINCOSF
sincosf (angle, out_s, out_c);
#else
*out_s = sinf (angle);
*out_c = cosf (angle);
#endif
}
static void
align_points (const graphene_point_t *p,
const graphene_point_t *a,
const graphene_point_t *b,
graphene_point_t *q,
int n)
{
graphene_vec2_t n1;
float angle;
float s, c;
get_tangent (a, b, &n1);
angle = - atan2 (graphene_vec2_get_y (&n1), graphene_vec2_get_x (&n1));
_sincosf (angle, &s, &c);
for (int i = 0; i < n; i++)
{
q[i].x = (p[i].x - a->x) * c - (p[i].y - a->y) * s;
q[i].y = (p[i].x - a->x) * s + (p[i].y - a->y) * c;
}
}
static void
find_point_on_line (const graphene_point_t *p1,
const graphene_point_t *p2,
const graphene_point_t *q,
float *t)
{
if (p2->x != p1->x)
*t = (q->x - p1->x) / (p2->x - p1->x);
else
*t = (q->y - p1->y) / (p2->y - p1->y);
}
/* find solutions for at^2 + bt + c = 0 */
static int
solve_quadratic (float a, float b, float c, float t[2])
{
float d;
int n = 0;
if (fabs (a) > 0.0001)
{
if (b*b > 4*a*c)
{
d = sqrt (b*b - 4*a*c);
t[n++] = (-b + d)/(2*a);
t[n++] = (-b - d)/(2*a);
}
else
{
t[n++] = -b / (2*a);
}
}
else if (fabs (b) > 0.0001)
{
t[n++] = -c / b;
}
return n;
}
static int
filter_allowable (float t[3],
int n)
{
float g[3];
int j = 0;
for (int i = 0; i < n; i++)
if (0 < t[i] && t[i] < 1)
g[j++] = t[i];
for (int i = 0; i < j; i++)
t[i] = g[i];
return j;
}
/* Solve P = 0 where P is
* P = (1-t)^2*pa + 2*t*(1-t)*pb + t^2*pc
*/
static int
get_quadratic_roots (float pa, float pb, float pc, float roots[2])
{
float a, b, c, d;
int n_roots = 0;
a = pa - 2 * pb + pc;
b = 2 * (pb - pa);
c = pa;
d = b*b - 4*a*c;
if (d > 0.0001)
{
float q = sqrt (d);
roots[n_roots] = (-b + q) / (2 * a);
if (acceptable (roots[n_roots]))
n_roots++;
roots[n_roots] = (-b - q) / (2 * a);
if (acceptable (roots[n_roots]))
n_roots++;
}
else if (fabs (d) < 0.0001)
{
roots[n_roots] = -b / (2 * a);
if (acceptable (roots[n_roots]))
n_roots++;
}
return n_roots;
}
static float
cuberoot (float v)
{
if (v < 0)
return -pow (-v, 1.f / 3);
return pow (v, 1.f / 3);
}
/* Solve P = 0 where P is
* P = (1-t)^3*pa + 3*t*(1-t)^2*pb + 3*t^2*(1-t)*pc + t^3*pd
*/
static int
get_cubic_roots (float pa, float pb, float pc, float pd, float roots[3])
{
float a, b, c, d;
float q, q2;
float p, p3;
float discriminant;
float u1, v1, sd;
int n_roots = 0;
d = -pa + 3*pb - 3*pc + pd;
a = 3*pa - 6*pb + 3*pc;
b = -3*pa + 3*pb;
c = pa;
if (fabs (d) < 0.0001)
{
if (fabs (a) < 0.0001)
{
if (fabs (b) < 0.0001)
return 0;
if (acceptable (-c / b))
{
roots[0] = -c / b;
return 1;
}
return 0;
}
q = sqrt (b*b - 4*a*c);
roots[n_roots] = (-b + q) / (2 * a);
if (acceptable (roots[n_roots]))
n_roots++;
roots[n_roots] = (-b - q) / (2 * a);
if (acceptable (roots[n_roots]))
n_roots++;
return n_roots;
}
a /= d;
b /= d;
c /= d;
p = (3*b - a*a)/3;
p3 = p/3;
q = (2*a*a*a - 9*a*b + 27*c)/27;
q2 = q/2;
discriminant = q2*q2 + p3*p3*p3;
if (discriminant < 0)
{
float mp3 = -p/3;
float mp33 = mp3*mp3*mp3;
float r = sqrt (mp33);
float t = -q / (2*r);
float cosphi = t < -1 ? -1 : (t > 1 ? 1 : t);
float phi = acos (cosphi);
float crtr = cuberoot (r);
float t1 = 2*crtr;
roots[n_roots] = t1 * cos (phi/3) - a/3;
if (acceptable (roots[n_roots]))
n_roots++;
roots[n_roots] = t1 * cos ((phi + 2*M_PI) / 3) - a/3;
if (acceptable (roots[n_roots]))
n_roots++;
roots[n_roots] = t1 * cos ((phi + 4*M_PI) / 3) - a/3;
if (acceptable (roots[n_roots]))
n_roots++;
return n_roots;
}
if (discriminant == 0)
{
u1 = q2 < 0 ? cuberoot (-q2) : -cuberoot (q2);
roots[n_roots] = 2*u1 - a/3;
if (acceptable (roots[n_roots]))
n_roots++;
roots[n_roots] = -u1 - a/3;
if (acceptable (roots[n_roots]))
n_roots++;
return n_roots;
}
sd = sqrt (discriminant);
u1 = cuberoot (sd - q2);
v1 = cuberoot (sd + q2);
roots[n_roots] = u1 - v1 - a/3;
if (acceptable (roots[n_roots]))
n_roots++;
return n_roots;
}
/* }}} */
/* {{{ Cusps and inflections */
/* Get the points where the curvature of curve is
* zero, or a maximum or minimum, inside the open
* interval from 0 to 1.
*/
int
gsk_curve_get_curvature_points (const GskCurve *curve,
float t[3])
{
const graphene_point_t *pts = curve->cubic.points;
graphene_point_t p[4];
float a, b, c, d;
float x, y, z;
int n;
if (curve->op != GSK_PATH_CUBIC)
return 0;
align_points (pts, &pts[0], &pts[3], p, 4);
a = p[2].x * p[1].y;
b = p[3].x * p[1].y;
c = p[1].x * p[2].y;
d = p[3].x * p[2].y;
x = - 3*a + 2*b + 3*c - d;
y = 3*a - b - 3*c;
z = c - a;
n = solve_quadratic (x, y, z, t);
return filter_allowable (t, n);
}
/* Find cusps inside the open interval from 0 to 1.
*
* According to Stone & deRose, A Geometric Characterization
* of Parametric Cubic curves, a necessary and sufficient
* condition is that the first derivative vanishes.
*/
int
gsk_curve_get_cusps (const GskCurve *curve,
float t[2])
{
const graphene_point_t *pts = curve->cubic.points;
graphene_point_t p[3];
float ax, bx, cx;
float ay, by, cy;
float tx[3];
int nx;
int n = 0;
if (curve->op != GSK_PATH_CUBIC)
return 0;
p[0].x = 3 * (pts[1].x - pts[0].x);
p[0].y = 3 * (pts[1].y - pts[0].y);
p[1].x = 3 * (pts[2].x - pts[1].x);
p[1].y = 3 * (pts[2].y - pts[1].y);
p[2].x = 3 * (pts[3].x - pts[2].x);
p[2].y = 3 * (pts[3].y - pts[2].y);
ax = p[0].x - 2 * p[1].x + p[2].x;
bx = - 2 * p[0].x + 2 * p[1].x;
cx = p[0].x;
nx = solve_quadratic (ax, bx, cx, tx);
nx = filter_allowable (tx, nx);
ay = p[0].y - 2 * p[1].y + p[2].y;
by = - 2 * p[0].y + 2 * p[1].y;
cy = p[0].y;
for (int i = 0; i < nx; i++)
{
float ti = tx[i];
if (0 < ti && ti < 1 &&
fabs (ay * ti * ti + by * ti + cy) < 0.001)
t[n++] = ti;
}
return n;
}
/* }}} */
/* {{{ Intersection */
static int
line_intersect (const GskCurve *curve1,
const GskCurve *curve2,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
const graphene_point_t *pts1 = curve1->line.points;
const graphene_point_t *pts2 = curve2->line.points;
float a1 = pts1[0].x - pts1[1].x;
float b1 = pts1[0].y - pts1[1].y;
float a2 = pts2[0].x - pts2[1].x;
float b2 = pts2[0].y - pts2[1].y;
float det = a1 * b2 - b1 * a2;
if (fabs(det) > 0.01)
{
float tt = ((pts1[0].x - pts2[0].x) * b2 - (pts1[0].y - pts2[0].y) * a2) / det;
float ss = - ((pts1[0].y - pts2[0].y) * a1 - (pts1[0].x - pts2[0].x) * b1) / det;
if (acceptable (tt) && acceptable (ss))
{
p[0].x = pts1[0].x + tt * (pts1[1].x - pts1[0].x);
p[0].y = pts1[0].y + tt * (pts1[1].y - pts1[0].y);
t1[0] = tt;
t2[0] = ss;
return 1;
}
}
else /* parallel lines */
{
float r = a1 * (pts1[1].y - pts2[0].y) - (pts1[1].x - pts2[0].x) * b1;
float dist = (r * r) / (a1 * a1 + b1 * b1);
float t, s, tt, ss;
if (dist > 0.01)
return 0;
if (pts1[1].x != pts1[0].x)
{
t = (pts2[0].x - pts1[0].x) / (pts1[1].x - pts1[0].x);
s = (pts2[1].x - pts1[0].x) / (pts1[1].x - pts1[0].x);
}
else
{
t = (pts2[0].y - pts1[0].y) / (pts1[1].y - pts1[0].y);
s = (pts2[1].y - pts1[0].y) / (pts1[1].y - pts1[0].y);
}
if ((t < 0 && s < 0) || (t > 1 && s > 1))
return 0;
if (acceptable (t))
{
t1[0] = t;
t2[0] = 0;
p[0] = pts2[0];
}
else if (t < 0)
{
if (pts2[1].x != pts2[0].x)
tt = (pts1[0].x - pts2[0].x) / (pts2[1].x - pts2[0].x);
else
tt = (pts1[0].y - pts2[0].y) / (pts2[1].y - pts2[0].y);
t1[0] = 0;
t2[0] = tt;
p[0] = pts1[0];
}
else
{
if (pts2[1].x != pts2[0].x)
tt = (pts1[1].x - pts2[0].x) / (pts2[1].x - pts2[0].x);
else
tt = (pts1[1].y - pts2[0].y) / (pts2[1].y - pts2[0].y);
t1[0] = 1;
t2[0] = tt;
p[0] = pts1[1];
}
if (acceptable (s))
{
if (t2[0] == 1)
return 1;
t1[1] = s;
t2[1] = 1;
p[1] = pts2[1];
}
else if (s < 0)
{
if (t1[0] == 0)
return 1;
if (pts2[1].x != pts2[0].x)
ss = (pts1[0].x - pts2[0].x) / (pts2[1].x - pts2[0].x);
else
ss = (pts1[0].y - pts2[0].y) / (pts2[1].y - pts2[0].y);
t1[1] = 0;
t2[1] = ss;
p[1] = pts1[0];
}
else
{
if (t1[0] == 1)
return 1;
if (pts2[1].x != pts2[0].x)
ss = (pts1[1].x - pts2[0].x) / (pts2[1].x - pts2[0].x);
else
ss = (pts1[1].y - pts2[0].y) / (pts2[1].y - pts2[0].y);
t1[1] = 1;
t2[1] = ss;
p[1] = pts1[1];
}
return 2;
}
return 0;
}
static int
line_quad_intersect (const GskCurve *curve1,
const GskCurve *curve2,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
const graphene_point_t *a = &curve1->line.points[0];
const graphene_point_t *b = &curve1->line.points[1];
graphene_point_t pts[4];
float t[2];
int m, i, j;
/* Rotate things to place curve1 on the x axis,
* then solve curve2 for y == 0.
*/
align_points (curve2->quad.points, a, b, pts, 3);
m = get_quadratic_roots (pts[0].y, pts[1].y, pts[2].y, t);
j = 0;
for (i = 0; i < m; i++)
{
t2[j] = t[i];
gsk_curve_get_point (curve2, t2[j], &p[j]);
find_point_on_line (a, b, &p[j], &t1[j]);
if (acceptable (t1[j]))
j++;
if (j == n)
break;
}
return j;
}
static int
line_cubic_intersect (const GskCurve *curve1,
const GskCurve *curve2,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
const graphene_point_t *a = &curve1->line.points[0];
const graphene_point_t *b = &curve1->line.points[1];
graphene_point_t pts[4];
float t[3];
int m, i, j;
/* Rotate things to place curve1 on the x axis,
* then solve curve2 for y == 0.
*/
align_points (curve2->cubic.points, a, b, pts, 4);
m = get_cubic_roots (pts[0].y, pts[1].y, pts[2].y, pts[3].y, t);
j = 0;
for (i = 0; i < m; i++)
{
t2[j] = t[i];
gsk_curve_get_point (curve2, t2[j], &p[j]);
find_point_on_line (a, b, &p[j], &t1[j]);
if (acceptable (t1[j]))
j++;
if (j == n)
break;
}
return j;
}
#define MAX_LEVEL 25
#define TOLERANCE 0.001
static void
cubic_intersect_recurse (const GskCurve *curve1,
const GskCurve *curve2,
float t1l,
float t1r,
float t2l,
float t2r,
float *t1,
float *t2,
graphene_point_t *p,
int n,
int *pos,
int level)
{
GskCurve p11, p12, p21, p22;
GskBoundingBox b1, b2;
float d1, d2;
if (*pos == n)
return;
if (level == MAX_LEVEL)
return;
gsk_curve_get_bounds (curve1, &b1);
gsk_curve_get_bounds (curve2, &b2);
if (!gsk_bounding_box_intersection (&b1, &b2, NULL))
return;
gsk_curve_get_tight_bounds (curve1, &b1);
if (!gsk_bounding_box_intersection (&b1, &b2, NULL))
return;
gsk_curve_get_tight_bounds (curve2, &b2);
if (!gsk_bounding_box_intersection (&b1, &b2, NULL))
return;
d1 = (t1r - t1l) / 2;
d2 = (t2r - t2l) / 2;
if (b1.max.x - b1.min.x < TOLERANCE && b1.max.y - b1.min.y < TOLERANCE &&
b2.max.x - b2.min.x < TOLERANCE && b2.max.y - b2.min.y < TOLERANCE)
{
graphene_point_t c;
t1[*pos] = t1l + d1;
t2[*pos] = t2l + d2;
gsk_curve_get_point (curve1, 0.5, &c);
for (int i = 0; i < *pos; i++)
{
if (graphene_point_near (&c, &p[i], 0.1))
return;
}
p[*pos] = c;
(*pos)++;
return;
}
gsk_curve_split (curve1, 0.5, &p11, &p12);
gsk_curve_split (curve2, 0.5, &p21, &p22);
cubic_intersect_recurse (&p11, &p21, t1l, t1l + d1, t2l, t2l + d2, t1, t2, p, n, pos, level + 1);
cubic_intersect_recurse (&p11, &p22, t1l, t1l + d1, t2l + d2, t2r, t1, t2, p, n, pos, level + 1);
cubic_intersect_recurse (&p12, &p21, t1l + d1, t1r, t2l, t2l + d2, t1, t2, p, n, pos, level + 1);
cubic_intersect_recurse (&p12, &p22, t1l + d1, t1r, t2l + d2, t2r, t1, t2, p, n, pos, level + 1);
}
static int
cubic_intersect (const GskCurve *curve1,
const GskCurve *curve2,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
int pos = 0;
cubic_intersect_recurse (curve1, curve2, 0, 1, 0, 1, t1, t2, p, n, &pos, 0);
return pos;
}
static void
get_bounds (const GskCurve *curve,
float tl,
float tr,
GskBoundingBox *bounds)
{
GskCurve c;
gsk_curve_segment (curve, tl, tr, &c);
gsk_curve_get_tight_bounds (&c, bounds);
}
static void
general_intersect_recurse (const GskCurve *curve1,
const GskCurve *curve2,
float t1l,
float t1r,
float t2l,
float t2r,
float *t1,
float *t2,
graphene_point_t *p,
int n,
int *pos,
int level)
{
GskBoundingBox b1, b2;
float d1, d2;
if (*pos == n)
return;
if (level == MAX_LEVEL)
return;
get_bounds (curve1, t1l, t1r, &b1);
get_bounds (curve2, t2l, t2r, &b2);
if (!gsk_bounding_box_intersection (&b1, &b2, NULL))
return;
d1 = (t1r - t1l) / 2;
d2 = (t2r - t2l) / 2;
if (b1.max.x - b1.min.x < TOLERANCE && b1.max.y - b1.min.y < TOLERANCE &&
b2.max.x - b2.min.x < TOLERANCE && b2.max.y - b2.min.y < TOLERANCE)
{
graphene_point_t c;
t1[*pos] = t1l + d1;
t2[*pos] = t2l + d2;
gsk_curve_get_point (curve1, t1[*pos], &c);
for (int i = 0; i < *pos; i++)
{
if (graphene_point_near (&c, &p[i], 0.1))
return;
}
p[*pos] = c;
(*pos)++;
return;
}
/* Note that in the conic case, we cannot just split the curves and
* pass the two halves down, since splitting changes the parametrization,
* and we need the t's to be valid parameters wrt to the original curve.
*
* So, instead, we determine the bounding boxes above by always starting
* from the original curve. That is a bit less efficient, but also works
* for conics.
*/
general_intersect_recurse (curve1, curve2, t1l, t1l + d1, t2l, t2l + d2, t1, t2, p, n, pos, level + 1);
general_intersect_recurse (curve1, curve2, t1l, t1l + d1, t2l + d2, t2r, t1, t2, p, n, pos, level + 1);
general_intersect_recurse (curve1, curve2, t1l + d1, t1r, t2l, t2l + d2, t1, t2, p, n, pos, level + 1);
general_intersect_recurse (curve1, curve2, t1l + d1, t1r, t2l + d2, t2r, t1, t2, p, n, pos, level + 1);
}
static int
general_intersect (const GskCurve *curve1,
const GskCurve *curve2,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
int pos = 0;
general_intersect_recurse (curve1, curve2, 0, 1, 0, 1, t1, t2, p, n, &pos, 0);
return pos;
}
static int
curve_self_intersect (const GskCurve *curve,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
float tt[3], ss[3], s;
graphene_point_t pp[3];
int m;
GskCurve cs, ce;
if (curve->op != GSK_PATH_CUBIC)
return 0;
s = 0.5;
m = gsk_curve_get_curvature_points (curve, tt);
for (int i = 0; i < m; i++)
{
if (gsk_curve_get_curvature (curve, tt[i], NULL) == 0)
{
s = tt[i];
break;
}
}
gsk_curve_split (curve, s, &cs, &ce);
m = cubic_intersect (&cs, &ce, tt, ss, pp, 3);
if (m > 1)
{
/* One of the (at most 2) intersections we found
* must be the common point where we split the curve.
* It will have a t value of 1 and an s value of 0.
*/
if (fabs (tt[0] - 1) > 1e-3)
{
t1[0] = t2[0] = tt[0] * s;
p[0] = pp[0];
}
else if (fabs (tt[1] - 1) > 1e-3)
{
t1[0] = t2[0] = tt[1] * s;
p[0] = pp[1];
}
if (n == 1)
return 1;
if (fabs (ss[0]) > 1e-3)
{
t1[1] = t2[1] = s + ss[0] * (1 - s);
p[1] = pp[0];
}
else if (fabs (ss[1]) > 1e-3)
{
t1[1] = t2[1] = s + ss[1] * (1 - s);
p[1] = pp[1];
}
return 2;
}
return 0;
}
static inline gboolean
curve_equal (const GskCurve *c1,
const GskCurve *c2)
{
gsize curve_size[] = {
sizeof (GskLineCurve),
sizeof (GskLineCurve),
sizeof (GskLineCurve),
sizeof (GskQuadCurve),
sizeof (GskCubicCurve),
sizeof (GskConicCurve)
};
return c1->op == c2->op && memcmp (c1, c2, curve_size[c1->op]) == 0;
}
/* Place intersections between the curves in p, and their Bezier positions
* in t1 and t2, up to n. Return the number of intersections found.
*
* Note that two cubic Beziers can have up to 9 intersections.
*/
int
gsk_curve_intersect (const GskCurve *curve1,
const GskCurve *curve2,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
GskPathOperation op1 = curve1->op;
GskPathOperation op2 = curve2->op;
if (op1 == GSK_PATH_CLOSE)
op1 = GSK_PATH_LINE;
if (op2 == GSK_PATH_CLOSE)
op2 = GSK_PATH_LINE;
if (curve_equal (curve1, curve2))
return curve_self_intersect (curve1, t1, t2, p, n);
/* We special-case line-line and line-cubic intersections,
* since we can solve them directly.
* Everything else is done via bisection.
*/
if (op1 == GSK_PATH_LINE && op2 == GSK_PATH_LINE)
return line_intersect (curve1, curve2, t1, t2, p, n);
else if (op1 == GSK_PATH_LINE && op2 == GSK_PATH_QUAD)
return line_quad_intersect (curve1, curve2, t1, t2, p, n);
else if (op1 == GSK_PATH_QUAD && op2 == GSK_PATH_LINE)
return line_quad_intersect (curve2, curve1, t2, t1, p, n);
else if (op1 == GSK_PATH_LINE && op2 == GSK_PATH_CUBIC)
return line_cubic_intersect (curve1, curve2, t1, t2, p, n);
else if (op1 == GSK_PATH_CUBIC && op2 == GSK_PATH_LINE)
return line_cubic_intersect (curve2, curve1, t2, t1, p, n);
else if ((op1 == GSK_PATH_QUAD || op1 == GSK_PATH_CUBIC) &&
(op2 == GSK_PATH_QUAD || op2 == GSK_PATH_CUBIC))
return cubic_intersect (curve1, curve2, t1, t2, p, n);
else
return general_intersect (curve1, curve2, t1, t2, p, n);
}
/* }}} */
/* vim:set foldmethod=marker expandtab: */