First commit 🎉

This commit is contained in:
Tony Bark 2025-07-17 01:49:18 -04:00
commit 43ea213f9b
728 changed files with 37080 additions and 0 deletions

View 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

View file

@ -0,0 +1 @@
uid://c44sonibms74d

View 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')

View file

@ -0,0 +1 @@
uid://d1vhl7uqwadfx

View file

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

View file

@ -0,0 +1 @@
uid://krf8u35pkjn3

View file

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

View file

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

View file

@ -0,0 +1 @@
uid://c5o55inhq2abl

View file

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

View file

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

View file

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