diff --git a/Example/Texture.gd b/Example/Texture.gd index b3a600e..15b5d67 100644 --- a/Example/Texture.gd +++ b/Example/Texture.gd @@ -1,17 +1,16 @@ extends TextureRect -var rect_data: Dictionary +var rect_data: RegionUnpacker.RectData # roughly 665886 with old imp func _on_Button_pressed() -> void: var time = Time.get_ticks_usec() var unpak := RegionUnpacker.new(10, 3) - rect_data = unpak.get_used_rects(texture.get_data()) - $Control.show_preview(rect_data["rects"]) + rect_data = unpak.get_used_rects(texture.get_image()) + $Control.show_preview(rect_data.rects) print(Time.get_ticks_usec() - time) - func _on_Texture_item_rect_changed() -> void: - if "rects" in rect_data.keys(): - $Control.show_preview(rect_data["rects"]) + if is_instance_valid(rect_data) and not rect_data.rects.is_empty(): + $Control.show_preview(rect_data.rects) diff --git a/Example/Texture.tscn b/Example/Texture.tscn index a5b6f32..8fc7dce 100644 --- a/Example/Texture.tscn +++ b/Example/Texture.tscn @@ -1,27 +1,47 @@ -[gd_scene load_steps=4 format=2] +[gd_scene load_steps=4 format=3 uid="uid://d35eiq1krfqyk"] -[ext_resource path="res://Example/Texture.gd" type="Script" id=1] -[ext_resource path="res://Example/example sheet.png" type="Texture" id=2] -[ext_resource path="res://addons/SmartSlicer/SmartSlicePreview.gd" type="Script" id=3] +[ext_resource type="Script" path="res://Example/Texture.gd" id="1"] +[ext_resource type="Texture2D" uid="uid://bwh16a5td5gwe" path="res://Example/example sheet.png" id="2"] +[ext_resource type="Script" path="res://addons/SmartSlicer/SmartSlicePreview.gd" id="3"] -[node name="Texture" type="TextureRect"] +[node name="Control" type="Control"] +layout_mode = 3 +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -texture = ExtResource( 2 ) -stretch_mode = 6 -script = ExtResource( 1 ) +grow_horizontal = 2 +grow_vertical = 2 -[node name="Control" type="Control" parent="."] +[node name="AspectRatioContainer" type="AspectRatioContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 -script = ExtResource( 3 ) +offset_right = 1.0 +offset_bottom = -2.0 +grow_horizontal = 2 +grow_vertical = 2 +ratio = 1.9912 + +[node name="Texture" type="TextureRect" parent="AspectRatioContainer"] +layout_mode = 2 +texture = ExtResource("2") +stretch_mode = 5 +script = ExtResource("1") + +[node name="Control" type="Control" parent="AspectRatioContainer/Texture"] +anchors_preset = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("3") [node name="Button" type="Button" parent="."] -margin_left = 791.0 -margin_top = 495.0 -margin_right = 984.0 -margin_bottom = 563.0 +layout_mode = 0 +offset_left = 791.0 +offset_top = 495.0 +offset_right = 984.0 +offset_bottom = 563.0 text = "Slice" -[connection signal="item_rect_changed" from="." to="." method="_on_Texture_item_rect_changed"] -[connection signal="pressed" from="Button" to="." method="_on_Button_pressed"] +[connection signal="item_rect_changed" from="AspectRatioContainer/Texture" to="AspectRatioContainer/Texture" method="_on_Texture_item_rect_changed"] +[connection signal="pressed" from="Button" to="AspectRatioContainer/Texture" method="_on_Button_pressed"] diff --git a/Example/example sheet.png.import b/Example/example sheet.png.import index 359cb7d..b6757a3 100644 --- a/Example/example sheet.png.import +++ b/Example/example sheet.png.import @@ -1,8 +1,9 @@ [remap] importer="texture" -type="StreamTexture" -path="res://.import/example sheet.png-0360b514ecb50cc6e83b9cd009b2aff9.stex" +type="CompressedTexture2D" +uid="uid://bwh16a5td5gwe" +path="res://.godot/imported/example sheet.png-0360b514ecb50cc6e83b9cd009b2aff9.ctex" metadata={ "vram_texture": false } @@ -10,26 +11,24 @@ metadata={ [deps] source_file="res://Example/example sheet.png" -dest_files=[ "res://.import/example sheet.png-0360b514ecb50cc6e83b9cd009b2aff9.stex" ] +dest_files=["res://.godot/imported/example sheet.png-0360b514ecb50cc6e83b9cd009b2aff9.ctex"] [params] compress/mode=0 +compress/high_quality=false compress/lossy_quality=0.7 -compress/hdr_mode=0 -compress/bptc_ldr=0 +compress/hdr_compression=1 compress/normal_map=0 -flags/repeat=0 -flags/filter=false -flags/mipmaps=false -flags/anisotropic=false -flags/srgb=2 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" process/fix_alpha_border=true process/premult_alpha=false -process/HDR_as_SRGB=false -process/invert_color=false process/normal_map_invert_y=false -stream=false -size_limit=0 -detect_3d=true -svg/scale=1.0 +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/SmartSlicer/Classes/RegionUnpacker.gd b/addons/SmartSlicer/Classes/RegionUnpacker.gd index 11f01f7..2625412 100644 --- a/addons/SmartSlicer/Classes/RegionUnpacker.gd +++ b/addons/SmartSlicer/Classes/RegionUnpacker.gd @@ -1,19 +1,42 @@ class_name RegionUnpacker -extends Reference +extends RefCounted # 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 +var _include_boundary_threshold: int ## Τhe size of rect below which merging accounts for boundary +## After crossing threshold the smaller image will merge with larger image +## if it is within the _merge_dist +var _merge_dist: int -# 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 +## Working array used as buffer for segments while flooding +var _allegro_flood_segments: Array[Segment] +## Results array per image while flooding +var _allegro_image_segments: Array[Segment] + + +class RectData: + var rects: Array[Rect2i] + var frame_size: Vector2i + + func _init(_rects: Array[Rect2i], _frame_size: Vector2i): + rects = _rects + frame_size = _frame_size + + +class Segment: + var flooding := false + var todo_above := false + var todo_below := false + var left_position := -5 + var right_position := -5 + var y := 0 + var next := 0 + + func _init(_y: int) -> void: + y = _y func _init(threshold: int, merge_dist: int) -> void: @@ -21,63 +44,58 @@ func _init(threshold: int, merge_dist: int) -> void: _merge_dist = merge_dist -func get_used_rects(image: Image) -> Dictionary: - if OS.get_name() == "HTML5": +func get_used_rects(image: Image) -> RectData: + if ProjectSettings.get_setting("rendering/driver/threads/thread_model") != 2: + # Single-threaded mode return get_rects(image) - else: - # If Thread model is set to "Multi-Threaded" in project settings>threads>thread model - if slice_thread.is_active(): + else: # Multi-threaded mode + if slice_thread.is_started(): slice_thread.wait_to_finish() - var error = slice_thread.start(self, "get_rects", image) + var error := slice_thread.start(get_rects.bind(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: +func get_rects(image: Image) -> RectData: + # Make a smaller image to make the loop shorter + var used_rect := image.get_used_rect() + if used_rect.size == Vector2i.ZERO: return clean_rects([]) - var test_image = image.get_rect(used_rect) - # prepare a bitmap to keep track of previous places + var test_image := image.get_region(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 + var rects: Array[Rect2i] = [] + var frame_size := Vector2i.ZERO for y in test_image.get_size().y: for x in test_image.get_size().x: - var position = Vector2(x, y) + var position := Vector2i(x, y) if test_image.get_pixelv(position).a > 0: # used portion of image detected - if !scanned_area.get_bit(position): + if !scanned_area.get_bitv(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") + var rects_info := clean_rects(rects) + rects_info.rects.sort_custom(sort_rects) return rects_info -func clean_rects(rects: Array) -> Dictionary: - var frame_size = Vector2.ZERO +func clean_rects(rects: Array[Rect2i]) -> RectData: + var frame_size := Vector2i.ZERO for i in rects.size(): - var target: Rect2 = rects.pop_front() - var test_rect = target + var target: Rect2i = 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 + test_rect.size += Vector2i(_merge_dist, _merge_dist) + test_rect.position -= Vector2i(_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]) @@ -91,61 +109,51 @@ func clean_rects(rects: Array) -> Dictionary: 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} + return RectData.new(rects, frame_size) -func sort_rects(rect_a: Rect2, rect_b: Rect2) -> bool: +func sort_rects(rect_a: Rect2i, rect_b: Rect2i) -> 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): + var start := rect_a.position + var size := Vector2i(rect_b.end.x, rect_a.end.y) + if Rect2i(start, size).intersects(rect_b): return true return false -func _estimate_rect(image: Image, position: Vector2) -> Rect2: +func _estimate_rect(image: Image, position: Vector2) -> Rect2i: 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() + var small_rect := _flood_fill(position, cel_image) 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) +## Add a new segment to the array +func _add_new_segment(y := 0) -> void: + _allegro_flood_segments.append(Segment.new(y)) -# 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 +## 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. +## this method is called by `_flood_fill` after the required data structures +## have been initialized +func _flood_line_around_point(position: Vector2i, image: Image) -> int: if not image.get_pixelv(position).a > 0: - return int(position.x) + 1 - var west: Vector2 = position - var east: Vector2 = position + return position.x + 1 + var west := position + var east := position while west.x >= 0 && image.get_pixelv(west).a > 0: - west += Vector2.LEFT + west += Vector2i.LEFT while east.x < image.get_width() && image.get_pixelv(east).a > 0: - east += Vector2.RIGHT + east += Vector2i.RIGHT # Make a note of the stuff we processed - var c = int(position.y) - var segment = _allegro_flood_segments[c] + var c := 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: @@ -154,7 +162,7 @@ func _flood_line_around_point(position: Vector2, image: Image) -> int: # found last current segment on this line c = _allegro_flood_segments.size() segment.next = c - _add_new_segment(int(position.y)) + _add_new_segment(position.y) segment = _allegro_flood_segments[c] # set the values for the current segment segment.flooding = true @@ -177,28 +185,28 @@ func _flood_line_around_point(position: Vector2, image: Image) -> int: _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 + return east.x + 1 func _check_flooded_segment(y: int, left: int, right: int, image: Image) -> bool: - var ret = false - var c: int = 0 + var ret := false + var c := 0 while left <= right: c = y while true: - var segment = _allegro_flood_segments[c] + 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) + left = _flood_line_around_point(Vector2i(left, y), image) ret = true break return ret -func _flood_fill(position: Vector2, image: Image) -> Rect2: +func _flood_fill(position: Vector2i, image: Image) -> Rect2i: # 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 @@ -208,17 +216,15 @@ func _flood_fill(position: Vector2, image: Image) -> Rect2: # 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() + var final_image := Image.new() final_image.copy_from(image) - final_image.fill(Color.transparent) - final_image.lock() + final_image.fill(Color.TRANSPARENT) _select_segments(final_image) - final_image.unlock() return final_image.get_used_rect() -func _compute_segments_for_image(position: Vector2, image: Image) -> void: +func _compute_segments_for_image(position: Vector2i, image: Image) -> void: # initially allocate at least 1 segment per line of image for j in image.get_height(): _add_new_segment(j) @@ -228,9 +234,9 @@ func _compute_segments_for_image(position: Vector2, image: Image) -> void: var done := false while not done: done = true - var max_index = _allegro_flood_segments.size() + var max_index := _allegro_flood_segments.size() for c in max_index: - var p = _allegro_flood_segments[c] + 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): @@ -244,8 +250,8 @@ func _compute_segments_for_image(position: Vector2, image: Image) -> void: 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) + var p := _allegro_image_segments[c] + var rect := Rect2i() + rect.position = Vector2i(p.left_position, p.y) + rect.end = Vector2i(p.right_position + 1, p.y + 1) + map.fill_rect(rect, Color.WHITE) diff --git a/addons/SmartSlicer/SmartSlicePreview.gd b/addons/SmartSlicer/SmartSlicePreview.gd index 906256b..a6a9a77 100644 --- a/addons/SmartSlicer/SmartSlicePreview.gd +++ b/addons/SmartSlicer/SmartSlicePreview.gd @@ -1,34 +1,34 @@ extends Control +## Add this as a child of the texturerect that contains the main spritesheet -# 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 color := Color("6680ff") ## Set this to a theme color later +var _sliced_rects: Array[Rect2i] var _stretch_amount: float var _offset: Vector2 -func show_preview(sliced_rects: Array) -> void: - var image = get_parent().texture.get_data() +func show_preview(sliced_rects: Array[Rect2i]) -> void: + var image: Image = get_parent().texture.get_image() if image.get_size().x > image.get_size().y: - _stretch_amount = rect_size.x / image.get_size().x + _stretch_amount = size.x / image.get_size().x else: - _stretch_amount = rect_size.y / image.get_size().y + _stretch_amount = size.y / image.get_size().y _sliced_rects = sliced_rects.duplicate() - _offset = (0.5 * (rect_size - (image.get_size() * _stretch_amount))).floor() - update() + _offset = (0.5 * (size - (image.get_size() * _stretch_amount))).floor() + queue_redraw() func _draw() -> void: draw_set_transform(_offset, 0, Vector2.ONE) for i in _sliced_rects.size(): - var rect = _sliced_rects[i] + 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") + var font: Font = Control.new().get_theme_font("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/project.godot b/project.godot index f2f6158..1736451 100644 --- a/project.godot +++ b/project.godot @@ -6,32 +6,19 @@ ; [section] ; section goes between [] ; param=value ; assign values to parameters -config_version=4 - -_global_script_classes=[ { -"base": "Reference", -"class": "RegionUnpacker", -"language": "GDScript", -"path": "res://addons/SmartSlicer/Classes/RegionUnpacker.gd" -} ] -_global_script_class_icons={ -"RegionUnpacker": "" -} +config_version=5 [application] config/name="SmartSlicer" run/main_scene="res://Example/Texture.tscn" +config/features=PackedStringArray("4.1") [display] -window/stretch/mode="2d" +window/stretch/mode="canvas_items" window/stretch/aspect="expand" -[global] - -thread=false - [gui] common/drop_mouse_on_gui_input_disabled=true @@ -42,7 +29,5 @@ common/enable_pause_aware_picking=true [rendering] -quality/driver/driver_name="GLES2" -threads/thread_model=2 +renderer/rendering_method="gl_compatibility" vram_compression/import_etc=true -vram_compression/import_etc2=false