[gegl] warp: process stroke incrementally; misc improvements



commit 4cd57c78df7d374b9cfcd86f12d80e73f0534bc6
Author: Ell <ell_se yahoo com>
Date:   Tue May 16 19:37:58 2017 -0400

    warp: process stroke incrementally; misc improvements
    
    When processing a stroke, keep a record of the processed path, and
    keep the processed buffer around.  When the path changes, check if
    the previously processed path is an initial segment of the new path,
    in which case, only process the new points in the path, working on
    top of the existing buffer.  As a result, since the GIMP warp tool
    only ever appends points to the stroke path, it never has to
    reprocess the entire path during a stroke, as it did before.
    
    Cache the processed buffer ourselves, and pass it directly as output,
    avoiding copying.
    
    Stamp the first point of the path, and stroke segments such that the
    final endpoint of the segment is always stamped, making the behvaior
    more consistent.
    
    Add a "spacing" property, controlling the stroke spacing, as a
    fraction of the "brush" size.  This property defaults to 0.01, as
    was previously hardcoded.  This allows GIMP to use higher values
    for this property; 0.01 is too low for interactive use.

 operations/common/warp.c |  341 +++++++++++++++++++++++++++++++++------------
 1 files changed, 250 insertions(+), 91 deletions(-)
---
diff --git a/operations/common/warp.c b/operations/common/warp.c
index 4300349..ecb630c 100644
--- a/operations/common/warp.c
+++ b/operations/common/warp.c
@@ -41,6 +41,9 @@ property_double (size, _("Size"), 40.0)
 property_double (hardness, _("Hardness"), 0.5)
   value_range (0.0, 1.0)
 
+property_double (spacing, _("Spacing"), 0.01)
+  value_range (0.0, 100.0)
+
 property_path   (stroke, _("Stroke"), NULL)
 
 property_enum   (behavior, _("Behavior"),
@@ -57,55 +60,132 @@ property_enum   (behavior, _("Behavior"),
 #include "gegl-plugin.h"
 #include "gegl-path.h"
 
-static void path_changed (GeglPath            *path,
-                          const GeglRectangle *roi,
-                          gpointer             userdata);
+static void node_invalidated (GeglNode            *node,
+                              const GeglRectangle *roi,
+                              GeglOperation       *operation);
+
+static void path_changed     (GeglPath            *path,
+                              const GeglRectangle *roi,
+                              GeglOperation       *operation);
+
 #include "gegl-op.h"
 
-typedef struct {
-  gdouble     *lookup;
-  GeglBuffer  *buffer;
-  gdouble      last_x;
-  gdouble      last_y;
-  gboolean     last_point_set;
+typedef struct WarpPointList
+{
+  GeglPathPoint         point;
+  struct WarpPointList *next;
+} WarpPointList;
+
+typedef struct
+{
+  gdouble       *lookup;
+  GeglBuffer    *buffer;
+  WarpPointList *processed_stroke;
+  gboolean       processed_stroke_valid;
+  gdouble        last_x;
+  gdouble        last_y;
 } WarpPrivate;
 
 static void
+clear_cache (GeglProperties *o)
+{
+  WarpPrivate *priv = (WarpPrivate *) o->user_data;
+
+  if (! priv)
+    return;
+
+  if (priv->lookup)
+    {
+      g_free (priv->lookup);
+
+      priv->lookup = NULL;
+    }
+
+  if (priv->buffer)
+    {
+      g_object_unref (priv->buffer);
+
+      priv->buffer = NULL;
+    }
+
+  while (priv->processed_stroke)
+    {
+      WarpPointList *next = priv->processed_stroke->next;
+
+      g_slice_free (WarpPointList, priv->processed_stroke);
+
+      priv->processed_stroke = next;
+    }
+
+  priv->processed_stroke_valid = FALSE;
+}
+
+static void
+node_invalidated (GeglNode            *node,
+                  const GeglRectangle *rect,
+                  GeglOperation       *operation)
+{
+  /* if the node is invalidated, clear all cached data.  in particular, redraw
+   * the entire stroke upon the next call to process().
+   */
+  clear_cache (GEGL_PROPERTIES (operation));
+}
+
+static void
 path_changed (GeglPath            *path,
               const GeglRectangle *roi,
-              gpointer             userdata)
+              GeglOperation       *operation)
 {
-  GeglRectangle   rect = *roi;
-  GeglProperties *o    = GEGL_PROPERTIES (userdata);
+  GeglRectangle   rect;
+  GeglProperties *o    = GEGL_PROPERTIES (operation);
+  WarpPrivate    *priv = (WarpPrivate *) o->user_data;
+
+  /* mark the processed stroke as invalid, so that we recheck it against the
+   * new path upon the next call to process().
+   */
+  if (priv)
+    priv->processed_stroke_valid = FALSE;
+
   /* invalidate the incoming rectangle */
 
-  rect.x -= o->size/2;
-  rect.y -= o->size/2;
-  rect.width += o->size;
-  rect.height += o->size;
+  rect.x      = floor (roi->x - o->size/2);
+  rect.y      = floor (roi->y - o->size/2);
+  rect.width  = ceil (roi->x + roi->width  + o->size/2) - rect.x;
+  rect.height = ceil (roi->y + roi->height + o->size/2) - rect.y;
+
+  /* we don't want to clear the cached data right away; we potentially do this
+   * in process(), if the cached path is not an initial segment of the new
+   * path.  block our INVALIDATED handler so that the cache isn't cleared.
+   */
+  g_signal_handlers_block_by_func (operation->node,
+                                   node_invalidated, operation);
+
+  gegl_operation_invalidate (operation, &rect, FALSE);
 
-  gegl_operation_invalidate (userdata, &rect, FALSE);
+  g_signal_handlers_unblock_by_func (operation->node,
+                                     node_invalidated, operation);
+}
+
+static void
+attach (GeglOperation *operation)
+{
+  GEGL_OPERATION_CLASS (gegl_op_parent_class)->attach (operation);
+
+  g_signal_connect_object (operation->node, "invalidated",
+                           G_CALLBACK (node_invalidated), operation, 0);
 }
 
 static void
 prepare (GeglOperation *operation)
 {
-  GeglProperties *o     = GEGL_PROPERTIES (operation);
-  WarpPrivate    *priv;
+  GeglProperties *o = GEGL_PROPERTIES (operation);
 
   const Babl *format = babl_format_n (babl_type ("float"), 2);
   gegl_operation_set_format (operation, "input", format);
   gegl_operation_set_format (operation, "output", format);
 
   if (!o->user_data)
-    {
-      o->user_data = g_slice_new (WarpPrivate);
-    }
-
-  priv = (WarpPrivate*) o->user_data;
-  priv->last_point_set = FALSE;
-  priv->lookup = NULL;
-  priv->buffer = NULL;
+    o->user_data = g_slice_new0 (WarpPrivate);
 }
 
 static void
@@ -115,6 +195,8 @@ finalize (GObject *object)
 
   if (o->user_data)
     {
+      clear_cache (o);
+
       g_slice_free (WarpPrivate, o->user_data);
       o->user_data = NULL;
     }
@@ -207,10 +289,9 @@ get_stamp_force (GeglProperties *o,
 }
 
 static void
-stamp (GeglProperties          *o,
-       const GeglRectangle     *result,
-       gdouble                  x,
-       gdouble                  y)
+stamp (GeglProperties *o,
+       gdouble         x,
+       gdouble         y)
 {
   WarpPrivate         *priv = (WarpPrivate*) o->user_data;
   GeglBufferIterator  *it;
@@ -232,19 +313,6 @@ stamp (GeglProperties          *o,
   area.width  -= area.x;
   area.height -= area.y;
 
-  /* first point of the stroke */
-  if (!priv->last_point_set)
-    {
-      priv->last_x = x;
-      priv->last_y = y;
-      priv->last_point_set = TRUE;
-      return;
-    }
-
-  /* don't stamp if outside the roi treated */
-  if (!gegl_rectangle_intersect (NULL, result, &area))
-    return;
-
   format = babl_format_n (babl_type ("float"), 2);
 
   /* If needed, compute the mean deformation */
@@ -402,81 +470,172 @@ stamp (GeglProperties          *o,
 }
 
 static gboolean
-process (GeglOperation       *operation,
-         GeglBuffer          *input,
-         GeglBuffer          *output,
-         const GeglRectangle *result,
-         gint                 level)
+process (GeglOperation        *operation,
+         GeglOperationContext *context,
+         const gchar          *output_prop,
+         const GeglRectangle  *result,
+         gint                  level)
 {
-  GeglProperties      *o    = GEGL_PROPERTIES (operation);
-  WarpPrivate         *priv = (WarpPrivate*) o->user_data;
-  gdouble              dist;
-  gdouble              stamps;
-  gdouble              spacing = MAX (o->size * 0.01, 0.5); /*1% spacing for starters*/
+  GeglProperties  *o    = GEGL_PROPERTIES (operation);
+  WarpPrivate     *priv = (WarpPrivate*) o->user_data;
 
-  GeglPathPoint        prev, next, lerp;
-  gulong               i;
-  GeglPathList        *event;
+  GeglBuffer      *input;
 
-  if (!o->stroke)
+  gdouble          spacing = MAX (o->size * o->spacing, 0.5);
+  gdouble          dist;
+  gint             stamps;
+  gint             i;
+  gdouble          t;
+
+  GeglPathPoint    prev, next, lerp;
+  GeglPathList    *event;
+  WarpPointList   *processed_event;
+  WarpPointList  **processed_event_ptr;
+
+  if (!o->stroke || strcmp (output_prop, "output"))
     return FALSE;
-  priv->buffer = gegl_buffer_dup (input);
-  event = gegl_path_get_path (o->stroke);
 
-  prev = *(event->d.point);
+  /* if the previously processed stroke is valid, the cached buffer can be
+   * passed as output right away.
+   */
+  if (priv->processed_stroke_valid)
+    {
+      g_assert (priv->buffer != NULL);
+
+      gegl_operation_context_set_object (context,
+                                         "output", G_OBJECT (priv->buffer));
+
+      return TRUE;
+    }
+
+  /* ... otherwise, we need to check if the previously processed stroke is an
+   * initial segment of the current stroke ...
+   */
+
+  event               = gegl_path_get_path (o->stroke);
+  processed_event     = priv->processed_stroke;
+  processed_event_ptr = &priv->processed_stroke;
+
+  while (event && processed_event)
+    {
+      if (event->d.point[0].x != processed_event->point.x ||
+          event->d.point[0].y != processed_event->point.y)
+        {
+          break;
+        }
+
+      processed_event_ptr = &processed_event->next;
+
+      event           = event->next;
+      processed_event = processed_event->next;
+    }
 
-  while (event->next)
+  /* if the loop stopped before we reached the last event of the processed
+   * stroke, it's not an initial segment, and we need to clear the cache, and
+   * process the entire stroke.
+   */
+  if (processed_event)
     {
-      event = event->next;
-      next = *(event->d.point);
-      dist = gegl_path_point_dist (&next, &prev);
-      stamps = dist / spacing;
+      clear_cache (o);
+
+      event               = gegl_path_get_path (o->stroke);
+      processed_event_ptr = &priv->processed_stroke;
+    }
+  /* otherwise, we simply continue processing remaining stroke on top of the
+   * previously processed buffer.
+   */
+
+  /* intialize the cached buffer if we don't already have one. */
+  if (! priv->buffer)
+    {
+      input = GEGL_BUFFER (gegl_operation_context_get_object (context,
+                                                              "input"));
+
+      priv->buffer = gegl_buffer_dup (input);
+
+      /* we pass the buffer as output directly while keeping it cached, so mark
+       * it as forked.
+       */
+      gegl_object_set_has_forked (G_OBJECT (priv->buffer));
+    }
 
-      if (stamps < 1)
+  if (event)
+    {
+      /* is this the first event of the stroke? */
+      if (! priv->processed_stroke)
         {
-          stamp (o, result, next.x, next.y);
-          prev = next;
+          prev = *(event->d.point);
+
+          priv->last_x = prev.x;
+          priv->last_y = prev.y;
         }
       else
         {
-          for (i = 0; i < stamps; i++)
+          prev.x = priv->last_x;
+          prev.y = priv->last_y;
+        }
+
+      for (; event; event = event->next)
+        {
+          next = *(event->d.point);
+          dist = gegl_path_point_dist (&next, &prev);
+          stamps = floor (dist / spacing) + 1;
+
+          /* stroke the current segment, such that there's always a stamp at
+           * its final endpoint, and at positive integer multiples of
+           * `spacing` away from it.
+           */
+
+          if (stamps == 1)
             {
-              gegl_path_point_lerp (&lerp, &prev, &next, (i * spacing) / dist);
-              stamp (o, result, lerp.x, lerp.y);
+              stamp (o, next.x, next.y);
             }
-          prev = lerp;
-        }
-    }
+          else
+            {
+              for (i = 0; i < stamps; i++)
+                {
+                  t = 1.0 - ((stamps - i - 1) * spacing) / dist;
 
-  /* Affect the output buffer */
-  gegl_buffer_copy (priv->buffer, result, GEGL_ABYSS_NONE,
-                    output, result);
-  gegl_buffer_set_extent (output, gegl_buffer_get_extent (input));
-  g_object_unref (priv->buffer);
+                  gegl_path_point_lerp (&lerp, &prev, &next, t);
+                  stamp (o, lerp.x, lerp.y);
+                }
+            }
 
-  /* prepare for the recomputing of the op */
-  priv->last_point_set = FALSE;
+          prev = next;
 
-  /* free the LUT */
-  if (priv->lookup)
-    {
-      g_free (priv->lookup);
-      priv->lookup = NULL;
+          /* append the current event to the processed path. */
+          processed_event        = g_slice_new (WarpPointList);
+          processed_event->point = next;
+
+          *processed_event_ptr = processed_event;
+          processed_event_ptr  = &processed_event->next;
+        }
+
+      *processed_event_ptr = NULL;
     }
 
+  priv->processed_stroke_valid = TRUE;
+
+  /* pass the processed buffer as output */
+  gegl_operation_context_set_object (context,
+                                     "output", G_OBJECT (priv->buffer));
+
   return TRUE;
 }
 
 static void
 gegl_op_class_init (GeglOpClass *klass)
 {
-  GObjectClass               *object_class    = G_OBJECT_CLASS (klass);
-  GeglOperationClass         *operation_class = GEGL_OPERATION_CLASS (klass);
-  GeglOperationFilterClass   *filter_class    = GEGL_OPERATION_FILTER_CLASS (klass);
+  GObjectClass       *object_class    = G_OBJECT_CLASS (klass);
+  GeglOperationClass *operation_class = GEGL_OPERATION_CLASS (klass);
 
   object_class->finalize   = finalize;
+  operation_class->attach  = attach;
   operation_class->prepare = prepare;
-  filter_class->process    = process;
+  operation_class->process = process;
+  operation_class->no_cache = TRUE; /* we're effectively doing the caching
+                                     * ourselves.
+                                     */
   operation_class->threaded = FALSE;
 
   gegl_operation_class_set_keys (operation_class,


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