From 4d5eebc670f043a32311a755cf7b61d26f6ab56e Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 3 Dec 2024 02:59:01 +0200 Subject: [PATCH] The bucket tool now works with draw tiles mode --- src/Classes/Cels/CelTileMap.gd | 23 +++ src/Tools/BaseTool.gd | 3 +- src/Tools/DesignTools/Bucket.gd | 249 ++++++++++++++++++++------ src/Tools/UtilityTools/ColorPicker.gd | 3 +- 4 files changed, 218 insertions(+), 60 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 9fbb369bb..5ab399658 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -125,6 +125,29 @@ func get_cell_position(coords: Vector2i) -> int: return x + y +## Returns the position of a cell in the tilemap +## at tilemap coordinates [param coords] in the cel's image. +func get_cell_position_in_tilemap_space(coords: Vector2i) -> int: + var x := coords.x + x = clampi(x, 0, horizontal_cells - 1) + var y := coords.y + y = clampi(y, 0, vertical_cells - 1) + y *= horizontal_cells + return x + y + + +## Returns the index of a cell in the tilemap +## at pixel coordinates [param coords] in the cel's image. +func get_cell_index_at_coords(coords: Vector2i) -> int: + return cells[get_cell_position(coords)].index + + +## Returns the index of a cell in the tilemap +## at tilemap coordinates [param coords] in the cel's image. +func get_cell_index_at_coords_in_tilemap_space(coords: Vector2i) -> int: + return cells[get_cell_position_in_tilemap_space(coords)].index + + ## Returns [code]true[/code] if the tile at cell position [param cell_position] ## with image [param image_portion] is equal to [param tile_image]. func tiles_equal(cell_position: int, image_portion: Image, tile_image: Image) -> bool: diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index 6a2e5f33e..e3de4759d 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -374,9 +374,8 @@ func _pick_color(pos: Vector2i) -> void: if pos.x < 0 or pos.y < 0: return if is_placing_tiles(): - var tile_position := get_cell_position(pos) var cel := Global.current_project.get_current_cel() as CelTileMap - Tools.selected_tile_index_changed.emit(cel.cells[tile_position].index) + Tools.selected_tile_index_changed.emit(cel.get_cell_index_at_coords(pos)) return var image := Image.new() image.copy_from(_get_draw_image()) diff --git a/src/Tools/DesignTools/Bucket.gd b/src/Tools/DesignTools/Bucket.gd index 02db74905..c54a91d35 100644 --- a/src/Tools/DesignTools/Bucket.gd +++ b/src/Tools/DesignTools/Bucket.gd @@ -186,6 +186,11 @@ func draw_end(pos: Vector2i) -> void: commit_undo() +func draw_tile(pos: Vector2i, cel: CelTileMap) -> void: + var tile_position := get_cell_position(pos) + cel.set_index(tile_position, TileSetPanel.selected_tile_index) + + func fill(pos: Vector2i) -> void: match _fill_area: FillArea.AREA: @@ -199,6 +204,17 @@ func fill(pos: Vector2i) -> void: func fill_in_color(pos: Vector2i) -> void: var project := Global.current_project + if is_placing_tiles(): + for cel in _get_selected_draw_cels(): + if cel is not CelTileMap: + continue + var tilemap_cel := cel as CelTileMap + var tile_index := tilemap_cel.get_cell_index_at_coords(pos) + for i in tilemap_cel.cells.size(): + var cell := tilemap_cel.cells[i] + if cell.index == tile_index: + tilemap_cel.set_index(i, TileSetPanel.selected_tile_index) + return var color := project.get_current_cel().get_image().get_pixelv(pos) var images := _get_selected_draw_images() for image in images: @@ -311,6 +327,74 @@ func fill_in_selection() -> void: gen.generate_image(image, PATTERN_FILL_SHADER, params, project.size) +func _flood_fill(pos: Vector2i) -> 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 := Global.current_project + if is_placing_tiles(): + for cel in _get_selected_draw_cels(): + if cel is not CelTileMap: + continue + var tile_index := (cel as CelTileMap).get_cell_index_at_coords(pos) + # init flood data structures + _allegro_flood_segments = [] + _allegro_image_segments = [] + _compute_segments_for_tilemap(pos, cel, tile_index) + _color_segments_tilemap(cel) + return + + var images := _get_selected_draw_images() + for image in images: + if Tools.check_alpha_lock(image, pos): + continue + var color: Color = image.get_pixelv(pos) + 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): + continue + 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(pos, 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( + pos: Vector2i, 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(pos, 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 + + ## Add a new segment to the array func _add_new_segment(y := 0) -> void: _allegro_flood_segments.append(Segment.new(y)) @@ -407,62 +491,6 @@ func _check_flooded_segment( return ret -func _flood_fill(pos: Vector2i) -> 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 := Global.current_project - var images := _get_selected_draw_images() - for image in images: - if Tools.check_alpha_lock(image, pos): - continue - var color: Color = image.get_pixelv(pos) - 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): - continue - 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(pos, 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( - pos: Vector2i, 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(pos, 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: ImageExtended) -> void: if _fill_with == FillWith.COLOR or _pattern == null: # This is needed to ensure that the color used to fill is not wrong, due to float @@ -493,6 +521,115 @@ func _set_pixel_pattern(image: ImageExtended, x: int, y: int, pattern_size: Vect image.set_pixel_custom(x, y, pc) +func _compute_segments_for_tilemap(pos: Vector2i, cel: CelTileMap, src_index: int) -> void: + # initially allocate at least 1 segment per line of the tilemap + for j in cel.vertical_cells: + _add_new_segment(j) + pos /= cel.tileset.tile_size + # start flood algorithm + _flood_line_around_point_tilemap(pos, cel, src_index) + # 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_tilemap( + p.y + 1, p.left_position, p.right_position, cel, src_index + ): + done = false + if p.todo_above: # check above the segment? + p.todo_above = false + if _check_flooded_segment_tilemap( + p.y - 1, p.left_position, p.right_position, cel, src_index + ): + done = false + + +## Fill an horizontal segment around the specified 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. +## Τhis method is called by [method _flood_fill] after the required data structures +## have been initialized. +func _flood_line_around_point_tilemap(pos: Vector2i, cel: CelTileMap, src_index: int) -> int: + if cel.get_cell_index_at_coords_in_tilemap_space(pos) != src_index: + return pos.x + 1 + var west := pos + var east := pos + while west.x >= 0 && cel.get_cell_index_at_coords_in_tilemap_space(west) == src_index: + west += Vector2i.LEFT + while ( + east.x < cel.horizontal_cells + && cel.get_cell_index_at_coords_in_tilemap_space(east) == src_index + ): + east += Vector2i.RIGHT + # Make a note of the stuff we processed + var c := pos.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(pos.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 = pos.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 = pos.y > 0 + segment.todo_below = pos.y < cel.vertical_cells - 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 east.x + 1 + + +func _check_flooded_segment_tilemap( + y: int, left: int, right: int, cel: CelTileMap, src_index: int +) -> bool: + var ret := false + var c := 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_tilemap(Vector2i(left, y), cel, src_index) + ret = true + break + return ret + + +func _color_segments_tilemap(cel: CelTileMap) -> void: + for c in _allegro_image_segments.size(): + var p := _allegro_image_segments[c] + for px in range(p.left_position, p.right_position + 1): + draw_tile(Vector2i(px, p.y) * cel.tileset.tile_size, cel) + + func commit_undo() -> void: var project := Global.current_project project.update_tilemaps(_undo_data) diff --git a/src/Tools/UtilityTools/ColorPicker.gd b/src/Tools/UtilityTools/ColorPicker.gd index 7117b3ecf..a5bb3df99 100644 --- a/src/Tools/UtilityTools/ColorPicker.gd +++ b/src/Tools/UtilityTools/ColorPicker.gd @@ -66,9 +66,8 @@ func _pick_color(pos: Vector2i) -> void: if pos.x < 0 or pos.y < 0: return if is_placing_tiles(): - var tile_position := get_cell_position(pos) var cel := Global.current_project.get_current_cel() as CelTileMap - Tools.selected_tile_index_changed.emit(cel.cells[tile_position].index) + Tools.selected_tile_index_changed.emit(cel.get_cell_index_at_coords(pos)) return var image := Image.new() image.copy_from(_get_draw_image())