commit 8a1166fc195a4021cbea8634758fd708dea54459 Author: gitea Date: Sat Apr 26 10:41:46 2025 -0500 init commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f28239b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +charset = utf-8 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0af181c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +/android/ diff --git a/addons/nklbdev.importality/atlas_maker.gd b/addons/nklbdev.importality/atlas_maker.gd new file mode 100644 index 0000000..0dfc99f --- /dev/null +++ b/addons/nklbdev.importality/atlas_maker.gd @@ -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 diff --git a/addons/nklbdev.importality/atlas_maker.gd.uid b/addons/nklbdev.importality/atlas_maker.gd.uid new file mode 100644 index 0000000..2bc6697 --- /dev/null +++ b/addons/nklbdev.importality/atlas_maker.gd.uid @@ -0,0 +1 @@ +uid://chw0kk15w0ads diff --git a/addons/nklbdev.importality/combined_editor_import_plugin.gd b/addons/nklbdev.importality/combined_editor_import_plugin.gd new file mode 100644 index 0000000..1facfdd --- /dev/null +++ b/addons/nklbdev.importality/combined_editor_import_plugin.gd @@ -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 diff --git a/addons/nklbdev.importality/combined_editor_import_plugin.gd.uid b/addons/nklbdev.importality/combined_editor_import_plugin.gd.uid new file mode 100644 index 0000000..9899505 --- /dev/null +++ b/addons/nklbdev.importality/combined_editor_import_plugin.gd.uid @@ -0,0 +1 @@ +uid://cuylpl4l0ud3d diff --git a/addons/nklbdev.importality/command_line_image_format_loader_extension.gd b/addons/nklbdev.importality/command_line_image_format_loader_extension.gd new file mode 100644 index 0000000..19d1d30 --- /dev/null +++ b/addons/nklbdev.importality/command_line_image_format_loader_extension.gd @@ -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 + diff --git a/addons/nklbdev.importality/command_line_image_format_loader_extension.gd.uid b/addons/nklbdev.importality/command_line_image_format_loader_extension.gd.uid new file mode 100644 index 0000000..13066ed --- /dev/null +++ b/addons/nklbdev.importality/command_line_image_format_loader_extension.gd.uid @@ -0,0 +1 @@ +uid://da2j5mwim55n5 diff --git a/addons/nklbdev.importality/common.gd b/addons/nklbdev.importality/common.gd new file mode 100644 index 0000000..c4098bf --- /dev/null +++ b/addons/nklbdev.importality/common.gd @@ -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 diff --git a/addons/nklbdev.importality/common.gd.uid b/addons/nklbdev.importality/common.gd.uid new file mode 100644 index 0000000..f1bebb9 --- /dev/null +++ b/addons/nklbdev.importality/common.gd.uid @@ -0,0 +1 @@ +uid://bwbl7nh2dyo40 diff --git a/addons/nklbdev.importality/dir_access_ext.gd b/addons/nklbdev.importality/dir_access_ext.gd new file mode 100644 index 0000000..29991c3 --- /dev/null +++ b/addons/nklbdev.importality/dir_access_ext.gd @@ -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 diff --git a/addons/nklbdev.importality/dir_access_ext.gd.uid b/addons/nklbdev.importality/dir_access_ext.gd.uid new file mode 100644 index 0000000..b36ab6d --- /dev/null +++ b/addons/nklbdev.importality/dir_access_ext.gd.uid @@ -0,0 +1 @@ +uid://dhbce1x2erq3 diff --git a/addons/nklbdev.importality/editor_plugin.gd b/addons/nklbdev.importality/editor_plugin.gd new file mode 100644 index 0000000..c1f7cac --- /dev/null +++ b/addons/nklbdev.importality/editor_plugin.gd @@ -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() diff --git a/addons/nklbdev.importality/editor_plugin.gd.uid b/addons/nklbdev.importality/editor_plugin.gd.uid new file mode 100644 index 0000000..b265eab --- /dev/null +++ b/addons/nklbdev.importality/editor_plugin.gd.uid @@ -0,0 +1 @@ +uid://fh3t767vvki diff --git a/addons/nklbdev.importality/export/_.gd b/addons/nklbdev.importality/export/_.gd new file mode 100644 index 0000000..ad36ec5 --- /dev/null +++ b/addons/nklbdev.importality/export/_.gd @@ -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]) diff --git a/addons/nklbdev.importality/export/_.gd.uid b/addons/nklbdev.importality/export/_.gd.uid new file mode 100644 index 0000000..f6de8fc --- /dev/null +++ b/addons/nklbdev.importality/export/_.gd.uid @@ -0,0 +1 @@ +uid://c3omv6s66bvkh diff --git a/addons/nklbdev.importality/export/aseprite.gd b/addons/nklbdev.importality/export/aseprite.gd new file mode 100644 index 0000000..f468b35 --- /dev/null +++ b/addons/nklbdev.importality/export/aseprite.gd @@ -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 diff --git a/addons/nklbdev.importality/export/aseprite.gd.uid b/addons/nklbdev.importality/export/aseprite.gd.uid new file mode 100644 index 0000000..a9688f8 --- /dev/null +++ b/addons/nklbdev.importality/export/aseprite.gd.uid @@ -0,0 +1 @@ +uid://c3qof0o2rjew diff --git a/addons/nklbdev.importality/export/krita.gd b/addons/nklbdev.importality/export/krita.gd new file mode 100644 index 0000000..33f188c --- /dev/null +++ b/addons/nklbdev.importality/export/krita.gd @@ -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 diff --git a/addons/nklbdev.importality/export/krita.gd.uid b/addons/nklbdev.importality/export/krita.gd.uid new file mode 100644 index 0000000..4ab7fa2 --- /dev/null +++ b/addons/nklbdev.importality/export/krita.gd.uid @@ -0,0 +1 @@ +uid://bnpwq4kns36g6 diff --git a/addons/nklbdev.importality/export/pencil2d.gd b/addons/nklbdev.importality/export/pencil2d.gd new file mode 100644 index 0000000..0641cd3 --- /dev/null +++ b/addons/nklbdev.importality/export/pencil2d.gd @@ -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 Render the file to + # --camera Name of the camera layer to use + # --width Width of the output frames + # --height Height of the output frames + # --start The first frame you want to include in the exported movie + # --end 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 + diff --git a/addons/nklbdev.importality/export/pencil2d.gd.uid b/addons/nklbdev.importality/export/pencil2d.gd.uid new file mode 100644 index 0000000..74034c3 --- /dev/null +++ b/addons/nklbdev.importality/export/pencil2d.gd.uid @@ -0,0 +1 @@ +uid://cgsbagkpxk5y7 diff --git a/addons/nklbdev.importality/export/piskel.gd b/addons/nklbdev.importality/export/piskel.gd new file mode 100644 index 0000000..0856982 --- /dev/null +++ b/addons/nklbdev.importality/export/piskel.gd @@ -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 diff --git a/addons/nklbdev.importality/export/piskel.gd.uid b/addons/nklbdev.importality/export/piskel.gd.uid new file mode 100644 index 0000000..8eb55da --- /dev/null +++ b/addons/nklbdev.importality/export/piskel.gd.uid @@ -0,0 +1 @@ +uid://q3joovito34w diff --git a/addons/nklbdev.importality/export/pixelorama.gd b/addons/nklbdev.importality/export/pixelorama.gd new file mode 100644 index 0000000..10a757e --- /dev/null +++ b/addons/nklbdev.importality/export/pixelorama.gd @@ -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 + diff --git a/addons/nklbdev.importality/export/pixelorama.gd.uid b/addons/nklbdev.importality/export/pixelorama.gd.uid new file mode 100644 index 0000000..d417825 --- /dev/null +++ b/addons/nklbdev.importality/export/pixelorama.gd.uid @@ -0,0 +1 @@ +uid://ccbaalk42lyab diff --git a/addons/nklbdev.importality/external_scripts/_.gd b/addons/nklbdev.importality/external_scripts/_.gd new file mode 100644 index 0000000..4cddfd9 --- /dev/null +++ b/addons/nklbdev.importality/external_scripts/_.gd @@ -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 diff --git a/addons/nklbdev.importality/external_scripts/_.gd.uid b/addons/nklbdev.importality/external_scripts/_.gd.uid new file mode 100644 index 0000000..9ad3056 --- /dev/null +++ b/addons/nklbdev.importality/external_scripts/_.gd.uid @@ -0,0 +1 @@ +uid://cyqv3ts6gmg5k diff --git a/addons/nklbdev.importality/external_scripts/middle_import_script_base.gd b/addons/nklbdev.importality/external_scripts/middle_import_script_base.gd new file mode 100644 index 0000000..2f9f9da --- /dev/null +++ b/addons/nklbdev.importality/external_scripts/middle_import_script_base.gd @@ -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 diff --git a/addons/nklbdev.importality/external_scripts/middle_import_script_base.gd.uid b/addons/nklbdev.importality/external_scripts/middle_import_script_base.gd.uid new file mode 100644 index 0000000..7d0b71f --- /dev/null +++ b/addons/nklbdev.importality/external_scripts/middle_import_script_base.gd.uid @@ -0,0 +1 @@ +uid://ch5hkfhkqf2lc diff --git a/addons/nklbdev.importality/external_scripts/middle_import_script_example.gd b/addons/nklbdev.importality/external_scripts/middle_import_script_example.gd new file mode 100644 index 0000000..e02e793 --- /dev/null +++ b/addons/nklbdev.importality/external_scripts/middle_import_script_example.gd @@ -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)); diff --git a/addons/nklbdev.importality/external_scripts/middle_import_script_example.gd.uid b/addons/nklbdev.importality/external_scripts/middle_import_script_example.gd.uid new file mode 100644 index 0000000..8500faf --- /dev/null +++ b/addons/nklbdev.importality/external_scripts/middle_import_script_example.gd.uid @@ -0,0 +1 @@ +uid://c4mdsfp261tbu diff --git a/addons/nklbdev.importality/external_scripts/post_import_script_base.gd b/addons/nklbdev.importality/external_scripts/post_import_script_base.gd new file mode 100644 index 0000000..736016c --- /dev/null +++ b/addons/nklbdev.importality/external_scripts/post_import_script_base.gd @@ -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 diff --git a/addons/nklbdev.importality/external_scripts/post_import_script_base.gd.uid b/addons/nklbdev.importality/external_scripts/post_import_script_base.gd.uid new file mode 100644 index 0000000..2a1c7c3 --- /dev/null +++ b/addons/nklbdev.importality/external_scripts/post_import_script_base.gd.uid @@ -0,0 +1 @@ +uid://be164xb1v365c diff --git a/addons/nklbdev.importality/external_scripts/post_import_script_example.gd b/addons/nklbdev.importality/external_scripts/post_import_script_example.gd new file mode 100644 index 0000000..21533bf --- /dev/null +++ b/addons/nklbdev.importality/external_scripts/post_import_script_example.gd @@ -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 diff --git a/addons/nklbdev.importality/external_scripts/post_import_script_example.gd.uid b/addons/nklbdev.importality/external_scripts/post_import_script_example.gd.uid new file mode 100644 index 0000000..134bc82 --- /dev/null +++ b/addons/nklbdev.importality/external_scripts/post_import_script_example.gd.uid @@ -0,0 +1 @@ +uid://wy5d2axdtea diff --git a/addons/nklbdev.importality/import/_.gd b/addons/nklbdev.importality/import/_.gd new file mode 100644 index 0000000..19adbb0 --- /dev/null +++ b/addons/nklbdev.importality/import/_.gd @@ -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 diff --git a/addons/nklbdev.importality/import/_.gd.uid b/addons/nklbdev.importality/import/_.gd.uid new file mode 100644 index 0000000..1821eef --- /dev/null +++ b/addons/nklbdev.importality/import/_.gd.uid @@ -0,0 +1 @@ +uid://cnw6e1ikxkc4j diff --git a/addons/nklbdev.importality/import/_node.gd b/addons/nklbdev.importality/import/_node.gd new file mode 100644 index 0000000..fb1006d --- /dev/null +++ b/addons/nklbdev.importality/import/_node.gd @@ -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) diff --git a/addons/nklbdev.importality/import/_node.gd.uid b/addons/nklbdev.importality/import/_node.gd.uid new file mode 100644 index 0000000..f0c6b87 --- /dev/null +++ b/addons/nklbdev.importality/import/_node.gd.uid @@ -0,0 +1 @@ +uid://bsmnxi7wqyckj diff --git a/addons/nklbdev.importality/import/_node_with_animation_player.gd b/addons/nklbdev.importality/import/_node_with_animation_player.gd new file mode 100644 index 0000000..be6b10b --- /dev/null +++ b/addons/nklbdev.importality/import/_node_with_animation_player.gd @@ -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 diff --git a/addons/nklbdev.importality/import/_node_with_animation_player.gd.uid b/addons/nklbdev.importality/import/_node_with_animation_player.gd.uid new file mode 100644 index 0000000..a959df7 --- /dev/null +++ b/addons/nklbdev.importality/import/_node_with_animation_player.gd.uid @@ -0,0 +1 @@ +uid://c5tklyam00r8k diff --git a/addons/nklbdev.importality/import/_sprite_with_animation_player.gd b/addons/nklbdev.importality/import/_sprite_with_animation_player.gd new file mode 100644 index 0000000..050c7cb --- /dev/null +++ b/addons/nklbdev.importality/import/_sprite_with_animation_player.gd @@ -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) diff --git a/addons/nklbdev.importality/import/_sprite_with_animation_player.gd.uid b/addons/nklbdev.importality/import/_sprite_with_animation_player.gd.uid new file mode 100644 index 0000000..2fc6298 --- /dev/null +++ b/addons/nklbdev.importality/import/_sprite_with_animation_player.gd.uid @@ -0,0 +1 @@ +uid://jpcaxy1f6srb diff --git a/addons/nklbdev.importality/import/animated_sprite_2d.gd b/addons/nklbdev.importality/import/animated_sprite_2d.gd new file mode 100644 index 0000000..7214ed9 --- /dev/null +++ b/addons/nklbdev.importality/import/animated_sprite_2d.gd @@ -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 diff --git a/addons/nklbdev.importality/import/animated_sprite_2d.gd.uid b/addons/nklbdev.importality/import/animated_sprite_2d.gd.uid new file mode 100644 index 0000000..c10bcfd --- /dev/null +++ b/addons/nklbdev.importality/import/animated_sprite_2d.gd.uid @@ -0,0 +1 @@ +uid://byeghoxxqevfp diff --git a/addons/nklbdev.importality/import/animated_sprite_3d.gd b/addons/nklbdev.importality/import/animated_sprite_3d.gd new file mode 100644 index 0000000..cd2ea50 --- /dev/null +++ b/addons/nklbdev.importality/import/animated_sprite_3d.gd @@ -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 diff --git a/addons/nklbdev.importality/import/animated_sprite_3d.gd.uid b/addons/nklbdev.importality/import/animated_sprite_3d.gd.uid new file mode 100644 index 0000000..c1d0986 --- /dev/null +++ b/addons/nklbdev.importality/import/animated_sprite_3d.gd.uid @@ -0,0 +1 @@ +uid://bg331u3nm4w8o diff --git a/addons/nklbdev.importality/import/sprite_2d_with_animation_player.gd b/addons/nklbdev.importality/import/sprite_2d_with_animation_player.gd new file mode 100644 index 0000000..1ceb977 --- /dev/null +++ b/addons/nklbdev.importality/import/sprite_2d_with_animation_player.gd @@ -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 diff --git a/addons/nklbdev.importality/import/sprite_2d_with_animation_player.gd.uid b/addons/nklbdev.importality/import/sprite_2d_with_animation_player.gd.uid new file mode 100644 index 0000000..68b54c3 --- /dev/null +++ b/addons/nklbdev.importality/import/sprite_2d_with_animation_player.gd.uid @@ -0,0 +1 @@ +uid://dbik163y6wqp diff --git a/addons/nklbdev.importality/import/sprite_3d_with_animation_player.gd b/addons/nklbdev.importality/import/sprite_3d_with_animation_player.gd new file mode 100644 index 0000000..6a5a0d3 --- /dev/null +++ b/addons/nklbdev.importality/import/sprite_3d_with_animation_player.gd @@ -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 diff --git a/addons/nklbdev.importality/import/sprite_3d_with_animation_player.gd.uid b/addons/nklbdev.importality/import/sprite_3d_with_animation_player.gd.uid new file mode 100644 index 0000000..1ee4a4d --- /dev/null +++ b/addons/nklbdev.importality/import/sprite_3d_with_animation_player.gd.uid @@ -0,0 +1 @@ +uid://c3vtlehm0qj6s diff --git a/addons/nklbdev.importality/import/sprite_frames.gd b/addons/nklbdev.importality/import/sprite_frames.gd new file mode 100644 index 0000000..cd71de7 --- /dev/null +++ b/addons/nklbdev.importality/import/sprite_frames.gd @@ -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 diff --git a/addons/nklbdev.importality/import/sprite_frames.gd.uid b/addons/nklbdev.importality/import/sprite_frames.gd.uid new file mode 100644 index 0000000..2575004 --- /dev/null +++ b/addons/nklbdev.importality/import/sprite_frames.gd.uid @@ -0,0 +1 @@ +uid://7divemm1kjo6 diff --git a/addons/nklbdev.importality/import/sprite_sheet.gd b/addons/nklbdev.importality/import/sprite_sheet.gd new file mode 100644 index 0000000..23744d2 --- /dev/null +++ b/addons/nklbdev.importality/import/sprite_sheet.gd @@ -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 diff --git a/addons/nklbdev.importality/import/sprite_sheet.gd.uid b/addons/nklbdev.importality/import/sprite_sheet.gd.uid new file mode 100644 index 0000000..4e8f082 --- /dev/null +++ b/addons/nklbdev.importality/import/sprite_sheet.gd.uid @@ -0,0 +1 @@ +uid://cpbdxtrpuluxc diff --git a/addons/nklbdev.importality/import/texture_rect_with_animation_player.gd b/addons/nklbdev.importality/import/texture_rect_with_animation_player.gd new file mode 100644 index 0000000..33f6e68 --- /dev/null +++ b/addons/nklbdev.importality/import/texture_rect_with_animation_player.gd @@ -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 diff --git a/addons/nklbdev.importality/import/texture_rect_with_animation_player.gd.uid b/addons/nklbdev.importality/import/texture_rect_with_animation_player.gd.uid new file mode 100644 index 0000000..96eb9e5 --- /dev/null +++ b/addons/nklbdev.importality/import/texture_rect_with_animation_player.gd.uid @@ -0,0 +1 @@ +uid://dqo2tgxc28q6l diff --git a/addons/nklbdev.importality/options.gd b/addons/nklbdev.importality/options.gd new file mode 100644 index 0000000..11b3cf7 --- /dev/null +++ b/addons/nklbdev.importality/options.gd @@ -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 diff --git a/addons/nklbdev.importality/options.gd.uid b/addons/nklbdev.importality/options.gd.uid new file mode 100644 index 0000000..e6118e4 --- /dev/null +++ b/addons/nklbdev.importality/options.gd.uid @@ -0,0 +1 @@ +uid://2wlv6noymbn3 diff --git a/addons/nklbdev.importality/plugin.cfg b/addons/nklbdev.importality/plugin.cfg new file mode 100644 index 0000000..9b75789 --- /dev/null +++ b/addons/nklbdev.importality/plugin.cfg @@ -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" diff --git a/addons/nklbdev.importality/rect_packer.gd b/addons/nklbdev.importality/rect_packer.gd new file mode 100644 index 0000000..cca2186 --- /dev/null +++ b/addons/nklbdev.importality/rect_packer.gd @@ -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 diff --git a/addons/nklbdev.importality/rect_packer.gd.uid b/addons/nklbdev.importality/rect_packer.gd.uid new file mode 100644 index 0000000..38801f2 --- /dev/null +++ b/addons/nklbdev.importality/rect_packer.gd.uid @@ -0,0 +1 @@ +uid://cxg4gsatgtv8c diff --git a/addons/nklbdev.importality/result.gd b/addons/nklbdev.importality/result.gd new file mode 100644 index 0000000..d85dc5b --- /dev/null +++ b/addons/nklbdev.importality/result.gd @@ -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)" diff --git a/addons/nklbdev.importality/result.gd.uid b/addons/nklbdev.importality/result.gd.uid new file mode 100644 index 0000000..13dc39d --- /dev/null +++ b/addons/nklbdev.importality/result.gd.uid @@ -0,0 +1 @@ +uid://dvhbtf8bjxqcj diff --git a/addons/nklbdev.importality/setting.gd b/addons/nklbdev.importality/setting.gd new file mode 100644 index 0000000..79b3e86 --- /dev/null +++ b/addons/nklbdev.importality/setting.gd @@ -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 diff --git a/addons/nklbdev.importality/setting.gd.uid b/addons/nklbdev.importality/setting.gd.uid new file mode 100644 index 0000000..32efda9 --- /dev/null +++ b/addons/nklbdev.importality/setting.gd.uid @@ -0,0 +1 @@ +uid://47n78hm4mbvb diff --git a/addons/nklbdev.importality/sprite_sheet_builder/_.gd b/addons/nklbdev.importality/sprite_sheet_builder/_.gd new file mode 100644 index 0000000..f6bf752 --- /dev/null +++ b/addons/nklbdev.importality/sprite_sheet_builder/_.gd @@ -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))) diff --git a/addons/nklbdev.importality/sprite_sheet_builder/_.gd.uid b/addons/nklbdev.importality/sprite_sheet_builder/_.gd.uid new file mode 100644 index 0000000..5264164 --- /dev/null +++ b/addons/nklbdev.importality/sprite_sheet_builder/_.gd.uid @@ -0,0 +1 @@ +uid://d1mx4gmvwjihh diff --git a/addons/nklbdev.importality/sprite_sheet_builder/grid_based.gd b/addons/nklbdev.importality/sprite_sheet_builder/grid_based.gd new file mode 100644 index 0000000..30ae4b9 --- /dev/null +++ b/addons/nklbdev.importality/sprite_sheet_builder/grid_based.gd @@ -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 diff --git a/addons/nklbdev.importality/sprite_sheet_builder/grid_based.gd.uid b/addons/nklbdev.importality/sprite_sheet_builder/grid_based.gd.uid new file mode 100644 index 0000000..48f1973 --- /dev/null +++ b/addons/nklbdev.importality/sprite_sheet_builder/grid_based.gd.uid @@ -0,0 +1 @@ +uid://c65mteocd0oco diff --git a/addons/nklbdev.importality/sprite_sheet_builder/packed.gd b/addons/nklbdev.importality/sprite_sheet_builder/packed.gd new file mode 100644 index 0000000..24a0f93 --- /dev/null +++ b/addons/nklbdev.importality/sprite_sheet_builder/packed.gd @@ -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 diff --git a/addons/nklbdev.importality/sprite_sheet_builder/packed.gd.uid b/addons/nklbdev.importality/sprite_sheet_builder/packed.gd.uid new file mode 100644 index 0000000..a811820 --- /dev/null +++ b/addons/nklbdev.importality/sprite_sheet_builder/packed.gd.uid @@ -0,0 +1 @@ +uid://1gcef3dr85gs diff --git a/addons/nklbdev.importality/standalone_image_format_loader_extension.gd b/addons/nklbdev.importality/standalone_image_format_loader_extension.gd new file mode 100644 index 0000000..999e3dd --- /dev/null +++ b/addons/nklbdev.importality/standalone_image_format_loader_extension.gd @@ -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 [] diff --git a/addons/nklbdev.importality/standalone_image_format_loader_extension.gd.uid b/addons/nklbdev.importality/standalone_image_format_loader_extension.gd.uid new file mode 100644 index 0000000..a63c3c7 --- /dev/null +++ b/addons/nklbdev.importality/standalone_image_format_loader_extension.gd.uid @@ -0,0 +1 @@ +uid://cyv1pn7443b6j diff --git a/addons/nklbdev.importality/uuid.gd b/addons/nklbdev.importality/uuid.gd new file mode 100644 index 0000000..160cc51 --- /dev/null +++ b/addons/nklbdev.importality/uuid.gd @@ -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) diff --git a/addons/nklbdev.importality/uuid.gd.uid b/addons/nklbdev.importality/uuid.gd.uid new file mode 100644 index 0000000..94543e6 --- /dev/null +++ b/addons/nklbdev.importality/uuid.gd.uid @@ -0,0 +1 @@ +uid://blafvco4qr8ah diff --git a/addons/nklbdev.importality/xml.gd b/addons/nklbdev.importality/xml.gd new file mode 100644 index 0000000..20d4e1d --- /dev/null +++ b/addons/nklbdev.importality/xml.gd @@ -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 "" % [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 "" % [text] + +class XMLNodeComment: + extends XMLNode + func _init(text: String) -> void: + super(text) + func _get_solid_text() -> String: return "" % [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 diff --git a/addons/nklbdev.importality/xml.gd.uid b/addons/nklbdev.importality/xml.gd.uid new file mode 100644 index 0000000..ddb7e39 --- /dev/null +++ b/addons/nklbdev.importality/xml.gd.uid @@ -0,0 +1 @@ +uid://bmnx2anyw4n70 diff --git a/color_palette.aseprite b/color_palette.aseprite new file mode 100644 index 0000000..7387afc Binary files /dev/null and b/color_palette.aseprite differ diff --git a/color_palette.aseprite.import b/color_palette.aseprite.import new file mode 100644 index 0000000..df9c99d --- /dev/null +++ b/color_palette.aseprite.import @@ -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 diff --git a/entities/block/block.tscn b/entities/block/block.tscn new file mode 100644 index 0000000..f7a3a4e --- /dev/null +++ b/entities/block/block.tscn @@ -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") diff --git a/entities/player/player.aseprite b/entities/player/player.aseprite new file mode 100644 index 0000000..4c352f3 Binary files /dev/null and b/entities/player/player.aseprite differ diff --git a/entities/player/player.aseprite.import b/entities/player/player.aseprite.import new file mode 100644 index 0000000..c2b438d --- /dev/null +++ b/entities/player/player.aseprite.import @@ -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 diff --git a/entities/player/player.gd b/entities/player/player.gd new file mode 100644 index 0000000..a6ed590 --- /dev/null +++ b/entities/player/player.gd @@ -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() diff --git a/entities/player/player.gd.uid b/entities/player/player.gd.uid new file mode 100644 index 0000000..9fe65fd --- /dev/null +++ b/entities/player/player.gd.uid @@ -0,0 +1 @@ +uid://boxbqdc21wrbr diff --git a/entities/player/player.tscn b/entities/player/player.tscn new file mode 100644 index 0000000..c0d929a --- /dev/null +++ b/entities/player/player.tscn @@ -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") diff --git a/entities/terrain/grass/grass.aseprite b/entities/terrain/grass/grass.aseprite new file mode 100644 index 0000000..fbb40a3 Binary files /dev/null and b/entities/terrain/grass/grass.aseprite differ diff --git a/entities/terrain/grass/grass.aseprite.import b/entities/terrain/grass/grass.aseprite.import new file mode 100644 index 0000000..d286ee3 --- /dev/null +++ b/entities/terrain/grass/grass.aseprite.import @@ -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 diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..9d8b7fa --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..f2a059b --- /dev/null +++ b/icon.svg.import @@ -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 diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..03076a9 --- /dev/null +++ b/project.godot @@ -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" diff --git a/worlds/World 1.tscn b/worlds/World 1.tscn new file mode 100644 index 0000000..e1e597a --- /dev/null +++ b/worlds/World 1.tscn @@ -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")]