OSDN Git Service

automatic stroke splitting (for undo); recording of painting time
authorMartin Renold <martinxyz@gmx.ch>
Fri, 16 Mar 2007 21:13:31 +0000 (21:13 +0000)
committerMartin Renold <martinxyz@gmx.ch>
Fri, 16 Mar 2007 21:13:31 +0000 (21:13 +0000)
svn://old.homeip.net/code/mypaint@464

PLAN
brushes/order.conf
brushes/s015.myb [new file with mode: 0644]
brushes/s015_prev.png [new file with mode: 0644]
document.py
drawwindow.py
gtkmybrush.c
gtkmybrush.h
gtkmydrawwidget.c
infinitemydrawwidget.py

diff --git a/PLAN b/PLAN
index 8314e00..3f5fe50 100644 (file)
--- a/PLAN
+++ b/PLAN
@@ -1,7 +1,3 @@
-- undo.
-  - (probably the most wanted feature, even if only one stroke)
-  - should be a lot easier together with layers
-
 - layers.
   - alpha support (for both rendering and brush strokes)
   - rewrite the rendering code
   - set canvas memory limit
   - set canvas border outside visible window (in percent)
   - set "reallocate how much in advance" maybe?
+  - zoom quality
 
 - dedicated "slow random" input
+  - or just "random slowness"?
+    - could lead to unwanted correlations
 
 - keyboard shortcut to change brush ocupacy
 
 - canvas rotation
   - (fast rotozoom? also would be nice to use OpenGL if available)
 
+- allow dots drawing
+  - a dot should draw even without motion
+  - is dabs-per-second enough, or is dabs-per-dpressure needed?
+
 - pen/eraser
   - recognize tool and asociate brush and color, as brushkeys do
   - default to a white sharp ink tip on the eraser
index 228a858..201f5df 100644 (file)
@@ -1,6 +1,7 @@
 # this file saves brushorder
 # the first one (upper left) will be selected at startup
 o043
+s015
 s014
 s013
 s010
diff --git a/brushes/s015.myb b/brushes/s015.myb
new file mode 100644 (file)
index 0000000..86398e2
--- /dev/null
@@ -0,0 +1,33 @@
+# mypaint brush file
+# you can edit this file and then select the brush in mypaint (again) to reload
+version 2
+opaque 0.3
+opaque_multiply 0.0 | pressure (0.000000 0.000000), (0.293210 0.128333), (1.000000 0.160000)
+opaque_linearize 0.0
+radius_logarithmic -0.1 | pressure (0.000000 0.000000), (0.209877 -0.185833), (1.000000 -2.230000) | speed1 (0.000000 0.000000), (1.000000 0.910000) | speed2 (0.000000 0.000000), (0.024691 0.146250), (0.197531 0.330417), (1.000000 0.520000)
+hardness 1.0
+dabs_per_basic_radius 0.0
+dabs_per_actual_radius 2.0
+dabs_per_second 0.0
+radius_by_random 0.0
+speed1_slowness 0.04
+speed2_slowness 1.0
+speed1_gamma 4.0
+speed2_gamma 4.0
+offset_by_random 2.0 | pressure (0.000000 0.000000), (0.060127 -3.229167), (0.212025 -4.531250), (0.310127 -4.843750), (1.000000 -5.000000) | speed1 (0.000000 0.000000), (0.335443 0.000000), (0.626582 0.097500), (1.000000 1.170000) | speed2 (0.000000 0.000000), (0.329114 0.000000), (1.000000 0.390000)
+offset_by_speed 0.0
+offset_by_speed_slowness 1.0
+slow_tracking 0.0
+slow_tracking_per_dab 0.0
+color_h 0.119327677079
+color_s 0.352693144723
+color_v 0.224368657969
+change_color_h 0.0
+change_color_s 0.0
+change_color_v 0.0
+adapt_color_from_image 0.0
+stroke_treshold 0.0
+stroke_duration_logarithmic 4.0
+stroke_holdtime 0.0
+custom_input 0.0
+custom_input_slowness 0.0
diff --git a/brushes/s015_prev.png b/brushes/s015_prev.png
new file mode 100644 (file)
index 0000000..346170d
Binary files /dev/null and b/brushes/s015_prev.png differ
index 1901194..2cb4450 100644 (file)
@@ -35,10 +35,11 @@ class Stroke:
         assert not self.finished
         self.stroke_data = self.mdw.stop_recording()
         self.bbox = self.brush.get_stroke_bbox()
+        self.total_painting_time = self.brush.get_stroke_total_painting_time()
         x, y, w, h = self.bbox
         self.empty = w <= 0 and h <= 0
         if not self.empty:
-            print 'Recorded', len(self.stroke_data), 'bytes.'
+            print 'Recorded', len(self.stroke_data), 'bytes. (painting time: %.2fs)' % self.total_painting_time
             print self.bbox
         #print 'Compressed size:', len(zlib.compress(self.stroke_data)), 'bytes.'
         del self.mdw, self.brush
index fcdf252..709a13a 100644 (file)
@@ -41,6 +41,7 @@ class Window(gtk.Window):
         self.stroke = document.Stroke()
         self.stroke.start_recording(self.mdw, self.app.brush)
         self.app.brush.observers.append(self.brush_modified_cb) # FIXME: should remove when this Window is destroyed
+        self.app.brush.connect("split-stroke", self.split_stroke_cb)
 
         self.init_child_dialogs()
 
@@ -242,11 +243,8 @@ class Window(gtk.Window):
         self.command_stack.redo()
 
     def split_stroke(self):
-        self.stroke.stop_recording()
-        if not self.stroke.empty:
-            self.command_stack.add(command.Stroke(self.layer, self.stroke))
-        self.stroke = document.Stroke()
-        self.stroke.start_recording(self.mdw, self.app.brush)
+        # let the brush emit the signal
+        self.app.brush.split_stroke()
 
     def brush_modified_cb(self):
         # OPTIMIZE: called at every brush setting modification, must return fast
@@ -257,6 +255,14 @@ class Window(gtk.Window):
         # also make sure proximity events outside the window are checked
         self.split_stroke()
 
+    def split_stroke_cb(self, widget):
+        print 'SPLIT'
+        self.stroke.stop_recording()
+        if not self.stroke.empty:
+            self.command_stack.add(command.Stroke(self.layer, self.stroke))
+        self.stroke = document.Stroke()
+        self.stroke.start_recording(self.mdw, self.app.brush)
+
     def record_stroke_cb(self, action):
         print 'TODO'
         #if self.recording:
index 3acb164..6e0a6ce 100644 (file)
@@ -33,6 +33,7 @@
 
 // prototypes
 void gtk_my_brush_settings_base_values_have_changed (GtkMyBrush * b);
+void gtk_my_brush_split_stroke (GtkMyBrush * b);
 
 void
 gtk_my_brush_set_base_value (GtkMyBrush * b, int id, float value)
@@ -86,6 +87,12 @@ static void gtk_my_brush_finalize (GObject *object);
 
 static gpointer parent_class;
 
+enum {
+  SPLIT_STROKE,
+  LAST_SIGNAL
+};
+guint gtk_my_brush_signals[LAST_SIGNAL] = { 0 };
+
 GType
 gtk_my_brush_get_type (void)
 {
@@ -120,6 +127,15 @@ gtk_my_brush_class_init (GtkMyBrushClass *class)
   GObjectClass *gobject_class = G_OBJECT_CLASS (class);
   parent_class = g_type_class_peek_parent (class);
   gobject_class->finalize = gtk_my_brush_finalize;
+
+  gtk_my_brush_signals[SPLIT_STROKE] = g_signal_new 
+    ("split-stroke",
+     G_TYPE_FROM_CLASS (class),
+     G_SIGNAL_RUN_LAST,
+     G_STRUCT_OFFSET (GtkMyBrushClass, split_stroke),
+     NULL, NULL,
+     g_cclosure_marshal_VOID__VOID,
+     G_TYPE_NONE, 0);
 }
 
 static void
@@ -355,8 +371,8 @@ void brush_update_settings_values (GtkMyBrush * b)
 // draw the dab, then let draw_brush_dab() do the actual drawing.
 //
 // This is always called "directly" after brush_update_settings_values.
-// The bbox parameter is a return value XXX
-void brush_prepare_and_draw_dab (GtkMyBrush * b, GtkMySurfaceOld * s, Rect * bbox)
+// The bbox is enlarged so the dab fits in. Returns zero if nothing was drawn.
+int brush_prepare_and_draw_dab (GtkMyBrush * b, GtkMySurfaceOld * s, Rect * bbox)
 {
   float * settings = b->settings_value;
   float x, y, opaque;
@@ -381,7 +397,7 @@ void brush_prepare_and_draw_dab (GtkMyBrush * b, GtkMySurfaceOld * s, Rect * bbo
   opaque = settings[BRUSH_OPAQUE] * settings[BRUSH_OPAQUE_MULTIPLY];
   if (opaque >= 1.0) opaque = 1.0;
   //if (opaque <= 0.0) opaque = 0.0;
-  if (opaque <= 0.0) return;
+  if (opaque <= 0.0) return 0;
   if (settings[BRUSH_OPAQUE_LINEARIZE]) {
     // OPTIMIZE: no need to recalculate this for each dab
     float alpha, beta, alpha_dab, beta_dab;
@@ -491,10 +507,9 @@ void brush_prepare_and_draw_dab (GtkMyBrush * b, GtkMySurfaceOld * s, Rect * bbo
     if (hardness > 1.0) hardness = 1.0;
     if (hardness < 0.0) hardness = 0.0;
 
-    int painted;
-    painted = draw_brush_dab (s, bbox, b->rng, 
-                              x, y, radius, opaque, hardness,
-                              c[0], c[1], c[2]);
+    return draw_brush_dab (s, bbox, b->rng, 
+                           x, y, radius, opaque, hardness,
+                           c[0], c[1], c[2]);
   }
 }
 
@@ -595,6 +610,8 @@ void gtk_my_brush_stroke_to (GtkMyBrush * b, GtkMySurfaceOld * s, float x, float
   }
 
   //g_print("dist = %f\n", b->states[STATE_DIST]);
+  enum { UNKNOWN, YES, NO } painted = UNKNOWN;
+  double dtime_left = dtime;
 
   while (dist_moved + dist_todo >= 1.0) { // there are dabs pending
     { // linear interpolation (nonlinear variant was too slow, see SVN log)
@@ -610,7 +627,7 @@ void gtk_my_brush_stroke_to (GtkMyBrush * b, GtkMySurfaceOld * s, float x, float
       b->dx        = frac * (x - b->states[STATE_X]);
       b->dy        = frac * (y - b->states[STATE_Y]);
       b->dpressure = frac * (pressure - b->states[STATE_PRESSURE]);
-      b->dtime     = frac * (dtime - 0.0);
+      b->dtime     = frac * (dtime_left - 0.0);
       // Though it looks different, time is interpolated exactly like x/y/pressure.
     }
     
@@ -619,10 +636,15 @@ void gtk_my_brush_stroke_to (GtkMyBrush * b, GtkMySurfaceOld * s, float x, float
     b->states[STATE_PRESSURE] += b->dpressure;
 
     brush_update_settings_values (b);
-    brush_prepare_and_draw_dab (b, s, &bbox);
+    int painted_now = brush_prepare_and_draw_dab (b, s, &bbox);
+    if (painted_now) {
+      painted = YES;
+    } else if (painted == UNKNOWN) {
+      painted = NO;
+    }
 
-    dtime   -= b->dtime;
-    dist_todo  = brush_count_dabs_to (b, x, y, pressure, dtime);
+    dtime_left   -= b->dtime;
+    dist_todo  = brush_count_dabs_to (b, x, y, pressure, dtime_left);
   }
 
   {
@@ -635,12 +657,12 @@ void gtk_my_brush_stroke_to (GtkMyBrush * b, GtkMySurfaceOld * s, float x, float
     b->dx        = x - b->states[STATE_X];
     b->dy        = y - b->states[STATE_Y];
     b->dpressure = pressure - b->states[STATE_PRESSURE];
-    b->dtime     = dtime;
+    b->dtime     = dtime_left;
     
     b->states[STATE_X] = x;
     b->states[STATE_Y] = y;
     b->states[STATE_PRESSURE] = pressure;
-    //dtime = 0; but that value is not used any more
+    //dtime_left = 0; but that value is not used any more
 
     brush_update_settings_values (b);
   }
@@ -654,6 +676,60 @@ void gtk_my_brush_stroke_to (GtkMyBrush * b, GtkMySurfaceOld * s, float x, float
     ExpandRectToIncludePoint(&b->stroke_bbox, bbox.x, bbox.y);
     ExpandRectToIncludePoint(&b->stroke_bbox, bbox.x+bbox.w-1, bbox.y+bbox.h-1);
   }
+
+  // stroke separation logic
+
+  if (painted == UNKNOWN) {
+    if (b->stroke_idling_time > 0) {
+      // still idling
+      painted = NO;
+    } else {
+      // probably still painting (we get more events than brushdabs)
+      painted = YES;
+      if (pressure == 0) g_print ("info: assuming 'still painting' while there is no pressure\n");
+    }
+  }
+  if (painted == YES) {
+    if (b->stroke_idling_time > 0) g_print ("idling ==> painting\n");
+    b->stroke_total_painting_time += dtime;
+    b->stroke_idling_time = 0;
+    // force a stroke split after some time
+    if (b->stroke_total_painting_time > 5 + 10*pressure) {
+      // but only if pressure is not being released
+      if (b->dpressure >= 0) {
+        gtk_my_brush_split_stroke (b);
+      }
+    }
+  } else if (painted == NO) {
+    if (b->stroke_idling_time == 0) g_print ("painting ==> idling\n");
+    b->stroke_idling_time += dtime;
+    if (b->stroke_total_painting_time == 0) {
+      // not yet painted, split to discard the useless motion data
+      g_assert (b->stroke_bbox.w == 0);
+      if (b->stroke_idling_time > 1.0) {
+        gtk_my_brush_split_stroke (b);
+      }
+    } else {
+      // Usually we have pressure==0 here. But some brushes can paint
+      // nothing at full pressure (eg gappy lines, or a stroke that
+      // fades out). In either case this is the prefered moment to split.
+      if (b->stroke_total_painting_time+b->stroke_idling_time > 1.5 + 5*pressure) {
+        gtk_my_brush_split_stroke (b);
+      }
+    }
+  }
+}
+
+void gtk_my_brush_split_stroke (GtkMyBrush * b)
+{
+  g_signal_emit (b, gtk_my_brush_signals[SPLIT_STROKE], 0);
+  b->stroke_idling_time = 0;
+  b->stroke_total_painting_time = 0;
+}
+
+float gtk_my_brush_get_stroke_total_painting_time (GtkMyBrush * b)
+{
+  return b->stroke_total_painting_time;
 }
 
 #define SIZE 256
index cfaeeba..6f0f72a 100644 (file)
@@ -52,7 +52,9 @@ struct _GtkMyBrush
   Mapping * settings[BRUSH_SETTINGS_COUNT];
 
   int print_inputs; // debug menu
-  Rect stroke_bbox; // track it here, get/reset it from python (eg to decide if a stroke is empty)
+  Rect stroke_bbox; // track it here, get/reset from python
+  double stroke_total_painting_time;
+  double stroke_idling_time; 
 
   int must_reset;
 
@@ -66,6 +68,8 @@ struct _GtkMyBrush
 struct _GtkMyBrushClass
 {
   GObjectClass parent_class;
+
+  void (*split_stroke) (GtkMyBrush *b);
 };
 
 
@@ -83,6 +87,8 @@ void gtk_my_brush_set_print_inputs (GtkMyBrush * b, int value);
 
 Rect gtk_my_brush_get_stroke_bbox (GtkMyBrush * b);
 void gtk_my_brush_reset_stroke_bbox (GtkMyBrush * b);
+void gtk_my_brush_split_stroke (GtkMyBrush * b);
+float gtk_my_brush_get_stroke_total_painting_time (GtkMyBrush * b);
 
 GdkPixbuf* gtk_my_brush_get_colorselection_pixbuf (GtkMyBrush * b);
 
index f97cd48..a7d5ba1 100644 (file)
@@ -87,7 +87,7 @@ gtk_my_draw_widget_class_init (GtkMyDrawWidgetClass *class)
 
 
   gtk_my_draw_widget_signals[DRAGGING_FINISHED] = g_signal_new 
-    ("dragging_finished",
+    ("dragging-finished",
      G_TYPE_FROM_CLASS (class),
      G_SIGNAL_RUN_LAST,
      G_STRUCT_OFFSET (GtkMyDrawWidgetClass, dragging_finished),
index 3ae7855..9458627 100644 (file)
@@ -9,7 +9,7 @@ class InfiniteMyDrawWidget(MyDrawWidget):
         self.init_canvas()
         MyDrawWidget.clear(self)
         self.connect("size-allocate", self.size_allocate_event_cb)
-        self.connect("dragging_finished", self.dragging_finished_cb)
+        self.connect("dragging-finished", self.dragging_finished_cb)
         self.connect("proximity-in-event", self.proximity_cb)
         self.connect("proximity-out-event", self.proximity_cb)
         self.toolchange_observers = []