mirror of
https://github.com/Orama-Interactive/Pixelorama.git
synced 2025-01-19 01:29:49 +00:00
42428595c5
* 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
5745b26a6e
* 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
252 lines
8.6 KiB
GDScript
252 lines
8.6 KiB
GDScript
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)
|