Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Added autoSlicer * Fixed the remaining things (Autoslicer fully functional) * Update RegionUnpacker.gd * Formatting * formatting * formatting * neatify * Update RegionUnpacker.gd * formatting * formatting * Update RegionUnpacker.gd * Update README.md * Added region unpacker class * Optimized the region unpacker, this should now be twice as fast addon version: f01526e50db98eea6d4d69db3c241d360887af7f * change Smart Slicer to 5745b26a6e0b7e10bc4a46d07b5f9f0dd8f26c96 Variable-Interactive/SmartSlicer@5745b26 * Delete addons/SmartSlicer/Shader directory * removed shader * Update SmartSlicer Version 278b1c5a80b2c8b89279e405156d556732ce98d2 * Formatting (This is torture LOL) 2578b74ba84289aa109ae715b4a6c90fd5e23126 * Delete addons/SmartSlicer/addons/SmartSlicer/Classes directory * Formatting Version remains same * Delete SmartSlicePreview.gd * use _draw instead of line2d and moved SmartSlicerPreview.gd for better organization * Formatting * More formatting * Fix bugs related to import * fix crash on attempting to open empty image as new spritesheet tab (smart) * removed accidental print * fix empty image warnings
- Loading branch information
1 parent
42196cd
commit 4242859
Showing
8 changed files
with
667 additions
and
115 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.