init commit

This commit is contained in:
2025-04-26 10:41:46 -05:00
commit 8a1166fc19
94 changed files with 4374 additions and 0 deletions

4
.editorconfig Normal file
View File

@@ -0,0 +1,4 @@
root = true
[*]
charset = utf-8

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Godot 4+ specific ignores
.godot/
/android/

View File

@@ -0,0 +1,39 @@
@tool
extends RefCounted
const _Result = preload("result.gd").Class
class AtlasMakingResult:
extends _Result
var atlas: Texture2D
func success(atlas: Texture2D) -> void:
super._success()
self.atlas = atlas
var __editor_file_system: EditorFileSystem
func _init(editor_file_system: EditorFileSystem) -> void:
__editor_file_system = editor_file_system
func make_atlas(
atlas_image: Image,
res_source_file_path: String,
editor_import_plugin: EditorImportPlugin,
) -> AtlasMakingResult:
var result: AtlasMakingResult = AtlasMakingResult.new()
var res_png_path: String = res_source_file_path + ".png"
if not (res_png_path.is_absolute_path() and res_png_path.begins_with("res://")):
result.fail(ERR_FILE_BAD_PATH, "Path to PNG-file is not valid: %s" % [res_png_path])
return result
var error: Error
error = atlas_image.save_png(res_png_path)
if error:
result.fail(error, "An error occured while saving atlas-image to png-file: %s" % [res_png_path])
return result
__editor_file_system.update_file(res_png_path)
error = editor_import_plugin.append_import_external_resource(res_png_path)
if error:
result.fail(error, "An error occured while appending import external resource (atlas texture)")
return result
result.success(ResourceLoader.load(res_png_path, "Texture2D", ResourceLoader.CACHE_MODE_IGNORE))
return result

View File

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

View File

@@ -0,0 +1,222 @@
@tool
extends EditorImportPlugin
const _Common = preload("common.gd")
const _Options = preload("options.gd")
const _Exporter = preload("export/_.gd")
const _Importer = preload("import/_.gd")
const _AtlasMaker = preload("atlas_maker.gd")
const _MiddleImportScript = preload("external_scripts/middle_import_script_base.gd")
const _PostImportScript = preload("external_scripts/post_import_script_base.gd")
const __empty_callable: Callable = Callable()
var __exporter: _Exporter
var __importer: _Importer
var __import_order: int = 0
var __importer_name: String
var __priority: float = 1
var __resource_type: StringName
var __save_extension: String
var __visible_name: String
var __options: Array[Dictionary]
var __options_visibility_checkers: Dictionary
var __atlas_maker: _AtlasMaker
var __editor_file_system: EditorFileSystem
func _init(exporter: _Exporter, importer: _Importer, atlas_maker: _AtlasMaker, editor_file_system: EditorFileSystem) -> void:
__importer = importer
__exporter = exporter
__atlas_maker = atlas_maker
__import_order = 1
__importer_name = "%s %s" % [exporter.get_name(), importer.get_name()]
__priority = 1
__resource_type = importer.get_resource_type()
__save_extension = importer.get_save_extension()
__visible_name = "%s -> %s" % [exporter.get_name(), importer.get_name()]
__editor_file_system = editor_file_system
var options: Array[Dictionary]
__options.append_array(importer.get_options())
__options.append(_Options.create_option(
_Options.MIDDLE_IMPORT_SCRIPT_PATH, "", PROPERTY_HINT_FILE, "*.gd", PROPERTY_USAGE_DEFAULT))
__options.append(_Options.create_option(
_Options.POST_IMPORT_SCRIPT_PATH, "", PROPERTY_HINT_FILE, "*.gd", PROPERTY_USAGE_DEFAULT))
__options.append_array(exporter.get_options())
for option in __options:
if option.has("get_is_visible"):
__options_visibility_checkers[option.name] = option.get_is_visible
func _import(
res_source_file_path: String,
res_save_file_path: String,
options: Dictionary,
platform_variants: Array[String],
gen_files: Array[String]
) -> Error:
var error: Error
var export_result: _Exporter.ExportResult = \
__exporter.export(res_source_file_path, options, self)
if export_result.error:
push_error("Export is failed. Errors chain:\n%s" % [export_result])
return export_result.error
var middle_import_script_context: _MiddleImportScript.Context = _MiddleImportScript.Context.new()
middle_import_script_context.atlas_image = export_result.atlas_image
middle_import_script_context.sprite_sheet = export_result.sprite_sheet
middle_import_script_context.animation_library = export_result.animation_library
# -------- MIDDLE IMPORT BEGIN --------
var middle_import_script_path: String = options[_Options.MIDDLE_IMPORT_SCRIPT_PATH].strip_edges()
if middle_import_script_path:
if not (middle_import_script_path.is_absolute_path() and middle_import_script_path.begins_with("res://")):
push_error("Middle import script path is not valid: %s" % [middle_import_script_path])
return ERR_FILE_BAD_PATH
var middle_import_script: Script = ResourceLoader \
.load(middle_import_script_path, "Script") as Script
if middle_import_script == null:
push_error("Failed to load middle import script: %s" % [middle_import_script_path])
return ERR_FILE_CORRUPT
if not __is_script_inherited_from(middle_import_script, _MiddleImportScript):
push_error("The script specified as middle import script is not inherited from external_scripts/middle_import_script_base.gd: %s" % [middle_import_script_path])
return ERR_INVALID_DECLARATION
error = middle_import_script.modify_context(
res_source_file_path,
res_save_file_path,
self,
__editor_file_system,
options,
middle_import_script_context)
if error:
push_error("Failed to perform middle-import-script")
return error
error = __append_gen_files(gen_files, middle_import_script_context.gen_files_to_add)
if error:
push_error("Failed to add gen files from middle-import-script context")
return error
# -------- MIDDLE IMPORT END --------
var atlas_making_result: _AtlasMaker.AtlasMakingResult = \
__atlas_maker.make_atlas(middle_import_script_context.atlas_image, res_source_file_path, self)
if atlas_making_result.error:
push_error("Atlas texture making is failed. Errors chain:\n%s" % [atlas_making_result])
return atlas_making_result.error
var import_result: _Importer.ImportResult = __importer.import(
res_source_file_path,
atlas_making_result.atlas,
middle_import_script_context.sprite_sheet,
middle_import_script_context.animation_library,
options,
res_save_file_path)
if import_result.error:
push_error("Import is failed. Errors chain:\n%s" % [import_result])
return import_result.error
var post_import_script_context: _PostImportScript.Context = _PostImportScript.Context.new()
post_import_script_context.resource = import_result.resource
post_import_script_context.resource_saver_flags = import_result.resource_saver_flags
post_import_script_context.save_extension = _get_save_extension()
# -------- POST IMPORT BEGIN --------
var post_import_script_path: String = options[_Options.POST_IMPORT_SCRIPT_PATH].strip_edges()
if post_import_script_path:
if not (post_import_script_path.is_absolute_path() and post_import_script_path.begins_with("res://")):
push_error("Post import script path is not valid: %s" % [post_import_script_path])
return ERR_FILE_BAD_PATH
var post_import_script: Script = ResourceLoader \
.load(post_import_script_path, "Script") as Script
if post_import_script == null:
push_error("Failed to load post import script: %s" % [post_import_script_path])
return ERR_FILE_CORRUPT
if not __is_script_inherited_from(post_import_script, _PostImportScript):
push_error("The script specified as post import script is not inherited from external_scripts/post_import_script_base.gd: %s" % [post_import_script_path])
return ERR_INVALID_DECLARATION
error = post_import_script.modify_context(
res_source_file_path,
res_save_file_path,
self,
__editor_file_system,
options,
middle_import_script_context.middle_import_data,
post_import_script_context)
if error:
push_error("Failed to perform post-import-script")
return error
error = __append_gen_files(gen_files, post_import_script_context.gen_files_to_add)
if error:
push_error("Failed to add gen files from post-import-script context")
return error
# -------- POST IMPORT END --------
error = ResourceSaver.save(
post_import_script_context.resource,
"%s.%s" % [res_save_file_path, post_import_script_context.save_extension],
post_import_script_context.resource_saver_flags)
if error:
push_error("Failed to save the new resource via ResourceSaver")
return error
func _get_import_options(path: String, preset_index: int) -> Array[Dictionary]:
return __options
func _get_option_visibility(path: String, option_name: StringName, options: Dictionary) -> bool:
if __options_visibility_checkers.has(option_name):
return __options_visibility_checkers[option_name].call(options)
return true
func _get_import_order() -> int:
return __import_order
func _get_importer_name() -> String:
return __importer_name
func _get_preset_count() -> int:
return 1
func _get_preset_name(preset_index: int) -> String:
return "Default"
func _get_priority() -> float:
return __priority
func _get_recognized_extensions() -> PackedStringArray:
return __exporter.get_recognized_extensions()
func _get_resource_type() -> String:
return __importer.get_resource_type()
func _get_save_extension() -> String:
return __importer.get_save_extension()
func _get_visible_name() -> String:
return __visible_name
func __is_script_inherited_from(script: Script, base_script: Script) -> bool:
while script != null:
if script == base_script:
return true
script = script.get_base_script()
return false
func __append_gen_files(gen_files: PackedStringArray, gen_files_to_add: PackedStringArray) -> Error:
for gen_file_path in gen_files_to_add:
gen_file_path = gen_file_path.strip_edges()
if gen_files.has(gen_file_path):
continue
if not gen_file_path.is_absolute_path():
push_error("Gen-file-path is not valid path: %s" % [gen_file_path])
return ERR_FILE_BAD_PATH
if not gen_file_path.begins_with("res://"):
push_error("Gen-file-path is not a resource file system path (res://): %s" % [gen_file_path])
return ERR_FILE_BAD_PATH
if not FileAccess.file_exists(gen_file_path):
push_error("The file at the gen-file-path was not found: %s" % [gen_file_path])
return ERR_FILE_NOT_FOUND
gen_files.push_back(gen_file_path)
return OK

View File

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

View File

@@ -0,0 +1,151 @@
@tool
extends "standalone_image_format_loader_extension.gd"
const _Common = preload("common.gd")
static var command_building_rules_for_custom_image_loader_setting: _Setting = _Setting.new(
"command_building_rules_for_custom_image_loader", PackedStringArray(), TYPE_PACKED_STRING_ARRAY, PROPERTY_HINT_NONE)
func _get_recognized_extensions() -> PackedStringArray:
var rules_by_extensions_result: _Setting.GettingValueResult = command_building_rules_for_custom_image_loader_setting.get_value()
if rules_by_extensions_result.error:
push_error("Failed to get command building rules for custom image loader setting")
return PackedStringArray()
var extensions: PackedStringArray
for rule_string in rules_by_extensions_result.value:
var parsed_rule: Dictionary = _parse_rule(rule_string)
if parsed_rule.is_empty():
push_error("Failed to parse command building rule")
return PackedStringArray()
for extension in parsed_rule.extensions as PackedStringArray:
if extensions.has(extension):
push_error("There are duplicated file extensions found in command building rules")
return PackedStringArray()
extensions.push_back(extension)
return extensions
func get_settings() -> Array[_Setting]:
return [command_building_rules_for_custom_image_loader_setting]
static var regex_middle_spaces: RegEx = RegEx.create_from_string("(?<=\\S)\\s(?>=\\S)")
static func normalize_string(source: String) -> String:
return regex_middle_spaces.sub(source.strip_edges(), " ", true)
func _parse_rule(rule_string: String) -> Dictionary:
var parts: PackedStringArray = rule_string.split(":", false, 1)
if parts.size() != 2:
push_error("Failed to find colon (:) delimiter in command building rule between file extensions and command template")
return {}
var extensions: PackedStringArray
for extensions_splitted_by_spaces in normalize_string(parts[0]).split(" ", false):
extensions.append_array(extensions_splitted_by_spaces.split(",", false))
if extensions.is_empty():
push_error("Extensions list in command building rule is empty")
return {}
var command_template: String = parts[1].strip_edges()
if command_template.is_empty():
push_error("Command template in command building rule is empty")
return {}
return {
extensions = extensions,
command_template = command_template,
}
func _load_image(
image: Image,
file_access: FileAccess,
flags,
scale: float
) -> Error:
var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value()
if temp_dir_path_result.error:
push_error("Failed to get Temporary Files Directory Path to export image from source file: %s" % [temp_dir_path_result])
return ERR_UNCONFIGURED
var rules_by_extensions_result: _Setting.GettingValueResult = command_building_rules_for_custom_image_loader_setting.get_value()
if rules_by_extensions_result.error:
push_error("Failed to get command building rules for custom image loader setting")
return ERR_UNCONFIGURED
var command_templates_by_extensions: Dictionary
for rule_string in rules_by_extensions_result.value:
var parsed_rule: Dictionary = _parse_rule(rule_string)
if parsed_rule.is_empty():
push_error("Failed to parse command building rule")
return ERR_UNCONFIGURED
for extension in parsed_rule.extensions as PackedStringArray:
if command_templates_by_extensions.has(extension):
push_error("There are duplicated file extensions found in command building rules")
return ERR_UNCONFIGURED
command_templates_by_extensions[extension] = \
parsed_rule.command_template
var global_input_path: String = file_access.get_path_absolute()
var extension = global_input_path.get_extension()
var global_output_path: String = ProjectSettings.globalize_path(
temp_dir_path_result.value.path_join("temp.png"))
var command_template: String = command_templates_by_extensions.get(extension, "") as String
if command_template.is_empty():
push_error("Failed to find command template for file extension: " + extension)
return ERR_UNCONFIGURED
var command_template_parts: PackedStringArray = _Common.split_words_with_quotes(command_template)
if command_template_parts.is_empty():
push_error("Failed to recognize command template parts for extension: %s" % [extension])
return ERR_UNCONFIGURED
for command_template_part_index in command_template_parts.size():
var command_template_part: String = command_template_parts[command_template_part_index]
command_template_parts[command_template_part_index] = \
command_template_parts[command_template_part_index] \
.replace("{in_path}", global_input_path) \
.replace("{in_path_b}", global_input_path.replace("/", "\\")) \
.replace("{in_path_base}", global_input_path.get_basename()) \
.replace("{in_path_base_b}", global_input_path.get_basename().replace("/", "\\")) \
.replace("{in_file}", global_input_path.get_file()) \
.replace("{in_file_base}", global_input_path.get_file().get_basename()) \
.replace("{in_dir}", global_input_path.get_base_dir()) \
.replace("{in_dir_b}", global_input_path.get_base_dir().replace("/", "\\")) \
.replace("{in_ext}", extension) \
.replace("{out_path}", global_output_path) \
.replace("{out_path_b}", global_output_path.replace("/", "\\")) \
.replace("{out_path_base}", global_output_path.get_basename()) \
.replace("{out_path_base_b}", global_output_path.get_basename().replace("/", "\\")) \
.replace("{out_file}", global_output_path.get_file()) \
.replace("{out_file_base}", global_output_path.get_file().get_basename()) \
.replace("{out_dir}", global_output_path.get_base_dir()) \
.replace("{out_dir_b}", global_output_path.get_base_dir().replace("/", "\\")) \
.replace("{out_ext}", "png")
var command: String = command_template_parts[0]
var arguments: PackedStringArray = command_template_parts.slice(1)
var output: Array
var exit_code: int = OS.execute(command, arguments, output, true, false)
if exit_code:
for arg_index in arguments.size():
arguments[arg_index] = "\nArgument: " + arguments[arg_index]
push_error(" ".join([
"An error occurred while executing",
"the external image converting utility command.",
"Process exited with code %s:\nCommand: %s%s"
]) % [exit_code, command, "".join(arguments)])
return ERR_QUERY_FAILED
if not FileAccess.file_exists(global_output_path):
push_error("The output temporary PNG file is not found: %s" % [global_output_path])
return ERR_UNCONFIGURED
var err: Error = image.load_png_from_buffer(FileAccess.get_file_as_bytes(global_output_path))
if err:
push_error("Failed to load temporary PNG file as image: %s" % [global_output_path])
return err
err = DirAccess.remove_absolute(global_output_path)
if err:
push_warning("Failed to remove temporary file \"%s\". Continuing..." % [global_output_path])
return OK

View File

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

View File

@@ -0,0 +1,152 @@
@tool
const _Setting = preload("setting.gd")
const SPRITE_SHEET_LAYOUTS_NAMES: PackedStringArray = [
"Packed",
"Horizontal strips",
"Vertical strips",
]
enum SpriteSheetLayout {
PACKED = 0,
HORIZONTAL_STRIPS = 1,
VERTICAL_STRIPS = 2,
}
const EDGES_ARTIFACTS_AVOIDANCE_METHODS_NAMES: PackedStringArray = [
"None",
"Transparent spacing",
"Solid color surrounding",
"Borders extrusion",
"Transparent expansion",
]
enum EdgesArtifactsAvoidanceMethod {
NONE = 0,
TRANSPARENT_SPACING = 1,
SOLID_COLOR_SURROUNDING = 2,
BORDERS_EXTRUSION = 3,
TRANSPARENT_EXPANSION = 4,
}
const ANIMATION_DIRECTIONS_NAMES: PackedStringArray = [
"Forward",
"Reverse",
"Ping-pong",
"Ping-pong reverse",
]
enum AnimationDirection {
FORWARD = 0,
REVERSE = 1,
PING_PONG = 2,
PING_PONG_REVERSE = 3,
}
class SpriteInfo:
extends RefCounted
var region: Rect2i
var offset: Vector2i
class SpriteSheetInfo:
extends RefCounted
var source_image_size: Vector2i
var sprites: Array[SpriteInfo]
class FrameInfo:
extends RefCounted
var sprite: SpriteInfo
var duration: float
class AnimationInfo:
extends RefCounted
var name: String
var direction: AnimationDirection
var repeat_count: int
var frames: Array[FrameInfo]
func get_flatten_frames() -> Array[FrameInfo]:
var iteration_frames: Array[FrameInfo] = frames.duplicate()
if direction == AnimationDirection.REVERSE or direction == AnimationDirection.PING_PONG_REVERSE:
iteration_frames.reverse()
if direction == AnimationDirection.PING_PONG or direction == AnimationDirection.PING_PONG_REVERSE:
var returning_frames: Array[FrameInfo] = iteration_frames.duplicate()
returning_frames.pop_back()
returning_frames.reverse()
returning_frames.pop_back()
iteration_frames.append_array(returning_frames)
if repeat_count <= 1:
return iteration_frames
var output_frames: Array[FrameInfo]
var iteration_frames_count: int = iteration_frames.size()
output_frames.resize(iteration_frames_count * repeat_count)
for iteration_number in repeat_count:
for frame_index in iteration_frames_count:
output_frames[iteration_number * iteration_frames_count + frame_index] = \
iteration_frames[frame_index]
return output_frames
class AnimationLibraryInfo:
extends RefCounted
var animations: Array[AnimationInfo]
var autoplay_index: int = -1
static func get_vector2i(dict: Dictionary, x_key: String, y_key: String) -> Vector2i:
return Vector2i(int(dict[x_key]), int(dict[y_key]))
static var common_temporary_files_directory_path_setting: _Setting = _Setting.new(
"temporary_files_directory_path", "", TYPE_STRING, PROPERTY_HINT_GLOBAL_DIR,
"", true, func(v: String): return v.is_empty())
const __backslash: String = "\\"
const __quote: String = "\""
const __space: String = " "
const __tab: String = "\t"
const __empty: String = ""
static func split_words_with_quotes(source: String) -> PackedStringArray:
var parts: PackedStringArray
if source.is_empty():
return parts
var quotation: bool
var previous: String
var current: String
var next: String = source[0]
var chars_count = source.length()
var part: String
for char_idx in chars_count:
previous = current
current = next
next = source[char_idx + 1] if char_idx < chars_count - 1 else ""
if quotation:
# seek for quotation end
if previous != __backslash and current == __quote:
if next == __space or next == __tab or next == __empty:
quotation = false
parts.push_back(part)
part = ""
continue
else:
push_error("Invalid quotation start at %s:\n%s\n%s" % [char_idx, source, " ".repeat(char_idx) + "^"])
return PackedStringArray()
else:
# seek for quotation start
if current == __space or current == __tab:
if not part.is_empty():
parts.push_back(part)
part = ""
continue
else:
if previous != __backslash and current == __quote:
if previous == __space or previous == __tab or previous == __empty:
quotation = true
continue
else:
push_error("Invalid quotation end at %s:\n%s\n%s" % [char_idx, source, " ".repeat(char_idx) + "^"])
return PackedStringArray()
part += current
if quotation:
push_error("Invalid quotation end at %s:\n%s\n%s" % [chars_count - 1, source, " ".repeat(chars_count - 1) + "^"])
return PackedStringArray()
if not part.is_empty():
parts.push_back(part)
return parts

View File

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

View File

@@ -0,0 +1,80 @@
extends Object
const _Result = preload("result.gd").Class
class CreationResult:
extends _Result
var path: String
func success(path: String) -> void:
super._success()
self.path = path
class RemovalResult:
extends _Result
static func create_directory_with_unique_name(base_directory_path: String) -> CreationResult:
const error_description: String = "Failed to create a directory with unique name"
var name: String
var path: String
var result = CreationResult.new()
var error = DirAccess.make_dir_recursive_absolute(base_directory_path)
match error:
OK, ERR_ALREADY_EXISTS:
pass
_:
var inner_result: CreationResult = CreationResult.new()
inner_result.fail(ERR_QUERY_FAILED, "Failed to create base directory recursive")
result.fail(
ERR_CANT_CREATE,
"%s: %s \"%s\"" %
[error_description, error, error_string(error)],
inner_result)
return result
while true:
name = "%d" % (Time.get_unix_time_from_system() * 1000)
path = base_directory_path.path_join(name)
if not DirAccess.dir_exists_absolute(path):
error = DirAccess.make_dir_absolute(path)
match error:
ERR_ALREADY_EXISTS:
pass
OK:
result.success(path)
break
_:
result.fail(
ERR_CANT_CREATE,
"%s: %s \"%s\"" %
[error_description, error, error_string(error)])
break
return result
static func remove_dir_recursive(dir_path: String) -> RemovalResult:
const error_description: String = "Failed to remove a directory with contents recursive"
var result: RemovalResult = RemovalResult.new()
for child_file_name in DirAccess.get_files_at(dir_path):
var child_file_path = dir_path.path_join(child_file_name)
var error: Error = DirAccess.remove_absolute(child_file_path)
if error:
var inner_result: RemovalResult = RemovalResult.new()
inner_result.fail(
ERR_QUERY_FAILED,
"Failed to remove a file: \"%s\". Error: %s \"%s\"" %
[child_file_path, error, error_string(error)])
result.fail(ERR_QUERY_FAILED, "%s: \"%s\"" % [error_description, dir_path], inner_result)
return result
for child_dir_name in DirAccess.get_directories_at(dir_path):
var child_dir_path = dir_path.path_join(child_dir_name)
var inner_result: RemovalResult = remove_dir_recursive(child_dir_path)
if inner_result.error:
result.fail(ERR_QUERY_FAILED, "%s: \"%s\"" % [error_description, dir_path], inner_result)
return result
var error: Error = DirAccess.remove_absolute(dir_path)
if error:
result.fail(
ERR_QUERY_FAILED,
"%s: \"%s\". Error: %s \"%s\"" %
[error_description, dir_path, error, error_string(error)])
return result

View File

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

View File

@@ -0,0 +1,76 @@
@tool
extends EditorPlugin
const ExporterBase = preload("export/_.gd")
const _AtlasMaker = preload("atlas_maker.gd")
const EXPORTERS_SCRIPTS: Array[GDScript] = [
preload("export/aseprite.gd"),
preload("export/krita.gd"),
preload("export/pencil2d.gd"),
preload("export/piskel.gd"),
preload("export/pixelorama.gd"),
]
const ImporterBase = preload("import/_.gd")
const IMPORTERS_SCRIPTS: Array[GDScript] = [
preload("import/animated_sprite_2d.gd"),
preload("import/animated_sprite_3d.gd"),
preload("import/sprite_2d_with_animation_player.gd"),
preload("import/sprite_3d_with_animation_player.gd"),
preload("import/sprite_frames.gd"),
preload("import/texture_rect_with_animation_player.gd"),
preload("import/sprite_sheet.gd"),
]
const StandaloneImageFormatLoaderExtension = preload("standalone_image_format_loader_extension.gd")
const STANDALONE_IMAGE_FORMAT_LOADER_EXTENSIONS: Array[GDScript] = [
preload("command_line_image_format_loader_extension.gd")
]
const CombinedEditorImportPlugin = preload("combined_editor_import_plugin.gd")
var __editor_import_plugins: Array[EditorImportPlugin]
var __image_format_loader_extensions: Array[ImageFormatLoaderExtension]
func _enter_tree() -> void:
var editor_interface: EditorInterface = get_editor_interface()
var editor_file_system: EditorFileSystem = editor_interface.get_resource_filesystem()
var editor_settings: EditorSettings = editor_interface.get_editor_settings()
var exporters: Array[ExporterBase]
for Exporter in EXPORTERS_SCRIPTS:
var exporter: ExporterBase = Exporter.new(editor_file_system)
for setting in exporter.get_settings():
setting.register(editor_settings)
exporters.push_back(exporter)
var image_format_loader_extension: ImageFormatLoaderExtension = \
exporter.get_image_format_loader_extension()
if image_format_loader_extension:
__image_format_loader_extensions.push_back(image_format_loader_extension)
image_format_loader_extension.add_format_loader()
var importers: Array[ImporterBase]
for Importer in IMPORTERS_SCRIPTS:
importers.push_back(Importer.new())
var atlas_maker: _AtlasMaker = _AtlasMaker.new(editor_file_system)
for exporter in exporters:
for importer in importers:
var editor_import_plugin: EditorImportPlugin = \
CombinedEditorImportPlugin.new(exporter, importer, atlas_maker, editor_file_system)
__editor_import_plugins.push_back(editor_import_plugin)
add_import_plugin(editor_import_plugin)
for Extension in STANDALONE_IMAGE_FORMAT_LOADER_EXTENSIONS:
var image_format_loader_extension: StandaloneImageFormatLoaderExtension = \
Extension.new() as StandaloneImageFormatLoaderExtension
for setting in image_format_loader_extension.get_settings():
setting.register(editor_settings)
__image_format_loader_extensions.push_back(image_format_loader_extension)
image_format_loader_extension.add_format_loader()
func _exit_tree() -> void:
for editor_import_plugin in __editor_import_plugins:
remove_import_plugin(editor_import_plugin)
__editor_import_plugins.clear()
for image_format_loader_extension in __image_format_loader_extensions:
image_format_loader_extension.remove_format_loader()
__image_format_loader_extensions.clear()

View File

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

View File

@@ -0,0 +1,212 @@
@tool
extends RefCounted
const _Result = preload("../result.gd").Class
const _Common = preload("../common.gd")
const _Options = preload("../options.gd")
const _Setting = preload("../setting.gd")
const _DirAccessExtensions = preload("../dir_access_ext.gd")
const _SpriteSheetBuilderBase = preload("../sprite_sheet_builder/_.gd")
const _GridBasedSpriteSheetBuilder = preload("../sprite_sheet_builder/grid_based.gd")
const _PackedSpriteSheetBuilder = preload("../sprite_sheet_builder/packed.gd")
const ATLAS_TEXTURE_RESOURCE_TYPE_NAMES: PackedStringArray = [
"Embedded PortableCompressedTexture2D (compact)",
"Embedded ImageTexture (large)",
"Separated image (custom)",
]
enum AtlasResourceType {
EMBEDDED_PORTABLE_COMPRESSED_TEXTURE_2D = 0,
EMBEDDED_IMAGE_TEXTURE = 1,
SEPARATED_IMAGE = 2,
}
class ExportResult:
extends _Result
var atlas_image: Image
var sprite_sheet: _Common.SpriteSheetInfo
var animation_library: _Common.AnimationLibraryInfo
func _get_result_type_description() -> String:
return "Export"
func success(
atlas_image: Image,
sprite_sheet: _Common.SpriteSheetInfo,
animation_library: _Common.AnimationLibraryInfo
) -> void:
_success()
self.atlas_image = atlas_image
self.sprite_sheet = sprite_sheet
self.animation_library = animation_library
var __name: String
var __recognized_extensions: PackedStringArray
var __settings: Array[_Setting] = [_Common.common_temporary_files_directory_path_setting]
var __options: Array[Dictionary] = [
_Options.create_option(_Options.EDGES_ARTIFACTS_AVOIDANCE_METHOD, _Common.EdgesArtifactsAvoidanceMethod.NONE,
PROPERTY_HINT_ENUM, ",".join(_Common.EDGES_ARTIFACTS_AVOIDANCE_METHODS_NAMES),
PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED),
_Options.create_option(_Options.SPRITES_SURROUNDING_COLOR, Color.TRANSPARENT,
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT,
func(o): return o[_Options.EDGES_ARTIFACTS_AVOIDANCE_METHOD] == \
_Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING),
_Options.create_option(_Options.SPRITE_SHEET_LAYOUT, _Common.SpriteSheetLayout.PACKED,
PROPERTY_HINT_ENUM, ",".join(_Common.SPRITE_SHEET_LAYOUTS_NAMES),
PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED),
_Options.create_option(_Options.MAX_CELLS_IN_STRIP, 0,
PROPERTY_HINT_RANGE, "0,,1,or_greater", PROPERTY_USAGE_DEFAULT,
func(o): return o[_Options.SPRITE_SHEET_LAYOUT] != \
_Common.SpriteSheetLayout.PACKED),
_Options.create_option(_Options.TRIM_SPRITES_TO_OVERALL_MIN_SIZE, true,
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT,
func(o): return o[_Options.SPRITE_SHEET_LAYOUT] != \
_Common.SpriteSheetLayout.PACKED),
_Options.create_option(_Options.COLLAPSE_TRANSPARENT_SPRITES, true,
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT,
func(o): return o[_Options.SPRITE_SHEET_LAYOUT] != \
_Common.SpriteSheetLayout.PACKED),
_Options.create_option(_Options.MERGE_DUPLICATED_SPRITES, true,
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT,
func(o): return o[_Options.SPRITE_SHEET_LAYOUT] != \
_Common.SpriteSheetLayout.PACKED),
]
var __image_format_loader_extension: ImageFormatLoaderExtension
func _init(
name: String,
recognized_extensions: PackedStringArray,
options: Array[Dictionary],
settings: Array[_Setting],
image_format_loader_extension: ImageFormatLoaderExtension = null
) -> void:
__name = name
__recognized_extensions = recognized_extensions
__options.append_array(options)
__settings.append_array(settings)
__image_format_loader_extension = image_format_loader_extension
func get_recognized_extensions() -> PackedStringArray:
return __recognized_extensions
func get_options() -> Array[Dictionary]:
return __options
func get_name() -> String:
return __name
func get_settings() -> Array[_Setting]:
return __settings
func get_image_format_loader_extension() -> ImageFormatLoaderExtension:
return __image_format_loader_extension
func export(
res_source_file_path: String,
options: Dictionary,
editor_import_plugin: EditorImportPlugin
) -> ExportResult:
return _export(
res_source_file_path,
options)
func _export(source_file: String, options: Dictionary) -> ExportResult:
assert(false, "This method is abstract and must be overriden.")
var result: ExportResult = ExportResult.new()
result.fail(ERR_UNCONFIGURED)
return result
enum AnimationOptions {
FramesCount = 1,
Direction = 2,
RepeatCount = 4,
}
static var __option_regex: RegEx = RegEx.create_from_string("\\s-\\p{L}:\\s*\\S+")
static var __natural_number_regex: RegEx = RegEx.create_from_string("\\A\\d+\\z")
class AnimationParamsParsingResult:
extends _Result
var name: String
var first_frame_index: int
var frames_count: int
var direction: _Common.AnimationDirection
var repeat_count: int
func _get_result_type_description() -> String:
return "Animation parameters parsing"
static func _parse_animation_params(
raw_animation_params: String,
animation_options: AnimationOptions,
first_frame_index: int,
frames_count: int = 0
) -> AnimationParamsParsingResult:
var result = AnimationParamsParsingResult.new()
if first_frame_index < 0:
result.fail(ERR_INVALID_DATA, "Wrong value for animation first frame index. Expected natural number, got: %s" % [first_frame_index])
return result
result.first_frame_index = first_frame_index
result.frames_count = frames_count
result.direction = -1
result.repeat_count = -1
raw_animation_params = raw_animation_params.strip_edges()
var options_matches: Array[RegExMatch] = __option_regex.search_all(raw_animation_params)
var first_match_position: int = raw_animation_params.length()
for option_match in options_matches:
var match_position: int = option_match.get_start()
assert(match_position >= 0)
if match_position < first_match_position:
first_match_position = match_position
var raw_option: String = option_match.get_string().strip_edges()
var raw_value = raw_option.substr(3).strip_edges()
match raw_option.substr(0, 3):
"-f:":
if animation_options & AnimationOptions.FramesCount:
if result.frames_count == 0:
if __natural_number_regex.search(raw_value):
result.frames_count = raw_value.to_int()
if result.frames_count <= 0:
result.fail(ERR_INVALID_DATA, "Wrong value format for frames count. Expected positive integer number, got: \"%s\"" % [raw_value])
return result
"-d:":
if animation_options & AnimationOptions.Direction:
if result.direction < 0:
match raw_value:
"f": result.direction = _Common.AnimationDirection.FORWARD
"r": result.direction = _Common.AnimationDirection.REVERSE
"pp": result.direction = _Common.AnimationDirection.PING_PONG
"ppr": result.direction = _Common.AnimationDirection.PING_PONG_REVERSE
_:
result.fail(ERR_INVALID_DATA, "Wrong value format for animation direction. Expected one of: [\"f\", \"r\", \"pp\", \"ppr\"], got: \"%s\"" % [raw_value])
return result
"-r:":
if animation_options & AnimationOptions.RepeatCount:
if result.repeat_count < 0:
if __natural_number_regex.search(raw_value):
result.repeat_count = raw_value.to_int()
else:
result.fail(ERR_INVALID_DATA, "Wrong value format for repeat count. Expected positive integer number or zero, got: \"%s\"" % [raw_value])
return result
_: pass # Ignore unknown parameter
result.name = raw_animation_params.left(first_match_position).strip_edges()
if result.frames_count <= 0:
result.fail(ERR_UNCONFIGURED, "Animation frames count is required but not specified")
return result
return result
func _create_sprite_sheet_builder(options: Dictionary) -> _SpriteSheetBuilderBase:
var sprite_sheet_layout: _Common.SpriteSheetLayout = options[_Options.SPRITE_SHEET_LAYOUT]
return \
_PackedSpriteSheetBuilder.new(
options[_Options.EDGES_ARTIFACTS_AVOIDANCE_METHOD],
options[_Options.SPRITES_SURROUNDING_COLOR]) \
if sprite_sheet_layout == _Common.SpriteSheetLayout.PACKED else \
_GridBasedSpriteSheetBuilder.new(
options[_Options.EDGES_ARTIFACTS_AVOIDANCE_METHOD],
_GridBasedSpriteSheetBuilder.StripDirection.HORIZONTAL
if sprite_sheet_layout == _Common.SpriteSheetLayout.HORIZONTAL_STRIPS else
_GridBasedSpriteSheetBuilder.StripDirection.HORIZONTAL,
options[_Options.MAX_CELLS_IN_STRIP],
options[_Options.TRIM_SPRITES_TO_OVERALL_MIN_SIZE],
options[_Options.COLLAPSE_TRANSPARENT_SPRITES],
options[_Options.MERGE_DUPLICATED_SPRITES],
options[_Options.SPRITES_SURROUNDING_COLOR])

View File

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

View File

@@ -0,0 +1,288 @@
@tool
extends "_.gd"
const __aseprite_sheet_types_by_sprite_sheet_layout: PackedStringArray = \
[ "packed", "rows", "columns" ]
const __aseprite_animation_directions: PackedStringArray = \
[ "forward", "reverse", "pingpong", "pingpong_reverse" ]
var __os_command_setting: _Setting = _Setting.new(
"aseprite_or_libre_sprite_command", "", TYPE_STRING, PROPERTY_HINT_NONE,
"", true, func(v: String): return v.is_empty())
var __os_command_arguments_setting: _Setting = _Setting.new(
"aseprite_or_libre_sprite_command_arguments", PackedStringArray(), TYPE_PACKED_STRING_ARRAY, PROPERTY_HINT_NONE,
"", true, func(v: PackedStringArray): return false)
func _init(editor_file_system: EditorFileSystem) -> void:
var recognized_extensions: PackedStringArray = ["ase", "aseprite"]
super("Aseprite", recognized_extensions, [],
[__os_command_setting, __os_command_arguments_setting],
CustomImageFormatLoaderExtension.new(
recognized_extensions,
__os_command_setting,
__os_command_arguments_setting,
_Common.common_temporary_files_directory_path_setting))
func _export(res_source_file_path: String, options: Dictionary) -> ExportResult:
var result: ExportResult = ExportResult.new()
var err: Error
var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value()
if os_command_result.error:
result.fail(ERR_UNCONFIGURED, "Failed to get Aseprite Command to export spritesheet", os_command_result)
return result
var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value()
if os_command_arguments_result.error:
result.fail(ERR_UNCONFIGURED, "Failed to get Aseprite Command Arguments to export spritesheet", os_command_arguments_result)
return result
var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value()
if temp_dir_path_result.error:
result.fail(ERR_UNCONFIGURED, "Failed to get Temporary Files Directory Path to export spritesheet", temp_dir_path_result)
return result
var global_temp_dir_path: String = ProjectSettings.globalize_path(
temp_dir_path_result.value.strip_edges())
var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \
_DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path)
if unique_temp_dir_creation_result.error:
result.fail(ERR_QUERY_FAILED, "Failed to create unique temporary directory to export spritesheet", unique_temp_dir_creation_result)
return result
var unique_temp_dir_path: String = unique_temp_dir_creation_result.path
var global_source_file_path: String = ProjectSettings.globalize_path(res_source_file_path)
var global_png_path: String = unique_temp_dir_path.path_join("temp.png")
var global_json_path: String = unique_temp_dir_path.path_join("temp.json")
var command: String = os_command_result.value.strip_edges()
var arguments: PackedStringArray = \
os_command_arguments_result.value + \
PackedStringArray([
"--batch",
"--format", "json-array",
"--list-tags",
"--sheet", global_png_path,
"--data", global_json_path,
global_source_file_path])
var output: Array = []
var exit_code: int = OS.execute(command, arguments, output, true, false)
if exit_code:
for arg_index in arguments.size():
arguments[arg_index] = "\nArgument: " + arguments[arg_index]
result.fail(ERR_QUERY_FAILED, " ".join([
"An error occurred while executing the Aseprite command.",
"Process exited with code %s:\nCommand: %s%s"
]) % [exit_code, command, "".join(arguments)])
return result
var raw_atlas_image: Image = Image.load_from_file(global_png_path)
var json = JSON.new()
err = json.parse(FileAccess.get_file_as_string(global_json_path))
if err:
result.fail(ERR_INVALID_DATA, "Failed to parse sprite sheet json data with error %s \"%s\"" % [err, error_string(err)])
return result
var raw_sprite_sheet_data: Dictionary = json.data
var sprite_sheet_layout: _Common.SpriteSheetLayout = options[_Options.SPRITE_SHEET_LAYOUT]
var source_image_size: Vector2i = _Common.get_vector2i(
raw_sprite_sheet_data.frames[0].sourceSize, "w", "h")
var frames_images_by_indices: Dictionary
var tags_data: Array = raw_sprite_sheet_data.meta.frameTags
var frames_data: Array = raw_sprite_sheet_data.frames
var frames_count: int = frames_data.size()
if tags_data.is_empty():
var default_animation_name: String = options[_Options.DEFAULT_ANIMATION_NAME].strip_edges()
if default_animation_name.is_empty():
default_animation_name = "default"
tags_data.push_back({
name = default_animation_name,
from = 0,
to = frames_count - 1,
direction = __aseprite_animation_directions[options[_Options.DEFAULT_ANIMATION_DIRECTION]],
repeat = options[_Options.DEFAULT_ANIMATION_REPEAT_COUNT]
})
var animations_count: int = tags_data.size()
for tag_data in tags_data:
for frame_index in range(tag_data.from, tag_data.to + 1):
if frames_images_by_indices.has(frame_index):
continue
var frame_data: Dictionary = frames_data[frame_index]
frames_images_by_indices[frame_index] = raw_atlas_image.get_region(Rect2i(
_Common.get_vector2i(frame_data.frame, "x", "y"),
source_image_size))
var used_frames_indices: PackedInt32Array = PackedInt32Array(frames_images_by_indices.keys())
used_frames_indices.sort()
var used_frames_count: int = used_frames_indices.size()
var sprite_sheet_frames_indices_by_global_frame_indices: Dictionary
for sprite_sheet_frame_index in used_frames_indices.size():
sprite_sheet_frames_indices_by_global_frame_indices[
used_frames_indices[sprite_sheet_frame_index]] = \
sprite_sheet_frame_index
var used_frames_images: Array[Image]
used_frames_images.resize(used_frames_count)
for i in used_frames_count:
used_frames_images[i] = frames_images_by_indices[used_frames_indices[i]]
var sprite_sheet_builder: _SpriteSheetBuilderBase = _create_sprite_sheet_builder(options)
var sprite_sheet_building_result: _SpriteSheetBuilderBase.SpriteSheetBuildingResult = sprite_sheet_builder.build_sprite_sheet(used_frames_images)
if sprite_sheet_building_result.error:
result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result)
return result
var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet
var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new()
var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges()
var all_frames: Array[_Common.FrameInfo]
all_frames.resize(used_frames_count)
var unique_animations_names: PackedStringArray
for animation_index in animations_count:
var tag_data: Dictionary = tags_data[animation_index]
var animation_params_parsing_result: AnimationParamsParsingResult = _parse_animation_params(
tag_data.name.strip_edges(),
AnimationOptions.Direction | AnimationOptions.RepeatCount,
tag_data.from,
tag_data.to - tag_data.from + 1)
if animation_params_parsing_result.error:
result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters",
animation_params_parsing_result)
return result
if unique_animations_names.has(animation_params_parsing_result.name):
result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" %
[animation_params_parsing_result.name, animation_index])
return result
unique_animations_names.push_back(animation_params_parsing_result.name)
var animation = _Common.AnimationInfo.new()
animation.name = animation_params_parsing_result.name
if animation.name.is_empty():
result.fail(ERR_INVALID_DATA, "A tag with empty name found")
return result
if animation.name == autoplay_animation_name:
animation_library.autoplay_index = animation_index
animation.direction = __aseprite_animation_directions.find(tag_data.direction)
if animation_params_parsing_result.direction >= 0:
animation.direction = animation_params_parsing_result.direction
animation.repeat_count = int(tag_data.get("repeat", "0"))
if animation_params_parsing_result.repeat_count >= 0:
animation.repeat_count = animation_params_parsing_result.repeat_count
for global_frame_index in range(tag_data.from, tag_data.to + 1):
var sprite_sheet_frame_index: int = \
sprite_sheet_frames_indices_by_global_frame_indices[global_frame_index]
var frame: _Common.FrameInfo = all_frames[sprite_sheet_frame_index]
if frame == null:
frame = _Common.FrameInfo.new()
frame.sprite = sprite_sheet.sprites[sprite_sheet_frame_index]
frame.duration = frames_data[global_frame_index].duration * 0.001
all_frames[sprite_sheet_frame_index] = frame
animation.frames.push_back(frame)
animation_library.animations.push_back(animation)
if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0:
push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name])
if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error:
push_warning(
"Failed to remove unique temporary directory: \"%s\"" %
[unique_temp_dir_path])
result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library)
return result
class CustomImageFormatLoaderExtension:
extends ImageFormatLoaderExtension
var __recognized_extensions: PackedStringArray
var __os_command_setting: _Setting
var __os_command_arguments_setting: _Setting
var __common_temporary_files_directory_path_setting: _Setting
func _init(recognized_extensions: PackedStringArray,
os_command_setting: _Setting,
os_command_arguments_setting: _Setting,
common_temporary_files_directory_path_setting: _Setting
) -> void:
__recognized_extensions = recognized_extensions
__os_command_setting = os_command_setting
__os_command_arguments_setting = os_command_arguments_setting
__common_temporary_files_directory_path_setting = \
common_temporary_files_directory_path_setting
func _get_recognized_extensions() -> PackedStringArray:
return __recognized_extensions
func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error:
var global_source_file_path: String = file_access.get_path_absolute()
var err: Error
var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value()
if os_command_result.error:
push_error(os_command_result.error_description)
return os_command_result.error
var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value()
if os_command_arguments_result.error:
push_error(os_command_arguments_result.error_description)
return os_command_arguments_result.error
var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value()
if temp_dir_path_result.error:
push_error("Failed to get Temporary Files Directory Path to export spritesheet")
return temp_dir_path_result.error
var global_temp_dir_path: String = ProjectSettings.globalize_path(
temp_dir_path_result.value.strip_edges())
var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \
_DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path)
if unique_temp_dir_creation_result.error:
push_error("Failed to create unique temporary directory to export spritesheet")
return unique_temp_dir_creation_result.error
var unique_temp_dir_path: String = unique_temp_dir_creation_result.path
var global_png_path: String = unique_temp_dir_path.path_join("temp.png")
var global_json_path: String = unique_temp_dir_path.path_join("temp.json")
var command: String = os_command_result.value.strip_edges()
var arguments: PackedStringArray = \
os_command_arguments_result.value + \
PackedStringArray([
"--batch",
"--format", "json-array",
"--list-tags",
"--sheet", global_png_path,
"--data", global_json_path,
global_source_file_path,
])
var output: Array = []
var exit_code: int = OS.execute(command, arguments, output, true, false)
if exit_code:
for arg_index in arguments.size():
arguments[arg_index] = "\nArgument: " + arguments[arg_index]
push_error(" ".join([
"An error occurred while executing the Aseprite command.",
"Process exited with code %s:\nCommand: %s%s"
]) % [exit_code, command, "".join(arguments)])
return ERR_QUERY_FAILED
var raw_atlas_image: Image = Image.load_from_file(global_png_path)
var json = JSON.new()
err = json.parse(FileAccess.get_file_as_string(global_json_path))
if err:
push_error("Failed to parse sprite sheet json data with error %s \"%s\"" % [err, error_string(err)])
return ERR_INVALID_DATA
var raw_sprite_sheet_data: Dictionary = json.data
var source_image_size: Vector2i = _Common.get_vector2i(
raw_sprite_sheet_data.frames[0].sourceSize, "w", "h")
if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error:
push_warning(
"Failed to remove unique temporary directory: \"%s\"" %
[unique_temp_dir_path])
image.copy_from(raw_atlas_image.get_region(Rect2i(Vector2i.ZERO, source_image_size)))
return OK

View File

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

View File

@@ -0,0 +1,286 @@
@tool
extends "_.gd"
const _XML = preload("../xml.gd")
var __os_command_setting: _Setting = _Setting.new(
"krita_command", "", TYPE_STRING, PROPERTY_HINT_NONE,
"", true, func(v: String): return v.is_empty())
var __os_command_arguments_setting: _Setting = _Setting.new(
"krita_command_arguments", PackedStringArray(), TYPE_PACKED_STRING_ARRAY, PROPERTY_HINT_NONE,
"", true, func(v: PackedStringArray): return false)
func _init(editor_file_system: EditorFileSystem) -> void:
var recognized_extensions: PackedStringArray = ["kra", "krita"]
super("Krita", recognized_extensions, [], [
__os_command_setting,
__os_command_arguments_setting,
], CustomImageFormatLoaderExtension.new(recognized_extensions))
func __validate_image_name(image_name: String) -> _Result:
var result: _Result = _Result.new()
var image_name_with_underscored_invalid_characters: String = image_name.validate_filename()
var unsupported_characters: PackedStringArray
for character_index in image_name.length():
var validated_character = image_name_with_underscored_invalid_characters[character_index]
if validated_character == "_":
var original_character = image_name[character_index]
if original_character != "_":
if not unsupported_characters.has(original_character):
unsupported_characters.push_back(original_character)
if not unsupported_characters.is_empty():
result.fail(ERR_FILE_BAD_PATH, "There are unsupported characters in Krita Document Title: \"%s\"" % ["".join(unsupported_characters)])
return result
func _export(res_source_file_path: String, options: Dictionary) -> ExportResult:
var result: ExportResult = ExportResult.new()
var err: Error
var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value()
if os_command_result.error:
result.fail(ERR_UNCONFIGURED, "Failed to get Krita Command to export spritesheet", os_command_result)
return result
var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value()
if os_command_arguments_result.error:
result.fail(ERR_UNCONFIGURED, "Failed to get Krita Command Arguments to export spritesheet", os_command_arguments_result)
return result
var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value()
if temp_dir_path_result.error:
result.fail(ERR_UNCONFIGURED, "Failed to get Temporary Files Directory Path to export spritesheet", temp_dir_path_result)
return result
var global_temp_dir_path: String = ProjectSettings.globalize_path(
temp_dir_path_result.value.strip_edges())
var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \
_DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path)
if unique_temp_dir_creation_result.error:
result.fail(ERR_QUERY_FAILED, "Failed to create unique temporary directory to export spritesheet", unique_temp_dir_creation_result)
return result
var unique_temp_dir_path: String = unique_temp_dir_creation_result.path
var global_source_file_path: String = ProjectSettings.globalize_path(res_source_file_path)
var zip_reader: ZIPReader = ZIPReader.new()
var zip_error: Error = zip_reader.open(global_source_file_path)
if zip_error:
result.fail(zip_error, "Failed to open Krita file \"%s\" as ZIP archive with error: %s (%s)" % [res_source_file_path, zip_error, error_string(zip_error)])
return result
var files_names_in_zip: PackedStringArray = zip_reader.get_files()
var maindoc_filename: String = "maindoc.xml"
var maindoc_buffer: PackedByteArray = zip_reader.read_file(maindoc_filename)
var maindoc_xml_root: _XML.XMLNodeRoot = _XML.parse_buffer(maindoc_buffer)
var maindoc_doc_xml_element: _XML.XMLNodeElement = maindoc_xml_root.get_elements("DOC").front()
var image_xml_element: _XML.XMLNodeElement = maindoc_doc_xml_element.get_elements("IMAGE").front()
var image_name: String = image_xml_element.get_string("name")
var image_size: Vector2i = image_xml_element.get_vector2i("width", "height")
var image_name_validation_result: _Result = __validate_image_name(image_name)
if image_name_validation_result.error:
result.fail(ERR_INVALID_DATA,
"Krita Document Title have unsupported format",
image_name_validation_result)
return result
var has_keyframes: bool
for layer_xml_element in image_xml_element.get_elements("layers").front().get_elements("layer"):
if layer_xml_element.attributes.has("keyframes"):
has_keyframes = true
break
if not has_keyframes:
result.fail(ERR_INVALID_DATA, "Source file has no keyframes")
return result
var animation_xml_element: _XML.XMLNodeElement = image_xml_element.get_elements("animation").front()
var animation_framerate: int = max(1, animation_xml_element.get_elements("framerate").front().get_int("value"))
var animation_range_xml_element: _XML.XMLNodeElement = animation_xml_element.get_elements("range").front()
var animation_index_filename: String = "%s/animation/index.xml" % image_name
var animation_index_buffer: PackedByteArray = zip_reader.read_file(animation_index_filename)
var animation_index_xml_root: _XML.XMLNodeRoot = _XML.parse_buffer(animation_index_buffer)
var animation_index_animation_metadata_xml_element: _XML.XMLNodeElement = animation_index_xml_root.get_elements("animation-metadata").front()
var animation_index_animation_metadata_range_xml_element: _XML.XMLNodeElement = animation_index_animation_metadata_xml_element.get_elements("range").front()
var export_settings_xml_element: _XML.XMLNodeElement = animation_index_animation_metadata_xml_element.get_elements("export-settings").front()
var sequence_file_path_xml_element: _XML.XMLNodeElement = export_settings_xml_element.get_elements("sequenceFilePath").front()
var sequence_base_name_xml_element: _XML.XMLNodeElement = export_settings_xml_element.get_elements("sequenceBaseName").front()
var animations_parameters_parsing_results: Array[AnimationParamsParsingResult]
var total_animations_frames_count: int
var first_animations_frame_index: int = -1
var last_animations_frame_index: int = -1
var global_temp_kra_path: String
var temp_file_base_name: String = "img"
var temp_kra_file_name: String = temp_file_base_name + ".kra"
var temp_png_file_name_pattern: String = temp_file_base_name + ".png"
var storyboard_index_file_name: String = "%s/storyboard/index.xml" % image_name
if storyboard_index_file_name in files_names_in_zip:
var storyboard_index_xml_root: _XML.XMLNodeRoot = _XML.parse_buffer(zip_reader.read_file("%s/storyboard/index.xml" % image_name))
var storyboard_info_xml_element: _XML.XMLNodeElement = storyboard_index_xml_root.get_elements("storyboard-info").front()
var storyboard_item_list_xml_element: _XML.XMLNodeElement = storyboard_info_xml_element.get_elements("StoryboardItemList").front()
var storyboard_item_xml_elements: Array[_XML.XMLNodeElement] = storyboard_item_list_xml_element.get_elements("storyboarditem")
var unique_animations_names: PackedStringArray
for animation_index in storyboard_item_xml_elements.size():
var story_xml_element: _XML.XMLNodeElement = storyboard_item_xml_elements[animation_index]
var animation_first_frame: int = story_xml_element.get_int("frame")
var animation_params_parsing_result: AnimationParamsParsingResult = _parse_animation_params(
story_xml_element.get_string("item-name").strip_edges(),
AnimationOptions.Direction | AnimationOptions.RepeatCount,
animation_first_frame,
story_xml_element.get_int("duration-frame") + \
animation_framerate * story_xml_element.get_int("duration-second"))
if animation_params_parsing_result.error:
result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters",
animation_params_parsing_result)
return result
if unique_animations_names.has(animation_params_parsing_result.name):
result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" %
[animation_params_parsing_result.name, animation_index])
return result
unique_animations_names.push_back(animation_params_parsing_result.name)
animations_parameters_parsing_results.push_back(animation_params_parsing_result)
total_animations_frames_count += animation_params_parsing_result.frames_count
if first_animations_frame_index < 0 or animation_params_parsing_result.first_frame_index < first_animations_frame_index:
first_animations_frame_index = animation_params_parsing_result.first_frame_index
var animation_last_frame_index: int = animation_params_parsing_result.first_frame_index + animation_params_parsing_result.frames_count - 1
if last_animations_frame_index < 0 or animation_last_frame_index > last_animations_frame_index:
last_animations_frame_index = animation_last_frame_index
animation_range_xml_element.attributes["from"] = str(first_animations_frame_index)
animation_range_xml_element.attributes["to"] = str(last_animations_frame_index)
global_temp_kra_path = unique_temp_dir_path.path_join(temp_kra_file_name)
animation_index_animation_metadata_range_xml_element.attributes["from"] = str(first_animations_frame_index)
animation_index_animation_metadata_range_xml_element.attributes["to"] = str(last_animations_frame_index)
var zip_writer = ZIPPacker.new()
zip_writer.open(global_temp_kra_path, ZIPPacker.APPEND_CREATE)
for filename in zip_reader.get_files():
zip_writer.start_file(filename)
match filename:
maindoc_filename:
zip_writer.write_file(maindoc_xml_root.dump_to_buffer())
animation_index_filename:
zip_writer.write_file(animation_index_xml_root.dump_to_buffer())
_: zip_writer.write_file(zip_reader.read_file(filename))
zip_writer.close_file()
zip_writer.close()
else:
first_animations_frame_index = animation_range_xml_element.get_int("from")
last_animations_frame_index = animation_range_xml_element.get_int("to")
total_animations_frames_count = last_animations_frame_index - first_animations_frame_index + 1
var default_animation_params_parsing_result: AnimationParamsParsingResult = AnimationParamsParsingResult.new()
default_animation_params_parsing_result.name = options[_Options.DEFAULT_ANIMATION_NAME].strip_edges()
if not default_animation_params_parsing_result.name:
default_animation_params_parsing_result.name = "default"
default_animation_params_parsing_result.first_frame_index = first_animations_frame_index
default_animation_params_parsing_result.frames_count = last_animations_frame_index - first_animations_frame_index + 1
default_animation_params_parsing_result.direction = options[_Options.DEFAULT_ANIMATION_DIRECTION]
default_animation_params_parsing_result.repeat_count = options[_Options.DEFAULT_ANIMATION_REPEAT_COUNT]
animations_parameters_parsing_results.push_back(default_animation_params_parsing_result)
global_temp_kra_path = global_source_file_path
zip_reader.close()
var global_temp_png_path_pattern: String = unique_temp_dir_path.path_join(temp_png_file_name_pattern)
var command: String = os_command_result.value.strip_edges()
var arguments: PackedStringArray = \
os_command_arguments_result.value + \
PackedStringArray([
"--export-sequence",
"--export-filename", global_temp_png_path_pattern,
global_temp_kra_path])
var output: Array
var exit_code: int = OS.execute(command, arguments, output, true, false)
if exit_code:
for arg_index in arguments.size():
arguments[arg_index] = "\nArgument: " + arguments[arg_index]
result.fail(ERR_QUERY_FAILED, " ".join([
"An error occurred while executing the Krita command.",
"Process exited with code %s:\nCommand: %s%s"
]) % [exit_code, command, "".join(arguments)])
return result
var unique_frames_count: int = last_animations_frame_index + 1 # - first_stories_frame
var frames_images: Array[Image]
for image_idx in unique_frames_count:
var global_frame_png_path: String = unique_temp_dir_path \
.path_join("%s%04d.png" % [temp_file_base_name, image_idx])
if FileAccess.file_exists(global_frame_png_path):
var image: Image = Image.load_from_file(global_frame_png_path)
frames_images.push_back(image)
else:
frames_images.push_back(frames_images.back())
var sprite_sheet_builder: _SpriteSheetBuilderBase = _create_sprite_sheet_builder(options)
var sprite_sheet_building_result: _SpriteSheetBuilderBase.SpriteSheetBuildingResult = sprite_sheet_builder.build_sprite_sheet(frames_images)
if sprite_sheet_building_result.error:
result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result)
return result
var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet
var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new()
var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges()
var frames_duration: float = 1.0 / animation_framerate
var all_frames: Array[_Common.FrameInfo]
all_frames.resize(unique_frames_count)
for animation_index in animations_parameters_parsing_results.size():
var animation_params_parsing_result: AnimationParamsParsingResult = animations_parameters_parsing_results[animation_index]
var animation = _Common.AnimationInfo.new()
animation.name = animation_params_parsing_result.name
if animation.name == autoplay_animation_name:
animation_library.autoplay_index = animation_index
animation.direction = animation_params_parsing_result.direction
if animation.direction < 0:
animation.direction = _Common.AnimationDirection.FORWARD
animation.repeat_count = animation_params_parsing_result.repeat_count
if animation.repeat_count < 0:
animation.repeat_count = 1
for animation_frame_index in animation_params_parsing_result.frames_count:
var global_frame_index: int = animation_params_parsing_result.first_frame_index + animation_frame_index
var frame: _Common.FrameInfo = all_frames[global_frame_index]
if frame == null:
frame = _Common.FrameInfo.new()
frame.sprite = sprite_sheet.sprites[global_frame_index]
frame.duration = frames_duration
all_frames[global_frame_index] = frame
animation.frames.push_back(frame)
animation_library.animations.push_back(animation)
if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0:
push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name])
if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error:
push_warning(
"Failed to remove unique temporary directory: \"%s\"" %
[unique_temp_dir_path])
result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library)
return result
class CustomImageFormatLoaderExtension:
extends ImageFormatLoaderExtension
var __recognized_extensions: PackedStringArray
func _init(recognized_extensions: PackedStringArray) -> void:
__recognized_extensions = recognized_extensions
func _get_recognized_extensions() -> PackedStringArray:
return __recognized_extensions
func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error:
var zip_reader := ZIPReader.new()
zip_reader.open(file_access.get_path_absolute())
image.load_png_from_buffer(zip_reader.read_file("mergedimage.png"))
zip_reader.close()
return OK

View File

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

View File

@@ -0,0 +1,267 @@
@tool
extends "_.gd"
const _XML = preload("../xml.gd")
var __os_command_setting: _Setting = _Setting.new(
"pencil2d_command", "", TYPE_STRING, PROPERTY_HINT_NONE,
"", true, func(v: String): return v.is_empty())
var __os_command_arguments_setting: _Setting = _Setting.new(
"pencil2d_command_arguments", PackedStringArray(), TYPE_PACKED_STRING_ARRAY, PROPERTY_HINT_NONE,
"", true, func(v: PackedStringArray): return false)
const __ANIMATIONS_PARAMETERS_OPTION: StringName = "pencil2d/animations_parameters"
func _init(editor_file_system: EditorFileSystem) -> void:
var recognized_extensions: PackedStringArray = ["pclx"]
super("Pencil2D", recognized_extensions, [
_Options.create_option(__ANIMATIONS_PARAMETERS_OPTION, PackedStringArray(),
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT)],
[ __os_command_setting, __os_command_arguments_setting ],
CustomImageFormatLoaderExtension.new(
recognized_extensions,
__os_command_setting,
__os_command_arguments_setting,
_Common.common_temporary_files_directory_path_setting))
func _export(res_source_file_path: String, options: Dictionary) -> ExportResult:
var result: ExportResult = ExportResult.new()
var err: Error
var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value()
if os_command_result.error:
result.fail(ERR_UNCONFIGURED, "Failed to get Pencil2D Command to export spritesheet", os_command_result)
return result
var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value()
if os_command_arguments_result.error:
result.fail(ERR_UNCONFIGURED, "Failed to get Pencil2D Command Arguments to export spritesheet", os_command_arguments_result)
return result
var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value()
if temp_dir_path_result.error:
result.fail(ERR_UNCONFIGURED, "Failed to get Temporary Files Directory Path to export spritesheet", temp_dir_path_result)
return result
var global_temp_dir_path: String = ProjectSettings.globalize_path(
temp_dir_path_result.value.strip_edges())
var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \
_DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path)
if unique_temp_dir_creation_result.error:
result.fail(ERR_QUERY_FAILED, "Failed to create unique temporary directory to export spritesheet", unique_temp_dir_creation_result)
return result
var unique_temp_dir_path: String = unique_temp_dir_creation_result.path
var global_source_file_path: String = ProjectSettings.globalize_path(res_source_file_path)
var zip_reader: ZIPReader = ZIPReader.new()
var zip_error: Error = zip_reader.open(global_source_file_path)
if zip_error:
result.fail(zip_error, "Failed to open Pencil2D file \"%s\" as ZIP archive with error: %s (%s)" % [res_source_file_path, zip_error, error_string(zip_error)])
return result
var buffer: PackedByteArray = zip_reader.read_file("main.xml")
var main_xml_root: _XML.XMLNodeRoot = _XML.parse_buffer(buffer)
zip_reader.close()
var animation_framerate: int = main_xml_root \
.get_elements("document").front() \
.get_elements("projectdata").front() \
.get_elements("fps").front() \
.get_int("value")
var raw_animations_params_list: PackedStringArray = options[__ANIMATIONS_PARAMETERS_OPTION]
var animations_params_parsing_results: Array[AnimationParamsParsingResult]
animations_params_parsing_results.resize(raw_animations_params_list.size())
var unique_animations_names: PackedStringArray
var frame_indices_to_export
var unique_frames_count: int = 0
var animation_first_frame_index: int = 0
for animation_index in raw_animations_params_list.size():
var raw_animation_params: String = raw_animations_params_list[animation_index]
var animation_params_parsing_result: AnimationParamsParsingResult = _parse_animation_params(
raw_animation_params,
AnimationOptions.FramesCount | AnimationOptions.Direction | AnimationOptions.RepeatCount,
animation_first_frame_index)
if animation_params_parsing_result.error:
result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters", animation_params_parsing_result)
return result
if unique_animations_names.has(animation_params_parsing_result.name):
result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" %
[animation_params_parsing_result.name, animation_index])
return result
unique_animations_names.push_back(animation_params_parsing_result.name)
unique_frames_count += animation_params_parsing_result.frames_count
animation_first_frame_index += animation_params_parsing_result.frames_count
animations_params_parsing_results[animation_index] = animation_params_parsing_result
# -o --export <output_path> Render the file to <output_path>
# --camera <layer_name> Name of the camera layer to use
# --width <integer> Width of the output frames
# --height <integer> Height of the output frames
# --start <frame> The first frame you want to include in the exported movie
# --end <frame> The last frame you want to include in the exported movie. Can also be last or last-sound to automatically use the last frame containing animation or sound respectively
# --transparency Render transparency when possible
# input Path to input pencil file
var png_base_name: String = "temp"
var global_temp_png_path: String = unique_temp_dir_path.path_join("%s.png" % png_base_name)
var command: String = os_command_result.value.strip_edges()
var arguments: PackedStringArray = \
os_command_arguments_result.value + \
PackedStringArray([
"--export", global_temp_png_path,
"--start", 1,
"--end", unique_frames_count,
"--transparency",
global_source_file_path])
var output: Array
var exit_code: int = OS.execute(command, arguments, output, true, false)
if exit_code:
for arg_index in arguments.size():
arguments[arg_index] = "\nArgument: " + arguments[arg_index]
result.fail(ERR_QUERY_FAILED, " ".join([
"An error occurred while executing the Pencil2D command.",
"Process exited with code %s:\nCommand: %s%s"
]) % [exit_code, command, "".join(arguments)])
return result
var frames_images: Array[Image]
for image_idx in unique_frames_count:
var global_frame_png_path: String = unique_temp_dir_path \
.path_join("%s%04d.png" % [png_base_name, image_idx + 1])
frames_images.push_back(Image.load_from_file(global_frame_png_path))
var sprite_sheet_builder: _SpriteSheetBuilderBase = _create_sprite_sheet_builder(options)
var sprite_sheet_building_result: _SpriteSheetBuilderBase.SpriteSheetBuildingResult = sprite_sheet_builder.build_sprite_sheet(frames_images)
if sprite_sheet_building_result.error:
result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result)
return result
var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet
var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new()
var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges()
var frames_duration: float = 1.0 / animation_framerate
var all_frames: Array[_Common.FrameInfo]
all_frames.resize(unique_frames_count)
for animation_index in animations_params_parsing_results.size():
var animation_params_parsing_result: AnimationParamsParsingResult = animations_params_parsing_results[animation_index]
var animation = _Common.AnimationInfo.new()
animation.name = animation_params_parsing_result.name
if animation.name == autoplay_animation_name:
animation_library.autoplay_index = animation_index
animation.direction = animation_params_parsing_result.direction
if animation.direction < 0:
animation.direction = _Common.AnimationDirection.FORWARD
animation.repeat_count = animation_params_parsing_result.repeat_count
if animation.repeat_count < 0:
animation.repeat_count = 1
for animation_frame_index in animation_params_parsing_result.frames_count:
var global_frame_index: int = animation_params_parsing_result.first_frame_index + animation_frame_index
var frame: _Common.FrameInfo = all_frames[global_frame_index]
if frame == null:
frame = _Common.FrameInfo.new()
frame.sprite = sprite_sheet.sprites[global_frame_index]
frame.duration = frames_duration
all_frames[global_frame_index] = frame
animation.frames.push_back(frame)
animation_library.animations.push_back(animation)
if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0:
push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name])
if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error:
push_warning(
"Failed to remove unique temporary directory: \"%s\"" %
[unique_temp_dir_path])
result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library)
return result
class CustomImageFormatLoaderExtension:
extends ImageFormatLoaderExtension
var __recognized_extensions: PackedStringArray
var __os_command_setting: _Setting
var __os_command_arguments_setting: _Setting
var __common_temporary_files_directory_path_setting: _Setting
func _init(recognized_extensions: PackedStringArray,
os_command_setting: _Setting,
os_command_arguments_setting: _Setting,
common_temporary_files_directory_path: _Setting,
) -> void:
__recognized_extensions = recognized_extensions
__os_command_setting = os_command_setting
__os_command_arguments_setting = os_command_arguments_setting
__common_temporary_files_directory_path_setting = common_temporary_files_directory_path
func _get_recognized_extensions() -> PackedStringArray:
return __recognized_extensions
func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error:
var err: Error
var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value()
if os_command_result.error:
push_error(os_command_result.error_description)
return os_command_result.error
var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value()
if os_command_arguments_result.error:
push_error(os_command_arguments_result.error_description)
return os_command_arguments_result.error
var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value()
if temp_dir_path_result.error:
push_error("Failed to get Temporary Files Directory Path to export spritesheet")
return temp_dir_path_result.error
var global_temp_dir_path: String = ProjectSettings.globalize_path(
temp_dir_path_result.value.strip_edges())
var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \
_DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path)
if unique_temp_dir_creation_result.error:
push_error("Failed to create unique temporary directory to export spritesheet")
return unique_temp_dir_creation_result.error
var unique_temp_dir_path: String = unique_temp_dir_creation_result.path
var global_source_file_path: String = ProjectSettings.globalize_path(file_access.get_path())
const png_base_name: String = "img"
var global_temp_png_path: String = unique_temp_dir_path.path_join("%s.png" % png_base_name)
var command: String = os_command_result.value.strip_edges()
var arguments: PackedStringArray = \
os_command_arguments_result.value + \
PackedStringArray([
"--export", global_temp_png_path,
"--start", 1,
"--end", 1,
"--transparency",
global_source_file_path])
var output: Array
var exit_code: int = OS.execute(command, arguments, output, true, false)
if exit_code:
for arg_index in arguments.size():
arguments[arg_index] = "\nArgument: " + arguments[arg_index]
push_error(" ".join([
"An error occurred while executing the Pencil2D command.",
"Process exited with code %s:\nCommand: %s%s"
]) % [exit_code, command, "".join(arguments)])
return ERR_QUERY_FAILED
var global_frame_png_path: String = unique_temp_dir_path \
.path_join("%s0001.png" % [png_base_name])
err = image.load_png_from_buffer(FileAccess.get_file_as_bytes(global_frame_png_path))
if err:
push_error("An error occurred while image loading")
return err
if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error:
push_warning(
"Failed to remove unique temporary directory: \"%s\"" %
[unique_temp_dir_path])
return OK

View File

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

View File

@@ -0,0 +1,149 @@
@tool
extends "_.gd"
const __ANIMATIONS_PARAMETERS_OPTION: StringName = "piskel/animations_parameters"
func _init(editor_file_system: EditorFileSystem) -> void:
var recognized_extensions: PackedStringArray = ["piskel"]
super("Piskel", recognized_extensions, [
_Options.create_option(__ANIMATIONS_PARAMETERS_OPTION, PackedStringArray(),
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT)],
[],
CustomImageFormatLoaderExtension.new(recognized_extensions))
func _export(res_source_file_path: String, options: Dictionary) -> ExportResult:
var result: ExportResult = ExportResult.new()
var raw_animations_params_list: PackedStringArray = options[__ANIMATIONS_PARAMETERS_OPTION]
var animations_params_parsing_results: Array[AnimationParamsParsingResult]
animations_params_parsing_results.resize(raw_animations_params_list.size())
var unique_animations_names: PackedStringArray
var frame_indices_to_export
var unique_frames_count: int = 0
var animation_first_frame_index: int = 0
for animation_index in raw_animations_params_list.size():
var raw_animation_params: String = raw_animations_params_list[animation_index]
var animation_params_parsing_result: AnimationParamsParsingResult = _parse_animation_params(
raw_animation_params,
AnimationOptions.FramesCount | AnimationOptions.Direction | AnimationOptions.RepeatCount,
animation_first_frame_index)
if animation_params_parsing_result.error:
result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters", animation_params_parsing_result)
return result
if unique_animations_names.has(animation_params_parsing_result.name):
result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" %
[animation_params_parsing_result.name, animation_index])
return result
unique_animations_names.push_back(animation_params_parsing_result.name)
unique_frames_count += animation_params_parsing_result.frames_count
animation_first_frame_index += animation_params_parsing_result.frames_count
animations_params_parsing_results[animation_index] = animation_params_parsing_result
var document: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(res_source_file_path))
document.modelVersion #int 2
var piskel: Dictionary = document.piskel
piskel.name #string New Piskel
piskel.description #string asdfasdfasdf
piskel.fps #int 12,
var image_size: Vector2i = Vector2i(piskel.width, piskel.height)
# piskel.hiddenFrames#Array may absend
var blended_layers: Image
var layer_image: Image = Image.new()
var frames_count: int
var layer_image_size: Vector2i = image_size
for layer_string in piskel.layers: #Array
var layer: Dictionary = JSON.parse_string(layer_string)
layer.name #string layer 1
layer.opacity #float 1
if frames_count == 0:
frames_count = layer.frameCount
layer_image_size.x = image_size.x * frames_count
else:
assert(frames_count == layer.frameCount)
for chunk in layer.chunks:
# chunk.layout # array [ [ 0 ], [ 1 ], [ 2 ] ]
layer_image.load_png_from_buffer(Marshalls.base64_to_raw(chunk.base64PNG.trim_prefix("data:image/png;base64,")))
assert(layer_image.get_size() == layer_image_size)
if blended_layers == null:
blended_layers = layer_image
layer_image = Image.new()
else:
blended_layers.blend_rect(layer_image, Rect2i(Vector2i.ZERO, layer_image.get_size()), Vector2i.ZERO)
var frames_images: Array[Image]
frames_images.resize(frames_count)
for frame_index in frames_count:
frames_images[frame_index] = blended_layers.get_region(
Rect2i(Vector2i(frame_index * image_size.x, 0), image_size))
var sprite_sheet_builder: _SpriteSheetBuilderBase = _create_sprite_sheet_builder(options)
var sprite_sheet_building_result: _SpriteSheetBuilderBase.SpriteSheetBuildingResult = \
sprite_sheet_builder.build_sprite_sheet(frames_images)
if sprite_sheet_building_result.error:
result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result)
return result
var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet
var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new()
var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges()
var frames_duration: float = 1.0 / piskel.fps
var all_frames: Array[_Common.FrameInfo]
all_frames.resize(unique_frames_count)
for animation_index in animations_params_parsing_results.size():
var animation_params_parsing_result: AnimationParamsParsingResult = animations_params_parsing_results[animation_index]
var animation = _Common.AnimationInfo.new()
animation.name = animation_params_parsing_result.name
if animation.name == autoplay_animation_name:
animation_library.autoplay_index = animation_index
animation.direction = animation_params_parsing_result.direction
if animation.direction < 0:
animation.direction = _Common.AnimationDirection.FORWARD
animation.repeat_count = animation_params_parsing_result.repeat_count
if animation.repeat_count < 0:
animation.repeat_count = 1
for animation_frame_index in animation_params_parsing_result.frames_count:
var global_frame_index: int = animation_params_parsing_result.first_frame_index + animation_frame_index
var frame: _Common.FrameInfo = all_frames[global_frame_index]
if frame == null:
frame = _Common.FrameInfo.new()
frame.sprite = sprite_sheet.sprites[global_frame_index]
frame.duration = frames_duration
all_frames[global_frame_index] = frame
animation.frames.push_back(frame)
animation_library.animations.push_back(animation)
if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0:
push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name])
result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library)
return result
class CustomImageFormatLoaderExtension:
extends ImageFormatLoaderExtension
var __recognized_extensions: PackedStringArray
func _init(recognized_extensions: PackedStringArray) -> void:
__recognized_extensions = recognized_extensions
func _get_recognized_extensions() -> PackedStringArray:
return __recognized_extensions
func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error:
var document: Dictionary = JSON.parse_string(file_access.get_as_text())
var piskel: Dictionary = document.piskel
var image_size: Vector2i = Vector2i(piskel.width, piskel.height)
var image_rect: Rect2i = Rect2i(Vector2i.ZERO, image_size)
image.set_data(1, 1, false, Image.FORMAT_RGBA8, [0, 0, 0, 0])
image.resize(image_size.x, image_size.y)
var layer_image: Image = Image.new()
for layer_string in piskel.layers: #Array
var layer: Dictionary = JSON.parse_string(layer_string)
layer.opacity #float 1
for chunk in layer.chunks:
# chunk.layout # array [ [ 0 ], [ 1 ], [ 2 ] ]
layer_image.load_png_from_buffer(Marshalls.base64_to_raw(chunk.base64PNG.trim_prefix("data:image/png;base64,")))
image.blend_rect(layer_image, image_rect, Vector2i.ZERO)
return OK

View File

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

View File

@@ -0,0 +1,225 @@
@tool
extends "_.gd"
enum PxoLayerType {
PIXEL_LAYER = 0,
GROUP_LAYER = 1,
LAYER_3D = 2,
}
func _init(editor_file_system: EditorFileSystem) -> void:
var recognized_extensions: PackedStringArray = ["pxo"]
super("Pixelorama", recognized_extensions, [
], [
# settings
], CustomImageFormatLoaderExtension.new(recognized_extensions))
func _export(res_source_file_path: String, options: Dictionary) -> ExportResult:
var result: ExportResult = ExportResult.new()
var file: FileAccess = FileAccess.open_compressed(res_source_file_path, FileAccess.READ, FileAccess.COMPRESSION_ZSTD)
if file == null or file.get_open_error() == ERR_FILE_UNRECOGNIZED:
file = FileAccess.open(res_source_file_path, FileAccess.READ)
if file == null:
result.fail(ERR_FILE_CANT_OPEN, "Failed to open file with unknown error")
return result
var open_error: Error = file.get_open_error()
if open_error:
result.fail(ERR_FILE_CANT_OPEN, "Failed to open file with error: %s \"%s\"" % [open_error, error_string(open_error)])
return result
var first_line: String = file.get_line()
var images_data: PackedByteArray = file.get_buffer(file.get_length() - file.get_position())
file.close()
var pxo_project: Dictionary = JSON.parse_string(first_line)
var image_size: Vector2i = Vector2i(pxo_project.size_x, pxo_project.size_y)
var pxo_cel_image_buffer_size: int = image_size.x * image_size.y * 4
var pxo_cel_image_buffer_offset: int
var pxo_cel_image: Image = Image.create(image_size.x, image_size.y, false, Image.FORMAT_RGBA8)
var pixel_layers_count: int
for pxo_layer in pxo_project.layers:
if pxo_layer.type == PxoLayerType.PIXEL_LAYER:
pixel_layers_count += 1
var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges()
var unique_frames_indices_by_frame_index: Dictionary
var unique_frames: Array[_Common.FrameInfo]
var unique_frames_images: Array[Image]
var unique_frames_count: int
var pixel_layer_index: int
var image_rect: Rect2i = Rect2i(Vector2i.ZERO, image_size)
var frame: _Common.FrameInfo
var is_animation_default: bool = pxo_project.tags.is_empty()
if is_animation_default:
var default_animation_name: String = options[_Options.DEFAULT_ANIMATION_NAME].strip_edges()
if default_animation_name.is_empty():
default_animation_name = "default"
pxo_project.tags.push_back({
name = default_animation_name,
from = 1,
to = pxo_project.frames.size()})
var animations_count: int = pxo_project.tags.size()
var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new()
animation_library.animations.resize(animations_count)
var pxo_cel_opacity: float
var unique_animations_names: PackedStringArray
for animation_index in animations_count:
var pxo_tag: Dictionary = pxo_project.tags[animation_index]
var animation: _Common.AnimationInfo = _Common.AnimationInfo.new()
animation_library.animations[animation_index] = animation
var animation_frames_count: int = pxo_tag.to + 1 - pxo_tag.from
if is_animation_default:
animation.name = pxo_tag.name
if animation.name == autoplay_animation_name:
animation_library.autoplay_index = animation_index
animation.direction = options[_Options.DEFAULT_ANIMATION_DIRECTION]
animation.repeat_count = options[_Options.DEFAULT_ANIMATION_REPEAT_COUNT]
else:
var animation_params_parsing_result: AnimationParamsParsingResult = _parse_animation_params(
pxo_tag.name, AnimationOptions.Direction | AnimationOptions.RepeatCount,
pxo_tag.from, animation_frames_count)
if animation_params_parsing_result.error:
result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters",
animation_params_parsing_result)
return result
if unique_animations_names.has(animation_params_parsing_result.name):
result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" %
[animation_params_parsing_result.name, animation_index])
return result
unique_animations_names.push_back(animation_params_parsing_result.name)
animation.name = animation_params_parsing_result.name
if animation.name == autoplay_animation_name:
animation_library.autoplay_index = animation_index
animation.direction = animation_params_parsing_result.direction
if animation.direction < 0:
animation.direction = _Common.AnimationDirection.FORWARD
animation.repeat_count = animation_params_parsing_result.repeat_count
if animation.repeat_count < 0:
animation.repeat_count = 1
animation.frames.resize(animation_frames_count)
var frame_image: Image
for animation_frame_index in animation_frames_count:
var frame_index: int = pxo_tag.from - 1 + animation_frame_index
var unique_frame_index: int = unique_frames_indices_by_frame_index.get(frame_index, -1)
if unique_frame_index >= 0:
frame = unique_frames[unique_frame_index]
else:
frame = _Common.FrameInfo.new()
unique_frames.push_back(frame)
frame_image = Image.create(image_size.x, image_size.y, false, Image.FORMAT_RGBA8)
unique_frames_images.push_back(frame_image)
unique_frame_index = unique_frames_count
unique_frames_count += 1
pixel_layer_index = -1
var pxo_frame: Dictionary = pxo_project.frames[frame_index]
frame.duration = pxo_frame.duration / pxo_project.fps
for cel_index in pxo_frame.cels.size():
var pxo_cel = pxo_frame.cels[cel_index]
var pxo_layer = pxo_project.layers[cel_index]
if pxo_layer.type == PxoLayerType.PIXEL_LAYER:
pixel_layer_index += 1
var l: Dictionary = pxo_layer
while l.parent >= 0 and pxo_layer.visible:
if not l.visible:
pxo_layer.visible = false
break
l = pxo_project.layers[l.parent]
pxo_cel_opacity = pxo_cel.opacity
if not pxo_layer.visible or pxo_cel_opacity == 0:
continue
pxo_cel_image_buffer_offset = pxo_cel_image_buffer_size * \
(pixel_layers_count * frame_index + pixel_layer_index)
var pxo_cel_image_buffer: PackedByteArray = images_data.slice(
pxo_cel_image_buffer_offset,
pxo_cel_image_buffer_offset + pxo_cel_image_buffer_size)
for alpha_index in range(3, pxo_cel_image_buffer_size, 4):
pxo_cel_image_buffer[alpha_index] = roundi(pxo_cel_image_buffer[alpha_index] * pxo_cel_opacity)
pxo_cel_image.set_data(image_size.x, image_size.y, false, Image.FORMAT_RGBA8, pxo_cel_image_buffer)
frame_image.blend_rect(pxo_cel_image, image_rect, Vector2i.ZERO)
unique_frames_indices_by_frame_index[frame_index] = unique_frame_index
animation.frames[animation_frame_index] = frame
if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0:
push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name])
var sprite_sheet_builder: _SpriteSheetBuilderBase = _create_sprite_sheet_builder(options)
var sprite_sheet_building_result: _SpriteSheetBuilderBase.SpriteSheetBuildingResult = \
sprite_sheet_builder.build_sprite_sheet(unique_frames_images)
if sprite_sheet_building_result.error:
result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result)
return result
var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet
for unique_frame_index in unique_frames_count:
var unique_frame: _Common.FrameInfo = unique_frames[unique_frame_index]
unique_frame.sprite = sprite_sheet.sprites[unique_frame_index]
result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library)
return result
class CustomImageFormatLoaderExtension:
extends ImageFormatLoaderExtension
var __recognized_extensions: PackedStringArray
func _init(recognized_extensions: PackedStringArray) -> void:
__recognized_extensions = recognized_extensions
func _get_recognized_extensions() -> PackedStringArray:
return __recognized_extensions
func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error:
var file: FileAccess = FileAccess.open_compressed(file_access.get_path(), FileAccess.READ, FileAccess.COMPRESSION_ZSTD)
if file == null or file.get_open_error() == ERR_FILE_UNRECOGNIZED:
file = FileAccess.open(file_access.get_path(), FileAccess.READ)
if file == null:
push_error("Failed to open file with unknown error")
return ERR_FILE_CANT_OPEN
var open_error: Error = file.get_open_error()
if open_error:
push_error("Failed to open file with error: %s \"%s\"" % [open_error, error_string(open_error)])
return ERR_FILE_CANT_OPEN
var first_line: String = file.get_line()
var pxo_project: Dictionary = JSON.parse_string(first_line)
var image_size: Vector2i = Vector2i(pxo_project.size_x, pxo_project.size_y)
var pxo_cel_image_buffer_size: int = image_size.x * image_size.y * 4
var pxo_cel_image_buffer_offset: int
var pxo_cel_image: Image = Image.create(image_size.x, image_size.y, false, Image.FORMAT_RGBA8)
var pixel_layer_index: int = -1
image.set_data(1, 1, false, Image.FORMAT_RGBA8, [0, 0, 0, 0])
image.resize(image_size.x, image_size.y)
var image_rect: Rect2i = Rect2i(Vector2i.ZERO, image_size)
for layer_index in pxo_project.layers.size():
var pxo_layer: Dictionary = pxo_project.layers[layer_index]
if pxo_layer.type != PxoLayerType.PIXEL_LAYER:
continue
pixel_layer_index += 1
var l: Dictionary = pxo_layer
while l.parent >= 0 and pxo_layer.visible:
if not l.visible:
pxo_layer.visible = false
break
l = pxo_project.layers[l.parent]
if not pxo_layer.visible:
continue
var pxo_cel: Dictionary = pxo_project.frames[0].cels[layer_index]
var pxo_cel_opacity = pxo_cel.opacity
if pxo_cel_opacity == 0:
continue
pxo_cel_image_buffer_offset = pxo_cel_image_buffer_size * layer_index
var pxo_cel_image_buffer: PackedByteArray = file.get_buffer(pxo_cel_image_buffer_size)
for alpha_index in range(3, pxo_cel_image_buffer_size, 4):
pxo_cel_image_buffer[alpha_index] = roundi(pxo_cel_image_buffer[alpha_index] * pxo_cel_opacity)
pxo_cel_image.set_data(image_size.x, image_size.y, false, Image.FORMAT_RGBA8, pxo_cel_image_buffer)
image.blend_rect(pxo_cel_image, image_rect, Vector2i.ZERO)
file.close()
return OK

View File

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

View File

@@ -0,0 +1,10 @@
extends RefCounted
const SpriteSheetLayout = preload("../common.gd").SpriteSheetLayout
const EdgesArtifactsAvoidanceMethod = preload("../common.gd").EdgesArtifactsAvoidanceMethod
const AnimationDirection = preload("../common.gd").AnimationDirection
const SpriteInfo = preload("../common.gd").SpriteInfo
const SpriteSheetInfo = preload("../common.gd").SpriteSheetInfo
const FrameInfo = preload("../common.gd").FrameInfo
const AnimationInfo = preload("../common.gd").AnimationInfo
const AnimationLibraryInfo = preload("../common.gd").AnimationLibraryInfo

View File

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

View File

@@ -0,0 +1,18 @@
extends "_.gd"
class Context:
extends RefCounted
var atlas_image: Image
var sprite_sheet: SpriteSheetInfo
var animation_library: AnimationLibraryInfo
var gen_files_to_add: PackedStringArray
var middle_import_data: Variant
static func modify_context(
res_source_file_path: String,
res_save_file_path: String,
editor_import_plugin: EditorImportPlugin,
editor_file_system: EditorFileSystem,
options: Dictionary,
context: Context) -> Error:
return OK

View File

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

View File

@@ -0,0 +1,125 @@
extends "res://addons/nklbdev.importality/external_scripts/middle_import_script_base.gd"
static func modify_context(
# Path to the source file from which the import is performed
res_source_file_path: String,
# Path to save imported resource file
res_save_file_path: String,
# EditorImportPlugin instance to call append_import_external_resource
# or other methods
editor_import_plugin: EditorImportPlugin,
# EditorFileSystem instance to call update_file method
editor_file_system: EditorFileSystem,
# Import options
options: Dictionary,
# Context-object to modify
context: Context) -> Error:
# ------------------------------------------------
# You can modify or replace objects in context fields.
# (Be careful not to shoot yourself in the foot!)
# ------------------------------------------------
#
# context.atlas_image: Image
# The image that will be saved as a PNG file next to the original file
# and automatically imported by the engine into a resource
# that will be used as an atlas
#
# context.sprite_sheet: SpriteSheetInfo
# Sprite sheet data. Stores source image size and sprites data (SpriteInfo)
#
# context.animation_library: AnimationLibraryInfo
# Animations data. Uses sprites data (SpriteInfo) stored in context.sprite_sheet
#
# gen_files_to_add: PackedStringArray
# Gen-files paths to add to gen_files array of import-function
#
# context.middle_import_data: Variant
# Your custom data to use in the post-import script
# You can save your new resources directly in .godot/import folder
# in *.res or *.tres formats.
#
# But with images the situation is somewhat more complicated.
# You can embed your image into the main importing resource,
# but this will take up a lot of memory space and it will not be optimized
# because Godot can only create a CompressedTexture2D resource on its own,
# and only as a separated *.ctex file.
# You cannot save an image in *.ctex format yourself. Sad but true.
#
# In this case you need to save the image in the main resource file system
# as a file in supported graphics format:
# bmp, dds, exr, hdr, jpg/jpeg, png, tga, svg/svgz or webp.
#
# When Godot detects changes in the file system, the image will be imported.
# This will happen a little later.
# If you want to immediately use the texture from this image during
# the current import process, you need to force the engine to import
# this file right now. Do something like this:
# ------------------------------------------------
#var my_new_image: Image = Image.new()
#my_new_image.create(32, 32, false, Image.FORMAT_RGBA8)
#my_new_image.fill(Color.WHITE)
#var my_new_texture_path: String = "res://my_new_texture.png"
#var error: Error
#
## 1. Save your image
#error = my_new_image.save_png(my_new_texture_path)
#if error: # Do not forget to handle errors!
# push_error("Failed to save my image!")
# return error
#
## 2. Update file in resource filesystem before loading it with ResourceLoader
## You need this because the resource created from the image does not yet exist.
## This will force the engine to import the image, and the resource
## (Texture2D, BitMap or other) created from it will be available at this path.
# editor_file_system.update_file(my_new_texture_path)
#
## 3. Append path to your resource. After this,
## the resource will be available for download via ResourceLoader
#error = editor_import_plugin.append_import_external_resource(my_new_texture_path)
#if error: # Do not forget to handle errors!
#push_error("Failed to append import my image as external resource!")
#return error
#
## Add the path to your resource to the list of generated files
## so that the engine will establish a dependency between
## the main imported resource and your new separated resource.
#context.gen_files_to_add.push_back(my_new_texture_path)
#
## Hooray, your image has been imported and you can get
## the Texture2D resource from this path using ResourceLoader!
## You can now use this resource inside the main importing resource
## without the need for embedding.
#var my_new_texture: Texture2D = ResourceLoader.load(my_new_texture_path, "Texture2D", ResourceLoader.CACHE_MODE_IGNORE)
box_blur(context.atlas_image)
grayscale(context.atlas_image)
return OK
static func box_blur(image: Image) -> void:
var image_copy = image.duplicate()
var image_size: Vector2i = image.get_size()
for y in range(1, image_size.y - 1): for x in range(1, image_size.x - 1):
# Set P to the average of 9 pixels:
# X X X
# X P X
# X X X
image.set_pixel(x, y, (
image_copy.get_pixel(x - 1, y + 1) + # Top left
image_copy.get_pixel(x + 0, y + 1) + # Top center
image_copy.get_pixel(x + 1, y + 1) + # Top right
image_copy.get_pixel(x - 1, y + 0) + # Mid left
image_copy.get_pixel(x + 0, y + 0) + # Current pixel
image_copy.get_pixel(x + 1, y + 0) + # Mid right
image_copy.get_pixel(x - 1, y - 1) + # Low left
image_copy.get_pixel(x + 0, y - 1) + # Low center
image_copy.get_pixel(x + 1, y - 1) # Low right
) / 9.0)
static func grayscale(image: Image) -> void:
var image_size: Vector2i = image.get_size()
for y in image_size.y: for x in image_size.x:
var pixel_color: Color = image.get_pixel(x, y)
var luminance: float = pixel_color.get_luminance()
image.set_pixel(x, y, Color(luminance, luminance, luminance, pixel_color.a));

View File

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

View File

@@ -0,0 +1,19 @@
extends "_.gd"
class Context:
extends RefCounted
var resource: Resource
var resource_saver_flags: ResourceSaver.SaverFlags
var save_extension: String
var gen_files_to_add: PackedStringArray
static func modify_context(
res_source_file_path: String,
res_save_file_path: String,
editor_import_plugin: EditorImportPlugin,
editor_file_system: EditorFileSystem,
options: Dictionary,
middle_import_data: Variant,
context: Context,
) -> Error:
return OK

View File

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

View File

@@ -0,0 +1,42 @@
extends "res://addons/nklbdev.importality/external_scripts/post_import_script_base.gd"
static func modify_context(
# Path to the source file from which the import is performed
res_source_file_path: String,
# Path to save imported resource file
res_save_file_path: String,
# EditorImportPlugin instance to call append_import_external_resource
# or other methods
editor_import_plugin: EditorImportPlugin,
# EditorFileSystem instance to call update_file method
editor_file_system: EditorFileSystem,
# Import options
options: Dictionary,
# Your custom data from middle-import script
middle_import_data: Variant,
# Context-object to modify
context: Context,
) -> Error:
# ------------------------------------------------
# You can modify or replace objects in context fields.
# (Be careful not to shoot yourself in the foot!)
# ------------------------------------------------
#
# resource: Resource
# A save-ready resource that you can modify or replace as you wish
#
# resource_saver_flags: ResourceSaver.SaverFlags
# Resource save flags for use in ResourceSaver.save method
#
# gen_files_to_add: PackedStringArray
# Gen-files paths to add to gen_files array of import-function
#
# save_extension: String
# Save resource file extension
var animated_sprite_2d: AnimatedSprite2D = (context.resource as PackedScene).instantiate() as AnimatedSprite2D
animated_sprite_2d.modulate = Color.RED
var packed_scene = PackedScene.new()
packed_scene.pack(animated_sprite_2d)
context.resource = packed_scene
return OK

View File

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

View File

@@ -0,0 +1,70 @@
@tool
extends RefCounted
const _Result = preload("../result.gd").Class
const _Common = preload("../common.gd")
const _Options = preload("../options.gd")
class ImportResult:
extends _Result
var resource: Resource
var resource_saver_flags: ResourceSaver.SaverFlags
func _get_result_type_description() -> String:
return "Import"
func success(
resource: Resource,
resource_saver_flags: ResourceSaver.SaverFlags = ResourceSaver.FLAG_NONE
) -> void:
_success()
self.resource = resource
self.resource_saver_flags = resource_saver_flags
var __options: Array[Dictionary] = [
_Options.create_option(_Options.DEFAULT_ANIMATION_NAME, "default",
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT),
_Options.create_option(_Options.DEFAULT_ANIMATION_DIRECTION, _Common.AnimationDirection.FORWARD,
PROPERTY_HINT_ENUM, ",".join(_Common.ANIMATION_DIRECTIONS_NAMES), PROPERTY_USAGE_DEFAULT),
_Options.create_option(_Options.DEFAULT_ANIMATION_REPEAT_COUNT, 0,
PROPERTY_HINT_RANGE, "0,,1,or_greater", PROPERTY_USAGE_DEFAULT),
_Options.create_option(_Options.AUTOPLAY_ANIMATION_NAME, "",
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT),
_Options.create_option(_Options.ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED, false,
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT),
]
var __name: String
var __resource_type: StringName
var __save_extension: String
func _init(
name: String,
resource_type: String,
save_extension: String,
options: Array[Dictionary] = []) -> void:
__name = name
__resource_type = resource_type
__save_extension = save_extension
__options.append_array(options)
func get_name() -> String:
return __name
func get_resource_type() -> StringName:
return __resource_type
func get_save_extension() -> String:
return __save_extension
func get_options() -> Array[Dictionary]:
return __options
func import(
source_file_path: String,
atlas: Texture2D,
sprite_sheet: _Common.SpriteSheetInfo,
animation_library: _Common.AnimationLibraryInfo,
options: Dictionary,
save_path: String) -> ImportResult:
assert(false, "This method is abstract and must be overriden.")
var result: ImportResult = ImportResult.new()
result.fail(ERR_UNCONFIGURED, "This method is abstract and must be overriden.")
return result

View File

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

View File

@@ -0,0 +1,14 @@
@tool
extends "_.gd"
func _init(
name: String,
resource_type: String,
save_extension: String,
options: Array[Dictionary] = []
) -> void:
options.append_array([
_Options.create_option(_Options.ROOT_NODE_NAME, "",
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT),
])
super(name, resource_type, save_extension, options)

View File

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

View File

@@ -0,0 +1,64 @@
@tool
extends "_node.gd"
class TrackFrame:
extends RefCounted
var duration: float
var value: Variant
func _init(duration: float, value: Variant) -> void:
self.duration = duration
self.value = value
static func _create_animation_player(
animation_library_info: _Common.AnimationLibraryInfo,
track_value_getters_by_property_path: Dictionary
) -> AnimationPlayer:
var animation_player: AnimationPlayer = AnimationPlayer.new()
animation_player.name = "AnimationPlayer"
var animation_library: AnimationLibrary = AnimationLibrary.new()
for animation_info in animation_library_info.animations:
var animation: Animation = Animation.new()
var frames: Array[_Common.FrameInfo] = animation_info.get_flatten_frames()
for property_path in track_value_getters_by_property_path.keys():
__create_track(animation, property_path,
frames, track_value_getters_by_property_path[property_path])
animation.length = 0
for frame in frames:
animation.length += frame.duration
animation.loop_mode = Animation.LOOP_LINEAR if animation_info.repeat_count == 0 else Animation.LOOP_NONE
animation_library.add_animation(animation_info.name, animation)
animation_player.add_animation_library("", animation_library)
if animation_library_info.autoplay_index >= 0:
animation_player.autoplay = animation_library_info \
.animations[animation_library_info.autoplay_index].name
return animation_player
static func __create_track(
animation: Animation,
property_path: NodePath,
frames: Array[_Common.FrameInfo],
track_value_getter: Callable # func(f: FrameModel) -> Variant for each f in frames
) -> int:
var track_index = animation.add_track(Animation.TYPE_VALUE)
animation.track_set_path(track_index, property_path)
animation.value_track_set_update_mode(track_index, Animation.UPDATE_DISCRETE)
animation.track_set_interpolation_loop_wrap(track_index, false)
animation.track_set_interpolation_type(track_index, Animation.INTERPOLATION_NEAREST)
var track_frames = frames.map(func (frame: _Common.FrameInfo):
return TrackFrame.new(frame.duration, track_value_getter.call(frame)))
var transition: float = 1
var track_length: float = 0
var previous_track_frame: TrackFrame = null
for track_frame in track_frames:
if previous_track_frame == null or track_frame.value != previous_track_frame.value:
animation.track_insert_key(track_index, track_length, track_frame.value, transition)
previous_track_frame = track_frame
track_length += track_frame.duration
return track_index

View File

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

View File

@@ -0,0 +1,27 @@
@tool
extends "_node_with_animation_player.gd"
const ANIMATION_STRATEGIES_NAMES: PackedStringArray = [
"Animate sprite's region and offset",
"Animate single atlas texture's region and margin",
"Animate multiple atlas textures instances",
]
enum AnimationStrategy {
SPRITE_REGION_AND_OFFSET = 0,
SINGLE_ATLAS_TEXTURE_REGION_AND_MARGIN = 1,
MULTIPLE_ATLAS_TEXTURES_INSTANCES = 2,
}
func _init(
name: String,
resource_type: String,
save_extension: String,
options: Array[Dictionary] = []
) -> void:
options.append_array([
_Options.create_option(_Options.ANIMATION_STRATEGY, AnimationStrategy.SPRITE_REGION_AND_OFFSET,
PROPERTY_HINT_ENUM, ",".join(ANIMATION_STRATEGIES_NAMES), PROPERTY_USAGE_DEFAULT),
_Options.create_option(_Options.SPRITE_CENTERED, false,
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT),
])
super(name, resource_type, save_extension, options)

View File

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

View File

@@ -0,0 +1,45 @@
@tool
extends "_node.gd"
const _SpriteFramesImporter = preload("sprite_frames.gd")
var __sprite_frames_importer: _SpriteFramesImporter
func _init() -> void:
super("AnimatedSprite2D", "PackedScene", "scn")
__sprite_frames_importer = _SpriteFramesImporter.new()
func import(
res_source_file_path: String,
atlas: Texture2D,
sprite_sheet: _Common.SpriteSheetInfo,
animation_library: _Common.AnimationLibraryInfo,
options: Dictionary,
save_path: String
) -> ImportResult:
var result: ImportResult = ImportResult.new()
var sprite_frames_import_result: ImportResult = __sprite_frames_importer \
.import(res_source_file_path, atlas, sprite_sheet, animation_library, options, save_path)
if sprite_frames_import_result.error:
return sprite_frames_import_result
var sprite_frames: SpriteFrames = sprite_frames_import_result.resource
var animated_sprite: AnimatedSprite2D = AnimatedSprite2D.new()
var node_name: String = options[_Options.ROOT_NODE_NAME].strip_edges()
animated_sprite.name = res_source_file_path.get_file().get_basename() \
if node_name.is_empty() else node_name
animated_sprite.sprite_frames = sprite_frames
if animation_library.autoplay_index >= 0:
if animation_library.autoplay_index >= animation_library.animations.size():
result.fail(ERR_INVALID_DATA, "Autoplay animation index overflow")
return result
animated_sprite.autoplay = animation_library \
.animations[animation_library.autoplay_index].name
var packed_scene: PackedScene = PackedScene.new()
packed_scene.pack(animated_sprite)
result.success(packed_scene,
ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES)
return result

View File

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

View File

@@ -0,0 +1,42 @@
@tool
extends "_node.gd"
const _SpriteFramesImporter = preload("sprite_frames.gd")
var __sprite_frames_importer: _SpriteFramesImporter
func _init() -> void:
super("AnimatedSprite3D", "PackedScene", "scn")
__sprite_frames_importer = _SpriteFramesImporter.new()
func import(
res_source_file_path: String,
atlas: Texture2D,
sprite_sheet: _Common.SpriteSheetInfo,
animation_library: _Common.AnimationLibraryInfo,
options: Dictionary,
save_path: String
) -> ImportResult:
var result: ImportResult = ImportResult.new()
var sprite_frames_import_result: ImportResult = __sprite_frames_importer \
.import(res_source_file_path, atlas, sprite_sheet, animation_library, options, save_path)
if sprite_frames_import_result.error:
return sprite_frames_import_result
var sprite_frames: SpriteFrames = sprite_frames_import_result.resource
var animated_sprite: AnimatedSprite3D = AnimatedSprite3D.new()
var node_name: String = options[_Options.ROOT_NODE_NAME].strip_edges()
animated_sprite.name = res_source_file_path.get_file().get_basename() \
if node_name.is_empty() else node_name
animated_sprite.sprite_frames = sprite_frames
if animation_library.autoplay_index >= 0:
animated_sprite.autoplay = animation_library \
.animations[animation_library.autoplay_index].name
var packed_scene: PackedScene = PackedScene.new()
packed_scene.pack(animated_sprite)
result.success(packed_scene,
ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES)
return result

View File

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

View File

@@ -0,0 +1,94 @@
@tool
extends "_sprite_with_animation_player.gd"
func _init() -> void:
super("Sprite2D with AnimationPlayer", "PackedScene", "scn")
func import(
res_source_file_path: String,
atlas: Texture2D,
sprite_sheet: _Common.SpriteSheetInfo,
animation_library: _Common.AnimationLibraryInfo,
options: Dictionary,
save_path: String
) -> ImportResult:
var result: ImportResult = ImportResult.new()
var sprite_size: Vector2i = sprite_sheet.source_image_size
var sprite: Sprite2D = Sprite2D.new()
var node_name: String = options[_Options.ROOT_NODE_NAME].strip_edges()
sprite.name = res_source_file_path.get_file().get_basename() \
if node_name.is_empty() else node_name
sprite.centered = options[_Options.SPRITE_CENTERED]
var filter_clip_enabled: bool = options[_Options.ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED]
var animation_player: AnimationPlayer
match options[_Options.ANIMATION_STRATEGY]:
AnimationStrategy.SPRITE_REGION_AND_OFFSET:
sprite.texture = atlas
sprite.region_enabled = true
animation_player = _create_animation_player(animation_library, {
".:offset": func(frame: _Common.FrameInfo) -> Vector2:
return \
Vector2(frame.sprite.offset) - 0.5 * (frame.sprite.region.size - sprite_size) \
if sprite.centered else \
frame.sprite.offset,
".:region_rect": func(frame: _Common.FrameInfo) -> Rect2:
return Rect2(frame.sprite.region) })
AnimationStrategy.SINGLE_ATLAS_TEXTURE_REGION_AND_MARGIN:
var atlas_texture: AtlasTexture = AtlasTexture.new()
atlas_texture.filter_clip = filter_clip_enabled
atlas_texture.resource_local_to_scene = true
atlas_texture.atlas = atlas
atlas_texture.region = Rect2(0, 0, 1, 1)
atlas_texture.margin = Rect2(2, 2, 0, 0)
sprite.texture = atlas_texture
animation_player = _create_animation_player(animation_library, {
".:texture:margin": func(frame: _Common.FrameInfo) -> Rect2:
return \
Rect2(frame.sprite.offset,
sprite_size - frame.sprite.region.size) \
if frame.sprite.region.has_area() else \
Rect2(2, 2, 0, 0),
".:texture:region": func(frame: _Common.FrameInfo) -> Rect2:
return Rect2(frame.sprite.region) if frame.sprite.region.has_area() else Rect2(0, 0, 1, 1) })
AnimationStrategy.MULTIPLE_ATLAS_TEXTURES_INSTANCES:
var atlas_textures: Array[AtlasTexture]
var empty_atlas_texture: AtlasTexture = AtlasTexture.new()
empty_atlas_texture.filter_clip = filter_clip_enabled
empty_atlas_texture.atlas = atlas
empty_atlas_texture.region = Rect2(0, 0, 1, 1)
empty_atlas_texture.margin = Rect2(2, 2, 0, 0)
animation_player = _create_animation_player(animation_library, {
".:texture": func(frame: _Common.FrameInfo) -> Texture2D:
if not frame.sprite.region.has_area():
return empty_atlas_texture
var region: Rect2 = frame.sprite.region
var margin: Rect2 = Rect2(
frame.sprite.offset,
sprite_size - frame.sprite.region.size)
var equivalent_atlas_textures: Array = atlas_textures.filter(
func(t: AtlasTexture) -> bool: return t.margin == margin and t.region == region)
if not equivalent_atlas_textures.is_empty():
return equivalent_atlas_textures.front()
var atlas_texture: AtlasTexture = AtlasTexture.new()
atlas_texture.atlas = atlas
atlas_texture.filter_clip = filter_clip_enabled
atlas_texture.region = region
atlas_texture.margin = margin
atlas_textures.append(atlas_texture)
return atlas_texture})
sprite.add_child(animation_player)
animation_player.owner = sprite
var packed_scene: PackedScene = PackedScene.new()
packed_scene.pack(sprite)
result.success(packed_scene,
ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES)
return result

View File

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

View File

@@ -0,0 +1,97 @@
@tool
extends "_sprite_with_animation_player.gd"
func _init() -> void:
super("Sprite3D with AnimationPlayer", "PackedScene", "scn")
func import(
res_source_file_path: String,
atlas: Texture2D,
sprite_sheet: _Common.SpriteSheetInfo,
animation_library: _Common.AnimationLibraryInfo,
options: Dictionary,
save_path: String
) -> ImportResult:
var result: ImportResult = ImportResult.new()
var sprite_size: Vector2i = sprite_sheet.source_image_size
var sprite: Sprite3D = Sprite3D.new()
var node_name: String = options[_Options.ROOT_NODE_NAME].strip_edges()
sprite.name = res_source_file_path.get_file().get_basename() \
if node_name.is_empty() else node_name
sprite.centered = options[_Options.SPRITE_CENTERED]
var filter_clip_enabled: bool = options[_Options.ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED]
var animation_player: AnimationPlayer
match options[_Options.ANIMATION_STRATEGY]:
AnimationStrategy.SPRITE_REGION_AND_OFFSET:
sprite.texture = atlas
sprite.region_enabled = true
animation_player = _create_animation_player(animation_library, {
".:offset": func(frame: _Common.FrameInfo) -> Vector2:
return Vector2( # spatial sprite offset (the Y-axis is Up-directed)
frame.sprite.offset.x,
sprite_size.y - frame.sptite.offset.y -
frame.sprite.region.size.y) + \
# add center correction
((Vector2(frame.sprite.region.size - sprite_size) * 0.5)
if sprite.centered else Vector2.ZERO),
".:region_rect": func(frame: _Common.FrameInfo) -> Rect2:
return Rect2(frame.sprite.region) })
AnimationStrategy.SINGLE_ATLAS_TEXTURE_REGION_AND_MARGIN:
var atlas_texture: AtlasTexture = AtlasTexture.new()
atlas_texture.filter_clip = filter_clip_enabled
atlas_texture.resource_local_to_scene = true
atlas_texture.atlas = atlas
atlas_texture.region = Rect2(0, 0, 1, 1)
atlas_texture.margin = Rect2(2, 2, 0, 0)
sprite.texture = atlas_texture
animation_player = _create_animation_player(animation_library, {
".:texture:margin": func(frame: _Common.FrameInfo) -> Rect2:
return \
Rect2(frame.sprite.offset,
sprite_size - frame.sprite.region.size) \
if frame.sprite.region.has_area() else \
Rect2(2, 2, 0, 0),
".:texture:region": func(frame: _Common.FrameInfo) -> Rect2:
return Rect2(frame.sprite.region) if frame.sprite.region.has_area() else Rect2(0, 0, 1, 1) })
AnimationStrategy.MULTIPLE_ATLAS_TEXTURES_INSTANCES:
var atlas_textures: Array[AtlasTexture]
var empty_atlas_texture: AtlasTexture = AtlasTexture.new()
empty_atlas_texture.filter_clip = filter_clip_enabled
empty_atlas_texture.atlas = atlas
empty_atlas_texture.region = Rect2(0, 0, 1, 1)
empty_atlas_texture.margin = Rect2(2, 2, 0, 0)
animation_player = _create_animation_player(animation_library, {
".:texture": func(frame: _Common.FrameInfo) -> Texture2D:
if not frame.sprite.region.has_area():
return empty_atlas_texture
var region: Rect2 = frame.sprite.region
var margin: Rect2 = Rect2(
frame.sprite.offset,
sprite_size - frame.sprite.region.size)
var equivalent_atlas_textures: Array = atlas_textures.filter(
func(t: AtlasTexture) -> bool: return t.margin == margin and t.region == region)
if not equivalent_atlas_textures.is_empty():
return equivalent_atlas_textures.front()
var atlas_texture: AtlasTexture = AtlasTexture.new()
atlas_texture.atlas = atlas
atlas_texture.filter_clip = filter_clip_enabled
atlas_texture.region = region
atlas_texture.margin = margin
atlas_textures.append(atlas_texture)
return atlas_texture})
sprite.add_child(animation_player)
animation_player.owner = sprite
var packed_scene: PackedScene = PackedScene.new()
packed_scene.pack(sprite)
result.success(packed_scene,
ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES)
return result

View File

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

View File

@@ -0,0 +1,64 @@
@tool
extends "_.gd"
func _init() -> void: super("SpriteFrames", "SpriteFrames", "res")
func import(
res_source_file_path: String,
atlas: Texture2D,
sprite_sheet: _Common.SpriteSheetInfo,
animation_library: _Common.AnimationLibraryInfo,
options: Dictionary,
save_path: String
) -> ImportResult:
var result: ImportResult = ImportResult.new()
var sprite_frames: SpriteFrames = SpriteFrames.new()
for animation_name in sprite_frames.get_animation_names():
sprite_frames.remove_animation(animation_name)
var filter_clip_enabled: bool = options[_Options.ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED]
var atlas_textures: Array[AtlasTexture]
var empty_atlas_texture: AtlasTexture
for animation in animation_library.animations:
sprite_frames.add_animation(animation.name)
sprite_frames.set_animation_loop(animation.name, animation.repeat_count == 0)
sprite_frames.set_animation_speed(animation.name, 1)
var previous_texture: Texture2D
for frame in animation.get_flatten_frames():
var atlas_texture: AtlasTexture
if frame.sprite.region.has_area():
var region: Rect2 = frame.sprite.region
var margin: Rect2 = Rect2(
frame.sprite.offset,
sprite_sheet.source_image_size - frame.sprite.region.size)
var equivalent_atlas_textures: Array = atlas_textures.filter(
func(t: AtlasTexture) -> bool: return t.margin == margin and t.region == region)
if not equivalent_atlas_textures.is_empty():
atlas_texture = equivalent_atlas_textures.front()
if atlas_texture == null:
atlas_texture = AtlasTexture.new()
atlas_texture.filter_clip = filter_clip_enabled
atlas_texture.atlas = atlas
atlas_texture.region = region
atlas_texture.margin = margin
atlas_textures.push_back(atlas_texture)
else:
if empty_atlas_texture == null:
empty_atlas_texture = AtlasTexture.new()
empty_atlas_texture.filter_clip = filter_clip_enabled
empty_atlas_texture.atlas = atlas
empty_atlas_texture.region = Rect2(0, 0, 1, 1)
empty_atlas_texture.margin = Rect2(2, 2, 0, 0)
atlas_texture = empty_atlas_texture
if atlas_texture == previous_texture:
var last_frame_index: int = sprite_frames.get_frame_count(animation.name) - 1
sprite_frames.set_frame(animation.name, last_frame_index, atlas_texture,
sprite_frames.get_frame_duration(animation.name, last_frame_index) + frame.duration)
continue
sprite_frames.add_frame(animation.name, atlas_texture, frame.duration)
previous_texture = atlas_texture
result.success(sprite_frames,
ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES)
return result

View File

@@ -0,0 +1 @@
uid://7divemm1kjo6

View File

@@ -0,0 +1,83 @@
@tool
extends "_.gd"
const __ANIMATION_MERGE_EQUAL_CONSEQUENT_FRAMES_OPTION: StringName = "animation/merge_equal_sonsequent_frames"
const __ANIMATION_FLATTEN_REPETITION_OPTION: StringName = "animation/flatten_repetition"
func _init() -> void: super("Sprite sheet (JSON)", "JSON", "res", [
_Options.create_option(__ANIMATION_MERGE_EQUAL_CONSEQUENT_FRAMES_OPTION, true,
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT),
_Options.create_option(__ANIMATION_FLATTEN_REPETITION_OPTION, true,
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT),
])
func import(
res_source_file_path: String,
atlas: Texture2D,
sprite_sheet: _Common.SpriteSheetInfo,
animation_library: _Common.AnimationLibraryInfo,
options: Dictionary,
save_path: String
) -> ImportResult:
var result: ImportResult = ImportResult.new()
var unique_indixes_by_sprites: Dictionary
var unique_sprite_index: int = 0
var sprites: Array[Dictionary]
for sprite in sprite_sheet.sprites:
if not unique_indixes_by_sprites.has(sprite):
unique_indixes_by_sprites[sprite] = unique_sprite_index
sprites.push_back({
region = sprite.region,
offset = sprite.offset
})
unique_sprite_index += 1
var flatten_animation_repetition: bool = options[__ANIMATION_FLATTEN_REPETITION_OPTION]
var merge_equal_consequent_frames: bool = options[__ANIMATION_MERGE_EQUAL_CONSEQUENT_FRAMES_OPTION]
var animations: Array[Dictionary]
for animation in animation_library.animations:
var frames_data: Array[Dictionary]
var frames: Array[_Common.FrameInfo] = \
animation.get_flatten_frames() \
if flatten_animation_repetition else \
animation.frames
var previous_sprite_index: int = -1
for frame in frames:
var sprite_index: int = unique_indixes_by_sprites[frame.sprite]
if merge_equal_consequent_frames and sprite_index == previous_sprite_index:
frames_data.back().duration += frame.duration
else:
frames_data.push_back({
sprite_index = sprite_index,
duration = frame.duration,
})
previous_sprite_index = sprite_index
animations.push_back({
name = animation.name,
direction =
_Common.AnimationDirection.FORWARD
if flatten_animation_repetition else
animation.direction,
repeat_count =
mini(1, animation.repeat_count)
if flatten_animation_repetition else
animation.repeat_count,
frames = frames_data,
})
var json: JSON = JSON.new()
json.data = {
sprite_sheet = {
atlas = atlas,
source_image_size = sprite_sheet.source_image_size,
sprites = sprites,
},
animation_library = {
animations = animations,
autoplay_index = animation_library.autoplay_index,
},
}
json.get_parsed_text()
result.success(json, ResourceSaver.FLAG_COMPRESS)
return result

View File

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

View File

@@ -0,0 +1,98 @@
@tool
extends "_node_with_animation_player.gd"
const ANIMATION_STRATEGIES_NAMES: PackedStringArray = [
"Animate single atlas texture's region and margin",
"Animate multiple atlas textures instances",
]
enum AnimationStrategy {
SINGLE_ATLAS_TEXTURE_REGION_AND_MARGIN = 1,
MULTIPLE_ATLAS_TEXTURES_INSTANCES = 2
}
func _init() -> void:
super("TextureRect with AnimationPlayer", "PackedScene", "scn", [
_Options.create_option(_Options.ANIMATION_STRATEGY, AnimationStrategy.SINGLE_ATLAS_TEXTURE_REGION_AND_MARGIN,
PROPERTY_HINT_ENUM, ",".join(ANIMATION_STRATEGIES_NAMES), PROPERTY_USAGE_DEFAULT),
])
func import(
res_source_file_path: String,
atlas: Texture2D,
sprite_sheet: _Common.SpriteSheetInfo,
animation_library: _Common.AnimationLibraryInfo,
options: Dictionary,
save_path: String
) -> ImportResult:
var result: ImportResult = ImportResult.new()
var texture_rect: TextureRect = TextureRect.new()
var node_name: String = options[_Options.ROOT_NODE_NAME].strip_edges()
texture_rect.name = res_source_file_path.get_file().get_basename() \
if node_name.is_empty() else node_name
texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
var sprite_size: Vector2i = sprite_sheet.source_image_size
texture_rect.size = sprite_size
var filter_clip_enabled: bool = options[_Options.ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED]
var animation_player: AnimationPlayer
match options[_Options.ANIMATION_STRATEGY]:
AnimationStrategy.SINGLE_ATLAS_TEXTURE_REGION_AND_MARGIN:
var atlas_texture: AtlasTexture = AtlasTexture.new()
atlas_texture.atlas = atlas
atlas_texture.filter_clip = filter_clip_enabled
atlas_texture.resource_local_to_scene = true
atlas_texture.region = Rect2(0, 0, 1, 1)
atlas_texture.margin = Rect2(2, 2, 0, 0)
texture_rect.texture = atlas_texture
animation_player = _create_animation_player(animation_library, {
".:texture:margin": func(frame: _Common.FrameInfo) -> Rect2:
return \
Rect2(frame.sprite.offset,
sprite_size - frame.sprite.region.size) \
if frame.sprite.region.has_area() else \
Rect2(2, 2, 0, 0),
".:texture:region" : func(frame: _Common.FrameInfo) -> Rect2:
return \
Rect2(frame.sprite.region) \
if frame.sprite.region.has_area() else \
Rect2(0, 0, 1, 1) })
AnimationStrategy.MULTIPLE_ATLAS_TEXTURES_INSTANCES:
var atlas_textures: Array[AtlasTexture]
var empty_atlas_texture: AtlasTexture = AtlasTexture.new()
empty_atlas_texture.filter_clip = filter_clip_enabled
empty_atlas_texture.atlas = atlas
empty_atlas_texture.region = Rect2(0, 0, 1, 1)
empty_atlas_texture.margin = Rect2(2, 2, 0, 0)
animation_player = _create_animation_player(animation_library, {
".:texture": func(frame: _Common.FrameInfo) -> Texture2D:
if not frame.sprite.region.has_area():
return empty_atlas_texture
var region: Rect2 = frame.sprite.region
var margin: Rect2 = Rect2(
frame.sprite.offset,
sprite_size - frame.sprite.region.size)
var equivalent_atlas_textures: Array = atlas_textures.filter(
func(t: AtlasTexture) -> bool: return t.margin == margin and t.region == region)
if not equivalent_atlas_textures.is_empty():
return equivalent_atlas_textures.front()
var atlas_texture: AtlasTexture = AtlasTexture.new()
atlas_texture.atlas = atlas
atlas_texture.filter_clip = filter_clip_enabled
atlas_texture.region = region
atlas_texture.margin = margin
atlas_textures.append(atlas_texture)
return atlas_texture})
texture_rect.add_child(animation_player)
animation_player.owner = texture_rect
var packed_scene: PackedScene = PackedScene.new()
packed_scene.pack(texture_rect)
result.success(packed_scene,
ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES)
return result

View File

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

View File

@@ -0,0 +1,41 @@
@tool
const _Common = preload("common.gd")
const __empty_callable: Callable = Callable()
const SPRITE_SHEET_LAYOUT: StringName = "sprite_sheet/layout"
const MAX_CELLS_IN_STRIP: StringName = "sprite_sheet/max_cells_in_strip"
const EDGES_ARTIFACTS_AVOIDANCE_METHOD: StringName = "sprite_sheet/edges_artifacts_avoidance_method"
const SPRITES_SURROUNDING_COLOR: StringName = "sprite_sheet/sprites_surrounding_color"
const TRIM_SPRITES_TO_OVERALL_MIN_SIZE: StringName = "sprite_sheet/trim_sprites_to_overall_min_size"
const COLLAPSE_TRANSPARENT_SPRITES: StringName = "sprite_sheet/collapse_transparent_sprites"
const MERGE_DUPLICATED_SPRITES: StringName = "sprite_sheet/merge_duplicated_sprites"
const DEFAULT_ANIMATION_NAME: StringName = "animation/default/name"
const DEFAULT_ANIMATION_DIRECTION: StringName = "animation/default/direction"
const DEFAULT_ANIMATION_REPEAT_COUNT: StringName = "animation/default/repeat_count"
const AUTOPLAY_ANIMATION_NAME: StringName = "animation/autoplay_name"
const ROOT_NODE_NAME: StringName = "root_node_name"
const ANIMATION_STRATEGY: StringName = "animation/strategy"
const SPRITE_CENTERED: StringName = "sprite/centered"
const ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED: StringName = "atlas_textures/region_filter_clip_enabled"
const MIDDLE_IMPORT_SCRIPT_PATH: StringName = "middle_import_script"
const POST_IMPORT_SCRIPT_PATH: StringName = "post_import_script"
static func create_option(
name: StringName,
default_value: Variant,
property_hint: PropertyHint = PROPERTY_HINT_NONE,
hint_string: String = "",
usage: PropertyUsageFlags = PROPERTY_USAGE_NONE,
get_is_visible: Callable = __empty_callable
) -> Dictionary:
var option_data: Dictionary = {
name = name,
default_value = default_value,
}
if hint_string: option_data["hint_string"] = hint_string
if property_hint: option_data["property_hint"] = property_hint
if usage: option_data["usage"] = usage
if get_is_visible != __empty_callable:
option_data["get_is_visible"] = get_is_visible
return option_data

View File

@@ -0,0 +1 @@
uid://2wlv6noymbn3

View File

@@ -0,0 +1,7 @@
[plugin]
name="Importality"
description="Universal raster graphics and animations importers pack"
author="Nikolay Lebedev aka nklbdev"
version="0.3.0"
script="editor_plugin.gd"

View File

@@ -0,0 +1,138 @@
@tool
# This code is taken from: https://github.com/semibran/pack/blob/master/lib/pack.js
# Copyright (c) 2018 Brandon Semilla (MIT License) - original author
# Copyright (c) 2023 Nikolay Lebedev (MIT License) - porting to gdscript, refactoring and optimization
const _Result = preload("result.gd").Class
const _Common = preload("common.gd")
const __WHITESPACE_WEIGHT: float = 1
const __SIDE_LENGTH_WEIGHT: float = 10
class RectPackingResult:
extends _Result
# Total size of the entire layout of rectangles.
var bounds: Vector2i
# Computed positions of the input rectangles
# in the same order as their sizes were passed in.
var rects_positions: Array[Vector2i]
func _get_result_type_description() -> String:
return "Rect packing"
func success(bounds: Vector2i, rects_positions: Array[Vector2i]) -> void:
_success()
self.bounds = bounds
self.rects_positions = rects_positions
static func __add_rect_to_cache(rect: Rect2i, cache: Dictionary, cache_grid_size: Vector2i) -> void:
var left_top_cell: Vector2i = rect.position / cache_grid_size
var right_bottom_cell: Vector2i = rect.end / cache_grid_size + (rect.end % cache_grid_size).sign()
for y in range(left_top_cell.y, right_bottom_cell.y):
for x in range(left_top_cell.x, right_bottom_cell.x):
var cell: Vector2i = Vector2i(x, y)
if cache.has(cell):
cache[cell].push_back(rect)
else:
cache[cell] = [rect] as Array[Rect2i]
const __empty_rect_array: Array[Rect2i] = []
static func __has_intersection(rect: Rect2i, cache: Dictionary, cache_grid_size: Vector2i) -> bool:
var left_top_cell: Vector2i = rect.position / cache_grid_size
var right_bottom_cell: Vector2i = rect.end / cache_grid_size + (rect.end % cache_grid_size).sign()
for y in range(left_top_cell.y, right_bottom_cell.y):
for x in range(left_top_cell.x, right_bottom_cell.x):
for cached_rect in cache.get(Vector2i(x, y), __empty_rect_array):
if cached_rect.intersects(rect):
return true
return false
# The function takes an array of rectangle sizes as input and compactly packs them.
static func pack(rects_sizes: Array[Vector2i]) -> RectPackingResult:
var result: RectPackingResult = RectPackingResult.new()
var rects_count: int = rects_sizes.size()
if rects_count == 0:
result.success(Vector2i.ZERO, [])
return result
var rects_positions: Array[Vector2i]
rects_positions.resize(rects_count)
var min_area: int
var rect_sizes_sum: Vector2i
for size in rects_sizes:
if size.x < 0 or size.y < 0:
result.fail(ERR_INVALID_DATA, "Negative rect size found")
return result
min_area += size.x * size.y
rect_sizes_sum += size
if min_area == 0:
result.success(Vector2i.ZERO, rects_positions)
return result
var average_rect_size: Vector2 = Vector2(rect_sizes_sum) / rects_count
var rect_cache_grid_size: Vector2i = average_rect_size.ceil() * 2
var average_squared_rect_side_length: float = sqrt(min_area / float(rects_count))
var rect_cache: Dictionary
var possible_bounds_side_length: int = ceili(sqrt(rects_count))
nearest_po2(possible_bounds_side_length)
var rects_order_arr: Array = PackedInt32Array(range(0, rects_count))
rects_order_arr.sort_custom(func(a: int, b: int) -> bool:
return rects_sizes[a].x * rects_sizes[a].y > rects_sizes[b].x * rects_sizes[b].y)
var rects_order: PackedInt32Array = PackedInt32Array(rects_order_arr)
var bounds: Vector2i = rects_sizes[rects_order[0]]
var utilized_area: int = bounds.x * bounds.y
var splits_by_axis: Array[PackedInt32Array] = [[0, bounds.x], [0, bounds.y]]
__add_rect_to_cache(Rect2i(Vector2i.ZERO, rects_sizes[rects_order[0]]), rect_cache, rect_cache_grid_size)
for rect_index in range(1, rects_count): # skip first rect at (0, 0)
var ordered_rect_index: int = rects_order[rect_index]
var rect: Rect2i = Rect2i(Vector2i.ZERO, rects_sizes[ordered_rect_index])
var rect_area: int = rect.get_area()
if rect_area == 0:
continue
utilized_area += rect_area
var best_score: float = INF
var best_new_bounds: Vector2i = bounds
for landing_rect_index in rect_index:
var ordered_landing_rect_index: int = rects_order[landing_rect_index]
var landing_rect: Rect2i = Rect2i(
rects_positions[ordered_landing_rect_index],
rects_sizes[ordered_landing_rect_index])
for split_axis_index in 2:
var orthogonal_asis_index: int = (split_axis_index + 1) % 2
var splits: PackedInt32Array = splits_by_axis[split_axis_index]
rect.position[orthogonal_asis_index] = landing_rect.end[orthogonal_asis_index]
for split_index in range(
splits.bsearch(landing_rect.position[split_axis_index]),
splits.bsearch(landing_rect.end[split_axis_index])):
rect.position[split_axis_index] = splits[split_index]
if __has_intersection(rect, rect_cache, rect_cache_grid_size):
continue
var new_bounds: Vector2i = Vector2i(
maxi(bounds.x, rect.end.x),
maxi(bounds.y, rect.end.y))
var score: float = \
__WHITESPACE_WEIGHT * (new_bounds.x * new_bounds.y - utilized_area) + \
__SIDE_LENGTH_WEIGHT * average_squared_rect_side_length * maxf(new_bounds.x, new_bounds.y)
if score < best_score:
best_score = score
rects_positions[ordered_rect_index] = rect.position
best_new_bounds = new_bounds
bounds = best_new_bounds
rect.position = rects_positions[ordered_rect_index]
__add_rect_to_cache(rect, rect_cache, rect_cache_grid_size)
# Add new splits at rect.end if they dot't already exist
for split_axis_index in 2:
var splits: PackedInt32Array = splits_by_axis[split_axis_index]
var position: int = rect.end[split_axis_index]
var split_index: int = splits.bsearch(position)
if split_index == splits.size():
splits.append(position)
elif splits[split_index] != position:
splits.insert(split_index, position)
result.success(bounds, rects_positions)
return result

View File

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

View File

@@ -0,0 +1,25 @@
class Class:
extends RefCounted
var error: Error
var error_description: String
var inner_result: Class
func _get_result_type_description() -> String:
return "Operation"
func fail(error: Error, error_description: String = "", inner_result: Class = null) -> void:
assert(error != OK)
self.error = error
self.error_description = error_description
self.inner_result = inner_result
func _success():
error = OK
error_description = ""
inner_result = null
func _to_string() -> String:
return "%s error: %s (%s)%s%s" % [
_get_result_type_description(),
error,
error_string(error),
(", description: \"%s\"" % [error_description]) if error_description else "",
(", inner error:\n%s" % [inner_result]) if inner_result else "",
] if error else "%s(success)"

View File

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

View File

@@ -0,0 +1,67 @@
@tool
extends RefCounted
const _Result = preload("result.gd").Class
var __editor_settings: EditorSettings
var __name: StringName
var __initial_value: Variant
var __type: Variant.Type
var __hint: PropertyHint
var __hint_string: String
var __is_required: bool
var __is_value_empty_func: Callable
func __default_is_value_empty_func(value: Variant) -> bool:
if value: return false
return true
func _init(
name: String,
initial_value: Variant,
type: int,
hint: int,
hint_string: String = "",
is_required: bool = false,
is_value_empty_func: Callable = __default_is_value_empty_func
) -> void:
__name = "importality/" + name
__initial_value = initial_value
__type = type
__hint = hint
__hint_string = hint_string
__is_required = is_required
__is_value_empty_func = is_value_empty_func
func register(editor_settings: EditorSettings) -> void:
__editor_settings = editor_settings
if not __editor_settings.has_setting(__name):
__editor_settings.set_setting(__name, __initial_value)
__editor_settings.set_initial_value(__name, __initial_value, false)
var property_info: Dictionary = {
"name": __name,
"type": __type,
"hint": __hint, }
if __hint_string:
property_info["hint_string"] = __hint_string
__editor_settings.add_property_info(property_info)
class GettingValueResult:
extends _Result
var value: Variant
func success(value: Variant) -> void:
_success()
self.value = value
func get_value() -> GettingValueResult:
var result = GettingValueResult.new()
var value = __editor_settings.get_setting(__name)
if __is_required:
if __is_value_empty_func.call(value):
result.fail(ERR_UNCONFIGURED,
"The project settging \"%s\" is not specified!" % [__name] + \
"Specify it in Projest Settings -> General -> Importality.")
return result
result.success(value)
return result

View File

@@ -0,0 +1 @@
uid://47n78hm4mbvb

View File

@@ -0,0 +1,86 @@
@tool
extends RefCounted
const _Result = preload("../result.gd").Class
const _Common = preload("../common.gd")
var _edges_artifacts_avoidance_method: _Common.EdgesArtifactsAvoidanceMethod
var _sprites_surrounding_color: Color
func _init(
edges_artifacts_avoidance_method: _Common.EdgesArtifactsAvoidanceMethod,
sprites_surrounding_color: Color = Color.TRANSPARENT
) -> void:
_edges_artifacts_avoidance_method = edges_artifacts_avoidance_method
_sprites_surrounding_color = sprites_surrounding_color
class SpriteSheetBuildingResult:
extends _Result
var sprite_sheet: _Common.SpriteSheetInfo
var atlas_image: Image
func _get_result_type_description() -> String:
return "Sprite sheet building"
func success(sprite_sheet: _Common.SpriteSheetInfo, atlas_image: Image) -> void:
_success()
self.sprite_sheet = sprite_sheet
self.atlas_image = atlas_image
func build_sprite_sheet(images: Array[Image]) -> SpriteSheetBuildingResult:
assert(false, "This method is abstract and must be overriden.")
return null
static func __hash_combine(a: int, b: int) -> int:
return a ^ (b + 0x9E3779B9 + (a<<6) + (a>>2))
const __hash_precision: int = 5
static func _get_image_hash(image: Image) -> int:
var image_size: Vector2i = image.get_size()
if image_size.x * image_size.y == 0:
return 0
var hash: int = 0
hash = __hash_combine(hash, image_size.x)
hash = __hash_combine(hash, image_size.y)
var grid_cell_size: Vector2i = image_size / __hash_precision
for y in range(0, image_size.y, grid_cell_size.y):
for x in range(0, image_size.x, grid_cell_size.x):
var pixel: Color = image.get_pixel(x, y)
hash = __hash_combine(hash, pixel.r8)
hash = __hash_combine(hash, pixel.g8)
hash = __hash_combine(hash, pixel.b8)
hash = __hash_combine(hash, pixel.a8)
return hash
static func _extrude_borders(image: Image, rect: Rect2i) -> void:
if not rect.has_area():
return
# extrude borders
# left border
image.blit_rect(image,
rect.grow_side(SIDE_RIGHT, 1 - rect.size.x),
rect.position + Vector2i.LEFT)
# top border
image.blit_rect(image,
rect.grow_side(SIDE_BOTTOM, 1 - rect.size.y),
rect.position + Vector2i.UP)
# right border
image.blit_rect(image,
rect.grow_side(SIDE_LEFT, 1 - rect.size.x),
rect.position + Vector2i(rect.size.x, 0))
# bottom border
image.blit_rect(image,
rect.grow_side(SIDE_TOP, 1 - rect.size.y),
rect.position + Vector2i(0, rect.size.y))
# corner pixels
# top left corner
image.set_pixelv(rect.position - Vector2i.ONE,
image.get_pixelv(rect.position))
# top right corner
image.set_pixelv(rect.position + Vector2i(rect.size.x, -1),
image.get_pixelv(rect.position + Vector2i(rect.size.x - 1, 0)))
# bottom right corner
image.set_pixelv(rect.end,
image.get_pixelv(rect.end - Vector2i.ONE))
# bottom left corner
image.set_pixelv(rect.position + Vector2i(-1, rect.size.y),
image.get_pixelv(rect.position + Vector2i(0, rect.size.y -1)))

View File

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

View File

@@ -0,0 +1,171 @@
@tool
extends "_.gd"
const _RectPacker = preload("../rect_packer.gd")
enum StripDirection {
HORIZONTAL = 0,
VERTICAL = 1,
}
var _strips_direction: StripDirection
var _max_cells_in_strip: int
var _trim_sprites_to_overall_min_size: bool
var _collapse_transparent: bool
var _merge_duplicates: bool
func _init(
edges_artifacts_avoidance_method: _Common.EdgesArtifactsAvoidanceMethod,
strips_direction: StripDirection,
max_cells_in_strip: int,
trim_sprites_to_overall_min_size: bool,
collapse_transparent: bool,
merge_duplicates: bool,
sprites_surrounding_color: Color = Color.TRANSPARENT
) -> void:
super(edges_artifacts_avoidance_method, sprites_surrounding_color)
_strips_direction = strips_direction
_max_cells_in_strip = max_cells_in_strip
_trim_sprites_to_overall_min_size = trim_sprites_to_overall_min_size
_collapse_transparent = collapse_transparent
_merge_duplicates = merge_duplicates
func build_sprite_sheet(images: Array[Image]) -> SpriteSheetBuildingResult:
var result: SpriteSheetBuildingResult = SpriteSheetBuildingResult.new()
var images_count: int = images.size()
var sprite_sheet: _Common.SpriteSheetInfo = _Common.SpriteSheetInfo.new()
if images_count == 0:
var atlas_image = Image.new()
atlas_image.set_data(1, 1, false, Image.FORMAT_RGBA8, PackedByteArray([0, 0, 0, 0]))
result.success(sprite_sheet, atlas_image)
return result
sprite_sheet.source_image_size = images.front().get_size()
if not images.all(func(i: Image) -> bool: return i.get_size() == sprite_sheet.source_image_size):
result.fail(ERR_INVALID_DATA, "Input images have different sizes")
return result
sprite_sheet.sprites.resize(images_count)
var first_axis: int = _strips_direction
var second_axis: int = 1 - first_axis
var max_image_used_rect: Rect2i
var images_infos_cache: Dictionary # of arrays of images indices by image hashes
var unique_sprites_indices: Array[int]
var collapsed_sprite: _Common.SpriteInfo = _Common.SpriteInfo.new()
var images_used_rects: Array[Rect2i]
for image_index in images_count:
var image: Image = images[image_index]
var image_used_rect: Rect2i = image.get_used_rect()
var is_image_invisible: bool = not image_used_rect.has_area()
if _collapse_transparent and is_image_invisible:
sprite_sheet.sprites[image_index] = collapsed_sprite
continue
elif _merge_duplicates:
var image_hash: int = _get_image_hash(image)
var similar_images_indices: PackedInt32Array = \
images_infos_cache.get(image_hash, PackedInt32Array())
var is_duplicate_found: bool = false
for similar_image_index in similar_images_indices:
var similar_image: Image = images[similar_image_index]
if image == similar_image or image.get_data() == similar_image.get_data():
sprite_sheet.sprites[image_index] = \
sprite_sheet.sprites[similar_image_index]
is_duplicate_found = true
break
if similar_images_indices.is_empty():
images_infos_cache[image_hash] = similar_images_indices
similar_images_indices.push_back(image_index)
if is_duplicate_found:
continue
var sprite: _Common.SpriteInfo = _Common.SpriteInfo.new()
sprite.region = image_used_rect
sprite_sheet.sprites[image_index] = sprite
unique_sprites_indices.push_back(image_index)
if not is_image_invisible:
max_image_used_rect = \
image_used_rect.merge(max_image_used_rect) \
if max_image_used_rect.has_area() else \
image_used_rect
var unique_sprites_count: int = unique_sprites_indices.size()
if _edges_artifacts_avoidance_method == _Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION:
sprite_sheet.source_image_size += Vector2i.ONE * 2
var grid_size: Vector2i
grid_size[second_axis] = \
unique_sprites_count / _max_cells_in_strip + \
sign(unique_sprites_count % _max_cells_in_strip) \
if _max_cells_in_strip > 0 else sign(unique_sprites_count)
grid_size[first_axis] = \
_max_cells_in_strip \
if grid_size[second_axis] > 1 else \
unique_sprites_count
var image_region: Rect2i = \
max_image_used_rect \
if _trim_sprites_to_overall_min_size else \
Rect2i(Vector2i.ZERO, sprite_sheet.source_image_size)
if _edges_artifacts_avoidance_method == _Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION:
image_region = image_region.grow(1)
var atlas_size: Vector2i = grid_size * image_region.size
match _edges_artifacts_avoidance_method:
_Common.EdgesArtifactsAvoidanceMethod.NONE:
pass
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_SPACING:
atlas_size += grid_size - Vector2i.ONE
_Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING:
atlas_size += grid_size + Vector2i.ONE
_Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION:
atlas_size += grid_size * 2
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION:
pass
var atlas_image = Image.create(atlas_size.x, atlas_size.y, false, Image.FORMAT_RGBA8)
if _edges_artifacts_avoidance_method == _Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING:
atlas_image.fill(_sprites_surrounding_color)
var cell: Vector2i
var cell_index: int
for sprite_index in unique_sprites_indices:
# calculate cell
var sprite: _Common.SpriteInfo = sprite_sheet.sprites[sprite_index]
if sprite == collapsed_sprite:
continue
sprite.region.size = image_region.size
sprite.offset = image_region.position
var image: Image = images[sprite_index]
cell[first_axis] = cell_index % _max_cells_in_strip if _max_cells_in_strip > 0 else cell_index
cell[second_axis] = cell_index / _max_cells_in_strip if _max_cells_in_strip > 0 else 0
sprite.region.position = cell * image_region.size
match _edges_artifacts_avoidance_method:
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_SPACING:
sprite.region.position += cell
_Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING:
sprite.region.position += cell + Vector2i.ONE
_Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION:
sprite.region.position += cell * 2 + Vector2i.ONE
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION:
pass
atlas_image.blit_rect(image, image_region, sprite.region.position)# +
#(Vector2i.ONE
#if _edges_artifacts_avoidance_method == \
# _Models.SpriteSheetModel.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION else
#Vector2i.ZERO))
match _edges_artifacts_avoidance_method:
_Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION:
_extrude_borders(atlas_image, sprite.region)
cell_index += 1
result.success(sprite_sheet, atlas_image)
return result

View File

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

View File

@@ -0,0 +1,217 @@
@tool
extends "_.gd"
const _RectPacker = preload("../rect_packer.gd")
class SpriteProps:
extends RefCounted
var images_props: Array[ImageProps]
var atlas_region_props: AtlasRegionProps
var offset: Vector2i
func create_sprite(atlas_region_position: Vector2i) -> _Common.SpriteInfo:
var sprite = _Common.SpriteInfo.new()
sprite.region = Rect2i(atlas_region_position, atlas_region_props.size)
sprite.offset = offset
return sprite
class AtlasRegionProps:
extends RefCounted
var sprites_props: Array[SpriteProps]
var images_props: Array[ImageProps]
var size: Vector2i
class ImageProps:
extends RefCounted
var sprite_props: SpriteProps
var atlas_region_props: AtlasRegionProps
var image: Image
var used_rect: Rect2i
var used_fragment: Image
var used_fragment_data: PackedByteArray
var used_fragment_data_hash: int
func _init(image: Image) -> void:
self.image = image
used_rect = image.get_used_rect()
if used_rect.has_area():
used_fragment = image.get_region(used_rect)
used_fragment_data = used_fragment.get_data()
used_fragment_data_hash = hash(used_fragment_data)
class SpriteSheetBuildingContext:
extends RefCounted
var images_props: Array[ImageProps]
var sprites_props: Array[SpriteProps]
var atlas_regions_props: Array[AtlasRegionProps]
var _similar_images_props_by_used_fragment_data_hash: Dictionary
var _collapsed_image_props: ImageProps
func _init(images: Array[Image]) -> void:
var images_count: int = images.size()
images_props.resize(images_count)
for image_index in images_count:
images_props[image_index] = _process_image_props(ImageProps.new(images[image_index]))
func _process_image_props(image_props: ImageProps) -> ImageProps:
if not image_props.used_rect.has_area():
if _collapsed_image_props == null:
_collapsed_image_props = image_props
return _collapsed_image_props
var similar_images_props: Array[ImageProps]
if not _similar_images_props_by_used_fragment_data_hash.has(image_props.used_fragment_data_hash):
_similar_images_props_by_used_fragment_data_hash[image_props.used_fragment_data_hash] = similar_images_props
else:
similar_images_props = _similar_images_props_by_used_fragment_data_hash[image_props.used_fragment_data_hash]
for similar_image_props in similar_images_props:
if image_props.image == similar_image_props.image:
# The same image found.
return similar_image_props
elif image_props.used_rect.size == similar_image_props.used_rect.size:
if image_props.used_fragment_data == similar_image_props.used_fragment_data:
if image_props.used_rect.position == similar_image_props.used_rect.position:
# An image with equal content found.
return similar_image_props
else:
# An image with equal, but offsetted content found.
# It will have the same region, but new sprite.
image_props.atlas_region_props = similar_image_props.atlas_region_props
image_props.sprite_props = SpriteProps.new()
image_props.sprite_props.offset = image_props.used_rect.position
image_props.sprite_props.images_props.push_back(image_props)
image_props.sprite_props.atlas_region_props = similar_image_props.atlas_region_props
sprites_props.push_back(image_props.sprite_props)
return image_props
# A new unique image found.
# It will have new region and sprite.
image_props.atlas_region_props = AtlasRegionProps.new()
image_props.atlas_region_props.size = image_props.used_rect.size
image_props.sprite_props = SpriteProps.new()
image_props.sprite_props.offset = image_props.used_rect.position
image_props.sprite_props.images_props.push_back(image_props)
image_props.sprite_props.atlas_region_props = image_props.atlas_region_props
image_props.atlas_region_props.sprites_props.push_back(image_props.sprite_props)
image_props.atlas_region_props.images_props.push_back(image_props)
sprites_props.push_back(image_props.sprite_props)
atlas_regions_props.push_back(image_props.atlas_region_props)
similar_images_props.push_back(image_props)
return image_props
func build_sprite_sheet(images: Array[Image]) -> SpriteSheetBuildingResult:
var result: SpriteSheetBuildingResult = SpriteSheetBuildingResult.new()
var images_count: int = images.size()
var sprite_sheet: _Common.SpriteSheetInfo = _Common.SpriteSheetInfo.new()
if images_count == 0:
var atlas_image = Image.new()
atlas_image.set_data(1, 1, false, Image.FORMAT_RGBA8, PackedByteArray([0, 0, 0, 0]))
result.success(sprite_sheet, atlas_image)
return result
sprite_sheet.source_image_size = images.front().get_size()
if not images.all(func(i: Image) -> bool:
return i.get_size() == sprite_sheet.source_image_size):
result.fail(ERR_INVALID_DATA, "Input images have different sizes")
return result
sprite_sheet.sprites.resize(images_count)
var context: SpriteSheetBuildingContext = SpriteSheetBuildingContext.new(images)
var atlas_regions_count: int = context.atlas_regions_props.size()
if atlas_regions_count == 0:
# All sprites are collapsed
var collapsed_sprite: _Common.SpriteInfo = _Common.SpriteInfo.new()
for image_index in images_count:
sprite_sheet.sprites[image_index] = collapsed_sprite
var atlas_image = Image.new()
atlas_image.set_data(1, 1, false, Image.FORMAT_RGBA8, PackedByteArray([0, 0, 0, 0]))
result.success(sprite_sheet, atlas_image)
return result
var atlas_regions_sizes: Array[Vector2i]
atlas_regions_sizes.resize(atlas_regions_count)
for atlas_region_index in atlas_regions_count:
atlas_regions_sizes[atlas_region_index] = \
context.atlas_regions_props[atlas_region_index].size
match _edges_artifacts_avoidance_method:
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_SPACING, \
_Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING:
for atlas_region_index in atlas_regions_count:
atlas_regions_sizes[atlas_region_index] += Vector2i.ONE
_Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION, \
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION:
for atlas_region_index in atlas_regions_count:
atlas_regions_sizes[atlas_region_index] += Vector2i.ONE * 2
var packing_result: _RectPacker.RectPackingResult = _RectPacker.pack(atlas_regions_sizes)
if packing_result.error:
result.fail(ERR_BUG, "Rect packing failed", packing_result)
return result
match _edges_artifacts_avoidance_method:
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_SPACING:
packing_result.bounds -= Vector2i.ONE
_Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING:
packing_result.bounds += Vector2i.ONE
for atlas_region_index in atlas_regions_count:
packing_result.rects_positions[atlas_region_index] += Vector2i.ONE
_Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION:
for atlas_region_index in atlas_regions_count:
packing_result.rects_positions[atlas_region_index] += Vector2i.ONE
var atlas_image: Image = Image.create(
packing_result.bounds.x, packing_result.bounds.y, false, Image.FORMAT_RGBA8)
if _edges_artifacts_avoidance_method == _Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING:
atlas_image.fill(_sprites_surrounding_color)
var extrude_sprites_borders: bool = _edges_artifacts_avoidance_method == \
_Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION
var expand_sprites: bool = _edges_artifacts_avoidance_method == \
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION
var atlas_regions_positions_by_atlas_regions_props: Dictionary
for atlas_region_index in atlas_regions_count:
var atlas_region_props: AtlasRegionProps = context.atlas_regions_props[atlas_region_index]
packing_result.rects_positions[atlas_region_index] += \
Vector2i.ONE if expand_sprites else Vector2i.ZERO
var image_props: ImageProps = atlas_region_props.images_props.front()
atlas_image.blit_rect(image_props.used_fragment,
Rect2i(Vector2i.ZERO, atlas_region_props.size),
packing_result.rects_positions[atlas_region_index])
if extrude_sprites_borders:
_extrude_borders(atlas_image, Rect2i(
packing_result.rects_positions[atlas_region_index],
atlas_region_props.size))
atlas_regions_positions_by_atlas_regions_props[atlas_region_props] = \
packing_result.rects_positions[atlas_region_index]
var sprites_by_sprites_props: Dictionary
for sprite_props in context.sprites_props:
var sprite: _Common.SpriteInfo = sprite_props.create_sprite(
atlas_regions_positions_by_atlas_regions_props[sprite_props.atlas_region_props])
if expand_sprites:
sprite.region = sprite.region.grow(1)
sprite.offset -= Vector2i.ONE
sprites_by_sprites_props[sprite_props] = sprite
var collapsed_sprite: _Common.SpriteInfo
for image_index in images_count:
var image_props: ImageProps = context.images_props[image_index]
var sprite: _Common.SpriteInfo = sprites_by_sprites_props.get(image_props.sprite_props, null)
if sprite == null:
if collapsed_sprite == null:
collapsed_sprite = _Common.SpriteInfo.new()
sprite = collapsed_sprite
sprite_sheet.sprites[image_index] = sprite
if expand_sprites:
sprite_sheet.source_image_size += Vector2i.ONE * 2
result.success(sprite_sheet, atlas_image)
return result

View File

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

View File

@@ -0,0 +1,8 @@
@tool
extends ImageFormatLoaderExtension
const _Setting = preload("setting.gd")
func get_settings() -> Array[_Setting]:
assert(false, "This method is abstract and must be overriden.")
return []

View File

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

View File

@@ -0,0 +1,29 @@
extends RefCounted
const __BYTE_MASK: int = 0b11111111
static var __default_rng: RandomNumberGenerator = RandomNumberGenerator.new()
var __bytes: PackedByteArray
func _init(rng: RandomNumberGenerator = null) -> void:
if rng == null:
rng = __default_rng
rng.randomize()
const size: int = 16
__bytes.resize(size)
for i in size:
__bytes[i] = rng.randi() & __BYTE_MASK
__bytes[6] = __bytes[6] & 0x0f | 0x40
__bytes[8] = __bytes[8] & 0x3f | 0x80
func to_bytes() -> PackedByteArray:
return __bytes.duplicate()
func is_equal(other: Object) -> bool:
return \
other != null and \
get_script() == other.get_script() and \
__bytes == other.__bytes
func _to_string() -> String:
return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % Array(__bytes)

View File

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

View File

@@ -0,0 +1,174 @@
@tool
class XMLNode:
extends RefCounted
var text: String
func _init(text: String) -> void:
self.text = text
func _get_solid_text() -> String:
assert(false, "This method is abstract and must be overriden in derived class")
return ""
func get_elements(text: String) -> Array[XMLNodeElement]:
assert(false, "This method is abstract and must be overriden in derived class")
return []
func _dump(target: PackedStringArray, indent: String, level: int) -> void:
target.append(indent.repeat(level) + _get_solid_text())
class XMLNodeParent:
extends XMLNode
var children: Array[XMLNode]
func _init(text: String) -> void:
super(text)
func _get_opening_tag() -> String: return ""
func _get_closing_tag() -> String: return ""
func _get_solid_text() -> String:
assert(false, "This method is abstract and must be overriden in derived class")
return ""
func _dump_children(target: PackedStringArray, indent: String, level: int) -> void:
for child in children:
child._dump(target, indent, level)
func _dump(target: PackedStringArray, indent: String, level: int) -> void:
var tag_indent: String = indent.repeat(level)
if children.is_empty():
target.append(tag_indent + _get_solid_text())
else:
target.append(tag_indent + _get_opening_tag())
_dump_children(target, indent, level + 1)
target.append(tag_indent + _get_closing_tag())
func get_elements(text: String) -> Array[XMLNodeElement]:
var result: Array[XMLNodeElement]
result.append_array(children.filter(func(n): return n is XMLNodeElement and n.text == text))
return result
class XMLNodeRoot:
extends XMLNodeParent
func _init() -> void:
super("")
func dump_to_string(indent: String = " ", new_line: String = "\n") -> String:
var target: PackedStringArray
_dump_children(target, indent, 0)
return new_line.join(target)
func dump_to_buffer(indent: String = " ", new_line: String = "\n") -> PackedByteArray:
return dump_to_string(indent, new_line).to_utf8_buffer()
func dump_to_file(absolute_file_path: String, indent: String = " ", new_line: String = "\n") -> void:
DirAccess.make_dir_recursive_absolute(absolute_file_path.get_base_dir())
var file: FileAccess = FileAccess.open(absolute_file_path, FileAccess.WRITE)
file.store_string(dump_to_string(indent, new_line))
file.close()
class XMLNodeElement:
extends XMLNodeParent
var attributes: Dictionary
var closed: bool
func _init(text: String, closed: bool = false) -> void:
super(text)
self.closed = closed
func _get_attributes_string() -> String:
return "".join(attributes.keys().map(func(k): return " %s=\"%s\"" % [k, attributes[k]]))
func _get_opening_tag() -> String: return "<%s%s>" % [text, _get_attributes_string()]
func _get_closing_tag() -> String:return "</%s>" % [text]
func _get_solid_text() -> String: return "<%s%s/>" % [text, _get_attributes_string()]
func get_string(attribute: String) -> String:
return attributes[attribute]
func get_int(attribute: String) -> int:
return attributes[attribute].to_int()
func get_int_encoded_hex_color(attribute: String, with_alpha: bool = false) -> Color:
var arr: PackedByteArray
arr.resize(4)
arr.encode_u32(0, attributes[attribute].to_int())
if not with_alpha:
arr.resize(3)
return Color(arr.hex_encode())
func get_vector2i(attribute_x: String, attribute_y: String) -> Vector2i:
return Vector2i(attributes[attribute_x].to_int(), attributes[attribute_y].to_int())
func get_rect2i(attribute_position_x: String, attribute_position_y: String, attribute_size_x: String, attribute_size_y: String) -> Rect2i:
return Rect2i(
attributes[attribute_position_x].to_int(),
attributes[attribute_position_y].to_int(),
attributes[attribute_size_x].to_int(),
attributes[attribute_size_y].to_int())
func get_bool(attribute: String) -> bool:
var raw_value: String = attributes[attribute]
if raw_value.is_empty():
return false
if raw_value.is_valid_int():
return bool(raw_value.to_int())
if raw_value.nocasecmp_to("True") == 0:
return true
if raw_value.nocasecmp_to("False") == 0:
return false
push_warning("Failed to parse bool value from string: \"%s\", returning false..." % [raw_value])
return false
class XMLNodeText:
extends XMLNode
func _init(text: String) -> void:
super(text)
func _get_solid_text() -> String: return text.strip_edges()
func _dump(target: PackedStringArray, indent: String, level: int) -> void:
var text: String = _get_solid_text()
if not text.is_empty():
target.append(indent.repeat(level) + text)
class XMLNodeCData:
extends XMLNode
func _init(text: String) -> void:
super(text)
func _get_solid_text() -> String: return "<![CDATA[%s]]>" % [text]
class XMLNodeComment:
extends XMLNode
func _init(text: String) -> void:
super(text)
func _get_solid_text() -> String: return "<!%s>" % [text]
class XMLNodeUnknown:
extends XMLNode
func _init(text: String) -> void:
super(text)
func _get_solid_text() -> String: return "<%s>" % [text]
static func parse_file(path: String) -> XMLNodeRoot:
var parser = XMLParser.new()
parser.open(path)
return __parse_xml(parser)
static func parse_buffer(buffer: PackedByteArray) -> XMLNodeRoot:
var parser = XMLParser.new()
parser.open_buffer(buffer)
return __parse_xml(parser)
static func parse_string(xml_string: String) -> XMLNodeRoot:
return parse_buffer(xml_string.to_utf8_buffer())
static func __parse_xml(parser: XMLParser) -> XMLNodeRoot:
var root = XMLNodeRoot.new()
var stack: Array[XMLNode] = [root]
while parser.read() != ERR_FILE_EOF:
match parser.get_node_type():
XMLParser.NODE_ELEMENT:
var node: XMLNode = XMLNodeElement.new(parser.get_node_name())
for attr_idx in parser.get_attribute_count():
node.attributes[parser.get_attribute_name(attr_idx)] = \
parser.get_attribute_value(attr_idx)
stack.back().children.push_back(node)
if not parser.is_empty():
stack.push_back(node)
XMLParser.NODE_ELEMENT_END:
if stack.size() < 2:
push_warning("Extra end tag found")
else:
stack.pop_back()
XMLParser.NODE_TEXT:
var text: String = parser.get_node_data().strip_edges()
if not text.is_empty():
stack.back().children.push_back(XMLNodeText.new(text))
XMLParser.NODE_CDATA:
stack.back().children.push_back(XMLNodeCData.new(parser.get_node_data()))
XMLParser.NODE_NONE:
push_error("Incorrect XML node found")
XMLParser.NODE_UNKNOWN:
stack.back().children.push_back(XMLNodeUnknown.new(parser.get_node_name()))
XMLParser.NODE_COMMENT:
stack.back().children.push_back(XMLNodeComment.new(parser.get_node_name()))
return root

View File

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

BIN
color_palette.aseprite Normal file

Binary file not shown.

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c3p2b4383qhub"
path="res://.godot/imported/color_palette.aseprite-6f54a6678036eed6830adb4c45132691.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://color_palette.aseprite"
dest_files=["res://.godot/imported/color_palette.aseprite-6f54a6678036eed6830adb4c45132691.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

16
entities/block/block.tscn Normal file
View File

@@ -0,0 +1,16 @@
[gd_scene load_steps=3 format=3 uid="uid://cs3b6wqwqh4dn"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_kl3cb"]
size = Vector2(32, 32)
[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_v713e"]
size = Vector2(32, 32)
[node name="Block" type="StaticBody2D"]
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("RectangleShape2D_kl3cb")
[node name="Sprite2D" type="Sprite2D" parent="."]
self_modulate = Color(0.109804, 0.0117647, 0.0431373, 1)
texture = SubResource("PlaceholderTexture2D_v713e")

Binary file not shown.

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dysny58qsa0wo"
path="res://.godot/imported/player.aseprite-8d5db18b9e3dc11772829f1e5211ef56.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://entities/player/player.aseprite"
dest_files=["res://.godot/imported/player.aseprite-8d5db18b9e3dc11772829f1e5211ef56.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

17
entities/player/player.gd Normal file
View File

@@ -0,0 +1,17 @@
extends CharacterBody2D
const SPEED = 300.0
func _physics_process(delta: float) -> void:
# Get the input direction and handle the movement/deceleration.
# As good practice, you should replace UI actions with custom gameplay actions.
var direction := Input.get_vector("ui_left", "ui_right","ui_up", "ui_down")
if direction:
velocity = direction * SPEED
else:
velocity = lerp(velocity, Vector2.ZERO, 0.75)
move_and_slide()

View File

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

View File

@@ -0,0 +1,17 @@
[gd_scene load_steps=4 format=3 uid="uid://cprggjrluc751"]
[ext_resource type="Script" uid="uid://boxbqdc21wrbr" path="res://entities/player/player.gd" id="1_symyc"]
[ext_resource type="Texture2D" uid="uid://dysny58qsa0wo" path="res://entities/player/player.aseprite" id="2_abrql"]
[sub_resource type="CircleShape2D" id="CircleShape2D_sfv1e"]
radius = 16.0
[node name="Player" type="CharacterBody2D"]
script = ExtResource("1_symyc")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("CircleShape2D_sfv1e")
[node name="Sprite2D" type="Sprite2D" parent="."]
self_modulate = Color(0.223529, 1, 1, 1)
texture = ExtResource("2_abrql")

Binary file not shown.

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://134x6njync81"
path="res://.godot/imported/grass.aseprite-9dd5d05c0ac7235409737001dca84e2c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://entities/terrain/grass/grass.aseprite"
dest_files=["res://.godot/imported/grass.aseprite-9dd5d05c0ac7235409737001dca84e2c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

1
icon.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>

After

Width:  |  Height:  |  Size: 994 B

37
icon.svg.import Normal file
View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dithqymx75lu5"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

24
project.godot Normal file
View File

@@ -0,0 +1,24 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="Ephemeral Echoes"
config/features=PackedStringArray("4.4", "GL Compatibility")
config/icon="res://icon.svg"
[editor_plugins]
enabled=PackedStringArray("res://addons/nklbdev.importality/plugin.cfg")
[rendering]
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"

90
worlds/World 1.tscn Normal file
View File

@@ -0,0 +1,90 @@
[gd_scene load_steps=5 format=4 uid="uid://bopet83k23n7t"]
[ext_resource type="Texture2D" uid="uid://134x6njync81" path="res://entities/terrain/grass/grass.aseprite" id="1_vd0v3"]
[ext_resource type="PackedScene" uid="uid://cprggjrluc751" path="res://entities/player/player.tscn" id="2_8t5gy"]
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_6qmhn"]
texture = ExtResource("1_vd0v3")
texture_region_size = Vector2i(32, 32)
0:0/0 = 0
0:0/0/terrain_set = 0
0:0/0/terrain = 0
0:0/0/terrains_peering_bit/right_side = 0
0:0/0/terrains_peering_bit/bottom_right_corner = 0
0:0/0/terrains_peering_bit/bottom_side = 0
0:1/0 = 0
0:1/0/terrain_set = 0
0:1/0/terrain = 0
0:1/0/terrains_peering_bit/right_side = 0
0:1/0/terrains_peering_bit/bottom_right_corner = 0
0:1/0/terrains_peering_bit/bottom_side = 0
0:1/0/terrains_peering_bit/top_side = 0
0:1/0/terrains_peering_bit/top_right_corner = 0
0:2/0 = 0
0:2/0/terrain_set = 0
0:2/0/terrain = 0
0:2/0/terrains_peering_bit/right_side = 0
0:2/0/terrains_peering_bit/top_side = 0
0:2/0/terrains_peering_bit/top_right_corner = 0
1:0/0 = 0
1:0/0/terrain_set = 0
1:0/0/terrain = 0
1:0/0/terrains_peering_bit/right_side = 0
1:0/0/terrains_peering_bit/bottom_right_corner = 0
1:0/0/terrains_peering_bit/bottom_side = 0
1:0/0/terrains_peering_bit/bottom_left_corner = 0
1:0/0/terrains_peering_bit/left_side = 0
1:1/0 = 0
1:1/0/terrain_set = 0
1:1/0/terrain = 0
1:1/0/terrains_peering_bit/right_side = 0
1:1/0/terrains_peering_bit/bottom_right_corner = 0
1:1/0/terrains_peering_bit/bottom_side = 0
1:1/0/terrains_peering_bit/bottom_left_corner = 0
1:1/0/terrains_peering_bit/left_side = 0
1:1/0/terrains_peering_bit/top_left_corner = 0
1:1/0/terrains_peering_bit/top_side = 0
1:1/0/terrains_peering_bit/top_right_corner = 0
1:2/0 = 0
1:2/0/terrain_set = 0
1:2/0/terrain = 0
1:2/0/terrains_peering_bit/right_side = 0
1:2/0/terrains_peering_bit/left_side = 0
1:2/0/terrains_peering_bit/top_left_corner = 0
1:2/0/terrains_peering_bit/top_side = 0
1:2/0/terrains_peering_bit/top_right_corner = 0
2:0/0 = 0
2:0/0/terrain_set = 0
2:0/0/terrain = 0
2:0/0/terrains_peering_bit/bottom_side = 0
2:0/0/terrains_peering_bit/bottom_left_corner = 0
2:0/0/terrains_peering_bit/left_side = 0
2:1/0 = 0
2:1/0/terrain_set = 0
2:1/0/terrain = 0
2:1/0/terrains_peering_bit/bottom_side = 0
2:1/0/terrains_peering_bit/bottom_left_corner = 0
2:1/0/terrains_peering_bit/left_side = 0
2:1/0/terrains_peering_bit/top_left_corner = 0
2:1/0/terrains_peering_bit/top_side = 0
2:2/0 = 0
2:2/0/terrain_set = 0
2:2/0/terrain = 0
2:2/0/terrains_peering_bit/left_side = 0
2:2/0/terrains_peering_bit/top_left_corner = 0
2:2/0/terrains_peering_bit/top_side = 0
[sub_resource type="TileSet" id="TileSet_wjxv1"]
tile_size = Vector2i(32, 32)
terrain_set_0/mode = 0
terrain_set_0/terrain_0/name = "Grass"
terrain_set_0/terrain_0/color = Color(0.376471, 0.784314, 0.207843, 1)
sources/0 = SubResource("TileSetAtlasSource_6qmhn")
[node name="World1" type="Node2D"]
[node name="TileMapLayer" type="TileMapLayer" parent="."]
tile_map_data = PackedByteArray("AAAAAAAAAAABAAEAAAAAAAEAAAABAAEAAAABAAEAAAABAAEAAAACAAIAAAABAAEAAAADAAIAAAABAAEAAAAEAAMAAAABAAEAAAAFAAMAAAABAAEAAAAFAAQAAAABAAEAAAAGAAQAAAABAAEAAAAHAAUAAAABAAEAAAAIAAUAAAACAAIAAAAIAAQAAAACAAEAAAAHAAMAAAABAAEAAAAGAAMAAAABAAEAAAAHAAQAAAABAAEAAAAGAAUAAAABAAEAAAAGAAYAAAAAAAIAAAAHAAYAAAACAAIAAAAFAAUAAAABAAIAAAAHAAIAAAABAAEAAAAIAAIAAAACAAEAAAAIAAEAAAACAAAAAAAHAAEAAAABAAEAAAAGAAIAAAABAAEAAAAEAAQAAAABAAEAAAADAAUAAAABAAIAAAAEAAUAAAABAAIAAAAGAAEAAAABAAEAAAAFAAEAAAABAAEAAAAEAAEAAAABAAEAAAADAAEAAAABAAEAAAABAAIAAAABAAEAAAAAAAMAAAABAAEAAAAAAAQAAAABAAEAAAABAAQAAAABAAEAAAACAAQAAAABAAEAAAADAAQAAAABAAEAAAAFAAIAAAABAAEAAAACAAMAAAABAAEAAAADAAMAAAABAAEAAAAHAAAAAAACAAEAAAAGAP//AAABAAEAAAAFAP//AAABAAEAAAAEAP//AAABAAEAAAACAAAAAAABAAEAAAADAP//AAABAAAAAAABAAAAAAABAAEAAAD//wIAAAABAAEAAAD//wMAAAABAAEAAAABAAMAAAABAAEAAAAEAAIAAAABAAEAAAAFAAAAAAABAAEAAAACAAEAAAABAAEAAAD//wQAAAABAAEAAAAAAAUAAAABAAEAAAD//wUAAAABAAEAAAD+/wYAAAABAAEAAAD+/wcAAAAAAAIAAAD//wcAAAABAAIAAAAAAAcAAAACAAIAAAABAAYAAAACAAIAAAACAAUAAAABAAIAAAAEAAAAAAABAAEAAAAFAP7/AAABAAEAAAAEAP7/AAAAAAAAAAACAP//AAABAAAAAAD+/wIAAAABAAEAAAAAAAIAAAABAAEAAAD+/wQAAAABAAEAAAD+/wMAAAABAAEAAAD//wEAAAABAAEAAAD//wAAAAABAAEAAAD+/wEAAAABAAEAAAD9/wQAAAABAAEAAAD9/wUAAAAAAAIAAAD+/wUAAAABAAEAAAABAAUAAAABAAEAAAAAAAYAAAABAAEAAAD//wYAAAABAAEAAAAIAAMAAAACAAEAAAAGAAAAAAABAAEAAAADAAAAAAABAAEAAAAGAP7/AAABAAEAAAAHAP7/AAACAAAAAAAHAP//AAACAAEAAAAGAP3/AAACAAAAAAAFAP3/AAAAAAAAAAD9/wMAAAABAAEAAAD9/wIAAAABAAEAAAD9/wEAAAABAAEAAAD9/wAAAAABAAEAAAD+/wAAAAABAAEAAAD9////AAABAAEAAAD+////AAABAAEAAAD/////AAABAAEAAAAAAP//AAABAAAAAAABAP//AAABAAAAAAD8////AAAAAAAAAAD8/wAAAAAAAAEAAAD8/wEAAAABAAEAAAD8/wIAAAABAAEAAAD8/wMAAAABAAEAAAD8/wQAAAABAAIAAAD+//7/AAABAAAAAAD///7/AAACAAAAAAD7/wIAAAABAAEAAAD6/wIAAAABAAEAAAD5/wIAAAAAAAEAAAD5/wMAAAAAAAIAAAD6/wMAAAABAAIAAAD7/wMAAAABAAEAAAD7/wEAAAABAAAAAAD6/wEAAAABAAAAAAD5/wEAAAAAAAAAAAD7/wQAAAAAAAIAAAD9//7/AAAAAAAAAAA=")
tile_set = SubResource("TileSet_wjxv1")
[node name="Player" parent="." instance=ExtResource("2_8t5gy")]