diff --git a/addons/README.md b/addons/README.md index bcd00467223..6643f79bcdf 100644 --- a/addons/README.md +++ b/addons/README.md @@ -27,3 +27,10 @@ Files extracted from source: - Version: Based on git commit e5df60ed1d53246e03dba36053ff009846ba5174 with a modification on dockable_container.gd (lines 187-191). - License: [CC0-1.0](https://github.com/gilzoide/godot-dockable-container/blob/main/LICENSE) +## SmartSlicer + +- Upstream: https://github.com/Variable-Interactive/SmartSlicer +- Version: Based on git commit 2804e6109f9667022c66522ce88a99a56fd67ca8 with a modification on SmartSlicePreview.gd (lines 31-32). Only the contents of addons folder are used and the script SmartSlicePreview.gd is moved to res://src/UI/Dialogs/HelperScripts/ for better organization +- License: [MIT](https://github.com/Variable-Interactive/SmartSlicer/blob/main/LICENSE) + + diff --git a/addons/SmartSlicer/Classes/RegionUnpacker.gd b/addons/SmartSlicer/Classes/RegionUnpacker.gd new file mode 100644 index 00000000000..11f01f74724 --- /dev/null +++ b/addons/SmartSlicer/Classes/RegionUnpacker.gd @@ -0,0 +1,251 @@ +class_name RegionUnpacker +extends Reference + +# THIS CLASS TAKES INSPIRATION FROM PIXELORAMA'S FLOOD FILL +# AND HAS BEEN MODIFIED FOR OPTIMIZATION + +var slice_thread := Thread.new() + +var _include_boundary_threshold: int # the size of rect below which merging accounts for boundaty +var _merge_dist: int # after crossing threshold the smaller image will merge with larger image +# if it is within the _merge_dist + +# 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 _init(threshold: int, merge_dist: int) -> void: + _include_boundary_threshold = threshold + _merge_dist = merge_dist + + +func get_used_rects(image: Image) -> Dictionary: + if OS.get_name() == "HTML5": + return get_rects(image) + else: + # If Thread model is set to "Multi-Threaded" in project settings>threads>thread model + if slice_thread.is_active(): + slice_thread.wait_to_finish() + var error = slice_thread.start(self, "get_rects", image) + if error == OK: + return slice_thread.wait_to_finish() + else: + return get_rects(image) + # If Thread model is set to "Single-Safe" in project settings>threads>thread model then + # comment the above code and uncomment below + #return get_rects({"image": image}) + + +func get_rects(image: Image) -> Dictionary: + # make a smaller image to make the loop shorter + var used_rect = image.get_used_rect() + if used_rect.size == Vector2.ZERO: + return clean_rects([]) + var test_image = image.get_rect(used_rect) + # prepare a bitmap to keep track of previous places + var scanned_area := BitMap.new() + scanned_area.create(test_image.get_size()) + test_image.lock() + # Scan the image + var rects = [] + var frame_size = Vector2.ZERO + for y in test_image.get_size().y: + for x in test_image.get_size().x: + var position = Vector2(x, y) + if test_image.get_pixelv(position).a > 0: # used portion of image detected + if !scanned_area.get_bit(position): + var rect := _estimate_rect(test_image, position) + scanned_area.set_bit_rect(rect, true) + rect.position += used_rect.position + rects.append(rect) + test_image.unlock() + var rects_info = clean_rects(rects) + rects_info["rects"].sort_custom(self, "sort_rects") + return rects_info + + +func clean_rects(rects: Array) -> Dictionary: + var frame_size = Vector2.ZERO + for i in rects.size(): + var target: Rect2 = rects.pop_front() + var test_rect = target + if ( + target.size.x < _include_boundary_threshold + or target.size.y < _include_boundary_threshold + ): + test_rect.size += Vector2(_merge_dist, _merge_dist) + test_rect.position -= Vector2(_merge_dist, _merge_dist) / 2 + var merged = false + for rect_i in rects.size(): + if test_rect.intersects(rects[rect_i]): + rects[rect_i] = target.merge(rects[rect_i]) + merged = true + break + if !merged: + rects.append(target) + + # calculation for a suitable frame size + if target.size.x > frame_size.x: + frame_size.x = target.size.x + if target.size.y > frame_size.y: + frame_size.y = target.size.y + return {"rects": rects, "frame_size": frame_size} + + +func sort_rects(rect_a: Rect2, rect_b: Rect2) -> bool: + # After many failed attempts, this version works for some reason (it's best not to disturb it) + if rect_a.end.y < rect_b.position.y: + return true + if rect_a.position.x < rect_b.position.x: + # if both lie in the same row + var start = rect_a.position + var size = Vector2(rect_b.end.x, rect_a.end.y) + if Rect2(start, size).intersects(rect_b): + return true + return false + + +func _estimate_rect(image: Image, position: Vector2) -> Rect2: + var cel_image := Image.new() + cel_image.copy_from(image) + cel_image.lock() + var small_rect: Rect2 = _flood_fill(position, cel_image) + cel_image.unlock() + return small_rect + + +# 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 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. +func _flood_line_around_point(position: Vector2, image: Image) -> int: + # this method is called by `_flood_fill` after the required data structures + # have been initialized + if not image.get_pixelv(position).a > 0: + return int(position.x) + 1 + var west: Vector2 = position + var east: Vector2 = position + while west.x >= 0 && image.get_pixelv(west).a > 0: + west += Vector2.LEFT + while east.x < image.get_width() && image.get_pixelv(east).a > 0: + 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(int(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 < image.get_height() - 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, image: Image) -> 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), image) + ret = true + break + return ret + + +func _flood_fill(position: Vector2, image: Image) -> Rect2: + # implements the floodfill routine by Shawn Hargreaves + # from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c + # init flood data structures + _allegro_flood_segments = [] + _allegro_image_segments = [] + _compute_segments_for_image(position, image) + # 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. + + var final_image = Image.new() + final_image.copy_from(image) + final_image.fill(Color.transparent) + final_image.lock() + _select_segments(final_image) + final_image.unlock() + + return final_image.get_used_rect() + + +func _compute_segments_for_image(position: Vector2, image: Image) -> 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, image) + # 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, image): + 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, image): + done = false + + +func _select_segments(map: Image) -> void: + # short circuit for flat colors + for c in _allegro_image_segments.size(): + var p = _allegro_image_segments[c] + var rect = Rect2() + rect.position = Vector2(p.left_position, p.y) + rect.end = Vector2(p.right_position + 1, p.y + 1) + map.fill_rect(rect, Color.white) diff --git a/project.godot b/project.godot index a3281c53e35..625bc053f11 100644 --- a/project.godot +++ b/project.godot @@ -224,6 +224,11 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://src/UI/ReferenceImages/ReferencesPanel.gd" }, { +"base": "Reference", +"class": "RegionUnpacker", +"language": "GDScript", +"path": "res://addons/SmartSlicer/Classes/RegionUnpacker.gd" +}, { "base": "Image", "class": "SelectionMap", "language": "GDScript", @@ -313,6 +318,7 @@ _global_script_class_icons={ "Project": "", "ReferenceImage": "", "ReferencesPanel": "", +"RegionUnpacker": "", "SelectionMap": "", "SelectionTool": "", "ShaderImageEffect": "", diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index 2a864f66313..e4561f2c741 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -470,14 +470,35 @@ func open_image_as_new_tab(path: String, image: Image) -> void: set_new_imported_tab(project, path) -func open_image_as_spritesheet_tab(path: String, image: Image, horiz: int, vert: int) -> void: - var project := Project.new([], path.get_file()) +func open_image_as_spritesheet_tab_smart( + path: String, image: Image, sliced_rects: Array, frame_size: Vector2 +) -> void: + if sliced_rects.size() == 0: # Image is empty sprite (manually set data to be consistent) + frame_size = image.get_size() + sliced_rects.append(Rect2(Vector2.ZERO, frame_size)) + var project := Project.new([], path.get_file(), frame_size) project.layers.append(PixelLayer.new(project)) Global.projects.append(project) + for rect in sliced_rects: + var offset: Vector2 = (0.5 * (frame_size - rect.size)).floor() + var frame := Frame.new() + var cropped_image := Image.new() + cropped_image.create(frame_size.x, frame_size.y, false, Image.FORMAT_RGBA8) + cropped_image.blit_rect(image, rect, offset) + cropped_image.convert(Image.FORMAT_RGBA8) + frame.cels.append(PixelCel.new(cropped_image, 1)) + project.frames.append(frame) + set_new_imported_tab(project, path) + + +func open_image_as_spritesheet_tab(path: String, image: Image, horiz: int, vert: int) -> void: horiz = min(horiz, image.get_size().x) vert = min(vert, image.get_size().y) var frame_width := image.get_size().x / horiz var frame_height := image.get_size().y / vert + var project := Project.new([], path.get_file(), Vector2(frame_width, frame_height)) + project.layers.append(PixelLayer.new(project)) + Global.projects.append(project) for yy in range(vert): for xx in range(horiz): var frame := Frame.new() @@ -492,6 +513,83 @@ func open_image_as_spritesheet_tab(path: String, image: Image, horiz: int, vert: set_new_imported_tab(project, path) +func open_image_as_spritesheet_layer_smart( + _path: String, + image: Image, + file_name: String, + sliced_rects: Array, + start_frame: int, + frame_size: Vector2 +) -> void: + # Resize canvas to if "frame_size.x" or "frame_size.y" is too large + var project: Project = Global.current_project + var project_width: int = max(frame_size.x, project.size.x) + var project_height: int = max(frame_size.y, project.size.y) + if project.size < Vector2(project_width, project_height): + DrawingAlgos.resize_canvas(project_width, project_height, 0, 0) + + # Initialize undo mechanism + project.undos += 1 + project.undo_redo.create_action("Add Spritesheet Layer") + + # Create new frames (if needed) + var new_frames_size = max(project.frames.size(), start_frame + sliced_rects.size()) + var frames := [] + var frame_indices := [] + if new_frames_size > project.frames.size(): + var required_frames = new_frames_size - project.frames.size() + frame_indices = range( + project.current_frame + 1, project.current_frame + required_frames + 1 + ) + for i in required_frames: + var new_frame := Frame.new() + for l in range(project.layers.size()): # Create as many cels as there are layers + new_frame.cels.append(project.layers[l].new_empty_cel()) + if project.layers[l].new_cels_linked: + var prev_cel: BaseCel = project.frames[project.current_frame].cels[l] + if prev_cel.link_set == null: + prev_cel.link_set = {} + project.undo_redo.add_do_method( + project.layers[l], "link_cel", prev_cel, prev_cel.link_set + ) + project.undo_redo.add_undo_method( + project.layers[l], "link_cel", prev_cel, null + ) + new_frame.cels[l].set_content(prev_cel.get_content(), prev_cel.image_texture) + new_frame.cels[l].link_set = prev_cel.link_set + frames.append(new_frame) + + # Create new layer for spritesheet + var layer := PixelLayer.new(project, file_name) + var cels := [] + for f in new_frames_size: + if f >= start_frame and f < (start_frame + sliced_rects.size()): + # Slice spritesheet + var offset: Vector2 = (0.5 * (frame_size - sliced_rects[f - start_frame].size)).floor() + image.convert(Image.FORMAT_RGBA8) + var cropped_image := Image.new() + cropped_image.create(project_width, project_height, false, Image.FORMAT_RGBA8) + cropped_image.blit_rect(image, sliced_rects[f - start_frame], offset) + cels.append(PixelCel.new(cropped_image)) + else: + cels.append(layer.new_empty_cel()) + + project.undo_redo.add_do_method(project, "add_frames", frames, frame_indices) + project.undo_redo.add_do_method(project, "add_layers", [layer], [project.layers.size()], [cels]) + project.undo_redo.add_do_method( + project, "change_cel", new_frames_size - 1, project.layers.size() + ) + project.undo_redo.add_do_method(Global, "undo_or_redo", false) + + project.undo_redo.add_undo_method(project, "remove_layers", [project.layers.size()]) + project.undo_redo.add_undo_method(project, "remove_frames", frame_indices) + project.undo_redo.add_undo_method( + project, "change_cel", project.current_frame, project.current_layer + ) + project.undo_redo.add_undo_method(Global, "undo_or_redo", true) + project.undo_redo.commit_action() + + func open_image_as_spritesheet_layer( _path: String, image: Image, file_name: String, horizontal: int, vertical: int, start_frame: int ) -> void: @@ -547,12 +645,14 @@ func open_image_as_spritesheet_layer( # Slice spritesheet var xx: int = (f - start_frame) % horizontal var yy: int = (f - start_frame) / horizontal + image.convert(Image.FORMAT_RGBA8) var cropped_image := Image.new() - cropped_image = image.get_rect( - Rect2(frame_width * xx, frame_height * yy, frame_width, frame_height) + cropped_image.create(project_width, project_height, false, Image.FORMAT_RGBA8) + cropped_image.blit_rect( + image, + Rect2(frame_width * xx, frame_height * yy, frame_width, frame_height), + Vector2.ZERO ) - cropped_image.crop(project.size.x, project.size.y) - cropped_image.convert(Image.FORMAT_RGBA8) cels.append(PixelCel.new(cropped_image)) else: cels.append(layer.new_empty_cel()) @@ -585,8 +685,11 @@ func open_image_at_cel(image: Image, layer_index := 0, frame_index := 0) -> void for i in project.frames.size(): if i == frame_index: image.convert(Image.FORMAT_RGBA8) + var cel_image = Image.new() + cel_image.create(project_width, project_height, false, Image.FORMAT_RGBA8) + cel_image.blit_rect(image, Rect2(Vector2.ZERO, image.get_size()), Vector2.ZERO) var cel: PixelCel = project.frames[i].cels[layer_index] - project.undo_redo.add_do_property(cel, "image", image) + project.undo_redo.add_do_property(cel, "image", cel_image) project.undo_redo.add_undo_property(cel, "image", cel.image) project.undo_redo.add_do_property(project, "selected_cels", []) @@ -612,7 +715,10 @@ func open_image_as_new_frame(image: Image, layer_index := 0) -> void: for i in project.layers.size(): if i == layer_index: image.convert(Image.FORMAT_RGBA8) - frame.cels.append(PixelCel.new(image, 1)) + var cel_image = Image.new() + cel_image.create(project_width, project_height, false, Image.FORMAT_RGBA8) + cel_image.blit_rect(image, Rect2(Vector2.ZERO, image.get_size()), Vector2.ZERO) + frame.cels.append(PixelCel.new(cel_image, 1)) else: frame.cels.append(project.layers[i].new_empty_cel()) @@ -644,7 +750,10 @@ func open_image_as_new_layer(image: Image, file_name: String, frame_index := 0) for i in project.frames.size(): if i == frame_index: image.convert(Image.FORMAT_RGBA8) - cels.append(PixelCel.new(image, 1)) + var cel_image = Image.new() + cel_image.create(project_width, project_height, false, Image.FORMAT_RGBA8) + cel_image.blit_rect(image, Rect2(Vector2.ZERO, image.get_size()), Vector2.ZERO) + cels.append(PixelCel.new(cel_image, 1)) else: cels.append(layer.new_empty_cel()) diff --git a/src/UI/Dialogs/HelperScripts/RowColumnLines.gd b/src/UI/Dialogs/HelperScripts/RowColumnLines.gd new file mode 100644 index 00000000000..46dc6d606b4 --- /dev/null +++ b/src/UI/Dialogs/HelperScripts/RowColumnLines.gd @@ -0,0 +1,39 @@ +extends Control + +var color: Color = Color("6680ff") # Set this to a theme color later +var _spritesheet_vertical +var _spritesheet_horizontal + + +func show_preview(spritesheet_vertical, spritesheet_horizontal) -> void: + _spritesheet_vertical = spritesheet_vertical + _spritesheet_horizontal = spritesheet_horizontal + update() + + +func _draw() -> void: + var texture_rect: TextureRect = get_parent() + var image = texture_rect.texture.get_data() + var image_size_y = texture_rect.rect_size.y + var image_size_x = texture_rect.rect_size.x + if image.get_size().x > image.get_size().y: + var scale_ratio = image.get_size().x / image_size_x + image_size_y = image.get_size().y / scale_ratio + else: + var scale_ratio = image.get_size().y / image_size_y + image_size_x = image.get_size().x / scale_ratio + + var offset_x = (texture_rect.rect_size.x - image_size_x) / 2 + var offset_y = (texture_rect.rect_size.y - image_size_y) / 2 + + var line_distance_vertical = image_size_y / _spritesheet_vertical + var line_distance_horizontal = image_size_x / _spritesheet_horizontal + + for i in range(1, _spritesheet_vertical): + var from = Vector2(offset_x, i * line_distance_vertical + offset_y) + var to = Vector2(image_size_x + offset_x, i * line_distance_vertical + offset_y) + draw_line(from, to, color) + for i in range(1, _spritesheet_horizontal): + var from = Vector2(i * line_distance_horizontal + offset_x, offset_y) + var to = Vector2(i * line_distance_horizontal + offset_x, image_size_y + offset_y) + draw_line(from, to, color) diff --git a/src/UI/Dialogs/HelperScripts/SmartSlicePreview.gd b/src/UI/Dialogs/HelperScripts/SmartSlicePreview.gd new file mode 100644 index 00000000000..f3e3e58e8c7 --- /dev/null +++ b/src/UI/Dialogs/HelperScripts/SmartSlicePreview.gd @@ -0,0 +1,36 @@ +extends Control + +# add this as a child of the texturerect that contains the main spritesheet +var color: Color = Color("6680ff") # Set this to a theme color later +var _sliced_rects: Array +var _stretch_amount: float +var _offset: Vector2 + + +func show_preview(sliced_rects: Array) -> void: + var image = get_parent().texture.get_data() + if image.get_size().x > image.get_size().y: + _stretch_amount = rect_size.x / image.get_size().x + else: + _stretch_amount = rect_size.y / image.get_size().y + _sliced_rects = sliced_rects.duplicate() + _offset = (0.5 * (rect_size - (image.get_size() * _stretch_amount))).floor() + update() + + +func _draw() -> void: + draw_set_transform(_offset, 0, Vector2.ONE) + for i in _sliced_rects.size(): + var rect = _sliced_rects[i] + var scaled_rect: Rect2 = rect + scaled_rect.position = (scaled_rect.position * _stretch_amount) + scaled_rect.size *= _stretch_amount + draw_rect(scaled_rect, color, false) + # show number + draw_set_transform(_offset + scaled_rect.position, 0, Vector2.ONE) +# var font: Font = Control.new().get_font("font") + # replace with font used by pixelorama + var font: Font = Global.control.theme.default_font + var font_height := font.get_height() + draw_string(font, Vector2(1, font_height), str(i)) + draw_set_transform(_offset, 0, Vector2.ONE) diff --git a/src/UI/Dialogs/PreviewDialog.gd b/src/UI/Dialogs/PreviewDialog.gd index 02779418249..2b15fe463ac 100644 --- a/src/UI/Dialogs/PreviewDialog.gd +++ b/src/UI/Dialogs/PreviewDialog.gd @@ -17,17 +17,28 @@ enum BrushTypes { FILE, PROJECT, RANDOM } var path: String var image: Image var current_import_option: int = ImageImportOptions.NEW_TAB +var smart_slice = false +var recycle_last_slice_result = false # should we recycle the current sliced_rects +var sliced_rects: Dictionary var spritesheet_horizontal := 1 var spritesheet_vertical := 1 var brush_type: int = BrushTypes.FILE var opened_once = false var is_master: bool = false var hiding: bool = false +var _content_offset = rect_size - get_child(0).rect_size # A workaround for a pixelorama bug onready var texture_rect: TextureRect = $VBoxContainer/CenterContainer/TextureRect onready var image_size_label: Label = $VBoxContainer/SizeContainer/ImageSizeLabel onready var frame_size_label: Label = $VBoxContainer/SizeContainer/FrameSizeLabel -onready var spritesheet_tab_options = $VBoxContainer/HBoxContainer/SpritesheetTabOptions +onready var smart_slice_checkbox = $VBoxContainer/HBoxContainer/SpritesheetTabOptions/SmartSlice +onready var merge_threshold = $VBoxContainer/HBoxContainer/SpritesheetTabOptions/Smart/Threshold +# gdlint: ignore=max-line-length +onready var merge_dist: TextureProgress = $VBoxContainer/HBoxContainer/SpritesheetTabOptions/Smart/MergeDist +# gdlint: ignore=max-line-length +onready var spritesheet_manual_tab_options = $VBoxContainer/HBoxContainer/SpritesheetTabOptions/Manual +onready var spritesheet_smart_tab_options = $VBoxContainer/HBoxContainer/SpritesheetTabOptions/Smart +onready var spritesheet_tab_options = spritesheet_smart_tab_options.get_parent() onready var spritesheet_lay_opt = $VBoxContainer/HBoxContainer/SpritesheetLayerOptions onready var new_frame_options = $VBoxContainer/HBoxContainer/NewFrameOptions onready var replace_cel_options = $VBoxContainer/HBoxContainer/ReplaceCelOptions @@ -62,11 +73,11 @@ func _on_PreviewDialog_about_to_show() -> void: var img_texture := ImageTexture.new() img_texture.create_from_image(image, 0) texture_rect.texture = img_texture - spritesheet_tab_options.get_node("HorizontalFrames").max_value = min( - spritesheet_tab_options.get_node("HorizontalFrames").max_value, image.get_size().x + spritesheet_manual_tab_options.get_node("HorizontalFrames").max_value = min( + spritesheet_manual_tab_options.get_node("HorizontalFrames").max_value, image.get_size().x ) - spritesheet_tab_options.get_node("VerticalFrames").max_value = min( - spritesheet_tab_options.get_node("VerticalFrames").max_value, image.get_size().y + spritesheet_manual_tab_options.get_node("VerticalFrames").max_value = min( + spritesheet_manual_tab_options.get_node("VerticalFrames").max_value, image.get_size().y ) image_size_label.text = ( tr("Image Size") @@ -115,20 +126,39 @@ func _on_PreviewDialog_confirmed() -> void: OpenSave.open_image_as_new_tab(path, image) elif current_import_option == ImageImportOptions.SPRITESHEET_TAB: - OpenSave.open_image_as_spritesheet_tab( - path, image, spritesheet_horizontal, spritesheet_vertical - ) + if smart_slice: + if !recycle_last_slice_result: + obtain_sliced_data() + OpenSave.open_image_as_spritesheet_tab_smart( + path, image, sliced_rects["rects"], sliced_rects["frame_size"] + ) + else: + OpenSave.open_image_as_spritesheet_tab( + path, image, spritesheet_horizontal, spritesheet_vertical + ) elif current_import_option == ImageImportOptions.SPRITESHEET_LAYER: var frame_index: int = spritesheet_lay_opt.get_node("AtFrameSpinbox").value - 1 - OpenSave.open_image_as_spritesheet_layer( - path, - image, - path.get_basename().get_file(), - spritesheet_horizontal, - spritesheet_vertical, - frame_index - ) + if smart_slice: + if !recycle_last_slice_result: + obtain_sliced_data() + OpenSave.open_image_as_spritesheet_layer_smart( + path, + image, + path.get_basename().get_file(), + sliced_rects["rects"], + frame_index, + sliced_rects["frame_size"] + ) + else: + OpenSave.open_image_as_spritesheet_layer( + path, + image, + path.get_basename().get_file(), + spritesheet_horizontal, + spritesheet_vertical, + frame_index + ) elif current_import_option == ImageImportOptions.NEW_FRAME: var layer_index: int = new_frame_options.get_node("AtLayerOption").get_selected_id() @@ -200,11 +230,13 @@ func synchronize() -> void: id == ImageImportOptions.SPRITESHEET_TAB or id == ImageImportOptions.SPRITESHEET_LAYER ): - dialog.spritesheet_tab_options.get_node("HorizontalFrames").value = min( - spritesheet_tab_options.get_node("HorizontalFrames").value, image.get_size().x + dialog.spritesheet_manual_tab_options.get_node("HorizontalFrames").value = min( + spritesheet_manual_tab_options.get_node("HorizontalFrames").value, + image.get_size().x ) - dialog.spritesheet_tab_options.get_node("VerticalFrames").value = min( - spritesheet_tab_options.get_node("VerticalFrames").value, image.get_size().y + dialog.spritesheet_manual_tab_options.get_node("VerticalFrames").value = min( + spritesheet_manual_tab_options.get_node("VerticalFrames").value, + image.get_size().y ) if id == ImageImportOptions.SPRITESHEET_LAYER: dialog.spritesheet_lay_opt.get_node("AtFrameSpinbox").value = (spritesheet_lay_opt.get_node( @@ -240,7 +272,10 @@ func synchronize() -> void: func _on_ImportOption_item_selected(id: int) -> void: current_import_option = id OpenSave.last_dialog_option = current_import_option - frame_size_label.visible = false + smart_slice_checkbox.pressed = false + apply_all.disabled = false + smart_slice = false + smart_slice_checkbox.visible = false spritesheet_tab_options.visible = false spritesheet_lay_opt.visible = false new_frame_options.visible = false @@ -249,22 +284,21 @@ func _on_ImportOption_item_selected(id: int) -> void: new_brush_options.visible = false texture_rect.get_child(0).visible = false texture_rect.get_child(1).visible = false - rect_size.x = 550 if id == ImageImportOptions.SPRITESHEET_TAB: frame_size_label.visible = true + smart_slice_checkbox.visible = true spritesheet_tab_options.visible = true texture_rect.get_child(0).visible = true texture_rect.get_child(1).visible = true - rect_size.x = spritesheet_tab_options.rect_size.x elif id == ImageImportOptions.SPRITESHEET_LAYER: frame_size_label.visible = true - spritesheet_tab_options.visible = true + smart_slice_checkbox.visible = true spritesheet_lay_opt.visible = true + spritesheet_tab_options.visible = true texture_rect.get_child(0).visible = true texture_rect.get_child(1).visible = true - rect_size.x = spritesheet_lay_opt.rect_size.x elif id == ImageImportOptions.NEW_FRAME: new_frame_options.visible = true @@ -306,59 +340,68 @@ func _on_ImportOption_item_selected(id: int) -> void: elif id == ImageImportOptions.BRUSH: new_brush_options.visible = true + rect_size = get_child(0).rect_size + _content_offset + update() -func _on_HorizontalFrames_value_changed(value: int) -> void: - spritesheet_horizontal = value - for child in texture_rect.get_node("HorizLines").get_children(): - child.queue_free() - spritesheet_frame_value_changed(value, false) +func _on_SmartSlice_toggled(button_pressed: bool) -> void: + setup_smart_slice(button_pressed) -func _on_VerticalFrames_value_changed(value: int) -> void: - spritesheet_vertical = value - for child in texture_rect.get_node("VerticalLines").get_children(): - child.queue_free() +func setup_smart_slice(enabled: bool) -> void: + spritesheet_smart_tab_options.visible = enabled + spritesheet_manual_tab_options.visible = !enabled + if is_master: # disable apply all (the algorithm is not fast enough for this) + apply_all.pressed = false + apply_all.disabled = enabled + smart_slice = enabled + if !recycle_last_slice_result and enabled: + slice_preview() + update() - spritesheet_frame_value_changed(value, true) +func obtain_sliced_data() -> void: + var unpak := RegionUnpacker.new(merge_threshold.value, merge_dist.value) + sliced_rects = unpak.get_used_rects(texture_rect.texture.get_data()) -func spritesheet_frame_value_changed(value: int, vertical: bool) -> void: - var image_size_y = texture_rect.rect_size.y - var image_size_x = texture_rect.rect_size.x - if image.get_size().x > image.get_size().y: - var scale_ratio = image.get_size().x / image_size_x - image_size_y = image.get_size().y / scale_ratio - else: - var scale_ratio = image.get_size().y / image_size_y - image_size_x = image.get_size().x / scale_ratio - var offset_x = (texture_rect.rect_size.x - image_size_x) / 2 - var offset_y = (texture_rect.rect_size.y - image_size_y) / 2 +func slice_preview(): + sliced_rects.clear() + obtain_sliced_data() + recycle_last_slice_result = true + var size = sliced_rects["frame_size"] + frame_size_label.text = tr("Frame Size") + ": " + str(size.x) + "×" + str(size.y) + + +func _on_Threshold_value_changed(_value: float) -> void: + recycle_last_slice_result = false - if value > 1: - var line_distance - if vertical: - line_distance = image_size_y / value - else: - line_distance = image_size_x / value - - for i in range(1, value): - var line_2d := Line2D.new() - line_2d.width = 1 - line_2d.position = Vector2.ZERO - if vertical: - line_2d.add_point(Vector2(offset_x, i * line_distance + offset_y)) - line_2d.add_point(Vector2(image_size_x + offset_x, i * line_distance + offset_y)) - texture_rect.get_node("VerticalLines").add_child(line_2d) - else: - line_2d.add_point(Vector2(i * line_distance + offset_x, offset_y)) - line_2d.add_point(Vector2(i * line_distance + offset_x, image_size_y + offset_y)) - texture_rect.get_node("HorizLines").add_child(line_2d) +func _on_MergeDist_value_changed(_value: float) -> void: + recycle_last_slice_result = false + + +func _on_Slice_pressed() -> void: + if !recycle_last_slice_result: + slice_preview() + update() + + +func _on_HorizontalFrames_value_changed(value: int) -> void: + spritesheet_horizontal = value + spritesheet_frame_value_changed() + + +func _on_VerticalFrames_value_changed(value: int) -> void: + spritesheet_vertical = value + spritesheet_frame_value_changed() + + +func spritesheet_frame_value_changed() -> void: var frame_width = floor(image.get_size().x / spritesheet_horizontal) var frame_height = floor(image.get_size().y / spritesheet_vertical) frame_size_label.text = tr("Frame Size") + ": " + str(frame_width) + "×" + str(frame_height) + update() func _on_BrushTypeOption_item_selected(index: int) -> void: @@ -428,3 +471,21 @@ func file_name_replace(name: String, folder: String) -> String: temp_name += "." + file_ext name = temp_name return name + + +func _on_PreviewDialog_item_rect_changed() -> void: + update() + + +func _draw() -> void: + $"%SmartSlice".show_preview([]) + $"%RowColumnLines".show_preview(1, 1) + if ( + current_import_option == ImageImportOptions.SPRITESHEET_TAB + or current_import_option == ImageImportOptions.SPRITESHEET_LAYER + ): + if smart_slice: + if "rects" in sliced_rects.keys(): + $"%SmartSlice".show_preview(sliced_rects["rects"]) + else: + $"%RowColumnLines".show_preview(spritesheet_vertical, spritesheet_horizontal) diff --git a/src/UI/Dialogs/PreviewDialog.tscn b/src/UI/Dialogs/PreviewDialog.tscn index 61fae68c701..cf5050eca16 100644 --- a/src/UI/Dialogs/PreviewDialog.tscn +++ b/src/UI/Dialogs/PreviewDialog.tscn @@ -1,18 +1,18 @@ -[gd_scene load_steps=2 format=2] +[gd_scene load_steps=5 format=2] [ext_resource path="res://src/UI/Dialogs/PreviewDialog.gd" type="Script" id=1] +[ext_resource path="res://src/UI/Nodes/ValueSlider.tscn" type="PackedScene" id=2] +[ext_resource path="res://src/UI/Dialogs/HelperScripts/SmartSlicePreview.gd" type="Script" id=3] +[ext_resource path="res://src/UI/Dialogs/HelperScripts/RowColumnLines.gd" type="Script" id=4] [node name="PreviewDialog" type="ConfirmationDialog"] -margin_right = 550.0 -margin_bottom = 410.0 +margin_right = 629.0 +margin_bottom = 513.0 rect_min_size = Vector2( 550, 70 ) popup_exclusive = true window_title = "Import Options" resizable = true script = ExtResource( 1 ) -__meta__ = { -"_edit_use_anchors_": false -} [node name="VBoxContainer" type="VBoxContainer" parent="."] anchor_right = 1.0 @@ -21,23 +21,16 @@ margin_left = 8.0 margin_top = 8.0 margin_right = -8.0 margin_bottom = -36.0 -__meta__ = { -"_edit_use_anchors_": false -} -[node name="CenterContainer" type="CenterContainer" parent="VBoxContainer"] -margin_right = 534.0 -margin_bottom = 324.0 +[node name="CenterContainer" type="AspectRatioContainer" parent="VBoxContainer"] +margin_right = 613.0 +margin_bottom = 395.0 size_flags_vertical = 3 -__meta__ = { -"_edit_use_anchors_": false -} [node name="TextureRect" type="TextureRect" parent="VBoxContainer/CenterContainer"] -margin_left = 117.0 -margin_top = 12.0 -margin_right = 417.0 -margin_bottom = 312.0 +margin_left = 109.0 +margin_right = 504.0 +margin_bottom = 395.0 rect_min_size = Vector2( 300, 300 ) expand = true stretch_mode = 6 @@ -45,12 +38,15 @@ __meta__ = { "_edit_use_anchors_": false } -[node name="HorizLines" type="Control" parent="VBoxContainer/CenterContainer/TextureRect"] -__meta__ = { -"_edit_use_anchors_": false -} +[node name="RowColumnLines" type="Control" parent="VBoxContainer/CenterContainer/TextureRect"] +unique_name_in_owner = true +script = ExtResource( 4 ) -[node name="VerticalLines" type="Control" parent="VBoxContainer/CenterContainer/TextureRect"] +[node name="SmartSlice" type="Control" parent="VBoxContainer/CenterContainer/TextureRect"] +unique_name_in_owner = true +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 3 ) [node name="ApplyAll" type="CheckBox" parent="VBoxContainer"] visible = false @@ -60,9 +56,9 @@ margin_bottom = 324.0 text = "Apply to all" [node name="SizeContainer" type="HBoxContainer" parent="VBoxContainer"] -margin_top = 328.0 -margin_right = 534.0 -margin_bottom = 342.0 +margin_top = 399.0 +margin_right = 613.0 +margin_bottom = 413.0 custom_constants/separation = 32 [node name="ImageSizeLabel" type="Label" parent="VBoxContainer/SizeContainer"] @@ -78,14 +74,14 @@ margin_bottom = 14.0 text = "Frame size: 64×64" [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] -margin_top = 346.0 -margin_right = 534.0 -margin_bottom = 366.0 +margin_top = 417.0 +margin_right = 613.0 +margin_bottom = 469.0 [node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"] -margin_top = 3.0 margin_right = 66.0 -margin_bottom = 17.0 +margin_bottom = 14.0 +size_flags_vertical = 0 text = "Import as:" [node name="ImportOption" type="OptionButton" parent="VBoxContainer/HBoxContainer"] @@ -93,21 +89,27 @@ margin_left = 70.0 margin_right = 151.0 margin_bottom = 20.0 mouse_default_cursor_shape = 2 +size_flags_vertical = 0 text = "New tab" -[node name="SpritesheetTabOptions" type="HBoxContainer" parent="VBoxContainer/HBoxContainer"] -visible = false +[node name="SpritesheetTabOptions" type="GridContainer" parent="VBoxContainer/HBoxContainer"] margin_left = 155.0 -margin_right = 533.0 +margin_right = 613.0 +margin_bottom = 52.0 +rect_min_size = Vector2( 380, 0 ) +size_flags_horizontal = 3 + +[node name="Manual" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/SpritesheetTabOptions"] +margin_right = 378.0 margin_bottom = 24.0 -[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/SpritesheetTabOptions"] +[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/SpritesheetTabOptions/Manual"] margin_top = 5.0 margin_right = 118.0 margin_bottom = 19.0 text = "Horizontal frames:" -[node name="HorizontalFrames" type="SpinBox" parent="VBoxContainer/HBoxContainer/SpritesheetTabOptions"] +[node name="HorizontalFrames" type="SpinBox" parent="VBoxContainer/HBoxContainer/SpritesheetTabOptions/Manual"] margin_left = 122.0 margin_right = 196.0 margin_bottom = 24.0 @@ -115,14 +117,14 @@ mouse_default_cursor_shape = 2 min_value = 1.0 value = 1.0 -[node name="Label2" type="Label" parent="VBoxContainer/HBoxContainer/SpritesheetTabOptions"] +[node name="Label2" type="Label" parent="VBoxContainer/HBoxContainer/SpritesheetTabOptions/Manual"] margin_left = 200.0 margin_top = 5.0 margin_right = 300.0 margin_bottom = 19.0 text = "Vertical frames:" -[node name="VerticalFrames" type="SpinBox" parent="VBoxContainer/HBoxContainer/SpritesheetTabOptions"] +[node name="VerticalFrames" type="SpinBox" parent="VBoxContainer/HBoxContainer/SpritesheetTabOptions/Manual"] margin_left = 304.0 margin_right = 378.0 margin_bottom = 24.0 @@ -130,11 +132,47 @@ mouse_default_cursor_shape = 2 min_value = 1.0 value = 1.0 +[node name="Smart" type="HBoxContainer" parent="VBoxContainer/HBoxContainer/SpritesheetTabOptions"] +visible = false +margin_top = 28.0 +margin_right = 458.0 +margin_bottom = 52.0 +size_flags_horizontal = 3 + +[node name="Threshold" parent="VBoxContainer/HBoxContainer/SpritesheetTabOptions/Smart" instance=ExtResource( 2 )] +margin_right = 195.0 +hint_tooltip = "Image having any one side smaller than this value will closs the threshold" +min_value = 1.0 +value = 10.0 +allow_greater = true +prefix = "Threshold:" + +[node name="MergeDist" parent="VBoxContainer/HBoxContainer/SpritesheetTabOptions/Smart" instance=ExtResource( 2 )] +margin_left = 199.0 +margin_right = 394.0 +hint_tooltip = "image (that crossed the threshold), this distance apart to any other image +will get merged with that image" +value = 3.0 +prefix = "Merge distance:" + +[node name="Slice" type="Button" parent="VBoxContainer/HBoxContainer/SpritesheetTabOptions/Smart"] +margin_left = 398.0 +margin_right = 458.0 +margin_bottom = 24.0 +text = "Refresh" + +[node name="SmartSlice" type="CheckBox" parent="VBoxContainer/HBoxContainer/SpritesheetTabOptions"] +margin_top = 28.0 +margin_right = 378.0 +margin_bottom = 52.0 +text = "Smart Slice" + [node name="SpritesheetLayerOptions" type="HBoxContainer" parent="VBoxContainer/HBoxContainer"] visible = false margin_left = 155.0 margin_right = 307.0 margin_bottom = 24.0 +size_flags_vertical = 0 [node name="Label3" type="Label" parent="VBoxContainer/HBoxContainer/SpritesheetLayerOptions"] margin_top = 5.0 @@ -264,9 +302,14 @@ margin_bottom = 24.0 [connection signal="about_to_show" from="." to="." method="_on_PreviewDialog_about_to_show"] [connection signal="confirmed" from="." to="." method="_on_PreviewDialog_confirmed"] +[connection signal="item_rect_changed" from="." to="." method="_on_PreviewDialog_item_rect_changed"] [connection signal="popup_hide" from="." to="." method="_on_PreviewDialog_popup_hide"] [connection signal="toggled" from="VBoxContainer/ApplyAll" to="." method="_on_ApplyAll_toggled"] [connection signal="item_selected" from="VBoxContainer/HBoxContainer/ImportOption" to="." method="_on_ImportOption_item_selected"] -[connection signal="value_changed" from="VBoxContainer/HBoxContainer/SpritesheetTabOptions/HorizontalFrames" to="." method="_on_HorizontalFrames_value_changed"] -[connection signal="value_changed" from="VBoxContainer/HBoxContainer/SpritesheetTabOptions/VerticalFrames" to="." method="_on_VerticalFrames_value_changed"] +[connection signal="value_changed" from="VBoxContainer/HBoxContainer/SpritesheetTabOptions/Manual/HorizontalFrames" to="." method="_on_HorizontalFrames_value_changed"] +[connection signal="value_changed" from="VBoxContainer/HBoxContainer/SpritesheetTabOptions/Manual/VerticalFrames" to="." method="_on_VerticalFrames_value_changed"] +[connection signal="value_changed" from="VBoxContainer/HBoxContainer/SpritesheetTabOptions/Smart/Threshold" to="." method="_on_Threshold_value_changed"] +[connection signal="value_changed" from="VBoxContainer/HBoxContainer/SpritesheetTabOptions/Smart/MergeDist" to="." method="_on_MergeDist_value_changed"] +[connection signal="pressed" from="VBoxContainer/HBoxContainer/SpritesheetTabOptions/Smart/Slice" to="." method="_on_Slice_pressed"] +[connection signal="toggled" from="VBoxContainer/HBoxContainer/SpritesheetTabOptions/SmartSlice" to="." method="_on_SmartSlice_toggled"] [connection signal="item_selected" from="VBoxContainer/HBoxContainer/NewBrushOptions/BrushTypeOption" to="." method="_on_BrushTypeOption_item_selected"]