From d7cfa87ffa390a3c79c41db70780c256a937fd55 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 20 May 2022 17:47:19 +0300 Subject: [PATCH] Optimize Magic Wand tool based on #667 and #672 --- src/Tools/SelectionTools/MagicWand.gd | 189 +++++++++++++++++++++----- 1 file changed, 152 insertions(+), 37 deletions(-) diff --git a/src/Tools/SelectionTools/MagicWand.gd b/src/Tools/SelectionTools/MagicWand.gd index 7bf44eaae..e4e43e785 100644 --- a/src/Tools/SelectionTools/MagicWand.gd +++ b/src/Tools/SelectionTools/MagicWand.gd @@ -1,5 +1,10 @@ extends SelectionTool +# working array used as buffer for segments while flooding +var _allegro_flood_segments: Array +# results array per image while flooding +var _allegro_image_segments: Array + func apply_selection(position: Vector2) -> void: var project: Project = Global.current_project @@ -14,21 +19,21 @@ func apply_selection(position: Vector2) -> void: var cel_image := Image.new() cel_image.copy_from(_get_draw_image()) cel_image.lock() - flood_fill(position, cel_image, selection_bitmap_copy) + _flood_fill(position, cel_image, selection_bitmap_copy) # Handle mirroring if Tools.horizontal_mirror: var mirror_x := position mirror_x.x = Global.current_project.x_symmetry_point - position.x - flood_fill(mirror_x, cel_image, selection_bitmap_copy) + _flood_fill(mirror_x, cel_image, selection_bitmap_copy) if Tools.vertical_mirror: var mirror_xy := mirror_x mirror_xy.y = Global.current_project.y_symmetry_point - position.y - flood_fill(mirror_xy, cel_image, selection_bitmap_copy) + _flood_fill(mirror_xy, cel_image, selection_bitmap_copy) if Tools.vertical_mirror: var mirror_y := position mirror_y.y = Global.current_project.y_symmetry_point - position.y - flood_fill(mirror_y, cel_image, selection_bitmap_copy) + _flood_fill(mirror_y, cel_image, selection_bitmap_copy) cel_image.unlock() project.selection_bitmap = selection_bitmap_copy Global.canvas.selection.big_bounding_rectangle = project.get_selection_rectangle( @@ -37,37 +42,147 @@ func apply_selection(position: Vector2) -> void: Global.canvas.selection.commit_undo("Select", undo_data) -func flood_fill(position: Vector2, image: Image, bitmap: BitMap) -> void: - var project: Project = Global.current_project - if position.x < 0 or position.y < 0: - return - if position.x > project.size.x - 1 or position.y > project.size.y - 1: - return - var color := image.get_pixelv(position) +# Add a new segment to the array +func _add_new_segment(y: int = 0) -> void: + var segment = {} + segment.flooding = false + segment.todo_above = false + segment.todo_below = false + segment.left_position = -5 # anything less than -1 is ok + segment.right_position = -5 + segment.y = y + segment.next = 0 + _allegro_flood_segments.append(segment) - # Flood fill logic - var processed := BitMap.new() - processed.create(image.get_size()) - var q = [position] - for n in q: - if processed.get_bit(n): - continue - var west: Vector2 = n - var east: Vector2 = n - while west.x >= 0 && image.get_pixelv(west).is_equal_approx(color): - west += Vector2.LEFT - while east.x < project.size.x && image.get_pixelv(east).is_equal_approx(color): - east += Vector2.RIGHT - for px in range(west.x + 1, east.x): - var p := Vector2(px, n.y) - if _intersect: - bitmap.set_bit(p, project.selection_bitmap.get_bit(p)) - else: - bitmap.set_bit(p, !_subtract) - processed.set_bit(p, true) - var north := p + Vector2.UP - var south := p + Vector2.DOWN - if north.y >= 0 && image.get_pixelv(north).is_equal_approx(color): - q.append(north) - if south.y < project.size.y && image.get_pixelv(south).is_equal_approx(color): - q.append(south) + +# fill an horizontal segment around the specifid position, and adds it to the +# list of segments filled. Returns the first x coordinate after the part of the +# line that has been filled. +func _flood_line_around_point( + position: Vector2, project: Project, image: Image, src_color: Color +) -> int: + # this method is called by `_flood_fill` after the required data structures + # have been initialized + if not image.get_pixelv(position).is_equal_approx(src_color): + return int(position.x) + 1 + var west: Vector2 = position + var east: Vector2 = position + while west.x >= 0 && image.get_pixelv(west).is_equal_approx(src_color): + west += Vector2.LEFT + while east.x < project.size.x && image.get_pixelv(east).is_equal_approx(src_color): + east += Vector2.RIGHT + # Make a note of the stuff we processed + var c = int(position.y) + var segment = _allegro_flood_segments[c] + # we may have already processed some segments on this y coordinate + if segment.flooding: + while segment.next > 0: + c = segment.next # index of next segment in this line of image + segment = _allegro_flood_segments[c] + # found last current segment on this line + c = _allegro_flood_segments.size() + segment.next = c + _add_new_segment(position.y) + segment = _allegro_flood_segments[c] + # set the values for the current segment + segment.flooding = true + segment.left_position = west.x + 1 + segment.right_position = east.x - 1 + segment.y = position.y + segment.next = 0 + # Should we process segments above or below this one? + # when there is a selected area, the pixels above and below the one we started creating this + # segment from may be outside it. It's easier to assume we should be checking for segments + # above and below this one than to specifically check every single pixel in it, because that + # test will be performed later anyway. + # On the other hand, this test we described is the same `project.can_pixel_get_drawn` does if + # there is no selection, so we don't need branching here. + segment.todo_above = position.y > 0 + segment.todo_below = position.y < project.size.y - 1 + # this is an actual segment we should be coloring, so we add it to the results for the + # current image + if segment.right_position >= segment.left_position: + _allegro_image_segments.append(segment) + # we know the point just east of the segment is not part of a segment that should be + # processed, else it would be part of this segment + return int(east.x) + 1 + + +func _check_flooded_segment( + y: int, left: int, right: int, project: Project, image: Image, src_color: Color +) -> bool: + var ret = false + var c: int = 0 + while left <= right: + c = y + while true: + var segment = _allegro_flood_segments[c] + if left >= segment.left_position and left <= segment.right_position: + left = segment.right_position + 2 + break + c = segment.next + if c == 0: # couldn't find a valid segment, so we draw a new one + left = _flood_line_around_point(Vector2(left, y), project, image, src_color) + ret = true + break + return ret + + +func _flood_fill(position: Vector2, image: Image, bitmap: BitMap) -> void: + # implements the floodfill routine by Shawn Hargreaves + # from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c + var project: Project = Global.current_project + var color: Color = image.get_pixelv(position) + # init flood data structures + _allegro_flood_segments = [] + _allegro_image_segments = [] + _compute_segments_for_image(position, project, image, color) + # now actually color the image: since we have already checked a few things for the points + # we'll process here, we're going to skip a bunch of safety checks to speed things up. + _select_segments(bitmap) + + +func _compute_segments_for_image( + position: Vector2, project: Project, image: Image, src_color: Color +) -> void: + # initially allocate at least 1 segment per line of image + for j in image.get_height(): + _add_new_segment(j) + # start flood algorithm + _flood_line_around_point(position, project, image, src_color) + # test all segments while also discovering more + var done := false + while not done: + done = true + var max_index = _allegro_flood_segments.size() + for c in max_index: + var p = _allegro_flood_segments[c] + if p.todo_below: # check below the segment? + p.todo_below = false + if _check_flooded_segment( + p.y + 1, p.left_position, p.right_position, project, image, src_color + ): + done = false + if p.todo_above: # check above the segment? + p.todo_above = false + if _check_flooded_segment( + p.y - 1, p.left_position, p.right_position, project, image, src_color + ): + done = false + + +func _select_segments(bitmap: BitMap) -> void: + # short circuit for flat colors + for c in _allegro_image_segments.size(): + var p = _allegro_image_segments[c] + for px in range(p.left_position, p.right_position + 1): + # We don't have to check again whether the point being processed is within the bounds + _set_bit(Vector2(px, p.y), bitmap) + + +func _set_bit(p: Vector2, bitmap: BitMap) -> void: + var project: Project = Global.current_project + if _intersect: + bitmap.set_bit(p, project.selection_bitmap.get_bit(p)) + else: + bitmap.set_bit(p, !_subtract)