[dia] path: add binary path operations to <Selection> context menu



commit ff19fc77f59b1e89640492a5ab9968727df9650a
Author: Hans Breuer <hans breuer org>
Date:   Sun Aug 3 15:10:25 2014 +0200

    path: add binary path operations to <Selection> context menu
    
    If there is more than one object selected the context menu offers extra
    entries: Union, Difference, Intersection and Exclusion. These are implemented
    based on DiaPathRenderer and new path_combine() function. The latter still has
    some issues, but it's good enough to check with another compiler/build system.

 app/disp_callbacks.c     |   63 ++++-
 lib/Makefile.am          |    1 +
 lib/create.h             |    2 +
 lib/diapathrenderer.c    |   87 +++++-
 lib/diapathrenderer.h    |    3 +
 lib/diatypes.h           |    7 +
 lib/libdia.def           |    1 +
 lib/makefile.msc         |    1 +
 lib/object.c             |   20 +-
 lib/object.h             |    1 +
 lib/path-math.c          |  778 ++++++++++++++++++++++++++++++++++++++++++++++
 samples/path-combine.dia |  Bin 0 -> 34654 bytes
 12 files changed, 945 insertions(+), 19 deletions(-)
---
diff --git a/app/disp_callbacks.c b/app/disp_callbacks.c
index 305a858..750e86e 100644
--- a/app/disp_callbacks.c
+++ b/app/disp_callbacks.c
@@ -217,8 +217,63 @@ add_convert_to_path_menu_item (GtkMenu *menu)
   gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);
   gtk_widget_show(menu_item);
 }
+
+static void
+_combine_to_path_callback (GtkAction *action, gpointer data)
+{
+  DDisplay *ddisp = ddisplay_active();
+  GList *cut_list;
+  DiaObject *obj;
+  Diagram *dia;
+
+  if (!ddisp) return;
+  dia = ddisp->diagram;
+
+  diagram_selected_break_external(ddisp->diagram);
+  cut_list = parent_list_affected(diagram_get_sorted_selected(ddisp->diagram));
+  obj = create_standard_path_from_list (cut_list, GPOINTER_TO_INT (data));
+  if (obj) {
+    /* remove the objects just combined */
+    Change *change = undo_delete_objects_children(dia, cut_list);
+    (change->apply)(change, dia);
+    /* add the new object with undo */
+    undo_insert_objects(dia, g_list_prepend(NULL, obj), 1);
+    undo_set_transactionpoint(ddisp->diagram->undo);
+
+    diagram_add_object (dia, obj);
+    diagram_select(dia, obj);
+    object_add_updates(obj, dia);
+
+    ddisplay_do_update_menu_sensitivity(ddisp);
+    diagram_flush(dia);
+  } else {
+    g_list_free (cut_list);
+  }
+}
+static void
+add_combine_to_path_menu_items (GtkMenu *menu)
+{
+  struct {
+    const gchar    *name;
+    PathCombineMode mode;
+  } _ops[] = {
+    { N_("Union"), PATH_UNION }, 
+    { N_("Difference"), PATH_DIFFERENCE }, 
+    { N_("Intersection"), PATH_INTERSECTION }, 
+    { N_("Exclusion"), PATH_EXCLUSION }, 
+  };
+  int i;
+  for (i = 0; i < G_N_ELEMENTS (_ops); ++i) {
+    GtkWidget *menu_item = gtk_menu_item_new_with_label(_ops[i].name);
+    g_signal_connect(G_OBJECT(menu_item), "activate",
+                    G_CALLBACK(_combine_to_path_callback), GINT_TO_POINTER (_ops[i].mode));
+    gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);
+    gtk_widget_show(menu_item);
+  }
+}
+
 static void
-create_object_menu(DiaMenu *dia_menu, gboolean root_menu)
+create_object_menu(DiaMenu *dia_menu, gboolean root_menu, gboolean combining)
 {
   int i;
   GtkWidget *menu;
@@ -273,7 +328,7 @@ create_object_menu(DiaMenu *dia_menu, gboolean root_menu)
              * DiaMenu pointer for the submenu. */
         if ( ((DiaMenu*)item->callback_data)->app_data == NULL ) {
               /* Create the popup menu items for the submenu. */
-          create_object_menu((DiaMenu*)(item->callback_data), FALSE) ;
+          create_object_menu((DiaMenu*)(item->callback_data), FALSE, FALSE) ;
           gtk_menu_item_set_submenu( GTK_MENU_ITEM (menu_item), 
                                      GTK_WIDGET(((DiaMenu*)(item->callback_data))->app_data));
         }
@@ -287,6 +342,8 @@ create_object_menu(DiaMenu *dia_menu, gboolean root_menu)
     add_follow_link_menu_item(GTK_MENU (menu));
     if (!native_convert_to_path)
       add_convert_to_path_menu_item(GTK_MENU (menu));
+    if (combining)
+      add_combine_to_path_menu_items (GTK_MENU (menu));
   }
 
   dia_menu->app_data = menu;
@@ -353,7 +410,7 @@ popup_object_menu(DDisplay *ddisp, GdkEvent *event)
   }
 
   if (dia_menu->app_data == NULL) {
-    create_object_menu(dia_menu, TRUE);
+    create_object_menu(dia_menu, TRUE, g_list_length (diagram->data->selected) > 1);
     /* append the Input Methods menu, if there is canvas editable text */
     if (obj && focus_get_first_on_object(obj) != NULL) {
       GtkWidget *menuitem = gtk_menu_item_new_with_mnemonic (_("Input _Methods"));
diff --git a/lib/Makefile.am b/lib/Makefile.am
index 1475d5c..fc1590c 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -171,6 +171,7 @@ libdia_la_SOURCES =  \
                diagdkrenderer.c \
                diapathrenderer.h \
                diapathrenderer.c \
+               path-math.c \
                diapatternselector.h \
                diapatternselector.c \
                diatransformrenderer.h \
diff --git a/lib/create.h b/lib/create.h
index ad8b01a..37065f0 100644
--- a/lib/create.h
+++ b/lib/create.h
@@ -142,6 +142,8 @@ DiaObject *create_standard_group(GList *items);
 
 DiaObject *create_standard_path_from_object (DiaObject *obj);
 
+DiaObject *create_standard_path_from_list (GList *objects, PathCombineMode  mode);
+
 G_END_DECLS
 
 #endif
diff --git a/lib/diapathrenderer.c b/lib/diapathrenderer.c
index 12f0700..8786f69 100644
--- a/lib/diapathrenderer.c
+++ b/lib/diapathrenderer.c
@@ -72,15 +72,10 @@ dia_path_renderer_init (DiaPathRenderer *self)
   self->stroke = attributes_get_foreground ();
   self->fill = attributes_get_background ();
 }
-/*!
- * \brief Destructor
- * If there are still pathes left, deallocate them
- */
+
 static void
-dia_path_renderer_finalize (GObject *object)
+_path_renderer_clear (DiaPathRenderer *self)
 {
-  DiaPathRenderer *self = DIA_PATH_RENDERER (object);
-
   if (self->pathes) {
     guint i;
    
@@ -92,6 +87,18 @@ dia_path_renderer_finalize (GObject *object)
     g_ptr_array_free (self->pathes, TRUE);
     self->pathes = NULL;
   }
+}
+/*!
+ * \brief Destructor
+ * If there are still paths left, deallocate them
+ * \memberof _DiaPathRenderer
+ */
+static void
+dia_path_renderer_finalize (GObject *object)
+{
+  DiaPathRenderer *self = DIA_PATH_RENDERER (object);
+
+  _path_renderer_clear (self);
   G_OBJECT_CLASS (dia_path_renderer_parent_class)->finalize (object);
 }
 
@@ -566,8 +573,6 @@ _bezier (DiaRenderer *self,
   }
   for (; i < numpoints; ++i)
     g_array_append_val (path, points[i]);
-  if (fill) /* might not be necessary anymore with draw_beziergon*/
-    _path_lineto (path, &points[0].p1);
 }
 /*!
  * \brief Create path from bezier line
@@ -713,9 +718,14 @@ draw_rounded_rect (DiaRenderer *self,
     radius = rx;
   if (radius > ry)
     radius = ry;
-  DIA_RENDERER_CLASS(dia_path_renderer_parent_class)->draw_rounded_rect(self, 
-                                                       ul_corner, lr_corner,
-                                                       NULL, stroke ? stroke : fill, radius);
+  if (stroke) /* deliberately ignoring fill for path building */
+    DIA_RENDERER_CLASS(dia_path_renderer_parent_class)->draw_rounded_rect(self, 
+                                                                         ul_corner, lr_corner,
+                                                                         NULL, stroke, radius);
+  else
+    DIA_RENDERER_CLASS(dia_path_renderer_parent_class)->draw_rounded_rect(self, 
+                                                                         ul_corner, lr_corner,
+                                                                         fill, NULL, radius);
   /* stroke is set by the piecewise rendering above already */
   if (fill)
     renderer->fill = *fill;
@@ -807,7 +817,6 @@ create_standard_path_from_object (DiaObject *obj)
     for (i = 0; i < pr->pathes->len; ++i) {
       GArray *points = g_ptr_array_index (pr->pathes, i);
       DiaObject *obj;
-
       if (points->len < 2)
        obj = NULL;
       else
@@ -829,3 +838,55 @@ create_standard_path_from_object (DiaObject *obj)
 
   return path;
 }
+
+/*!
+ * \brief Combine the list of object into a new path object
+ *
+ * Every single object get into a single path which gets combined
+ * with the following object. The result of that operation than gets
+ * combined again with the following object.
+ */
+DiaObject *
+create_standard_path_from_list (GList           *objects,
+                               PathCombineMode  mode)
+{
+  DiaObject *path;
+  DiaRenderer *renderer;
+  DiaPathRenderer *pr;
+  GList *list;
+  GArray *p1 = NULL, *p2 = NULL;
+
+  renderer = g_object_new (DIA_TYPE_PATH_RENDERER, 0);
+  pr = DIA_PATH_RENDERER (renderer);
+
+  for (list = objects; list != NULL; list = list->next) {
+    DiaObject *obj = list->data;
+    int i;
+
+    _path_renderer_clear (pr);
+    obj->ops->draw (obj, renderer);
+    /* get a single path from this rendererer run */
+    p2 = g_array_new (FALSE, FALSE, sizeof(BezPoint));
+    for (i = 0; i < pr->pathes->len; ++i) {
+      GArray *points = g_ptr_array_index (pr->pathes, i);
+      g_array_append_vals (p2, &g_array_index (points, BezPoint, 0), points->len);
+    }
+    if (p1 && p2) {
+      GArray *combined = path_combine (p1, p2, mode);
+      if (combined) {
+       g_array_free (p1, TRUE);
+       p1 = combined;
+       g_array_free (p2, TRUE);
+       p2 = NULL;
+      }
+    } else {
+      p1 = p2;
+      p2 = NULL;
+    }
+  }
+  path = create_standard_path (p1->len, &g_array_index (p1, BezPoint, 0));
+  /* copy style from first object processed */
+  object_copy_style (path, (DiaObject *)objects->data);
+  g_array_free (p1, TRUE);
+  return path;
+}
diff --git a/lib/diapathrenderer.h b/lib/diapathrenderer.h
index a679f03..f94ef85 100644
--- a/lib/diapathrenderer.h
+++ b/lib/diapathrenderer.h
@@ -47,4 +47,7 @@ void path_build_ellipse (GArray *path,
                         Point *center,
                         real width, real height);
 
+/* in path-math.c */
+GArray *path_combine (const GArray *p1, const GArray *p2, PathCombineMode mode);
+
 #endif
diff --git a/lib/diatypes.h b/lib/diatypes.h
index 35b38fc..4748c46 100644
--- a/lib/diatypes.h
+++ b/lib/diatypes.h
@@ -193,4 +193,11 @@ typedef struct _DiaFileSelectorClass  DiaFileSelectorClass;
 /** A standard definition of a function that takes a DiaObject */
 typedef void (*DiaObjectFunc) (const DiaObject *obj);
 
+typedef enum {
+  PATH_UNION = 1,
+  PATH_DIFFERENCE,
+  PATH_INTERSECTION,
+  PATH_EXCLUSION
+} PathCombineMode;
+
 #endif
diff --git a/lib/libdia.def b/lib/libdia.def
index 38d1f65..3afaa56 100644
--- a/lib/libdia.def
+++ b/lib/libdia.def
@@ -120,6 +120,7 @@ EXPORTS
  create_standard_group
  create_standard_image
  create_standard_path
+ create_standard_path_from_list
  create_standard_path_from_object
  create_standard_path_from_text
  create_standard_polygon
diff --git a/lib/makefile.msc b/lib/makefile.msc
index 7727296..01ba2ab 100644
--- a/lib/makefile.msc
+++ b/lib/makefile.msc
@@ -96,6 +96,7 @@ OBJECTS = \
        orth_conn.obj \
        paper.obj \
        parent.obj \
+       path-math.obj \
        pattern.obj \
        persistence.obj \
        plug-ins.obj \
diff --git a/lib/object.c b/lib/object.c
index 5e055b9..5ef482c 100644
--- a/lib/object.c
+++ b/lib/object.c
@@ -373,6 +373,20 @@ static PropDescription _style_prop_descs[] = {
   PROP_DESC_END
 };
 
+void
+object_copy_style (DiaObject *dest, const DiaObject *src)
+{
+  GPtrArray *props;
+
+  g_return_if_fail (src && src->ops->get_props != NULL);
+  g_return_if_fail (dest && dest->ops->set_props != NULL);
+
+  props = prop_list_from_descs (_style_prop_descs, pdtpp_true);
+  src->ops->get_props((DiaObject *)src, props);
+  dest->ops->set_props(dest, props);
+  prop_list_free(props);
+}
+
 static void
 _object_exchange (ObjectChange *change, DiaObject *obj)
 {
@@ -427,8 +441,8 @@ _object_exchange (ObjectChange *change, DiaObject *obj)
     parent_object->children = g_list_insert_before (parent_object->children, sibling, subst);
     parent_object->children = g_list_remove (parent_object->children, obj);
   }
-  /* apply style properties */
-  if (subst->ops->get_props)
+  /* apply style properties - only if it's not restore of original */
+  if (subst->ops->get_props && subst != c->orig)
     subst->ops->set_props(subst, props);
   prop_list_free(props);
   /* adding to the diagram last, to have the right update areas */
@@ -473,7 +487,7 @@ _object_exchange_free (ObjectChange *change)
  * \brief Replace an object with another one
  *
  * The type of an object can not change dynamically. To substitute one
- * object with another this function help. It does it's best to transfer
+ * object with another this function helps. It does it's best to transfer
  * all the existing object relations, e.g. connections, parent_layer 
  * and parenting information.
  *
diff --git a/lib/object.h b/lib/object.h
index 968305a..fb57e5d 100644
--- a/lib/object.h
+++ b/lib/object.h
@@ -600,6 +600,7 @@ void          object_save_props(DiaObject *obj, ObjectNode obj_node, DiaContext
    same type) */
 void          object_copy_props(DiaObject *dest, const DiaObject *src,
                                 gboolean is_default);
+void          object_copy_style(DiaObject *dest, const DiaObject *src);
 
 G_END_DECLS
 
diff --git a/lib/path-math.c b/lib/path-math.c
new file mode 100644
index 0000000..90b1e58
--- /dev/null
+++ b/lib/path-math.c
@@ -0,0 +1,778 @@
+/* Dia -- an diagram creation/manipulation program
+ * Copyright (C) 1998 Alexander Larsson
+ *
+ * path-math.c -- some helper function for binary path operations
+ * Copyright (C) 2014, Hans Breuer <Hans Breuer Org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+#include <config.h>
+
+#include "geometry.h"
+#include "boundingbox.h"
+
+#include <string.h> /* memcmp() */
+
+/*!
+ * \brief Take a path and calculate it to non overlapping pieces
+ */
+
+typedef struct _BezierSegment BezierSegment; 
+struct _BezierSegment {
+  Point p0;
+  Point p1;
+  Point p2;
+  Point p3;
+};
+
+/*!
+ * \brief Split a bezier segment into left and right half 
+ */
+static void
+bezier_split (const BezierSegment *a,
+             BezierSegment *a1,
+             BezierSegment *a2)
+{
+  /* see: Foley et al., Computer Graphics, p508 */
+  Point L2, L3, L4, H, R2, R3;
+  /* P1 = L1 */
+  a1->p0 = a->p0;
+  /* L2 = (P1 + P2) / 2 */
+  L2.x = (a->p0.x + a->p1.x) / 2;
+  L2.y = (a->p0.y + a->p1.y) / 2;
+  /* H = (P2 + P3) / 2 */
+  H.x = (a->p1.x + a->p2.x) / 2;
+  H.y = (a->p1.y + a->p2.y) / 2;
+  /* R3 = (P3 + P4) / 2 */
+  R3.x = (a->p2.x + a->p3.x) / 2;
+  R3.y = (a->p2.y + a->p3.y) / 2;
+  /* L3 = (L2 + H) / 2 */
+  L3.x = (L2.x + H.x) / 2;
+  L3.y = (L2.y + H.y) / 2;
+  /* R2 = (H + R3) / 2 */
+  R2.x = (H.x + R3.x) / 2;
+  R2.y = (H.y + R3.y) / 2;
+
+  a1->p1 = L2;
+  a1->p2 = L3;
+  /* L4 = R1 = (L3 + R2) / 2 */
+  L4.x = (L3.x + R2.x) / 2;
+  L4.y = (L3.y + R2.y) / 2;
+  a1->p3 = a2->p0 = L4;
+  a2->p1 = R2;
+  a2->p2 = R3;
+  /* P4 = R4 */
+  a2->p3 = a->p3;
+}
+
+static void
+bezier_split_at (const BezierSegment *a,
+                BezierSegment *a1,
+                BezierSegment *a2,
+                real split)
+{
+  real left = 1.0 - split;
+  real right = split;
+  /* see: Foley et al., Computer Graphics, p508 */
+  Point L2, L3, L4, H, R2, R3;
+  /* P1 = L1 */
+  a1->p0 = a->p0;
+  /* L2 = (P1 + P2) / 2 */
+  L2.x = (a->p0.x * left + a->p1.x * right);
+  L2.y = (a->p0.y * left + a->p1.y * right);
+  /* H = (P2 + P3) / 2 */
+  H.x = (a->p1.x * left + a->p2.x * right);
+  H.y = (a->p1.y * left + a->p2.y * right);
+  /* R3 = (P3 + P4) / 2 */
+  R3.x = (a->p2.x * left + a->p3.x * right);
+  R3.y = (a->p2.y * left + a->p3.y * right);
+  /* L3 = (L2 + H) / 2 */
+  L3.x = (L2.x * left + H.x * right);
+  L3.y = (L2.y * left + H.y * right);
+  /* R2 = (H + R3) / 2 */
+  R2.x = (H.x * left + R3.x * right);
+  R2.y = (H.y * left + R3.y * right);
+
+  a1->p1 = L2;
+  a1->p2 = L3;
+  /* L4 = R1 = (L3 + R2) / 2 */
+  L4.x = (L3.x * left + R2.x * right);
+  L4.y = (L3.y * left + R2.y * right);
+  a1->p3 = a2->p0 = L4;
+  a2->p1 = R2;
+  a2->p2 = R3;
+  /* P4 = R4 */
+  a2->p3 = a->p3;
+}
+
+typedef struct _Intersection Intersection;
+struct _Intersection {
+  Point pt;        /*!< the crossing point */
+  real  split_one; /*!< 0..1: relative placement on segment one */
+  real  split_two; /*!< 0..1: relative placement on segment two */
+  int   seg_one;   /*!< index of the segment */
+  int   seg_two;   /*!< index of the segment */
+};
+
+static gboolean
+_segment_has_point (const BezierSegment *bs,
+                   const Point *pt)
+{
+  BezPoint bp;
+  real dist;
+  bp.type = BEZ_CURVE_TO;
+  bp.p1 = bs->p1;
+  bp.p2 = bs->p2;
+  bp.p3 = bs->p3;
+  dist = distance_bez_seg_point (&bs->p0, &bp, 0, pt);
+
+  return dist <= 0.0;
+}
+
+static gboolean
+_segment_is_moveto (const BezierSegment *bs)
+{
+  if (   memcmp (&bs->p0, &bs->p1, sizeof (Point)) == 0
+      && memcmp (&bs->p0, &bs->p2, sizeof (Point)) == 0
+      && memcmp (&bs->p0, &bs->p3, sizeof (Point)) == 0)
+    return TRUE;
+  return FALSE;
+}
+
+/* search precision */
+static const real EPSILON = 0.0001;
+
+/*!
+ * \brief Calculate crossing points of two bezier segments
+ *
+ * Beware two bezier segments can intersect more than once, but this
+ * function only returns the first or no intersection. It is the
+ * responsibility of the caller to further split segments until there
+ * is no intersection left.
+ */
+static gboolean
+bezier_bezier_intersection (GArray *crossing,
+                           const BezierSegment *a,
+                           const BezierSegment *b,
+                           int depth,
+                           real asplit,
+                           real bsplit)
+{
+  Rectangle abox, bbox;
+  PolyBBExtras extra = { 0, };
+  gboolean small_a, small_b;
+
+  /* Avoid intersection overflow: if start and end are on the other segment
+   * assume full overlap and no crossing.
+   */
+  if (   (_segment_has_point (a, &b->p0) && _segment_has_point (a, &b->p3))
+      || (_segment_has_point (b, &a->p0) && _segment_has_point (b, &a->p3)))
+    return FALSE; /* XXX: more variants pending, partial overlap */
+
+  /* With very similar segments we would create a lot of points with not
+   * a very deep recursion (test with ying-yang symbol).
+   * Just comparing the segments on depth=1 is not good enough, so for
+   * now we are limiting the number of intersections
+   */
+  if (crossing->len > 127) { /* XXX: arbitrary limit */
+    g_warning ("Crossing limit (%d) reached", crossing->len);
+    return FALSE;
+  }
+
+  bicubicbezier2D_bbox (&a->p0, &a->p1, &a->p2, &a->p3, &extra, &abox);
+  bicubicbezier2D_bbox (&b->p0, &b->p1, &b->p2, &b->p3, &extra, &bbox);
+
+  if (!rectangle_intersects (&abox, &bbox))
+    return FALSE;
+  small_a = (abox.right - abox.left) < EPSILON && (abox.bottom - abox.top) < EPSILON;
+  small_b = (bbox.right - bbox.left) < EPSILON && (bbox.bottom - bbox.top) < EPSILON;
+  /* if the boxes are small enough we can calculate the point */
+  if (small_a && small_b) {
+    /* intersecting and both small, should not matter which one is used */
+    Point pt = { (bbox.right + bbox.left) / 2, (bbox.bottom + bbox.top) / 2 };
+    Intersection is;
+    int i;
+    for (i = 0; i < crossing->len; ++i) {
+      /* if it's already included we are done */
+      if (distance_point_point (&g_array_index (crossing, Intersection, i).pt, &pt) < 1.4142*EPSILON)
+        return TRUE; /* although we did not add it */
+    }
+    is.split_one = asplit;
+    is.split_two = bsplit;
+    is.pt = pt;
+    g_print ("d=%d; as=%g; bs=%g; ", depth, asplit, bsplit);
+    g_array_append_val (crossing, is);
+    return TRUE;
+  } else {
+    /* further splitting of a and b; it could be smart to only search in the
+     * intersection of a-box and b-box ... */
+    BezierSegment a1, a2;
+    BezierSegment b1, b2;
+    real ofs = 1.0/(1<<(depth+1));
+    gboolean ret = FALSE;
+
+    bezier_split (a, &a1, &a2);
+    bezier_split (b, &b1, &b2);
+
+    ret |= bezier_bezier_intersection (crossing, &a1, &b1, depth+1, asplit-ofs, bsplit-ofs);
+    ret |= bezier_bezier_intersection (crossing, &a2, &b1, depth+1, asplit+ofs, bsplit-ofs);
+    ret |= bezier_bezier_intersection (crossing, &a1, &b2, depth+1, asplit-ofs, bsplit+ofs);
+    ret |= bezier_bezier_intersection (crossing, &a2, &b2, depth+1, asplit+ofs, bsplit+ofs);
+    /* XXX: check !ret case, not sure if it should happen */
+    return ret;
+  }
+}
+
+static gboolean
+_segment_from_path (BezierSegment *a, const GArray *p1, int i)
+{
+  const BezPoint *abp0 = &g_array_index (p1, BezPoint, i-1);
+  const BezPoint *abp1 = &g_array_index (p1, BezPoint, i);
+  a->p0 = abp0->type == BEZ_CURVE_TO ? abp0->p3 : abp0->p1;
+  switch (abp1->type) {
+  case BEZ_CURVE_TO :
+    a->p1 = abp1->p1; a->p2 = abp1->p2; a->p3 = abp1->p3;
+    break;
+  case BEZ_LINE_TO :
+    if (distance_point_point (&a->p0, &abp1->p1) < EPSILON)
+      return FALSE; /* avoid a zero length line-to for confusion with move-to */
+    a->p1 = a->p2 = a->p3 = abp1->p1;
+    break;
+  case BEZ_MOVE_TO :
+    a->p0 = a->p1 = a->p2 = a->p3 = abp1->p1;
+    break;
+  }
+  return TRUE;
+}
+
+static void
+_curve_from_segment (BezPoint *bp, const BezierSegment *a, gboolean flip)
+{
+  if (_segment_is_moveto (a))
+    bp->type = BEZ_MOVE_TO;
+  else if (memcmp (&a->p1, &a->p2, sizeof(Point)) == 0)
+    bp->type = BEZ_LINE_TO;
+  else
+    bp->type = BEZ_CURVE_TO;
+  if (!flip) {
+    bp->p1 = a->p1;
+    bp->p2 = a->p2;
+    bp->p3 = a->p3;
+  } else {
+    bp->p1 = a->p2;
+    bp->p2 = a->p1;
+    bp->p3 = a->p0;
+  }
+}
+
+typedef struct _Split Split;
+struct _Split {
+  Point    pt;      /*!< the position of the split */
+  int      seg;     /*!< the index of the segment to split */
+  real     split;   /*!< 0..1: relative placement on segment */
+  gboolean used;    /*!< marked during _make_path() */
+  gboolean outside; /*!< not inside the other path */
+  GArray  *path;    /*!< subpath copy */
+};
+
+static GArray *
+_extract_splits (const GArray *crossing, gboolean one)
+{
+  GArray *result = g_array_new (FALSE, FALSE, sizeof(Split));
+  int i;
+  for (i = 0; i < crossing->len; ++i) {
+    Split sp = { 0 };
+    sp.pt = g_array_index (crossing, Intersection, i).pt;
+    if (one) {
+      sp.seg = g_array_index (crossing, Intersection, i).seg_one;
+      sp.split = g_array_index (crossing, Intersection, i).split_one;
+    } else {
+      sp.seg = g_array_index (crossing, Intersection, i).seg_two;
+      sp.split = g_array_index (crossing, Intersection, i).split_two;
+    }
+    sp.used = FALSE;
+    g_array_append_val (result, sp);
+  }
+  return result;
+}
+
+static GArray *
+_path_to_segments (const GArray *path)
+{
+  GArray *segs = g_array_new (FALSE, FALSE, sizeof(BezierSegment));
+  BezierSegment bs;
+  int i;
+  BezPoint *last_move = &g_array_index (path, BezPoint, 0);
+
+  for (i = 1; i < path->len; ++i) {
+    if (g_array_index (path, BezPoint, i).type == BEZ_MOVE_TO)
+      last_move = &g_array_index (path, BezPoint, i);
+    if (_segment_from_path (&bs, path, i))
+      g_array_append_val (segs, bs);
+  }
+  /* if the path is not closed do an explicit line-to */
+  if (memcmp (&last_move->p1, &bs.p3, sizeof(Point)) != 0) {
+    bs.p0 = bs.p3;
+    bs.p1 = bs.p2 = bs.p3 = last_move->p1;
+    g_array_append_val (segs, bs);
+  }
+
+  return segs;
+}
+
+/* GCompareFunc to sort Split */
+static gint
+_compare_split (gconstpointer as, gconstpointer bs)
+{
+  const Split *a = as;
+  const Split *b = bs;
+  if (a->seg > b->seg)
+    return 1;
+  if (a->seg < b->seg)
+    return -1;
+  if (a->split > b->split)
+    return 1;
+  if (a->split < b->split)
+    return -1;
+  return 0;
+}
+
+/*!
+ * Given the original segments and splits apply
+ * all segment splits and create unique segment index.
+ *
+ * Split.seg is the index to the segment to split before this function.
+ * After the split it indexes the last segment in the row of splits.
+ */
+static void
+_split_segments (GArray *segs, GArray *splits, const GArray *other)
+{
+  int i, sofs = 0;
+  GArray *pending;
+
+  /* splits must be sorted for the algorithm below */
+  g_array_sort (splits, _compare_split);
+
+  for (i = 0; i < splits->len; ++i) {
+    int j, to;
+    int from = i;
+    int from_seg = g_array_index (splits, Split, i).seg;
+    BezierSegment bs;
+    real t = 0;
+
+    g_return_if_fail (from_seg + sofs < segs->len);
+    bs = g_array_index (segs, BezierSegment, from_seg + sofs);
+    while (i < splits->len - 1 && from_seg == g_array_index (splits, Split, i+1).seg)
+      ++i; /* advance while segment reference is the same */
+    to = i;
+    for (j = from; j <= to; j++) {
+      BezierSegment left, right;
+      /* scale t to split the right segment */
+      real tL = g_array_index (splits, Split, j).split;
+      real tR = (tL - t) / (1.0 - t);
+      bezier_split_at (&bs, &left, &right, tR);
+      bs = right;
+      t = tL;
+      /* overwrite the exisiting */
+      g_return_if_fail (from_seg + sofs < segs->len);
+      g_array_index (segs, BezierSegment, from_seg + sofs) = left;
+      sofs += 1; /* increment segment offset for every segment added */
+      /* insert a new one behind that ... */
+      g_array_insert_val (segs, from_seg + sofs, right); /* ... potentially overwritten */
+      /* adjust the segment reference */
+      g_array_index (splits, Split, j).seg = from_seg + sofs;
+    }
+  }
+  pending = g_array_new (FALSE, FALSE, sizeof(BezierSegment));
+  /* for every sub-path determine if it is inside the full other path */
+  for (i = 0; i < splits->len; ++i) {
+    Split *sp = &g_array_index (splits, Split, i);
+    BezierSegment *bs = &g_array_index (segs, BezierSegment, sp->seg);
+    BezierSegment left, right;
+    Point pt;
+    int to, j;
+
+    if (i == 0 && sp->seg > 0)
+      g_array_append_vals (pending, &g_array_index (segs, BezierSegment, 0), sp->seg);
+
+    bezier_split (bs, &left, &right);
+    pt = right.p0;
+    sp->outside = distance_bez_shape_point (&g_array_index (other, BezPoint, 0), other->len,
+                                          0 /* line width */, &pt) > 0.0;
+    /* also remember the sub-path */
+    to = g_array_index (splits, Split, (i+1)%splits->len).seg;
+    sp->path = g_array_new (FALSE, FALSE, sizeof(BezierSegment));
+    if (to < sp->seg) {
+      g_array_append_vals (sp->path, bs, segs->len - sp->seg);
+#if 0
+      /* XXX: this is only correct if there is no move-to within the segments */
+      g_array_append_vals (sp->path, &g_array_index (segs, BezierSegment, 0), to);
+#else
+      g_array_append_vals (sp->path, &g_array_index (pending, BezierSegment, 0), pending->len);
+      g_array_set_size (pending, 0);
+#endif
+    } else {
+      for (j = sp->seg; j < to; ++j) {
+       if (_segment_is_moveto (bs)) {
+         g_array_append_vals (sp->path, &g_array_index (pending, BezierSegment, 0), pending->len);
+         g_array_set_size (pending, 0);
+         break;
+       }
+       g_array_append_val (sp->path, *bs);
+       bs++;
+      }
+      for (/* remains */; j < to; ++j) {
+       g_array_append_val (pending, *bs);
+       bs++;
+      }
+    }
+  }
+  g_array_free (pending, TRUE);
+}
+
+static void
+_free_splits (GArray *splits)
+{
+  int i;
+
+  g_return_if_fail (splits != NULL);
+
+  for (i = 0; i < splits->len; ++i) {
+    Split *sp = &g_array_index (splits, Split, i);
+    if (sp->path)
+      g_array_free (sp->path, TRUE);
+  }
+  g_array_free (splits, TRUE);
+}
+
+static GArray *
+_find_intersections (GArray *one, GArray *two)
+{
+  GArray *crossing = g_array_new (FALSE, FALSE, sizeof(Intersection));
+  int i, j, k;
+
+  /* find intersections */
+  for (i = 0; i < one->len; ++i) {
+    BezierSegment a = g_array_index (one, BezierSegment, i);
+    for (j = 0; j < two->len; ++j) {
+      BezierSegment b = g_array_index (two, BezierSegment, j);
+      int start = crossing->len;
+      if (bezier_bezier_intersection (crossing, &a, &b, 1, 0.5, 0.5)) {
+       /* Found intersection splits a and b into left and right.
+        * Any segment might be split more than once so seg_one and seg_two
+        * are not unique yet. Also the calculated split always refers to
+        * the whole segment. We could avoid the _split_segments() below by
+        * modifying `one� and `two� in place. But instead of later
+        * segmentation that would complicate the seq_* reference _and_ give
+        * worse runtime behavior because we would need to start over with
+        * every intersection found.
+        */
+       for (k = start; k < crossing->len; ++k) {
+         Intersection *is = &g_array_index (crossing, Intersection, k);
+         is->seg_one = i;
+         is->seg_two = j;
+       }
+        g_print ("with a:b %d:%d\n", i, j);
+      }
+    }
+  }
+
+  if (crossing->len > 0)
+    return crossing;
+  g_array_free (crossing, TRUE);
+  return NULL;
+}
+
+/*!
+ * \brief Find the next sub path to connect
+ *
+ * Ignores the crossing point of the Split, but just looks at the
+ * start and end of the given sub path.
+ */
+static gboolean
+_find_split (GArray *splits, Point *pt, gboolean outside, Split **next)
+{
+  int i;
+
+  for (i = 0; i < splits->len; ++i) {
+    Split *sp = &g_array_index (splits, Split, i);
+    /* one of two splits - prefer the one matching in start point */
+    BezierSegment *bs = &g_array_index (sp->path, BezierSegment, 0);
+    if (   !sp->used
+       && (sp->outside == outside)
+       && distance_point_point (&bs->p0, pt) < 1.4142 * EPSILON) {
+      *next = sp;
+      sp->used = TRUE;
+      return TRUE;
+    }
+  }
+  /* but also deliver segments ending in pt */
+  for (i = 0; i < splits->len; ++i) {
+    Split *sp = &g_array_index (splits, Split, i);
+    BezierSegment *bs = &g_array_index (sp->path, BezierSegment, sp->path->len - 1);
+    if (   !sp->used
+       && (sp->outside == outside)
+        && distance_point_point (&bs->p3, pt) < 1.4142 * EPSILON) {
+      *next = sp;
+      sp->used = TRUE;
+      return TRUE;
+    }
+  }
+  return FALSE;
+}
+
+static Point
+_append_segments (GArray  *path,
+                 GArray  *segs)
+{
+  BezPoint bp;
+  int      i;
+  gboolean flip;
+  BezPoint *ebp = &g_array_index (path, BezPoint, path->len - 1);
+  const BezierSegment *sseg = &g_array_index (segs, BezierSegment, 0);
+  const BezierSegment *eseg = &g_array_index (segs, BezierSegment, segs->len - 1);
+
+  /* always try to join with what we have */
+  if (distance_point_point (&sseg->p0,
+      ebp->type == BEZ_CURVE_TO ? &ebp->p3 : &ebp->p1) < EPSILON) {
+    /* matching in given direction */
+    flip = FALSE;
+  } else if (distance_point_point (&eseg->p3,
+            ebp->type == BEZ_CURVE_TO ? &ebp->p3 : &ebp->p1) < EPSILON) {
+    /* change direction of segments */
+    flip = TRUE;
+  } else {
+    /* neither matches so we can use any direction but should add a move-to */
+    bp.type = BEZ_MOVE_TO;
+    bp.p1 = sseg->p0;
+    g_array_append_val (path, bp);
+  }
+
+  if (flip) {
+    for (i = segs->len - 1; i >= 0; --i) { /* counting down - backwards append */
+      _curve_from_segment (&bp, &g_array_index (segs, BezierSegment, i), flip);
+      if (bp.type != BEZ_MOVE_TO) /* just ignore move-to here */
+       g_array_append_val (path, bp);
+    }
+  } else {
+    for (i = 0; i < segs->len; ++i) { /* preserve original direction */
+      _curve_from_segment (&bp, &g_array_index (segs, BezierSegment, i), flip);
+      if (bp.type != BEZ_MOVE_TO)
+       g_array_append_val (path, bp);
+    }
+  }
+  ebp = &g_array_index (path, BezPoint, path->len - 1);
+  return ebp->type == BEZ_CURVE_TO ? ebp->p3 : ebp->p1;
+}
+/*!
+ * \brief Just reassamble both paths again for debugging
+ */
+static GArray *
+_make_path0 (GArray *one, /*!< array<BezierSegment> from first path */
+            GArray *one_splits, /*!< array<Split> for first path */
+            GArray *two, /*!< second path */
+            GArray *two_splits /*!< splits */
+            )
+{
+  GArray *result = g_array_new (FALSE, FALSE, sizeof(BezPoint));
+  int sel;
+  for (sel = 0; sel < 2; ++sel) {
+    GArray *segs = sel == 0 ? one : two;
+    GArray *splits = sel == 0 ? one_splits : two_splits;
+    int i, isp = 0;
+    BezPoint bp;
+    bp.type = BEZ_MOVE_TO;
+    bp.p1 = g_array_index (segs, BezierSegment, 0).p0;
+    g_array_append_val (result, bp);
+    for (i = 0; i < segs->len; ++i) {
+      BezierSegment *seg = &g_array_index (segs, BezierSegment, i);
+      /* every split starts with a move-to */
+      if (   isp < splits->len
+         && 0
+         && i == g_array_index (splits, Split, isp).seg
+         && g_array_index (result, BezPoint, result->len - 1).type != BEZ_MOVE_TO) {
+       bp.type = BEZ_MOVE_TO;
+       bp.p1 = seg->p0;
+       g_array_append_val (result, bp);
+       ++isp;
+      }
+      _curve_from_segment (&bp, seg, FALSE);
+      g_array_append_val (result, bp);
+
+    }
+  }
+  return result;
+}
+/*!
+ * \brief Another reassambling for debugging
+ */
+static GArray *
+_make_path1 (GArray *one, /*!< array<BezierSegment> from first path */
+            GArray *one_splits, /*!< array<Split> for first path */
+            GArray *two, /*!< second path */
+            GArray *two_splits /*!< splits */
+            )
+{
+  GArray *result = g_array_new (FALSE, FALSE, sizeof(BezPoint));
+  int i, sel;
+
+  for (sel = 0; sel < 2; ++sel) {
+    GArray *splits = sel == 0 ? one_splits : two_splits;
+    for (i = 0; i < splits->len; ++i) {
+      Split *sp = &g_array_index (splits, Split, i);
+
+      if (i == 0) { /* must at least start with move-to */
+       BezPoint bp;
+       bp.type = BEZ_MOVE_TO;
+       bp.p1 = g_array_index (sp->path, BezierSegment, 0).p0;
+       g_array_append_val (result, bp);
+      }
+      _append_segments (result, sp->path);
+    }
+  }
+  return result;
+}
+/*!
+ * \brief Convert back to a single BezPoint path
+ */
+static GArray *
+_make_path (GArray *one, /*!< array<BezierSegment> from first path */
+           GArray *one_splits, /*!< array<Split> for first path */
+           GArray *two, /*!< second path */
+           GArray *two_splits, /*!< splits */
+           PathCombineMode mode)
+{
+  GArray *result = g_array_new (FALSE, FALSE, sizeof(BezPoint));
+  Split *sp;
+  int i, n = 0;
+  BezPoint bp;
+  Point cur_pt;
+  GArray *splits;
+  /* only intersection starts with an inside segment */
+  gboolean outside = mode == PATH_INTERSECTION ? FALSE : TRUE;
+
+  g_return_val_if_fail (mode != PATH_EXCLUSION, NULL);
+
+  bp.type = BEZ_MOVE_TO;
+  /* start with the first point of segment one */
+  for (i = 0; i < one_splits->len; ++i) { 
+    sp = &g_array_index (one_splits, Split, i);
+    if (sp->outside == outside)
+      break;
+  }
+  sp->used = TRUE;
+  bp.p1 = g_array_index (one, BezierSegment, sp->seg).p0;
+  g_array_append_val (result, bp);
+  do {
+    cur_pt = _append_segments (result, sp->path);
+    n++;
+    if (mode == PATH_DIFFERENCE)
+      outside = (n % 2) == 0 ? TRUE : FALSE;
+    splits = (n % 2) == 0 ? one_splits : two_splits;
+    /* find next intersection start from the last point of this sub path */
+    if (!_find_split (splits, &cur_pt, outside, &sp)) {
+      /* if we can not find something connected search for an unused 'one' path
+       * XXX: this might be part of the issue with lost move-to within the segment
+       *  The other part might be in _append_segments or even _split_segments
+       */
+      outside = mode == PATH_INTERSECTION ? FALSE : TRUE;
+      for (i = 0; i < one_splits->len; ++i) { 
+       sp = &g_array_index (one_splits, Split, i);
+       if (!sp->used && (sp->outside == outside))
+         break;
+       else
+         sp = NULL;
+      }
+      if (sp) { /* found a new start, make a move-to */
+       sp->used = TRUE;
+       bp.type = BEZ_MOVE_TO;
+       bp.p1 = g_array_index (sp->path, BezierSegment, 0).p0;
+       g_array_append_val (result, bp);
+      }
+    }
+  } while (sp);
+
+  return result;
+}
+
+/*!
+ * \brief Combine two path into a single one with the given operation
+ *
+ * This should (but does not) consider
+ *  - self intersections in a path
+ *  - winding rule
+ *  - typical path operations (Union, Difference, Intersection, Exclusion, ...)
+ */
+GArray *
+path_combine (const GArray   *p1,
+             const GArray   *p2,
+             PathCombineMode mode)
+{
+  GArray *result = NULL;
+  int i, j, k;
+  int total = 0;
+  GArray *crossing = NULL;
+  GArray *one, *two;
+  static int debug = 0;
+
+  g_return_val_if_fail (p1->len > 1 && p2->len > 1, NULL);
+
+  /* convert both paths to segment representation - TODO: self intersections */
+  one = _path_to_segments (p1);
+  two = _path_to_segments (p2);
+  crossing = _find_intersections (one, two);
+
+  if (crossing)
+    g_print ("Total path intersections: %d\n", crossing->len);
+
+  if (crossing) {
+    /* Now crossing includes points in arbitrary order. Every point has four lines
+     * going in or out - two from p1, two from p2. Split one and two into segments
+     * at the crossing points.
+     */
+    GArray *one_splits = _extract_splits (crossing, TRUE);
+    GArray *two_splits = _extract_splits (crossing, FALSE);
+    _split_segments (one, one_splits, p2);
+    _split_segments (two, two_splits, p1);
+
+    /* XXX: convert segments back to a single path */
+    if (one_splits->len < 2) { /* XXX: just joining again */
+      result = _make_path0 (one, one_splits, two, two_splits);
+    } else if (debug) {
+      result = _make_path1 (one, one_splits, two, two_splits);
+    } else {
+      if (mode == PATH_EXCLUSION) { /* most simple impl. */
+       GArray *res2;
+       result = _make_path (one, one_splits, two, two_splits, PATH_DIFFERENCE);
+       res2 = _make_path (two, two_splits, one, one_splits, PATH_DIFFERENCE);
+       g_array_append_vals (result, &g_array_index (res2, BezPoint, 0), res2->len);
+       g_array_free (res2, TRUE);
+      } else {
+       result = _make_path (one, one_splits, two, two_splits, mode);
+      }
+    }
+    _free_splits (one_splits);
+    _free_splits (two_splits);
+    g_array_free (crossing, TRUE);
+  }
+  g_array_free (one, TRUE);
+  g_array_free (two, TRUE);
+  if (!result || result->len < 2) {
+    if (result)
+      g_array_free (result, TRUE);
+    return NULL;
+  }
+  return result;
+}
diff --git a/samples/path-combine.dia b/samples/path-combine.dia
new file mode 100644
index 0000000..b1ea4e6
Binary files /dev/null and b/samples/path-combine.dia differ



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]