1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-02-08 03:19:49 +00:00
Pixelorama/src/Tools/Bucket.gd
Emmanouil Papadeas cac9b67335 Revert back to an OptionButton in the Bucket tool options and add "Whole Selection" option in the fill area
Still WIP, "Whole selection" isn't working with patterns yet.
2022-09-18 18:43:50 +03:00

498 lines
16 KiB
GDScript

extends BaseTool
enum FillArea { AREA, COLORS, SELECTION }
enum FillWith { COLOR, PATTERN }
const COLOR_REPLACE_SHADER := preload("res://src/Shaders/ColorReplace.shader")
var _prev_mode := 0
var _pattern: Patterns.Pattern
var _similarity := 100
var _fill_area: int = FillArea.AREA
var _fill_with: int = FillWith.COLOR
var _offset_x := 0
var _offset_y := 0
# 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 _ready() -> void:
update_pattern()
func _input(event: InputEvent) -> void:
if event.is_action_pressed("change_tool_mode"):
_prev_mode = _fill_area
if event.is_action("change_tool_mode"):
if _fill_area == FillArea.SELECTION:
_fill_area = FillArea.AREA
else:
_fill_area = _prev_mode ^ 1
_select_fill_area_optionbutton()
if event.is_action_released("change_tool_mode"):
_fill_area = _prev_mode
_select_fill_area_optionbutton()
func _on_FillAreaOptions_item_selected(index: int) -> void:
_fill_area = index
update_config()
save_config()
func _select_fill_area_optionbutton() -> void:
$FillAreaOptions.selected = _fill_area
$Similarity.visible = (_fill_area == FillArea.COLORS)
func _on_FillWithOptions_item_selected(index: int) -> void:
_fill_with = index
$Similarity/Value.value = _similarity
update_config()
save_config()
func _on_Value_value_changed(value: float) -> void:
_similarity = value
$Similarity/Slider.value = _similarity
update_config()
save_config()
func _on_Slider_value_changed(value: float) -> void:
_similarity = value
update_config()
save_config()
func _on_PatternType_pressed() -> void:
var popup: Popup = Global.patterns_popup
if !popup.is_connected("pattern_selected", self, "_on_Pattern_selected"):
popup.connect("pattern_selected", self, "_on_Pattern_selected", [], CONNECT_ONESHOT)
popup.popup(Rect2($FillPattern/Type.rect_global_position, Vector2(226, 72)))
func _on_Pattern_selected(pattern: Patterns.Pattern) -> void:
_pattern = pattern
update_pattern()
save_config()
func _on_PatternOffsetX_value_changed(value: float) -> void:
_offset_x = int(value)
update_config()
save_config()
func _on_PatternOffsetY_value_changed(value: float) -> void:
_offset_y = int(value)
update_config()
save_config()
func get_config() -> Dictionary:
if !_pattern:
return {"fill_area": _fill_area, "fill_with": _fill_with, "similarity": _similarity}
return {
"pattern_index": _pattern.index,
"fill_area": _fill_area,
"fill_with": _fill_with,
"similarity": _similarity,
"offset_x": _offset_x,
"offset_y": _offset_y,
}
func set_config(config: Dictionary) -> void:
if _pattern:
var index = config.get("pattern_index", _pattern.index)
_pattern = Global.patterns_popup.get_pattern(index)
_fill_area = config.get("fill_area", _fill_area)
_fill_with = config.get("fill_with", _fill_with)
_similarity = config.get("similarity", _similarity)
_offset_x = config.get("offset_x", _offset_x)
_offset_y = config.get("offset_y", _offset_y)
update_pattern()
func update_config() -> void:
_select_fill_area_optionbutton()
$FillWithOptions.selected = _fill_with
$Similarity/Value.value = _similarity
$Similarity/Slider.value = _similarity
$FillPattern.visible = _fill_with == FillWith.PATTERN
$FillPattern/XOffset/OffsetX.value = _offset_x
$FillPattern/YOffset/OffsetY.value = _offset_y
func update_pattern() -> void:
if _pattern == null:
if Global.patterns_popup.default_pattern == null:
return
else:
_pattern = Global.patterns_popup.default_pattern
var tex := ImageTexture.new()
if !_pattern.image.is_empty():
tex.create_from_image(_pattern.image, 0)
$FillPattern/Type/Texture.texture = tex
var size := _pattern.image.get_size()
$FillPattern/XOffset/OffsetX.max_value = size.x - 1
$FillPattern/YOffset/OffsetY.max_value = size.y - 1
func draw_start(position: Vector2) -> void:
.draw_start(position)
if Input.is_action_pressed("draw_color_picker"):
_pick_color(position)
return
Global.canvas.selection.transform_content_confirm()
if (
!Global.current_project.layers[Global.current_project.current_layer].can_layer_get_drawn()
or !Rect2(Vector2.ZERO, Global.current_project.size).has_point(position)
):
return
if (
Global.current_project.has_selection
and not Global.current_project.can_pixel_get_drawn(position)
):
return
var undo_data := _get_undo_data()
match _fill_area:
FillArea.AREA:
fill_in_area(position)
FillArea.COLORS:
fill_in_color(position)
FillArea.SELECTION:
fill_in_selection()
commit_undo("Draw", undo_data)
func draw_move(position: Vector2) -> void:
.draw_move(position)
func draw_end(position: Vector2) -> void:
.draw_end(position)
func fill_in_color(position: Vector2) -> void:
var project: Project = Global.current_project
var color: Color = _get_draw_image().get_pixelv(position)
var images := _get_selected_draw_images()
for image in images:
var pattern_image: Image
if _fill_with == FillWith.COLOR or _pattern == null:
if tool_slot.color.is_equal_approx(color):
return
else:
# End early if we are filling with an empty pattern
pattern_image = _pattern.image
var pattern_size := pattern_image.get_size()
if pattern_size.x == 0 or pattern_size.y == 0:
return
var selection: Image
var selection_tex := ImageTexture.new()
if project.has_selection:
selection = project.selection_map
else:
selection = Image.new()
selection.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
selection.fill(Color(1, 1, 1, 1))
selection_tex.create_from_image(selection)
var pattern_tex := ImageTexture.new()
if _pattern and pattern_image:
pattern_tex.create_from_image(pattern_image)
var params := {
"size": project.size,
"old_color": color,
"new_color": tool_slot.color,
"similarity_percent": _similarity,
"selection": selection_tex,
"pattern": pattern_tex,
"pattern_size": pattern_tex.get_size(),
# pixel offset converted to pattern uv offset
"pattern_uv_offset":
Vector2.ONE / pattern_tex.get_size() * Vector2(_offset_x, _offset_y),
"has_pattern": true if _fill_with == FillWith.PATTERN else false
}
var gen := ShaderImageEffect.new()
gen.generate_image(image, COLOR_REPLACE_SHADER, params, project.size)
func fill_in_area(position: Vector2) -> void:
var project: Project = Global.current_project
_flood_fill(position)
# Handle Mirroring
var mirror_x = project.x_symmetry_point - position.x
var mirror_y = project.y_symmetry_point - position.y
var mirror_x_inside: bool
var mirror_y_inside: bool
mirror_x_inside = project.can_pixel_get_drawn(Vector2(mirror_x, position.y))
mirror_y_inside = project.can_pixel_get_drawn(Vector2(position.x, mirror_y))
if Tools.horizontal_mirror and mirror_x_inside:
_flood_fill(Vector2(mirror_x, position.y))
if Tools.vertical_mirror and mirror_y_inside:
_flood_fill(Vector2(mirror_x, mirror_y))
if Tools.vertical_mirror and mirror_y_inside:
_flood_fill(Vector2(position.x, mirror_y))
func fill_in_selection() -> void:
var project: Project = Global.current_project
var images := _get_selected_draw_images()
if project.has_selection:
var filler := Image.new()
filler.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
filler.fill(tool_slot.color)
var rect: Rect2 = Global.canvas.selection.big_bounding_rectangle
var selection_map_copy := SelectionMap.new()
selection_map_copy.copy_from(project.selection_map)
# In case the selection map is bigger than the canvas
selection_map_copy.crop(project.size.x, project.size.y)
for image in images:
image.blit_rect_mask(filler, selection_map_copy, rect, rect.position)
else:
for image in images:
image.fill(tool_slot.color)
# 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)
# 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
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]
# 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) -> 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 images := _get_selected_draw_images()
for image in images:
var color: Color = image.get_pixelv(position)
if _fill_with == FillWith.COLOR 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 = []
_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.
_color_segments(image)
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 _color_segments(image: Image) -> void:
if _fill_with == FillWith.COLOR or _pattern == null:
var color_str = tool_slot.color.to_html()
# 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
image.set_pixel(px, p.y, Color(color_str))
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_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:
var redo_data := _get_undo_data()
var project: Project = Global.current_project
var frame := -1
var layer := -1
if Global.animation_timer.is_stopped() and project.selected_cels.size() == 1:
frame = project.current_frame
layer = project.current_layer
project.undos += 1
project.undo_redo.create_action(action)
for image in redo_data:
project.undo_redo.add_do_property(image, "data", redo_data[image])
image.unlock()
for image in undo_data:
project.undo_redo.add_undo_property(image, "data", undo_data[image])
project.undo_redo.add_do_method(Global, "undo_or_redo", false, frame, layer)
project.undo_redo.add_undo_method(Global, "undo_or_redo", true, frame, layer)
project.undo_redo.commit_action()
func _get_undo_data() -> Dictionary:
var data := {}
var images := _get_selected_draw_images()
for image in images:
image.unlock()
data[image] = image.data
image.lock()
return data
func _pick_color(position: Vector2) -> void:
var project: Project = Global.current_project
position = project.tiles.get_canon_position(position)
if position.x < 0 or position.y < 0:
return
var image := Image.new()
image.copy_from(_get_draw_image())
if position.x > image.get_width() - 1 or position.y > image.get_height() - 1:
return
image.lock()
var color := image.get_pixelv(position)
image.unlock()
var button := BUTTON_LEFT if Tools._slots[BUTTON_LEFT].tool_node == self else BUTTON_RIGHT
Tools.assign_color(color, button, false)