Files
gtk/gsk/gskpathstroke.c
Matthias Clasen e01b168fa8 Implement offsetting
Implement offsetting of paths by reusing the
infrastructure of the stroker.
2023-07-06 13:45:00 -04:00

2612 lines
71 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 "gskpathprivate.h"
#include "gskpathbuilder.h"
#include "gskstrokeprivate.h"
#include "gskcurveprivate.h"
#include "gskpathdashprivate.h"
#include "gskpathopprivate.h"
#define STROKE_DEBUG
/* {{{ General utilities */
#define RAD_TO_DEG(r) ((r)*180.0/M_PI)
#define DEG_TO_RAD(d) ((d)*M_PI/180.0)
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 int
cmpfloat (const void *p1, const void *p2)
{
const float *f1 = p1;
const float *f2 = p2;
return *f1 < *f2 ? -1 : (*f1 > *f2 ? 1 : 0);
}
/* }}} */
/* {{{ graphene 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 void
get_normal (const graphene_point_t *p0,
const graphene_point_t *p1,
graphene_vec2_t *n)
{
graphene_vec2_init (n, p0->y - p1->y, p1->x - p0->x);
graphene_vec2_normalize (n, n);
}
/* Return the angle between t1 and t2 in radians, such that
* 0 means straight continuation
* < 0 means right turn
* > 0 means left turn
*/
static float
angle_between (const graphene_vec2_t *t1,
const graphene_vec2_t *t2)
{
float angle = atan2 (graphene_vec2_get_y (t2), graphene_vec2_get_x (t2))
- atan2 (graphene_vec2_get_y (t1), graphene_vec2_get_x (t1));
if (angle > M_PI)
angle -= 2 * M_PI;
if (angle < - M_PI)
angle += 2 * M_PI;
return angle;
}
static float
angle_between_points (const graphene_point_t *c,
const graphene_point_t *a,
const graphene_point_t *b)
{
graphene_vec2_t t1, t2;
graphene_vec2_init (&t1, a->x - c->x, a->y - c->y);
graphene_vec2_init (&t2, b->x - c->x, b->y - c->y);
return angle_between (&t1, &t2);
}
static void
rotate_vector (const graphene_vec2_t *t,
float alpha,
graphene_vec2_t *t2)
{
float x, y, s, c;
x = graphene_vec2_get_x (t);
y = graphene_vec2_get_y (t);
_sincosf (alpha, &s, &c);
graphene_vec2_init (t2, c*x + s*y, c*y - s * x);
}
/* Set p to the intersection of the lines a + t * ab * and
* c + s * cd. Return 1 if the lines intersect, 0 otherwise.
*/
static int
line_intersect (const graphene_point_t *a,
const graphene_vec2_t *ab,
const graphene_point_t *c,
const graphene_vec2_t *cd,
graphene_point_t *p)
{
float a1 = graphene_vec2_get_y (ab);
float b1 = - graphene_vec2_get_x (ab);
float c1 = a1 * a->x + b1 * a->y;
float a2 = graphene_vec2_get_y (cd);
float b2 = - graphene_vec2_get_x (cd);
float c2 = a2 * c->x + b2 * c->y;
float det = a1 * b2 - a2 * b1;
if (fabs (det) <= 0.001)
return 0;
p->x = (b2 * c1 - b1 * c2) / det;
p->y = (a1 * c2 - a2 * c1) / det;
return 1;
}
static float
line_point_dist (const graphene_point_t *a,
const graphene_vec2_t *n,
const graphene_point_t *q)
{
graphene_vec2_t t;
graphene_vec2_init (&t, q->x - a->x, q->y - a->y);
return graphene_vec2_dot (n, &t);
}
static void
line_point (const graphene_point_t *a,
const graphene_vec2_t *n,
float t,
graphene_point_t *q)
{
q->x = a->x + t * graphene_vec2_get_x (n);
q->y = a->y + t * graphene_vec2_get_y (n);
}
static void
rotate_point (const graphene_point_t *z,
const graphene_point_t *p,
float angle,
graphene_point_t *q)
{
float x, y;
float s, c;
x = p->x - z->x;
y = p->y - z->y;
_sincosf (angle, &s, &c);
graphene_point_init (q, z->x + x*c + y*s, z->y - x*s + y*c);
}
static int
circle_intersect (const graphene_point_t *c0,
float r0,
const graphene_point_t *c1,
float r1,
graphene_point_t p[2])
{
float r, R;
float cx, cy, Cx, Cy;
float dx, dy;
float d;
float x, y;
float D;
float angle;
graphene_point_t p0, C;
if (r0 < r1)
{
r = r0; R = r1;
cx = c0->x; cy = c0->y;
Cx = c1->x; Cy = c1->y;
}
else
{
r = r1; R = r0;
Cx = c0->x; Cy = c0->y;
cx = c1->x; cy = c1->y;
}
dx = cx - Cx;
dy = cy - Cy;
d = sqrt (dx*dx + dy*dy);
if (d < 0.0001)
return 0;
x = (dx / d) * R + Cx;
y = (dy / d) * R + Cy;
if (fabs (R + r - d) < 0.0001 ||
fabs (R - (r + d)) < 0.0001)
{
graphene_point_init (&p[0], x, y);
return 1;
}
if (d + r < R || R + r < d)
return 0;
D = (r*r - d*d - R*R) / (-2 * d * R);
if (D >= 1)
angle = 0;
else if (D <= -1)
angle = M_PI;
else
angle = acos (D);
graphene_point_init (&p0, x, y);
graphene_point_init (&C, Cx, Cy);
rotate_point (&C, &p0, angle, &p[0]);
rotate_point (&C, &p0, -angle, &p[1]);
return 2;
}
static int
circle_line_intersect (const graphene_point_t *c,
float r,
const graphene_point_t *a,
const graphene_vec2_t *t,
graphene_point_t p[2])
{
float x, y;
float dx, dy;
float dr2;
float D;
float dis;
x = a->x - c->x;
y = a->y - c->y;
dx = graphene_vec2_get_x (t);
dy = graphene_vec2_get_y (t);
dr2 = dx * dx + dy * dy;
D = x * (y + dy) - (x + dx) * y;
dis = r*r*dr2 - D*D;
if (dis < 0)
return 0;
else if (dis == 0)
{
p[0].x = c->x + D*dy / dr2;
p[0].y = c->y - D*dx / dr2;
return 1;
}
else
{
int sdy = dy < 0 ? -1 : 1;
p[0].x = c->x + (D*dy + sdy*dx*sqrt (dis)) / dr2;
p[0].y = c->y + (-D*dx + fabs (dy)*sqrt (dis)) / dr2;
p[1].x = c->x + (D*dy - sdy*dx*sqrt (dis)) / dr2;
p[1].y = c->y + (-D*dx - fabs (dy)*sqrt (dis)) / dr2;
return 2;
}
}
static void
find_closest_point (const graphene_point_t *p,
int n,
const graphene_point_t *c,
graphene_point_t *d)
{
float dist;
*d = p[0];
dist = graphene_point_distance (&p[0], c, NULL, NULL);
for (int i = 1; i < n; i++)
{
float dist2 = graphene_point_distance (&p[i], c, NULL, NULL);
if (dist2 < dist)
{
dist = dist2;
*d = p[i];
}
}
}
/* Find the circle through p0 and p1 with tangent t
* at p1, and set c to its center.
*/
static int
find_tangent_circle (const graphene_point_t *p0,
const graphene_point_t *p1,
const graphene_vec2_t *t,
graphene_point_t *c)
{
graphene_vec2_t n1, n2;
graphene_point_t p2;
graphene_vec2_init (&n1, - graphene_vec2_get_y (t), graphene_vec2_get_x (t));
get_normal (p0, p1, &n2);
p2.x = (p0->x + p1->x) / 2;
p2.y = (p0->y + p1->y) / 2;
return line_intersect (p1, &n1, &p2, &n2, c);
}
/* }}} */
/* {{{ GskPathBuilder utilities */
static void
path_builder_move_to_point (GskPathBuilder *builder,
const graphene_point_t *point)
{
gsk_path_builder_move_to (builder, point->x, point->y);
}
static void
path_builder_line_to_point (GskPathBuilder *builder,
const graphene_point_t *point)
{
gsk_path_builder_line_to (builder, point->x, point->y);
}
/* Assumes that the current point of the builder is
* the start point of the curve
*/
static void
path_builder_add_curve (GskPathBuilder *builder,
const GskCurve *curve)
{
const graphene_point_t *p;
switch (curve->op)
{
case GSK_PATH_LINE:
p = curve->line.points;
gsk_path_builder_line_to (builder, p[1].x, p[1].y);
break;
case GSK_PATH_QUAD:
p = curve->quad.points;
gsk_path_builder_quad_to (builder, p[1].x, p[1].y,
p[2].x, p[2].y);
break;
case GSK_PATH_CUBIC:
p = curve->cubic.points;
gsk_path_builder_cubic_to (builder, p[1].x, p[1].y,
p[2].x, p[2].y,
p[3].x, p[3].y);
break;
case GSK_PATH_CONIC:
p = curve->conic.points;
gsk_path_builder_conic_to (builder, p[1].x, p[1].y,
p[3].x, p[3].y,
p[2].x);
break;
case GSK_PATH_MOVE:
case GSK_PATH_CLOSE:
default:
g_assert_not_reached ();
}
}
static void
path_builder_add_curves (GskPathBuilder *builder,
GArray *curves)
{
for (unsigned int i = 0; i < curves->len; i++)
{
GskCurve *c = &g_array_index (curves, GskCurve, i);
path_builder_add_curve (builder, c);
}
}
static gboolean
add_reverse_op (GskPathOperation op,
const graphene_point_t *pts,
gsize n_pts,
float weight,
gpointer user_data)
{
GskCurve c;
GskCurve *curve;
GList **ops = user_data;
if (op == GSK_PATH_MOVE)
return TRUE;
gsk_curve_init_foreach (&c, op, pts, n_pts, weight);
curve = g_new0 (GskCurve, 1);
gsk_curve_reverse (&c, curve);
*ops = g_list_prepend (*ops, curve);
return TRUE;
}
static void
path_builder_add_reverse_path (GskPathBuilder *builder,
GskPath *path)
{
GList *l, *ops;
ops = NULL;
gsk_path_foreach (path, -1, add_reverse_op, &ops);
for (l = ops; l; l = l->next)
{
GskCurve *curve = l->data;
path_builder_add_curve (builder, curve);
}
g_list_free_full (ops, g_free);
}
static gboolean
add_op (GskPathOperation op,
const graphene_point_t *pts,
gsize n_pts,
float weight,
gpointer user_data)
{
GskCurve c;
GskPathBuilder *builder = user_data;
if (op == GSK_PATH_MOVE)
return TRUE;
gsk_curve_init_foreach (&c, op, pts, n_pts, weight);
path_builder_add_curve (builder, &c);
return TRUE;
}
static void
path_builder_append_path (GskPathBuilder *builder,
GskPath *path)
{
gsk_path_foreach (path, -1, add_op, builder);
}
/* Draw a circular arc from the current point to e,
* around c
*/
static gboolean
path_builder_arc_to (GskPathBuilder *builder,
const graphene_point_t *c,
const graphene_point_t *e)
{
const graphene_point_t *s;
graphene_vec2_t ts, te;
graphene_point_t p;
float a, w;
s = gsk_path_builder_get_current_point (builder);
get_normal (c, s, &ts);
get_normal (c, e, &te);
if (!line_intersect (s, &ts, e, &te, &p))
return FALSE;
a = angle_between_points (&p, s, e);
w = fabs (sin (a / 2));
gsk_path_builder_conic_to (builder, p.x, p.y, e->x, e->y, w);
return TRUE;
}
/* }}} */
/* {{{ GskCurve utilities */
static gboolean
curve_is_ignorable (const GskCurve *curve)
{
if (curve->op == GSK_PATH_CUBIC)
{
const graphene_point_t *pts = curve->cubic.points;
if (graphene_point_near (&pts[0], &pts[1], 0.001) &&
graphene_point_near (&pts[1], &pts[2], 0.001) &&
graphene_point_near (&pts[2], &pts[3], 0.001))
return TRUE;
}
else if (curve->op == GSK_PATH_CONIC)
{
const graphene_point_t *pts = curve->conic.points;
if (graphene_point_near (&pts[0], &pts[1], 0.001) &&
graphene_point_near (&pts[1], &pts[3], 0.001))
return TRUE;
}
else if (curve->op == GSK_PATH_QUAD)
{
const graphene_point_t *pts = curve->quad.points;
if (graphene_point_near (&pts[0], &pts[1], 0.001) &&
graphene_point_near (&pts[1], &pts[2], 0.001))
return TRUE;
}
else if (curve->op == GSK_PATH_LINE)
{
const graphene_point_t *pts = curve->line.points;
if (graphene_point_near (&pts[0], &pts[1], 0.001))
return TRUE;
}
return FALSE;
}
static gboolean
cubic_is_simple (const GskCurve *curve)
{
const graphene_point_t *pts = curve->cubic.points;
float a1, a2, s;
graphene_vec2_t t1, t2, t3;
if (!graphene_point_near (&pts[0], &pts[1], 0.001) &&
!graphene_point_near (&pts[1], &pts[2], 0.001) &&
!graphene_point_near (&pts[2], &pts[3], 0.001))
{
get_tangent (&pts[0], &pts[1], &t1);
get_tangent (&pts[1], &pts[2], &t2);
get_tangent (&pts[2], &pts[3], &t3);
a1 = angle_between (&t1, &t2);
a2 = angle_between (&t2, &t3);
if ((a1 < 0 && a2 > 0) || (a1 > 0 && a2 < 0))
return FALSE;
}
gsk_curve_get_start_tangent (curve, &t1);
gsk_curve_get_end_tangent (curve, &t2);
s = graphene_vec2_dot (&t1, &t2);
if (fabs (acos (s)) >= M_PI / 3.f)
return FALSE;
return TRUE;
}
static gboolean
conic_is_simple (const GskCurve *curve)
{
const graphene_point_t *pts = curve->conic.points;
graphene_vec2_t n1, n2;
float s;
get_normal (&pts[0], &pts[1], &n1);
get_normal (&pts[1], &pts[3], &n2);
s = graphene_vec2_dot (&n1, &n2);
if (fabs (acos (s)) >= M_PI / 3.f)
return FALSE;
return TRUE;
}
static gboolean
conic_is_degenerate (const GskCurve *curve)
{
if (curve->op == GSK_PATH_CONIC)
{
const graphene_point_t *pts = curve->conic.points;
float a;
graphene_vec2_t t1, t2;
get_tangent (&pts[0], &pts[1], &t1);
get_tangent (&pts[1], &pts[3], &t2);
a = angle_between (&t1, &t2);
if (a < 0)
a += M_PI;
if (fabs (a) < DEG_TO_RAD (3))
return TRUE;
}
return FALSE;
}
typedef void (* AddCurveCallback) (const GskCurve *curve,
gpointer data);
static void
split_curve_by_cusps (const GskCurve *curve,
AddCurveCallback callback,
gpointer data)
{
float t[5], s;
int n;
if ((n = gsk_curve_get_cusps (curve, t)) > 0)
{
t[n++] = 0;
t[n++] = 1;
}
if (n <= 2)
callback (curve, data);
else
{
qsort (t, n, sizeof (float), cmpfloat);
s = 0;
for (int i = 1; i < n; i++)
{
GskCurve c;
if (fabs (t[i] - s) > 0.1 || i == n - 1)
{
gsk_curve_segment (curve, s, t[i], &c);
callback (&c, data);
s = t[i];
}
}
}
}
/* Given two curves that share an end point,
* find the intersection that is closest to
* their shared endpoint, if any, and return
* the distance. If there is no intersection,
* return G_MAXFLOAT.
*/
static float
find_closest_intersection (const GskCurve *c1,
const GskCurve *c2,
graphene_point_t *p_out)
{
graphene_point_t p[9];
float t1[9], t2[9];
int n;
n = gsk_curve_intersect (c1, c2, t1, t2, p, 9);
if (n > 1)
{
const graphene_point_t *end = gsk_curve_get_end_point (c1);
float min = G_MAXFLOAT;
int best = -1;
for (unsigned int i = 0; i < n; i++)
{
float d;
if (fabs (t2[i]) < 0.001)
continue;
d = graphene_point_distance (end, &p[i], NULL, NULL);
if (d < min)
{
best = i;
min = d;
}
}
if (best == -1)
return G_MAXFLOAT;
if (p_out)
*p_out = p[best];
return min;
}
return G_MAXFLOAT;
}
/* }}} */
/* {{{ Stroke helpers */
static void
add_line_join (GskPathBuilder *builder,
GskLineJoin line_join,
float width,
float miter_limit,
const graphene_point_t *c,
const GskCurve *aa,
const GskCurve *bb,
float angle)
{
const graphene_point_t *a;
const graphene_point_t *b;
graphene_vec2_t ta;
graphene_vec2_t tb;
float ml, w;
ml = MAX (miter_limit, 1);
w = width;
a = gsk_curve_get_end_point (aa);
gsk_curve_get_end_tangent (aa, &ta);
b = gsk_curve_get_start_point (bb);
gsk_curve_get_start_tangent (bb, &tb);
again:
switch (line_join)
{
case GSK_LINE_JOIN_ARCS:
{
float ka, kb;
graphene_point_t ca, cb;
graphene_point_t p[2];
int n;
if (line_intersect (a, &ta, b, &tb, p) == 0)
{
line_point (a, &ta, ml * w, &p[0]);
line_point (b, &tb, - ml * w, &p[1]);
path_builder_line_to_point (builder, &p[0]);
path_builder_line_to_point (builder, &p[1]);
path_builder_line_to_point (builder, b);
break;
}
ka = gsk_curve_get_curvature (aa, 1, &ca);
kb = gsk_curve_get_curvature (bb, 0, &cb);
if (ka == 0 && kb == 0)
{
line_join = GSK_LINE_JOIN_MITER_CLIP;
goto again;
}
if (ka > 1 / width || kb > 1 / width)
{
line_join = GSK_LINE_JOIN_ROUND;
goto again;
}
if (ka != 0 && kb != 0)
{
float ra, rb;
graphene_point_t p0;
ra = fabs (1/ka);
rb = fabs (1/kb);
n = circle_intersect (&ca, ra, &cb, rb, p);
if (n == 0)
{
graphene_vec2_t ac, bc;
float d;
float rr0, rr1, rr;
int m;
graphene_point_t cca, ccb;
float rra, rrb;
d = graphene_point_distance (&ca, &cb, NULL, NULL);
if (d > ra + rb)
{
gboolean big_enough;
/* disjoint circles */
get_tangent (a, &ca, &ac);
get_tangent (b, &cb, &bc);
rr0 = 0;
rr1 = d;
big_enough = FALSE;
while (fabs (rr1 - rr0) > 0.001)
{
rr = (rr0 + rr1) / 2;
rra = ra + rr;
rrb = rb + rr;
line_point (a, &ac, rra, &cca);
line_point (b, &bc, rrb, &ccb);
m = circle_intersect (&cca, rra, &ccb, rrb, p);
if (m == 1)
{
rr1 = rr;
break;
}
if (m == 2)
{
rr1 = rr;
big_enough = TRUE;
}
else
{
if (big_enough)
rr0 = rr;
else
rr1 += d;
}
}
ra += rr1;
rb += rr1;
line_point (a, &ac, ra, &ca);
line_point (b, &bc, rb, &cb);
n = circle_intersect (&ca, ra, &cb, rb, p);
}
else
{
/* contained circles */
get_tangent (a, &ca, &ac);
get_tangent (b, &cb, &bc);
rr0 = 0;
rr1 = rb - ra;
while (fabs (rr1 - rr0) > 0.001)
{
rr = (rr0 + rr1) / 2;
rra = ra + rr;
rrb = rb - rr;
line_point (a, &ac, rra, &cca);
line_point (b, &bc, rrb, &ccb);
m = circle_intersect (&cca, rra, &ccb, rrb, p);
if (m == 1)
{
rr1 = rr;
break;
}
if (m == 2)
rr1 = rr;
else
rr0 = rr;
}
ra += rr1;
rb -= rr1;
line_point (a, &ac, ra, &ca);
line_point (b, &bc, rb, &cb);
n = circle_intersect (&ca, ra, &cb, rb, p);
}
}
if (n > 0)
{
/* We have an intersection; connect things */
graphene_vec2_t n0;
graphene_point_t cm;
float rm;
graphene_vec2_t t;
float alpha;
graphene_point_t pa, pb;
find_closest_point (p, n, c, &p0);
get_normal (&ca, &cb, &n0);
if (find_tangent_circle (c, &p0, &n0, &cm))
{
rm = graphene_point_distance (c, &cm, NULL, NULL);
alpha = angle_between_points (&cm, c, &p0);
/* FIXME: currently, we don't let miter_limit go below 1,
* to avoid dealing with bevel overlap
*/
if (fabs (alpha * rm) > ml * w)
{
get_tangent (&cm, c, &t);
rotate_vector (&t, ml * w / rm, &t);
n = circle_line_intersect (&ca, ra, &cm, &t, p);
find_closest_point (p, n, &p0, &pa);
n = circle_line_intersect (&cb, rb, &cm, &t, p);
find_closest_point (p, n, &p0, &pb);
path_builder_arc_to (builder, &ca, &pa);
path_builder_line_to_point (builder, &pb);
path_builder_arc_to (builder, &cb, b);
}
else
{
path_builder_arc_to (builder, &ca, &p0);
path_builder_arc_to (builder, &cb, b);
}
}
else
{
if (graphene_point_distance (c, &p0, NULL, NULL) > ml * w)
{
get_tangent (c, &p0, &t);
line_point (c, &t, ml * w, &cm);
graphene_vec2_init (&t, - graphene_vec2_get_y (&t), graphene_vec2_get_x (&t));
n = circle_line_intersect (&ca, ra, &cm, &t, p);
find_closest_point (p, n, &p0, &pa);
n = circle_line_intersect (&cb, rb, &cm, &t, p);
find_closest_point (p, n, &p0, &pb);
path_builder_arc_to (builder, &ca, &pa);
path_builder_line_to_point (builder, &pb);
path_builder_arc_to (builder, &cb, b);
}
else
{
path_builder_arc_to (builder, &ca, &p0);
path_builder_arc_to (builder, &cb, b);
}
}
}
}
else if (ka != 0 && kb == 0)
{
float ra = fabs (1/ka);
graphene_vec2_t ac;
float rr0, rr1, rr, rra;
graphene_point_t cca;
int m;
n = circle_line_intersect (&ca, ra, b, &tb, p);
if (n == 0)
{
gboolean big_enough;
get_tangent (a, &ca, &ac);
rr = 0;
rr0 = 0;
rr1 = ra;
big_enough = FALSE;
while (fabs (rr1 - rr0) > 0.001)
{
rr = (rr0 + rr1) / 2;
rra = ra + rr;
line_point (a, &ac, rra, &cca);
m = circle_line_intersect (&cca, rra, b, &tb, p);
if (m == 1)
break;
if (m == 2)
{
big_enough = TRUE;
rr1 = rr;
}
else
{
if (big_enough)
rr0 = rr;
else
rr1 += ra;
}
}
ra += rr1;
line_point (a, &ac, ra, &ca);
n = circle_line_intersect (&ca, ra, b, &tb, p);
}
if (n > 0)
{
graphene_point_t p0, cm;
graphene_vec2_t n0;
float rm;
float alpha;
find_closest_point (p, n, c, &p0);
if (n == 1)
graphene_vec2_init_from_vec2 (&n0, &tb);
else
get_tangent (&p[0], &p[1], &n0);
if (find_tangent_circle (c, &p0, &n0, &cm))
{
rm = graphene_point_distance (c, &cm, NULL, NULL);
alpha = angle_between_points (&cm, c, &p0);
if (fabs (alpha * rm) > ml * w)
{
graphene_vec2_t t;
graphene_point_t pa, pb;
get_tangent (&cm, c, &t);
rotate_vector (&t, ml * w / rm, &t);
n = circle_line_intersect (&ca, ra, &cm, &t, p);
find_closest_point (p, n, &p0, &pa);
n = line_intersect (b, &tb, &cm, &t, p);
find_closest_point (p, n, &p0, &pb);
path_builder_arc_to (builder, &ca, &pa);
path_builder_line_to_point (builder, &pb);
path_builder_line_to_point (builder, b);
}
else
{
path_builder_arc_to (builder, &ca, &p0);
path_builder_line_to_point (builder, b);
}
}
else
{
if (graphene_point_distance (c, &p0, NULL, NULL) > ml * w)
{
graphene_vec2_t t;
graphene_point_t pa, pb;
get_tangent (c, &p0, &t);
line_point (c, &t, ml * w, &cm);
graphene_vec2_init (&t, - graphene_vec2_get_y (&t), graphene_vec2_get_x (&t));
n = circle_line_intersect (&ca, ra, &cm, &t, p);
find_closest_point (p, n, &p0, &pa);
n = line_intersect (b, &tb, &cm, &t, p);
find_closest_point (p, n, &p0, &pb);
path_builder_arc_to (builder, &ca, &pa);
path_builder_line_to_point (builder, &pb);
path_builder_line_to_point (builder, b);
}
else
{
path_builder_arc_to (builder, &ca, &p0);
path_builder_line_to_point (builder, b);
}
}
}
}
else if (ka == 0 && kb != 0)
{
float rb = fabs (1/kb);
graphene_vec2_t bc;
float rr0, rr1, rr, rrb;
graphene_point_t ccb;
int m;
n = circle_line_intersect (&cb, rb, a, &ta, p);
if (n == 0)
{
gboolean big_enough;
get_tangent (b, &cb, &bc);
rr = 0;
rr0 = 0;
rr1 = rb;
big_enough = FALSE;
while (fabs (rr1 - rr0) > 0.001)
{
rr = (rr0 + rr1) / 2;
rrb = rb + rr;
line_point (b, &bc, rrb, &ccb);
m = circle_line_intersect (&ccb, rrb, a, &ta, p);
if (m == 1)
break;
if (m == 2)
{
big_enough = TRUE;
rr1 = rr;
}
else
{
if (big_enough)
rr0 = rr;
else
rr1 += rb;
}
}
rb += rr1;
line_point (b, &bc, rb, &cb);
n = circle_line_intersect (&cb, rb, a, &ta, p);
}
if (n > 0)
{
graphene_point_t p0, cm;
graphene_vec2_t n0;
float rm;
float alpha;
find_closest_point (p, n, c, &p0);
if (n == 1)
graphene_vec2_init_from_vec2 (&n0, &tb);
else
get_tangent (&p[0], &p[1], &n0);
if (find_tangent_circle (c, &p0, &n0, &cm))
{
rm = graphene_point_distance (c, &cm, NULL, NULL);
alpha = angle_between_points (&cm, c, &p0);
if (fabs (alpha * rm) > ml * w)
{
graphene_vec2_t t;
graphene_point_t pa, pb;
get_tangent (&cm, c, &t);
rotate_vector (&t, ml * w / rm, &t);
n = line_intersect (a, &ta, &cm, &t, p);
find_closest_point (p, n, &p0, &pa);
n = circle_line_intersect (&cb, rb, &cm, &t, p);
find_closest_point (p, n, &p0, &pb);
path_builder_line_to_point (builder, &pa);
path_builder_line_to_point (builder, &pb);
path_builder_arc_to (builder, &cb, b);
}
else
{
path_builder_line_to_point (builder, &p0);
path_builder_arc_to (builder, &cb, b);
}
}
else
{
if (graphene_point_distance (c, &p0, NULL, NULL) > ml * w)
{
graphene_vec2_t t;
graphene_point_t pa, pb;
get_tangent (c, &p0, &t);
line_point (c, &t, ml * w, &cm);
graphene_vec2_init (&t, - graphene_vec2_get_y (&t), graphene_vec2_get_x (&t));
n = line_intersect (a, &ta, &cm, &t, p);
find_closest_point (p, n, &p0, &pa);
n = circle_line_intersect (&cb, rb, &cm, &t, p);
find_closest_point (p, n, &p0, &pb);
path_builder_line_to_point (builder, &pa);
path_builder_line_to_point (builder, &pb);
path_builder_arc_to (builder, &cb, b);
}
else
{
path_builder_line_to_point (builder, &p0);
path_builder_arc_to (builder, &cb, b);
}
}
}
}
}
break;
case GSK_LINE_JOIN_MITER:
case GSK_LINE_JOIN_MITER_CLIP:
{
graphene_point_t p;
if (line_intersect (a, &ta, b, &tb, &p))
{
graphene_vec2_t tam;
graphene_vec2_negate (&ta, &tam);
/* Check that 1 / sin (psi / 2) <= miter_limit,
* where psi is the angle between ta and tb
*/
if (2 <= miter_limit * miter_limit * (1 - graphene_vec2_dot (&tam, &tb)))
{
path_builder_line_to_point (builder, &p);
path_builder_line_to_point (builder, b);
}
else if (line_join == GSK_LINE_JOIN_MITER_CLIP)
{
graphene_point_t q, a1, b1;
graphene_vec2_t n;
q.x = (c->x + p.x) / 2;
q.y = (c->y + p.y) / 2;
get_normal (c, &p, &n);
line_intersect (a, &ta, &q, &n, &a1);
line_intersect (b, &tb, &q, &n, &b1);
if ((line_point_dist (&q, &n, c) < 0) !=
(line_point_dist (&q, &n, a) < 0))
{
path_builder_line_to_point (builder, &a1);
path_builder_line_to_point (builder, &b1);
path_builder_line_to_point (builder, b);
}
else
{
path_builder_line_to_point (builder, b);
}
}
else
{
path_builder_line_to_point (builder, b);
}
}
else
{
path_builder_line_to_point (builder, b);
}
}
break;
case GSK_LINE_JOIN_ROUND:
gsk_path_builder_svg_arc_to (builder,
w, w,
0, 0, angle > 0 ? 1 : 0,
b->x, b->y);
break;
case GSK_LINE_JOIN_BEVEL:
path_builder_line_to_point (builder, b);
break;
default:
g_assert_not_reached ();
}
}
static void
add_line_cap (GskPathBuilder *builder,
GskLineCap line_cap,
float line_width,
const graphene_point_t *s,
const graphene_point_t *e)
{
switch (line_cap)
{
case GSK_LINE_CAP_BUTT:
path_builder_line_to_point (builder, e);
break;
case GSK_LINE_CAP_ROUND:
gsk_path_builder_svg_arc_to (builder,
line_width / 2, line_width / 2,
0, 1, 0,
e->x, e->y);
break;
case GSK_LINE_CAP_SQUARE:
{
float cx = (s->x + e->x) / 2;
float cy = (s->y + e->y) / 2;
float dx = s->y - cy;
float dy = - s->x + cx;
gsk_path_builder_line_to (builder, s->x + dx, s->y + dy);
gsk_path_builder_line_to (builder, e->x + dx, e->y + dy);
path_builder_line_to_point (builder, e);
}
break;
default:
g_assert_not_reached ();
break;
}
}
/* }}} */
/* {{{ Segments */
/* A Segment contains a curve from the path we're operating on,
* plus its subdivision at a possible offset.
*/
typedef struct
{
GskCurve curve;
GArray *c;
float offset;
} Segment;
static void
segment_init (Segment *segment,
const GskCurve *curve)
{
memset (segment, 0, sizeof (Segment));
if (curve)
segment->curve = *curve;
segment->c = g_array_new (FALSE, TRUE, sizeof (GskCurve));
segment->offset = 0;
}
static void
segment_clear (Segment *segment)
{
g_array_unref (segment->c);
}
static void
segment_copy (Segment *src,
Segment *dest)
{
dest->curve = src->curve;
dest->offset = src->offset;
if (dest->c == NULL)
dest->c = g_array_new (FALSE, TRUE, sizeof (GskCurve));
else
g_array_set_size (dest->c, 0);
g_array_append_vals (dest->c, src->c->data, src->c->len);
}
static inline unsigned int
segment_get_n_curves (Segment *segment)
{
return segment->c->len;
}
static inline const GskCurve *
segment_get_curve (Segment *segment,
unsigned int pos)
{
return &g_array_index (segment->c, const GskCurve, pos);
}
static inline const GskCurve *
segment_get_first_curve (Segment *segment)
{
return segment_get_curve (segment, 0);
}
static inline const GskCurve *
segment_get_last_curve (Segment *segment)
{
return segment_get_curve (segment, segment->c->len - 1);
}
static void
segment_offset (Segment *segment,
float offset)
{
for (unsigned int i = 0; i < segment->c->len; i++)
{
GskCurve *c = &g_array_index (segment->c, GskCurve, i);
gsk_curve_offset (c, offset, c);
}
segment->offset += offset;
}
/* {{{ Subdivision */
#define MAX_SUBDIVISION 3
static inline void
add_curve (const GskCurve *curve,
Segment *segment)
{
if (curve_is_ignorable (curve))
return;
g_array_append_val (segment->c, *curve);
}
static void
curve_subdivide (const GskCurve *curve,
unsigned int level,
Segment *segment)
{
GskCurve c1, c2;
if (level == 0)
{
add_curve (curve, segment);
return;
}
if (level < MAX_SUBDIVISION && cubic_is_simple (curve))
{
add_curve (curve, segment);
return;
}
if (level == MAX_SUBDIVISION)
{
float t[5] = { 0, 1, };
int n = 2;
n += gsk_curve_get_curvature_points (curve, t + n);
if (n > 2)
{
float s;
qsort (t, n, sizeof (float), cmpfloat);
s = 0;
for (int i = 1; i < n; i++)
{
GskCurve c;
if (fabs (t[i] - s) > 0.1 || i == n - 1)
{
gsk_curve_segment (curve, s, t[i], &c);
curve_subdivide (&c, level - 1, segment);
s = t[i];
}
}
return;
}
}
gsk_curve_split (curve, 0.5, &c1, &c2);
curve_subdivide (&c1, level - 1, segment);
curve_subdivide (&c2, level - 1, segment);
}
static void
add_degenerate_conic (const GskCurve *curve,
Segment *segment)
{
graphene_point_t p[3];
GskCurve c;
p[0] = *gsk_curve_get_start_point (curve);
gsk_curve_get_point (curve, 0.5, &p[1]);
p[2] = *gsk_curve_get_end_point (curve);
gsk_curve_init_foreach (&c, GSK_PATH_LINE, p, 2, 0);
add_curve (&c, segment);
gsk_curve_init_foreach (&c, GSK_PATH_LINE, p + 1, 2, 0);
add_curve (&c, segment);
}
static void
conic_subdivide (const GskCurve *curve,
int level,
Segment *segment)
{
if (level == 0)
{
if (conic_is_degenerate (curve))
add_degenerate_conic (curve, segment);
else
add_curve (curve, segment);
}
else if (level < MAX_SUBDIVISION && conic_is_simple (curve))
{
add_curve (curve, segment);
}
else
{
GskCurve c1, c2;
gsk_curve_split (curve, 0.5, &c1, &c2);
conic_subdivide (&c1, level - 1, segment);
conic_subdivide (&c2, level - 1, segment);
}
}
static void
segment_subdivide (Segment *segment)
{
const GskCurve *curve = &segment->curve;
g_assert (segment->c->len == 0);
switch (curve->op)
{
case GSK_PATH_LINE:
add_curve (curve, segment);
break;
case GSK_PATH_QUAD:
case GSK_PATH_CUBIC:
curve_subdivide (curve, MAX_SUBDIVISION, segment);
break;
case GSK_PATH_CONIC:
conic_subdivide (curve, MAX_SUBDIVISION, segment);
break;
case GSK_PATH_MOVE:
case GSK_PATH_CLOSE:
default:
g_assert_not_reached ();
}
if (segment->c->len == 0)
{
graphene_point_t p[2];
GskCurve c;
g_warning ("This should not happen. Expect bad strokes");
p[0] = *gsk_curve_get_start_point (curve);
p[1] = *gsk_curve_get_end_point (curve);
gsk_curve_init_foreach (&c, GSK_PATH_LINE, p, 2, 0);
g_array_append_val (segment->c, *curve);
}
}
/* }}} */
/* {{{ Loop elision */
static gboolean
has_nontrivial_intersection (float *t1, float *t2, int n)
{
for (int i = 0; i < n; i++)
{
if (fabs (t1[i]) > 0.01 && fabs (t1[i] - 1.) > 0.01 &&
fabs (t2[i]) > 0.01 && fabs (t2[i] - 1.) > 0.01)
return TRUE;
}
return FALSE;
}
static int
segments_intersect (GArray *curves1,
GArray *curves2,
gboolean no_self,
int *i_out,
int *j_out,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
for (int i = curves1->len - 1; i >= 0; i--)
{
GskCurve *c1 = &g_array_index (curves1, GskCurve, i);
for (int j = 0; j < curves2->len; j++)
{
GskCurve *c2 = &g_array_index (curves2, GskCurve, j);
if (no_self && i == j)
break;
int count = gsk_curve_intersect (c1, c2, t1, t2, p, n);
if (count > 0)
{
if (no_self && i == j + 1 && !has_nontrivial_intersection (t1, t2, n))
continue;
*i_out = i;
*j_out = j;
return count;
}
}
}
return 0;
}
static gboolean
cut_loop (GArray *curves)
{
int i, j;
float t1, t2;
graphene_point_t p;
if (segments_intersect (curves, curves, TRUE, &j, &i, &t2, &t1, &p, 1) > 0)
{
GskCurve *r;
GskCurve c;
if (i + 1 < j)
g_array_remove_range (curves, i + 1, j - (i + 1));
else if (i + 1 == j)
{
/* nothing to do */
}
else if (i == j)
{
c = g_array_index (curves, GskCurve, i);
g_array_insert_val (curves, i + 1, c);
}
else
g_assert_not_reached ();
r = &g_array_index (curves, GskCurve, i);
gsk_curve_split (r, t1, &c, NULL);
*r = c;
r = &g_array_index (curves, GskCurve, i + 1);
gsk_curve_split (r, t2, NULL, &c);
*r = c;
return TRUE;
}
return FALSE;
}
static void
elide_self_intersections (Segment *segment)
{
if (segment->curve.op == GSK_PATH_LINE ||
segment->curve.op == GSK_PATH_QUAD)
return;
if (segment->curve.op == GSK_PATH_CUBIC)
{
GskCurve c;
float t1, t2;
graphene_point_t p;
/* If the original curve self-intersects, assume this
* is an intentional loop that we shouldn't cut.
*/
c = segment->curve;
if (gsk_curve_intersect (&c, &c, &t1, &t2, &p, 1) > 0)
{
return;
}
}
/* FIXME: For conics and non-inflecting cubics,
* it would be enough to check the 'inner' side.
*/
cut_loop (segment->c);
}
/* }}} */
/* }}} */
/* {{{ Stroke implementation */
/* The general theory of operation for the stroker:
*
* We walk the segments of the path, subdividing and offsetting
* each segment to the left and right, and collect the offset
* segments in a left and a right contour.
*
* When a segment is too curvy, or has cusps or inflections,
* we subdivide it before we add the pieces.
*
* Whenever we add a segment, we need to decide if the join
* is a smooth connection, a right turn, or a left turn. For
* sharp turns, we add a line join on the one side, and intersect
* the offset curves on the other.
*
* Since the intersection shortens both segments, we have to
* delay adding the previous segments to the outlines until
* we've handled the join at their end. We also need to hold
* off on adding the initial segment to the outlines until
* we've seen the end of the current contour of the path, to
* handle the join at before the initial segment for closed
* contours.
*
* If the contour turns out to not be closed when we reach
* the end, we collect the pending segments, reverse the
* left contour, and connect the right and left contour
* with end caps, closing the resulting outline.
*
* If the path isn't done after we've finished handling the
* outlines of the current contour, we start over with
* collecting offset segments of the next contour.
*
* We rely on the ability to offset, subdivide, intersect
* and reverse curves.
*/
typedef struct
{
GskPathBuilder *builder; // builder that collects the stroke
GskStroke *stroke; // stroke parameters
GskPathBuilder *left; // accumulates the left contour
GskPathBuilder *right; // accumulates the right contour
gboolean has_current_point; // seg0 has been set from a move
gboolean has_current_curve; // seg has been set from a curve
gboolean is_first_curve; // seg is the first segment we've seen
Segment l;
Segment r;
Segment l0;
Segment r0;
#ifdef STROKE_DEBUG
GskPathBuilder *debug;
#endif
} StrokeData;
static void
stroke_data_init (StrokeData *stroke_data,
GskPathBuilder *builder,
GskStroke *stroke)
{
memset (stroke_data, 0, sizeof (StrokeData));
stroke_data->builder = builder;
stroke_data->stroke = stroke;
segment_init (&stroke_data->l, NULL);
segment_init (&stroke_data->r, NULL);
segment_init (&stroke_data->l0, NULL);
segment_init (&stroke_data->r0, NULL);
}
static void
stroke_data_clear (StrokeData *stroke_data)
{
g_assert (stroke_data->left == NULL);
g_assert (stroke_data->right == NULL);
segment_clear (&stroke_data->l);
segment_clear (&stroke_data->r);
segment_clear (&stroke_data->l0);
segment_clear (&stroke_data->r0);
}
#ifdef STROKE_DEBUG
static void
add_debug_point (StrokeData *stroke_data,
const graphene_point_t *p,
int width)
{
gsk_path_builder_add_circle (stroke_data->debug, &p[0], width);
}
static void
add_debug (StrokeData *stroke_data,
const GskCurve *curve,
int width)
{
const graphene_point_t *p;
switch (curve->op)
{
case GSK_PATH_LINE:
case GSK_PATH_CLOSE:
p = curve->line.points;
add_debug_point (stroke_data, &p[0], width);
add_debug_point (stroke_data, &p[1], width);
break;
case GSK_PATH_QUAD:
p = curve->quad.points;
path_builder_move_to_point (stroke_data->debug, &p[0]);
path_builder_line_to_point (stroke_data->debug, &p[1]);
path_builder_line_to_point (stroke_data->debug, &p[2]);
add_debug_point (stroke_data, &p[0], width);
add_debug_point (stroke_data, &p[1], width / 2);
add_debug_point (stroke_data, &p[2], width);
break;
case GSK_PATH_CUBIC:
p = curve->cubic.points;
path_builder_move_to_point (stroke_data->debug, &p[0]);
path_builder_line_to_point (stroke_data->debug, &p[1]);
path_builder_line_to_point (stroke_data->debug, &p[2]);
path_builder_line_to_point (stroke_data->debug, &p[3]);
add_debug_point (stroke_data, &p[0], width);
add_debug_point (stroke_data, &p[1], width / 2);
add_debug_point (stroke_data, &p[2], width / 2);
add_debug_point (stroke_data, &p[3], width);
break;
case GSK_PATH_CONIC:
p = curve->conic.points;
path_builder_move_to_point (stroke_data->debug, &p[0]);
path_builder_line_to_point (stroke_data->debug, &p[1]);
path_builder_line_to_point (stroke_data->debug, &p[3]);
add_debug_point (stroke_data, &p[0], width);
add_debug_point (stroke_data, &p[1], width / 2);
add_debug_point (stroke_data, &p[3], width);
break;
case GSK_PATH_MOVE:
break;
default:
g_assert_not_reached ();
}
}
#endif
/* Append the prev segment to the right contour */
static void
append_right (StrokeData *stroke_data)
{
if (stroke_data->is_first_curve)
{
const GskCurve *r = segment_get_last_curve (&stroke_data->r);
path_builder_move_to_point (stroke_data->right, gsk_curve_get_end_point (r));
}
else
{
path_builder_add_curves (stroke_data->right, stroke_data->r.c);
}
}
/* Append the prev segment to the left contour */
static void
append_left (StrokeData *stroke_data)
{
if (stroke_data->is_first_curve)
{
const GskCurve *l = segment_get_last_curve (&stroke_data->l);
path_builder_move_to_point (stroke_data->left, gsk_curve_get_end_point (l));
}
else
{
path_builder_add_curves (stroke_data->left, stroke_data->l.c);
}
}
/* Add the previous segments, and the join between it and the new segment,
* and update stroke_data->seg to segment.
*
* If stroke_data->seg is the first segment of the contour, we don't
* add it yet, but save it in stroke_data->seg0 for later when we know if
* the contour is closed or not.
*/
static void
add_segment (StrokeData *stroke_data,
Segment *left,
Segment *right)
{
float angle;
graphene_vec2_t tangent1, tangent2;
GskLineJoin line_join;
const GskCurve *c1, *c2;
const GskCurve *r1, *r2;
const GskCurve *l1, *l2;
#ifdef STROKE_DEBUG
{
const char *s;
if ((s = g_getenv ("STROKE_DEBUG")) != NULL)
{
if (strstr (s, "o"))
add_debug (stroke_data, &left->curve, 3);
if (strstr (s, "l"))
for (unsigned int i = 0; i < segment_get_n_curves (left); i++)
add_debug (stroke_data, segment_get_curve (left, i), 2);
if (strstr (s, "r"))
for (unsigned int i = 0; i < segment_get_n_curves (right); i++)
add_debug (stroke_data, segment_get_curve (right, i), 2);
}
}
#endif
if (!stroke_data->has_current_curve)
{
r2 = segment_get_first_curve (right);
l2 = segment_get_first_curve (left);
path_builder_move_to_point (stroke_data->right, gsk_curve_get_start_point (r2));
path_builder_move_to_point (stroke_data->left, gsk_curve_get_start_point (l2));
segment_copy (left, &stroke_data->l0);
segment_copy (right, &stroke_data->r0);
segment_copy (left, &stroke_data->l);
segment_copy (right, &stroke_data->r);
stroke_data->has_current_curve = TRUE;
stroke_data->is_first_curve = TRUE;
return;
}
c1 = &stroke_data->l.curve;
r1 = segment_get_last_curve (&stroke_data->r);
l1 = segment_get_last_curve (&stroke_data->l);
c2 = &left->curve;
r2 = segment_get_first_curve (right);
l2 = segment_get_first_curve (left);
/* Look at the angle between the segments */
gsk_curve_get_end_tangent (c1, &tangent1);
gsk_curve_get_start_tangent (c2, &tangent2);
angle = angle_between (&tangent1, &tangent2);
if (fabs (angle) < DEG_TO_RAD (5))
line_join = GSK_LINE_JOIN_ROUND;
else
line_join = stroke_data->stroke->line_join;
if (fabs (angle) < DEG_TO_RAD (1))
{
/* Close enough to a straight line */
append_right (stroke_data);
append_left (stroke_data);
}
else
{
float maxdist;
if (fabs (M_PI - fabs (angle)) < DEG_TO_RAD (1))
{
/* For 180 turns, we look at the whole curves to
* decide if they are left or right turns
*/
get_tangent (gsk_curve_get_start_point (c1),
gsk_curve_get_end_point (c1),
&tangent1);
get_tangent (gsk_curve_get_start_point (c2),
gsk_curve_get_end_point (c2),
&tangent2);
angle = angle_between (&tangent1, &tangent2);
}
maxdist = find_closest_intersection (c1, c2, NULL);
if (angle > 0)
{
int i, j;
float t1, t2;
graphene_point_t p;
/* Right turn */
if (segments_intersect (stroke_data->r.c, right->c, FALSE, &i, &j, &t1, &t2, &p, 1) > 0 &&
graphene_point_distance (&p, gsk_curve_get_start_point (c2), NULL, NULL) < maxdist)
{
GskCurve c;
GskCurve *r;
g_array_set_size (stroke_data->r.c, i + 1);
r = &g_array_index (stroke_data->r.c, GskCurve, i);
gsk_curve_split (r, t1, &c, NULL);
g_array_index (stroke_data->r.c, GskCurve, i) = c;
g_array_remove_range (right->c, 0, j);
r = &g_array_index (right->c, GskCurve, 0);
gsk_curve_split (r, t2, NULL, &c);
g_array_index (right->c, GskCurve, 0) = c;
if (stroke_data->is_first_curve)
{
g_array_set_size (stroke_data->r0.c, 0);
g_array_append_vals (stroke_data->r0.c, stroke_data->r.c->data, stroke_data->r.c->len);
}
append_right (stroke_data);
}
else
{
append_right (stroke_data);
path_builder_line_to_point (stroke_data->right, gsk_curve_get_start_point (r2));
}
append_left (stroke_data);
add_line_join (stroke_data->left,
line_join,
stroke_data->stroke->line_width / 2,
stroke_data->stroke->miter_limit,
gsk_curve_get_start_point (c2),
l1,
l2,
angle);
}
else
{
int i, j;
float t1, t2;
graphene_point_t p;
/* Left turn */
append_right (stroke_data);
add_line_join (stroke_data->right,
line_join,
stroke_data->stroke->line_width / 2,
stroke_data->stroke->miter_limit,
gsk_curve_get_start_point (c2),
r1,
r2,
angle);
if (segments_intersect (stroke_data->l.c, left->c, FALSE, &i, &j, &t1, &t2, &p, 1) > 0 &&
graphene_point_distance (&p, gsk_curve_get_start_point (c2), NULL, NULL) < maxdist)
{
GskCurve c;
GskCurve *l;
g_array_set_size (stroke_data->l.c, i + 1);
l = &g_array_index (stroke_data->l.c, GskCurve, i);
gsk_curve_split (l, t1, &c, NULL);
g_array_index (stroke_data->l.c, GskCurve, i) = c;
g_array_remove_range (left->c, 0, j);
l = &g_array_index (left->c, GskCurve, 0);
gsk_curve_split (l, t2, NULL, &c);
g_array_index (left->c, GskCurve, 0) = c;
if (stroke_data->is_first_curve)
{
g_array_set_size (stroke_data->l0.c, 0);
g_array_append_vals (stroke_data->l0.c, stroke_data->l.c->data, stroke_data->l.c->len);
}
append_left (stroke_data);
}
else
{
append_left (stroke_data);
path_builder_line_to_point (stroke_data->left, gsk_curve_get_start_point (l2));
}
}
}
segment_copy (left, &stroke_data->l);
segment_copy (right, &stroke_data->r);
stroke_data->is_first_curve = FALSE;
}
/* Create a single closed contour and add it to
* stroke_data->builder, by connecting the right and the
* reversed left contour with caps.
*
* After this call, stroke_data->left and ->right are NULL.
*/
static void
cap_and_connect_contours (StrokeData *stroke_data)
{
GskPath *path;
const graphene_point_t *r0, *l0, *r1, *l1;
r1 = r0 = gsk_curve_get_start_point (segment_get_first_curve (&stroke_data->r0));
l1 = l0 = gsk_curve_get_start_point (segment_get_first_curve (&stroke_data->l0));
if (stroke_data->has_current_curve)
{
path_builder_add_curves (stroke_data->right, stroke_data->r.c);
path_builder_add_curves (stroke_data->left, stroke_data->l.c);
r1 = gsk_curve_get_end_point (segment_get_last_curve (&stroke_data->r));
l1 = gsk_curve_get_end_point (segment_get_last_curve (&stroke_data->l));
}
else
path_builder_move_to_point (stroke_data->right, r1);
add_line_cap (stroke_data->right,
stroke_data->stroke->line_cap,
stroke_data->stroke->line_width,
r1, l1);
if (stroke_data->has_current_curve)
{
path = gsk_path_builder_free_to_path (stroke_data->left);
path_builder_add_reverse_path (stroke_data->right, path);
gsk_path_unref (path);
if (!stroke_data->is_first_curve)
{
/* Add the first segment that wasn't added initially */
unsigned int n = segment_get_n_curves (&stroke_data->l0);
for (unsigned int i = 0; i < n; i++)
{
GskCurve r;
gsk_curve_reverse (segment_get_curve (&stroke_data->l0, n - 1 - i), &r);
path_builder_add_curve (stroke_data->right, &r);
}
}
}
else
gsk_path_builder_unref (stroke_data->left);
add_line_cap (stroke_data->right,
stroke_data->stroke->line_cap,
stroke_data->stroke->line_width,
l0, r0);
if (stroke_data->has_current_curve)
{
if (!stroke_data->is_first_curve)
{
/* Add the first segment that wasn't added initially */
unsigned int n = segment_get_n_curves (&stroke_data->r0);
for (unsigned int i = 0; i < n; i++)
{
path_builder_add_curve (stroke_data->right, segment_get_curve (&stroke_data->r0, i));
}
}
}
gsk_path_builder_close (stroke_data->right);
path = gsk_path_builder_free_to_path (stroke_data->right);
gsk_path_builder_add_path (stroke_data->builder, path);
gsk_path_unref (path);
stroke_data->left = NULL;
stroke_data->right = NULL;
}
/* Close the left and the right contours and add them to
* stroke_data->builder.
*
* After this call, stroke_data->left and ->right are NULL.
*/
static void
close_contours (StrokeData *stroke_data)
{
GskPath *path;
GskPathBuilder *builder;
if (stroke_data->has_current_curve)
{
/* add final join and first segment */
add_segment (stroke_data, &stroke_data->l0, &stroke_data->r0);
path_builder_add_curves (stroke_data->right, stroke_data->r.c);
path_builder_add_curves (stroke_data->left, stroke_data->l.c);
}
gsk_path_builder_close (stroke_data->right);
path = gsk_path_builder_free_to_path (stroke_data->right);
gsk_path_builder_add_path (stroke_data->builder, path);
gsk_path_unref (path);
builder = gsk_path_builder_new ();
path_builder_move_to_point (builder, gsk_path_builder_get_current_point (stroke_data->left));
path = gsk_path_builder_free_to_path (stroke_data->left);
path_builder_add_reverse_path (builder, path);
gsk_path_unref (path);
gsk_path_builder_close (builder);
path = gsk_path_builder_free_to_path (builder);
gsk_path_builder_add_path (stroke_data->builder, path);
gsk_path_unref (path);
stroke_data->left = NULL;
stroke_data->right = NULL;
}
static void
add_segment_for_curve (const GskCurve *curve,
StrokeData *stroke_data)
{
Segment left = { 0, };
Segment right = { 0, };
segment_init (&left, curve);
segment_subdivide (&left);
segment_copy (&left, &right);
segment_offset (&left, - stroke_data->stroke->line_width / 2);
segment_offset (&right, stroke_data->stroke->line_width / 2);
elide_self_intersections (&left);
elide_self_intersections (&right);
add_segment (stroke_data, &left, &right);
segment_clear (&left);
segment_clear (&right);
}
static gboolean
stroke_op (GskPathOperation op,
const graphene_point_t *pts,
gsize n_pts,
float weight,
gpointer user_data)
{
StrokeData *stroke_data = user_data;
GskCurve curve;
Segment left = { 0, };
Segment right = { 0, };
switch (op)
{
case GSK_PATH_MOVE:
if (stroke_data->has_current_point)
cap_and_connect_contours (stroke_data);
gsk_curve_init_foreach (&curve,
GSK_PATH_LINE,
(const graphene_point_t[]) { pts[0], GRAPHENE_POINT_INIT (pts[0].x + 1, pts[0].y) },
2, 0.f);
segment_init (&left, &curve);
segment_subdivide (&left);
segment_copy (&left, &right);
segment_offset (&left, - stroke_data->stroke->line_width / 2);
segment_offset (&right, stroke_data->stroke->line_width / 2);
segment_copy (&left, &stroke_data->l0);
segment_copy (&right, &stroke_data->r0);
segment_clear (&left);
segment_clear (&right);
stroke_data->right = gsk_path_builder_new ();
stroke_data->left = gsk_path_builder_new ();
stroke_data->has_current_point = TRUE;
stroke_data->has_current_curve = FALSE;
break;
case GSK_PATH_CLOSE:
if (stroke_data->has_current_point)
{
if (!graphene_point_near (&pts[0], &pts[1], 0.001))
{
gsk_curve_init_foreach (&curve, GSK_PATH_LINE, pts, n_pts, weight);
add_segment_for_curve (&curve, stroke_data);
}
close_contours (stroke_data);
}
stroke_data->has_current_point = FALSE;
stroke_data->has_current_curve = FALSE;
break;
case GSK_PATH_LINE:
case GSK_PATH_QUAD:
case GSK_PATH_CONIC:
gsk_curve_init_foreach (&curve, op, pts, n_pts, weight);
add_segment_for_curve (&curve, stroke_data);
break;
case GSK_PATH_CUBIC:
gsk_curve_init_foreach (&curve, op, pts, n_pts, weight);
split_curve_by_cusps (&curve, (AddCurveCallback)add_segment_for_curve, stroke_data);
break;
default:
g_assert_not_reached ();
}
return TRUE;
}
/*
* gsk_contour_default_add_stroke:
* @contour: the GskContour to stroke
* @builder: a GskPathBuilder to add the results to
* @stroke: stroke parameters
*
* Strokes @contour according to the parameters given in @stroke,
* and adds the resulting curves to @builder. Note that stroking
* a contour will in general produce multiple contours - either
* because @contour is closed and has a left and right outline,
* or because @stroke requires dashes.
*/
void
gsk_contour_default_add_stroke (const GskContour *contour,
GskPathBuilder *builder,
GskStroke *stroke)
{
StrokeData stroke_data;
stroke_data_init (&stroke_data, builder, stroke);
#ifdef STROKE_DEBUG
stroke_data.debug = gsk_path_builder_new ();
#endif
if (stroke->dash_length <= 0)
gsk_contour_foreach (contour, GSK_PATH_TOLERANCE_DEFAULT, stroke_op, &stroke_data);
else
gsk_contour_dash (contour, stroke, GSK_PATH_TOLERANCE_DEFAULT, stroke_op, &stroke_data);
if (stroke_data.has_current_point)
cap_and_connect_contours (&stroke_data);
#ifdef STROKE_DEBUG
GskPath *path = gsk_path_builder_free_to_path (stroke_data.debug);
gsk_path_builder_add_path (builder, path);
gsk_path_unref (path);
#endif
stroke_data_clear (&stroke_data);
}
/* }}} */
/* {{{ Offset implementation */
typedef struct
{
GskPathBuilder *builder;
float distance;
GskLineJoin line_join;
float miter_limit;
GskPathBuilder *offset;
gboolean has_current_point;
gboolean has_current_curve;
gboolean is_first_curve;
Segment o;
Segment o0;
} OffsetData;
static void
offset_data_init (OffsetData *offset_data,
GskPathBuilder *builder,
float distance,
GskLineJoin line_join,
float miter_limit)
{
memset (offset_data, 0, sizeof (OffsetData));
offset_data->builder = builder;
offset_data->distance = distance;
offset_data->line_join = line_join;
offset_data->miter_limit = miter_limit;
segment_init (&offset_data->o, NULL);
segment_init (&offset_data->o0, NULL);
}
static void
offset_data_clear (OffsetData *offset_data)
{
segment_clear (&offset_data->o);
segment_clear (&offset_data->o0);
}
static void
append_offset (OffsetData *offset_data)
{
if (offset_data->is_first_curve)
{
const GskCurve *o = segment_get_last_curve (&offset_data->o);
path_builder_move_to_point (offset_data->offset, gsk_curve_get_end_point (o));
}
else
{
path_builder_add_curves (offset_data->offset, offset_data->o.c);
}
}
static void
add_offset (OffsetData *offset_data,
Segment *offset)
{
const GskCurve *c1, *c2;
const GskCurve *o1, *o2;
float angle;
graphene_vec2_t tangent1, tangent2;
GskLineJoin line_join;
if (!offset_data->has_current_curve)
{
const GskCurve *o = segment_get_first_curve (offset);
path_builder_move_to_point (offset_data->offset, gsk_curve_get_start_point (o));
segment_copy (offset, &offset_data->o0);
segment_copy (offset, &offset_data->o);
offset_data->has_current_curve = TRUE;
offset_data->is_first_curve = TRUE;
return;
}
c1 = &offset_data->o.curve;
o1 = segment_get_last_curve (&offset_data->o);
c2 = &offset->curve;
o2 = segment_get_first_curve (offset);
gsk_curve_get_end_tangent (c1, &tangent1);
gsk_curve_get_start_tangent (c2, &tangent2);
angle = angle_between (&tangent1, &tangent2);
if (fabs (angle) < DEG_TO_RAD (5))
line_join = GSK_LINE_JOIN_ROUND;
else
line_join = offset_data->line_join;
if (fabs (angle) < DEG_TO_RAD (1))
{
/* Close enough to a straight line */
append_offset (offset_data);
}
else
{
float maxdist;
if (fabs (M_PI - fabs (angle)) < DEG_TO_RAD (1))
{
/* For 180 turns, we look at the whole curves to
* decide if they are left or right turns
*/
get_tangent (gsk_curve_get_start_point (c1),
gsk_curve_get_end_point (c1),
&tangent1);
get_tangent (gsk_curve_get_start_point (c2),
gsk_curve_get_end_point (c2),
&tangent2);
angle = angle_between (&tangent1, &tangent2);
}
maxdist = find_closest_intersection (c1, c2, NULL);
if ((angle > 0) == (offset_data->distance > 0))
{
int i, j;
float t1, t2;
graphene_point_t p;
/* Turn towards the offset */
if (segments_intersect (offset_data->o.c, offset->c, FALSE, &i, &j, &t1, &t2, &p, 1) > 0 &&
graphene_point_distance (&p, gsk_curve_get_start_point (c2), NULL, NULL) < maxdist)
{
GskCurve c;
GskCurve *r;
g_array_set_size (offset_data->o.c, i + 1);
r = &g_array_index (offset_data->o.c, GskCurve, i);
gsk_curve_split (r, t1, &c, NULL);
g_array_index (offset_data->o.c, GskCurve, i) = c;
g_array_remove_range (offset->c, 0, j);
r = &g_array_index (offset->c, GskCurve, 0);
gsk_curve_split (r, t2, NULL, &c);
g_array_index (offset->c, GskCurve, 0) = c;
if (offset_data->is_first_curve)
{
g_array_set_size (offset_data->o0.c, 0);
g_array_append_vals (offset_data->o0.c, offset_data->o.c->data, offset_data->o.c->len);
}
append_offset (offset_data);
}
else
{
append_offset (offset_data);
path_builder_line_to_point (offset_data->offset, gsk_curve_get_start_point (o2));
}
}
else
{
/* Turn away */
append_offset (offset_data);
add_line_join (offset_data->offset,
line_join,
offset_data->distance,
offset_data->miter_limit,
gsk_curve_get_start_point (c2),
o1,
o2,
angle);
}
}
segment_copy (offset, &offset_data->o);
offset_data->is_first_curve = FALSE;
}
static void
finish_offset_contour (OffsetData *offset_data)
{
GskPath *path;
if (offset_data->has_current_curve)
{
if (!offset_data->is_first_curve)
path_builder_add_curves (offset_data->offset, offset_data->o.c);
path_builder_move_to_point (offset_data->builder, gsk_curve_get_start_point (segment_get_first_curve (&offset_data->o0)));
path_builder_add_curves (offset_data->builder, offset_data->o0.c);
path = gsk_path_builder_free_to_path (offset_data->offset);
path_builder_append_path (offset_data->builder, path);
gsk_path_unref (path);
}
else
{
gsk_path_builder_unref (offset_data->offset);
}
offset_data->offset = NULL;
}
static void
close_offset_contour (OffsetData *offset_data)
{
GskPath *path;
if (offset_data->has_current_curve)
{
/* add final join and first segment */
add_offset (offset_data, &offset_data->o0);
path_builder_add_curves (offset_data->offset, offset_data->o.c);
}
gsk_path_builder_close (offset_data->offset);
path = gsk_path_builder_free_to_path (offset_data->offset);
gsk_path_builder_add_path (offset_data->builder, path);
gsk_path_unref (path);
offset_data->offset = NULL;
}
static void
add_offset_for_curve (const GskCurve *curve,
OffsetData *offset_data)
{
Segment offset = { 0, };
segment_init (&offset, curve);
segment_subdivide (&offset);
segment_offset (&offset, offset_data->distance);
elide_self_intersections (&offset);
add_offset (offset_data, &offset);
segment_clear (&offset);
}
static gboolean
offset_op (GskPathOperation op,
const graphene_point_t *pts,
gsize n_pts,
float weight,
gpointer user_data)
{
OffsetData *offset_data = user_data;
Segment offset = { 0, };
GskCurve curve;
switch (op)
{
case GSK_PATH_MOVE:
if (offset_data->has_current_point)
finish_offset_contour (offset_data);
gsk_curve_init_foreach (&curve,
GSK_PATH_LINE,
(const graphene_point_t[]) { pts[0], GRAPHENE_POINT_INIT (pts[0].x + 1, pts[0].y) },
2, 0.f);
segment_init (&offset, &curve);
segment_subdivide (&offset);
segment_offset (&offset, offset_data->distance);
segment_copy (&offset, &offset_data->o0);
segment_clear (&offset);
offset_data->offset = gsk_path_builder_new ();
offset_data->has_current_point = TRUE;
offset_data->has_current_curve = FALSE;
break;
case GSK_PATH_CLOSE:
if (offset_data->has_current_point)
{
if (!graphene_point_near (&pts[0], &pts[1], 0.001))
{
gsk_curve_init_foreach (&curve, GSK_PATH_LINE, pts, n_pts, weight);
add_offset_for_curve (&curve, offset_data);
}
close_offset_contour (offset_data);
}
offset_data->has_current_point = FALSE;
offset_data->has_current_curve = FALSE;
break;
case GSK_PATH_LINE:
case GSK_PATH_QUAD:
case GSK_PATH_CONIC:
gsk_curve_init_foreach (&curve, op, pts, n_pts, weight);
add_offset_for_curve (&curve, offset_data);
break;
case GSK_PATH_CUBIC:
gsk_curve_init_foreach (&curve, op, pts, n_pts, weight);
split_curve_by_cusps (&curve, (AddCurveCallback)add_offset_for_curve, offset_data);
break;
default:
g_assert_not_reached ();
}
return TRUE;
}
void
gsk_contour_default_offset (const GskContour *contour,
GskPathBuilder *builder,
float distance,
GskLineJoin line_join,
float miter_limit)
{
OffsetData offset_data;
offset_data_init (&offset_data, builder, distance, line_join, miter_limit);
gsk_contour_foreach (contour, GSK_PATH_TOLERANCE_DEFAULT, offset_op, &offset_data);
if (offset_data.has_current_point)
finish_offset_contour (&offset_data);
offset_data_clear (&offset_data);
}
/* }}} */
/* vim:set foldmethod=marker expandtab: */