OSDN Git Service

layer move: blank tiles during & after drag
authorAndrew Chadwick <andrewc-git@piffle.org>
Fri, 25 Nov 2011 23:16:12 +0000 (23:16 +0000)
committerAndrew Chadwick <andrewc-git@piffle.org>
Fri, 9 Dec 2011 18:25:47 +0000 (18:25 +0000)
Avoid flickering and some redraws by blanking tiles needing it just
before writing if they're also to be written, or in a big batch right at
the end of processing each position update if they're still pending.
Looks a bit crap and dropping-y for sparse layers, but it seems to work.

Refactor the interactive move by encapsulating its redraw progress state
in a helper object rather than passing lots of stuff around. Move
responsibility for tile-side bookkeeping back into the surface
implementation where it belongs.

gui/dragfunc.py
lib/layer.py
lib/tiledsurface.py

index d93d765..cd8b5b6 100644 (file)
@@ -97,72 +97,54 @@ class LayerMoveDragFunc (DragFunc):
 
     def on_start(self):
         self.layer = self.model.get_current_layer()
-        self.snapshot = None
-        self.chunks = []
-        self.chunks_i = -1
-        self.offsets = None
+        self.move = None
 
     def on_update(self, dx, dy, x, y):
         cr = self.tdw.get_model_coordinates_cairo_context()
         model_x, model_y = cr.device_to_user(x, y)
-        if self.snapshot is None:
+        if self.move is None:
+            self.move = self.layer.get_move(model_x, model_y)
             self.model_x0 = model_x
             self.model_y0 = model_y
-            self.snapshot, self.chunks \
-              = self.layer.begin_interactive_move(model_x, model_y)
-            self.chunks_i = 0
         model_dx = model_x - self.model_x0
         model_dy = model_y - self.model_y0
         self.final_model_dx = model_dx
         self.final_model_dy = model_dy
-        self.offsets = self.layer.update_interactive_move(model_dx, model_dy)
-        self.chunks_i = 0
+        self.move.update(model_dx, model_dy)
         if self.idle_srcid is None:
             self.idle_srcid = gobject.idle_add(self.idle_cb)
 
     def on_stop(self):
         self.idle_srcid = None
-        if self.offsets is not None:
-            if self.chunks_i < len(self.chunks):
-                chunks = self.chunks[self.chunks_i:]
-                self.chunks = []
-                self.chunks_i = -1
-                layer = self.layer
-                self.tdw.set_sensitive(False)
-                self.tdw.set_override_cursor(gdk.Cursor(gdk.WATCH))
-                while gtk.events_pending():
-                    gtk.main_iteration() # HACK to set the cursor
-                # Finish up
-                layer.process_interactive_move_queue(\
-                    self.snapshot, chunks, self.offsets)
-                self.tdw.set_sensitive(True)
-                self.tdw.set_override_cursor(None)
-                self.offsets = None
-            dx = self.final_model_dx
-            dy = self.final_model_dy
-            self.model.record_layer_move(self.layer, dx, dy)
-
-    def idle_cb(self, k=200):
+        if self.move is None:
+            return
+        self.tdw.set_sensitive(False)
+        self.tdw.set_override_cursor(gdk.Cursor(gdk.WATCH))
+        while gtk.events_pending():
+            gtk.main_iteration() # HACK to set the cursor
+        # Finish up
+        self.move.process(n=-1)
+        self.move.cleanup()
+        self.tdw.set_sensitive(True)
+        self.tdw.set_override_cursor(None)
+        self.offsets = None
+        dx = self.final_model_dx
+        dy = self.final_model_dy
+        self.model.record_layer_move(self.layer, dx, dy)
+
+    def idle_cb(self):
         if self.idle_srcid is None:
             # Asked to terminate
+            self.move.cleanup()
             return False
-        assert self.chunks_i >= 0
-        assert self.offsets is not None
-        i = self.chunks_i
-        if self.chunks_i >= len(self.chunks):
-            self.idle_srcid = None
-            return False
-        chunks = self.chunks[i:i+k]
-        self.layer.process_interactive_move_queue(
-                self.snapshot, chunks, self.offsets)
-        self.chunks_i += k
-        if self.chunks_i < len(self.chunks):
+        if self.move.process():
             return True
         else:
-            # Stop, but mark as restartable
+            self.move.cleanup()
             self.idle_srcid = None
             return False
 
+
 class MoveFrameDragFunc (DragFunc):
 
     cursor = gdk.ICON
index 679e875..8c3a1f7 100644 (file)
@@ -100,38 +100,16 @@ class Layer:
     def translate(self, dx, dy):
         """Translate a layer non-interactively.
         """
-        snapshot, chunks = self.begin_interactive_move(0, 0)
-        offsets = self.update_interactive_move(dx, dy)
-        self.process_interactive_move_queue(snapshot, chunks, offsets)
+        move = self.get_move(0, 0)
+        move.update(dx, dy)
+        move.process(n=-1)
+        move.cleanup()
 
 
-    def begin_interactive_move(self, x, y):
-        """Start an interactive move.
-
-        Returns ``(snapshot, chunks)``. In the current implementation,
-        ``chunks`` is a list of tile indices which is sorted by proximity to
-        the initial position.
-        """
-        return self._surface.begin_interactive_move(x, y)
-
-
-    def update_interactive_move(self, dx, dy):
-        """Update for a new offset during an interactive move.
-
-        Call whenever there's a new pointer position in the drag. Returns a
-        single offsets object. After calling, reprocess the chunks queue with
-        the new offsets. This method blanks the current layer, so the chunks
-        queue must be processed fully after the final call to it.
+    def get_move(self, x, y):
+        """Get a translation/move object for this layer.
         """
-        surf = self._surface
-        return surf.update_interactive_move(dx, dy)
-
-
-    def process_interactive_move_queue(self, snapshot, chunks, offsets):
-        """Processes part of an interactive move.
-        """
-        surf = self._surface
-        return surf.process_interactive_move_queue(snapshot, chunks, offsets)
+        return self._surface.get_move(x, y)
 
 
     def add_stroke(self, stroke, snapshot_before):
@@ -142,6 +120,7 @@ class Layer:
         shape.brush_string = stroke.brush_settings
         self.strokes.append(shape)
 
+
     def save_strokemap_to_file(self, f, translate_x, translate_y):
         brush2id = {}
         for stroke in self.strokes:
index 3d6dd4f..705488c 100644 (file)
@@ -406,85 +406,95 @@ class Surface(mypaintlib.TiledSurface):
             if not data.rgba.any():
                 self.tiledict.pop(pos)
 
+    def get_move(self, x, y):
+        return _InteractiveMove(self, x, y)
 
-    def begin_interactive_move(self, x, y):
-        snapshot = self.save_snapshot()
-        chunks = snapshot.tiledict.keys()
+
+class _InteractiveMove:
+
+    def __init__(self, surface, x, y):
+        self.surface = surface
+        self.snapshot = surface.save_snapshot()
+        self.chunks = self.snapshot.tiledict.keys()
         tx = x // N
         ty = y // N
-        #chebyshev = lambda p: max(abs(tx - p[0]), abs(ty - p[1]))
-        #euclidean = lambda p: math.sqrt((tx - p[0])**2 + (ty - p[1])**2)
-        def mandala(p):
-            # A sort of distance weighted, dithered effect favouring
-            # the area around the mouse pointer.
-            c = max(abs(tx - p[0]), abs(ty - p[1]))
-            for i in (17,13,11,7,5,3,2):
-                if (p[0] % i == 0) and (p[1] % i == 0):
-                    return c // i
-            return c
-        chunks.sort(key=mandala)
-        return (snapshot, chunks)
-
-
-    def update_interactive_move(self, dx, dy):
-        # Blank everything currently in the layer
-        dirty_tiles = set(self.tiledict.keys())
-        self.tiledict = {}
-        for dirty_pos in dirty_tiles:
-            self._mark_mipmap_dirty(*dirty_pos)
-        bbox = get_tiles_bbox(dirty_tiles)
-        self.notify_observers(*bbox)
+        chebyshev = lambda p: max(abs(tx - p[0]), abs(ty - p[1]))
+        manhattan = lambda p: abs(tx - p[0]) + abs(ty - p[1])
+        euclidean = lambda p: math.sqrt((tx - p[0])**2 + (ty - p[1])**2)
+        self.chunks.sort(key=manhattan)
+        self.chunks_i = 0
+
+    def update(self, dx, dy):
+        # Tiles to be blanked at the end of processing
+        self.blanked = set(self.surface.tiledict.keys())
         # Calculate offsets
-        dx = int(dx)
-        dy = int(dy)
-        slices_x = calc_translation_slices(dx)
-        slices_y = calc_translation_slices(dy)
-        return (slices_x, slices_y)
-
-
-    def process_interactive_move_queue(self, snapshot, chunks, offsets):
-        dirty = set()
-        slices_x, slices_y = offsets
-        for tile_pos in chunks:
+        self.slices_x = self._calc_slices(int(dx))
+        self.slices_y = self._calc_slices(int(dy))
+        self.chunks_i = 0
+
+    def cleanup(self):
+        # called at the end of each set of processing batches
+        for b in self.blanked:
+            self.surface.tiledict.pop(b, None)
+            self.surface._mark_mipmap_dirty(*b)
+        bbox = get_tiles_bbox(self.blanked)
+        self.surface.notify_observers(*bbox)
+
+    def process(self, n=200):
+        if self.chunks_i > len(self.chunks):
+            return False
+        written = set()
+        if n <= 0:
+            n = len(self.chunks)  # process all remaining
+        for tile_pos in self.chunks[self.chunks_i : self.chunks_i + n]:
             src_tx, src_ty = tile_pos
-            src_tile = snapshot.tiledict[(src_tx, src_ty)]
-            is_integral = len(slices_x) == 1 and len(slices_y) == 1
-            for (src_x0, src_x1), (targ_tdx, targ_x0, targ_x1) in slices_x:
-                for (src_y0, src_y1), (targ_tdy, targ_y0, targ_y1) in slices_y:
+            src_tile = self.snapshot.tiledict[(src_tx, src_ty)]
+            is_integral = len(self.slices_x) == 1 and len(self.slices_y) == 1
+            for (src_x0, src_x1), (targ_tdx, targ_x0, targ_x1) in self.slices_x:
+                for (src_y0, src_y1), (targ_tdy, targ_y0, targ_y1) in self.slices_y:
                     targ_tx = src_tx + targ_tdx
                     targ_ty = src_ty + targ_tdy
                     if is_integral:
-                        self.tiledict[(targ_tx, targ_ty)] = src_tile.copy()
+                        self.surface.tiledict[(targ_tx, targ_ty)] = src_tile.copy()
                     else:
-                        targ_tile = self.tiledict.get((targ_tx, targ_ty), None)
+                        targ_tile = None
+                        if (targ_tx, targ_ty) in self.blanked:
+                            targ_tile = Tile()
+                            self.surface.tiledict[(targ_tx, targ_ty)] = targ_tile
+                            self.blanked.remove( (targ_tx, targ_ty) )
+                        else:
+                            targ_tile = self.surface.tiledict.get((targ_tx, targ_ty), None)
                         if targ_tile is None:
                             targ_tile = Tile()
-                            self.tiledict[(targ_tx, targ_ty)] = targ_tile
+                            self.surface.tiledict[(targ_tx, targ_ty)] = targ_tile
                         targ_tile.rgba[targ_y0:targ_y1, targ_x0:targ_x1] \
                           = src_tile.rgba[src_y0:src_y1, src_x0:src_x1]
-                    dirty.add((targ_tx, targ_ty))
-        for dirty_pos in dirty:
-            self._mark_mipmap_dirty(*dirty_pos)
-        bbox = get_tiles_bbox(dirty) # hopefully relatively contiguous
-        self.notify_observers(*bbox)
-
-
-def calc_translation_slices(dc):
-    """Returns a list of offsets and slice extents for a translation of `dc`.
-
-    The returned slice list's members are of the form
-
-        ((src_c0, src_c1), (targ_tdc, targ_c0, targ_c1))
-
-    where ``src_c0`` and ``src_c1`` determine the extents of the source slice
-    within a tile, their ``targ_`` equivalents specify where to put that slice
-    in the target tile, and ``targ_tdc`` is the tile offset.
-    """
-    dcr = dc % N
-    tdc = (dc // N)
-    if dcr == 0:
-        return [ ((0, N), (tdc, 0, N)) ]
-    else:
-        return [ ((0, N-dcr), (tdc, dcr, N)) ,
-                 ((N-dcr, N), (tdc+1, 0, dcr)) ]
+                    written.add((targ_tx, targ_ty))
+        self.blanked -= written
+        for pos in written:
+            self.surface._mark_mipmap_dirty(*pos)
+        bbox = get_tiles_bbox(written) # hopefully relatively contiguous
+        self.surface.notify_observers(*bbox)
+        self.chunks_i += n
+        return self.chunks_i < len(self.chunks)
+
+    @staticmethod
+    def _calc_slices(dc):
+        """Returns a list of offsets and slice extents for a translation of `dc`.
+
+        The returned slice list's members are of the form
+
+            ((src_c0, src_c1), (targ_tdc, targ_c0, targ_c1))
+
+        where ``src_c0`` and ``src_c1`` determine the extents of the source slice
+        within a tile, their ``targ_`` equivalents specify where to put that slice
+        in the target tile, and ``targ_tdc`` is the tile offset.
+        """
+        dcr = dc % N
+        tdc = (dc // N)
+        if dcr == 0:
+            return [ ((0, N), (tdc, 0, N)) ]
+        else:
+            return [ ((0, N-dcr), (tdc, dcr, N)) ,
+                     ((N-dcr, N), (tdc+1, 0, dcr)) ]