1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 09:09:47 +00:00

Spritesheet Autoslicer (#893)

* 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
This commit is contained in:
Variable 2023-08-06 16:56:39 +05:00 committed by GitHub
parent 42196cda3b
commit 42428595c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 668 additions and 116 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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": "",

View file

@ -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())

View file

@ -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)

View file

@ -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)

View file

@ -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_SmartSlice_toggled(button_pressed: bool) -> void:
setup_smart_slice(button_pressed)
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()
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 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
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
for child in texture_rect.get_node("HorizLines").get_children():
child.queue_free()
spritesheet_frame_value_changed(value, false)
spritesheet_frame_value_changed()
func _on_VerticalFrames_value_changed(value: int) -> void:
spritesheet_vertical = value
for child in texture_rect.get_node("VerticalLines").get_children():
child.queue_free()
spritesheet_frame_value_changed(value, true)
spritesheet_frame_value_changed()
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
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 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)

View file

@ -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"]