Skip to content

Commit

Permalink
flood-fill: complete implementation
Browse files Browse the repository at this point in the history
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
Andrew Chadwick committed Sep 30, 2013
1 parent c56355e commit 0d3ab8d
Show file tree
Hide file tree
Showing 15 changed files with 872 additions and 36 deletions.
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.
698 changes: 698 additions & 0 deletions desktop/icons/hicolor/scalable/actions/mypaint-tool-flood-fill.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion gui/colorpicker.py
Expand Up @@ -21,6 +21,7 @@
import canvasevent import canvasevent
from overlays import rounded_box, Overlay from overlays import rounded_box, Overlay
import colors import colors
import fill




## Color picking mode, with a preview rectangle overlay ## Color picking mode, with a preview rectangle overlay
Expand Down Expand Up @@ -57,7 +58,8 @@ def stackable_on(self, mode):
# Any drawing mode # Any drawing mode
import linemode import linemode
return isinstance(mode, linemode.LineModeBase) \ return isinstance(mode, linemode.LineModeBase) \
or isinstance(mode, canvasevent.SwitchableFreehandMode) or isinstance(mode, canvasevent.SwitchableFreehandMode) \
or isinstance(mode, fill.FloodFillMode)




def __init__(self, **kwds): def __init__(self, **kwds):
Expand Down
103 changes: 103 additions & 0 deletions gui/fill.py
@@ -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)
4 changes: 2 additions & 2 deletions gui/resources.xml
Expand Up @@ -1539,7 +1539,7 @@ Ctrl+Shift: constrain angle.</property>
<object class="GtkRadioAction" id="FloodFillMode"> <object class="GtkRadioAction" id="FloodFillMode">
<property name="label" translatable="yes" <property name="label" translatable="yes"
context="Menu|Edit|Current Mode|">Flood Fill</property> 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" <property name="tooltip" translatable="yes"
>Pick a color from the screen</property> >Pick a color from the screen</property>
<property name="group">ColorPickMode</property> <property name="group">ColorPickMode</property>
Expand All @@ -1550,7 +1550,7 @@ Ctrl+Shift: constrain angle.</property>
<object class="GtkAction" id="FlipFloodFillMode"> <object class="GtkAction" id="FlipFloodFillMode">
<property name="label" translatable="yes" <property name="label" translatable="yes"
context="Menu|Edit|">Flood Fill</property> 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"/> <signal name="activate" handler="mode_flip_action_activated_cb"/>
</object> </object>
</child> </child>
Expand Down
1 change: 1 addition & 0 deletions gui/toolbar.xml
Expand Up @@ -38,6 +38,7 @@ the Free Software Foundation; either version 2 of the License, or
<placeholder name="blendmodes-toolitems"/> <placeholder name="blendmodes-toolitems"/>
<placeholder name="linemodes-toolitems"/> <placeholder name="linemodes-toolitems"/>
<toolitem action="ColorPickMode"/> <toolitem action="ColorPickMode"/>
<toolitem action="FloodFillMode"/>
<toolitem action="LayerMoveMode"/> <toolitem action="LayerMoveMode"/>
<toolitem action="FrameEditMode"/> <toolitem action="FrameEditMode"/>
<toolitem action="Symmetry"/> <toolitem action="Symmetry"/>
Expand Down
21 changes: 19 additions & 2 deletions lib/document.py
Expand Up @@ -347,9 +347,26 @@ def flood_fill(self, x, y, color):
:param y: Starting point Y coordinate :param y: Starting point Y coordinate
:param color: an RGB color :param color: an RGB color
:type color: tuple :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): def layer_modified_cb(self, *args):
Expand Down
14 changes: 7 additions & 7 deletions lib/fill.hpp
Expand Up @@ -110,10 +110,10 @@ tile_flood_fill (PyObject *tile, /* HxWx4 array of uint16 */
#endif #endif
if (min_x < 0) min_x = 0; if (min_x < 0) min_x = 0;
if (min_y < 0) min_y = 0; if (min_y < 0) min_y = 0;
if (max_x >= MYPAINT_TILE_SIZE) max_x = MYPAINT_TILE_SIZE; if (max_x > MYPAINT_TILE_SIZE-1) max_x = MYPAINT_TILE_SIZE-1;
if (max_y >= MYPAINT_TILE_SIZE) max_y = MYPAINT_TILE_SIZE; if (max_y > MYPAINT_TILE_SIZE-1) max_y = MYPAINT_TILE_SIZE-1;
if (min_x > max_x || min_y > max_y) { if (min_x > max_x || min_y > max_y) {
return Py_BuildValue("[]"); return Py_BuildValue("[()()()()]");
} }


// Populate a working queue with seeds // Populate a working queue with seeds
Expand All @@ -123,6 +123,8 @@ tile_flood_fill (PyObject *tile, /* HxWx4 array of uint16 */
PyObject *seed_tup = PySequence_GetItem(seeds, i); PyObject *seed_tup = PySequence_GetItem(seeds, i);
x = (int) PyInt_AsLong(PySequence_GetItem(seed_tup, 0)); x = (int) PyInt_AsLong(PySequence_GetItem(seed_tup, 0));
y = (int) PyInt_AsLong(PySequence_GetItem(seed_tup, 1)); 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 // Skip seed point if we've already been here
const fix15_short_t *pixel = _floodfill_getpixel(tile_arr, x, y); const fix15_short_t *pixel = _floodfill_getpixel(tile_arr, x, y);
if ( (pixel[0] == fill_r15) && if ( (pixel[0] == fill_r15) &&
Expand Down Expand Up @@ -163,7 +165,7 @@ tile_flood_fill (PyObject *tile, /* HxWx4 array of uint16 */
for (int i=0; i<2; ++i) for (int i=0; i<2; ++i)
{ {
for ( int x = x0 + x_offset[i] ; for ( int x = x0 + x_offset[i] ;
x >= min_x && x < max_x ; x >= min_x && x <= max_x ;
x += x_delta[i] ) x += x_delta[i] )
{ {
// Halt expansion if we've already filled this pixel // Halt expansion if we've already filled this pixel
Expand All @@ -181,7 +183,7 @@ tile_flood_fill (PyObject *tile, /* HxWx4 array of uint16 */
break; break;
} }
// Also halt if we're outside the bbox range // 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; break;
} }
// Fill this pixel, and continue iterating in this direction. // Fill this pixel, and continue iterating in this direction.
Expand Down Expand Up @@ -236,6 +238,4 @@ tile_flood_fill (PyObject *tile, /* HxWx4 array of uint16 */
} }






#endif //__HAVE_FILL #endif //__HAVE_FILL
3 changes: 2 additions & 1 deletion lib/helpers.py
Expand Up @@ -37,8 +37,9 @@
except ImportError: 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.") 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): def __init__(self, x=0, y=0, w=0, h=0):
object.__init__(self)
self.x = x self.x = x
self.y = y self.y = y
self.w = w self.w = w
Expand Down
60 changes: 37 additions & 23 deletions lib/tiledsurface.py
Expand Up @@ -551,7 +551,22 @@ def flood_fill(self, x, y, color, bbox):
# Colour to fill with # Colour to fill with
fill_r, fill_g, fill_b = color 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) tx, ty = int(x//N), int(y//N)
px, py = int(x%N), int(y%N) px, py = int(x%N), int(y%N)


Expand All @@ -564,17 +579,6 @@ def flood_fill(self, x, y, color, bbox):
targ_b = 0 targ_b = 0
targ_a = 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 # Flood-fill loop
dirty_tiles = set() dirty_tiles = set()
tileq = [ ((tx, ty), [(px, py)]) ] tileq = [ ((tx, ty), [(px, py)]) ]
Expand All @@ -588,8 +592,8 @@ def flood_fill(self, x, y, color, bbox):
# Pixel limits within this tile... # Pixel limits within this tile...
min_x = 0 min_x = 0
min_y = 0 min_y = 0
max_x = N max_x = N-1
max_y = N max_y = N-1
# ... vary at the edges # ... vary at the edges
if tx == min_tx: if tx == min_tx:
min_x = min_px min_x = min_px
Expand All @@ -600,23 +604,33 @@ def flood_fill(self, x, y, color, bbox):
if ty == max_ty: if ty == max_ty:
max_y = max_py max_y = max_py
# Flood-fill one tile # 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: with self.tile_request(tx, ty, readonly=False) as tile:
overflows = mypaintlib.tile_flood_fill(tile, seeds, if not existing:
targ_r, targ_g, targ_b, targ_a, tile[min_y:max_y+1,min_x:max_x+1] = col
fill_r, fill_g, fill_b, seeds_n = zip(range(min_x, max_x+1), [N-1]*N)
min_x, min_y, max_x, max_y) 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 # Enqueue overflows in each cardinal direction
seeds_n, seeds_e, seeds_s, seeds_w = overflows if seeds_n and ty > min_ty:
if seeds_n:
tpos = (tx, ty-1) tpos = (tx, ty-1)
tileq.append((tpos, seeds_n)) tileq.append((tpos, seeds_n))
if seeds_w: if seeds_w and tx > min_tx:
tpos = (tx-1, ty) tpos = (tx-1, ty)
tileq.append((tpos, seeds_w)) tileq.append((tpos, seeds_w))
if seeds_s: if seeds_s and ty < max_ty:
tpos = (tx, ty+1) tpos = (tx, ty+1)
tileq.append((tpos, seeds_s)) tileq.append((tpos, seeds_s))
if seeds_e: if seeds_e and tx < max_tx:
tpos = (tx+1, ty) tpos = (tx+1, ty)
tileq.append((tpos, seeds_e)) tileq.append((tpos, seeds_e))
# Ensure tile will be redrawn # Ensure tile will be redrawn
Expand Down
Binary file added svg/mypaint-tool-flood-fill.svgz
Binary file not shown.

0 comments on commit 0d3ab8d

Please sign in to comment.