Permalink
Browse files

flood-fill: complete implementation

Implement flood fill mode.
Add proper icons (taken from Inkscape's dist; GPLv2).
Various range bugfxes.
Flood-fill without a frame or any content at all fills first: one tile,
with no frame and some content: the area including the existing content and
the seed point.
Fill empty tiles with a slice assignment.
  • Loading branch information...
1 parent c56355e commit 0d3ab8d4ad3ff9c600aa53dc3fa3691f1de4a9fa @achadwick achadwick committed Sep 12, 2013
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -21,6 +21,7 @@
import canvasevent
from overlays import rounded_box, Overlay
import colors
+import fill
## Color picking mode, with a preview rectangle overlay
@@ -57,7 +58,8 @@ def stackable_on(self, mode):
# Any drawing mode
import linemode
return isinstance(mode, linemode.LineModeBase) \
- or isinstance(mode, canvasevent.SwitchableFreehandMode)
+ or isinstance(mode, canvasevent.SwitchableFreehandMode) \
+ or isinstance(mode, fill.FloodFillMode)
def __init__(self, **kwds):
View
@@ -0,0 +1,103 @@
+# This file is part of MyPaint.
+# Copyright (C) 2013 by Andrew Chadwick <a.t.chadwick@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+
+"""Flood fill tool"""
+
+## Imports
+
+import gi
+from gi.repository import Gdk
+from gettext import gettext as _
+
+import canvasevent
+
+from application import get_app
+
+## Class defs
+
+class FloodFillMode (canvasevent.SwitchableModeMixin,
+ canvasevent.ScrollableModeMixin,
+ canvasevent.SingleClickMode):
+ """Mode for flood-filling with the current brush color"""
+
+ ## Class constants
+
+ __action_name__ = "FloodFillMode"
+ permitted_switch_actions = set([
+ 'RotateViewMode', 'ZoomViewMode', 'PanViewMode',
+ 'ColorPickMode',
+ ])
+
+
+ ## Instance vars (and defaults)
+
+ _inside_frame = True
+ _x = None
+ _y = None
+
+ @property
+ def cursor(self):
+ app = get_app()
+ if self._inside_frame:
+ name = "cursor_crosshair_precise_open"
+ else:
+ name = "cursor_arrow_forbidden"
+ return app.cursors.get_action_cursor(self.__action_name__, name)
+
+
+ ## Method defs
+
+ def enter(self, **kwds):
+ super(FloodFillMode, self).enter(**kwds)
+ self._tdws = set()
+
+ @classmethod
+ def get_name(cls):
+ return _(u'Flood Fill')
+
+ def get_usage(self):
+ return _(u"Click to fill with the current color")
+
+ def __init__(self, ignore_modifiers=False, **kwds):
+ super(FloodFillMode, self).__init__(**kwds)
+
+ def clicked_cb(self, tdw, event):
+ x, y = tdw.display_to_model(event.x, event.y)
+ self._x = x
+ self._y = y
+ self._tdws.add(tdw)
+ self._update_cursor()
+ color = self.doc.app.brush_color_manager.get_color()
+ self.doc.model.flood_fill(x, y, color.get_rgb())
+ return False
+
+ def motion_notify_cb(self, tdw, event):
+ x, y = tdw.display_to_model(event.x, event.y)
+ self._x = x
+ self._y = y
+ self._tdws.add(tdw)
+ self._update_cursor()
+ return super(FloodFillMode, self).motion_notify_cb(tdw, event)
+
+ def model_structure_changed_cb(self, doc):
+ super(FloodFillMode, self).model_structure_changed_cb(doc)
+ self._update_cursor()
+
+ def _update_cursor(self):
+ x, y = self._x, self._y
+ model = self.doc.model
+ was_inside = self._inside_frame
+ if not model.frame_enabled:
+ self._inside_frame and not model.layer.is_empty()
+ else:
+ fx1, fy1, fw, fh = model.get_frame()
+ fx2, fy2 = fx1+fw, fy1+fh
+ self._inside_frame = (x >= fx1 and y >= fy1 and
+ x < fx2 and y < fy2)
+ if was_inside != self._inside_frame:
+ for tdw in self._tdws:
+ tdw.set_override_cursor(self.cursor)
View
@@ -1539,7 +1539,7 @@ Ctrl+Shift: constrain angle.</property>
<object class="GtkRadioAction" id="FloodFillMode">
<property name="label" translatable="yes"
context="Menu|Edit|Current Mode|">Flood Fill</property>
- <property name="icon-name">insert-object</property>
+ <property name="icon-name">mypaint-tool-flood-fill</property>
<property name="tooltip" translatable="yes"
>Pick a color from the screen</property>
<property name="group">ColorPickMode</property>
@@ -1550,7 +1550,7 @@ Ctrl+Shift: constrain angle.</property>
<object class="GtkAction" id="FlipFloodFillMode">
<property name="label" translatable="yes"
context="Menu|Edit|">Flood Fill</property>
- <property name="icon-name">insert-object</property>
+ <property name="icon-name">mypaint-tool-flood-fill</property>
<signal name="activate" handler="mode_flip_action_activated_cb"/>
</object>
</child>
View
@@ -38,6 +38,7 @@ the Free Software Foundation; either version 2 of the License, or
<placeholder name="blendmodes-toolitems"/>
<placeholder name="linemodes-toolitems"/>
<toolitem action="ColorPickMode"/>
+ <toolitem action="FloodFillMode"/>
<toolitem action="LayerMoveMode"/>
<toolitem action="FrameEditMode"/>
<toolitem action="Symmetry"/>
View
@@ -347,9 +347,26 @@ def flood_fill(self, x, y, color):
:param y: Starting point Y coordinate
:param color: an RGB color
:type color: tuple
- """
- self.do(command.FloodFill(self, x, y, color, self.get_effective_bbox()))
+ Filling an infinite canvas requires limits. If the frame is enabled,
+ this limits the maximum size of the fill, and filling outside the frame
+ is not possible.
+
+ Otherwise, if the entire document is empty, the limits are dynamic.
+ Initially only a single tile will be filled. This can then form one
+ corner for the next fill's limiting rectangle. This is a little quirky,
+ but allows big areas to be filled rapidly as needed on blank layers.
+ """
+ bbox = helpers.Rect(*tuple(self.get_effective_bbox()))
+ if bbox.empty():
+ bbox = helpers.Rect()
+ bbox.x = N*int(x//N)
+ bbox.y = N*int(y//N)
+ bbox.w = N
+ bbox.h = N
+ elif not self.frame_enabled:
+ bbox.expandToIncludePoint(x, y)
+ self.do(command.FloodFill(self, x, y, color, bbox))
def layer_modified_cb(self, *args):
View
@@ -110,10 +110,10 @@ tile_flood_fill (PyObject *tile, /* HxWx4 array of uint16 */
#endif
if (min_x < 0) min_x = 0;
if (min_y < 0) min_y = 0;
- if (max_x >= MYPAINT_TILE_SIZE) max_x = MYPAINT_TILE_SIZE;
- if (max_y >= MYPAINT_TILE_SIZE) max_y = MYPAINT_TILE_SIZE;
+ if (max_x > MYPAINT_TILE_SIZE-1) max_x = MYPAINT_TILE_SIZE-1;
+ if (max_y > MYPAINT_TILE_SIZE-1) max_y = MYPAINT_TILE_SIZE-1;
if (min_x > max_x || min_y > max_y) {
- return Py_BuildValue("[]");
+ return Py_BuildValue("[()()()()]");
}
// Populate a working queue with seeds
@@ -123,6 +123,8 @@ tile_flood_fill (PyObject *tile, /* HxWx4 array of uint16 */
PyObject *seed_tup = PySequence_GetItem(seeds, i);
x = (int) PyInt_AsLong(PySequence_GetItem(seed_tup, 0));
y = (int) PyInt_AsLong(PySequence_GetItem(seed_tup, 1));
+ x = MAX(0, MIN(x, MYPAINT_TILE_SIZE-1));
+ y = MAX(0, MIN(y, MYPAINT_TILE_SIZE-1));
// Skip seed point if we've already been here
const fix15_short_t *pixel = _floodfill_getpixel(tile_arr, x, y);
if ( (pixel[0] == fill_r15) &&
@@ -163,7 +165,7 @@ tile_flood_fill (PyObject *tile, /* HxWx4 array of uint16 */
for (int i=0; i<2; ++i)
{
for ( int x = x0 + x_offset[i] ;
- x >= min_x && x < max_x ;
+ x >= min_x && x <= max_x ;
x += x_delta[i] )
{
// Halt expansion if we've already filled this pixel
@@ -181,7 +183,7 @@ tile_flood_fill (PyObject *tile, /* HxWx4 array of uint16 */
break;
}
// Also halt if we're outside the bbox range
- if (x < min_x || y < min_y || x >= max_x || y >= max_y) {
+ if (x < min_x || y < min_y || x > max_x || y > max_y) {
break;
}
// Fill this pixel, and continue iterating in this direction.
@@ -236,6 +238,4 @@ tile_flood_fill (PyObject *tile, /* HxWx4 array of uint16 */
}
-
-
#endif //__HAVE_FILL
View
@@ -37,8 +37,9 @@
except ImportError:
raise ImportError("Could not import json. You either need to use python >= 2.6 or install one of python-cjson, python-json or python-simplejson.")
-class Rect:
+class Rect (object):
def __init__(self, x=0, y=0, w=0, h=0):
+ object.__init__(self)
self.x = x
self.y = y
self.w = w
View
@@ -551,7 +551,22 @@ def flood_fill(self, x, y, color, bbox):
# Colour to fill with
fill_r, fill_g, fill_b = color
- # Tile and pixel addressind for the seed point
+ # Maximum area to fill: tile and in-tile pixel extents
+ bbx, bby, bbw, bbh = bbox
+ if bbh <= 0 or bbw <= 0:
+ return
+ bbbrx = bbx + bbw - 1
+ bbbry = bby + bbh - 1
+ min_tx = int(bbx // N)
+ min_ty = int(bby // N)
+ max_tx = int(bbbrx // N)
+ max_ty = int(bbbry // N)
+ min_px = int(bbx % N)
+ min_py = int(bby % N)
+ max_px = int(bbbrx % N)
+ max_py = int(bbbry % N)
+
+ # Tile and pixel addressing for the seed point
tx, ty = int(x//N), int(y//N)
px, py = int(x%N), int(y%N)
@@ -564,17 +579,6 @@ def flood_fill(self, x, y, color, bbox):
targ_b = 0
targ_a = 0
- # Maximum area to fill: tile and in-tile pixel extents
- bbx, bby, bbw, bbh = bbox
- min_tx = int(bbx // N)
- min_ty = int(bby // N)
- max_tx = int((bbx + bbw) // N)
- max_ty = int((bby + bbh) // N)
- min_px = int(bbx % N)
- min_py = int(bby % N)
- max_px = int((bbx+bbw) % N)
- max_py = int((bby+bbh) % N)
-
# Flood-fill loop
dirty_tiles = set()
tileq = [ ((tx, ty), [(px, py)]) ]
@@ -588,8 +592,8 @@ def flood_fill(self, x, y, color, bbox):
# Pixel limits within this tile...
min_x = 0
min_y = 0
- max_x = N
- max_y = N
+ max_x = N-1
+ max_y = N-1
# ... vary at the edges
if tx == min_tx:
min_x = min_px
@@ -600,23 +604,33 @@ def flood_fill(self, x, y, color, bbox):
if ty == max_ty:
max_y = max_py
# Flood-fill one tile
+ existing = (tx, ty) in self.tiledict
+ one = 1<<15
+ col = (int(fill_r*one), int(fill_g*one), int(fill_b*one), one)
with self.tile_request(tx, ty, readonly=False) as tile:
- overflows = mypaintlib.tile_flood_fill(tile, seeds,
- targ_r, targ_g, targ_b, targ_a,
- fill_r, fill_g, fill_b,
- min_x, min_y, max_x, max_y)
+ if not existing:
+ tile[min_y:max_y+1,min_x:max_x+1] = col
+ seeds_n = zip(range(min_x, max_x+1), [N-1]*N)
+ seeds_e = zip([0]*N, range(min_y, min_y+1))
+ seeds_s = zip(range(min_x, max_x+1), [0]*N)
+ seeds_w = zip([N-1]*N, range(min_y, min_y+1))
+ else:
+ overflows = mypaintlib.tile_flood_fill(tile, seeds,
+ targ_r, targ_g, targ_b, targ_a,
+ fill_r, fill_g, fill_b,
+ min_x, min_y, max_x, max_y)
+ seeds_n, seeds_e, seeds_s, seeds_w = overflows
# Enqueue overflows in each cardinal direction
- seeds_n, seeds_e, seeds_s, seeds_w = overflows
- if seeds_n:
+ if seeds_n and ty > min_ty:
tpos = (tx, ty-1)
tileq.append((tpos, seeds_n))
- if seeds_w:
+ if seeds_w and tx > min_tx:
tpos = (tx-1, ty)
tileq.append((tpos, seeds_w))
- if seeds_s:
+ if seeds_s and ty < max_ty:
tpos = (tx, ty+1)
tileq.append((tpos, seeds_s))
- if seeds_e:
+ if seeds_e and tx < max_tx:
tpos = (tx+1, ty)
tileq.append((tpos, seeds_e))
# Ensure tile will be redrawn
Binary file not shown.

0 comments on commit 0d3ab8d

Please sign in to comment.