From 1a19c3a297f7beac521f2de6d8cac50b0642fe62 Mon Sep 17 00:00:00 2001 From: Matteo Piovanelli Date: Thu, 21 Apr 2022 03:57:39 +0200 Subject: [PATCH] Further speed up of bucket fill (#672) * Removed some calls to can_pixel_get_drawn by managing has_selection condition differently * Refactored `_set_pixel` to shortcircuit. This allowed shortcircuiting away some tests to speed up common code paths. * Further refactor of `_set_pixel` Removed unused functions. Made conditions for pattern-fills clearer. * Refactor. * Refactor. * Removed dev timers. --- src/Tools/Bucket.gd | 90 +++++++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/src/Tools/Bucket.gd b/src/Tools/Bucket.gd index 55df84f9c..b6a320a6c 100644 --- a/src/Tools/Bucket.gd +++ b/src/Tools/Bucket.gd @@ -256,10 +256,22 @@ func _flood_line_around_point(position: Vector2, project: Project, image: Image, return int(position.x) + 1 var west: Vector2 = position var east: Vector2 = position - while project.can_pixel_get_drawn(west) && image.get_pixelv(west).is_equal_approx(src_color): - west += Vector2.LEFT - while project.can_pixel_get_drawn(east) && image.get_pixelv(east).is_equal_approx(src_color): - east += Vector2.RIGHT + if project.has_selection: + while ( + project.can_pixel_get_drawn(west) + && image.get_pixelv(west).is_equal_approx(src_color) + ): + west += Vector2.LEFT + while ( + project.can_pixel_get_drawn(east) + && image.get_pixelv(east).is_equal_approx(src_color) + ): + east += Vector2.RIGHT + else: + 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] @@ -280,16 +292,14 @@ func _flood_line_around_point(position: Vector2, project: Project, image: Image, segment.y = position.y segment.next = 0 # Should we process segments above or below this one? - if project.has_selection: - # 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. - segment.todo_above = position.y > 0 - segment.todo_below = position.y < project.size.y - 1 - else: - segment.todo_above = project.can_pixel_get_drawn(position + Vector2.UP) - segment.todo_below = project.can_pixel_get_drawn(position + Vector2.DOWN) + # 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 _allegro_image_segments.append(segment) @@ -326,8 +336,14 @@ func _flood_fill(position: Vector2) -> void: for image in images: var color: Color = image.get_pixelv(position) if _fill_with == 0 or _pattern == null: + # end early if we are filling with the same color if tool_slot.color.is_equal_approx(color): return + else: + # end early if we are filling with an empty pattern + var pattern_size = _pattern.image.get_size() + if pattern_size.x == 0 or pattern_size.y == 0: + return # init flood data structures _allegro_flood_segments = [] _allegro_image_segments = [] @@ -354,31 +370,33 @@ func _flood_fill(position: Vector2) -> void: p.y - 1, p.left_position, p.right_position, project, image, color ): done = false - # now actually color the image - for c in _allegro_image_segments.size(): - var p = _allegro_image_segments[c] - if p.flooding: # sanity check: should always be true + # 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. + if _fill_with == 0 or _pattern == null: + # 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): - _set_pixel(image, px, p.y, tool_slot.color) + # We don't have to check again whether the point being processed is within the bounds + image.set_pixel(px, p.y, tool_slot.color) + else: + # shortcircuit tests for patternfills + var pattern_size = _pattern.image.get_size() + # we know the pattern had a valid size when we began flooding, so we can skip testing that + # again for every point in the pattern. + for c in _allegro_image_segments.size(): + var p = _allegro_image_segments[c] + for px in range(p.left_position, p.right_position + 1): + _set_pixel_pattern(image, px, p.y, pattern_size) -func _set_pixel(image: Image, x: int, y: int, color: Color) -> void: - var project: Project = Global.current_project - if !project.can_pixel_get_drawn(Vector2(x, y)): - return - - if _fill_with == 0 or _pattern == null: - image.set_pixel(x, y, color) - else: - var size := _pattern.image.get_size() - if size.x == 0 or size.y == 0: - return - _pattern.image.lock() - var px := int(x + _offset_x) % int(size.x) - var py := int(y + _offset_y) % int(size.y) - var pc := _pattern.image.get_pixel(px, py) - _pattern.image.unlock() - image.set_pixel(x, y, pc) +func _set_pixel_pattern(image: Image, x: int, y: int, pattern_size: Vector2) -> void: + _pattern.image.lock() + var px := int(x + _offset_x) % int(pattern_size.x) + var py := int(y + _offset_y) % int(pattern_size.y) + var pc := _pattern.image.get_pixel(px, py) + _pattern.image.unlock() + image.set_pixel(x, y, pc) func commit_undo(action: String, undo_data: Dictionary) -> void: