OSDN Git Service

Implement stroke picking from canvas
authorMartin Renold <martinxyz@gmx.ch>
Sat, 27 Jun 2009 19:55:28 +0000 (21:55 +0200)
committerMartin Renold <martinxyz@gmx.ch>
Thu, 2 Jul 2009 16:49:12 +0000 (18:49 +0200)
Record a one-bit bitmap of each stroke. This is saved per layer.
When looking for the stroke at a given pixel, this bitmap is
checked for each stroke.

TODO: don't waste so much memory

gui/brushsettingswindow.py
gui/drawwindow.py
lib/command.py
lib/document.py
lib/layer.py
lib/tiledsurface.py

index ccce2eb..93008d0 100644 (file)
@@ -123,7 +123,6 @@ class Window(gtk.Window):
                     new_stroke = cmd.stroke.copy_using_different_brush(self.app.brush)
                     snapshot_before = doc.layer.save_snapshot()
                     new_stroke.render(doc.layer.surface)
-                    snapshot_after = doc.layer.save_snapshot()
-                    doc.do(command.Stroke(doc, new_stroke, snapshot_before, snapshot_after))
+                    doc.do(command.Stroke(doc, new_stroke, snapshot_before))
                     break
 
index c594184..df3f017 100644 (file)
@@ -15,7 +15,7 @@ Painting is done in tileddrawwidget.py.
 
 MYPAINT_VERSION="0.7.0+git"
 
-import os, re, math
+import os, re, math, sys
 from time import time
 from glob import glob
 import traceback
@@ -25,7 +25,7 @@ from gtk import gdk, keysyms
 
 import tileddrawwidget, colorselectionwindow, historypopup, \
        stategroup, keyboard, colorpicker
-from lib import document, helpers
+from lib import document, helpers, tiledsurface
 
 class Window(gtk.Window):
     def __init__(self, app):
@@ -168,6 +168,8 @@ class Window(gtk.Window):
               <menuitem action='LessOpaque'/>
               <separator/>
               <menuitem action='Eraser'/>
+              <separator/>
+              <menuitem action='PickContext'/>
             </menu>
             <menu action='ColorMenu'>
               <menuitem action='ColorSelectionWindow'/>
@@ -231,6 +233,7 @@ class Window(gtk.Window):
             ('MoreOpaque',   None, 'More Opaque', 's', None, self.more_opaque_cb),
             ('LessOpaque',   None, 'Less Opaque', 'a', None, self.less_opaque_cb),
             ('Eraser',       None, 'Toggle Eraser Mode', 'e', None, self.eraser_cb),
+            ('PickContext',  None, 'Pick Context (layer, brush and color)', 'w', None, self.pick_context_cb),
 
             ('ColorMenu',    None, 'Color'),
             ('Darker',       None, 'Darker', None, None, self.darker_cb),
@@ -573,6 +576,31 @@ class Window(gtk.Window):
         self.doc.select_layer(0)
         self.layerblink_state.activate(action)
 
+    def pick_context_cb(self, action):
+        x, y = self.tdw.get_cursor_in_model_coordinates()
+        for idx, layer in reversed(list(enumerate(self.doc.layers))):
+            alpha = layer.surface.get_alpha (x, y, 5)
+            if alpha > 0.1:
+                self.doc.select_layer(idx)
+                self.layerblink_state.activate(action)
+
+                # find the most recent (last) stroke that touches our picking point
+                brush = self.doc.layer.get_brush_at(x, y)
+
+                if brush:
+                    # FIXME: clean brush concept?
+                    self.app.brush.load_from_string(brush)
+                    self.app.select_brush(None)
+                else:
+                    print 'Nothing found!'
+
+                #self.app.brush.copy_settings_from(stroke.brush_settings)
+                #self.app.select_brush()
+                    
+                return
+        self.doc.select_layer(0)
+        self.layerblink_state.activate(action)
+
     def layerblink_state_enter(self):
         self.tdw.current_layer_solo = True
         self.tdw.queue_draw()
index a5bd2ec..a63e152 100644 (file)
@@ -6,7 +6,7 @@
 # the Free Software Foundation; either version 2 of the License, or
 # (at your option) any later version.
 
-import layer
+import layer, tiledsurface
 
 class CommandStack:
     def __init__(self):
@@ -62,17 +62,17 @@ class Action:
     # - undo
     automatic_undo = False
 
-# FIXME: the code below looks horrible, there must be a less redundant way to implement this.
-# - eg command_* special members on the document class?
-# - and then auto-build wrapper methods?
 
 class Stroke(Action):
-    def __init__(self, doc, stroke, snapshot_before, snapshot_after):
+    def __init__(self, doc, stroke, snapshot_before):
+        """called only when the stroke was just completed and is now fully rendered"""
         self.doc = doc
         assert stroke.finished
         self.stroke = stroke # immutable; not used for drawing any more, just for inspection
         self.before = snapshot_before
-        self.after = snapshot_after
+        self.doc.layer.add_stroke(stroke, snapshot_before)
+        # this snapshot will include the updated stroke list (modified by the line above)
+        self.after = self.doc.layer.save_snapshot()
     def undo(self):
         self.doc.layer.load_snapshot(self.before)
     def redo(self):
index 70148f1..38c758e 100644 (file)
@@ -96,11 +96,8 @@ class Document():
         if not self.stroke: return
         self.stroke.stop_recording()
         if not self.stroke.empty:
-            self.layer.strokes.append(self.stroke)
-            before = self.snapshot_before_stroke
-            after = self.layer.save_snapshot()
-            self.command_stack.do(command.Stroke(self, self.stroke, before, after))
-            self.snapshot_before_stroke = after
+            self.command_stack.do(command.Stroke(self, self.stroke, self.snapshot_before_stroke))
+            del self.snapshot_before_stroke
             self.unsaved_painting_time += self.stroke.total_painting_time
             for f in self.stroke_observers:
                 f(self.stroke, self.brush)
index f86c650..54fdf87 100644 (file)
@@ -6,7 +6,11 @@
 # the Free Software Foundation; either version 2 of the License, or
 # (at your option) any later version.
 
+from numpy import *
+import time
+
 import tiledsurface
+N = tiledsurface.N
 
 class Layer:
     # A layer contains a list of strokes, and possibly a background
@@ -28,12 +32,11 @@ class Layer:
 
     def __init__(self):
         self.surface = tiledsurface.Surface()
-        self.strokes = []
-        self.background = None
+        self.clear()
 
     def clear(self):
         self.background = None
-        self.strokes = []
+        self.strokes = [] # contains StrokeInfo instances (not stroke.Stroke)
         self.surface.clear()
 
     def load_from_pixbuf(self, pixbuf):
@@ -50,12 +53,68 @@ class Layer:
         self.background = background
         self.surface.load_snapshot(data)
 
+    def add_stroke(self, stroke, snapshot_before):
+        before = snapshot_before[2] # extract surface snapshot
+        after  = self.surface.save_snapshot()
+        self.strokes.append(StrokeInfo(stroke, before, after))
+
     def merge_into(self, dst):
         """
-        Merges this layer into dst, modifying only dst.
+        Merge this layer into dst, modifying only dst.
         """
         src = self
         dst.background = None # hm... this breaks "full-rerender" capability, but should work fine... FIXME: redesign needed?
-        dst.strokes = [] # this one too...
+        dst.strokes.extend(self.strokes) # TODO: test this codepath!
         for tx, ty in src.surface.get_tiles():
             src.surface.composite_tile_over(dst.surface.get_tile_memory(tx, ty, readonly=False), tx, ty)
+
+    def get_brush_at(self, x, y):
+        x, y = int(x), int(y)
+        for s in reversed(self.strokes):
+            if s.touches_pixel(x, y):
+                return s.brush
+
+
+
+class StrokeInfo:
+    def __init__(self, stroke, snapshot_before, snapshot_after):
+        self.brush = stroke.brush_settings
+        t0 = time.time()
+        # extract the layer from each snapshot
+        a, b = snapshot_before.tiledict, snapshot_after.tiledict
+        # enumerate all tiles that have changed
+        a_tiles = set(a.items())
+        b_tiles = set(b.items())
+        changes = a_tiles.symmetric_difference(b_tiles)
+        tiles_modified = set([pos for pos, data in changes])
+
+        # for each tile, calculate the exact difference and update our brushmap
+        self.strokemap = {}
+        for tx, ty in tiles_modified:
+            # get the pixel data to compare
+            a_data = a.get((tx, ty), tiledsurface.transparent_tile).rgba
+            b_data = b.get((tx, ty), tiledsurface.transparent_tile).rgba
+
+            # calculate the "perceptual" amount of difference
+            absdiff = zeros((N, N), 'uint32')
+            for i in range(4): # RGBA
+                absdiff += abs(a_data[:,:,i].astype('uint32') - b_data[:,:,i])
+            # ignore badly visible (parts of) strokes, eg. very faint strokes
+            #
+            # This is an arbitrary threshold. If it is too high, an
+            # ink stroke with slightly different color than the one
+            # below will not be pickable.  If it is too high, barely
+            # visible strokes will make things below unpickable.
+            #
+            threshold = (1<<15)*4 / 16 # require 1/16 of the max difference (also not bad: 1/8)
+            is_different = absdiff > threshold
+            # except if there is no previous stroke below it
+            is_different |= (absdiff > 0) #& (brushmap_data == 0) --- FIXME: not possible any more
+            self.strokemap[tx, ty] = is_different
+
+        print 'brushmap update took %.3f seconds' % (time.time() - t0)
+
+    def touches_pixel(self, x, y):
+        tile = self.strokemap.get((x/N, y/N))
+        if tile is not None:
+            return tile[y%N, x%N]
index f91aed1..95bd42a 100644 (file)
@@ -32,7 +32,7 @@ class Tile:
     def copy(self):
         return Tile(copy_from=self)
         
-transparentTile = Tile()
+transparent_tile = Tile()
 
 def get_tiles_bbox(tiles):
     res = helpers.Rect()
@@ -68,7 +68,7 @@ class Surface(mypaintlib.TiledSurface):
         t = self.tiledict.get((tx, ty))
         if t is None:
             if readonly:
-                t = transparentTile
+                t = transparent_tile
             else:
                 t = Tile()
                 self.tiledict[(tx, ty)] = t