OSDN Git Service

layer move: blank tiles during & after drag
[mypaint-anime/master.git] / lib / layer.py
index f86c650..8c3a1f7 100644 (file)
 # the Free Software Foundation; either version 2 of the License, or
 # (at your option) any later version.
 
-import tiledsurface
+import struct, zlib
+from numpy import *
+from gettext import gettext as _
+
+import tiledsurface, strokemap
+
+COMPOSITE_OPS = [
+    # (internal-name, display-name)
+    ("svg:src-over", _("Normal")),
+    ("svg:multiply", _("Multiply")),
+    ("svg:color-burn", _("Burn")),
+    ("svg:color-dodge", _("Dodge")),
+    ("svg:screen", _("Screen")),
+    ]
+
+DEFAULT_COMPOSITE_OP = COMPOSITE_OPS[0][0]
+VALID_COMPOSITE_OPS = set([n for n, d in COMPOSITE_OPS])
 
 class Layer:
-    # A layer contains a list of strokes, and possibly a background
-    # pixmap. There is also a surface with those strokes rendered.
-    #
-    # Some history from the time when each snapshot required a
-    # larger-than-screen pixbuf: The undo system used to work by
-    # finding the closest snapshot (there were only two or three of
-    # them) and rerendering the missing strokes. There used to be
-    # quite some generic code here for this. The last svn revision
-    # including this code was r242.
-    #
-    # Now the strokes are here just for possible future features, like
-    # "pick brush from stroke", or "repaint layer with different
-    # brush" for brush previews.
-    #
-    # The strokes don't take much memory compared to the snapshots
-    # IMO, but this should better be measured...
-
-    def __init__(self):
-        self.surface = tiledsurface.Surface()
-        self.strokes = []
-        self.background = None
+    """Representation of a layer in the document model.
+
+    The actual content of the layer is held by the surface implementation.
+    This is an internal detail that very few consumers should care about."""
+
+    def __init__(self, name="", compositeop=DEFAULT_COMPOSITE_OP):
+        self._surface = tiledsurface.Surface()
+        self.opacity = 1.0
+        self.name = name
+        self.visible = True
+        self.locked = False
+        self.compositeop = compositeop
+        # Called when contents of layer changed,
+        # with the bounding box of the changed region
+        self.content_observers = []
+
+        # Forward from surface implementation
+        self._surface.observers.append(self._notify_content_observers)
+
+        self.clear()
+
+    def _notify_content_observers(self, *args):
+        for f in self.content_observers:
+            f(*args)
+
+    def get_effective_opacity(self):
+        if self.visible:
+            return self.opacity
+        else:
+            return 0.0
+    effective_opacity = property(get_effective_opacity)
+
+    def get_alpha(self, x, y, radius):
+        return self._surface.get_alpha(x, y, radius)
+
+    def get_bbox(self):
+        return self._surface.get_bbox()
+
+    def is_empty(self):
+        return self._surface.is_empty()
+
+    def save_as_png(self, filename, *args, **kwargs):
+        self._surface.save_as_png(filename, *args, **kwargs)
+
+    def stroke_to(self, brush, x, y, pressure, xtilt, ytilt, dtime):
+        """Render a part of a stroke."""
+        self._surface.begin_atomic()
+        split = brush.stroke_to(self._surface, x, y,
+                                    pressure, xtilt, ytilt, dtime)
+        self._surface.end_atomic()
+        return split
 
     def clear(self):
-        self.background = None
-        self.strokes = []
-        self.surface.clear()
+        self.strokes = [] # contains StrokeShape instances (not stroke.Stroke)
+        self._surface.clear()
 
-    def load_from_pixbuf(self, pixbuf):
+    def load_from_surface(self, surface):
         self.strokes = []
-        self.background = pixbuf
-        self.surface.load_from_data(pixbuf)
+        self._surface.load_from_surface(surface)
+
+    def render_as_pixbuf(self, *rect, **kwargs):
+        return self._surface.render_as_pixbuf(*rect, **kwargs)
 
     def save_snapshot(self):
-        return (self.strokes[:], self.background, self.surface.save_snapshot())
+        return (self.strokes[:], self._surface.save_snapshot(), self.opacity)
 
     def load_snapshot(self, data):
-        strokes, background, data = data
+        strokes, data, self.opacity = data
         self.strokes = strokes[:]
-        self.background = background
-        self.surface.load_snapshot(data)
+        self._surface.load_snapshot(data)
+
+
+    def translate(self, dx, dy):
+        """Translate a layer non-interactively.
+        """
+        move = self.get_move(0, 0)
+        move.update(dx, dy)
+        move.process(n=-1)
+        move.cleanup()
+
+
+    def get_move(self, x, y):
+        """Get a translation/move object for this layer.
+        """
+        return self._surface.get_move(x, y)
+
+
+    def add_stroke(self, stroke, snapshot_before):
+        before = snapshot_before[1] # extract surface snapshot
+        after  = self._surface.save_snapshot()
+        shape = strokemap.StrokeShape()
+        shape.init_from_snapshots(before, after)
+        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:
+            s = stroke.brush_string
+            # save brush (if not already known)
+            if s not in brush2id:
+                brush2id[s] = len(brush2id)
+                s = zlib.compress(s)
+                f.write('b')
+                f.write(struct.pack('>I', len(s)))
+                f.write(s)
+            # save stroke
+            s = stroke.save_to_string(translate_x, translate_y)
+            f.write('s')
+            f.write(struct.pack('>II', brush2id[stroke.brush_string], len(s)))
+            f.write(s)
+        f.write('}')
+
+
+    def load_strokemap_from_file(self, f, translate_x, translate_y):
+        assert not self.strokes
+        brushes = []
+        while True:
+            t = f.read(1)
+            if t == 'b':
+                length, = struct.unpack('>I', f.read(4))
+                tmp = f.read(length)
+                brushes.append(zlib.decompress(tmp))
+            elif t == 's':
+                brush_id, length = struct.unpack('>II', f.read(2*4))
+                stroke = strokemap.StrokeShape()
+                tmp = f.read(length)
+                stroke.init_from_string(tmp, translate_x, translate_y)
+                stroke.brush_string = brushes[brush_id]
+                self.strokes.append(stroke)
+            elif t == '}':
+                break
+            else:
+                assert False, 'invalid strokemap'
 
     def merge_into(self, dst):
         """
-        Merges this layer into dst, modifying only dst.
+        Merge this layer into dst, modifying only dst.
         """
+        # We must respect layer visibility, because saving a
+        # transparent PNG just calls this function for each layer.
         src = self
-        dst.background = None # hm... this breaks "full-rerender" capability, but should work fine... FIXME: redesign needed?
-        dst.strokes = [] # this one too...
-        for tx, ty in src.surface.get_tiles():
-            src.surface.composite_tile_over(dst.surface.get_tile_memory(tx, ty, readonly=False), tx, ty)
+        dst.strokes.extend(self.strokes)
+        for tx, ty in dst._surface.get_tiles():
+            surf = dst._surface.get_tile_memory(tx, ty, readonly=False)
+            surf[:,:,:] = dst.effective_opacity * surf[:,:,:]
+        for tx, ty in src._surface.get_tiles():
+            surf = dst._surface.get_tile_memory(tx, ty, readonly=False)
+            src._surface.composite_tile(surf, tx, ty,
+                opacity=self.effective_opacity,
+                mode=self.compositeop)
+        dst.opacity = 1.0
+
+    def get_stroke_info_at(self, x, y):
+        x, y = int(x), int(y)
+        for s in reversed(self.strokes):
+            if s.touches_pixel(x, y):
+                return s
+
+    def get_last_stroke_info(self):
+        if not self.strokes:
+            return None
+        return self.strokes[-1]