First commit 🎉
This commit is contained in:
commit
43ea213f9b
728 changed files with 37080 additions and 0 deletions
390
addons/popochiu/editor/importers/aseprite/animation_creator.gd
Normal file
390
addons/popochiu/editor/importers/aseprite/animation_creator.gd
Normal file
|
@ -0,0 +1,390 @@
|
|||
@tool
|
||||
# This logic has been taken almost as-is from Vinicius Gerevini's
|
||||
# Aseprite Wizard plugin. Credits goes to him for the real magic.
|
||||
# See: https://godotengine.org/asset-library/asset/713
|
||||
extends RefCounted
|
||||
|
||||
const RESULT_CODE = preload("res://addons/popochiu/editor/config/result_codes.gd")
|
||||
const _DEFAULT_AL = "" # Empty string equals default "Global" animation library
|
||||
|
||||
# Vars configured on initialization
|
||||
var _file_system: EditorFileSystem
|
||||
var _aseprite: RefCounted
|
||||
|
||||
# Vars configured on animations creation
|
||||
var _target_node: Node
|
||||
var _player: AnimationPlayer
|
||||
var _options: Dictionary
|
||||
|
||||
# Class-logic vars
|
||||
var _spritesheet_metadata = {}
|
||||
var _target_sprite: Sprite2D
|
||||
var _output: Dictionary
|
||||
|
||||
|
||||
#region Public #####################################################################################
|
||||
func init(aseprite: RefCounted, editor_file_system: EditorFileSystem = null):
|
||||
_file_system = editor_file_system
|
||||
_aseprite = aseprite
|
||||
|
||||
|
||||
## Public interfaces, dedicated to specific popochiu objects
|
||||
func create_character_animations(character: Node, player: AnimationPlayer, options: Dictionary):
|
||||
# Chores
|
||||
_target_node = character
|
||||
_player = player
|
||||
_options = options
|
||||
|
||||
# Duly check everything is valid and cleanup animations
|
||||
var result = _perform_common_checks()
|
||||
if result != RESULT_CODE.SUCCESS:
|
||||
return result
|
||||
|
||||
# Create the spritesheet
|
||||
result = await _create_spritesheet_from_file()
|
||||
if result != RESULT_CODE.SUCCESS:
|
||||
return result
|
||||
|
||||
# Load tags information
|
||||
result = await _load_spritesheet_metadata()
|
||||
if result != RESULT_CODE.SUCCESS:
|
||||
return result
|
||||
|
||||
# Set the texture in the sprite and configure
|
||||
# the animations in the AnimationPlayer
|
||||
_setup_texture()
|
||||
result = _configure_animations()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
func create_prop_animations(prop: Node, aseprite_tag: String, options: Dictionary):
|
||||
# Chores
|
||||
_target_node = prop
|
||||
# TODO: if the prop has no AnimationPlayer, add one!
|
||||
_player = prop.get_node("AnimationPlayer")
|
||||
_options = options
|
||||
|
||||
var prop_animation_name = aseprite_tag.to_snake_case()
|
||||
|
||||
# Duly check everything is valid and cleanup animations
|
||||
var result = _perform_common_checks()
|
||||
if result != RESULT_CODE.SUCCESS:
|
||||
return result
|
||||
|
||||
# Create the spritesheet
|
||||
result = await _create_spritesheet_from_tag(aseprite_tag)
|
||||
if result != RESULT_CODE.SUCCESS:
|
||||
return result
|
||||
|
||||
# Load tags information
|
||||
result = await _load_spritesheet_metadata(aseprite_tag)
|
||||
if result != RESULT_CODE.SUCCESS:
|
||||
return result
|
||||
|
||||
# Set the texture in the sprite and configure
|
||||
# the animations in the AnimationPlayer
|
||||
_setup_texture()
|
||||
result = _configure_animations()
|
||||
|
||||
# Sorry, mom...
|
||||
_player.autoplay = prop.name.to_snake_case()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private ####################################################################################
|
||||
## This function creates a spritesheet with the whole file content
|
||||
func _create_spritesheet_from_file():
|
||||
## TODO: See _aseprite.export_layer() when the time comes to add layers selection
|
||||
_output = _aseprite.export_file(_options.source, _options.output_folder, _options)
|
||||
if _output.is_empty():
|
||||
return RESULT_CODE.ERR_ASEPRITE_EXPORT_FAILED
|
||||
return RESULT_CODE.SUCCESS
|
||||
|
||||
|
||||
## This function creates a spritesheet with the frames of a specific tag
|
||||
## WARNING: it's case sensitive
|
||||
func _create_spritesheet_from_tag(selected_tag: String):
|
||||
## TODO: See _aseprite.export_layer() when the time comes to add layers selection
|
||||
_output = _aseprite.export_tag(_options.source, selected_tag, _options.output_folder, _options)
|
||||
if _output.is_empty():
|
||||
return RESULT_CODE.ERR_ASEPRITE_EXPORT_FAILED
|
||||
return RESULT_CODE.SUCCESS
|
||||
|
||||
|
||||
func _load_spritesheet_metadata(selected_tag: String = ""):
|
||||
_spritesheet_metadata = {
|
||||
tags = {},
|
||||
frames = {},
|
||||
meta = {},
|
||||
sprite_sheet = {}
|
||||
}
|
||||
|
||||
# Refresh filesystem
|
||||
await _scan_filesystem()
|
||||
|
||||
# Collect all needed info
|
||||
var source_file = _output.data_file
|
||||
var sprite_sheet = _output.sprite_sheet
|
||||
|
||||
# Try to access, decode and validate Aseprite JSON output
|
||||
var file = FileAccess.open(source_file, FileAccess.READ)
|
||||
if file == null:
|
||||
return file.get_open_error()
|
||||
|
||||
var test_json_conv = JSON.new()
|
||||
test_json_conv.parse(file.get_as_text())
|
||||
var content = test_json_conv.get_data()
|
||||
|
||||
if not _aseprite.is_valid_spritesheet(content):
|
||||
return RESULT_CODE.ERR_INVALID_ASEPRITE_SPRITESHEET
|
||||
|
||||
# Save image metadata from JSON data
|
||||
_spritesheet_metadata.meta = content.meta
|
||||
|
||||
# Save frames metadata from JSON data
|
||||
_spritesheet_metadata.frames = _aseprite.get_content_frames(content)
|
||||
|
||||
# Save tags metadata, starting from user's selection, and retrieving
|
||||
# other information from JSON data
|
||||
var tags = _options.get("tags").filter(func(tag): return tag.get("import"))
|
||||
|
||||
for t in tags:
|
||||
# If a tag is specified, ignore every other ones
|
||||
if not selected_tag.is_empty() and selected_tag != t.tag_name: continue
|
||||
# Create a lookup table for tags
|
||||
_spritesheet_metadata.tags[t.tag_name] = t
|
||||
|
||||
for ft in _aseprite.get_content_meta_tags(content):
|
||||
if not _spritesheet_metadata.tags.has(ft.name): continue
|
||||
_spritesheet_metadata.tags.get(ft.name).merge({
|
||||
from = ft.from,
|
||||
to = ft.to,
|
||||
direction = ft.direction,
|
||||
})
|
||||
|
||||
# If a tag is specified, the tags lookup table should contain
|
||||
# a single tag information. In this case the to and from properties
|
||||
# must be shifted back in the [1 - tag_length] range.
|
||||
if not selected_tag.is_empty():
|
||||
# Using a temp variable to make this readable
|
||||
var t = _spritesheet_metadata.tags[selected_tag]
|
||||
# NOTE: imagine this goes from 34 to 54, we need to shift
|
||||
# the range back of a 33 amount, so it goes from 1 to (54 - 33)
|
||||
t.to = t.to - t.from + 1
|
||||
t.from = 0
|
||||
_spritesheet_metadata.tags[selected_tag] = t
|
||||
|
||||
# Save spritesheet path from the command output
|
||||
_spritesheet_metadata.sprite_sheet = sprite_sheet
|
||||
|
||||
# Remove the JSON file if config says so
|
||||
if PopochiuEditorConfig.should_remove_source_files():
|
||||
DirAccess.remove_absolute(_output.data_file)
|
||||
await _scan_filesystem()
|
||||
|
||||
return RESULT_CODE.SUCCESS
|
||||
|
||||
|
||||
func _configure_animations():
|
||||
if not _player.has_animation_library(_DEFAULT_AL):
|
||||
_player.add_animation_library(_DEFAULT_AL, AnimationLibrary.new())
|
||||
|
||||
if _spritesheet_metadata.tags.size() > 0:
|
||||
var result = RESULT_CODE.SUCCESS
|
||||
# RESTART_FROM_HERE: WARNING: in case of prop and inventory, the JSON file contains
|
||||
# the whole set of tags, so we must take the tag.from and tag.to and remap the range
|
||||
# from "1" to "tag.to +1 - tag.from + 1" (do the math an you'll see that's correct)
|
||||
for tag in _spritesheet_metadata.tags.values():
|
||||
var selected_frames = _spritesheet_metadata.frames.slice(tag.from, tag.to + 1) # slice is [)
|
||||
result = _add_animation_frames(tag.tag_name, selected_frames, tag.direction)
|
||||
if result != RESULT_CODE.SUCCESS:
|
||||
break
|
||||
return result
|
||||
else:
|
||||
return _add_animation_frames("default", _spritesheet_metadata.frames)
|
||||
|
||||
|
||||
func _add_animation_frames(anim_name: String, frames: Array, direction = 'forward'):
|
||||
# TODO: ATM there is no way to assign a walk/talk/grab/idle animation
|
||||
# with a different name than the standard ones. The engine is searching for
|
||||
# lowercase names in the AnimationPlayer, thus we are forcing snake_case
|
||||
# animations name conversion.
|
||||
# We have to add methods or properties to the Character to assign different
|
||||
# animations (but maybe we can do with anim_prefix or other strategies).
|
||||
var animation_name = anim_name.to_snake_case()
|
||||
var is_loopable = _spritesheet_metadata.tags.get(anim_name).get("loops")
|
||||
|
||||
# Create animation library if it doesn't exist
|
||||
# This is always true if the user selected to wipe old animations.
|
||||
# See _remove_animations_from_player() function.
|
||||
if not _player.has_animation_library(_DEFAULT_AL):
|
||||
_player.add_animation_library(_DEFAULT_AL, AnimationLibrary.new())
|
||||
|
||||
if not _player.get_animation_library(_DEFAULT_AL).has_animation(animation_name):
|
||||
_player.get_animation_library(_DEFAULT_AL).add_animation(animation_name, Animation.new())
|
||||
|
||||
# Here is where animations are created.
|
||||
# TODO: we need to "fork" the logic so that Character has a single spritesheet
|
||||
# containing all tags, while Rooms/Props and Inventory Items has a single spritesheet
|
||||
# for each tag, so that you can have each prop with its own animation (PnC)
|
||||
var animation = _player.get_animation(animation_name)
|
||||
_create_meta_tracks(animation)
|
||||
var frame_track = _get_property_track_path("frame")
|
||||
var frame_track_index = _create_track(_target_sprite, animation, frame_track)
|
||||
|
||||
if direction == 'reverse':
|
||||
frames.reverse()
|
||||
|
||||
var animation_length = 0
|
||||
|
||||
for frame in frames:
|
||||
var frame_key = _get_frame_key(frame)
|
||||
animation.track_insert_key(frame_track_index, animation_length, frame_key)
|
||||
animation_length += frame.duration / 1000 ## NOTE: animation_length is in seconds
|
||||
|
||||
if direction == 'pingpong':
|
||||
frames.remove_at(frames.size() - 1)
|
||||
if is_loopable:
|
||||
frames.remove_at(0)
|
||||
frames.reverse()
|
||||
|
||||
for frame in frames:
|
||||
var frame_key = _get_frame_key(frame)
|
||||
animation.track_insert_key(frame_track_index, animation_length, frame_key)
|
||||
animation_length += frame.duration / 1000 ## NOTE: animation_length is in seconds
|
||||
|
||||
animation.length = animation_length
|
||||
animation.loop_mode = Animation.LOOP_LINEAR if is_loopable else Animation.LOOP_NONE
|
||||
|
||||
return RESULT_CODE.SUCCESS
|
||||
|
||||
|
||||
## TODO: insert validate tokens in animation name
|
||||
func _create_track(target_sprite: Node, animation: Animation, track: String):
|
||||
var track_index = animation.find_track(track, Animation.TYPE_VALUE)
|
||||
|
||||
if track_index != -1:
|
||||
animation.remove_track(track_index)
|
||||
|
||||
track_index = animation.add_track(Animation.TYPE_VALUE)
|
||||
## Here we set a label for the track in the sprite_path:property_changed format
|
||||
## so that _get_property_track_path can rebuild it by naming convention
|
||||
animation.track_set_path(track_index, track)
|
||||
animation.track_set_interpolation_loop_wrap(track_index, false)
|
||||
animation.value_track_set_update_mode(track_index, Animation.UPDATE_DISCRETE)
|
||||
|
||||
return track_index
|
||||
|
||||
|
||||
func _get_property_track_path(prop: String) -> String:
|
||||
var node_path = _player.get_node(_player.root_node).get_path_to(_target_sprite)
|
||||
return "%s:%s" % [node_path, prop]
|
||||
|
||||
|
||||
func _scan_filesystem():
|
||||
_file_system.scan()
|
||||
await _file_system.filesystem_changed
|
||||
|
||||
|
||||
func _remove_properties_from_path(path: NodePath) -> NodePath:
|
||||
var string_path := path as String
|
||||
if !(":" in string_path):
|
||||
return string_path as NodePath
|
||||
|
||||
var property_path := path.get_concatenated_subnames() as String
|
||||
string_path = string_path.substr(0, string_path.length() - property_path.length() - 1)
|
||||
|
||||
return string_path as NodePath
|
||||
|
||||
|
||||
# ---- SPRITE NODE LOGIC ---------------------------------------------------------------------------
|
||||
## What follow is logic specifically gathered for Sprite elements. TextureRect should
|
||||
## be treated in a different way (see texture_rect_animation_creator.gd file in
|
||||
## original Aseprite Wizard plugin by Vinicius Gerevini)
|
||||
func _setup_texture():
|
||||
# Load texture in target sprite (ignoring cache and forcing a refres)
|
||||
var texture = ResourceLoader.load(
|
||||
_spritesheet_metadata.sprite_sheet, 'Image', ResourceLoader.CACHE_MODE_IGNORE
|
||||
)
|
||||
texture.take_over_path(_spritesheet_metadata.sprite_sheet)
|
||||
_target_sprite.texture = texture
|
||||
|
||||
if _spritesheet_metadata.frames.is_empty():
|
||||
return
|
||||
|
||||
_target_sprite.hframes = (
|
||||
_spritesheet_metadata.meta.size.w / _spritesheet_metadata.frames[0].sourceSize.w
|
||||
)
|
||||
_target_sprite.vframes = (
|
||||
_spritesheet_metadata.meta.size.h / _spritesheet_metadata.frames[0].sourceSize.h
|
||||
)
|
||||
|
||||
|
||||
func _create_meta_tracks(animation: Animation):
|
||||
var hframes_track = _get_property_track_path("hframes")
|
||||
var hframes_track_index = _create_track(_target_sprite, animation, hframes_track)
|
||||
animation.track_insert_key(hframes_track_index, 0, _target_sprite.hframes)
|
||||
|
||||
var vframes_track = _get_property_track_path("vframes")
|
||||
var vframes_track_index = _create_track(_target_sprite, animation, vframes_track)
|
||||
animation.track_insert_key(vframes_track_index, 0, _target_sprite.vframes)
|
||||
|
||||
var visible_track = _get_property_track_path("visible")
|
||||
var visible_track_index = _create_track(_target_sprite, animation, visible_track)
|
||||
animation.track_insert_key(visible_track_index, 0, true)
|
||||
|
||||
|
||||
func _get_frame_key(frame: Dictionary):
|
||||
return _calculate_frame_index(_target_sprite,frame)
|
||||
|
||||
|
||||
func _calculate_frame_index(sprite: Node, frame: Dictionary) -> int:
|
||||
var column = floor(frame.frame.x * sprite.hframes / sprite.texture.get_width())
|
||||
var row = floor(frame.frame.y * sprite.vframes / sprite.texture.get_height())
|
||||
return (row * sprite.hframes) + column
|
||||
|
||||
|
||||
func _perform_common_checks():
|
||||
# Checks
|
||||
if not _aseprite.check_command_path():
|
||||
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FULL_PATH
|
||||
|
||||
if not _aseprite.test_command():
|
||||
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FOUND
|
||||
|
||||
if not FileAccess.file_exists(_options.source):
|
||||
return RESULT_CODE.ERR_SOURCE_FILE_NOT_FOUND
|
||||
|
||||
if not DirAccess.dir_exists_absolute(_options.output_folder):
|
||||
return RESULT_CODE.ERR_OUTPUT_FOLDER_NOT_FOUND
|
||||
|
||||
_target_sprite = _find_sprite_in_target()
|
||||
|
||||
if _target_sprite == null:
|
||||
return RESULT_CODE.ERR_NO_SPRITE_FOUND
|
||||
|
||||
if typeof(_options.get("tags")) != TYPE_ARRAY:
|
||||
return RESULT_CODE.ERR_TAGS_OPTIONS_ARRAY_EMPTY
|
||||
|
||||
if (_options.wipe_old_animations):
|
||||
_remove_animations_from_player(_player)
|
||||
|
||||
return RESULT_CODE.SUCCESS
|
||||
|
||||
|
||||
func _find_sprite_in_target() -> Node:
|
||||
if not _target_node.has_node("Sprite2D"):
|
||||
return null
|
||||
return _target_node.get_node("Sprite2D")
|
||||
|
||||
|
||||
func _remove_animations_from_player(player: AnimationPlayer):
|
||||
if player.has_animation_library(_DEFAULT_AL):
|
||||
player.remove_animation_library(_DEFAULT_AL)
|
||||
|
||||
|
||||
#endregion
|
|
@ -0,0 +1 @@
|
|||
uid://c44sonibms74d
|
249
addons/popochiu/editor/importers/aseprite/aseprite_controller.gd
Normal file
249
addons/popochiu/editor/importers/aseprite/aseprite_controller.gd
Normal file
|
@ -0,0 +1,249 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
|
||||
# ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ PUBLIC ░░░░
|
||||
func export_file(file_name: String, output_folder: String, options: Dictionary) -> Dictionary:
|
||||
var exception_pattern = options.get('exception_pattern', "")
|
||||
var only_visible_layers = options.get('only_visible_layers', false)
|
||||
var output_name = (
|
||||
file_name if options.get('output_filename') == ""
|
||||
else options.get('output_filename', file_name)
|
||||
)
|
||||
var basename = _get_file_basename(output_name)
|
||||
var output_dir = output_folder.replace("res://", "./")
|
||||
var data_file = "%s/%s.json" % [output_dir, basename]
|
||||
var sprite_sheet = "%s/%s.png" % [output_dir, basename]
|
||||
var output = []
|
||||
var arguments = _export_command_common_arguments(file_name, data_file, sprite_sheet)
|
||||
|
||||
if not only_visible_layers:
|
||||
arguments.push_front("--all-layers")
|
||||
|
||||
_add_sheet_type_arguments(arguments, options)
|
||||
|
||||
_add_ignore_layer_arguments(file_name, arguments, exception_pattern)
|
||||
|
||||
var exit_code = _execute(arguments, output)
|
||||
if exit_code != 0:
|
||||
printerr('[Popochiu] Aseprite: failed to export spritesheet')
|
||||
printerr(output)
|
||||
return {}
|
||||
|
||||
return {
|
||||
'data_file': data_file.replace("./", "res://"),
|
||||
"sprite_sheet": sprite_sheet.replace("./", "res://")
|
||||
}
|
||||
|
||||
|
||||
func export_layers(file_name: String, output_folder: String, options: Dictionary) -> Array:
|
||||
var exception_pattern = options.get('exception_pattern', "")
|
||||
var only_visible_layers = options.get('only_visible_layers', false)
|
||||
var basename = _get_file_basename(file_name)
|
||||
var layers = list_layers(file_name, only_visible_layers)
|
||||
var exception_regex = _compile_regex(exception_pattern)
|
||||
|
||||
var output = []
|
||||
|
||||
for layer in layers:
|
||||
if layer != "" and (not exception_regex or exception_regex.search(layer) == null):
|
||||
output.push_back(export_layer(file_name, layer, output_folder, options))
|
||||
|
||||
return output
|
||||
|
||||
|
||||
func export_layer(file_name: String, layer_name: String, output_folder: String, options: Dictionary) -> Dictionary:
|
||||
var output_prefix = options.get('output_filename', "").strip_edges()
|
||||
var output_dir = output_folder.replace("res://", "./").strip_edges()
|
||||
var data_file = "%s/%s%s.json" % [output_dir, output_prefix, layer_name]
|
||||
var sprite_sheet = "%s/%s%s.png" % [output_dir, output_prefix, layer_name]
|
||||
var output = []
|
||||
var arguments = _export_command_common_arguments(file_name, data_file, sprite_sheet)
|
||||
arguments.push_front(layer_name)
|
||||
arguments.push_front("--layer")
|
||||
|
||||
_add_sheet_type_arguments(arguments, options)
|
||||
|
||||
var exit_code = _execute(arguments, output)
|
||||
if exit_code != 0:
|
||||
printerr('[Popochiu] Aseprite: Failed to export layer spritesheet. Command output follows:')
|
||||
print(output)
|
||||
return {}
|
||||
|
||||
return {
|
||||
'data_file': data_file.replace("./", "res://"),
|
||||
"sprite_sheet": sprite_sheet.replace("./", "res://")
|
||||
}
|
||||
|
||||
|
||||
# IMPROVE: See if we can extract JSON data limited to the single tag
|
||||
# (so we don't have to reckon offset framerange)
|
||||
func export_tag(file_name: String, tag_name: String, output_folder: String, options: Dictionary) -> Dictionary:
|
||||
var output_prefix = options.get('output_filename', "").strip_edges()
|
||||
var output_dir = output_folder.replace("res://", "./").strip_edges()
|
||||
var data_file = "%s/%s%s.json" % [output_dir, output_prefix, tag_name]
|
||||
var sprite_sheet = "%s/%s%s.png" % [output_dir, output_prefix, tag_name]
|
||||
var output = []
|
||||
var arguments = _export_command_common_arguments(file_name, data_file, sprite_sheet)
|
||||
arguments.push_front(tag_name)
|
||||
arguments.push_front("--tag")
|
||||
|
||||
_add_sheet_type_arguments(arguments, options)
|
||||
|
||||
var exit_code = _execute(arguments, output)
|
||||
if exit_code != 0:
|
||||
printerr('[Popochiu] Aseprite: Failed to export tag spritesheet. Command output follows:')
|
||||
print(output)
|
||||
return {}
|
||||
|
||||
return {
|
||||
'data_file': data_file.replace("./", "res://"),
|
||||
"sprite_sheet": sprite_sheet.replace("./", "res://")
|
||||
}
|
||||
|
||||
|
||||
func list_layers(file_name: String, only_visible = false) -> Array:
|
||||
var output = []
|
||||
var arguments = ["-b", "--list-layers", file_name]
|
||||
|
||||
if not only_visible:
|
||||
arguments.push_front("--all-layers")
|
||||
|
||||
var exit_code = _execute(arguments, output)
|
||||
|
||||
if exit_code != 0:
|
||||
printerr('[Popochiu] Aseprite: failed listing layers')
|
||||
printerr(output)
|
||||
return []
|
||||
|
||||
return _sanitize_list_output(output)
|
||||
|
||||
|
||||
func list_tags(file_name: String) -> Array:
|
||||
var output = []
|
||||
var arguments = ["-b", "--list-tags", file_name]
|
||||
|
||||
var exit_code = _execute(arguments, output)
|
||||
|
||||
if exit_code != 0:
|
||||
printerr('[Popochiu] Aseprite: failed listing tags')
|
||||
printerr(output)
|
||||
return []
|
||||
|
||||
return _sanitize_list_output(output)
|
||||
|
||||
|
||||
func is_valid_spritesheet(content):
|
||||
return content.has("frames") and content.has("meta") and content.meta.has('image')
|
||||
|
||||
|
||||
func get_content_frames(content):
|
||||
return content.frames if typeof(content.frames) == TYPE_ARRAY else content.frames.values()
|
||||
|
||||
|
||||
func get_content_meta_tags(content):
|
||||
return content.meta.frameTags if content.meta.has("frameTags") else []
|
||||
|
||||
|
||||
func check_command_path():
|
||||
# On Linux, MacOS or other *nix platforms, nothing to do
|
||||
if not OS.get_name() in ["Windows", "UWP"]:
|
||||
return true
|
||||
|
||||
# On Windows, OS.Execute() calls trigger an uncatchable
|
||||
# internal error if the invoked executable is not found.
|
||||
# Since the error is unclear, we have to check that the aseprite
|
||||
# command is given as a full path and return an error if it's not.
|
||||
var regex = RegEx.new()
|
||||
regex.compile("^[A-Z|a-z]:[\\\\|\\/].+\\.exe$")
|
||||
return \
|
||||
regex.search(_get_aseprite_command()) \
|
||||
and \
|
||||
FileAccess.file_exists(_get_aseprite_command())
|
||||
|
||||
|
||||
func test_command():
|
||||
var exit_code = OS.execute(_get_aseprite_command(), ['--version'], [], true)
|
||||
return exit_code == 0
|
||||
|
||||
|
||||
|
||||
# ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ PRIVATE ░░░░
|
||||
func _add_ignore_layer_arguments(file_name: String, arguments: Array, exception_pattern: String):
|
||||
var layers = _get_exception_layers(file_name, exception_pattern)
|
||||
if not layers.is_empty():
|
||||
for l in layers:
|
||||
arguments.push_front(l)
|
||||
arguments.push_front('--ignore-layer')
|
||||
|
||||
|
||||
func _add_sheet_type_arguments(arguments: Array, options : Dictionary):
|
||||
var column_count : int = options.get("column_count", 0)
|
||||
if column_count > 0:
|
||||
arguments.push_back("--merge-duplicates") # Yes, this is undocumented
|
||||
arguments.push_back("--sheet-columns")
|
||||
arguments.push_back(column_count)
|
||||
else:
|
||||
arguments.push_back("--sheet-pack")
|
||||
|
||||
|
||||
func _get_exception_layers(file_name: String, exception_pattern: String) -> Array:
|
||||
var layers = list_layers(file_name)
|
||||
var regex = _compile_regex(exception_pattern)
|
||||
if regex == null:
|
||||
return []
|
||||
|
||||
var exception_layers = []
|
||||
for layer in layers:
|
||||
if regex.search(layer) != null:
|
||||
exception_layers.push_back(layer)
|
||||
|
||||
return exception_layers
|
||||
|
||||
|
||||
func _sanitize_list_output(output) -> Array:
|
||||
if output.is_empty():
|
||||
return output
|
||||
|
||||
var raw = output[0].split('\n')
|
||||
var sanitized = []
|
||||
for s in raw:
|
||||
sanitized.append(s.strip_edges())
|
||||
return sanitized
|
||||
|
||||
|
||||
func _export_command_common_arguments(source_name: String, data_path: String, spritesheet_path: String) -> Array:
|
||||
return [
|
||||
"-b",
|
||||
"--list-tags",
|
||||
"--data",
|
||||
data_path,
|
||||
"--format",
|
||||
"json-array",
|
||||
"--sheet",
|
||||
spritesheet_path,
|
||||
source_name
|
||||
]
|
||||
|
||||
|
||||
func _execute(arguments, output):
|
||||
return OS.execute(_get_aseprite_command(), arguments, output, true, true)
|
||||
|
||||
|
||||
func _get_aseprite_command() -> String:
|
||||
return PopochiuEditorConfig.get_command()
|
||||
|
||||
|
||||
func _get_file_basename(file_path: String) -> String:
|
||||
return file_path.get_file().trim_suffix('.%s' % file_path.get_extension())
|
||||
|
||||
|
||||
func _compile_regex(pattern):
|
||||
if pattern == "":
|
||||
return
|
||||
|
||||
var rgx = RegEx.new()
|
||||
if rgx.compile(pattern) == OK:
|
||||
return rgx
|
||||
|
||||
printerr('[Popochiu] exception regex error')
|
|
@ -0,0 +1 @@
|
|||
uid://d1vhl7uqwadfx
|
|
@ -0,0 +1,94 @@
|
|||
@tool
|
||||
extends HBoxContainer
|
||||
|
||||
signal tag_state_changed
|
||||
|
||||
const RESULT_CODE = preload("res://addons/popochiu/editor/config/result_codes.gd")
|
||||
|
||||
var _anim_tag_state: Dictionary = {}
|
||||
|
||||
@onready var tag_name_label = $HBoxContainer/TagName
|
||||
@onready var import_toggle = $Panel/HBoxContainer/Import
|
||||
@onready var loops_toggle = $Panel/HBoxContainer/Loops
|
||||
@onready var separator = $Panel/HBoxContainer/Separator
|
||||
@onready var visible_toggle = $Panel/HBoxContainer/Visible
|
||||
@onready var clickable_toggle = $Panel/HBoxContainer/Clickable
|
||||
|
||||
|
||||
#region Godot ######################################################################################
|
||||
func _ready():
|
||||
# Common toggle icons
|
||||
import_toggle.icon = get_theme_icon('Load', 'EditorIcons')
|
||||
loops_toggle.icon = get_theme_icon('Loop', 'EditorIcons')
|
||||
# Room-related toggle icons
|
||||
visible_toggle.icon = get_theme_icon('GuiVisibilityVisible', 'EditorIcons')
|
||||
clickable_toggle.icon = get_theme_icon('ToolSelect', 'EditorIcons')
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public #####################################################################################
|
||||
func init(tag_cfg: Dictionary):
|
||||
if tag_cfg.tag_name == null or tag_cfg.tag_name == "":
|
||||
printerr(RESULT_CODE.get_error_message(RESULT_CODE.ERR_UNNAMED_TAG_DETECTED))
|
||||
return false
|
||||
|
||||
_anim_tag_state = _load_default_tag_state()
|
||||
_anim_tag_state.merge(tag_cfg, true)
|
||||
_setup_scene()
|
||||
|
||||
func show_prop_buttons():
|
||||
separator.visible = true
|
||||
visible_toggle.visible = true
|
||||
clickable_toggle.visible = true
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region SetGet #####################################################################################
|
||||
func get_cfg() -> Dictionary:
|
||||
return _anim_tag_state
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private ####################################################################################
|
||||
func _setup_scene():
|
||||
import_toggle.button_pressed = _anim_tag_state.import
|
||||
loops_toggle.button_pressed = _anim_tag_state.loops
|
||||
tag_name_label.text = _anim_tag_state.tag_name
|
||||
visible_toggle.button_pressed = _anim_tag_state.prop_visible
|
||||
clickable_toggle.button_pressed = _anim_tag_state.prop_clickable
|
||||
emit_signal("tag_state_changed")
|
||||
|
||||
|
||||
func _load_default_tag_state() -> Dictionary:
|
||||
return {
|
||||
"tag_name": "",
|
||||
"import": PopochiuConfig.is_default_animation_import_enabled(),
|
||||
"loops": PopochiuConfig.is_default_animation_loop_enabled(),
|
||||
"prop_visible": PopochiuConfig.is_default_animation_prop_visible(),
|
||||
"prop_clickable": PopochiuConfig.is_default_animation_prop_clickable(),
|
||||
}
|
||||
|
||||
|
||||
func _on_import_toggled(button_pressed):
|
||||
_anim_tag_state.import = button_pressed
|
||||
emit_signal("tag_state_changed")
|
||||
|
||||
|
||||
func _on_loops_toggled(button_pressed):
|
||||
_anim_tag_state.loops = button_pressed
|
||||
emit_signal("tag_state_changed")
|
||||
|
||||
|
||||
func _on_visible_toggled(button_pressed):
|
||||
_anim_tag_state.prop_visible = button_pressed
|
||||
emit_signal("tag_state_changed")
|
||||
|
||||
func _on_clickable_toggled(button_pressed):
|
||||
_anim_tag_state.prop_clickable = button_pressed
|
||||
emit_signal("tag_state_changed")
|
||||
|
||||
|
||||
#endregion
|
|
@ -0,0 +1 @@
|
|||
uid://krf8u35pkjn3
|
|
@ -0,0 +1,94 @@
|
|||
[gd_scene load_steps=6 format=3 uid="uid://rphyltbm12m4"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/popochiu/editor/importers/aseprite/docks/animation_tag_row.gd" id="1"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_77wem"]
|
||||
|
||||
[sub_resource type="Image" id="Image_vdhps"]
|
||||
data = {
|
||||
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
|
||||
"format": "RGBA8",
|
||||
"height": 16,
|
||||
"mipmaps": false,
|
||||
"width": 16
|
||||
}
|
||||
|
||||
[sub_resource type="ImageTexture" id="ImageTexture_c80ss"]
|
||||
image = SubResource("Image_vdhps")
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sd1l8"]
|
||||
|
||||
[node name="AnimationTagRow" type="HBoxContainer"]
|
||||
offset_right = 320.0
|
||||
offset_bottom = 20.0
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TagName" type="Label" parent="HBoxContainer"]
|
||||
layout_mode = 2
|
||||
text = "Tag Name"
|
||||
|
||||
[node name="Panel" type="Panel" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_styles/panel = SubResource("StyleBoxEmpty_77wem")
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="Panel"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 1
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
offset_left = -60.0
|
||||
offset_bottom = 20.0
|
||||
grow_horizontal = 0
|
||||
|
||||
[node name="Visible" type="Button" parent="Panel/HBoxContainer"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 3
|
||||
tooltip_text = "This prop will be visible"
|
||||
toggle_mode = true
|
||||
icon = SubResource("ImageTexture_c80ss")
|
||||
flat = true
|
||||
|
||||
[node name="Clickable" type="Button" parent="Panel/HBoxContainer"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 3
|
||||
tooltip_text = "This prop will be clickable"
|
||||
toggle_mode = true
|
||||
icon = SubResource("ImageTexture_c80ss")
|
||||
flat = true
|
||||
|
||||
[node name="Separator" type="Panel" parent="Panel/HBoxContainer"]
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(1, 0)
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_sd1l8")
|
||||
|
||||
[node name="Import" type="Button" parent="Panel/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 3
|
||||
tooltip_text = "Import this animation"
|
||||
toggle_mode = true
|
||||
icon = SubResource("ImageTexture_c80ss")
|
||||
flat = true
|
||||
|
||||
[node name="Loops" type="Button" parent="Panel/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 3
|
||||
tooltip_text = "Set animation as looping"
|
||||
toggle_mode = true
|
||||
icon = SubResource("ImageTexture_c80ss")
|
||||
flat = true
|
||||
|
||||
[connection signal="toggled" from="Panel/HBoxContainer/Visible" to="." method="_on_visible_toggled"]
|
||||
[connection signal="toggled" from="Panel/HBoxContainer/Clickable" to="." method="_on_clickable_toggled"]
|
||||
[connection signal="toggled" from="Panel/HBoxContainer/Import" to="." method="_on_import_toggled"]
|
||||
[connection signal="toggled" from="Panel/HBoxContainer/Loops" to="." method="_on_loops_toggled"]
|
|
@ -0,0 +1,429 @@
|
|||
@tool
|
||||
extends PanelContainer
|
||||
|
||||
# TODO: review coding standards for those constants
|
||||
const RESULT_CODE = preload("res://addons/popochiu/editor/config/result_codes.gd")
|
||||
const LOCAL_OBJ_CONFIG = preload("res://addons/popochiu/editor/config/local_obj_config.gd")
|
||||
# TODO: this can be specialized, even if for a two buttons... ?
|
||||
const AnimationTagRow =\
|
||||
preload("res://addons/popochiu/editor/importers/aseprite/docks/animation_tag_row.gd")
|
||||
|
||||
var scene: Node
|
||||
var target_node: Node
|
||||
var file_system: EditorFileSystem
|
||||
|
||||
# ---- External logic
|
||||
var _animation_tag_row_scene: PackedScene =\
|
||||
preload("res://addons/popochiu/editor/importers/aseprite/docks/animation_tag_row.tscn")
|
||||
var _aseprite = preload("../aseprite_controller.gd").new() ## TODO: should be absolute?
|
||||
# ---- References for children scripts
|
||||
var _root_node: Node
|
||||
var _options: Dictionary
|
||||
# ---- Importer parameters variables
|
||||
var _source: String = ""
|
||||
var _tags_cache: Array = []
|
||||
var _file_dialog_aseprite: FileDialog
|
||||
var _output_folder_dialog: FileDialog
|
||||
var _importing := false
|
||||
var _output_folder := ""
|
||||
var _out_folder_default := "[Same as scene]"
|
||||
|
||||
|
||||
#region Godot ######################################################################################
|
||||
func _ready():
|
||||
_set_elements_styles()
|
||||
|
||||
if not PopochiuEditorConfig.aseprite_importer_enabled():
|
||||
_show_info()
|
||||
return
|
||||
|
||||
# Check access to Aseprite executable
|
||||
var result = _check_aseprite()
|
||||
if result == RESULT_CODE.SUCCESS:
|
||||
_show_importer()
|
||||
else:
|
||||
PopochiuUtils.print_error(RESULT_CODE.get_error_message(result))
|
||||
_show_warning()
|
||||
|
||||
# Load inspector dock configuration from node
|
||||
var cfg = LOCAL_OBJ_CONFIG.load_config(target_node)
|
||||
if cfg == null:
|
||||
_load_default_config()
|
||||
_set_options_visible(true)
|
||||
else:
|
||||
_load_config(cfg)
|
||||
_set_tags_visible(cfg.get("tags_exp"))
|
||||
_set_options_visible(cfg.get("op_exp"))
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private ####################################################################################
|
||||
func _check_aseprite() -> int:
|
||||
if not _aseprite.check_command_path():
|
||||
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FULL_PATH
|
||||
|
||||
if not _aseprite.test_command():
|
||||
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FOUND
|
||||
|
||||
return RESULT_CODE.SUCCESS
|
||||
|
||||
|
||||
func _list_tags(file: String):
|
||||
if not _aseprite.check_command_path():
|
||||
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FULL_PATH
|
||||
if not _aseprite.test_command():
|
||||
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FOUND
|
||||
return _aseprite.list_tags(file)
|
||||
|
||||
|
||||
## TODO: Currently unused. keeping this as reference
|
||||
## to populate a checkable list of layers
|
||||
func _list_layers(file: String, only_visibles = false):
|
||||
if not _aseprite.check_command_path():
|
||||
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FULL_PATH
|
||||
if not _aseprite.test_command():
|
||||
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FOUND
|
||||
return _aseprite.list_layers(file, only_visibles)
|
||||
|
||||
|
||||
func _load_config(cfg):
|
||||
if cfg.has("source"):
|
||||
_set_source(cfg.source)
|
||||
|
||||
_output_folder = cfg.get("o_folder", "")
|
||||
get_node("%OutFolderButton").text = (
|
||||
_output_folder if _output_folder != "" else _out_folder_default
|
||||
)
|
||||
get_node("%OutFileName").text = cfg.get("o_name", "")
|
||||
get_node("%VisibleLayersCheckButton").set_pressed_no_signal(
|
||||
cfg.get("only_visible_layers", false)
|
||||
)
|
||||
get_node("%WipeOldAnimationsCheckButton").set_pressed_no_signal(
|
||||
cfg.get("wipe_old_anims", false)
|
||||
)
|
||||
|
||||
_set_tags_visible(cfg.get("tags_exp", false))
|
||||
_set_options_visible(cfg.get("op_exp", false))
|
||||
_populate_tags(cfg.get("tags", []))
|
||||
|
||||
|
||||
func _save_config():
|
||||
_update_tags_cache()
|
||||
|
||||
var cfg := {
|
||||
"source": _source,
|
||||
"tags": _tags_cache,
|
||||
"tags_exp": get_node("%Tags").visible,
|
||||
"op_exp": get_node("%Options").visible,
|
||||
"o_folder": _output_folder,
|
||||
"o_name": get_node("%OutFileName").text,
|
||||
"only_visible_layers": get_node("%VisibleLayersCheckButton").is_pressed(),
|
||||
"wipe_old_anims": get_node("%WipeOldAnimationsCheckButton").is_pressed(),
|
||||
}
|
||||
|
||||
LOCAL_OBJ_CONFIG.save_config(target_node, cfg)
|
||||
|
||||
|
||||
func _load_default_config():
|
||||
# Reset variables
|
||||
_source = ""
|
||||
_tags_cache = []
|
||||
_output_folder = ""
|
||||
|
||||
# Empty tags list
|
||||
_empty_tags_container()
|
||||
|
||||
# Reset inspector fields
|
||||
get_node("%SourceButton").text = "[empty]"
|
||||
get_node("%SourceButton").tooltip_text = ""
|
||||
get_node("%OutFolderButton").text = "[empty]"
|
||||
get_node("%OutFileName").clear()
|
||||
get_node("%VisibleLayersCheckButton").set_pressed_no_signal(false)
|
||||
get_node("%WipeOldAnimationsCheckButton").set_pressed_no_signal(
|
||||
PopochiuConfig.is_default_wipe_old_anims_enabled()
|
||||
)
|
||||
|
||||
|
||||
func _set_source(source):
|
||||
_source = source
|
||||
get_node("%SourceButton").text = _source
|
||||
get_node("%SourceButton").tooltip_text = _source
|
||||
|
||||
|
||||
func _on_source_pressed():
|
||||
_open_source_dialog()
|
||||
|
||||
|
||||
func _on_aseprite_file_selected(path):
|
||||
_set_source(ProjectSettings.localize_path(path))
|
||||
_populate_tags(_get_tags_from_source())
|
||||
_save_config()
|
||||
_file_dialog_aseprite.queue_free()
|
||||
|
||||
|
||||
func _on_rescan_pressed():
|
||||
_populate_tags(\
|
||||
_merge_with_cache(_get_tags_from_source())\
|
||||
)
|
||||
_save_config()
|
||||
|
||||
|
||||
func _on_import_pressed():
|
||||
if _importing:
|
||||
return
|
||||
|
||||
_importing = true
|
||||
_root_node = get_tree().get_edited_scene_root()
|
||||
|
||||
if _source == "":
|
||||
_show_message("Aseprite file not selected")
|
||||
_importing = false
|
||||
return
|
||||
|
||||
_options = {
|
||||
"source": ProjectSettings.globalize_path(_source),
|
||||
"tags": _tags_cache,
|
||||
"output_folder": (
|
||||
_output_folder if _output_folder != "" else _root_node.scene_file_path.get_base_dir()
|
||||
),
|
||||
"output_filename": get_node("%OutFileName").text,
|
||||
"only_visible_layers": get_node("%VisibleLayersCheckButton").is_pressed(),
|
||||
"wipe_old_animations": get_node("%WipeOldAnimationsCheckButton").is_pressed(),
|
||||
}
|
||||
|
||||
_save_config()
|
||||
|
||||
|
||||
func _on_reset_pressed():
|
||||
var _confirmation_dialog = _show_confirmation(\
|
||||
"This will reset the importer preferences." + \
|
||||
"This cannot be undone! Are you sure?", "Confirmation required!")
|
||||
_confirmation_dialog.get_ok_button().connect("pressed", Callable(self, "_reset_prefs_metadata"))
|
||||
|
||||
|
||||
func _reset_prefs_metadata():
|
||||
if target_node.has_meta(LOCAL_OBJ_CONFIG.LOCAL_OBJ_CONFIG_META_NAME):
|
||||
target_node.remove_meta(LOCAL_OBJ_CONFIG.LOCAL_OBJ_CONFIG_META_NAME)
|
||||
_load_default_config()
|
||||
notify_property_list_changed()
|
||||
|
||||
|
||||
func _open_source_dialog():
|
||||
_file_dialog_aseprite = _create_aseprite_file_selection()
|
||||
get_parent().add_child(_file_dialog_aseprite)
|
||||
if _source != "":
|
||||
_file_dialog_aseprite.set_current_dir(
|
||||
ProjectSettings.globalize_path(
|
||||
_source.get_base_dir()
|
||||
)
|
||||
)
|
||||
_file_dialog_aseprite.popup_centered_ratio()
|
||||
|
||||
|
||||
func _create_aseprite_file_selection():
|
||||
var file_dialog = FileDialog.new()
|
||||
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||
file_dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
file_dialog.title = "Select Aseprite animation file"
|
||||
file_dialog.connect("file_selected", Callable(self, "_on_aseprite_file_selected"))
|
||||
file_dialog.set_filters(PackedStringArray(["*.ase","*.aseprite"]))
|
||||
return file_dialog
|
||||
|
||||
|
||||
func _populate_tags(tags: Array):
|
||||
## reset tags container
|
||||
_empty_tags_container()
|
||||
|
||||
# Add each tag found
|
||||
for t in tags:
|
||||
if t.tag_name == "":
|
||||
continue
|
||||
|
||||
var tag_row: AnimationTagRow = _animation_tag_row_scene.instantiate()
|
||||
get_node("%Tags").add_child(tag_row)
|
||||
tag_row.init(t)
|
||||
tag_row.connect("tag_state_changed", Callable(self, "_save_config"))
|
||||
_customize_tag_ui(tag_row)
|
||||
# Invoke customization hook implementable in child classes
|
||||
_update_tags_cache()
|
||||
|
||||
|
||||
func _customize_tag_ui(tagrow: AnimationTagRow):
|
||||
## This can be implemented by child classes if necessary
|
||||
pass
|
||||
|
||||
|
||||
func _empty_tags_container():
|
||||
# Clean the inspector tags container empty
|
||||
for tl in get_node("%Tags").get_children():
|
||||
get_node("%Tags").remove_child(tl)
|
||||
tl.queue_free()
|
||||
|
||||
|
||||
func _update_tags_cache():
|
||||
_tags_cache = _get_tags_from_ui()
|
||||
|
||||
|
||||
func _merge_with_cache(tags: Array) -> Array:
|
||||
var tags_cache_index = {}
|
||||
var result = []
|
||||
for t in _tags_cache:
|
||||
tags_cache_index[t.tag_name] = t
|
||||
|
||||
for i in tags.size():
|
||||
result.push_back(
|
||||
tags_cache_index[tags[i].tag_name]
|
||||
if tags_cache_index.has(tags[i].tag_name)
|
||||
else tags[i]
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
func _get_tags_from_ui() -> Array:
|
||||
var tags_list = []
|
||||
for tag_row in get_node("%Tags").get_children():
|
||||
var tag_row_cfg = tag_row.get_cfg()
|
||||
if tag_row_cfg.tag_name == "":
|
||||
continue
|
||||
tags_list.push_back(tag_row_cfg)
|
||||
return tags_list
|
||||
|
||||
|
||||
func _get_tags_from_source() -> Array:
|
||||
var tags_found = _list_tags(ProjectSettings.globalize_path(_source))
|
||||
if typeof(tags_found) == TYPE_INT:
|
||||
PopochiuUtils.print_error(RESULT_CODE.get_error_message(tags_found))
|
||||
return []
|
||||
var tags_list = []
|
||||
for t in tags_found:
|
||||
if t == "":
|
||||
continue
|
||||
tags_list.push_back({
|
||||
tag_name = t
|
||||
})
|
||||
return tags_list
|
||||
|
||||
|
||||
func _show_message(
|
||||
message: String, title: String = "", object: Object = null, method := ""
|
||||
):
|
||||
var warning_dialog = AcceptDialog.new()
|
||||
|
||||
if title != "":
|
||||
warning_dialog.title = title
|
||||
|
||||
warning_dialog.dialog_text = message
|
||||
warning_dialog.popup_window = true
|
||||
|
||||
var callback := Callable(warning_dialog, "queue_free")
|
||||
|
||||
if is_instance_valid(object) and not method.is_empty():
|
||||
callback = func():
|
||||
object.call(method)
|
||||
|
||||
warning_dialog.confirmed.connect(callback)
|
||||
warning_dialog.close_requested.connect(callback)
|
||||
|
||||
PopochiuEditorHelper.show_dialog(warning_dialog)
|
||||
|
||||
|
||||
func _show_confirmation(message: String, title: String = ""):
|
||||
var _confirmation_dialog = ConfirmationDialog.new()
|
||||
get_parent().add_child(_confirmation_dialog)
|
||||
if title != "":
|
||||
_confirmation_dialog.title = title
|
||||
_confirmation_dialog.dialog_text = message
|
||||
_confirmation_dialog.popup_centered()
|
||||
_confirmation_dialog.connect("close_requested", Callable(_confirmation_dialog, "queue_free"))
|
||||
return _confirmation_dialog
|
||||
|
||||
|
||||
func _on_options_title_toggled(button_pressed):
|
||||
_set_options_visible(button_pressed)
|
||||
_save_config()
|
||||
|
||||
|
||||
func _set_options_visible(is_visible):
|
||||
get_node("%Options").visible = is_visible
|
||||
get_node("%OptionsTitle").icon = (
|
||||
PopochiuEditorConfig.get_icon(PopochiuEditorConfig.Icons.EXPANDED) if is_visible
|
||||
else PopochiuEditorConfig.get_icon(PopochiuEditorConfig.Icons.COLLAPSED)
|
||||
)
|
||||
|
||||
func _on_tags_title_toggled(button_pressed: bool) -> void:
|
||||
_set_tags_visible(button_pressed)
|
||||
_save_config()
|
||||
|
||||
|
||||
func _set_tags_visible(is_visible: bool) -> void:
|
||||
get_node("%Tags").visible = is_visible
|
||||
get_node("%TagsTitle").icon = (
|
||||
PopochiuEditorConfig.get_icon(PopochiuEditorConfig.Icons.EXPANDED) if is_visible
|
||||
else PopochiuEditorConfig.get_icon(PopochiuEditorConfig.Icons.COLLAPSED)
|
||||
)
|
||||
|
||||
func _on_out_folder_pressed():
|
||||
_output_folder_dialog = _create_output_folder_selection()
|
||||
get_parent().add_child(_output_folder_dialog)
|
||||
if _output_folder != _out_folder_default:
|
||||
_output_folder_dialog.current_dir = _output_folder
|
||||
_output_folder_dialog.popup_centered_ratio()
|
||||
|
||||
|
||||
func _create_output_folder_selection():
|
||||
var file_dialog = FileDialog.new()
|
||||
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
|
||||
file_dialog.access = FileDialog.ACCESS_RESOURCES
|
||||
file_dialog.title = "Select destination folder"
|
||||
file_dialog.connect("dir_selected", Callable(self, "_on_output_folder_selected"))
|
||||
return file_dialog
|
||||
|
||||
|
||||
func _on_output_folder_selected(path):
|
||||
_output_folder = path
|
||||
get_node("%OutFolderButton").text = (
|
||||
_output_folder if _output_folder != "" else _out_folder_default
|
||||
)
|
||||
_output_folder_dialog.queue_free()
|
||||
_save_config()
|
||||
|
||||
|
||||
func _set_elements_styles():
|
||||
# Set sections title colors according to current theme
|
||||
var section_color = get_theme_color("prop_section", "Editor")
|
||||
var section_style = StyleBoxFlat.new()
|
||||
section_style.set_bg_color(section_color)
|
||||
get_node("%TagsTitleBar").set("theme_override_styles/panel", section_style)
|
||||
get_node("%OptionsTitleBar").set("theme_override_styles/panel", section_style)
|
||||
|
||||
# Set style of warning panel
|
||||
get_node("%WarningPanel").add_theme_stylebox_override(
|
||||
"panel",
|
||||
get_node("%WarningPanel").get_theme_stylebox("sub_inspector_bg11", "Editor")
|
||||
)
|
||||
get_node("%WarningLabel").add_theme_color_override("font_color", Color("c46c71"))
|
||||
|
||||
|
||||
func _show_info():
|
||||
get_node("%Info").visible = true
|
||||
get_node("%Warning").visible = false
|
||||
get_node("%Importer").visible = false
|
||||
|
||||
|
||||
func _show_warning():
|
||||
get_node("%Info").visible = false
|
||||
get_node("%Warning").visible = true
|
||||
get_node("%Importer").visible = false
|
||||
|
||||
|
||||
func _show_importer():
|
||||
get_node("%Info").visible = false
|
||||
get_node("%Warning").visible = false
|
||||
get_node("%Importer").visible = true
|
||||
|
||||
# TODO: Introduce layer selection list, more or less as tags
|
||||
|
||||
|
||||
#endregion
|
|
@ -0,0 +1 @@
|
|||
uid://c5o55inhq2abl
|
|
@ -0,0 +1,225 @@
|
|||
[gd_scene load_steps=4 format=3 uid="uid://bcanby6n3eahm"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="1"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wwoxk"]
|
||||
bg_color = Color(0, 0, 0, 1)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ctsm1"]
|
||||
content_margin_left = 4.0
|
||||
content_margin_top = 4.0
|
||||
content_margin_right = 4.0
|
||||
content_margin_bottom = 4.0
|
||||
bg_color = Color(1, 0.364706, 0.364706, 1)
|
||||
draw_center = false
|
||||
corner_detail = 1
|
||||
|
||||
[node name="AsepriteImporterInspectorDock" type="PanelContainer"]
|
||||
offset_right = 14.0
|
||||
offset_bottom = 14.0
|
||||
theme_override_styles/panel = SubResource("1")
|
||||
|
||||
[node name="Margin" type="MarginContainer" parent="."]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_top = 2
|
||||
theme_override_constants/margin_bottom = 2
|
||||
|
||||
[node name="Importer" type="VBoxContainer" parent="Margin"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Source" type="HBoxContainer" parent="Margin/Importer"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "Location of the Aseprite (*.ase, *.aseprite) source file."
|
||||
|
||||
[node name="Label" type="Label" parent="Margin/Importer/Source"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 2.0
|
||||
text = "Aseprite File"
|
||||
|
||||
[node name="SourceButton" type="Button" parent="Margin/Importer/Source"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 2.0
|
||||
text = "[empty]"
|
||||
clip_text = true
|
||||
|
||||
[node name="RescanButton" type="Button" parent="Margin/Importer/Source"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Rescan"
|
||||
|
||||
[node name="TagsTitleBar" type="PanelContainer" parent="Margin/Importer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_wwoxk")
|
||||
|
||||
[node name="TagsTitle" type="Button" parent="Margin/Importer/TagsTitleBar"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_pressed_color = Color(0.8, 0.807843, 0.827451, 1)
|
||||
toggle_mode = true
|
||||
text = "Animation tags"
|
||||
|
||||
[node name="Tags" type="VBoxContainer" parent="Margin/Importer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="OptionsTitleBar" type="PanelContainer" parent="Margin/Importer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_wwoxk")
|
||||
|
||||
[node name="OptionsTitle" type="Button" parent="Margin/Importer/OptionsTitleBar"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_pressed_color = Color(0.8, 0.807843, 0.827451, 1)
|
||||
toggle_mode = true
|
||||
text = "Options"
|
||||
|
||||
[node name="Options" type="VBoxContainer" parent="Margin/Importer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="OutFolder" type="HBoxContainer" parent="Margin/Importer/Options"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "Location where the spritesheet file should be saved."
|
||||
|
||||
[node name="Label" type="Label" parent="Margin/Importer/Options/OutFolder"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 2.0
|
||||
text = "Output folder"
|
||||
|
||||
[node name="OutFolderButton" type="Button" parent="Margin/Importer/Options/OutFolder"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 2.0
|
||||
text = "[empty]"
|
||||
clip_text = true
|
||||
|
||||
[node name="OutFile" type="HBoxContainer" parent="Margin/Importer/Options"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "Base filename for spritesheet. In case the layer option is used, this works as a prefix to the layer name."
|
||||
|
||||
[node name="Label" type="Label" parent="Margin/Importer/Options/OutFile"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 2.0
|
||||
text = "Output file name"
|
||||
|
||||
[node name="OutFileName" type="LineEdit" parent="Margin/Importer/Options/OutFile"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 2.0
|
||||
|
||||
[node name="VisibleLayers" type="HBoxContainer" parent="Margin/Importer/Options"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "If active, layers not visible in the source file won't be included in the final image."
|
||||
|
||||
[node name="Label" type="Label" parent="Margin/Importer/Options/VisibleLayers"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 2.0
|
||||
text = "Only visible layers"
|
||||
|
||||
[node name="VisibleLayersCheckButton" type="CheckButton" parent="Margin/Importer/Options/VisibleLayers"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 2.0
|
||||
|
||||
[node name="WipeOldAnimations" type="HBoxContainer" parent="Margin/Importer/Options"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "If active, layers not visible in the source file won't be included in the final image."
|
||||
|
||||
[node name="Label" type="Label" parent="Margin/Importer/Options/WipeOldAnimations"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 2.0
|
||||
tooltip_text = "Set this to OFF if you want to add new animations on top of old ones. Anims with same name will be updated."
|
||||
mouse_filter = 0
|
||||
text = "Wipe old animations"
|
||||
|
||||
[node name="WipeOldAnimationsCheckButton" type="CheckButton" parent="Margin/Importer/Options/WipeOldAnimations"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 2.0
|
||||
|
||||
[node name="Import" type="Button" parent="Margin/Importer"]
|
||||
layout_mode = 2
|
||||
text = "Import"
|
||||
|
||||
[node name="Reset" type="Button" parent="Margin/Importer"]
|
||||
layout_mode = 2
|
||||
text = "Reset Preferences"
|
||||
|
||||
[node name="Warning" type="VBoxContainer" parent="Margin"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="Margin/Warning"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="WarningPanel" type="Panel" parent="Margin/Warning/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(222, 50)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_ctsm1")
|
||||
|
||||
[node name="WarningLabel" type="Label" parent="Margin/Warning/HBoxContainer/WarningPanel"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 42)
|
||||
layout_mode = 0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 6
|
||||
theme_override_colors/font_color = Color(0.768627, 0.423529, 0.443137, 1)
|
||||
text = "Error loading Aseprite Importer!
|
||||
Check Output panel for details."
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Info" type="VBoxContainer" parent="Margin"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="Margin/Info"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="InfoPanel" type="Panel" parent="Margin/Info/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(222, 50)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_ctsm1")
|
||||
|
||||
[node name="InfoLabel" type="Label" parent="Margin/Info/HBoxContainer/InfoPanel"]
|
||||
custom_minimum_size = Vector2(0, 42)
|
||||
layout_mode = 0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 6
|
||||
text = "Aseprite Importer disabled.
|
||||
Can be enabled in Editor Settings."
|
||||
|
||||
[connection signal="pressed" from="Margin/Importer/Source/SourceButton" to="." method="_on_source_pressed"]
|
||||
[connection signal="pressed" from="Margin/Importer/Source/RescanButton" to="." method="_on_rescan_pressed"]
|
||||
[connection signal="toggled" from="Margin/Importer/TagsTitleBar/TagsTitle" to="." method="_on_tags_title_toggled"]
|
||||
[connection signal="toggled" from="Margin/Importer/OptionsTitleBar/OptionsTitle" to="." method="_on_options_title_toggled"]
|
||||
[connection signal="pressed" from="Margin/Importer/Options/OutFolder/OutFolderButton" to="." method="_on_out_folder_pressed"]
|
||||
[connection signal="focus_exited" from="Margin/Importer/Options/OutFile/OutFileName" to="." method="_save_config"]
|
||||
[connection signal="pressed" from="Margin/Importer/Options/VisibleLayers/VisibleLayersCheckButton" to="." method="_save_config"]
|
||||
[connection signal="pressed" from="Margin/Importer/Options/WipeOldAnimations/WipeOldAnimationsCheckButton" to="." method="_save_config"]
|
||||
[connection signal="pressed" from="Margin/Importer/Import" to="." method="_on_import_pressed"]
|
||||
[connection signal="pressed" from="Margin/Importer/Reset" to="." method="_on_reset_pressed"]
|
|
@ -0,0 +1,55 @@
|
|||
@tool
|
||||
extends "res://addons/popochiu/editor/importers/aseprite/docks/aseprite_importer_inspector_dock.gd"
|
||||
|
||||
var _animation_player_path: String
|
||||
var _animation_creator = preload(
|
||||
"res://addons/popochiu/editor/importers/aseprite/animation_creator.gd"
|
||||
).new()
|
||||
|
||||
#region Godot ######################################################################################
|
||||
func _ready():
|
||||
if not target_node.has_node("AnimationPlayer"):
|
||||
PopochiuUtils.print_error(
|
||||
RESULT_CODE.get_error_message(RESULT_CODE.ERR_NO_ANIMATION_PLAYER_FOUND)
|
||||
)
|
||||
return
|
||||
|
||||
_animation_player_path = target_node.get_node("AnimationPlayer").get_path()
|
||||
|
||||
# Instantiate animation creator
|
||||
_animation_creator.init(_aseprite, file_system)
|
||||
|
||||
super()
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private ####################################################################################
|
||||
func _on_import_pressed():
|
||||
# Set everything up
|
||||
# This will populate _root_node and _options class variables
|
||||
super()
|
||||
|
||||
if _animation_player_path == "" or not _root_node.has_node(_animation_player_path):
|
||||
_show_message("AnimationPlayer not found")
|
||||
_importing = false
|
||||
return
|
||||
|
||||
var result = await _animation_creator.create_character_animations(
|
||||
target_node, _root_node.get_node(_animation_player_path), _options
|
||||
)
|
||||
_importing = false
|
||||
|
||||
if typeof(result) == TYPE_INT and result != RESULT_CODE.SUCCESS:
|
||||
PopochiuUtils.print_error(RESULT_CODE.get_error_message(result))
|
||||
_show_message("Some errors occurred. Please check output panel.", "Warning!")
|
||||
else:
|
||||
_show_message("%d animation tags processed." % [_tags_cache.size()], "Done!")
|
||||
|
||||
|
||||
func _customize_tag_ui(tag_row: AnimationTagRow):
|
||||
# Nothing special has to be done for Character tags
|
||||
pass
|
||||
|
||||
|
||||
#endregion
|
|
@ -0,0 +1 @@
|
|||
uid://cgc5psgxjoc17
|
|
@ -0,0 +1,117 @@
|
|||
@tool
|
||||
extends "res://addons/popochiu/editor/importers/aseprite/docks/aseprite_importer_inspector_dock.gd"
|
||||
|
||||
var _animation_creator = preload(\
|
||||
"res://addons/popochiu/editor/importers/aseprite/animation_creator.gd").new()
|
||||
|
||||
|
||||
#region Godot ######################################################################################
|
||||
func _ready():
|
||||
# Instantiate animation creator
|
||||
_animation_creator.init(_aseprite, file_system)
|
||||
|
||||
super()
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private ####################################################################################
|
||||
func _on_import_pressed():
|
||||
# Set everything up
|
||||
# This will populate _root_node and _options class variables
|
||||
super()
|
||||
|
||||
var props_container = _root_node.get_node("Props")
|
||||
var result: int = RESULT_CODE.SUCCESS
|
||||
|
||||
# Create a prop for each tag that must be imported
|
||||
# and populate it with the right sprite
|
||||
for tag in _options.get("tags"):
|
||||
# Ignore unwanted tags
|
||||
if not tag.import: continue
|
||||
|
||||
# Always convert to PascalCase as a standard
|
||||
# TODO: check Godot 4 standards, I can't find info
|
||||
var prop_name: String = tag.tag_name.to_pascal_case()
|
||||
|
||||
# In case the prop is there, use the one we already have
|
||||
var prop = props_container.get_node_or_null(prop_name)
|
||||
if prop == null:
|
||||
# Create a new prop if necessary, specifying the
|
||||
# interaction flags.
|
||||
prop = _create_prop(prop_name, tag.prop_clickable, tag.prop_visible)
|
||||
else:
|
||||
# Force flags (a bit redundant but they may have been changed
|
||||
# in the Importer interface, for already imported props)
|
||||
prop.clickable = tag.prop_clickable
|
||||
prop.visible = tag.prop_visible
|
||||
|
||||
prop.set_meta("ANIM_NAME", tag.tag_name)
|
||||
|
||||
for prop in props_container.get_children():
|
||||
if not prop.has_meta("ANIM_NAME"): continue
|
||||
# TODO: check if animation player exists in prop, if not add it
|
||||
# same for Sprite2D even if it should be there...
|
||||
|
||||
# Make the output folder match the prop's folder
|
||||
_options.output_folder = prop.scene_file_path.get_base_dir()
|
||||
|
||||
# Import a single tag animation
|
||||
result = await _animation_creator.create_prop_animations(
|
||||
prop,
|
||||
prop.get_meta("ANIM_NAME"),
|
||||
_options
|
||||
)
|
||||
|
||||
for prop in props_container.get_children():
|
||||
if not prop.has_meta("ANIM_NAME"): continue
|
||||
# Save the prop
|
||||
result = await _save_prop(prop)
|
||||
|
||||
# TODO: maybe check if this is better done with signals
|
||||
_importing = false
|
||||
|
||||
if typeof(result) == TYPE_INT and result != RESULT_CODE.SUCCESS:
|
||||
PopochiuUtils.print_error(RESULT_CODE.get_error_message(result))
|
||||
_show_message("Some errors occurred. Please check output panel.", "Warning!")
|
||||
else:
|
||||
await get_tree().create_timer(0.1).timeout
|
||||
|
||||
# Once the popup is closed, call _clean_props()
|
||||
_show_message(
|
||||
"%d animation tags processed." % [_tags_cache.size()],
|
||||
"Done!"
|
||||
)
|
||||
|
||||
|
||||
func _customize_tag_ui(tag_row: AnimationTagRow):
|
||||
# Show props-related buttons if we are in a room
|
||||
tag_row.show_prop_buttons()
|
||||
|
||||
|
||||
func _create_prop(name: String, is_clickable: bool = true, is_visible: bool = true):
|
||||
var factory = PopochiuPropFactory.new()
|
||||
var param := PopochiuPropFactory.PopochiuPropFactoryParam.new()
|
||||
param.obj_name = name
|
||||
param.room = _root_node
|
||||
param.is_interactive = is_clickable
|
||||
param.is_visible = is_visible
|
||||
|
||||
if factory.create(param) != ResultCodes.SUCCESS:
|
||||
return
|
||||
|
||||
return factory.get_obj_scene()
|
||||
|
||||
func _save_prop(prop: PopochiuProp):
|
||||
var packed_scene: PackedScene = PackedScene.new()
|
||||
packed_scene.pack(prop)
|
||||
if ResourceSaver.save(packed_scene, prop.scene_file_path) != OK:
|
||||
PopochiuUtils.print_error(
|
||||
"Couldn't save animations for prop %s at %s" %
|
||||
[prop.name, prop.scene_file_path]
|
||||
)
|
||||
return ResultCodes.ERR_CANT_SAVE_OBJ_SCENE
|
||||
return ResultCodes.SUCCESS
|
||||
|
||||
|
||||
#endregion
|
|
@ -0,0 +1 @@
|
|||
uid://cv4mdldfurgpc
|
Loading…
Add table
Add a link
Reference in a new issue