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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
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
Loading
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import canvasevent
from overlays import rounded_box, Overlay
import colors
import fill


## Color picking mode, with a preview rectangle overlay
Expand Down Expand Up @@ -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):
Expand Down
103 changes: 103 additions & 0 deletions gui/fill.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
1 change: 1 addition & 0 deletions gui/toolbar.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>
Expand Down
21 changes: 19 additions & 2 deletions lib/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
14 changes: 7 additions & 7 deletions lib/fill.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) &&
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 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
Expand All @@ -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.
Expand Down Expand Up @@ -236,6 +238,4 @@ tile_flood_fill (PyObject *tile, /* HxWx4 array of uint16 */
}




#endif //__HAVE_FILL
3 changes: 2 additions & 1 deletion lib/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 37 additions & 23 deletions lib/tiledsurface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)]) ]
Expand All @@ -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
Expand All @@ -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
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.