1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-31 07:29:49 +00:00

Optimize Magic Wand tool based on #667 and #672

This commit is contained in:
Emmanouil Papadeas 2022-05-20 17:47:19 +03:00
parent c3c337280e
commit d7cfa87ffa

View file

@ -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)