Files
gtk/gsk/gskpathops.c
Matthias Clasen a2c35d8682 Implement boolean operations on paths
Implement union, intersection, difference and
symmetric difference of two paths, as well as
simplification of a single path.
2023-07-08 20:45:42 -04:00

1728 lines
43 KiB
C

/*
* Copyright © 2022 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
*/
#include "config.h"
#include <math.h>
#include "gskpathprivate.h"
#include "gskcurveprivate.h"
#include "gskpathbuilder.h"
#include "gskpathmeasure.h"
#include "gskstrokeprivate.h"
#include "gdk/gdkprivate.h"
#define PATH_OPS_DEBUG
#if defined(PATH_OPS_DEBUG) && !defined(G_ENABLE_DEBUG)
#define G_ENABLE_DEBUG
#endif
#ifdef G_ENABLE_DEBUG
#define DEBUG(opdata, ...) \
if ((opdata)->debug) \
{ \
g_print (__VA_ARGS__); \
g_print ("\n"); \
}
#else
#define DEBUG(opdata, ...)
#endif
/* {{{ General utilities */
#define RAD_TO_DEG(r) ((r + M_PI)*180.0/M_PI)
#define DEG_TO_RAD(d) ((d)*M_PI/180.0)
static void
g_list_insert_after (GList *l,
gpointer data)
{
GList *res G_GNUC_UNUSED;
/* Silence warn-if-unused */
res = g_list_insert_before (l, l->next, data);
}
/* }}} */
/* {{{ GskPath utilities */
static GskPath *
gsk_path_remove_unclosed (GskPath *path)
{
GSList *contours;
GskPath *path2;
if (gsk_path_get_flags (path) & GSK_PATH_CLOSED)
return path;
contours = NULL;
for (int i = 0; i < gsk_path_get_n_contours (path); i++)
{
const GskContour *contour = gsk_path_get_contour (path, i);
if (gsk_contour_get_flags (contour) & GSK_PATH_CLOSED)
contours = g_slist_prepend (contours, (gpointer)contour);
}
contours = g_slist_reverse (contours);
path2 = gsk_path_new_from_contours (contours);
g_slist_free (contours);
gsk_path_unref (path);
return path2;
}
/* }}} */
/* {{{ GskCurve utilities */
static gboolean
curve_is_tiny (GskCurve *curve)
{
GskBoundingBox bounds;
gsk_curve_get_bounds (curve, &bounds);
return bounds.max.x - bounds.min.x < 0.01 &&
bounds.max.y - bounds.min.y < 0.01;
}
/* This assumes the endpoints are the same */
static gboolean
curves_coincide (GskCurve *c1,
GskCurve *c2)
{
graphene_point_t p1, p2;
if (c1->op != c2->op)
return FALSE;
if (c1->op == GSK_PATH_LINE)
return TRUE;
gsk_curve_get_point (c1, 0.5, &p1);
gsk_curve_get_point (c2, 0.5, &p2);
return graphene_point_near (&p1, &p2, 0.01);
}
/* }}} */
/* {{{ Graph types and helpers */
typedef struct
{
graphene_point_t p;
GPtrArray *edges;
int inconsistent;
int boundaries;
#ifdef G_ENABLE_DEBUG
char *name;
#endif
} Node;
static void
node_free (Node *c)
{
g_ptr_array_free (c->edges, TRUE);
#ifdef G_ENABLE_DEBUG
g_free (c->name);
#endif
g_free (c);
}
/* Used to describe what we find to the side of an edge */
typedef enum
{
AREA_UNKNOWN,
AREA_IN,
AREA_OUT,
} AreaClassification;
typedef struct
{
GskCurve curve;
Node *start;
Node *end;
AreaClassification area_left1;
AreaClassification area_right1;
AreaClassification area_left2;
AreaClassification area_right2;
AreaClassification area_left;
AreaClassification area_right;
gboolean interior; // area_left == area_right
gboolean coincides; // part of both paths
gboolean collected;
gboolean remove;
float start_angle;
float end_angle;
int path_num;
int curve_num;
int intersect_next;
#ifdef G_ENABLE_DEBUG
char *name;
#endif
} Edge;
static void
edge_free (Edge *c)
{
#ifdef G_ENABLE_DEBUG
g_free (c->name);
#endif
g_free (c);
}
static void
reverse_edge (Edge *edge)
{
GskCurve r;
Node *s;
AreaClassification a;
float angle;
gsk_curve_reverse (&edge->curve, &r);
edge->curve = r;
#define SWAP(a,b,c) a = b; b = c; c = a;
SWAP (s, edge->start, edge->end);
SWAP (a, edge->area_left1, edge->area_right1);
SWAP (a, edge->area_left2, edge->area_right2);
SWAP (a, edge->area_left, edge->area_right);
SWAP (angle, edge->start_angle, edge->end_angle);
#undef SWAP
}
static void
merge_nodes (GList **nodes,
Node *c1,
Node *c2)
{
if (c1 == c2)
return;
for (int i = 0; i < c2->edges->len; i++)
{
Edge *edge = g_ptr_array_index (c2->edges, i);
if (edge->start == c2)
edge->start = c1;
if (edge->end == c2)
edge->end = c1;
g_ptr_array_add (c1->edges, edge);
}
*nodes = g_list_remove (*nodes, c2);
node_free (c2);
for (int i = c1->edges->len - 1; i >= 0; i--)
{
Edge *edge = g_ptr_array_index (c1->edges, i);
if (edge->start == edge->end)
edge->remove = TRUE;
}
}
/* }}} */
/* {{{ Path Op Data */
typedef struct
{
GskPathOp operation;
GskFillRule fill_rule;
GskPathMeasure *first;
GskPathMeasure *second;
GList *edges;
GList *nodes;
Edge *start;
int curve_num;
int path_num;
#ifdef G_ENABLE_DEBUG
gboolean debug;
#endif
} PathOpData;
static void
pathopdata_init (PathOpData *opdata,
GskPathOp operation,
GskFillRule fill_rule,
GskPath *first,
GskPath *second)
{
memset (opdata, 0, sizeof (PathOpData));
opdata->operation = operation;
opdata->fill_rule = fill_rule;
opdata->first = gsk_path_measure_new (first);
if (second)
opdata->second = gsk_path_measure_new (second);
#ifdef G_ENABLE_DEBUG
opdata->debug = g_strcmp0 (g_getenv ("GSK_PATH_DEBUG"), "1") == 0;
#endif
}
static void
pathopdata_clear (PathOpData *opdata)
{
gsk_path_measure_unref (opdata->first);
if (opdata->second)
gsk_path_measure_unref (opdata->second);
g_list_free_full (opdata->edges, (GDestroyNotify) edge_free);
g_list_free_full (opdata->nodes, (GDestroyNotify) node_free);
}
/* }}} */
/* {{{ Debugging */
#ifdef G_ENABLE_DEBUG
static void
dump_node (Node *c)
{
g_print ("%s%s %f %f\n", c->name, c->inconsistent ? " BAD" : "", c->p.x, c->p.y);
for (int i = 0; i < c->edges->len; i++)
{
Edge *n = g_ptr_array_index (c->edges, i);
float angle;
const char *class = " 10";
const char *ind1 = " ";
const char *ind2 = "";
char buf[4];
if (n->area_left != AREA_UNKNOWN && n->area_right != AREA_UNKNOWN)
{
if (n->end == c)
buf[0] = '>';
else
buf[0] = '<';
buf[1] = class[n->area_left];
buf[2] = class[n->area_right];
buf[3] = '\0';
ind1 = buf;
if (n->interior)
{
buf[0] = '[';
ind2 = "]";
}
else if (n->collected)
{
buf[0] = '(';
ind2 = ")";
}
}
angle = n->start == c ? n->start_angle : n->end_angle;
g_print ("\t%s %s %s %g\n", ind1, n->name, ind2, RAD_TO_DEG (angle));
}
}
/* Check that our graph structure is intact:
* Every edge is included in the edges of its
* start and end nodes.
*/
static void
validate_edges (PathOpData *opdata)
{
for (GList *l = opdata->edges; l; l = l->next)
{
Edge *c = l->data;
if (opdata->debug)
{
const char *class = " 10";
const char *ind1 = " ";
const char *ind2 = "";
char buf[4];
if (c->area_left != AREA_UNKNOWN && c->area_right != AREA_UNKNOWN)
{
buf[0] = ' ';
buf[1] = class[c->area_left];
buf[2] = class[c->area_right];
buf[3] = '\0';
ind1 = buf;
if (c->interior)
{
buf[0] = '[';
ind2 = "]";
}
if (c->coincides)
buf[0] = '=';
}
g_print ("%s %s: ", ind1, c->name);
char *s = gsk_curve_to_string (&c->curve);
g_print ("%s %s\n", s, ind2);
g_free (s);
}
g_assert (c->curve.op == GSK_PATH_LINE ||
c->curve.op == GSK_PATH_QUAD ||
c->curve.op == GSK_PATH_CUBIC);
g_assert (g_ptr_array_find (c->start->edges, c, NULL));
g_assert (g_ptr_array_find (c->end->edges, c, NULL));
}
}
/* Check that our graph structure is intact:
* All edges of a node are connected to that node.
*/
static void
validate_nodes (PathOpData *opdata)
{
for (GList *l = opdata->nodes; l; l = l->next)
{
Node *c = l->data;
for (int i = 0; i < c->edges->len; i++)
{
Edge *edge G_GNUC_UNUSED;
edge = g_ptr_array_index (c->edges, i);
g_assert (edge->start == c || edge->end == c);
}
}
}
#endif /* G_ENABLE_DEBUG */
#ifdef PATH_OPS_DEBUG
/* Use with dot -Tpng -O inconsistent.dot to get some visualization
* of the graph we ended up with. Inconsistent nodes appear in red.
*/
static void
dump_dotfile (GList *edges,
GList *nodes,
const char *filename)
{
GString *s;
s = g_string_new ("");
g_string_append (s, "digraph {\n");
for (GList *l = nodes; l; l = l->next)
{
Node *c = l->data;
g_string_append_printf (s, "\"%p\" [label=\"%s\",color=%s]\n", c, c->name, c->inconsistent ? (c->inconsistent == 1 ? "red" : "purple") : (c->boundaries == 0 ? "gray" : "black"));
}
for (GList *l = edges; l; l = l->next)
{
Edge *edge = l->data;
g_string_append_printf (s, "\"%p\" -> \"%p\" [label=\"%s\",color=%s]\n", edge->start, edge->end, edge->name, edge->interior ? "gray" : "black");
}
g_string_append (s, "}\n");
g_file_set_contents (filename, s->str, s->len, NULL);
g_string_free (s, TRUE);
}
#endif
/* }}} */
/* {{{ Collection helpers */
static gboolean
collect_cb (GskPathOperation op,
const graphene_point_t *pts,
gsize n_pts,
float weight,
gpointer user_data)
{
PathOpData *opdata = user_data;
Edge *edge;
Node *node;
if (op == GSK_PATH_MOVE)
return TRUE;
if (op == GSK_PATH_CLOSE)
{
Edge *prev;
if (!graphene_point_near (&pts[0], &pts[1], 0.01))
collect_cb (GSK_PATH_LINE, pts, n_pts, weight, user_data);
prev = opdata->edges->data;
if (opdata->start) /* Ignore M followed by Z */
merge_nodes (&opdata->nodes, opdata->start->start, prev->end);
opdata->start = NULL;
return TRUE;
}
edge = g_new0 (Edge, 1);
gsk_curve_init (&edge->curve, gsk_pathop_encode (op, pts));
if (opdata->start)
{
Edge *prev = opdata->edges->data;
edge->start = prev->end;
}
else
{
opdata->start = edge;
node = g_new0 (Node, 1);
node->p = *gsk_curve_get_start_point (&edge->curve);
node->edges = g_ptr_array_new ();
#ifdef G_ENABLE_DEBUG
if (opdata->debug)
node->name = g_strdup_printf ("start %d", opdata->curve_num);
#endif
opdata->nodes = g_list_prepend (opdata->nodes, node);
edge->start = node;
}
node = g_new0 (Node, 1);
node->p = *gsk_curve_get_end_point (&edge->curve);
node->edges = g_ptr_array_new ();
#ifdef G_ENABLE_DEBUG
if (opdata->debug)
node->name = g_strdup_printf ("end %d", opdata->curve_num);
#endif
opdata->nodes = g_list_prepend (opdata->nodes, node);
edge->end = node;
g_ptr_array_add (edge->start->edges, edge);
g_ptr_array_add (edge->end->edges, edge);
opdata->edges = g_list_prepend (opdata->edges, edge);
#ifdef G_ENABLE_DEBUG
if (opdata->debug)
{
const char *opname[] = { "Move", "Close", "Line", "Curve", "Conic" };
edge->name = g_strdup_printf ("%s %d", opname[op], opdata->curve_num);
}
#endif
edge->curve_num = opdata->curve_num++;
edge->path_num = opdata->path_num;
return TRUE;
}
static void
collect_edges (GskPath *path,
PathOpData *opdata)
{
GskPath *p;
p = gsk_path_remove_unclosed (gsk_path_ref (path));
gsk_path_foreach (p, GSK_PATH_FOREACH_ALLOW_QUAD|GSK_PATH_FOREACH_ALLOW_CUBIC, collect_cb, opdata);
gsk_path_unref (p);
opdata->path_num++;
}
/* }}} */
/* {{{ Splitting helpers */
#define NEAR(f1, f2) (fabs ((f2) - (f1)) < 0.005)
typedef struct
{
float t1;
float t2;
graphene_point_t p;
Node *node;
} SplitPoint;
static int
compare_t1 (const void *p1,
const void *p2)
{
const SplitPoint *sp1 = p1;
const SplitPoint *sp2 = p2;
return sp1->t1 < sp2->t1 ? -1 : (sp1->t1 > sp2->t1 ? 1 : 0);
}
static int
compare_t2 (const void *p1,
const void *p2)
{
const SplitPoint *sp1 = p1;
const SplitPoint *sp2 = p2;
return sp1->t2 < sp2->t2 ? -1 : (sp1->t2 > sp2->t2 ? 1 : 0);
}
static void
split_edges (PathOpData *opdata)
{
GList *l, *ll;
l = opdata->edges;
while (l)
{
Edge *cd1 = l->data;
while (l && curve_is_tiny (&cd1->curve))
{
l = l->next;
if (!l)
break;
cd1 = l->data;
}
if (!l)
break;
ll = l;
while (ll)
{
Edge *cd2;
float t1[9], t2[9];
graphene_point_t p[9];
SplitPoint sp[9];
int n;
GskCurve cs = { 0, };
GskCurve ce = { 0, };
Edge *cd;
GList *before;
#ifdef G_ENABLE_DEBUG
char *name = NULL;
#endif
int path_num1, path_num2;
int curve_num1, curve_num2;
cd1 = l->data;
cd2 = ll->data;
while (ll && (curve_is_tiny (&cd2->curve) || cd2->curve_num <= cd1->intersect_next))
{
ll = ll->next;
if (!ll)
break;
cd2 = ll->data;
}
if (!ll)
break;
if (cd1->curve.op == GSK_PATH_LINE && cd1->curve_num == cd2->curve_num)
{
/* Two segments of the same original line won't intersect */
ll = ll->next;
continue;
}
path_num1 = cd1->path_num;
path_num2 = cd2->path_num;
curve_num1 = cd1->curve_num;
curve_num2 = cd2->curve_num;
n = gsk_curve_intersect (&cd1->curve, &cd2->curve, t1, t2, p, G_N_ELEMENTS (t1));
#ifdef G_ENABLE_DEBUG
if (n > 0)
DEBUG (opdata, "%d intersections between %s and %s", n, cd1->name, cd2->name);
#endif
if (n == 1)
{
if (cd1->start == cd2->start || cd1->start == cd2->end ||
cd1->end == cd2->start || cd1->end == cd2->end)
{
/* We already got this one, move on */
ll = ll->next;
continue;
}
}
for (int i = 0; i < n; i++)
sp[i] = (SplitPoint) { t1[i], t2[i], p[i], NULL };
qsort (sp, n, sizeof (SplitPoint), compare_t1);
#ifdef G_ENABLE_DEBUG
if (opdata->debug)
name = g_strdup (cd1->name);
#endif
before = l;
for (int i = 0; i < n; i++)
{
if (NEAR (sp[i].t1, 0))
sp[i].node = cd1->start;
else if (NEAR (sp[i].t1, 1))
sp[i].node = cd1->end;
else
{
sp[i].node = g_new0 (Node, 1);
sp[i].node->p = sp[i].p;
sp[i].node->edges = g_ptr_array_new ();
opdata->nodes = g_list_prepend (opdata->nodes, sp[i].node);
gsk_curve_split (&cd1->curve, sp[i].t1, &cs, &ce);
cd1->curve = cs;
cd1->intersect_next = curve_num2;
cd = g_new0 (Edge, 1);
cd->area_left1 = cd1->area_left1;
cd->area_right1 = cd1->area_right1;
cd->area_left2 = cd1->area_left2;
cd->area_right2 = cd1->area_right2;
cd->path_num = path_num1;
cd->curve_num = curve_num1;
cd->intersect_next = curve_num2;
cd->curve = ce;
cd->end = cd1->end;
g_ptr_array_remove (cd1->end->edges, cd1);
g_ptr_array_add (cd->end->edges, cd);
#ifdef G_ENABLE_DEBUG
if (opdata->debug)
{
sp[i].node->name = g_strdup_printf ("split %s/%s", name, cd2->name);
if (i == 0)
{
char *nn = g_strdup_printf ("%s.0", name);
g_free (cd1->name);
cd1->name = nn;
}
cd->name = g_strdup_printf ("%s.%d", name, i + 1);
if (i == 0)
{
char *s = gsk_curve_to_string (&cd1->curve);
g_print ("split %s from %s at %g: %s\n", cd1->name, name, sp[i].t1, s);
g_free (s);
}
char * s = gsk_curve_to_string (&cd->curve);
g_print ("split %s from %s at %g: %s", cd->name, name, sp[i].t1, s);
g_free (s);
}
#endif
cd1->end = sp[i].node;
cd->start = sp[i].node;
g_ptr_array_add (cd1->end->edges, cd1);
g_ptr_array_add (cd->start->edges, cd);
g_list_insert_after (before, cd);
before = before->next;
cd1 = cd;
for (int j = i + 1; j < n; j++)
sp[j].t1 = (sp[j].t1 - sp[i].t1) / (1 - sp[i].t1);
}
}
#ifdef G_ENABLE_DEBUG
if (opdata->debug)
g_free (name);
#endif
qsort (sp, n, sizeof (SplitPoint), compare_t2);
#ifdef G_ENABLE_DEBUG
if (opdata->debug)
name = g_strdup (cd2->name);
#endif
for (int i = 0; i < n; i++)
{
if (NEAR (sp[i].t2, 0))
{
for (int k = 0; k < n; k++)
{
if (sp[k].node == cd2->start)
sp[k].node = sp[i].node;
}
merge_nodes (&opdata->nodes, sp[i].node, cd2->start);
}
else if (NEAR (sp[i].t2, 1))
{
for (int k = 0; k < n; k++)
{
if (sp[k].node == cd2->end)
sp[k].node = sp[i].node;
}
merge_nodes (&opdata->nodes, sp[i].node, cd2->end);
}
else
{
gsk_curve_split (&cd2->curve, sp[i].t2, &cs, &ce);
cd2->curve = cs;
cd2->intersect_next = curve_num1;
cd = g_new0 (Edge, 1);
cd->area_left1 = cd2->area_left1;
cd->area_right1 = cd2->area_right1;
cd->area_left2 = cd2->area_left2;
cd->area_right2 = cd2->area_right2;
cd->path_num = path_num2;
cd->curve_num = curve_num2;
cd->intersect_next = curve_num1;
cd->curve = ce;
cd->end = cd2->end;
g_ptr_array_remove (cd2->end->edges, cd2);
g_ptr_array_add (cd->end->edges, cd);
#ifdef G_ENABLE_DEBUG
if (opdata->debug)
{
if (i == 0)
{
char *nn = g_strdup_printf ("%s.0", name);
g_free (cd2->name);
cd2->name = nn;
}
cd->name = g_strdup_printf ("%s.%d", name, i + 1);
if (i == 0)
{
char * s = gsk_curve_to_string (&cd2->curve);
g_print ("split %s from %s at %g: %s\n", cd2->name, name, sp[i].t2, s);
g_free (s);
}
char *s = gsk_curve_to_string (&cd->curve);
g_print ("split %s from %s at %g: %s\n", cd->name, name, sp[i].t2, s);
g_free (s);
}
#endif
cd2->end = sp[i].node;
cd->start = sp[i].node;
g_ptr_array_add (cd2->end->edges, cd2);
g_ptr_array_add (cd->start->edges, cd);
g_list_insert_after (ll, cd);
ll = ll->next;
cd2 = cd;
for (int j = i + 1; j < n; j++)
sp[j].t2 = (sp[j].t2 - sp[i].t2) / (1 - sp[i].t2);
}
}
#ifdef G_ENABLE_DEBUG
if (opdata->debug)
g_free (name);
#endif
ll = ll->next;
}
l = l->next;
}
}
/* }}} */
/* {{{ Classification helpers */
/* Each edge that comes into a node is either interior, or it is
* a boundary between inner and outer. There must be an even number
* of non-interior edges, and the area must alternate between inner
* and outer.
*/
static void
check_minimal_consistency (Node *c)
{
c->inconsistent = 0;
c->boundaries = 0;
for (int i = 0; i < c->edges->len; i++)
{
Edge *edge = g_ptr_array_index (c->edges, i);
if (!edge->interior)
c->boundaries++;
}
if (c->boundaries % 2 != 0)
{
/* Edges are misclassified */
c->inconsistent = 1;
}
}
static void
check_consistency (Node *c)
{
check_minimal_consistency (c);
if (c->inconsistent)
return;
for (int i = 0; i < c->edges->len; i++)
{
Edge *edge = g_ptr_array_index (c->edges, i);
Edge *edge2 = g_ptr_array_index (c->edges, (i + 1) % c->edges->len);
AreaClassification area1, area2, area;
if (edge->end == c)
{
area1 = edge->area_right1;
area2 = edge->area_right2;
area = edge->area_right;
}
else
{
area1 = edge->area_left1;
area2 = edge->area_left2;
area = edge->area_left;
}
if (edge2->end == c)
{
if (area1 != edge2->area_left1 ||
area2 != edge2->area_left2 ||
area != edge2->area_left)
c->inconsistent = 2;
}
else
{
if (area1 != edge2->area_right1 ||
area2 != edge2->area_right2 ||
area != edge2->area_right)
c->inconsistent = 2;
}
if (!edge->coincides)
{
if ((edge->path_num == 0 &&
edge->area_left2 != edge->area_right2) ||
(edge->path_num == 1 &&
edge->area_left1 != edge->area_right1))
c->inconsistent = 2;
}
if (c->inconsistent)
return;
}
}
static void
compute_coincidence (Edge *edge)
{
if (edge->coincides || edge->remove)
return;
for (int i = 0; i < edge->start->edges->len; i++)
{
Edge *other = g_ptr_array_index (edge->start->edges, i);
if (other->remove)
continue;
if (other != edge &&
other->curve.op == edge->curve.op &&
other->path_num != edge->path_num &&
g_ptr_array_find (edge->end->edges, other, NULL) &&
curves_coincide (&edge->curve, &other->curve))
{
edge->coincides = TRUE;
other->remove = TRUE;
break;
}
}
}
static inline AreaClassification
apply_op (GskPathOp op,
AreaClassification c1,
AreaClassification c2)
{
switch (op)
{
case GSK_PATH_OP_SIMPLIFY: return c1;
case GSK_PATH_OP_UNION: return (c1 == AREA_IN) || (c2 == AREA_IN) ? AREA_IN : AREA_OUT;
case GSK_PATH_OP_INTERSECTION: return (c1 == AREA_IN) && (c2 == AREA_IN) ? AREA_IN : AREA_OUT;
case GSK_PATH_OP_DIFFERENCE: return (c1 == AREA_IN) && (c2 == AREA_OUT) ? AREA_IN : AREA_OUT;
case GSK_PATH_OP_XOR: return c1 != c2 ? AREA_IN : AREA_OUT;
default: g_assert_not_reached ();
}
}
static void
classify_boundary (Edge *edge,
PathOpData *opdata)
{
graphene_point_t pos, pos1, pos2;
graphene_vec2_t tangent;
if (edge->area_left != AREA_UNKNOWN && edge->area_right != AREA_UNKNOWN)
return;
gsk_curve_get_point (&edge->curve, 0.5, &pos);
gsk_curve_get_tangent (&edge->curve, 0.5, &tangent);
pos1.x = pos.x + 0.5 * graphene_vec2_get_y (&tangent);
pos1.y = pos.y - 0.5 * graphene_vec2_get_x (&tangent);
pos2.x = pos.x - 0.5 * graphene_vec2_get_y (&tangent);
pos2.y = pos.y + 0.5 * graphene_vec2_get_x (&tangent);
if (edge->path_num == 1 && !edge->coincides)
{
/* Classifying wrt to the other path; check the point
* on the curve, which is safe since we're already
* intersected. The only case we need to avoid is if
* this edge coincides with an edge on the other path.
*/
edge->area_left1 = edge->area_right1 =
gsk_path_measure_in_fill (opdata->first, &pos, opdata->fill_rule) ? AREA_IN : AREA_OUT;
}
else
{
if (edge->area_left1 == AREA_UNKNOWN)
edge->area_left1 = gsk_path_measure_in_fill (opdata->first, &pos1, opdata->fill_rule) ? AREA_IN : AREA_OUT;
if (edge->area_right1 == AREA_UNKNOWN)
edge->area_right1 = gsk_path_measure_in_fill (opdata->first, &pos2, opdata->fill_rule) ? AREA_IN : AREA_OUT;
}
if (opdata->second)
{
if (edge->path_num == 0 && !edge->coincides)
{
/* Classifying wrt to the other path, see above */
edge->area_left2 = edge->area_right2 =
gsk_path_measure_in_fill (opdata->second, &pos, opdata->fill_rule) ? AREA_IN : AREA_OUT;
}
else
{
if (edge->area_left2 == AREA_UNKNOWN)
edge->area_left2 = gsk_path_measure_in_fill (opdata->second, &pos1, opdata->fill_rule) ? AREA_IN : AREA_OUT;
if (edge->area_right2 == AREA_UNKNOWN)
edge->area_right2 = gsk_path_measure_in_fill (opdata->second, &pos2, opdata->fill_rule) ? AREA_IN : AREA_OUT;
}
}
else
{
edge->area_left2 = AREA_OUT;
edge->area_right2 = AREA_OUT;
}
edge->area_left = apply_op (opdata->operation, edge->area_left1, edge->area_left2);
edge->area_right = apply_op (opdata->operation, edge->area_right1, edge->area_right2);
edge->interior = edge->area_left == edge->area_right;
}
static inline void
reset_classification (Edge *edge)
{
edge->area_left = AREA_UNKNOWN;
edge->area_right = AREA_UNKNOWN;
edge->area_left1 = AREA_UNKNOWN;
edge->area_right1 = AREA_UNKNOWN;
edge->area_left2 = AREA_UNKNOWN;
edge->area_right2 = AREA_UNKNOWN;
}
static inline void
copy_classification (Edge *from,
Edge *to)
{
to->area_left1 = from->area_left1;
to->area_right1 = from->area_right1;
to->area_left2 = from->area_left2;
to->area_right2 = from->area_right2;
to->area_left = from->area_left;
to->area_right = from->area_right;
to->interior = from->interior;
}
static void
propagate_classification_dir (Edge *edge,
gboolean forward)
{
Node *c = forward ? edge->end : edge->start;
Edge *other_edge;
guint idx;
if (c->edges->len != 2)
return;
g_ptr_array_find (c->edges, edge, &idx);
other_edge = g_ptr_array_index (c->edges, 1 - idx);
if (other_edge->area_left == AREA_UNKNOWN ||
other_edge->area_right == AREA_UNKNOWN)
{
copy_classification (edge, other_edge);
propagate_classification_dir (other_edge, forward);
}
}
static void
propagate_classification (Edge *edge)
{
propagate_classification_dir (edge, FALSE);
propagate_classification_dir (edge, TRUE);
}
static void
compute_angles (Edge *edge)
{
graphene_vec2_t tangent;
gsk_curve_get_start_tangent (&edge->curve, &tangent);
edge->start_angle = atan2 (- graphene_vec2_get_y (&tangent),
graphene_vec2_get_x (&tangent));
gsk_curve_get_end_tangent (&edge->curve, &tangent);
graphene_vec2_negate (&tangent, &tangent);
edge->end_angle = atan2 (- graphene_vec2_get_y (&tangent),
graphene_vec2_get_x (&tangent));
}
static inline float
get_turning_direction (Edge *c1,
Node *c)
{
if (c1->curve.op == GSK_PATH_CUBIC)
{
graphene_point_t p;
const graphene_point_t *q;
if (c1->start == c)
{
gsk_curve_get_point (&c1->curve, 0.333, &p);
q = gsk_curve_get_start_point (&c1->curve);
}
else
{
gsk_curve_get_point (&c1->curve, 0.666, &p);
q = gsk_curve_get_end_point (&c1->curve);
}
return atan2 (- (p.y - q->y), p.x - q->x);
}
return c1->start == c ? c1->start_angle : c1->end_angle;
}
static int
compare_angle (gconstpointer p1,
gconstpointer p2,
gpointer user_data)
{
Node *c = user_data;
Edge *c1 = *(Edge **)p1;
Edge *c2 = *(Edge **)p2;
float f1 = c1->start == c ? c1->start_angle : c1->end_angle;
float f2 = c2->start == c ? c2->start_angle : c2->end_angle;
if (fabs (fmod (f1 - f2, 2 * G_PI)) < 0.01)
{
/* Deal with curves that come in at the same angle
* by looking in which direction they turn
*/
f1 = get_turning_direction (c1, c);
f2 = get_turning_direction (c2, c);
}
return f1 < f2 ? -1 : (f1 > f2 ? 1 : 0);
}
static void
classify_edges (PathOpData *opdata)
{
GList *l;
for (l = opdata->edges; l; l = l->next)
{
Edge *edge = l->data;
if (edge->remove)
continue;
compute_coincidence (edge);
compute_angles (edge);
}
l = opdata->edges;
while (l)
{
Edge *edge = l->data;
GList *next = l->next;
if (edge->remove)
{
g_ptr_array_remove (edge->start->edges, edge);
g_ptr_array_remove (edge->end->edges, edge);
opdata->edges = g_list_remove (opdata->edges, edge);
edge_free (edge);
}
l = next;
}
for (l = opdata->nodes; l; l = l->next)
{
Node *c = l->data;
g_ptr_array_sort_with_data (c->edges, compare_angle, c);
}
/* We do classification after sorting, so we can possibly
* use node order during propagation.
*/
for (l = opdata->edges; l; l = l->next)
{
Edge *edge = l->data;
classify_boundary (edge, opdata);
propagate_classification (edge);
}
for (l = opdata->nodes; l; l = l->next)
{
Node *c = l->data;
check_consistency (c);
}
}
/* }}} */
/* {{{ Consistency helpers */
static void
propagate_changed_classification (Edge *edge,
gboolean forward)
{
Node *c = forward ? edge->end : edge->start;
Edge *other_edge;
guint idx;
if (c->edges->len != 2)
{
check_minimal_consistency (c);
return;
}
g_ptr_array_find (c->edges, edge, &idx);
other_edge = g_ptr_array_index (c->edges, 1 - idx);
if (other_edge->area_left != edge->area_left ||
other_edge->area_right != edge->area_right ||
other_edge->interior != edge->interior)
{
copy_classification (edge, other_edge);
propagate_changed_classification (other_edge, forward);
}
}
static void
propagate_changes (Node *c)
{
for (unsigned int i = 0; i < c->edges->len; i++)
{
Edge *edge = g_ptr_array_index (c->edges, i);
propagate_changed_classification (edge, edge->start == c);
}
}
static gboolean
graph_has_inconsistencies (PathOpData *opdata)
{
for (GList *l = opdata->nodes; l; l = l->next)
{
Node *c = l->data;
if (c->inconsistent)
return TRUE;
}
return FALSE;
}
static Edge *
other_edge (Node *n, Edge *e)
{
if (n->edges->len == 2)
{
if (e == g_ptr_array_index (n->edges, 0))
return g_ptr_array_index (n->edges, 1);
else
return g_ptr_array_index (n->edges, 0);
}
return NULL;
}
/* Return the intersection (i.e. >2-edge) node
* that you arrive at by following the path
* starting with edge e.
*/
static Node *
other_end (Node *n, Edge *e)
{
gboolean forward = e->start == n;
do {
Node *n2 = forward ? e->end : e->start;
e = other_edge (n2, e);
if (e == NULL)
return n2;
if (n2 == n)
return NULL;
} while (1);
}
/* Check if all the edges along the path
* from n via e agree on classification.
*/
static gboolean
path_is_consistent (Node *n, Edge *e, PathOpData *opdata)
{
AreaClassification area_left = e->area_left;
AreaClassification area_right = e->area_right;
gboolean interior = e->interior;
gboolean forward = e->start == n;
do {
Node *n2 = forward ? e->end : e->start;
e = other_edge (n2, e);
if (e == NULL)
return TRUE;
if (n2 == n)
return TRUE;
/* FIXME: do this without changing e */
reset_classification (e);
classify_boundary (e, opdata);
if (e->area_left != area_left || e->area_right != area_right || e->interior != interior)
return FALSE;
} while (1);
}
/* Apply heuristic fixes to make the graph more consistent.
*
* First idea: If we find a path connecting two odd parity nodes,
* and we toggle the path from interior to boundary (or the other
* way around), then both nodes now have even parity. A good
* candidate for such a path is one where some of the edges
* disagree on classification (when explicitly computing it,
* that is - they will be set the the same classification via
* propagation at the outset).
*
* Second idea: Recomputing the classification of every edge
* around a node might yield different results (since some of
* the current edge classification are probably propagated).
*/
static void
apply_fixups (PathOpData *opdata)
{
GList *bad = NULL;
for (GList *l = opdata->nodes; l; l = l->next)
{
Node *c = l->data;
if (c->inconsistent)
bad = g_list_prepend (bad, c);
}
if (bad == NULL)
return;
#ifdef G_ENABLE_DEBUG
if (opdata->debug)
{
g_print ("found %d bad nodes\n", g_list_length (bad));
for (GList *l = bad; l; l = l->next)
{
Node *c = l->data;
dump_node (c);
}
}
#endif
while (bad != NULL)
{
Node *n1 = bad->data;
Node *n2 = NULL;
Edge *edge = NULL;
bad = g_list_remove (bad, n1);
check_consistency (n1);
if (!n1->inconsistent)
{
DEBUG (opdata, "inconsistency disappeared");
continue;
}
if (n1->inconsistent == 1)
{
for (GList *l = bad; l; l = l->next)
{
Edge *fallback_edge = NULL;
n2 = NULL;
for (unsigned int i = 0; i < n1->edges->len; i++)
{
Edge *e = g_ptr_array_index (n1->edges, i);
Node *other = other_end (n1, e);
if (other == n1)
{
/* Ignore loops, since they don't help us for parity */
continue;
}
if (other && other->inconsistent == 1)
{
if ((e->start == n1 && e->end == other) ||
(e->start == other && e->end == n1))
{
/* Just one hop, keep this one for fallback */
fallback_edge = e;
continue;
}
if (path_is_consistent (n1, e, opdata))
{
/* Not a good candidate */
continue;
}
/* We found a connection between two bad nodes
* where some of the edges along the way disagree on
* classification - a good candidate for fixups.
*/
bad = g_list_remove (bad, other);
n2 = other;
edge = e;
break;
}
}
if (n2 == NULL && fallback_edge != NULL)
{
edge = fallback_edge;
if (edge->start == n1)
n2 = edge->end;
else
n2 = edge->start;
bad = g_list_remove (bad, n2);
}
if (n2 != NULL)
break;
}
if (n2 != NULL)
{
/* If we have two neighboring nodes with an odd
* boundary count, we can try to fix it by toggling
* one of the connecting edges.
*/
if (n1->inconsistent == 1)
{
DEBUG (opdata, "found 2 odd boundary nodes");
DEBUG (opdata, "toggling %s from %s to %s",
edge->name,
edge->interior ? "interior" : "boundary",
edge->interior ? "boundary" : "interior");
edge->interior = !edge->interior;
propagate_changed_classification (edge, edge->start == n1);
check_minimal_consistency (n1);
}
}
}
if (n1->inconsistent == 1)
{
/* Try to reclassify.
* This may help if some of the classifications
* were propagated.
*
* FIXME: we should only keep these changes if
* the node is consistent afterwards.
*/
for (int i = 0; i < n1->edges->len; i++)
{
edge = g_ptr_array_index (n1->edges, i);
reset_classification (edge);
classify_boundary (edge, opdata);
}
propagate_changes (n1);
check_minimal_consistency (n1);
}
}
}
/* }}} */
/* {{{ Reassembly helpers */
static Edge *
find_next (PathOpData *opdata,
Edge *edge)
{
Node *c = edge->end;
Edge *next = NULL;
Edge *next_fallback = NULL;
guint idx;
guint dir;
g_assert (c != NULL);
g_assert (c->edges != NULL);
#ifdef G_ENABLE_DEBUG
if (opdata->debug)
{
g_print ("%s ends at: \n", edge->name);
dump_node (edge->end);
}
#endif
/* Edges are sorted counterclockwise by their tangent.
* We pick the next eligible edge to the left
* or to the right of curve, depending on whether
* the left or right is inside.
*/
g_ptr_array_find (c->edges, edge, &idx);
dir = edge->area_left == AREA_IN ? c->edges->len - 1 : 1;
DEBUG (opdata, "picking %s", edge->area_left == AREA_IN ? "cw" : "ccw");
for (int d = 0; d < c->edges->len; d++)
{
guint pos = (guint)((idx + dir * (d + 1)) % c->edges->len);
Edge *n = g_ptr_array_index (c->edges, pos);
float angle;
if (n->collected)
continue;
if (n->interior)
continue;
angle = n->end == c ? n->end_angle : n->start_angle;
if (fabs (angle - edge->end_angle) < 0.0001)
{
next_fallback = n;
continue;
}
next = n;
break;
}
if (next == NULL)
next = next_fallback;
if (next && next->end == c)
reverse_edge (next);
return next;
}
static void
reassemble_contours (PathOpData *opdata,
GskPathBuilder *builder)
{
GList *l;
for (l = opdata->edges; l; l = l->next)
{
Edge *edge = l->data;
Node *start;
if (edge->collected || edge->interior)
continue;
DEBUG (opdata, "start new contour %s", edge->name);
if (edge->area_left == AREA_OUT)
reverse_edge (edge);
start = edge->start;
gsk_path_builder_move_to (builder, start->p.x, start->p.y);
gsk_curve_builder_to (&edge->curve, builder);
edge->collected = TRUE;
/* collect segments, following through nodes */
for (edge = find_next (opdata, edge); edge; edge = find_next (opdata, edge))
{
g_assert (!edge->interior);
if (edge->collected)
{
g_warning ("find_next returned a collected edge, falling off");
break;
}
DEBUG (opdata, "append %s", edge->name);
gsk_curve_builder_to (&edge->curve, builder);
edge->collected = TRUE;
if (edge->curve.op == GSK_PATH_CLOSE)
{
DEBUG (opdata, "explicitly closed");
break;
}
if (edge->end == start)
{
DEBUG (opdata, "implicitly closed");
gsk_path_builder_close (builder);
break;
}
}
}
}
/* }}} */
/* {{{ Pathops implementation */
/*
* The general plan of operation is as follows:
*
* 1. Collect all the edges in a list.
*
* 2. Add all intersections, splitting the edges as needed,
* and keep Node structs that record which edges are meeting
* at which intersections. Remove coinciding edges.
*
* 3. Classify each edge as boundary or not. This is where
* the different boolean ops differ from each other.
*
* 4. Sort the edges at each node, counterclockwise.
*
* 5. Fix up the resulting graph.
*
* 6. Walk the graph, taking the proper turns at each node, to
* reassemble contours. Continue doing so until all boundary
* edges have been added to a contour.
*
* Note that mistakes can happen in various stages, due to rounding
* errors and approximations (e.g. GskPathMeasure is using linear
* approximation to determine whether a point is inside).
*
* We try to identify places where our graph is inconsistent by
* checking some invariants:
*
* a) At every node, an even number of boundary edges must meet
*
* b) Neighboring edges of a node must agree on the area between them
*
* We apply some heuristic fixes to patch up these inconsistencies.
*/
GskPath *
gsk_path_op (GskPathOp operation,
GskFillRule fill_rule,
GskPath *first,
GskPath *second)
{
PathOpData opdata;
GskPathBuilder *builder;
pathopdata_init (&opdata, operation,fill_rule, first, second);
builder = gsk_path_builder_new ();
DEBUG (&opdata, "collecting");
collect_edges (first, &opdata);
if (second)
collect_edges (second, &opdata);
opdata.edges = g_list_reverse (opdata.edges);
opdata.nodes = g_list_reverse (opdata.nodes);
#ifdef G_ENABLE_DEBUG
validate_edges (&opdata);
validate_nodes (&opdata);
#endif
DEBUG (&opdata, "splitting");
split_edges (&opdata);
#ifdef G_ENABLE_DEBUG
validate_edges (&opdata);
validate_nodes (&opdata);
#endif
DEBUG (&opdata, "classifying");
classify_edges (&opdata);
#ifdef G_ENABLE_DEBUG
validate_edges (&opdata);
validate_nodes (&opdata);
#endif
if (graph_has_inconsistencies (&opdata))
{
#ifdef PATH_OPS_DEBUG
dump_dotfile (opdata.edges, opdata.nodes, "inconsistent.dot");
if (opdata.debug)
{
for (GList *l = opdata.nodes; l; l = l->next)
{
Node *n = l->data;
if (n->inconsistent == 1)
gsk_path_builder_add_circle (builder, &n->p, 4);
else if (n->inconsistent == 2)
gsk_path_builder_add_circle (builder, &n->p, 2);
}
}
#endif
DEBUG (&opdata, "fixups");
apply_fixups (&opdata);
#ifdef PATH_OPS_DEBUG
dump_dotfile (opdata.edges, opdata.nodes, "after-fixups.dot");
#endif
}
DEBUG (&opdata, "reassembling");
reassemble_contours (&opdata, builder);
/* Cleanup */
pathopdata_clear (&opdata);
return gsk_path_remove_unclosed (gsk_path_builder_free_to_path (builder));
}
/* }}} */
/* vim:set foldmethod=marker expandtab: */