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
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
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
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):
<menuitem action='LessOpaque'/>
<separator/>
<menuitem action='Eraser'/>
+ <separator/>
+ <menuitem action='PickContext'/>
</menu>
<menu action='ColorMenu'>
<menuitem action='ColorSelectionWindow'/>
('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),
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()
# 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):
# - 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):
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)
# 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
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):
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]
def copy(self):
return Tile(copy_from=self)
-transparentTile = Tile()
+transparent_tile = Tile()
def get_tiles_bbox(tiles):
res = helpers.Rect()
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