init commit
This commit is contained in:
4
.editorconfig
Normal file
4
.editorconfig
Normal file
@@ -0,0 +1,4 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
39
addons/nklbdev.importality/atlas_maker.gd
Normal file
39
addons/nklbdev.importality/atlas_maker.gd
Normal file
@@ -0,0 +1,39 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
|
||||
const _Result = preload("result.gd").Class
|
||||
|
||||
class AtlasMakingResult:
|
||||
extends _Result
|
||||
var atlas: Texture2D
|
||||
func success(atlas: Texture2D) -> void:
|
||||
super._success()
|
||||
self.atlas = atlas
|
||||
|
||||
var __editor_file_system: EditorFileSystem
|
||||
|
||||
func _init(editor_file_system: EditorFileSystem) -> void:
|
||||
__editor_file_system = editor_file_system
|
||||
|
||||
func make_atlas(
|
||||
atlas_image: Image,
|
||||
res_source_file_path: String,
|
||||
editor_import_plugin: EditorImportPlugin,
|
||||
) -> AtlasMakingResult:
|
||||
var result: AtlasMakingResult = AtlasMakingResult.new()
|
||||
var res_png_path: String = res_source_file_path + ".png"
|
||||
if not (res_png_path.is_absolute_path() and res_png_path.begins_with("res://")):
|
||||
result.fail(ERR_FILE_BAD_PATH, "Path to PNG-file is not valid: %s" % [res_png_path])
|
||||
return result
|
||||
var error: Error
|
||||
error = atlas_image.save_png(res_png_path)
|
||||
if error:
|
||||
result.fail(error, "An error occured while saving atlas-image to png-file: %s" % [res_png_path])
|
||||
return result
|
||||
__editor_file_system.update_file(res_png_path)
|
||||
error = editor_import_plugin.append_import_external_resource(res_png_path)
|
||||
if error:
|
||||
result.fail(error, "An error occured while appending import external resource (atlas texture)")
|
||||
return result
|
||||
result.success(ResourceLoader.load(res_png_path, "Texture2D", ResourceLoader.CACHE_MODE_IGNORE))
|
||||
return result
|
||||
1
addons/nklbdev.importality/atlas_maker.gd.uid
Normal file
1
addons/nklbdev.importality/atlas_maker.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://chw0kk15w0ads
|
||||
222
addons/nklbdev.importality/combined_editor_import_plugin.gd
Normal file
222
addons/nklbdev.importality/combined_editor_import_plugin.gd
Normal file
@@ -0,0 +1,222 @@
|
||||
@tool
|
||||
extends EditorImportPlugin
|
||||
|
||||
const _Common = preload("common.gd")
|
||||
const _Options = preload("options.gd")
|
||||
const _Exporter = preload("export/_.gd")
|
||||
const _Importer = preload("import/_.gd")
|
||||
const _AtlasMaker = preload("atlas_maker.gd")
|
||||
const _MiddleImportScript = preload("external_scripts/middle_import_script_base.gd")
|
||||
const _PostImportScript = preload("external_scripts/post_import_script_base.gd")
|
||||
|
||||
const __empty_callable: Callable = Callable()
|
||||
|
||||
var __exporter: _Exporter
|
||||
var __importer: _Importer
|
||||
var __import_order: int = 0
|
||||
var __importer_name: String
|
||||
var __priority: float = 1
|
||||
var __resource_type: StringName
|
||||
var __save_extension: String
|
||||
var __visible_name: String
|
||||
var __options: Array[Dictionary]
|
||||
var __options_visibility_checkers: Dictionary
|
||||
var __atlas_maker: _AtlasMaker
|
||||
var __editor_file_system: EditorFileSystem
|
||||
|
||||
func _init(exporter: _Exporter, importer: _Importer, atlas_maker: _AtlasMaker, editor_file_system: EditorFileSystem) -> void:
|
||||
__importer = importer
|
||||
__exporter = exporter
|
||||
__atlas_maker = atlas_maker
|
||||
__import_order = 1
|
||||
__importer_name = "%s %s" % [exporter.get_name(), importer.get_name()]
|
||||
__priority = 1
|
||||
__resource_type = importer.get_resource_type()
|
||||
__save_extension = importer.get_save_extension()
|
||||
__visible_name = "%s -> %s" % [exporter.get_name(), importer.get_name()]
|
||||
__editor_file_system = editor_file_system
|
||||
var options: Array[Dictionary]
|
||||
__options.append_array(importer.get_options())
|
||||
__options.append(_Options.create_option(
|
||||
_Options.MIDDLE_IMPORT_SCRIPT_PATH, "", PROPERTY_HINT_FILE, "*.gd", PROPERTY_USAGE_DEFAULT))
|
||||
__options.append(_Options.create_option(
|
||||
_Options.POST_IMPORT_SCRIPT_PATH, "", PROPERTY_HINT_FILE, "*.gd", PROPERTY_USAGE_DEFAULT))
|
||||
__options.append_array(exporter.get_options())
|
||||
for option in __options:
|
||||
if option.has("get_is_visible"):
|
||||
__options_visibility_checkers[option.name] = option.get_is_visible
|
||||
|
||||
func _import(
|
||||
res_source_file_path: String,
|
||||
res_save_file_path: String,
|
||||
options: Dictionary,
|
||||
platform_variants: Array[String],
|
||||
gen_files: Array[String]
|
||||
) -> Error:
|
||||
var error: Error
|
||||
|
||||
var export_result: _Exporter.ExportResult = \
|
||||
__exporter.export(res_source_file_path, options, self)
|
||||
if export_result.error:
|
||||
push_error("Export is failed. Errors chain:\n%s" % [export_result])
|
||||
return export_result.error
|
||||
|
||||
var middle_import_script_context: _MiddleImportScript.Context = _MiddleImportScript.Context.new()
|
||||
middle_import_script_context.atlas_image = export_result.atlas_image
|
||||
middle_import_script_context.sprite_sheet = export_result.sprite_sheet
|
||||
middle_import_script_context.animation_library = export_result.animation_library
|
||||
|
||||
|
||||
# -------- MIDDLE IMPORT BEGIN --------
|
||||
var middle_import_script_path: String = options[_Options.MIDDLE_IMPORT_SCRIPT_PATH].strip_edges()
|
||||
if middle_import_script_path:
|
||||
if not (middle_import_script_path.is_absolute_path() and middle_import_script_path.begins_with("res://")):
|
||||
push_error("Middle import script path is not valid: %s" % [middle_import_script_path])
|
||||
return ERR_FILE_BAD_PATH
|
||||
var middle_import_script: Script = ResourceLoader \
|
||||
.load(middle_import_script_path, "Script") as Script
|
||||
if middle_import_script == null:
|
||||
push_error("Failed to load middle import script: %s" % [middle_import_script_path])
|
||||
return ERR_FILE_CORRUPT
|
||||
if not __is_script_inherited_from(middle_import_script, _MiddleImportScript):
|
||||
push_error("The script specified as middle import script is not inherited from external_scripts/middle_import_script_base.gd: %s" % [middle_import_script_path])
|
||||
return ERR_INVALID_DECLARATION
|
||||
error = middle_import_script.modify_context(
|
||||
res_source_file_path,
|
||||
res_save_file_path,
|
||||
self,
|
||||
__editor_file_system,
|
||||
options,
|
||||
middle_import_script_context)
|
||||
if error:
|
||||
push_error("Failed to perform middle-import-script")
|
||||
return error
|
||||
error = __append_gen_files(gen_files, middle_import_script_context.gen_files_to_add)
|
||||
if error:
|
||||
push_error("Failed to add gen files from middle-import-script context")
|
||||
return error
|
||||
# -------- MIDDLE IMPORT END --------
|
||||
|
||||
|
||||
|
||||
var atlas_making_result: _AtlasMaker.AtlasMakingResult = \
|
||||
__atlas_maker.make_atlas(middle_import_script_context.atlas_image, res_source_file_path, self)
|
||||
if atlas_making_result.error:
|
||||
push_error("Atlas texture making is failed. Errors chain:\n%s" % [atlas_making_result])
|
||||
return atlas_making_result.error
|
||||
var import_result: _Importer.ImportResult = __importer.import(
|
||||
res_source_file_path,
|
||||
atlas_making_result.atlas,
|
||||
middle_import_script_context.sprite_sheet,
|
||||
middle_import_script_context.animation_library,
|
||||
options,
|
||||
res_save_file_path)
|
||||
if import_result.error:
|
||||
push_error("Import is failed. Errors chain:\n%s" % [import_result])
|
||||
return import_result.error
|
||||
|
||||
var post_import_script_context: _PostImportScript.Context = _PostImportScript.Context.new()
|
||||
post_import_script_context.resource = import_result.resource
|
||||
post_import_script_context.resource_saver_flags = import_result.resource_saver_flags
|
||||
post_import_script_context.save_extension = _get_save_extension()
|
||||
|
||||
|
||||
|
||||
# -------- POST IMPORT BEGIN --------
|
||||
var post_import_script_path: String = options[_Options.POST_IMPORT_SCRIPT_PATH].strip_edges()
|
||||
if post_import_script_path:
|
||||
if not (post_import_script_path.is_absolute_path() and post_import_script_path.begins_with("res://")):
|
||||
push_error("Post import script path is not valid: %s" % [post_import_script_path])
|
||||
return ERR_FILE_BAD_PATH
|
||||
var post_import_script: Script = ResourceLoader \
|
||||
.load(post_import_script_path, "Script") as Script
|
||||
if post_import_script == null:
|
||||
push_error("Failed to load post import script: %s" % [post_import_script_path])
|
||||
return ERR_FILE_CORRUPT
|
||||
if not __is_script_inherited_from(post_import_script, _PostImportScript):
|
||||
push_error("The script specified as post import script is not inherited from external_scripts/post_import_script_base.gd: %s" % [post_import_script_path])
|
||||
return ERR_INVALID_DECLARATION
|
||||
error = post_import_script.modify_context(
|
||||
res_source_file_path,
|
||||
res_save_file_path,
|
||||
self,
|
||||
__editor_file_system,
|
||||
options,
|
||||
middle_import_script_context.middle_import_data,
|
||||
post_import_script_context)
|
||||
if error:
|
||||
push_error("Failed to perform post-import-script")
|
||||
return error
|
||||
error = __append_gen_files(gen_files, post_import_script_context.gen_files_to_add)
|
||||
if error:
|
||||
push_error("Failed to add gen files from post-import-script context")
|
||||
return error
|
||||
# -------- POST IMPORT END --------
|
||||
|
||||
|
||||
error = ResourceSaver.save(
|
||||
post_import_script_context.resource,
|
||||
"%s.%s" % [res_save_file_path, post_import_script_context.save_extension],
|
||||
post_import_script_context.resource_saver_flags)
|
||||
if error:
|
||||
push_error("Failed to save the new resource via ResourceSaver")
|
||||
return error
|
||||
|
||||
func _get_import_options(path: String, preset_index: int) -> Array[Dictionary]:
|
||||
return __options
|
||||
|
||||
func _get_option_visibility(path: String, option_name: StringName, options: Dictionary) -> bool:
|
||||
if __options_visibility_checkers.has(option_name):
|
||||
return __options_visibility_checkers[option_name].call(options)
|
||||
return true
|
||||
|
||||
func _get_import_order() -> int:
|
||||
return __import_order
|
||||
|
||||
func _get_importer_name() -> String:
|
||||
return __importer_name
|
||||
|
||||
func _get_preset_count() -> int:
|
||||
return 1
|
||||
|
||||
func _get_preset_name(preset_index: int) -> String:
|
||||
return "Default"
|
||||
|
||||
func _get_priority() -> float:
|
||||
return __priority
|
||||
|
||||
func _get_recognized_extensions() -> PackedStringArray:
|
||||
return __exporter.get_recognized_extensions()
|
||||
|
||||
func _get_resource_type() -> String:
|
||||
return __importer.get_resource_type()
|
||||
|
||||
func _get_save_extension() -> String:
|
||||
return __importer.get_save_extension()
|
||||
|
||||
func _get_visible_name() -> String:
|
||||
return __visible_name
|
||||
|
||||
func __is_script_inherited_from(script: Script, base_script: Script) -> bool:
|
||||
while script != null:
|
||||
if script == base_script:
|
||||
return true
|
||||
script = script.get_base_script()
|
||||
return false
|
||||
|
||||
func __append_gen_files(gen_files: PackedStringArray, gen_files_to_add: PackedStringArray) -> Error:
|
||||
for gen_file_path in gen_files_to_add:
|
||||
gen_file_path = gen_file_path.strip_edges()
|
||||
if gen_files.has(gen_file_path):
|
||||
continue
|
||||
if not gen_file_path.is_absolute_path():
|
||||
push_error("Gen-file-path is not valid path: %s" % [gen_file_path])
|
||||
return ERR_FILE_BAD_PATH
|
||||
if not gen_file_path.begins_with("res://"):
|
||||
push_error("Gen-file-path is not a resource file system path (res://): %s" % [gen_file_path])
|
||||
return ERR_FILE_BAD_PATH
|
||||
if not FileAccess.file_exists(gen_file_path):
|
||||
push_error("The file at the gen-file-path was not found: %s" % [gen_file_path])
|
||||
return ERR_FILE_NOT_FOUND
|
||||
gen_files.push_back(gen_file_path)
|
||||
return OK
|
||||
@@ -0,0 +1 @@
|
||||
uid://cuylpl4l0ud3d
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://da2j5mwim55n5
|
||||
152
addons/nklbdev.importality/common.gd
Normal file
152
addons/nklbdev.importality/common.gd
Normal file
@@ -0,0 +1,152 @@
|
||||
@tool
|
||||
|
||||
const _Setting = preload("setting.gd")
|
||||
|
||||
const SPRITE_SHEET_LAYOUTS_NAMES: PackedStringArray = [
|
||||
"Packed",
|
||||
"Horizontal strips",
|
||||
"Vertical strips",
|
||||
]
|
||||
enum SpriteSheetLayout {
|
||||
PACKED = 0,
|
||||
HORIZONTAL_STRIPS = 1,
|
||||
VERTICAL_STRIPS = 2,
|
||||
}
|
||||
|
||||
const EDGES_ARTIFACTS_AVOIDANCE_METHODS_NAMES: PackedStringArray = [
|
||||
"None",
|
||||
"Transparent spacing",
|
||||
"Solid color surrounding",
|
||||
"Borders extrusion",
|
||||
"Transparent expansion",
|
||||
]
|
||||
enum EdgesArtifactsAvoidanceMethod {
|
||||
NONE = 0,
|
||||
TRANSPARENT_SPACING = 1,
|
||||
SOLID_COLOR_SURROUNDING = 2,
|
||||
BORDERS_EXTRUSION = 3,
|
||||
TRANSPARENT_EXPANSION = 4,
|
||||
}
|
||||
|
||||
const ANIMATION_DIRECTIONS_NAMES: PackedStringArray = [
|
||||
"Forward",
|
||||
"Reverse",
|
||||
"Ping-pong",
|
||||
"Ping-pong reverse",
|
||||
]
|
||||
enum AnimationDirection {
|
||||
FORWARD = 0,
|
||||
REVERSE = 1,
|
||||
PING_PONG = 2,
|
||||
PING_PONG_REVERSE = 3,
|
||||
}
|
||||
|
||||
class SpriteInfo:
|
||||
extends RefCounted
|
||||
var region: Rect2i
|
||||
var offset: Vector2i
|
||||
|
||||
class SpriteSheetInfo:
|
||||
extends RefCounted
|
||||
var source_image_size: Vector2i
|
||||
var sprites: Array[SpriteInfo]
|
||||
|
||||
class FrameInfo:
|
||||
extends RefCounted
|
||||
var sprite: SpriteInfo
|
||||
var duration: float
|
||||
|
||||
class AnimationInfo:
|
||||
extends RefCounted
|
||||
var name: String
|
||||
var direction: AnimationDirection
|
||||
var repeat_count: int
|
||||
var frames: Array[FrameInfo]
|
||||
func get_flatten_frames() -> Array[FrameInfo]:
|
||||
var iteration_frames: Array[FrameInfo] = frames.duplicate()
|
||||
if direction == AnimationDirection.REVERSE or direction == AnimationDirection.PING_PONG_REVERSE:
|
||||
iteration_frames.reverse()
|
||||
if direction == AnimationDirection.PING_PONG or direction == AnimationDirection.PING_PONG_REVERSE:
|
||||
var returning_frames: Array[FrameInfo] = iteration_frames.duplicate()
|
||||
returning_frames.pop_back()
|
||||
returning_frames.reverse()
|
||||
returning_frames.pop_back()
|
||||
iteration_frames.append_array(returning_frames)
|
||||
if repeat_count <= 1:
|
||||
return iteration_frames
|
||||
var output_frames: Array[FrameInfo]
|
||||
var iteration_frames_count: int = iteration_frames.size()
|
||||
output_frames.resize(iteration_frames_count * repeat_count)
|
||||
for iteration_number in repeat_count:
|
||||
for frame_index in iteration_frames_count:
|
||||
output_frames[iteration_number * iteration_frames_count + frame_index] = \
|
||||
iteration_frames[frame_index]
|
||||
return output_frames
|
||||
|
||||
class AnimationLibraryInfo:
|
||||
extends RefCounted
|
||||
var animations: Array[AnimationInfo]
|
||||
var autoplay_index: int = -1
|
||||
|
||||
static func get_vector2i(dict: Dictionary, x_key: String, y_key: String) -> Vector2i:
|
||||
return Vector2i(int(dict[x_key]), int(dict[y_key]))
|
||||
|
||||
static var common_temporary_files_directory_path_setting: _Setting = _Setting.new(
|
||||
"temporary_files_directory_path", "", TYPE_STRING, PROPERTY_HINT_GLOBAL_DIR,
|
||||
"", true, func(v: String): return v.is_empty())
|
||||
|
||||
const __backslash: String = "\\"
|
||||
const __quote: String = "\""
|
||||
const __space: String = " "
|
||||
const __tab: String = "\t"
|
||||
const __empty: String = ""
|
||||
static func split_words_with_quotes(source: String) -> PackedStringArray:
|
||||
var parts: PackedStringArray
|
||||
if source.is_empty():
|
||||
return parts
|
||||
|
||||
var quotation: bool
|
||||
|
||||
var previous: String
|
||||
var current: String
|
||||
var next: String = source[0]
|
||||
var chars_count = source.length()
|
||||
|
||||
var part: String
|
||||
for char_idx in chars_count:
|
||||
previous = current
|
||||
current = next
|
||||
next = source[char_idx + 1] if char_idx < chars_count - 1 else ""
|
||||
if quotation:
|
||||
# seek for quotation end
|
||||
if previous != __backslash and current == __quote:
|
||||
if next == __space or next == __tab or next == __empty:
|
||||
quotation = false
|
||||
parts.push_back(part)
|
||||
part = ""
|
||||
continue
|
||||
else:
|
||||
push_error("Invalid quotation start at %s:\n%s\n%s" % [char_idx, source, " ".repeat(char_idx) + "^"])
|
||||
return PackedStringArray()
|
||||
else:
|
||||
# seek for quotation start
|
||||
if current == __space or current == __tab:
|
||||
if not part.is_empty():
|
||||
parts.push_back(part)
|
||||
part = ""
|
||||
continue
|
||||
else:
|
||||
if previous != __backslash and current == __quote:
|
||||
if previous == __space or previous == __tab or previous == __empty:
|
||||
quotation = true
|
||||
continue
|
||||
else:
|
||||
push_error("Invalid quotation end at %s:\n%s\n%s" % [char_idx, source, " ".repeat(char_idx) + "^"])
|
||||
return PackedStringArray()
|
||||
part += current
|
||||
if quotation:
|
||||
push_error("Invalid quotation end at %s:\n%s\n%s" % [chars_count - 1, source, " ".repeat(chars_count - 1) + "^"])
|
||||
return PackedStringArray()
|
||||
if not part.is_empty():
|
||||
parts.push_back(part)
|
||||
return parts
|
||||
1
addons/nklbdev.importality/common.gd.uid
Normal file
1
addons/nklbdev.importality/common.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bwbl7nh2dyo40
|
||||
80
addons/nklbdev.importality/dir_access_ext.gd
Normal file
80
addons/nklbdev.importality/dir_access_ext.gd
Normal file
@@ -0,0 +1,80 @@
|
||||
extends Object
|
||||
|
||||
const _Result = preload("result.gd").Class
|
||||
|
||||
class CreationResult:
|
||||
extends _Result
|
||||
var path: String
|
||||
func success(path: String) -> void:
|
||||
super._success()
|
||||
self.path = path
|
||||
|
||||
class RemovalResult:
|
||||
extends _Result
|
||||
|
||||
static func create_directory_with_unique_name(base_directory_path: String) -> CreationResult:
|
||||
const error_description: String = "Failed to create a directory with unique name"
|
||||
var name: String
|
||||
var path: String
|
||||
var result = CreationResult.new()
|
||||
|
||||
var error = DirAccess.make_dir_recursive_absolute(base_directory_path)
|
||||
match error:
|
||||
OK, ERR_ALREADY_EXISTS:
|
||||
pass
|
||||
_:
|
||||
var inner_result: CreationResult = CreationResult.new()
|
||||
inner_result.fail(ERR_QUERY_FAILED, "Failed to create base directory recursive")
|
||||
result.fail(
|
||||
ERR_CANT_CREATE,
|
||||
"%s: %s \"%s\"" %
|
||||
[error_description, error, error_string(error)],
|
||||
inner_result)
|
||||
return result
|
||||
|
||||
while true:
|
||||
name = "%d" % (Time.get_unix_time_from_system() * 1000)
|
||||
path = base_directory_path.path_join(name)
|
||||
if not DirAccess.dir_exists_absolute(path):
|
||||
error = DirAccess.make_dir_absolute(path)
|
||||
match error:
|
||||
ERR_ALREADY_EXISTS:
|
||||
pass
|
||||
OK:
|
||||
result.success(path)
|
||||
break
|
||||
_:
|
||||
result.fail(
|
||||
ERR_CANT_CREATE,
|
||||
"%s: %s \"%s\"" %
|
||||
[error_description, error, error_string(error)])
|
||||
break
|
||||
return result
|
||||
|
||||
static func remove_dir_recursive(dir_path: String) -> RemovalResult:
|
||||
const error_description: String = "Failed to remove a directory with contents recursive"
|
||||
var result: RemovalResult = RemovalResult.new()
|
||||
for child_file_name in DirAccess.get_files_at(dir_path):
|
||||
var child_file_path = dir_path.path_join(child_file_name)
|
||||
var error: Error = DirAccess.remove_absolute(child_file_path)
|
||||
if error:
|
||||
var inner_result: RemovalResult = RemovalResult.new()
|
||||
inner_result.fail(
|
||||
ERR_QUERY_FAILED,
|
||||
"Failed to remove a file: \"%s\". Error: %s \"%s\"" %
|
||||
[child_file_path, error, error_string(error)])
|
||||
result.fail(ERR_QUERY_FAILED, "%s: \"%s\"" % [error_description, dir_path], inner_result)
|
||||
return result
|
||||
for child_dir_name in DirAccess.get_directories_at(dir_path):
|
||||
var child_dir_path = dir_path.path_join(child_dir_name)
|
||||
var inner_result: RemovalResult = remove_dir_recursive(child_dir_path)
|
||||
if inner_result.error:
|
||||
result.fail(ERR_QUERY_FAILED, "%s: \"%s\"" % [error_description, dir_path], inner_result)
|
||||
return result
|
||||
var error: Error = DirAccess.remove_absolute(dir_path)
|
||||
if error:
|
||||
result.fail(
|
||||
ERR_QUERY_FAILED,
|
||||
"%s: \"%s\". Error: %s \"%s\"" %
|
||||
[error_description, dir_path, error, error_string(error)])
|
||||
return result
|
||||
1
addons/nklbdev.importality/dir_access_ext.gd.uid
Normal file
1
addons/nklbdev.importality/dir_access_ext.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dhbce1x2erq3
|
||||
76
addons/nklbdev.importality/editor_plugin.gd
Normal file
76
addons/nklbdev.importality/editor_plugin.gd
Normal file
@@ -0,0 +1,76 @@
|
||||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
const ExporterBase = preload("export/_.gd")
|
||||
const _AtlasMaker = preload("atlas_maker.gd")
|
||||
|
||||
const EXPORTERS_SCRIPTS: Array[GDScript] = [
|
||||
preload("export/aseprite.gd"),
|
||||
preload("export/krita.gd"),
|
||||
preload("export/pencil2d.gd"),
|
||||
preload("export/piskel.gd"),
|
||||
preload("export/pixelorama.gd"),
|
||||
]
|
||||
|
||||
const ImporterBase = preload("import/_.gd")
|
||||
const IMPORTERS_SCRIPTS: Array[GDScript] = [
|
||||
preload("import/animated_sprite_2d.gd"),
|
||||
preload("import/animated_sprite_3d.gd"),
|
||||
preload("import/sprite_2d_with_animation_player.gd"),
|
||||
preload("import/sprite_3d_with_animation_player.gd"),
|
||||
preload("import/sprite_frames.gd"),
|
||||
preload("import/texture_rect_with_animation_player.gd"),
|
||||
preload("import/sprite_sheet.gd"),
|
||||
]
|
||||
|
||||
const StandaloneImageFormatLoaderExtension = preload("standalone_image_format_loader_extension.gd")
|
||||
const STANDALONE_IMAGE_FORMAT_LOADER_EXTENSIONS: Array[GDScript] = [
|
||||
preload("command_line_image_format_loader_extension.gd")
|
||||
]
|
||||
|
||||
const CombinedEditorImportPlugin = preload("combined_editor_import_plugin.gd")
|
||||
|
||||
var __editor_import_plugins: Array[EditorImportPlugin]
|
||||
var __image_format_loader_extensions: Array[ImageFormatLoaderExtension]
|
||||
|
||||
func _enter_tree() -> void:
|
||||
var editor_interface: EditorInterface = get_editor_interface()
|
||||
var editor_file_system: EditorFileSystem = editor_interface.get_resource_filesystem()
|
||||
var editor_settings: EditorSettings = editor_interface.get_editor_settings()
|
||||
|
||||
var exporters: Array[ExporterBase]
|
||||
for Exporter in EXPORTERS_SCRIPTS:
|
||||
var exporter: ExporterBase = Exporter.new(editor_file_system)
|
||||
for setting in exporter.get_settings():
|
||||
setting.register(editor_settings)
|
||||
exporters.push_back(exporter)
|
||||
var image_format_loader_extension: ImageFormatLoaderExtension = \
|
||||
exporter.get_image_format_loader_extension()
|
||||
if image_format_loader_extension:
|
||||
__image_format_loader_extensions.push_back(image_format_loader_extension)
|
||||
image_format_loader_extension.add_format_loader()
|
||||
var importers: Array[ImporterBase]
|
||||
for Importer in IMPORTERS_SCRIPTS:
|
||||
importers.push_back(Importer.new())
|
||||
var atlas_maker: _AtlasMaker = _AtlasMaker.new(editor_file_system)
|
||||
for exporter in exporters:
|
||||
for importer in importers:
|
||||
var editor_import_plugin: EditorImportPlugin = \
|
||||
CombinedEditorImportPlugin.new(exporter, importer, atlas_maker, editor_file_system)
|
||||
__editor_import_plugins.push_back(editor_import_plugin)
|
||||
add_import_plugin(editor_import_plugin)
|
||||
for Extension in STANDALONE_IMAGE_FORMAT_LOADER_EXTENSIONS:
|
||||
var image_format_loader_extension: StandaloneImageFormatLoaderExtension = \
|
||||
Extension.new() as StandaloneImageFormatLoaderExtension
|
||||
for setting in image_format_loader_extension.get_settings():
|
||||
setting.register(editor_settings)
|
||||
__image_format_loader_extensions.push_back(image_format_loader_extension)
|
||||
image_format_loader_extension.add_format_loader()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
for editor_import_plugin in __editor_import_plugins:
|
||||
remove_import_plugin(editor_import_plugin)
|
||||
__editor_import_plugins.clear()
|
||||
for image_format_loader_extension in __image_format_loader_extensions:
|
||||
image_format_loader_extension.remove_format_loader()
|
||||
__image_format_loader_extensions.clear()
|
||||
1
addons/nklbdev.importality/editor_plugin.gd.uid
Normal file
1
addons/nklbdev.importality/editor_plugin.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://fh3t767vvki
|
||||
212
addons/nklbdev.importality/export/_.gd
Normal file
212
addons/nklbdev.importality/export/_.gd
Normal file
@@ -0,0 +1,212 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
|
||||
const _Result = preload("../result.gd").Class
|
||||
const _Common = preload("../common.gd")
|
||||
const _Options = preload("../options.gd")
|
||||
const _Setting = preload("../setting.gd")
|
||||
const _DirAccessExtensions = preload("../dir_access_ext.gd")
|
||||
|
||||
const _SpriteSheetBuilderBase = preload("../sprite_sheet_builder/_.gd")
|
||||
const _GridBasedSpriteSheetBuilder = preload("../sprite_sheet_builder/grid_based.gd")
|
||||
const _PackedSpriteSheetBuilder = preload("../sprite_sheet_builder/packed.gd")
|
||||
|
||||
const ATLAS_TEXTURE_RESOURCE_TYPE_NAMES: PackedStringArray = [
|
||||
"Embedded PortableCompressedTexture2D (compact)",
|
||||
"Embedded ImageTexture (large)",
|
||||
"Separated image (custom)",
|
||||
]
|
||||
enum AtlasResourceType {
|
||||
EMBEDDED_PORTABLE_COMPRESSED_TEXTURE_2D = 0,
|
||||
EMBEDDED_IMAGE_TEXTURE = 1,
|
||||
SEPARATED_IMAGE = 2,
|
||||
}
|
||||
|
||||
class ExportResult:
|
||||
extends _Result
|
||||
var atlas_image: Image
|
||||
var sprite_sheet: _Common.SpriteSheetInfo
|
||||
var animation_library: _Common.AnimationLibraryInfo
|
||||
func _get_result_type_description() -> String:
|
||||
return "Export"
|
||||
func success(
|
||||
atlas_image: Image,
|
||||
sprite_sheet: _Common.SpriteSheetInfo,
|
||||
animation_library: _Common.AnimationLibraryInfo
|
||||
) -> void:
|
||||
_success()
|
||||
self.atlas_image = atlas_image
|
||||
self.sprite_sheet = sprite_sheet
|
||||
self.animation_library = animation_library
|
||||
|
||||
var __name: String
|
||||
var __recognized_extensions: PackedStringArray
|
||||
var __settings: Array[_Setting] = [_Common.common_temporary_files_directory_path_setting]
|
||||
var __options: Array[Dictionary] = [
|
||||
_Options.create_option(_Options.EDGES_ARTIFACTS_AVOIDANCE_METHOD, _Common.EdgesArtifactsAvoidanceMethod.NONE,
|
||||
PROPERTY_HINT_ENUM, ",".join(_Common.EDGES_ARTIFACTS_AVOIDANCE_METHODS_NAMES),
|
||||
PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED),
|
||||
_Options.create_option(_Options.SPRITES_SURROUNDING_COLOR, Color.TRANSPARENT,
|
||||
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT,
|
||||
func(o): return o[_Options.EDGES_ARTIFACTS_AVOIDANCE_METHOD] == \
|
||||
_Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING),
|
||||
_Options.create_option(_Options.SPRITE_SHEET_LAYOUT, _Common.SpriteSheetLayout.PACKED,
|
||||
PROPERTY_HINT_ENUM, ",".join(_Common.SPRITE_SHEET_LAYOUTS_NAMES),
|
||||
PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED),
|
||||
_Options.create_option(_Options.MAX_CELLS_IN_STRIP, 0,
|
||||
PROPERTY_HINT_RANGE, "0,,1,or_greater", PROPERTY_USAGE_DEFAULT,
|
||||
func(o): return o[_Options.SPRITE_SHEET_LAYOUT] != \
|
||||
_Common.SpriteSheetLayout.PACKED),
|
||||
_Options.create_option(_Options.TRIM_SPRITES_TO_OVERALL_MIN_SIZE, true,
|
||||
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT,
|
||||
func(o): return o[_Options.SPRITE_SHEET_LAYOUT] != \
|
||||
_Common.SpriteSheetLayout.PACKED),
|
||||
_Options.create_option(_Options.COLLAPSE_TRANSPARENT_SPRITES, true,
|
||||
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT,
|
||||
func(o): return o[_Options.SPRITE_SHEET_LAYOUT] != \
|
||||
_Common.SpriteSheetLayout.PACKED),
|
||||
_Options.create_option(_Options.MERGE_DUPLICATED_SPRITES, true,
|
||||
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT,
|
||||
func(o): return o[_Options.SPRITE_SHEET_LAYOUT] != \
|
||||
_Common.SpriteSheetLayout.PACKED),
|
||||
]
|
||||
var __image_format_loader_extension: ImageFormatLoaderExtension
|
||||
|
||||
func _init(
|
||||
name: String,
|
||||
recognized_extensions: PackedStringArray,
|
||||
options: Array[Dictionary],
|
||||
settings: Array[_Setting],
|
||||
image_format_loader_extension: ImageFormatLoaderExtension = null
|
||||
) -> void:
|
||||
__name = name
|
||||
__recognized_extensions = recognized_extensions
|
||||
__options.append_array(options)
|
||||
__settings.append_array(settings)
|
||||
__image_format_loader_extension = image_format_loader_extension
|
||||
|
||||
func get_recognized_extensions() -> PackedStringArray:
|
||||
return __recognized_extensions
|
||||
|
||||
func get_options() -> Array[Dictionary]:
|
||||
return __options
|
||||
|
||||
func get_name() -> String:
|
||||
return __name
|
||||
|
||||
func get_settings() -> Array[_Setting]:
|
||||
return __settings
|
||||
|
||||
func get_image_format_loader_extension() -> ImageFormatLoaderExtension:
|
||||
return __image_format_loader_extension
|
||||
|
||||
func export(
|
||||
res_source_file_path: String,
|
||||
options: Dictionary,
|
||||
editor_import_plugin: EditorImportPlugin
|
||||
) -> ExportResult:
|
||||
return _export(
|
||||
res_source_file_path,
|
||||
options)
|
||||
|
||||
func _export(source_file: String, options: Dictionary) -> ExportResult:
|
||||
assert(false, "This method is abstract and must be overriden.")
|
||||
var result: ExportResult = ExportResult.new()
|
||||
result.fail(ERR_UNCONFIGURED)
|
||||
return result
|
||||
|
||||
enum AnimationOptions {
|
||||
FramesCount = 1,
|
||||
Direction = 2,
|
||||
RepeatCount = 4,
|
||||
}
|
||||
|
||||
static var __option_regex: RegEx = RegEx.create_from_string("\\s-\\p{L}:\\s*\\S+")
|
||||
static var __natural_number_regex: RegEx = RegEx.create_from_string("\\A\\d+\\z")
|
||||
|
||||
class AnimationParamsParsingResult:
|
||||
extends _Result
|
||||
var name: String
|
||||
var first_frame_index: int
|
||||
var frames_count: int
|
||||
var direction: _Common.AnimationDirection
|
||||
var repeat_count: int
|
||||
func _get_result_type_description() -> String:
|
||||
return "Animation parameters parsing"
|
||||
|
||||
static func _parse_animation_params(
|
||||
raw_animation_params: String,
|
||||
animation_options: AnimationOptions,
|
||||
first_frame_index: int,
|
||||
frames_count: int = 0
|
||||
) -> AnimationParamsParsingResult:
|
||||
var result = AnimationParamsParsingResult.new()
|
||||
if first_frame_index < 0:
|
||||
result.fail(ERR_INVALID_DATA, "Wrong value for animation first frame index. Expected natural number, got: %s" % [first_frame_index])
|
||||
return result
|
||||
result.first_frame_index = first_frame_index
|
||||
result.frames_count = frames_count
|
||||
result.direction = -1
|
||||
result.repeat_count = -1
|
||||
raw_animation_params = raw_animation_params.strip_edges()
|
||||
var options_matches: Array[RegExMatch] = __option_regex.search_all(raw_animation_params)
|
||||
var first_match_position: int = raw_animation_params.length()
|
||||
for option_match in options_matches:
|
||||
var match_position: int = option_match.get_start()
|
||||
assert(match_position >= 0)
|
||||
if match_position < first_match_position:
|
||||
first_match_position = match_position
|
||||
var raw_option: String = option_match.get_string().strip_edges()
|
||||
var raw_value = raw_option.substr(3).strip_edges()
|
||||
match raw_option.substr(0, 3):
|
||||
"-f:":
|
||||
if animation_options & AnimationOptions.FramesCount:
|
||||
if result.frames_count == 0:
|
||||
if __natural_number_regex.search(raw_value):
|
||||
result.frames_count = raw_value.to_int()
|
||||
if result.frames_count <= 0:
|
||||
result.fail(ERR_INVALID_DATA, "Wrong value format for frames count. Expected positive integer number, got: \"%s\"" % [raw_value])
|
||||
return result
|
||||
"-d:":
|
||||
if animation_options & AnimationOptions.Direction:
|
||||
if result.direction < 0:
|
||||
match raw_value:
|
||||
"f": result.direction = _Common.AnimationDirection.FORWARD
|
||||
"r": result.direction = _Common.AnimationDirection.REVERSE
|
||||
"pp": result.direction = _Common.AnimationDirection.PING_PONG
|
||||
"ppr": result.direction = _Common.AnimationDirection.PING_PONG_REVERSE
|
||||
_:
|
||||
result.fail(ERR_INVALID_DATA, "Wrong value format for animation direction. Expected one of: [\"f\", \"r\", \"pp\", \"ppr\"], got: \"%s\"" % [raw_value])
|
||||
return result
|
||||
"-r:":
|
||||
if animation_options & AnimationOptions.RepeatCount:
|
||||
if result.repeat_count < 0:
|
||||
if __natural_number_regex.search(raw_value):
|
||||
result.repeat_count = raw_value.to_int()
|
||||
else:
|
||||
result.fail(ERR_INVALID_DATA, "Wrong value format for repeat count. Expected positive integer number or zero, got: \"%s\"" % [raw_value])
|
||||
return result
|
||||
_: pass # Ignore unknown parameter
|
||||
result.name = raw_animation_params.left(first_match_position).strip_edges()
|
||||
if result.frames_count <= 0:
|
||||
result.fail(ERR_UNCONFIGURED, "Animation frames count is required but not specified")
|
||||
return result
|
||||
return result
|
||||
|
||||
func _create_sprite_sheet_builder(options: Dictionary) -> _SpriteSheetBuilderBase:
|
||||
var sprite_sheet_layout: _Common.SpriteSheetLayout = options[_Options.SPRITE_SHEET_LAYOUT]
|
||||
return \
|
||||
_PackedSpriteSheetBuilder.new(
|
||||
options[_Options.EDGES_ARTIFACTS_AVOIDANCE_METHOD],
|
||||
options[_Options.SPRITES_SURROUNDING_COLOR]) \
|
||||
if sprite_sheet_layout == _Common.SpriteSheetLayout.PACKED else \
|
||||
_GridBasedSpriteSheetBuilder.new(
|
||||
options[_Options.EDGES_ARTIFACTS_AVOIDANCE_METHOD],
|
||||
_GridBasedSpriteSheetBuilder.StripDirection.HORIZONTAL
|
||||
if sprite_sheet_layout == _Common.SpriteSheetLayout.HORIZONTAL_STRIPS else
|
||||
_GridBasedSpriteSheetBuilder.StripDirection.HORIZONTAL,
|
||||
options[_Options.MAX_CELLS_IN_STRIP],
|
||||
options[_Options.TRIM_SPRITES_TO_OVERALL_MIN_SIZE],
|
||||
options[_Options.COLLAPSE_TRANSPARENT_SPRITES],
|
||||
options[_Options.MERGE_DUPLICATED_SPRITES],
|
||||
options[_Options.SPRITES_SURROUNDING_COLOR])
|
||||
1
addons/nklbdev.importality/export/_.gd.uid
Normal file
1
addons/nklbdev.importality/export/_.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c3omv6s66bvkh
|
||||
288
addons/nklbdev.importality/export/aseprite.gd
Normal file
288
addons/nklbdev.importality/export/aseprite.gd
Normal file
@@ -0,0 +1,288 @@
|
||||
@tool
|
||||
extends "_.gd"
|
||||
|
||||
const __aseprite_sheet_types_by_sprite_sheet_layout: PackedStringArray = \
|
||||
[ "packed", "rows", "columns" ]
|
||||
const __aseprite_animation_directions: PackedStringArray = \
|
||||
[ "forward", "reverse", "pingpong", "pingpong_reverse" ]
|
||||
|
||||
var __os_command_setting: _Setting = _Setting.new(
|
||||
"aseprite_or_libre_sprite_command", "", TYPE_STRING, PROPERTY_HINT_NONE,
|
||||
"", true, func(v: String): return v.is_empty())
|
||||
|
||||
var __os_command_arguments_setting: _Setting = _Setting.new(
|
||||
"aseprite_or_libre_sprite_command_arguments", PackedStringArray(), TYPE_PACKED_STRING_ARRAY, PROPERTY_HINT_NONE,
|
||||
"", true, func(v: PackedStringArray): return false)
|
||||
|
||||
func _init(editor_file_system: EditorFileSystem) -> void:
|
||||
var recognized_extensions: PackedStringArray = ["ase", "aseprite"]
|
||||
super("Aseprite", recognized_extensions, [],
|
||||
[__os_command_setting, __os_command_arguments_setting],
|
||||
CustomImageFormatLoaderExtension.new(
|
||||
recognized_extensions,
|
||||
__os_command_setting,
|
||||
__os_command_arguments_setting,
|
||||
_Common.common_temporary_files_directory_path_setting))
|
||||
|
||||
func _export(res_source_file_path: String, options: Dictionary) -> ExportResult:
|
||||
var result: ExportResult = ExportResult.new()
|
||||
var err: Error
|
||||
|
||||
var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value()
|
||||
if os_command_result.error:
|
||||
result.fail(ERR_UNCONFIGURED, "Failed to get Aseprite Command to export spritesheet", os_command_result)
|
||||
return result
|
||||
|
||||
var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value()
|
||||
if os_command_arguments_result.error:
|
||||
result.fail(ERR_UNCONFIGURED, "Failed to get Aseprite Command Arguments to export spritesheet", os_command_arguments_result)
|
||||
return result
|
||||
|
||||
var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value()
|
||||
if temp_dir_path_result.error:
|
||||
result.fail(ERR_UNCONFIGURED, "Failed to get Temporary Files Directory Path to export spritesheet", temp_dir_path_result)
|
||||
return result
|
||||
var global_temp_dir_path: String = ProjectSettings.globalize_path(
|
||||
temp_dir_path_result.value.strip_edges())
|
||||
var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \
|
||||
_DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path)
|
||||
if unique_temp_dir_creation_result.error:
|
||||
result.fail(ERR_QUERY_FAILED, "Failed to create unique temporary directory to export spritesheet", unique_temp_dir_creation_result)
|
||||
return result
|
||||
var unique_temp_dir_path: String = unique_temp_dir_creation_result.path
|
||||
|
||||
var global_source_file_path: String = ProjectSettings.globalize_path(res_source_file_path)
|
||||
|
||||
var global_png_path: String = unique_temp_dir_path.path_join("temp.png")
|
||||
var global_json_path: String = unique_temp_dir_path.path_join("temp.json")
|
||||
|
||||
var command: String = os_command_result.value.strip_edges()
|
||||
var arguments: PackedStringArray = \
|
||||
os_command_arguments_result.value + \
|
||||
PackedStringArray([
|
||||
"--batch",
|
||||
"--format", "json-array",
|
||||
"--list-tags",
|
||||
"--sheet", global_png_path,
|
||||
"--data", global_json_path,
|
||||
global_source_file_path])
|
||||
|
||||
var output: Array = []
|
||||
var exit_code: int = OS.execute(command, arguments, output, true, false)
|
||||
if exit_code:
|
||||
for arg_index in arguments.size():
|
||||
arguments[arg_index] = "\nArgument: " + arguments[arg_index]
|
||||
result.fail(ERR_QUERY_FAILED, " ".join([
|
||||
"An error occurred while executing the Aseprite command.",
|
||||
"Process exited with code %s:\nCommand: %s%s"
|
||||
]) % [exit_code, command, "".join(arguments)])
|
||||
return result
|
||||
var raw_atlas_image: Image = Image.load_from_file(global_png_path)
|
||||
var json = JSON.new()
|
||||
err = json.parse(FileAccess.get_file_as_string(global_json_path))
|
||||
if err:
|
||||
result.fail(ERR_INVALID_DATA, "Failed to parse sprite sheet json data with error %s \"%s\"" % [err, error_string(err)])
|
||||
return result
|
||||
var raw_sprite_sheet_data: Dictionary = json.data
|
||||
|
||||
var sprite_sheet_layout: _Common.SpriteSheetLayout = options[_Options.SPRITE_SHEET_LAYOUT]
|
||||
var source_image_size: Vector2i = _Common.get_vector2i(
|
||||
raw_sprite_sheet_data.frames[0].sourceSize, "w", "h")
|
||||
|
||||
var frames_images_by_indices: Dictionary
|
||||
var tags_data: Array = raw_sprite_sheet_data.meta.frameTags
|
||||
var frames_data: Array = raw_sprite_sheet_data.frames
|
||||
var frames_count: int = frames_data.size()
|
||||
if tags_data.is_empty():
|
||||
var default_animation_name: String = options[_Options.DEFAULT_ANIMATION_NAME].strip_edges()
|
||||
if default_animation_name.is_empty():
|
||||
default_animation_name = "default"
|
||||
tags_data.push_back({
|
||||
name = default_animation_name,
|
||||
from = 0,
|
||||
to = frames_count - 1,
|
||||
direction = __aseprite_animation_directions[options[_Options.DEFAULT_ANIMATION_DIRECTION]],
|
||||
repeat = options[_Options.DEFAULT_ANIMATION_REPEAT_COUNT]
|
||||
})
|
||||
var animations_count: int = tags_data.size()
|
||||
for tag_data in tags_data:
|
||||
for frame_index in range(tag_data.from, tag_data.to + 1):
|
||||
if frames_images_by_indices.has(frame_index):
|
||||
continue
|
||||
var frame_data: Dictionary = frames_data[frame_index]
|
||||
frames_images_by_indices[frame_index] = raw_atlas_image.get_region(Rect2i(
|
||||
_Common.get_vector2i(frame_data.frame, "x", "y"),
|
||||
source_image_size))
|
||||
var used_frames_indices: PackedInt32Array = PackedInt32Array(frames_images_by_indices.keys())
|
||||
used_frames_indices.sort()
|
||||
var used_frames_count: int = used_frames_indices.size()
|
||||
var sprite_sheet_frames_indices_by_global_frame_indices: Dictionary
|
||||
for sprite_sheet_frame_index in used_frames_indices.size():
|
||||
sprite_sheet_frames_indices_by_global_frame_indices[
|
||||
used_frames_indices[sprite_sheet_frame_index]] = \
|
||||
sprite_sheet_frame_index
|
||||
var used_frames_images: Array[Image]
|
||||
used_frames_images.resize(used_frames_count)
|
||||
for i in used_frames_count:
|
||||
used_frames_images[i] = frames_images_by_indices[used_frames_indices[i]]
|
||||
|
||||
var sprite_sheet_builder: _SpriteSheetBuilderBase = _create_sprite_sheet_builder(options)
|
||||
|
||||
var sprite_sheet_building_result: _SpriteSheetBuilderBase.SpriteSheetBuildingResult = sprite_sheet_builder.build_sprite_sheet(used_frames_images)
|
||||
if sprite_sheet_building_result.error:
|
||||
result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result)
|
||||
return result
|
||||
var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet
|
||||
|
||||
var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new()
|
||||
var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges()
|
||||
|
||||
var all_frames: Array[_Common.FrameInfo]
|
||||
all_frames.resize(used_frames_count)
|
||||
var unique_animations_names: PackedStringArray
|
||||
for animation_index in animations_count:
|
||||
var tag_data: Dictionary = tags_data[animation_index]
|
||||
|
||||
var animation_params_parsing_result: AnimationParamsParsingResult = _parse_animation_params(
|
||||
tag_data.name.strip_edges(),
|
||||
AnimationOptions.Direction | AnimationOptions.RepeatCount,
|
||||
tag_data.from,
|
||||
tag_data.to - tag_data.from + 1)
|
||||
if animation_params_parsing_result.error:
|
||||
result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters",
|
||||
animation_params_parsing_result)
|
||||
return result
|
||||
if unique_animations_names.has(animation_params_parsing_result.name):
|
||||
result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" %
|
||||
[animation_params_parsing_result.name, animation_index])
|
||||
return result
|
||||
unique_animations_names.push_back(animation_params_parsing_result.name)
|
||||
var animation = _Common.AnimationInfo.new()
|
||||
animation.name = animation_params_parsing_result.name
|
||||
if animation.name.is_empty():
|
||||
result.fail(ERR_INVALID_DATA, "A tag with empty name found")
|
||||
return result
|
||||
if animation.name == autoplay_animation_name:
|
||||
animation_library.autoplay_index = animation_index
|
||||
animation.direction = __aseprite_animation_directions.find(tag_data.direction)
|
||||
if animation_params_parsing_result.direction >= 0:
|
||||
animation.direction = animation_params_parsing_result.direction
|
||||
animation.repeat_count = int(tag_data.get("repeat", "0"))
|
||||
if animation_params_parsing_result.repeat_count >= 0:
|
||||
animation.repeat_count = animation_params_parsing_result.repeat_count
|
||||
for global_frame_index in range(tag_data.from, tag_data.to + 1):
|
||||
var sprite_sheet_frame_index: int = \
|
||||
sprite_sheet_frames_indices_by_global_frame_indices[global_frame_index]
|
||||
var frame: _Common.FrameInfo = all_frames[sprite_sheet_frame_index]
|
||||
if frame == null:
|
||||
frame = _Common.FrameInfo.new()
|
||||
frame.sprite = sprite_sheet.sprites[sprite_sheet_frame_index]
|
||||
frame.duration = frames_data[global_frame_index].duration * 0.001
|
||||
all_frames[sprite_sheet_frame_index] = frame
|
||||
animation.frames.push_back(frame)
|
||||
animation_library.animations.push_back(animation)
|
||||
|
||||
if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0:
|
||||
push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name])
|
||||
|
||||
if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error:
|
||||
push_warning(
|
||||
"Failed to remove unique temporary directory: \"%s\"" %
|
||||
[unique_temp_dir_path])
|
||||
|
||||
result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library)
|
||||
return result
|
||||
|
||||
class CustomImageFormatLoaderExtension:
|
||||
extends ImageFormatLoaderExtension
|
||||
|
||||
var __recognized_extensions: PackedStringArray
|
||||
var __os_command_setting: _Setting
|
||||
var __os_command_arguments_setting: _Setting
|
||||
var __common_temporary_files_directory_path_setting: _Setting
|
||||
|
||||
func _init(recognized_extensions: PackedStringArray,
|
||||
os_command_setting: _Setting,
|
||||
os_command_arguments_setting: _Setting,
|
||||
common_temporary_files_directory_path_setting: _Setting
|
||||
) -> void:
|
||||
__recognized_extensions = recognized_extensions
|
||||
__os_command_setting = os_command_setting
|
||||
__os_command_arguments_setting = os_command_arguments_setting
|
||||
__common_temporary_files_directory_path_setting = \
|
||||
common_temporary_files_directory_path_setting
|
||||
|
||||
func _get_recognized_extensions() -> PackedStringArray:
|
||||
return __recognized_extensions
|
||||
|
||||
func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error:
|
||||
var global_source_file_path: String = file_access.get_path_absolute()
|
||||
var err: Error
|
||||
|
||||
var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value()
|
||||
if os_command_result.error:
|
||||
push_error(os_command_result.error_description)
|
||||
return os_command_result.error
|
||||
|
||||
var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value()
|
||||
if os_command_arguments_result.error:
|
||||
push_error(os_command_arguments_result.error_description)
|
||||
return os_command_arguments_result.error
|
||||
|
||||
var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value()
|
||||
if temp_dir_path_result.error:
|
||||
push_error("Failed to get Temporary Files Directory Path to export spritesheet")
|
||||
return temp_dir_path_result.error
|
||||
var global_temp_dir_path: String = ProjectSettings.globalize_path(
|
||||
temp_dir_path_result.value.strip_edges())
|
||||
var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \
|
||||
_DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path)
|
||||
if unique_temp_dir_creation_result.error:
|
||||
push_error("Failed to create unique temporary directory to export spritesheet")
|
||||
return unique_temp_dir_creation_result.error
|
||||
var unique_temp_dir_path: String = unique_temp_dir_creation_result.path
|
||||
|
||||
var global_png_path: String = unique_temp_dir_path.path_join("temp.png")
|
||||
var global_json_path: String = unique_temp_dir_path.path_join("temp.json")
|
||||
|
||||
var command: String = os_command_result.value.strip_edges()
|
||||
var arguments: PackedStringArray = \
|
||||
os_command_arguments_result.value + \
|
||||
PackedStringArray([
|
||||
"--batch",
|
||||
"--format", "json-array",
|
||||
"--list-tags",
|
||||
"--sheet", global_png_path,
|
||||
"--data", global_json_path,
|
||||
global_source_file_path,
|
||||
])
|
||||
|
||||
var output: Array = []
|
||||
var exit_code: int = OS.execute(command, arguments, output, true, false)
|
||||
if exit_code:
|
||||
for arg_index in arguments.size():
|
||||
arguments[arg_index] = "\nArgument: " + arguments[arg_index]
|
||||
push_error(" ".join([
|
||||
"An error occurred while executing the Aseprite command.",
|
||||
"Process exited with code %s:\nCommand: %s%s"
|
||||
]) % [exit_code, command, "".join(arguments)])
|
||||
return ERR_QUERY_FAILED
|
||||
|
||||
var raw_atlas_image: Image = Image.load_from_file(global_png_path)
|
||||
var json = JSON.new()
|
||||
err = json.parse(FileAccess.get_file_as_string(global_json_path))
|
||||
if err:
|
||||
push_error("Failed to parse sprite sheet json data with error %s \"%s\"" % [err, error_string(err)])
|
||||
return ERR_INVALID_DATA
|
||||
var raw_sprite_sheet_data: Dictionary = json.data
|
||||
|
||||
var source_image_size: Vector2i = _Common.get_vector2i(
|
||||
raw_sprite_sheet_data.frames[0].sourceSize, "w", "h")
|
||||
|
||||
if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error:
|
||||
push_warning(
|
||||
"Failed to remove unique temporary directory: \"%s\"" %
|
||||
[unique_temp_dir_path])
|
||||
|
||||
image.copy_from(raw_atlas_image.get_region(Rect2i(Vector2i.ZERO, source_image_size)))
|
||||
return OK
|
||||
1
addons/nklbdev.importality/export/aseprite.gd.uid
Normal file
1
addons/nklbdev.importality/export/aseprite.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c3qof0o2rjew
|
||||
286
addons/nklbdev.importality/export/krita.gd
Normal file
286
addons/nklbdev.importality/export/krita.gd
Normal file
@@ -0,0 +1,286 @@
|
||||
@tool
|
||||
extends "_.gd"
|
||||
|
||||
const _XML = preload("../xml.gd")
|
||||
|
||||
var __os_command_setting: _Setting = _Setting.new(
|
||||
"krita_command", "", TYPE_STRING, PROPERTY_HINT_NONE,
|
||||
"", true, func(v: String): return v.is_empty())
|
||||
|
||||
var __os_command_arguments_setting: _Setting = _Setting.new(
|
||||
"krita_command_arguments", PackedStringArray(), TYPE_PACKED_STRING_ARRAY, PROPERTY_HINT_NONE,
|
||||
"", true, func(v: PackedStringArray): return false)
|
||||
|
||||
func _init(editor_file_system: EditorFileSystem) -> void:
|
||||
var recognized_extensions: PackedStringArray = ["kra", "krita"]
|
||||
super("Krita", recognized_extensions, [], [
|
||||
__os_command_setting,
|
||||
__os_command_arguments_setting,
|
||||
], CustomImageFormatLoaderExtension.new(recognized_extensions))
|
||||
|
||||
func __validate_image_name(image_name: String) -> _Result:
|
||||
var result: _Result = _Result.new()
|
||||
var image_name_with_underscored_invalid_characters: String = image_name.validate_filename()
|
||||
var unsupported_characters: PackedStringArray
|
||||
for character_index in image_name.length():
|
||||
var validated_character = image_name_with_underscored_invalid_characters[character_index]
|
||||
if validated_character == "_":
|
||||
var original_character = image_name[character_index]
|
||||
if original_character != "_":
|
||||
if not unsupported_characters.has(original_character):
|
||||
unsupported_characters.push_back(original_character)
|
||||
if not unsupported_characters.is_empty():
|
||||
result.fail(ERR_FILE_BAD_PATH, "There are unsupported characters in Krita Document Title: \"%s\"" % ["".join(unsupported_characters)])
|
||||
return result
|
||||
|
||||
func _export(res_source_file_path: String, options: Dictionary) -> ExportResult:
|
||||
var result: ExportResult = ExportResult.new()
|
||||
var err: Error
|
||||
|
||||
var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value()
|
||||
if os_command_result.error:
|
||||
result.fail(ERR_UNCONFIGURED, "Failed to get Krita Command to export spritesheet", os_command_result)
|
||||
return result
|
||||
|
||||
var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value()
|
||||
if os_command_arguments_result.error:
|
||||
result.fail(ERR_UNCONFIGURED, "Failed to get Krita Command Arguments to export spritesheet", os_command_arguments_result)
|
||||
return result
|
||||
|
||||
var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value()
|
||||
if temp_dir_path_result.error:
|
||||
result.fail(ERR_UNCONFIGURED, "Failed to get Temporary Files Directory Path to export spritesheet", temp_dir_path_result)
|
||||
return result
|
||||
var global_temp_dir_path: String = ProjectSettings.globalize_path(
|
||||
temp_dir_path_result.value.strip_edges())
|
||||
var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \
|
||||
_DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path)
|
||||
if unique_temp_dir_creation_result.error:
|
||||
result.fail(ERR_QUERY_FAILED, "Failed to create unique temporary directory to export spritesheet", unique_temp_dir_creation_result)
|
||||
return result
|
||||
var unique_temp_dir_path: String = unique_temp_dir_creation_result.path
|
||||
|
||||
var global_source_file_path: String = ProjectSettings.globalize_path(res_source_file_path)
|
||||
|
||||
var zip_reader: ZIPReader = ZIPReader.new()
|
||||
var zip_error: Error = zip_reader.open(global_source_file_path)
|
||||
if zip_error:
|
||||
result.fail(zip_error, "Failed to open Krita file \"%s\" as ZIP archive with error: %s (%s)" % [res_source_file_path, zip_error, error_string(zip_error)])
|
||||
return result
|
||||
|
||||
var files_names_in_zip: PackedStringArray = zip_reader.get_files()
|
||||
|
||||
var maindoc_filename: String = "maindoc.xml"
|
||||
var maindoc_buffer: PackedByteArray = zip_reader.read_file(maindoc_filename)
|
||||
var maindoc_xml_root: _XML.XMLNodeRoot = _XML.parse_buffer(maindoc_buffer)
|
||||
var maindoc_doc_xml_element: _XML.XMLNodeElement = maindoc_xml_root.get_elements("DOC").front()
|
||||
|
||||
var image_xml_element: _XML.XMLNodeElement = maindoc_doc_xml_element.get_elements("IMAGE").front()
|
||||
var image_name: String = image_xml_element.get_string("name")
|
||||
var image_size: Vector2i = image_xml_element.get_vector2i("width", "height")
|
||||
var image_name_validation_result: _Result = __validate_image_name(image_name)
|
||||
if image_name_validation_result.error:
|
||||
result.fail(ERR_INVALID_DATA,
|
||||
"Krita Document Title have unsupported format",
|
||||
image_name_validation_result)
|
||||
return result
|
||||
|
||||
var has_keyframes: bool
|
||||
for layer_xml_element in image_xml_element.get_elements("layers").front().get_elements("layer"):
|
||||
if layer_xml_element.attributes.has("keyframes"):
|
||||
has_keyframes = true
|
||||
break
|
||||
if not has_keyframes:
|
||||
result.fail(ERR_INVALID_DATA, "Source file has no keyframes")
|
||||
return result
|
||||
|
||||
var animation_xml_element: _XML.XMLNodeElement = image_xml_element.get_elements("animation").front()
|
||||
var animation_framerate: int = max(1, animation_xml_element.get_elements("framerate").front().get_int("value"))
|
||||
var animation_range_xml_element: _XML.XMLNodeElement = animation_xml_element.get_elements("range").front()
|
||||
|
||||
var animation_index_filename: String = "%s/animation/index.xml" % image_name
|
||||
var animation_index_buffer: PackedByteArray = zip_reader.read_file(animation_index_filename)
|
||||
var animation_index_xml_root: _XML.XMLNodeRoot = _XML.parse_buffer(animation_index_buffer)
|
||||
var animation_index_animation_metadata_xml_element: _XML.XMLNodeElement = animation_index_xml_root.get_elements("animation-metadata").front()
|
||||
var animation_index_animation_metadata_range_xml_element: _XML.XMLNodeElement = animation_index_animation_metadata_xml_element.get_elements("range").front()
|
||||
var export_settings_xml_element: _XML.XMLNodeElement = animation_index_animation_metadata_xml_element.get_elements("export-settings").front()
|
||||
var sequence_file_path_xml_element: _XML.XMLNodeElement = export_settings_xml_element.get_elements("sequenceFilePath").front()
|
||||
var sequence_base_name_xml_element: _XML.XMLNodeElement = export_settings_xml_element.get_elements("sequenceBaseName").front()
|
||||
|
||||
var animations_parameters_parsing_results: Array[AnimationParamsParsingResult]
|
||||
var total_animations_frames_count: int
|
||||
var first_animations_frame_index: int = -1
|
||||
var last_animations_frame_index: int = -1
|
||||
var global_temp_kra_path: String
|
||||
var temp_file_base_name: String = "img"
|
||||
var temp_kra_file_name: String = temp_file_base_name + ".kra"
|
||||
var temp_png_file_name_pattern: String = temp_file_base_name + ".png"
|
||||
|
||||
var storyboard_index_file_name: String = "%s/storyboard/index.xml" % image_name
|
||||
if storyboard_index_file_name in files_names_in_zip:
|
||||
var storyboard_index_xml_root: _XML.XMLNodeRoot = _XML.parse_buffer(zip_reader.read_file("%s/storyboard/index.xml" % image_name))
|
||||
var storyboard_info_xml_element: _XML.XMLNodeElement = storyboard_index_xml_root.get_elements("storyboard-info").front()
|
||||
var storyboard_item_list_xml_element: _XML.XMLNodeElement = storyboard_info_xml_element.get_elements("StoryboardItemList").front()
|
||||
var storyboard_item_xml_elements: Array[_XML.XMLNodeElement] = storyboard_item_list_xml_element.get_elements("storyboarditem")
|
||||
var unique_animations_names: PackedStringArray
|
||||
|
||||
for animation_index in storyboard_item_xml_elements.size():
|
||||
var story_xml_element: _XML.XMLNodeElement = storyboard_item_xml_elements[animation_index]
|
||||
var animation_first_frame: int = story_xml_element.get_int("frame")
|
||||
var animation_params_parsing_result: AnimationParamsParsingResult = _parse_animation_params(
|
||||
story_xml_element.get_string("item-name").strip_edges(),
|
||||
AnimationOptions.Direction | AnimationOptions.RepeatCount,
|
||||
animation_first_frame,
|
||||
story_xml_element.get_int("duration-frame") + \
|
||||
animation_framerate * story_xml_element.get_int("duration-second"))
|
||||
if animation_params_parsing_result.error:
|
||||
result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters",
|
||||
animation_params_parsing_result)
|
||||
return result
|
||||
if unique_animations_names.has(animation_params_parsing_result.name):
|
||||
result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" %
|
||||
[animation_params_parsing_result.name, animation_index])
|
||||
return result
|
||||
unique_animations_names.push_back(animation_params_parsing_result.name)
|
||||
animations_parameters_parsing_results.push_back(animation_params_parsing_result)
|
||||
total_animations_frames_count += animation_params_parsing_result.frames_count
|
||||
if first_animations_frame_index < 0 or animation_params_parsing_result.first_frame_index < first_animations_frame_index:
|
||||
first_animations_frame_index = animation_params_parsing_result.first_frame_index
|
||||
var animation_last_frame_index: int = animation_params_parsing_result.first_frame_index + animation_params_parsing_result.frames_count - 1
|
||||
if last_animations_frame_index < 0 or animation_last_frame_index > last_animations_frame_index:
|
||||
last_animations_frame_index = animation_last_frame_index
|
||||
|
||||
animation_range_xml_element.attributes["from"] = str(first_animations_frame_index)
|
||||
animation_range_xml_element.attributes["to"] = str(last_animations_frame_index)
|
||||
|
||||
global_temp_kra_path = unique_temp_dir_path.path_join(temp_kra_file_name)
|
||||
|
||||
animation_index_animation_metadata_range_xml_element.attributes["from"] = str(first_animations_frame_index)
|
||||
animation_index_animation_metadata_range_xml_element.attributes["to"] = str(last_animations_frame_index)
|
||||
|
||||
var zip_writer = ZIPPacker.new()
|
||||
zip_writer.open(global_temp_kra_path, ZIPPacker.APPEND_CREATE)
|
||||
for filename in zip_reader.get_files():
|
||||
zip_writer.start_file(filename)
|
||||
match filename:
|
||||
maindoc_filename:
|
||||
zip_writer.write_file(maindoc_xml_root.dump_to_buffer())
|
||||
animation_index_filename:
|
||||
zip_writer.write_file(animation_index_xml_root.dump_to_buffer())
|
||||
_: zip_writer.write_file(zip_reader.read_file(filename))
|
||||
zip_writer.close_file()
|
||||
zip_writer.close()
|
||||
else:
|
||||
first_animations_frame_index = animation_range_xml_element.get_int("from")
|
||||
last_animations_frame_index = animation_range_xml_element.get_int("to")
|
||||
total_animations_frames_count = last_animations_frame_index - first_animations_frame_index + 1
|
||||
var default_animation_params_parsing_result: AnimationParamsParsingResult = AnimationParamsParsingResult.new()
|
||||
default_animation_params_parsing_result.name = options[_Options.DEFAULT_ANIMATION_NAME].strip_edges()
|
||||
if not default_animation_params_parsing_result.name:
|
||||
default_animation_params_parsing_result.name = "default"
|
||||
default_animation_params_parsing_result.first_frame_index = first_animations_frame_index
|
||||
default_animation_params_parsing_result.frames_count = last_animations_frame_index - first_animations_frame_index + 1
|
||||
default_animation_params_parsing_result.direction = options[_Options.DEFAULT_ANIMATION_DIRECTION]
|
||||
default_animation_params_parsing_result.repeat_count = options[_Options.DEFAULT_ANIMATION_REPEAT_COUNT]
|
||||
animations_parameters_parsing_results.push_back(default_animation_params_parsing_result)
|
||||
global_temp_kra_path = global_source_file_path
|
||||
|
||||
zip_reader.close()
|
||||
|
||||
var global_temp_png_path_pattern: String = unique_temp_dir_path.path_join(temp_png_file_name_pattern)
|
||||
|
||||
var command: String = os_command_result.value.strip_edges()
|
||||
var arguments: PackedStringArray = \
|
||||
os_command_arguments_result.value + \
|
||||
PackedStringArray([
|
||||
"--export-sequence",
|
||||
"--export-filename", global_temp_png_path_pattern,
|
||||
global_temp_kra_path])
|
||||
|
||||
var output: Array
|
||||
var exit_code: int = OS.execute(command, arguments, output, true, false)
|
||||
if exit_code:
|
||||
for arg_index in arguments.size():
|
||||
arguments[arg_index] = "\nArgument: " + arguments[arg_index]
|
||||
result.fail(ERR_QUERY_FAILED, " ".join([
|
||||
"An error occurred while executing the Krita command.",
|
||||
"Process exited with code %s:\nCommand: %s%s"
|
||||
]) % [exit_code, command, "".join(arguments)])
|
||||
return result
|
||||
|
||||
var unique_frames_count: int = last_animations_frame_index + 1 # - first_stories_frame
|
||||
var frames_images: Array[Image]
|
||||
for image_idx in unique_frames_count:
|
||||
var global_frame_png_path: String = unique_temp_dir_path \
|
||||
.path_join("%s%04d.png" % [temp_file_base_name, image_idx])
|
||||
if FileAccess.file_exists(global_frame_png_path):
|
||||
var image: Image = Image.load_from_file(global_frame_png_path)
|
||||
frames_images.push_back(image)
|
||||
else:
|
||||
frames_images.push_back(frames_images.back())
|
||||
|
||||
var sprite_sheet_builder: _SpriteSheetBuilderBase = _create_sprite_sheet_builder(options)
|
||||
|
||||
var sprite_sheet_building_result: _SpriteSheetBuilderBase.SpriteSheetBuildingResult = sprite_sheet_builder.build_sprite_sheet(frames_images)
|
||||
if sprite_sheet_building_result.error:
|
||||
result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result)
|
||||
return result
|
||||
var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet
|
||||
|
||||
var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new()
|
||||
var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges()
|
||||
|
||||
var frames_duration: float = 1.0 / animation_framerate
|
||||
var all_frames: Array[_Common.FrameInfo]
|
||||
all_frames.resize(unique_frames_count)
|
||||
for animation_index in animations_parameters_parsing_results.size():
|
||||
var animation_params_parsing_result: AnimationParamsParsingResult = animations_parameters_parsing_results[animation_index]
|
||||
var animation = _Common.AnimationInfo.new()
|
||||
animation.name = animation_params_parsing_result.name
|
||||
if animation.name == autoplay_animation_name:
|
||||
animation_library.autoplay_index = animation_index
|
||||
animation.direction = animation_params_parsing_result.direction
|
||||
if animation.direction < 0:
|
||||
animation.direction = _Common.AnimationDirection.FORWARD
|
||||
animation.repeat_count = animation_params_parsing_result.repeat_count
|
||||
if animation.repeat_count < 0:
|
||||
animation.repeat_count = 1
|
||||
for animation_frame_index in animation_params_parsing_result.frames_count:
|
||||
var global_frame_index: int = animation_params_parsing_result.first_frame_index + animation_frame_index
|
||||
var frame: _Common.FrameInfo = all_frames[global_frame_index]
|
||||
if frame == null:
|
||||
frame = _Common.FrameInfo.new()
|
||||
frame.sprite = sprite_sheet.sprites[global_frame_index]
|
||||
frame.duration = frames_duration
|
||||
all_frames[global_frame_index] = frame
|
||||
animation.frames.push_back(frame)
|
||||
animation_library.animations.push_back(animation)
|
||||
|
||||
if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0:
|
||||
push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name])
|
||||
|
||||
if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error:
|
||||
push_warning(
|
||||
"Failed to remove unique temporary directory: \"%s\"" %
|
||||
[unique_temp_dir_path])
|
||||
|
||||
result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library)
|
||||
return result
|
||||
|
||||
class CustomImageFormatLoaderExtension:
|
||||
extends ImageFormatLoaderExtension
|
||||
|
||||
var __recognized_extensions: PackedStringArray
|
||||
|
||||
func _init(recognized_extensions: PackedStringArray) -> void:
|
||||
__recognized_extensions = recognized_extensions
|
||||
|
||||
func _get_recognized_extensions() -> PackedStringArray:
|
||||
return __recognized_extensions
|
||||
|
||||
func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error:
|
||||
var zip_reader := ZIPReader.new()
|
||||
zip_reader.open(file_access.get_path_absolute())
|
||||
image.load_png_from_buffer(zip_reader.read_file("mergedimage.png"))
|
||||
zip_reader.close()
|
||||
return OK
|
||||
1
addons/nklbdev.importality/export/krita.gd.uid
Normal file
1
addons/nklbdev.importality/export/krita.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bnpwq4kns36g6
|
||||
267
addons/nklbdev.importality/export/pencil2d.gd
Normal file
267
addons/nklbdev.importality/export/pencil2d.gd
Normal file
@@ -0,0 +1,267 @@
|
||||
@tool
|
||||
extends "_.gd"
|
||||
|
||||
const _XML = preload("../xml.gd")
|
||||
|
||||
var __os_command_setting: _Setting = _Setting.new(
|
||||
"pencil2d_command", "", TYPE_STRING, PROPERTY_HINT_NONE,
|
||||
"", true, func(v: String): return v.is_empty())
|
||||
|
||||
var __os_command_arguments_setting: _Setting = _Setting.new(
|
||||
"pencil2d_command_arguments", PackedStringArray(), TYPE_PACKED_STRING_ARRAY, PROPERTY_HINT_NONE,
|
||||
"", true, func(v: PackedStringArray): return false)
|
||||
|
||||
const __ANIMATIONS_PARAMETERS_OPTION: StringName = "pencil2d/animations_parameters"
|
||||
|
||||
func _init(editor_file_system: EditorFileSystem) -> void:
|
||||
var recognized_extensions: PackedStringArray = ["pclx"]
|
||||
super("Pencil2D", recognized_extensions, [
|
||||
_Options.create_option(__ANIMATIONS_PARAMETERS_OPTION, PackedStringArray(),
|
||||
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT)],
|
||||
[ __os_command_setting, __os_command_arguments_setting ],
|
||||
CustomImageFormatLoaderExtension.new(
|
||||
recognized_extensions,
|
||||
__os_command_setting,
|
||||
__os_command_arguments_setting,
|
||||
_Common.common_temporary_files_directory_path_setting))
|
||||
|
||||
func _export(res_source_file_path: String, options: Dictionary) -> ExportResult:
|
||||
var result: ExportResult = ExportResult.new()
|
||||
var err: Error
|
||||
|
||||
var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value()
|
||||
if os_command_result.error:
|
||||
result.fail(ERR_UNCONFIGURED, "Failed to get Pencil2D Command to export spritesheet", os_command_result)
|
||||
return result
|
||||
|
||||
var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value()
|
||||
if os_command_arguments_result.error:
|
||||
result.fail(ERR_UNCONFIGURED, "Failed to get Pencil2D Command Arguments to export spritesheet", os_command_arguments_result)
|
||||
return result
|
||||
|
||||
var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value()
|
||||
if temp_dir_path_result.error:
|
||||
result.fail(ERR_UNCONFIGURED, "Failed to get Temporary Files Directory Path to export spritesheet", temp_dir_path_result)
|
||||
return result
|
||||
var global_temp_dir_path: String = ProjectSettings.globalize_path(
|
||||
temp_dir_path_result.value.strip_edges())
|
||||
var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \
|
||||
_DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path)
|
||||
if unique_temp_dir_creation_result.error:
|
||||
result.fail(ERR_QUERY_FAILED, "Failed to create unique temporary directory to export spritesheet", unique_temp_dir_creation_result)
|
||||
return result
|
||||
var unique_temp_dir_path: String = unique_temp_dir_creation_result.path
|
||||
|
||||
var global_source_file_path: String = ProjectSettings.globalize_path(res_source_file_path)
|
||||
|
||||
var zip_reader: ZIPReader = ZIPReader.new()
|
||||
var zip_error: Error = zip_reader.open(global_source_file_path)
|
||||
if zip_error:
|
||||
result.fail(zip_error, "Failed to open Pencil2D file \"%s\" as ZIP archive with error: %s (%s)" % [res_source_file_path, zip_error, error_string(zip_error)])
|
||||
return result
|
||||
var buffer: PackedByteArray = zip_reader.read_file("main.xml")
|
||||
var main_xml_root: _XML.XMLNodeRoot = _XML.parse_buffer(buffer)
|
||||
zip_reader.close()
|
||||
var animation_framerate: int = main_xml_root \
|
||||
.get_elements("document").front() \
|
||||
.get_elements("projectdata").front() \
|
||||
.get_elements("fps").front() \
|
||||
.get_int("value")
|
||||
|
||||
var raw_animations_params_list: PackedStringArray = options[__ANIMATIONS_PARAMETERS_OPTION]
|
||||
var animations_params_parsing_results: Array[AnimationParamsParsingResult]
|
||||
animations_params_parsing_results.resize(raw_animations_params_list.size())
|
||||
var unique_animations_names: PackedStringArray
|
||||
var frame_indices_to_export
|
||||
var unique_frames_count: int = 0
|
||||
var animation_first_frame_index: int = 0
|
||||
for animation_index in raw_animations_params_list.size():
|
||||
var raw_animation_params: String = raw_animations_params_list[animation_index]
|
||||
var animation_params_parsing_result: AnimationParamsParsingResult = _parse_animation_params(
|
||||
raw_animation_params,
|
||||
AnimationOptions.FramesCount | AnimationOptions.Direction | AnimationOptions.RepeatCount,
|
||||
animation_first_frame_index)
|
||||
if animation_params_parsing_result.error:
|
||||
result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters", animation_params_parsing_result)
|
||||
return result
|
||||
if unique_animations_names.has(animation_params_parsing_result.name):
|
||||
result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" %
|
||||
[animation_params_parsing_result.name, animation_index])
|
||||
return result
|
||||
unique_animations_names.push_back(animation_params_parsing_result.name)
|
||||
unique_frames_count += animation_params_parsing_result.frames_count
|
||||
animation_first_frame_index += animation_params_parsing_result.frames_count
|
||||
animations_params_parsing_results[animation_index] = animation_params_parsing_result
|
||||
|
||||
# -o --export <output_path> Render the file to <output_path>
|
||||
# --camera <layer_name> Name of the camera layer to use
|
||||
# --width <integer> Width of the output frames
|
||||
# --height <integer> Height of the output frames
|
||||
# --start <frame> The first frame you want to include in the exported movie
|
||||
# --end <frame> The last frame you want to include in the exported movie. Can also be last or last-sound to automatically use the last frame containing animation or sound respectively
|
||||
# --transparency Render transparency when possible
|
||||
# input Path to input pencil file
|
||||
var png_base_name: String = "temp"
|
||||
var global_temp_png_path: String = unique_temp_dir_path.path_join("%s.png" % png_base_name)
|
||||
|
||||
var command: String = os_command_result.value.strip_edges()
|
||||
var arguments: PackedStringArray = \
|
||||
os_command_arguments_result.value + \
|
||||
PackedStringArray([
|
||||
"--export", global_temp_png_path,
|
||||
"--start", 1,
|
||||
"--end", unique_frames_count,
|
||||
"--transparency",
|
||||
global_source_file_path])
|
||||
|
||||
var output: Array
|
||||
var exit_code: int = OS.execute(command, arguments, output, true, false)
|
||||
if exit_code:
|
||||
for arg_index in arguments.size():
|
||||
arguments[arg_index] = "\nArgument: " + arguments[arg_index]
|
||||
result.fail(ERR_QUERY_FAILED, " ".join([
|
||||
"An error occurred while executing the Pencil2D command.",
|
||||
"Process exited with code %s:\nCommand: %s%s"
|
||||
]) % [exit_code, command, "".join(arguments)])
|
||||
return result
|
||||
|
||||
var frames_images: Array[Image]
|
||||
for image_idx in unique_frames_count:
|
||||
var global_frame_png_path: String = unique_temp_dir_path \
|
||||
.path_join("%s%04d.png" % [png_base_name, image_idx + 1])
|
||||
frames_images.push_back(Image.load_from_file(global_frame_png_path))
|
||||
|
||||
var sprite_sheet_builder: _SpriteSheetBuilderBase = _create_sprite_sheet_builder(options)
|
||||
|
||||
var sprite_sheet_building_result: _SpriteSheetBuilderBase.SpriteSheetBuildingResult = sprite_sheet_builder.build_sprite_sheet(frames_images)
|
||||
if sprite_sheet_building_result.error:
|
||||
result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result)
|
||||
return result
|
||||
var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet
|
||||
|
||||
var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new()
|
||||
var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges()
|
||||
|
||||
var frames_duration: float = 1.0 / animation_framerate
|
||||
var all_frames: Array[_Common.FrameInfo]
|
||||
all_frames.resize(unique_frames_count)
|
||||
for animation_index in animations_params_parsing_results.size():
|
||||
var animation_params_parsing_result: AnimationParamsParsingResult = animations_params_parsing_results[animation_index]
|
||||
var animation = _Common.AnimationInfo.new()
|
||||
animation.name = animation_params_parsing_result.name
|
||||
if animation.name == autoplay_animation_name:
|
||||
animation_library.autoplay_index = animation_index
|
||||
animation.direction = animation_params_parsing_result.direction
|
||||
if animation.direction < 0:
|
||||
animation.direction = _Common.AnimationDirection.FORWARD
|
||||
animation.repeat_count = animation_params_parsing_result.repeat_count
|
||||
if animation.repeat_count < 0:
|
||||
animation.repeat_count = 1
|
||||
for animation_frame_index in animation_params_parsing_result.frames_count:
|
||||
var global_frame_index: int = animation_params_parsing_result.first_frame_index + animation_frame_index
|
||||
var frame: _Common.FrameInfo = all_frames[global_frame_index]
|
||||
if frame == null:
|
||||
frame = _Common.FrameInfo.new()
|
||||
frame.sprite = sprite_sheet.sprites[global_frame_index]
|
||||
frame.duration = frames_duration
|
||||
all_frames[global_frame_index] = frame
|
||||
animation.frames.push_back(frame)
|
||||
animation_library.animations.push_back(animation)
|
||||
if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0:
|
||||
push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name])
|
||||
|
||||
if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error:
|
||||
push_warning(
|
||||
"Failed to remove unique temporary directory: \"%s\"" %
|
||||
[unique_temp_dir_path])
|
||||
|
||||
result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library)
|
||||
return result
|
||||
|
||||
class CustomImageFormatLoaderExtension:
|
||||
extends ImageFormatLoaderExtension
|
||||
|
||||
var __recognized_extensions: PackedStringArray
|
||||
var __os_command_setting: _Setting
|
||||
var __os_command_arguments_setting: _Setting
|
||||
var __common_temporary_files_directory_path_setting: _Setting
|
||||
|
||||
func _init(recognized_extensions: PackedStringArray,
|
||||
os_command_setting: _Setting,
|
||||
os_command_arguments_setting: _Setting,
|
||||
common_temporary_files_directory_path: _Setting,
|
||||
) -> void:
|
||||
__recognized_extensions = recognized_extensions
|
||||
__os_command_setting = os_command_setting
|
||||
__os_command_arguments_setting = os_command_arguments_setting
|
||||
__common_temporary_files_directory_path_setting = common_temporary_files_directory_path
|
||||
|
||||
func _get_recognized_extensions() -> PackedStringArray:
|
||||
return __recognized_extensions
|
||||
|
||||
func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error:
|
||||
var err: Error
|
||||
|
||||
var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value()
|
||||
if os_command_result.error:
|
||||
push_error(os_command_result.error_description)
|
||||
return os_command_result.error
|
||||
|
||||
var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value()
|
||||
if os_command_arguments_result.error:
|
||||
push_error(os_command_arguments_result.error_description)
|
||||
return os_command_arguments_result.error
|
||||
|
||||
var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value()
|
||||
if temp_dir_path_result.error:
|
||||
push_error("Failed to get Temporary Files Directory Path to export spritesheet")
|
||||
return temp_dir_path_result.error
|
||||
var global_temp_dir_path: String = ProjectSettings.globalize_path(
|
||||
temp_dir_path_result.value.strip_edges())
|
||||
var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \
|
||||
_DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path)
|
||||
if unique_temp_dir_creation_result.error:
|
||||
push_error("Failed to create unique temporary directory to export spritesheet")
|
||||
return unique_temp_dir_creation_result.error
|
||||
var unique_temp_dir_path: String = unique_temp_dir_creation_result.path
|
||||
|
||||
var global_source_file_path: String = ProjectSettings.globalize_path(file_access.get_path())
|
||||
|
||||
const png_base_name: String = "img"
|
||||
var global_temp_png_path: String = unique_temp_dir_path.path_join("%s.png" % png_base_name)
|
||||
|
||||
var command: String = os_command_result.value.strip_edges()
|
||||
var arguments: PackedStringArray = \
|
||||
os_command_arguments_result.value + \
|
||||
PackedStringArray([
|
||||
"--export", global_temp_png_path,
|
||||
"--start", 1,
|
||||
"--end", 1,
|
||||
"--transparency",
|
||||
global_source_file_path])
|
||||
|
||||
var output: Array
|
||||
var exit_code: int = OS.execute(command, arguments, output, true, false)
|
||||
if exit_code:
|
||||
for arg_index in arguments.size():
|
||||
arguments[arg_index] = "\nArgument: " + arguments[arg_index]
|
||||
push_error(" ".join([
|
||||
"An error occurred while executing the Pencil2D command.",
|
||||
"Process exited with code %s:\nCommand: %s%s"
|
||||
]) % [exit_code, command, "".join(arguments)])
|
||||
return ERR_QUERY_FAILED
|
||||
|
||||
var global_frame_png_path: String = unique_temp_dir_path \
|
||||
.path_join("%s0001.png" % [png_base_name])
|
||||
err = image.load_png_from_buffer(FileAccess.get_file_as_bytes(global_frame_png_path))
|
||||
if err:
|
||||
push_error("An error occurred while image loading")
|
||||
return err
|
||||
|
||||
if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error:
|
||||
push_warning(
|
||||
"Failed to remove unique temporary directory: \"%s\"" %
|
||||
[unique_temp_dir_path])
|
||||
|
||||
return OK
|
||||
|
||||
1
addons/nklbdev.importality/export/pencil2d.gd.uid
Normal file
1
addons/nklbdev.importality/export/pencil2d.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cgsbagkpxk5y7
|
||||
149
addons/nklbdev.importality/export/piskel.gd
Normal file
149
addons/nklbdev.importality/export/piskel.gd
Normal file
@@ -0,0 +1,149 @@
|
||||
@tool
|
||||
extends "_.gd"
|
||||
|
||||
const __ANIMATIONS_PARAMETERS_OPTION: StringName = "piskel/animations_parameters"
|
||||
|
||||
func _init(editor_file_system: EditorFileSystem) -> void:
|
||||
var recognized_extensions: PackedStringArray = ["piskel"]
|
||||
super("Piskel", recognized_extensions, [
|
||||
_Options.create_option(__ANIMATIONS_PARAMETERS_OPTION, PackedStringArray(),
|
||||
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT)],
|
||||
[],
|
||||
CustomImageFormatLoaderExtension.new(recognized_extensions))
|
||||
|
||||
func _export(res_source_file_path: String, options: Dictionary) -> ExportResult:
|
||||
var result: ExportResult = ExportResult.new()
|
||||
|
||||
var raw_animations_params_list: PackedStringArray = options[__ANIMATIONS_PARAMETERS_OPTION]
|
||||
var animations_params_parsing_results: Array[AnimationParamsParsingResult]
|
||||
animations_params_parsing_results.resize(raw_animations_params_list.size())
|
||||
var unique_animations_names: PackedStringArray
|
||||
var frame_indices_to_export
|
||||
var unique_frames_count: int = 0
|
||||
var animation_first_frame_index: int = 0
|
||||
for animation_index in raw_animations_params_list.size():
|
||||
var raw_animation_params: String = raw_animations_params_list[animation_index]
|
||||
var animation_params_parsing_result: AnimationParamsParsingResult = _parse_animation_params(
|
||||
raw_animation_params,
|
||||
AnimationOptions.FramesCount | AnimationOptions.Direction | AnimationOptions.RepeatCount,
|
||||
animation_first_frame_index)
|
||||
if animation_params_parsing_result.error:
|
||||
result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters", animation_params_parsing_result)
|
||||
return result
|
||||
if unique_animations_names.has(animation_params_parsing_result.name):
|
||||
result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" %
|
||||
[animation_params_parsing_result.name, animation_index])
|
||||
return result
|
||||
unique_animations_names.push_back(animation_params_parsing_result.name)
|
||||
unique_frames_count += animation_params_parsing_result.frames_count
|
||||
animation_first_frame_index += animation_params_parsing_result.frames_count
|
||||
animations_params_parsing_results[animation_index] = animation_params_parsing_result
|
||||
|
||||
var document: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(res_source_file_path))
|
||||
document.modelVersion #int 2
|
||||
var piskel: Dictionary = document.piskel
|
||||
piskel.name #string New Piskel
|
||||
piskel.description #string asdfasdfasdf
|
||||
piskel.fps #int 12,
|
||||
var image_size: Vector2i = Vector2i(piskel.width, piskel.height)
|
||||
# piskel.hiddenFrames#Array may absend
|
||||
var blended_layers: Image
|
||||
var layer_image: Image = Image.new()
|
||||
var frames_count: int
|
||||
var layer_image_size: Vector2i = image_size
|
||||
for layer_string in piskel.layers: #Array
|
||||
var layer: Dictionary = JSON.parse_string(layer_string)
|
||||
layer.name #string layer 1
|
||||
layer.opacity #float 1
|
||||
if frames_count == 0:
|
||||
frames_count = layer.frameCount
|
||||
layer_image_size.x = image_size.x * frames_count
|
||||
else:
|
||||
assert(frames_count == layer.frameCount)
|
||||
for chunk in layer.chunks:
|
||||
# chunk.layout # array [ [ 0 ], [ 1 ], [ 2 ] ]
|
||||
layer_image.load_png_from_buffer(Marshalls.base64_to_raw(chunk.base64PNG.trim_prefix("data:image/png;base64,")))
|
||||
assert(layer_image.get_size() == layer_image_size)
|
||||
if blended_layers == null:
|
||||
blended_layers = layer_image
|
||||
layer_image = Image.new()
|
||||
else:
|
||||
blended_layers.blend_rect(layer_image, Rect2i(Vector2i.ZERO, layer_image.get_size()), Vector2i.ZERO)
|
||||
|
||||
var frames_images: Array[Image]
|
||||
frames_images.resize(frames_count)
|
||||
for frame_index in frames_count:
|
||||
frames_images[frame_index] = blended_layers.get_region(
|
||||
Rect2i(Vector2i(frame_index * image_size.x, 0), image_size))
|
||||
|
||||
var sprite_sheet_builder: _SpriteSheetBuilderBase = _create_sprite_sheet_builder(options)
|
||||
|
||||
var sprite_sheet_building_result: _SpriteSheetBuilderBase.SpriteSheetBuildingResult = \
|
||||
sprite_sheet_builder.build_sprite_sheet(frames_images)
|
||||
if sprite_sheet_building_result.error:
|
||||
result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result)
|
||||
return result
|
||||
var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet
|
||||
|
||||
var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new()
|
||||
var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges()
|
||||
|
||||
var frames_duration: float = 1.0 / piskel.fps
|
||||
var all_frames: Array[_Common.FrameInfo]
|
||||
all_frames.resize(unique_frames_count)
|
||||
for animation_index in animations_params_parsing_results.size():
|
||||
var animation_params_parsing_result: AnimationParamsParsingResult = animations_params_parsing_results[animation_index]
|
||||
var animation = _Common.AnimationInfo.new()
|
||||
animation.name = animation_params_parsing_result.name
|
||||
if animation.name == autoplay_animation_name:
|
||||
animation_library.autoplay_index = animation_index
|
||||
animation.direction = animation_params_parsing_result.direction
|
||||
if animation.direction < 0:
|
||||
animation.direction = _Common.AnimationDirection.FORWARD
|
||||
animation.repeat_count = animation_params_parsing_result.repeat_count
|
||||
if animation.repeat_count < 0:
|
||||
animation.repeat_count = 1
|
||||
for animation_frame_index in animation_params_parsing_result.frames_count:
|
||||
var global_frame_index: int = animation_params_parsing_result.first_frame_index + animation_frame_index
|
||||
var frame: _Common.FrameInfo = all_frames[global_frame_index]
|
||||
if frame == null:
|
||||
frame = _Common.FrameInfo.new()
|
||||
frame.sprite = sprite_sheet.sprites[global_frame_index]
|
||||
frame.duration = frames_duration
|
||||
all_frames[global_frame_index] = frame
|
||||
animation.frames.push_back(frame)
|
||||
animation_library.animations.push_back(animation)
|
||||
if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0:
|
||||
push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name])
|
||||
|
||||
result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library)
|
||||
return result
|
||||
|
||||
class CustomImageFormatLoaderExtension:
|
||||
extends ImageFormatLoaderExtension
|
||||
|
||||
var __recognized_extensions: PackedStringArray
|
||||
|
||||
func _init(recognized_extensions: PackedStringArray) -> void:
|
||||
__recognized_extensions = recognized_extensions
|
||||
|
||||
func _get_recognized_extensions() -> PackedStringArray:
|
||||
return __recognized_extensions
|
||||
|
||||
func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error:
|
||||
|
||||
var document: Dictionary = JSON.parse_string(file_access.get_as_text())
|
||||
var piskel: Dictionary = document.piskel
|
||||
var image_size: Vector2i = Vector2i(piskel.width, piskel.height)
|
||||
var image_rect: Rect2i = Rect2i(Vector2i.ZERO, image_size)
|
||||
image.set_data(1, 1, false, Image.FORMAT_RGBA8, [0, 0, 0, 0])
|
||||
image.resize(image_size.x, image_size.y)
|
||||
var layer_image: Image = Image.new()
|
||||
for layer_string in piskel.layers: #Array
|
||||
var layer: Dictionary = JSON.parse_string(layer_string)
|
||||
layer.opacity #float 1
|
||||
for chunk in layer.chunks:
|
||||
# chunk.layout # array [ [ 0 ], [ 1 ], [ 2 ] ]
|
||||
layer_image.load_png_from_buffer(Marshalls.base64_to_raw(chunk.base64PNG.trim_prefix("data:image/png;base64,")))
|
||||
image.blend_rect(layer_image, image_rect, Vector2i.ZERO)
|
||||
return OK
|
||||
1
addons/nklbdev.importality/export/piskel.gd.uid
Normal file
1
addons/nklbdev.importality/export/piskel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://q3joovito34w
|
||||
225
addons/nklbdev.importality/export/pixelorama.gd
Normal file
225
addons/nklbdev.importality/export/pixelorama.gd
Normal file
@@ -0,0 +1,225 @@
|
||||
@tool
|
||||
extends "_.gd"
|
||||
|
||||
enum PxoLayerType {
|
||||
PIXEL_LAYER = 0,
|
||||
GROUP_LAYER = 1,
|
||||
LAYER_3D = 2,
|
||||
}
|
||||
|
||||
func _init(editor_file_system: EditorFileSystem) -> void:
|
||||
var recognized_extensions: PackedStringArray = ["pxo"]
|
||||
super("Pixelorama", recognized_extensions, [
|
||||
], [
|
||||
# settings
|
||||
], CustomImageFormatLoaderExtension.new(recognized_extensions))
|
||||
|
||||
func _export(res_source_file_path: String, options: Dictionary) -> ExportResult:
|
||||
var result: ExportResult = ExportResult.new()
|
||||
|
||||
var file: FileAccess = FileAccess.open_compressed(res_source_file_path, FileAccess.READ, FileAccess.COMPRESSION_ZSTD)
|
||||
if file == null or file.get_open_error() == ERR_FILE_UNRECOGNIZED:
|
||||
file = FileAccess.open(res_source_file_path, FileAccess.READ)
|
||||
if file == null:
|
||||
result.fail(ERR_FILE_CANT_OPEN, "Failed to open file with unknown error")
|
||||
return result
|
||||
var open_error: Error = file.get_open_error()
|
||||
if open_error:
|
||||
result.fail(ERR_FILE_CANT_OPEN, "Failed to open file with error: %s \"%s\"" % [open_error, error_string(open_error)])
|
||||
return result
|
||||
|
||||
var first_line: String = file.get_line()
|
||||
var images_data: PackedByteArray = file.get_buffer(file.get_length() - file.get_position())
|
||||
file.close()
|
||||
|
||||
var pxo_project: Dictionary = JSON.parse_string(first_line)
|
||||
var image_size: Vector2i = Vector2i(pxo_project.size_x, pxo_project.size_y)
|
||||
var pxo_cel_image_buffer_size: int = image_size.x * image_size.y * 4
|
||||
var pxo_cel_image_buffer_offset: int
|
||||
var pxo_cel_image: Image = Image.create(image_size.x, image_size.y, false, Image.FORMAT_RGBA8)
|
||||
var pixel_layers_count: int
|
||||
for pxo_layer in pxo_project.layers:
|
||||
if pxo_layer.type == PxoLayerType.PIXEL_LAYER:
|
||||
pixel_layers_count += 1
|
||||
|
||||
var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges()
|
||||
var unique_frames_indices_by_frame_index: Dictionary
|
||||
var unique_frames: Array[_Common.FrameInfo]
|
||||
var unique_frames_images: Array[Image]
|
||||
var unique_frames_count: int
|
||||
var pixel_layer_index: int
|
||||
var image_rect: Rect2i = Rect2i(Vector2i.ZERO, image_size)
|
||||
var frame: _Common.FrameInfo
|
||||
|
||||
|
||||
var is_animation_default: bool = pxo_project.tags.is_empty()
|
||||
if is_animation_default:
|
||||
var default_animation_name: String = options[_Options.DEFAULT_ANIMATION_NAME].strip_edges()
|
||||
if default_animation_name.is_empty():
|
||||
default_animation_name = "default"
|
||||
pxo_project.tags.push_back({
|
||||
name = default_animation_name,
|
||||
from = 1,
|
||||
to = pxo_project.frames.size()})
|
||||
var animations_count: int = pxo_project.tags.size()
|
||||
|
||||
var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new()
|
||||
animation_library.animations.resize(animations_count)
|
||||
var pxo_cel_opacity: float
|
||||
var unique_animations_names: PackedStringArray
|
||||
for animation_index in animations_count:
|
||||
var pxo_tag: Dictionary = pxo_project.tags[animation_index]
|
||||
var animation: _Common.AnimationInfo = _Common.AnimationInfo.new()
|
||||
animation_library.animations[animation_index] = animation
|
||||
var animation_frames_count: int = pxo_tag.to + 1 - pxo_tag.from
|
||||
if is_animation_default:
|
||||
animation.name = pxo_tag.name
|
||||
if animation.name == autoplay_animation_name:
|
||||
animation_library.autoplay_index = animation_index
|
||||
animation.direction = options[_Options.DEFAULT_ANIMATION_DIRECTION]
|
||||
animation.repeat_count = options[_Options.DEFAULT_ANIMATION_REPEAT_COUNT]
|
||||
else:
|
||||
var animation_params_parsing_result: AnimationParamsParsingResult = _parse_animation_params(
|
||||
pxo_tag.name, AnimationOptions.Direction | AnimationOptions.RepeatCount,
|
||||
pxo_tag.from, animation_frames_count)
|
||||
if animation_params_parsing_result.error:
|
||||
result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters",
|
||||
animation_params_parsing_result)
|
||||
return result
|
||||
if unique_animations_names.has(animation_params_parsing_result.name):
|
||||
result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" %
|
||||
[animation_params_parsing_result.name, animation_index])
|
||||
return result
|
||||
unique_animations_names.push_back(animation_params_parsing_result.name)
|
||||
animation.name = animation_params_parsing_result.name
|
||||
if animation.name == autoplay_animation_name:
|
||||
animation_library.autoplay_index = animation_index
|
||||
animation.direction = animation_params_parsing_result.direction
|
||||
if animation.direction < 0:
|
||||
animation.direction = _Common.AnimationDirection.FORWARD
|
||||
animation.repeat_count = animation_params_parsing_result.repeat_count
|
||||
if animation.repeat_count < 0:
|
||||
animation.repeat_count = 1
|
||||
|
||||
animation.frames.resize(animation_frames_count)
|
||||
|
||||
var frame_image: Image
|
||||
for animation_frame_index in animation_frames_count:
|
||||
var frame_index: int = pxo_tag.from - 1 + animation_frame_index
|
||||
var unique_frame_index: int = unique_frames_indices_by_frame_index.get(frame_index, -1)
|
||||
if unique_frame_index >= 0:
|
||||
frame = unique_frames[unique_frame_index]
|
||||
else:
|
||||
frame = _Common.FrameInfo.new()
|
||||
unique_frames.push_back(frame)
|
||||
frame_image = Image.create(image_size.x, image_size.y, false, Image.FORMAT_RGBA8)
|
||||
unique_frames_images.push_back(frame_image)
|
||||
unique_frame_index = unique_frames_count
|
||||
unique_frames_count += 1
|
||||
pixel_layer_index = -1
|
||||
var pxo_frame: Dictionary = pxo_project.frames[frame_index]
|
||||
frame.duration = pxo_frame.duration / pxo_project.fps
|
||||
for cel_index in pxo_frame.cels.size():
|
||||
var pxo_cel = pxo_frame.cels[cel_index]
|
||||
var pxo_layer = pxo_project.layers[cel_index]
|
||||
if pxo_layer.type == PxoLayerType.PIXEL_LAYER:
|
||||
pixel_layer_index += 1
|
||||
var l: Dictionary = pxo_layer
|
||||
while l.parent >= 0 and pxo_layer.visible:
|
||||
if not l.visible:
|
||||
pxo_layer.visible = false
|
||||
break
|
||||
l = pxo_project.layers[l.parent]
|
||||
pxo_cel_opacity = pxo_cel.opacity
|
||||
if not pxo_layer.visible or pxo_cel_opacity == 0:
|
||||
continue
|
||||
pxo_cel_image_buffer_offset = pxo_cel_image_buffer_size * \
|
||||
(pixel_layers_count * frame_index + pixel_layer_index)
|
||||
var pxo_cel_image_buffer: PackedByteArray = images_data.slice(
|
||||
pxo_cel_image_buffer_offset,
|
||||
pxo_cel_image_buffer_offset + pxo_cel_image_buffer_size)
|
||||
for alpha_index in range(3, pxo_cel_image_buffer_size, 4):
|
||||
pxo_cel_image_buffer[alpha_index] = roundi(pxo_cel_image_buffer[alpha_index] * pxo_cel_opacity)
|
||||
pxo_cel_image.set_data(image_size.x, image_size.y, false, Image.FORMAT_RGBA8, pxo_cel_image_buffer)
|
||||
frame_image.blend_rect(pxo_cel_image, image_rect, Vector2i.ZERO)
|
||||
unique_frames_indices_by_frame_index[frame_index] = unique_frame_index
|
||||
animation.frames[animation_frame_index] = frame
|
||||
if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0:
|
||||
push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name])
|
||||
|
||||
var sprite_sheet_builder: _SpriteSheetBuilderBase = _create_sprite_sheet_builder(options)
|
||||
|
||||
var sprite_sheet_building_result: _SpriteSheetBuilderBase.SpriteSheetBuildingResult = \
|
||||
sprite_sheet_builder.build_sprite_sheet(unique_frames_images)
|
||||
if sprite_sheet_building_result.error:
|
||||
result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result)
|
||||
return result
|
||||
var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet
|
||||
|
||||
for unique_frame_index in unique_frames_count:
|
||||
var unique_frame: _Common.FrameInfo = unique_frames[unique_frame_index]
|
||||
unique_frame.sprite = sprite_sheet.sprites[unique_frame_index]
|
||||
|
||||
result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library)
|
||||
return result
|
||||
|
||||
class CustomImageFormatLoaderExtension:
|
||||
extends ImageFormatLoaderExtension
|
||||
|
||||
var __recognized_extensions: PackedStringArray
|
||||
|
||||
func _init(recognized_extensions: PackedStringArray) -> void:
|
||||
__recognized_extensions = recognized_extensions
|
||||
|
||||
func _get_recognized_extensions() -> PackedStringArray:
|
||||
return __recognized_extensions
|
||||
|
||||
func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error:
|
||||
var file: FileAccess = FileAccess.open_compressed(file_access.get_path(), FileAccess.READ, FileAccess.COMPRESSION_ZSTD)
|
||||
if file == null or file.get_open_error() == ERR_FILE_UNRECOGNIZED:
|
||||
file = FileAccess.open(file_access.get_path(), FileAccess.READ)
|
||||
if file == null:
|
||||
push_error("Failed to open file with unknown error")
|
||||
return ERR_FILE_CANT_OPEN
|
||||
var open_error: Error = file.get_open_error()
|
||||
if open_error:
|
||||
push_error("Failed to open file with error: %s \"%s\"" % [open_error, error_string(open_error)])
|
||||
return ERR_FILE_CANT_OPEN
|
||||
|
||||
var first_line: String = file.get_line()
|
||||
|
||||
var pxo_project: Dictionary = JSON.parse_string(first_line)
|
||||
var image_size: Vector2i = Vector2i(pxo_project.size_x, pxo_project.size_y)
|
||||
var pxo_cel_image_buffer_size: int = image_size.x * image_size.y * 4
|
||||
var pxo_cel_image_buffer_offset: int
|
||||
var pxo_cel_image: Image = Image.create(image_size.x, image_size.y, false, Image.FORMAT_RGBA8)
|
||||
var pixel_layer_index: int = -1
|
||||
image.set_data(1, 1, false, Image.FORMAT_RGBA8, [0, 0, 0, 0])
|
||||
image.resize(image_size.x, image_size.y)
|
||||
var image_rect: Rect2i = Rect2i(Vector2i.ZERO, image_size)
|
||||
for layer_index in pxo_project.layers.size():
|
||||
var pxo_layer: Dictionary = pxo_project.layers[layer_index]
|
||||
if pxo_layer.type != PxoLayerType.PIXEL_LAYER:
|
||||
continue
|
||||
pixel_layer_index += 1
|
||||
var l: Dictionary = pxo_layer
|
||||
while l.parent >= 0 and pxo_layer.visible:
|
||||
if not l.visible:
|
||||
pxo_layer.visible = false
|
||||
break
|
||||
l = pxo_project.layers[l.parent]
|
||||
if not pxo_layer.visible:
|
||||
continue
|
||||
var pxo_cel: Dictionary = pxo_project.frames[0].cels[layer_index]
|
||||
var pxo_cel_opacity = pxo_cel.opacity
|
||||
if pxo_cel_opacity == 0:
|
||||
continue
|
||||
pxo_cel_image_buffer_offset = pxo_cel_image_buffer_size * layer_index
|
||||
var pxo_cel_image_buffer: PackedByteArray = file.get_buffer(pxo_cel_image_buffer_size)
|
||||
for alpha_index in range(3, pxo_cel_image_buffer_size, 4):
|
||||
pxo_cel_image_buffer[alpha_index] = roundi(pxo_cel_image_buffer[alpha_index] * pxo_cel_opacity)
|
||||
pxo_cel_image.set_data(image_size.x, image_size.y, false, Image.FORMAT_RGBA8, pxo_cel_image_buffer)
|
||||
image.blend_rect(pxo_cel_image, image_rect, Vector2i.ZERO)
|
||||
file.close()
|
||||
return OK
|
||||
|
||||
1
addons/nklbdev.importality/export/pixelorama.gd.uid
Normal file
1
addons/nklbdev.importality/export/pixelorama.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ccbaalk42lyab
|
||||
10
addons/nklbdev.importality/external_scripts/_.gd
Normal file
10
addons/nklbdev.importality/external_scripts/_.gd
Normal file
@@ -0,0 +1,10 @@
|
||||
extends RefCounted
|
||||
|
||||
const SpriteSheetLayout = preload("../common.gd").SpriteSheetLayout
|
||||
const EdgesArtifactsAvoidanceMethod = preload("../common.gd").EdgesArtifactsAvoidanceMethod
|
||||
const AnimationDirection = preload("../common.gd").AnimationDirection
|
||||
const SpriteInfo = preload("../common.gd").SpriteInfo
|
||||
const SpriteSheetInfo = preload("../common.gd").SpriteSheetInfo
|
||||
const FrameInfo = preload("../common.gd").FrameInfo
|
||||
const AnimationInfo = preload("../common.gd").AnimationInfo
|
||||
const AnimationLibraryInfo = preload("../common.gd").AnimationLibraryInfo
|
||||
1
addons/nklbdev.importality/external_scripts/_.gd.uid
Normal file
1
addons/nklbdev.importality/external_scripts/_.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cyqv3ts6gmg5k
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://ch5hkfhkqf2lc
|
||||
@@ -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));
|
||||
@@ -0,0 +1 @@
|
||||
uid://c4mdsfp261tbu
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://be164xb1v365c
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://wy5d2axdtea
|
||||
70
addons/nklbdev.importality/import/_.gd
Normal file
70
addons/nklbdev.importality/import/_.gd
Normal file
@@ -0,0 +1,70 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
|
||||
const _Result = preload("../result.gd").Class
|
||||
const _Common = preload("../common.gd")
|
||||
const _Options = preload("../options.gd")
|
||||
|
||||
class ImportResult:
|
||||
extends _Result
|
||||
var resource: Resource
|
||||
var resource_saver_flags: ResourceSaver.SaverFlags
|
||||
func _get_result_type_description() -> String:
|
||||
return "Import"
|
||||
func success(
|
||||
resource: Resource,
|
||||
resource_saver_flags: ResourceSaver.SaverFlags = ResourceSaver.FLAG_NONE
|
||||
) -> void:
|
||||
_success()
|
||||
self.resource = resource
|
||||
self.resource_saver_flags = resource_saver_flags
|
||||
|
||||
var __options: Array[Dictionary] = [
|
||||
_Options.create_option(_Options.DEFAULT_ANIMATION_NAME, "default",
|
||||
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT),
|
||||
_Options.create_option(_Options.DEFAULT_ANIMATION_DIRECTION, _Common.AnimationDirection.FORWARD,
|
||||
PROPERTY_HINT_ENUM, ",".join(_Common.ANIMATION_DIRECTIONS_NAMES), PROPERTY_USAGE_DEFAULT),
|
||||
_Options.create_option(_Options.DEFAULT_ANIMATION_REPEAT_COUNT, 0,
|
||||
PROPERTY_HINT_RANGE, "0,,1,or_greater", PROPERTY_USAGE_DEFAULT),
|
||||
_Options.create_option(_Options.AUTOPLAY_ANIMATION_NAME, "",
|
||||
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT),
|
||||
_Options.create_option(_Options.ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED, false,
|
||||
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT),
|
||||
]
|
||||
var __name: String
|
||||
var __resource_type: StringName
|
||||
var __save_extension: String
|
||||
|
||||
func _init(
|
||||
name: String,
|
||||
resource_type: String,
|
||||
save_extension: String,
|
||||
options: Array[Dictionary] = []) -> void:
|
||||
__name = name
|
||||
__resource_type = resource_type
|
||||
__save_extension = save_extension
|
||||
__options.append_array(options)
|
||||
|
||||
func get_name() -> String:
|
||||
return __name
|
||||
|
||||
func get_resource_type() -> StringName:
|
||||
return __resource_type
|
||||
|
||||
func get_save_extension() -> String:
|
||||
return __save_extension
|
||||
|
||||
func get_options() -> Array[Dictionary]:
|
||||
return __options
|
||||
|
||||
func import(
|
||||
source_file_path: String,
|
||||
atlas: Texture2D,
|
||||
sprite_sheet: _Common.SpriteSheetInfo,
|
||||
animation_library: _Common.AnimationLibraryInfo,
|
||||
options: Dictionary,
|
||||
save_path: String) -> ImportResult:
|
||||
assert(false, "This method is abstract and must be overriden.")
|
||||
var result: ImportResult = ImportResult.new()
|
||||
result.fail(ERR_UNCONFIGURED, "This method is abstract and must be overriden.")
|
||||
return result
|
||||
1
addons/nklbdev.importality/import/_.gd.uid
Normal file
1
addons/nklbdev.importality/import/_.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cnw6e1ikxkc4j
|
||||
14
addons/nklbdev.importality/import/_node.gd
Normal file
14
addons/nklbdev.importality/import/_node.gd
Normal file
@@ -0,0 +1,14 @@
|
||||
@tool
|
||||
extends "_.gd"
|
||||
|
||||
func _init(
|
||||
name: String,
|
||||
resource_type: String,
|
||||
save_extension: String,
|
||||
options: Array[Dictionary] = []
|
||||
) -> void:
|
||||
options.append_array([
|
||||
_Options.create_option(_Options.ROOT_NODE_NAME, "",
|
||||
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT),
|
||||
])
|
||||
super(name, resource_type, save_extension, options)
|
||||
1
addons/nklbdev.importality/import/_node.gd.uid
Normal file
1
addons/nklbdev.importality/import/_node.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bsmnxi7wqyckj
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://c5tklyam00r8k
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://jpcaxy1f6srb
|
||||
45
addons/nklbdev.importality/import/animated_sprite_2d.gd
Normal file
45
addons/nklbdev.importality/import/animated_sprite_2d.gd
Normal file
@@ -0,0 +1,45 @@
|
||||
@tool
|
||||
extends "_node.gd"
|
||||
|
||||
const _SpriteFramesImporter = preload("sprite_frames.gd")
|
||||
|
||||
var __sprite_frames_importer: _SpriteFramesImporter
|
||||
|
||||
func _init() -> void:
|
||||
super("AnimatedSprite2D", "PackedScene", "scn")
|
||||
__sprite_frames_importer = _SpriteFramesImporter.new()
|
||||
|
||||
func import(
|
||||
res_source_file_path: String,
|
||||
atlas: Texture2D,
|
||||
sprite_sheet: _Common.SpriteSheetInfo,
|
||||
animation_library: _Common.AnimationLibraryInfo,
|
||||
options: Dictionary,
|
||||
save_path: String
|
||||
) -> ImportResult:
|
||||
var result: ImportResult = ImportResult.new()
|
||||
|
||||
var sprite_frames_import_result: ImportResult = __sprite_frames_importer \
|
||||
.import(res_source_file_path, atlas, sprite_sheet, animation_library, options, save_path)
|
||||
if sprite_frames_import_result.error:
|
||||
return sprite_frames_import_result
|
||||
var sprite_frames: SpriteFrames = sprite_frames_import_result.resource
|
||||
|
||||
var animated_sprite: AnimatedSprite2D = AnimatedSprite2D.new()
|
||||
var node_name: String = options[_Options.ROOT_NODE_NAME].strip_edges()
|
||||
animated_sprite.name = res_source_file_path.get_file().get_basename() \
|
||||
if node_name.is_empty() else node_name
|
||||
animated_sprite.sprite_frames = sprite_frames
|
||||
|
||||
if animation_library.autoplay_index >= 0:
|
||||
if animation_library.autoplay_index >= animation_library.animations.size():
|
||||
result.fail(ERR_INVALID_DATA, "Autoplay animation index overflow")
|
||||
return result
|
||||
animated_sprite.autoplay = animation_library \
|
||||
.animations[animation_library.autoplay_index].name
|
||||
|
||||
var packed_scene: PackedScene = PackedScene.new()
|
||||
packed_scene.pack(animated_sprite)
|
||||
result.success(packed_scene,
|
||||
ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES)
|
||||
return result
|
||||
@@ -0,0 +1 @@
|
||||
uid://byeghoxxqevfp
|
||||
42
addons/nklbdev.importality/import/animated_sprite_3d.gd
Normal file
42
addons/nklbdev.importality/import/animated_sprite_3d.gd
Normal file
@@ -0,0 +1,42 @@
|
||||
@tool
|
||||
extends "_node.gd"
|
||||
|
||||
const _SpriteFramesImporter = preload("sprite_frames.gd")
|
||||
|
||||
var __sprite_frames_importer: _SpriteFramesImporter
|
||||
|
||||
func _init() -> void:
|
||||
super("AnimatedSprite3D", "PackedScene", "scn")
|
||||
__sprite_frames_importer = _SpriteFramesImporter.new()
|
||||
|
||||
func import(
|
||||
res_source_file_path: String,
|
||||
atlas: Texture2D,
|
||||
sprite_sheet: _Common.SpriteSheetInfo,
|
||||
animation_library: _Common.AnimationLibraryInfo,
|
||||
options: Dictionary,
|
||||
save_path: String
|
||||
) -> ImportResult:
|
||||
var result: ImportResult = ImportResult.new()
|
||||
|
||||
var sprite_frames_import_result: ImportResult = __sprite_frames_importer \
|
||||
.import(res_source_file_path, atlas, sprite_sheet, animation_library, options, save_path)
|
||||
if sprite_frames_import_result.error:
|
||||
return sprite_frames_import_result
|
||||
var sprite_frames: SpriteFrames = sprite_frames_import_result.resource
|
||||
|
||||
var animated_sprite: AnimatedSprite3D = AnimatedSprite3D.new()
|
||||
var node_name: String = options[_Options.ROOT_NODE_NAME].strip_edges()
|
||||
animated_sprite.name = res_source_file_path.get_file().get_basename() \
|
||||
if node_name.is_empty() else node_name
|
||||
animated_sprite.sprite_frames = sprite_frames
|
||||
|
||||
if animation_library.autoplay_index >= 0:
|
||||
animated_sprite.autoplay = animation_library \
|
||||
.animations[animation_library.autoplay_index].name
|
||||
|
||||
var packed_scene: PackedScene = PackedScene.new()
|
||||
packed_scene.pack(animated_sprite)
|
||||
result.success(packed_scene,
|
||||
ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES)
|
||||
return result
|
||||
@@ -0,0 +1 @@
|
||||
uid://bg331u3nm4w8o
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://dbik163y6wqp
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://c3vtlehm0qj6s
|
||||
64
addons/nklbdev.importality/import/sprite_frames.gd
Normal file
64
addons/nklbdev.importality/import/sprite_frames.gd
Normal file
@@ -0,0 +1,64 @@
|
||||
@tool
|
||||
extends "_.gd"
|
||||
|
||||
func _init() -> void: super("SpriteFrames", "SpriteFrames", "res")
|
||||
|
||||
func import(
|
||||
res_source_file_path: String,
|
||||
atlas: Texture2D,
|
||||
sprite_sheet: _Common.SpriteSheetInfo,
|
||||
animation_library: _Common.AnimationLibraryInfo,
|
||||
options: Dictionary,
|
||||
save_path: String
|
||||
) -> ImportResult:
|
||||
var result: ImportResult = ImportResult.new()
|
||||
|
||||
var sprite_frames: SpriteFrames = SpriteFrames.new()
|
||||
for animation_name in sprite_frames.get_animation_names():
|
||||
sprite_frames.remove_animation(animation_name)
|
||||
|
||||
var filter_clip_enabled: bool = options[_Options.ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED]
|
||||
var atlas_textures: Array[AtlasTexture]
|
||||
var empty_atlas_texture: AtlasTexture
|
||||
for animation in animation_library.animations:
|
||||
sprite_frames.add_animation(animation.name)
|
||||
sprite_frames.set_animation_loop(animation.name, animation.repeat_count == 0)
|
||||
sprite_frames.set_animation_speed(animation.name, 1)
|
||||
var previous_texture: Texture2D
|
||||
for frame in animation.get_flatten_frames():
|
||||
var atlas_texture: AtlasTexture
|
||||
if frame.sprite.region.has_area():
|
||||
var region: Rect2 = frame.sprite.region
|
||||
var margin: Rect2 = Rect2(
|
||||
frame.sprite.offset,
|
||||
sprite_sheet.source_image_size - frame.sprite.region.size)
|
||||
var equivalent_atlas_textures: Array = atlas_textures.filter(
|
||||
func(t: AtlasTexture) -> bool: return t.margin == margin and t.region == region)
|
||||
if not equivalent_atlas_textures.is_empty():
|
||||
atlas_texture = equivalent_atlas_textures.front()
|
||||
if atlas_texture == null:
|
||||
atlas_texture = AtlasTexture.new()
|
||||
atlas_texture.filter_clip = filter_clip_enabled
|
||||
atlas_texture.atlas = atlas
|
||||
atlas_texture.region = region
|
||||
atlas_texture.margin = margin
|
||||
atlas_textures.push_back(atlas_texture)
|
||||
else:
|
||||
if empty_atlas_texture == null:
|
||||
empty_atlas_texture = AtlasTexture.new()
|
||||
empty_atlas_texture.filter_clip = filter_clip_enabled
|
||||
empty_atlas_texture.atlas = atlas
|
||||
empty_atlas_texture.region = Rect2(0, 0, 1, 1)
|
||||
empty_atlas_texture.margin = Rect2(2, 2, 0, 0)
|
||||
atlas_texture = empty_atlas_texture
|
||||
if atlas_texture == previous_texture:
|
||||
var last_frame_index: int = sprite_frames.get_frame_count(animation.name) - 1
|
||||
sprite_frames.set_frame(animation.name, last_frame_index, atlas_texture,
|
||||
sprite_frames.get_frame_duration(animation.name, last_frame_index) + frame.duration)
|
||||
continue
|
||||
sprite_frames.add_frame(animation.name, atlas_texture, frame.duration)
|
||||
previous_texture = atlas_texture
|
||||
|
||||
result.success(sprite_frames,
|
||||
ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES)
|
||||
return result
|
||||
1
addons/nklbdev.importality/import/sprite_frames.gd.uid
Normal file
1
addons/nklbdev.importality/import/sprite_frames.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://7divemm1kjo6
|
||||
83
addons/nklbdev.importality/import/sprite_sheet.gd
Normal file
83
addons/nklbdev.importality/import/sprite_sheet.gd
Normal file
@@ -0,0 +1,83 @@
|
||||
@tool
|
||||
extends "_.gd"
|
||||
|
||||
const __ANIMATION_MERGE_EQUAL_CONSEQUENT_FRAMES_OPTION: StringName = "animation/merge_equal_sonsequent_frames"
|
||||
const __ANIMATION_FLATTEN_REPETITION_OPTION: StringName = "animation/flatten_repetition"
|
||||
|
||||
func _init() -> void: super("Sprite sheet (JSON)", "JSON", "res", [
|
||||
_Options.create_option(__ANIMATION_MERGE_EQUAL_CONSEQUENT_FRAMES_OPTION, true,
|
||||
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT),
|
||||
_Options.create_option(__ANIMATION_FLATTEN_REPETITION_OPTION, true,
|
||||
PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT),
|
||||
])
|
||||
|
||||
func import(
|
||||
res_source_file_path: String,
|
||||
atlas: Texture2D,
|
||||
sprite_sheet: _Common.SpriteSheetInfo,
|
||||
animation_library: _Common.AnimationLibraryInfo,
|
||||
options: Dictionary,
|
||||
save_path: String
|
||||
) -> ImportResult:
|
||||
var result: ImportResult = ImportResult.new()
|
||||
|
||||
var unique_indixes_by_sprites: Dictionary
|
||||
var unique_sprite_index: int = 0
|
||||
var sprites: Array[Dictionary]
|
||||
for sprite in sprite_sheet.sprites:
|
||||
if not unique_indixes_by_sprites.has(sprite):
|
||||
unique_indixes_by_sprites[sprite] = unique_sprite_index
|
||||
sprites.push_back({
|
||||
region = sprite.region,
|
||||
offset = sprite.offset
|
||||
})
|
||||
unique_sprite_index += 1
|
||||
|
||||
var flatten_animation_repetition: bool = options[__ANIMATION_FLATTEN_REPETITION_OPTION]
|
||||
var merge_equal_consequent_frames: bool = options[__ANIMATION_MERGE_EQUAL_CONSEQUENT_FRAMES_OPTION]
|
||||
var animations: Array[Dictionary]
|
||||
for animation in animation_library.animations:
|
||||
var frames_data: Array[Dictionary]
|
||||
var frames: Array[_Common.FrameInfo] = \
|
||||
animation.get_flatten_frames() \
|
||||
if flatten_animation_repetition else \
|
||||
animation.frames
|
||||
var previous_sprite_index: int = -1
|
||||
for frame in frames:
|
||||
var sprite_index: int = unique_indixes_by_sprites[frame.sprite]
|
||||
if merge_equal_consequent_frames and sprite_index == previous_sprite_index:
|
||||
frames_data.back().duration += frame.duration
|
||||
else:
|
||||
frames_data.push_back({
|
||||
sprite_index = sprite_index,
|
||||
duration = frame.duration,
|
||||
})
|
||||
previous_sprite_index = sprite_index
|
||||
animations.push_back({
|
||||
name = animation.name,
|
||||
direction =
|
||||
_Common.AnimationDirection.FORWARD
|
||||
if flatten_animation_repetition else
|
||||
animation.direction,
|
||||
repeat_count =
|
||||
mini(1, animation.repeat_count)
|
||||
if flatten_animation_repetition else
|
||||
animation.repeat_count,
|
||||
frames = frames_data,
|
||||
})
|
||||
|
||||
var json: JSON = JSON.new()
|
||||
json.data = {
|
||||
sprite_sheet = {
|
||||
atlas = atlas,
|
||||
source_image_size = sprite_sheet.source_image_size,
|
||||
sprites = sprites,
|
||||
},
|
||||
animation_library = {
|
||||
animations = animations,
|
||||
autoplay_index = animation_library.autoplay_index,
|
||||
},
|
||||
}
|
||||
json.get_parsed_text()
|
||||
result.success(json, ResourceSaver.FLAG_COMPRESS)
|
||||
return result
|
||||
1
addons/nklbdev.importality/import/sprite_sheet.gd.uid
Normal file
1
addons/nklbdev.importality/import/sprite_sheet.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cpbdxtrpuluxc
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://dqo2tgxc28q6l
|
||||
41
addons/nklbdev.importality/options.gd
Normal file
41
addons/nklbdev.importality/options.gd
Normal file
@@ -0,0 +1,41 @@
|
||||
@tool
|
||||
|
||||
const _Common = preload("common.gd")
|
||||
const __empty_callable: Callable = Callable()
|
||||
|
||||
const SPRITE_SHEET_LAYOUT: StringName = "sprite_sheet/layout"
|
||||
const MAX_CELLS_IN_STRIP: StringName = "sprite_sheet/max_cells_in_strip"
|
||||
const EDGES_ARTIFACTS_AVOIDANCE_METHOD: StringName = "sprite_sheet/edges_artifacts_avoidance_method"
|
||||
const SPRITES_SURROUNDING_COLOR: StringName = "sprite_sheet/sprites_surrounding_color"
|
||||
const TRIM_SPRITES_TO_OVERALL_MIN_SIZE: StringName = "sprite_sheet/trim_sprites_to_overall_min_size"
|
||||
const COLLAPSE_TRANSPARENT_SPRITES: StringName = "sprite_sheet/collapse_transparent_sprites"
|
||||
const MERGE_DUPLICATED_SPRITES: StringName = "sprite_sheet/merge_duplicated_sprites"
|
||||
const DEFAULT_ANIMATION_NAME: StringName = "animation/default/name"
|
||||
const DEFAULT_ANIMATION_DIRECTION: StringName = "animation/default/direction"
|
||||
const DEFAULT_ANIMATION_REPEAT_COUNT: StringName = "animation/default/repeat_count"
|
||||
const AUTOPLAY_ANIMATION_NAME: StringName = "animation/autoplay_name"
|
||||
const ROOT_NODE_NAME: StringName = "root_node_name"
|
||||
const ANIMATION_STRATEGY: StringName = "animation/strategy"
|
||||
const SPRITE_CENTERED: StringName = "sprite/centered"
|
||||
const ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED: StringName = "atlas_textures/region_filter_clip_enabled"
|
||||
const MIDDLE_IMPORT_SCRIPT_PATH: StringName = "middle_import_script"
|
||||
const POST_IMPORT_SCRIPT_PATH: StringName = "post_import_script"
|
||||
|
||||
static func create_option(
|
||||
name: StringName,
|
||||
default_value: Variant,
|
||||
property_hint: PropertyHint = PROPERTY_HINT_NONE,
|
||||
hint_string: String = "",
|
||||
usage: PropertyUsageFlags = PROPERTY_USAGE_NONE,
|
||||
get_is_visible: Callable = __empty_callable
|
||||
) -> Dictionary:
|
||||
var option_data: Dictionary = {
|
||||
name = name,
|
||||
default_value = default_value,
|
||||
}
|
||||
if hint_string: option_data["hint_string"] = hint_string
|
||||
if property_hint: option_data["property_hint"] = property_hint
|
||||
if usage: option_data["usage"] = usage
|
||||
if get_is_visible != __empty_callable:
|
||||
option_data["get_is_visible"] = get_is_visible
|
||||
return option_data
|
||||
1
addons/nklbdev.importality/options.gd.uid
Normal file
1
addons/nklbdev.importality/options.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://2wlv6noymbn3
|
||||
7
addons/nklbdev.importality/plugin.cfg
Normal file
7
addons/nklbdev.importality/plugin.cfg
Normal file
@@ -0,0 +1,7 @@
|
||||
[plugin]
|
||||
|
||||
name="Importality"
|
||||
description="Universal raster graphics and animations importers pack"
|
||||
author="Nikolay Lebedev aka nklbdev"
|
||||
version="0.3.0"
|
||||
script="editor_plugin.gd"
|
||||
138
addons/nklbdev.importality/rect_packer.gd
Normal file
138
addons/nklbdev.importality/rect_packer.gd
Normal file
@@ -0,0 +1,138 @@
|
||||
@tool
|
||||
# This code is taken from: https://github.com/semibran/pack/blob/master/lib/pack.js
|
||||
# Copyright (c) 2018 Brandon Semilla (MIT License) - original author
|
||||
# Copyright (c) 2023 Nikolay Lebedev (MIT License) - porting to gdscript, refactoring and optimization
|
||||
|
||||
const _Result = preload("result.gd").Class
|
||||
const _Common = preload("common.gd")
|
||||
|
||||
const __WHITESPACE_WEIGHT: float = 1
|
||||
const __SIDE_LENGTH_WEIGHT: float = 10
|
||||
|
||||
class RectPackingResult:
|
||||
extends _Result
|
||||
# Total size of the entire layout of rectangles.
|
||||
var bounds: Vector2i
|
||||
# Computed positions of the input rectangles
|
||||
# in the same order as their sizes were passed in.
|
||||
var rects_positions: Array[Vector2i]
|
||||
func _get_result_type_description() -> String:
|
||||
return "Rect packing"
|
||||
func success(bounds: Vector2i, rects_positions: Array[Vector2i]) -> void:
|
||||
_success()
|
||||
self.bounds = bounds
|
||||
self.rects_positions = rects_positions
|
||||
|
||||
static func __add_rect_to_cache(rect: Rect2i, cache: Dictionary, cache_grid_size: Vector2i) -> void:
|
||||
var left_top_cell: Vector2i = rect.position / cache_grid_size
|
||||
var right_bottom_cell: Vector2i = rect.end / cache_grid_size + (rect.end % cache_grid_size).sign()
|
||||
for y in range(left_top_cell.y, right_bottom_cell.y):
|
||||
for x in range(left_top_cell.x, right_bottom_cell.x):
|
||||
var cell: Vector2i = Vector2i(x, y)
|
||||
if cache.has(cell):
|
||||
cache[cell].push_back(rect)
|
||||
else:
|
||||
cache[cell] = [rect] as Array[Rect2i]
|
||||
|
||||
const __empty_rect_array: Array[Rect2i] = []
|
||||
static func __has_intersection(rect: Rect2i, cache: Dictionary, cache_grid_size: Vector2i) -> bool:
|
||||
var left_top_cell: Vector2i = rect.position / cache_grid_size
|
||||
var right_bottom_cell: Vector2i = rect.end / cache_grid_size + (rect.end % cache_grid_size).sign()
|
||||
for y in range(left_top_cell.y, right_bottom_cell.y):
|
||||
for x in range(left_top_cell.x, right_bottom_cell.x):
|
||||
for cached_rect in cache.get(Vector2i(x, y), __empty_rect_array):
|
||||
if cached_rect.intersects(rect):
|
||||
return true
|
||||
return false
|
||||
|
||||
# The function takes an array of rectangle sizes as input and compactly packs them.
|
||||
static func pack(rects_sizes: Array[Vector2i]) -> RectPackingResult:
|
||||
var result: RectPackingResult = RectPackingResult.new()
|
||||
var rects_count: int = rects_sizes.size()
|
||||
if rects_count == 0:
|
||||
result.success(Vector2i.ZERO, [])
|
||||
return result
|
||||
var rects_positions: Array[Vector2i]
|
||||
rects_positions.resize(rects_count)
|
||||
var min_area: int
|
||||
var rect_sizes_sum: Vector2i
|
||||
for size in rects_sizes:
|
||||
if size.x < 0 or size.y < 0:
|
||||
result.fail(ERR_INVALID_DATA, "Negative rect size found")
|
||||
return result
|
||||
min_area += size.x * size.y
|
||||
rect_sizes_sum += size
|
||||
if min_area == 0:
|
||||
result.success(Vector2i.ZERO, rects_positions)
|
||||
return result
|
||||
var average_rect_size: Vector2 = Vector2(rect_sizes_sum) / rects_count
|
||||
var rect_cache_grid_size: Vector2i = average_rect_size.ceil() * 2
|
||||
var average_squared_rect_side_length: float = sqrt(min_area / float(rects_count))
|
||||
|
||||
var rect_cache: Dictionary
|
||||
|
||||
var possible_bounds_side_length: int = ceili(sqrt(rects_count))
|
||||
nearest_po2(possible_bounds_side_length)
|
||||
|
||||
var rects_order_arr: Array = PackedInt32Array(range(0, rects_count))
|
||||
rects_order_arr.sort_custom(func(a: int, b: int) -> bool:
|
||||
return rects_sizes[a].x * rects_sizes[a].y > rects_sizes[b].x * rects_sizes[b].y)
|
||||
var rects_order: PackedInt32Array = PackedInt32Array(rects_order_arr)
|
||||
|
||||
var bounds: Vector2i = rects_sizes[rects_order[0]]
|
||||
var utilized_area: int = bounds.x * bounds.y
|
||||
|
||||
var splits_by_axis: Array[PackedInt32Array] = [[0, bounds.x], [0, bounds.y]]
|
||||
__add_rect_to_cache(Rect2i(Vector2i.ZERO, rects_sizes[rects_order[0]]), rect_cache, rect_cache_grid_size)
|
||||
|
||||
for rect_index in range(1, rects_count): # skip first rect at (0, 0)
|
||||
var ordered_rect_index: int = rects_order[rect_index]
|
||||
var rect: Rect2i = Rect2i(Vector2i.ZERO, rects_sizes[ordered_rect_index])
|
||||
var rect_area: int = rect.get_area()
|
||||
if rect_area == 0:
|
||||
continue
|
||||
utilized_area += rect_area
|
||||
|
||||
var best_score: float = INF
|
||||
var best_new_bounds: Vector2i = bounds
|
||||
for landing_rect_index in rect_index:
|
||||
var ordered_landing_rect_index: int = rects_order[landing_rect_index]
|
||||
var landing_rect: Rect2i = Rect2i(
|
||||
rects_positions[ordered_landing_rect_index],
|
||||
rects_sizes[ordered_landing_rect_index])
|
||||
for split_axis_index in 2:
|
||||
var orthogonal_asis_index: int = (split_axis_index + 1) % 2
|
||||
var splits: PackedInt32Array = splits_by_axis[split_axis_index]
|
||||
rect.position[orthogonal_asis_index] = landing_rect.end[orthogonal_asis_index]
|
||||
for split_index in range(
|
||||
splits.bsearch(landing_rect.position[split_axis_index]),
|
||||
splits.bsearch(landing_rect.end[split_axis_index])):
|
||||
rect.position[split_axis_index] = splits[split_index]
|
||||
if __has_intersection(rect, rect_cache, rect_cache_grid_size):
|
||||
continue
|
||||
var new_bounds: Vector2i = Vector2i(
|
||||
maxi(bounds.x, rect.end.x),
|
||||
maxi(bounds.y, rect.end.y))
|
||||
var score: float = \
|
||||
__WHITESPACE_WEIGHT * (new_bounds.x * new_bounds.y - utilized_area) + \
|
||||
__SIDE_LENGTH_WEIGHT * average_squared_rect_side_length * maxf(new_bounds.x, new_bounds.y)
|
||||
if score < best_score:
|
||||
best_score = score
|
||||
rects_positions[ordered_rect_index] = rect.position
|
||||
best_new_bounds = new_bounds
|
||||
bounds = best_new_bounds
|
||||
rect.position = rects_positions[ordered_rect_index]
|
||||
|
||||
__add_rect_to_cache(rect, rect_cache, rect_cache_grid_size)
|
||||
# Add new splits at rect.end if they dot't already exist
|
||||
for split_axis_index in 2:
|
||||
var splits: PackedInt32Array = splits_by_axis[split_axis_index]
|
||||
var position: int = rect.end[split_axis_index]
|
||||
var split_index: int = splits.bsearch(position)
|
||||
if split_index == splits.size():
|
||||
splits.append(position)
|
||||
elif splits[split_index] != position:
|
||||
splits.insert(split_index, position)
|
||||
|
||||
result.success(bounds, rects_positions)
|
||||
return result
|
||||
1
addons/nklbdev.importality/rect_packer.gd.uid
Normal file
1
addons/nklbdev.importality/rect_packer.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cxg4gsatgtv8c
|
||||
25
addons/nklbdev.importality/result.gd
Normal file
25
addons/nklbdev.importality/result.gd
Normal file
@@ -0,0 +1,25 @@
|
||||
class Class:
|
||||
extends RefCounted
|
||||
|
||||
var error: Error
|
||||
var error_description: String
|
||||
var inner_result: Class
|
||||
func _get_result_type_description() -> String:
|
||||
return "Operation"
|
||||
func fail(error: Error, error_description: String = "", inner_result: Class = null) -> void:
|
||||
assert(error != OK)
|
||||
self.error = error
|
||||
self.error_description = error_description
|
||||
self.inner_result = inner_result
|
||||
func _success():
|
||||
error = OK
|
||||
error_description = ""
|
||||
inner_result = null
|
||||
func _to_string() -> String:
|
||||
return "%s error: %s (%s)%s%s" % [
|
||||
_get_result_type_description(),
|
||||
error,
|
||||
error_string(error),
|
||||
(", description: \"%s\"" % [error_description]) if error_description else "",
|
||||
(", inner error:\n%s" % [inner_result]) if inner_result else "",
|
||||
] if error else "%s(success)"
|
||||
1
addons/nklbdev.importality/result.gd.uid
Normal file
1
addons/nklbdev.importality/result.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dvhbtf8bjxqcj
|
||||
67
addons/nklbdev.importality/setting.gd
Normal file
67
addons/nklbdev.importality/setting.gd
Normal file
@@ -0,0 +1,67 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
|
||||
const _Result = preload("result.gd").Class
|
||||
|
||||
var __editor_settings: EditorSettings
|
||||
|
||||
var __name: StringName
|
||||
var __initial_value: Variant
|
||||
var __type: Variant.Type
|
||||
var __hint: PropertyHint
|
||||
var __hint_string: String
|
||||
var __is_required: bool
|
||||
var __is_value_empty_func: Callable
|
||||
|
||||
func __default_is_value_empty_func(value: Variant) -> bool:
|
||||
if value: return false
|
||||
return true
|
||||
|
||||
func _init(
|
||||
name: String,
|
||||
initial_value: Variant,
|
||||
type: int,
|
||||
hint: int,
|
||||
hint_string: String = "",
|
||||
is_required: bool = false,
|
||||
is_value_empty_func: Callable = __default_is_value_empty_func
|
||||
) -> void:
|
||||
__name = "importality/" + name
|
||||
__initial_value = initial_value
|
||||
__type = type
|
||||
__hint = hint
|
||||
__hint_string = hint_string
|
||||
__is_required = is_required
|
||||
__is_value_empty_func = is_value_empty_func
|
||||
|
||||
func register(editor_settings: EditorSettings) -> void:
|
||||
__editor_settings = editor_settings
|
||||
if not __editor_settings.has_setting(__name):
|
||||
__editor_settings.set_setting(__name, __initial_value)
|
||||
__editor_settings.set_initial_value(__name, __initial_value, false)
|
||||
var property_info: Dictionary = {
|
||||
"name": __name,
|
||||
"type": __type,
|
||||
"hint": __hint, }
|
||||
if __hint_string:
|
||||
property_info["hint_string"] = __hint_string
|
||||
__editor_settings.add_property_info(property_info)
|
||||
|
||||
class GettingValueResult:
|
||||
extends _Result
|
||||
var value: Variant
|
||||
func success(value: Variant) -> void:
|
||||
_success()
|
||||
self.value = value
|
||||
|
||||
func get_value() -> GettingValueResult:
|
||||
var result = GettingValueResult.new()
|
||||
var value = __editor_settings.get_setting(__name)
|
||||
if __is_required:
|
||||
if __is_value_empty_func.call(value):
|
||||
result.fail(ERR_UNCONFIGURED,
|
||||
"The project settging \"%s\" is not specified!" % [__name] + \
|
||||
"Specify it in Projest Settings -> General -> Importality.")
|
||||
return result
|
||||
result.success(value)
|
||||
return result
|
||||
1
addons/nklbdev.importality/setting.gd.uid
Normal file
1
addons/nklbdev.importality/setting.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://47n78hm4mbvb
|
||||
86
addons/nklbdev.importality/sprite_sheet_builder/_.gd
Normal file
86
addons/nklbdev.importality/sprite_sheet_builder/_.gd
Normal file
@@ -0,0 +1,86 @@
|
||||
@tool
|
||||
extends RefCounted
|
||||
|
||||
const _Result = preload("../result.gd").Class
|
||||
const _Common = preload("../common.gd")
|
||||
|
||||
var _edges_artifacts_avoidance_method: _Common.EdgesArtifactsAvoidanceMethod
|
||||
var _sprites_surrounding_color: Color
|
||||
|
||||
func _init(
|
||||
edges_artifacts_avoidance_method: _Common.EdgesArtifactsAvoidanceMethod,
|
||||
sprites_surrounding_color: Color = Color.TRANSPARENT
|
||||
) -> void:
|
||||
_edges_artifacts_avoidance_method = edges_artifacts_avoidance_method
|
||||
_sprites_surrounding_color = sprites_surrounding_color
|
||||
|
||||
class SpriteSheetBuildingResult:
|
||||
extends _Result
|
||||
var sprite_sheet: _Common.SpriteSheetInfo
|
||||
var atlas_image: Image
|
||||
func _get_result_type_description() -> String:
|
||||
return "Sprite sheet building"
|
||||
func success(sprite_sheet: _Common.SpriteSheetInfo, atlas_image: Image) -> void:
|
||||
_success()
|
||||
self.sprite_sheet = sprite_sheet
|
||||
self.atlas_image = atlas_image
|
||||
|
||||
func build_sprite_sheet(images: Array[Image]) -> SpriteSheetBuildingResult:
|
||||
assert(false, "This method is abstract and must be overriden.")
|
||||
return null
|
||||
|
||||
static func __hash_combine(a: int, b: int) -> int:
|
||||
return a ^ (b + 0x9E3779B9 + (a<<6) + (a>>2))
|
||||
|
||||
const __hash_precision: int = 5
|
||||
static func _get_image_hash(image: Image) -> int:
|
||||
var image_size: Vector2i = image.get_size()
|
||||
if image_size.x * image_size.y == 0:
|
||||
return 0
|
||||
var hash: int = 0
|
||||
hash = __hash_combine(hash, image_size.x)
|
||||
hash = __hash_combine(hash, image_size.y)
|
||||
var grid_cell_size: Vector2i = image_size / __hash_precision
|
||||
for y in range(0, image_size.y, grid_cell_size.y):
|
||||
for x in range(0, image_size.x, grid_cell_size.x):
|
||||
var pixel: Color = image.get_pixel(x, y)
|
||||
hash = __hash_combine(hash, pixel.r8)
|
||||
hash = __hash_combine(hash, pixel.g8)
|
||||
hash = __hash_combine(hash, pixel.b8)
|
||||
hash = __hash_combine(hash, pixel.a8)
|
||||
return hash
|
||||
|
||||
static func _extrude_borders(image: Image, rect: Rect2i) -> void:
|
||||
if not rect.has_area():
|
||||
return
|
||||
# extrude borders
|
||||
# left border
|
||||
image.blit_rect(image,
|
||||
rect.grow_side(SIDE_RIGHT, 1 - rect.size.x),
|
||||
rect.position + Vector2i.LEFT)
|
||||
# top border
|
||||
image.blit_rect(image,
|
||||
rect.grow_side(SIDE_BOTTOM, 1 - rect.size.y),
|
||||
rect.position + Vector2i.UP)
|
||||
# right border
|
||||
image.blit_rect(image,
|
||||
rect.grow_side(SIDE_LEFT, 1 - rect.size.x),
|
||||
rect.position + Vector2i(rect.size.x, 0))
|
||||
# bottom border
|
||||
image.blit_rect(image,
|
||||
rect.grow_side(SIDE_TOP, 1 - rect.size.y),
|
||||
rect.position + Vector2i(0, rect.size.y))
|
||||
|
||||
# corner pixels
|
||||
# top left corner
|
||||
image.set_pixelv(rect.position - Vector2i.ONE,
|
||||
image.get_pixelv(rect.position))
|
||||
# top right corner
|
||||
image.set_pixelv(rect.position + Vector2i(rect.size.x, -1),
|
||||
image.get_pixelv(rect.position + Vector2i(rect.size.x - 1, 0)))
|
||||
# bottom right corner
|
||||
image.set_pixelv(rect.end,
|
||||
image.get_pixelv(rect.end - Vector2i.ONE))
|
||||
# bottom left corner
|
||||
image.set_pixelv(rect.position + Vector2i(-1, rect.size.y),
|
||||
image.get_pixelv(rect.position + Vector2i(0, rect.size.y -1)))
|
||||
1
addons/nklbdev.importality/sprite_sheet_builder/_.gd.uid
Normal file
1
addons/nklbdev.importality/sprite_sheet_builder/_.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d1mx4gmvwjihh
|
||||
171
addons/nklbdev.importality/sprite_sheet_builder/grid_based.gd
Normal file
171
addons/nklbdev.importality/sprite_sheet_builder/grid_based.gd
Normal file
@@ -0,0 +1,171 @@
|
||||
@tool
|
||||
extends "_.gd"
|
||||
|
||||
const _RectPacker = preload("../rect_packer.gd")
|
||||
|
||||
enum StripDirection {
|
||||
HORIZONTAL = 0,
|
||||
VERTICAL = 1,
|
||||
}
|
||||
|
||||
var _strips_direction: StripDirection
|
||||
var _max_cells_in_strip: int
|
||||
var _trim_sprites_to_overall_min_size: bool
|
||||
var _collapse_transparent: bool
|
||||
var _merge_duplicates: bool
|
||||
|
||||
func _init(
|
||||
edges_artifacts_avoidance_method: _Common.EdgesArtifactsAvoidanceMethod,
|
||||
strips_direction: StripDirection,
|
||||
max_cells_in_strip: int,
|
||||
trim_sprites_to_overall_min_size: bool,
|
||||
collapse_transparent: bool,
|
||||
merge_duplicates: bool,
|
||||
sprites_surrounding_color: Color = Color.TRANSPARENT
|
||||
) -> void:
|
||||
super(edges_artifacts_avoidance_method, sprites_surrounding_color)
|
||||
_strips_direction = strips_direction
|
||||
_max_cells_in_strip = max_cells_in_strip
|
||||
_trim_sprites_to_overall_min_size = trim_sprites_to_overall_min_size
|
||||
_collapse_transparent = collapse_transparent
|
||||
_merge_duplicates = merge_duplicates
|
||||
|
||||
func build_sprite_sheet(images: Array[Image]) -> SpriteSheetBuildingResult:
|
||||
var result: SpriteSheetBuildingResult = SpriteSheetBuildingResult.new()
|
||||
var images_count: int = images.size()
|
||||
|
||||
var sprite_sheet: _Common.SpriteSheetInfo = _Common.SpriteSheetInfo.new()
|
||||
|
||||
if images_count == 0:
|
||||
var atlas_image = Image.new()
|
||||
atlas_image.set_data(1, 1, false, Image.FORMAT_RGBA8, PackedByteArray([0, 0, 0, 0]))
|
||||
result.success(sprite_sheet, atlas_image)
|
||||
return result
|
||||
|
||||
sprite_sheet.source_image_size = images.front().get_size()
|
||||
if not images.all(func(i: Image) -> bool: return i.get_size() == sprite_sheet.source_image_size):
|
||||
result.fail(ERR_INVALID_DATA, "Input images have different sizes")
|
||||
return result
|
||||
|
||||
sprite_sheet.sprites.resize(images_count)
|
||||
|
||||
var first_axis: int = _strips_direction
|
||||
var second_axis: int = 1 - first_axis
|
||||
|
||||
var max_image_used_rect: Rect2i
|
||||
var images_infos_cache: Dictionary # of arrays of images indices by image hashes
|
||||
|
||||
var unique_sprites_indices: Array[int]
|
||||
var collapsed_sprite: _Common.SpriteInfo = _Common.SpriteInfo.new()
|
||||
var images_used_rects: Array[Rect2i]
|
||||
|
||||
for image_index in images_count:
|
||||
var image: Image = images[image_index]
|
||||
var image_used_rect: Rect2i = image.get_used_rect()
|
||||
var is_image_invisible: bool = not image_used_rect.has_area()
|
||||
|
||||
if _collapse_transparent and is_image_invisible:
|
||||
sprite_sheet.sprites[image_index] = collapsed_sprite
|
||||
continue
|
||||
elif _merge_duplicates:
|
||||
var image_hash: int = _get_image_hash(image)
|
||||
var similar_images_indices: PackedInt32Array = \
|
||||
images_infos_cache.get(image_hash, PackedInt32Array())
|
||||
var is_duplicate_found: bool = false
|
||||
for similar_image_index in similar_images_indices:
|
||||
var similar_image: Image = images[similar_image_index]
|
||||
if image == similar_image or image.get_data() == similar_image.get_data():
|
||||
sprite_sheet.sprites[image_index] = \
|
||||
sprite_sheet.sprites[similar_image_index]
|
||||
is_duplicate_found = true
|
||||
break
|
||||
if similar_images_indices.is_empty():
|
||||
images_infos_cache[image_hash] = similar_images_indices
|
||||
similar_images_indices.push_back(image_index)
|
||||
if is_duplicate_found:
|
||||
continue
|
||||
|
||||
var sprite: _Common.SpriteInfo = _Common.SpriteInfo.new()
|
||||
sprite.region = image_used_rect
|
||||
sprite_sheet.sprites[image_index] = sprite
|
||||
unique_sprites_indices.push_back(image_index)
|
||||
if not is_image_invisible:
|
||||
max_image_used_rect = \
|
||||
image_used_rect.merge(max_image_used_rect) \
|
||||
if max_image_used_rect.has_area() else \
|
||||
image_used_rect
|
||||
|
||||
var unique_sprites_count: int = unique_sprites_indices.size()
|
||||
|
||||
if _edges_artifacts_avoidance_method == _Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION:
|
||||
sprite_sheet.source_image_size += Vector2i.ONE * 2
|
||||
|
||||
var grid_size: Vector2i
|
||||
grid_size[second_axis] = \
|
||||
unique_sprites_count / _max_cells_in_strip + \
|
||||
sign(unique_sprites_count % _max_cells_in_strip) \
|
||||
if _max_cells_in_strip > 0 else sign(unique_sprites_count)
|
||||
grid_size[first_axis] = \
|
||||
_max_cells_in_strip \
|
||||
if grid_size[second_axis] > 1 else \
|
||||
unique_sprites_count
|
||||
|
||||
var image_region: Rect2i = \
|
||||
max_image_used_rect \
|
||||
if _trim_sprites_to_overall_min_size else \
|
||||
Rect2i(Vector2i.ZERO, sprite_sheet.source_image_size)
|
||||
if _edges_artifacts_avoidance_method == _Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION:
|
||||
image_region = image_region.grow(1)
|
||||
|
||||
var atlas_size: Vector2i = grid_size * image_region.size
|
||||
match _edges_artifacts_avoidance_method:
|
||||
_Common.EdgesArtifactsAvoidanceMethod.NONE:
|
||||
pass
|
||||
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_SPACING:
|
||||
atlas_size += grid_size - Vector2i.ONE
|
||||
_Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING:
|
||||
atlas_size += grid_size + Vector2i.ONE
|
||||
_Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION:
|
||||
atlas_size += grid_size * 2
|
||||
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION:
|
||||
pass
|
||||
|
||||
var atlas_image = Image.create(atlas_size.x, atlas_size.y, false, Image.FORMAT_RGBA8)
|
||||
|
||||
if _edges_artifacts_avoidance_method == _Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING:
|
||||
atlas_image.fill(_sprites_surrounding_color)
|
||||
|
||||
var cell: Vector2i
|
||||
var cell_index: int
|
||||
for sprite_index in unique_sprites_indices:
|
||||
# calculate cell
|
||||
var sprite: _Common.SpriteInfo = sprite_sheet.sprites[sprite_index]
|
||||
if sprite == collapsed_sprite:
|
||||
continue
|
||||
sprite.region.size = image_region.size
|
||||
sprite.offset = image_region.position
|
||||
var image: Image = images[sprite_index]
|
||||
cell[first_axis] = cell_index % _max_cells_in_strip if _max_cells_in_strip > 0 else cell_index
|
||||
cell[second_axis] = cell_index / _max_cells_in_strip if _max_cells_in_strip > 0 else 0
|
||||
sprite.region.position = cell * image_region.size
|
||||
match _edges_artifacts_avoidance_method:
|
||||
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_SPACING:
|
||||
sprite.region.position += cell
|
||||
_Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING:
|
||||
sprite.region.position += cell + Vector2i.ONE
|
||||
_Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION:
|
||||
sprite.region.position += cell * 2 + Vector2i.ONE
|
||||
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION:
|
||||
pass
|
||||
atlas_image.blit_rect(image, image_region, sprite.region.position)# +
|
||||
#(Vector2i.ONE
|
||||
#if _edges_artifacts_avoidance_method == \
|
||||
# _Models.SpriteSheetModel.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION else
|
||||
#Vector2i.ZERO))
|
||||
match _edges_artifacts_avoidance_method:
|
||||
_Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION:
|
||||
_extrude_borders(atlas_image, sprite.region)
|
||||
cell_index += 1
|
||||
|
||||
result.success(sprite_sheet, atlas_image)
|
||||
return result
|
||||
@@ -0,0 +1 @@
|
||||
uid://c65mteocd0oco
|
||||
217
addons/nklbdev.importality/sprite_sheet_builder/packed.gd
Normal file
217
addons/nklbdev.importality/sprite_sheet_builder/packed.gd
Normal file
@@ -0,0 +1,217 @@
|
||||
@tool
|
||||
extends "_.gd"
|
||||
|
||||
const _RectPacker = preload("../rect_packer.gd")
|
||||
|
||||
class SpriteProps:
|
||||
extends RefCounted
|
||||
var images_props: Array[ImageProps]
|
||||
var atlas_region_props: AtlasRegionProps
|
||||
|
||||
var offset: Vector2i
|
||||
|
||||
func create_sprite(atlas_region_position: Vector2i) -> _Common.SpriteInfo:
|
||||
var sprite = _Common.SpriteInfo.new()
|
||||
sprite.region = Rect2i(atlas_region_position, atlas_region_props.size)
|
||||
sprite.offset = offset
|
||||
return sprite
|
||||
|
||||
class AtlasRegionProps:
|
||||
extends RefCounted
|
||||
var sprites_props: Array[SpriteProps]
|
||||
var images_props: Array[ImageProps]
|
||||
|
||||
var size: Vector2i
|
||||
|
||||
class ImageProps:
|
||||
extends RefCounted
|
||||
var sprite_props: SpriteProps
|
||||
var atlas_region_props: AtlasRegionProps
|
||||
|
||||
var image: Image
|
||||
var used_rect: Rect2i
|
||||
var used_fragment: Image
|
||||
var used_fragment_data: PackedByteArray
|
||||
var used_fragment_data_hash: int
|
||||
|
||||
func _init(image: Image) -> void:
|
||||
self.image = image
|
||||
used_rect = image.get_used_rect()
|
||||
if used_rect.has_area():
|
||||
used_fragment = image.get_region(used_rect)
|
||||
used_fragment_data = used_fragment.get_data()
|
||||
used_fragment_data_hash = hash(used_fragment_data)
|
||||
|
||||
class SpriteSheetBuildingContext:
|
||||
extends RefCounted
|
||||
var images_props: Array[ImageProps]
|
||||
var sprites_props: Array[SpriteProps]
|
||||
var atlas_regions_props: Array[AtlasRegionProps]
|
||||
var _similar_images_props_by_used_fragment_data_hash: Dictionary
|
||||
var _collapsed_image_props: ImageProps
|
||||
|
||||
func _init(images: Array[Image]) -> void:
|
||||
var images_count: int = images.size()
|
||||
images_props.resize(images_count)
|
||||
for image_index in images_count:
|
||||
images_props[image_index] = _process_image_props(ImageProps.new(images[image_index]))
|
||||
|
||||
func _process_image_props(image_props: ImageProps) -> ImageProps:
|
||||
if not image_props.used_rect.has_area():
|
||||
if _collapsed_image_props == null:
|
||||
_collapsed_image_props = image_props
|
||||
return _collapsed_image_props
|
||||
|
||||
var similar_images_props: Array[ImageProps]
|
||||
if not _similar_images_props_by_used_fragment_data_hash.has(image_props.used_fragment_data_hash):
|
||||
_similar_images_props_by_used_fragment_data_hash[image_props.used_fragment_data_hash] = similar_images_props
|
||||
else:
|
||||
similar_images_props = _similar_images_props_by_used_fragment_data_hash[image_props.used_fragment_data_hash]
|
||||
for similar_image_props in similar_images_props:
|
||||
if image_props.image == similar_image_props.image:
|
||||
# The same image found.
|
||||
return similar_image_props
|
||||
elif image_props.used_rect.size == similar_image_props.used_rect.size:
|
||||
if image_props.used_fragment_data == similar_image_props.used_fragment_data:
|
||||
if image_props.used_rect.position == similar_image_props.used_rect.position:
|
||||
# An image with equal content found.
|
||||
return similar_image_props
|
||||
else:
|
||||
# An image with equal, but offsetted content found.
|
||||
# It will have the same region, but new sprite.
|
||||
image_props.atlas_region_props = similar_image_props.atlas_region_props
|
||||
image_props.sprite_props = SpriteProps.new()
|
||||
image_props.sprite_props.offset = image_props.used_rect.position
|
||||
image_props.sprite_props.images_props.push_back(image_props)
|
||||
image_props.sprite_props.atlas_region_props = similar_image_props.atlas_region_props
|
||||
sprites_props.push_back(image_props.sprite_props)
|
||||
return image_props
|
||||
# A new unique image found.
|
||||
# It will have new region and sprite.
|
||||
image_props.atlas_region_props = AtlasRegionProps.new()
|
||||
image_props.atlas_region_props.size = image_props.used_rect.size
|
||||
image_props.sprite_props = SpriteProps.new()
|
||||
image_props.sprite_props.offset = image_props.used_rect.position
|
||||
image_props.sprite_props.images_props.push_back(image_props)
|
||||
image_props.sprite_props.atlas_region_props = image_props.atlas_region_props
|
||||
image_props.atlas_region_props.sprites_props.push_back(image_props.sprite_props)
|
||||
image_props.atlas_region_props.images_props.push_back(image_props)
|
||||
sprites_props.push_back(image_props.sprite_props)
|
||||
atlas_regions_props.push_back(image_props.atlas_region_props)
|
||||
similar_images_props.push_back(image_props)
|
||||
return image_props
|
||||
|
||||
func build_sprite_sheet(images: Array[Image]) -> SpriteSheetBuildingResult:
|
||||
var result: SpriteSheetBuildingResult = SpriteSheetBuildingResult.new()
|
||||
var images_count: int = images.size()
|
||||
|
||||
var sprite_sheet: _Common.SpriteSheetInfo = _Common.SpriteSheetInfo.new()
|
||||
|
||||
if images_count == 0:
|
||||
var atlas_image = Image.new()
|
||||
atlas_image.set_data(1, 1, false, Image.FORMAT_RGBA8, PackedByteArray([0, 0, 0, 0]))
|
||||
result.success(sprite_sheet, atlas_image)
|
||||
return result
|
||||
|
||||
sprite_sheet.source_image_size = images.front().get_size()
|
||||
if not images.all(func(i: Image) -> bool:
|
||||
return i.get_size() == sprite_sheet.source_image_size):
|
||||
result.fail(ERR_INVALID_DATA, "Input images have different sizes")
|
||||
return result
|
||||
|
||||
sprite_sheet.sprites.resize(images_count)
|
||||
|
||||
var context: SpriteSheetBuildingContext = SpriteSheetBuildingContext.new(images)
|
||||
var atlas_regions_count: int = context.atlas_regions_props.size()
|
||||
if atlas_regions_count == 0:
|
||||
# All sprites are collapsed
|
||||
var collapsed_sprite: _Common.SpriteInfo = _Common.SpriteInfo.new()
|
||||
for image_index in images_count:
|
||||
sprite_sheet.sprites[image_index] = collapsed_sprite
|
||||
var atlas_image = Image.new()
|
||||
atlas_image.set_data(1, 1, false, Image.FORMAT_RGBA8, PackedByteArray([0, 0, 0, 0]))
|
||||
result.success(sprite_sheet, atlas_image)
|
||||
return result
|
||||
|
||||
var atlas_regions_sizes: Array[Vector2i]
|
||||
atlas_regions_sizes.resize(atlas_regions_count)
|
||||
for atlas_region_index in atlas_regions_count:
|
||||
atlas_regions_sizes[atlas_region_index] = \
|
||||
context.atlas_regions_props[atlas_region_index].size
|
||||
|
||||
match _edges_artifacts_avoidance_method:
|
||||
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_SPACING, \
|
||||
_Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING:
|
||||
for atlas_region_index in atlas_regions_count:
|
||||
atlas_regions_sizes[atlas_region_index] += Vector2i.ONE
|
||||
_Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION, \
|
||||
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION:
|
||||
for atlas_region_index in atlas_regions_count:
|
||||
atlas_regions_sizes[atlas_region_index] += Vector2i.ONE * 2
|
||||
|
||||
var packing_result: _RectPacker.RectPackingResult = _RectPacker.pack(atlas_regions_sizes)
|
||||
if packing_result.error:
|
||||
result.fail(ERR_BUG, "Rect packing failed", packing_result)
|
||||
return result
|
||||
|
||||
match _edges_artifacts_avoidance_method:
|
||||
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_SPACING:
|
||||
packing_result.bounds -= Vector2i.ONE
|
||||
_Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING:
|
||||
packing_result.bounds += Vector2i.ONE
|
||||
for atlas_region_index in atlas_regions_count:
|
||||
packing_result.rects_positions[atlas_region_index] += Vector2i.ONE
|
||||
_Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION:
|
||||
for atlas_region_index in atlas_regions_count:
|
||||
packing_result.rects_positions[atlas_region_index] += Vector2i.ONE
|
||||
|
||||
var atlas_image: Image = Image.create(
|
||||
packing_result.bounds.x, packing_result.bounds.y, false, Image.FORMAT_RGBA8)
|
||||
|
||||
if _edges_artifacts_avoidance_method == _Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING:
|
||||
atlas_image.fill(_sprites_surrounding_color)
|
||||
|
||||
var extrude_sprites_borders: bool = _edges_artifacts_avoidance_method == \
|
||||
_Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION
|
||||
var expand_sprites: bool = _edges_artifacts_avoidance_method == \
|
||||
_Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION
|
||||
|
||||
var atlas_regions_positions_by_atlas_regions_props: Dictionary
|
||||
for atlas_region_index in atlas_regions_count:
|
||||
var atlas_region_props: AtlasRegionProps = context.atlas_regions_props[atlas_region_index]
|
||||
packing_result.rects_positions[atlas_region_index] += \
|
||||
Vector2i.ONE if expand_sprites else Vector2i.ZERO
|
||||
var image_props: ImageProps = atlas_region_props.images_props.front()
|
||||
atlas_image.blit_rect(image_props.used_fragment,
|
||||
Rect2i(Vector2i.ZERO, atlas_region_props.size),
|
||||
packing_result.rects_positions[atlas_region_index])
|
||||
if extrude_sprites_borders:
|
||||
_extrude_borders(atlas_image, Rect2i(
|
||||
packing_result.rects_positions[atlas_region_index],
|
||||
atlas_region_props.size))
|
||||
atlas_regions_positions_by_atlas_regions_props[atlas_region_props] = \
|
||||
packing_result.rects_positions[atlas_region_index]
|
||||
|
||||
var sprites_by_sprites_props: Dictionary
|
||||
for sprite_props in context.sprites_props:
|
||||
var sprite: _Common.SpriteInfo = sprite_props.create_sprite(
|
||||
atlas_regions_positions_by_atlas_regions_props[sprite_props.atlas_region_props])
|
||||
if expand_sprites:
|
||||
sprite.region = sprite.region.grow(1)
|
||||
sprite.offset -= Vector2i.ONE
|
||||
sprites_by_sprites_props[sprite_props] = sprite
|
||||
|
||||
var collapsed_sprite: _Common.SpriteInfo
|
||||
for image_index in images_count:
|
||||
var image_props: ImageProps = context.images_props[image_index]
|
||||
var sprite: _Common.SpriteInfo = sprites_by_sprites_props.get(image_props.sprite_props, null)
|
||||
if sprite == null:
|
||||
if collapsed_sprite == null:
|
||||
collapsed_sprite = _Common.SpriteInfo.new()
|
||||
sprite = collapsed_sprite
|
||||
sprite_sheet.sprites[image_index] = sprite
|
||||
|
||||
if expand_sprites:
|
||||
sprite_sheet.source_image_size += Vector2i.ONE * 2
|
||||
result.success(sprite_sheet, atlas_image)
|
||||
return result
|
||||
@@ -0,0 +1 @@
|
||||
uid://1gcef3dr85gs
|
||||
@@ -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 []
|
||||
@@ -0,0 +1 @@
|
||||
uid://cyv1pn7443b6j
|
||||
29
addons/nklbdev.importality/uuid.gd
Normal file
29
addons/nklbdev.importality/uuid.gd
Normal file
@@ -0,0 +1,29 @@
|
||||
extends RefCounted
|
||||
|
||||
const __BYTE_MASK: int = 0b11111111
|
||||
static var __default_rng: RandomNumberGenerator = RandomNumberGenerator.new()
|
||||
|
||||
var __bytes: PackedByteArray
|
||||
|
||||
func _init(rng: RandomNumberGenerator = null) -> void:
|
||||
if rng == null:
|
||||
rng = __default_rng
|
||||
rng.randomize()
|
||||
const size: int = 16
|
||||
__bytes.resize(size)
|
||||
for i in size:
|
||||
__bytes[i] = rng.randi() & __BYTE_MASK
|
||||
__bytes[6] = __bytes[6] & 0x0f | 0x40
|
||||
__bytes[8] = __bytes[8] & 0x3f | 0x80
|
||||
|
||||
func to_bytes() -> PackedByteArray:
|
||||
return __bytes.duplicate()
|
||||
|
||||
func is_equal(other: Object) -> bool:
|
||||
return \
|
||||
other != null and \
|
||||
get_script() == other.get_script() and \
|
||||
__bytes == other.__bytes
|
||||
|
||||
func _to_string() -> String:
|
||||
return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % Array(__bytes)
|
||||
1
addons/nklbdev.importality/uuid.gd.uid
Normal file
1
addons/nklbdev.importality/uuid.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://blafvco4qr8ah
|
||||
174
addons/nklbdev.importality/xml.gd
Normal file
174
addons/nklbdev.importality/xml.gd
Normal file
@@ -0,0 +1,174 @@
|
||||
@tool
|
||||
|
||||
class XMLNode:
|
||||
extends RefCounted
|
||||
var text: String
|
||||
func _init(text: String) -> void:
|
||||
self.text = text
|
||||
func _get_solid_text() -> String:
|
||||
assert(false, "This method is abstract and must be overriden in derived class")
|
||||
return ""
|
||||
func get_elements(text: String) -> Array[XMLNodeElement]:
|
||||
assert(false, "This method is abstract and must be overriden in derived class")
|
||||
return []
|
||||
func _dump(target: PackedStringArray, indent: String, level: int) -> void:
|
||||
target.append(indent.repeat(level) + _get_solid_text())
|
||||
|
||||
class XMLNodeParent:
|
||||
extends XMLNode
|
||||
var children: Array[XMLNode]
|
||||
func _init(text: String) -> void:
|
||||
super(text)
|
||||
func _get_opening_tag() -> String: return ""
|
||||
func _get_closing_tag() -> String: return ""
|
||||
func _get_solid_text() -> String:
|
||||
assert(false, "This method is abstract and must be overriden in derived class")
|
||||
return ""
|
||||
func _dump_children(target: PackedStringArray, indent: String, level: int) -> void:
|
||||
for child in children:
|
||||
child._dump(target, indent, level)
|
||||
func _dump(target: PackedStringArray, indent: String, level: int) -> void:
|
||||
var tag_indent: String = indent.repeat(level)
|
||||
if children.is_empty():
|
||||
target.append(tag_indent + _get_solid_text())
|
||||
else:
|
||||
target.append(tag_indent + _get_opening_tag())
|
||||
_dump_children(target, indent, level + 1)
|
||||
target.append(tag_indent + _get_closing_tag())
|
||||
func get_elements(text: String) -> Array[XMLNodeElement]:
|
||||
var result: Array[XMLNodeElement]
|
||||
result.append_array(children.filter(func(n): return n is XMLNodeElement and n.text == text))
|
||||
return result
|
||||
|
||||
class XMLNodeRoot:
|
||||
extends XMLNodeParent
|
||||
func _init() -> void:
|
||||
super("")
|
||||
func dump_to_string(indent: String = " ", new_line: String = "\n") -> String:
|
||||
var target: PackedStringArray
|
||||
_dump_children(target, indent, 0)
|
||||
return new_line.join(target)
|
||||
func dump_to_buffer(indent: String = " ", new_line: String = "\n") -> PackedByteArray:
|
||||
return dump_to_string(indent, new_line).to_utf8_buffer()
|
||||
func dump_to_file(absolute_file_path: String, indent: String = " ", new_line: String = "\n") -> void:
|
||||
DirAccess.make_dir_recursive_absolute(absolute_file_path.get_base_dir())
|
||||
var file: FileAccess = FileAccess.open(absolute_file_path, FileAccess.WRITE)
|
||||
file.store_string(dump_to_string(indent, new_line))
|
||||
file.close()
|
||||
|
||||
class XMLNodeElement:
|
||||
extends XMLNodeParent
|
||||
var attributes: Dictionary
|
||||
var closed: bool
|
||||
func _init(text: String, closed: bool = false) -> void:
|
||||
super(text)
|
||||
self.closed = closed
|
||||
func _get_attributes_string() -> String:
|
||||
return "".join(attributes.keys().map(func(k): return " %s=\"%s\"" % [k, attributes[k]]))
|
||||
func _get_opening_tag() -> String: return "<%s%s>" % [text, _get_attributes_string()]
|
||||
func _get_closing_tag() -> String:return "</%s>" % [text]
|
||||
func _get_solid_text() -> String: return "<%s%s/>" % [text, _get_attributes_string()]
|
||||
func get_string(attribute: String) -> String:
|
||||
return attributes[attribute]
|
||||
func get_int(attribute: String) -> int:
|
||||
return attributes[attribute].to_int()
|
||||
func get_int_encoded_hex_color(attribute: String, with_alpha: bool = false) -> Color:
|
||||
var arr: PackedByteArray
|
||||
arr.resize(4)
|
||||
arr.encode_u32(0, attributes[attribute].to_int())
|
||||
if not with_alpha:
|
||||
arr.resize(3)
|
||||
return Color(arr.hex_encode())
|
||||
func get_vector2i(attribute_x: String, attribute_y: String) -> Vector2i:
|
||||
return Vector2i(attributes[attribute_x].to_int(), attributes[attribute_y].to_int())
|
||||
func get_rect2i(attribute_position_x: String, attribute_position_y: String, attribute_size_x: String, attribute_size_y: String) -> Rect2i:
|
||||
return Rect2i(
|
||||
attributes[attribute_position_x].to_int(),
|
||||
attributes[attribute_position_y].to_int(),
|
||||
attributes[attribute_size_x].to_int(),
|
||||
attributes[attribute_size_y].to_int())
|
||||
func get_bool(attribute: String) -> bool:
|
||||
var raw_value: String = attributes[attribute]
|
||||
if raw_value.is_empty():
|
||||
return false
|
||||
if raw_value.is_valid_int():
|
||||
return bool(raw_value.to_int())
|
||||
if raw_value.nocasecmp_to("True") == 0:
|
||||
return true
|
||||
if raw_value.nocasecmp_to("False") == 0:
|
||||
return false
|
||||
push_warning("Failed to parse bool value from string: \"%s\", returning false..." % [raw_value])
|
||||
return false
|
||||
|
||||
class XMLNodeText:
|
||||
extends XMLNode
|
||||
func _init(text: String) -> void:
|
||||
super(text)
|
||||
func _get_solid_text() -> String: return text.strip_edges()
|
||||
func _dump(target: PackedStringArray, indent: String, level: int) -> void:
|
||||
var text: String = _get_solid_text()
|
||||
if not text.is_empty():
|
||||
target.append(indent.repeat(level) + text)
|
||||
|
||||
class XMLNodeCData:
|
||||
extends XMLNode
|
||||
func _init(text: String) -> void:
|
||||
super(text)
|
||||
func _get_solid_text() -> String: return "<![CDATA[%s]]>" % [text]
|
||||
|
||||
class XMLNodeComment:
|
||||
extends XMLNode
|
||||
func _init(text: String) -> void:
|
||||
super(text)
|
||||
func _get_solid_text() -> String: return "<!%s>" % [text]
|
||||
|
||||
class XMLNodeUnknown:
|
||||
extends XMLNode
|
||||
func _init(text: String) -> void:
|
||||
super(text)
|
||||
func _get_solid_text() -> String: return "<%s>" % [text]
|
||||
|
||||
static func parse_file(path: String) -> XMLNodeRoot:
|
||||
var parser = XMLParser.new()
|
||||
parser.open(path)
|
||||
return __parse_xml(parser)
|
||||
|
||||
static func parse_buffer(buffer: PackedByteArray) -> XMLNodeRoot:
|
||||
var parser = XMLParser.new()
|
||||
parser.open_buffer(buffer)
|
||||
return __parse_xml(parser)
|
||||
|
||||
static func parse_string(xml_string: String) -> XMLNodeRoot:
|
||||
return parse_buffer(xml_string.to_utf8_buffer())
|
||||
|
||||
static func __parse_xml(parser: XMLParser) -> XMLNodeRoot:
|
||||
var root = XMLNodeRoot.new()
|
||||
var stack: Array[XMLNode] = [root]
|
||||
while parser.read() != ERR_FILE_EOF:
|
||||
match parser.get_node_type():
|
||||
XMLParser.NODE_ELEMENT:
|
||||
var node: XMLNode = XMLNodeElement.new(parser.get_node_name())
|
||||
for attr_idx in parser.get_attribute_count():
|
||||
node.attributes[parser.get_attribute_name(attr_idx)] = \
|
||||
parser.get_attribute_value(attr_idx)
|
||||
stack.back().children.push_back(node)
|
||||
if not parser.is_empty():
|
||||
stack.push_back(node)
|
||||
XMLParser.NODE_ELEMENT_END:
|
||||
if stack.size() < 2:
|
||||
push_warning("Extra end tag found")
|
||||
else:
|
||||
stack.pop_back()
|
||||
XMLParser.NODE_TEXT:
|
||||
var text: String = parser.get_node_data().strip_edges()
|
||||
if not text.is_empty():
|
||||
stack.back().children.push_back(XMLNodeText.new(text))
|
||||
XMLParser.NODE_CDATA:
|
||||
stack.back().children.push_back(XMLNodeCData.new(parser.get_node_data()))
|
||||
XMLParser.NODE_NONE:
|
||||
push_error("Incorrect XML node found")
|
||||
XMLParser.NODE_UNKNOWN:
|
||||
stack.back().children.push_back(XMLNodeUnknown.new(parser.get_node_name()))
|
||||
XMLParser.NODE_COMMENT:
|
||||
stack.back().children.push_back(XMLNodeComment.new(parser.get_node_name()))
|
||||
return root
|
||||
1
addons/nklbdev.importality/xml.gd.uid
Normal file
1
addons/nklbdev.importality/xml.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bmnx2anyw4n70
|
||||
BIN
color_palette.aseprite
Normal file
BIN
color_palette.aseprite
Normal file
Binary file not shown.
34
color_palette.aseprite.import
Normal file
34
color_palette.aseprite.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c3p2b4383qhub"
|
||||
path="res://.godot/imported/color_palette.aseprite-6f54a6678036eed6830adb4c45132691.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://color_palette.aseprite"
|
||||
dest_files=["res://.godot/imported/color_palette.aseprite-6f54a6678036eed6830adb4c45132691.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
16
entities/block/block.tscn
Normal file
16
entities/block/block.tscn
Normal file
@@ -0,0 +1,16 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://cs3b6wqwqh4dn"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_kl3cb"]
|
||||
size = Vector2(32, 32)
|
||||
|
||||
[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_v713e"]
|
||||
size = Vector2(32, 32)
|
||||
|
||||
[node name="Block" type="StaticBody2D"]
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
shape = SubResource("RectangleShape2D_kl3cb")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
self_modulate = Color(0.109804, 0.0117647, 0.0431373, 1)
|
||||
texture = SubResource("PlaceholderTexture2D_v713e")
|
||||
BIN
entities/player/player.aseprite
Normal file
BIN
entities/player/player.aseprite
Normal file
Binary file not shown.
34
entities/player/player.aseprite.import
Normal file
34
entities/player/player.aseprite.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dysny58qsa0wo"
|
||||
path="res://.godot/imported/player.aseprite-8d5db18b9e3dc11772829f1e5211ef56.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://entities/player/player.aseprite"
|
||||
dest_files=["res://.godot/imported/player.aseprite-8d5db18b9e3dc11772829f1e5211ef56.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
17
entities/player/player.gd
Normal file
17
entities/player/player.gd
Normal file
@@ -0,0 +1,17 @@
|
||||
extends CharacterBody2D
|
||||
|
||||
|
||||
const SPEED = 300.0
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
|
||||
# Get the input direction and handle the movement/deceleration.
|
||||
# As good practice, you should replace UI actions with custom gameplay actions.
|
||||
var direction := Input.get_vector("ui_left", "ui_right","ui_up", "ui_down")
|
||||
if direction:
|
||||
velocity = direction * SPEED
|
||||
else:
|
||||
velocity = lerp(velocity, Vector2.ZERO, 0.75)
|
||||
|
||||
move_and_slide()
|
||||
1
entities/player/player.gd.uid
Normal file
1
entities/player/player.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://boxbqdc21wrbr
|
||||
17
entities/player/player.tscn
Normal file
17
entities/player/player.tscn
Normal file
@@ -0,0 +1,17 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://cprggjrluc751"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://boxbqdc21wrbr" path="res://entities/player/player.gd" id="1_symyc"]
|
||||
[ext_resource type="Texture2D" uid="uid://dysny58qsa0wo" path="res://entities/player/player.aseprite" id="2_abrql"]
|
||||
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_sfv1e"]
|
||||
radius = 16.0
|
||||
|
||||
[node name="Player" type="CharacterBody2D"]
|
||||
script = ExtResource("1_symyc")
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||
shape = SubResource("CircleShape2D_sfv1e")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
self_modulate = Color(0.223529, 1, 1, 1)
|
||||
texture = ExtResource("2_abrql")
|
||||
BIN
entities/terrain/grass/grass.aseprite
Normal file
BIN
entities/terrain/grass/grass.aseprite
Normal file
Binary file not shown.
34
entities/terrain/grass/grass.aseprite.import
Normal file
34
entities/terrain/grass/grass.aseprite.import
Normal file
@@ -0,0 +1,34 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://134x6njync81"
|
||||
path="res://.godot/imported/grass.aseprite-9dd5d05c0ac7235409737001dca84e2c.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://entities/terrain/grass/grass.aseprite"
|
||||
dest_files=["res://.godot/imported/grass.aseprite-9dd5d05c0ac7235409737001dca84e2c.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
1
icon.svg
Normal file
1
icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
|
||||
|
After Width: | Height: | Size: 994 B |
37
icon.svg.import
Normal file
37
icon.svg.import
Normal file
@@ -0,0 +1,37 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dithqymx75lu5"
|
||||
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://icon.svg"
|
||||
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
24
project.godot
Normal file
24
project.godot
Normal file
@@ -0,0 +1,24 @@
|
||||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="Ephemeral Echoes"
|
||||
config/features=PackedStringArray("4.4", "GL Compatibility")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[editor_plugins]
|
||||
|
||||
enabled=PackedStringArray("res://addons/nklbdev.importality/plugin.cfg")
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
renderer/rendering_method.mobile="gl_compatibility"
|
||||
90
worlds/World 1.tscn
Normal file
90
worlds/World 1.tscn
Normal file
@@ -0,0 +1,90 @@
|
||||
[gd_scene load_steps=5 format=4 uid="uid://bopet83k23n7t"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://134x6njync81" path="res://entities/terrain/grass/grass.aseprite" id="1_vd0v3"]
|
||||
[ext_resource type="PackedScene" uid="uid://cprggjrluc751" path="res://entities/player/player.tscn" id="2_8t5gy"]
|
||||
|
||||
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_6qmhn"]
|
||||
texture = ExtResource("1_vd0v3")
|
||||
texture_region_size = Vector2i(32, 32)
|
||||
0:0/0 = 0
|
||||
0:0/0/terrain_set = 0
|
||||
0:0/0/terrain = 0
|
||||
0:0/0/terrains_peering_bit/right_side = 0
|
||||
0:0/0/terrains_peering_bit/bottom_right_corner = 0
|
||||
0:0/0/terrains_peering_bit/bottom_side = 0
|
||||
0:1/0 = 0
|
||||
0:1/0/terrain_set = 0
|
||||
0:1/0/terrain = 0
|
||||
0:1/0/terrains_peering_bit/right_side = 0
|
||||
0:1/0/terrains_peering_bit/bottom_right_corner = 0
|
||||
0:1/0/terrains_peering_bit/bottom_side = 0
|
||||
0:1/0/terrains_peering_bit/top_side = 0
|
||||
0:1/0/terrains_peering_bit/top_right_corner = 0
|
||||
0:2/0 = 0
|
||||
0:2/0/terrain_set = 0
|
||||
0:2/0/terrain = 0
|
||||
0:2/0/terrains_peering_bit/right_side = 0
|
||||
0:2/0/terrains_peering_bit/top_side = 0
|
||||
0:2/0/terrains_peering_bit/top_right_corner = 0
|
||||
1:0/0 = 0
|
||||
1:0/0/terrain_set = 0
|
||||
1:0/0/terrain = 0
|
||||
1:0/0/terrains_peering_bit/right_side = 0
|
||||
1:0/0/terrains_peering_bit/bottom_right_corner = 0
|
||||
1:0/0/terrains_peering_bit/bottom_side = 0
|
||||
1:0/0/terrains_peering_bit/bottom_left_corner = 0
|
||||
1:0/0/terrains_peering_bit/left_side = 0
|
||||
1:1/0 = 0
|
||||
1:1/0/terrain_set = 0
|
||||
1:1/0/terrain = 0
|
||||
1:1/0/terrains_peering_bit/right_side = 0
|
||||
1:1/0/terrains_peering_bit/bottom_right_corner = 0
|
||||
1:1/0/terrains_peering_bit/bottom_side = 0
|
||||
1:1/0/terrains_peering_bit/bottom_left_corner = 0
|
||||
1:1/0/terrains_peering_bit/left_side = 0
|
||||
1:1/0/terrains_peering_bit/top_left_corner = 0
|
||||
1:1/0/terrains_peering_bit/top_side = 0
|
||||
1:1/0/terrains_peering_bit/top_right_corner = 0
|
||||
1:2/0 = 0
|
||||
1:2/0/terrain_set = 0
|
||||
1:2/0/terrain = 0
|
||||
1:2/0/terrains_peering_bit/right_side = 0
|
||||
1:2/0/terrains_peering_bit/left_side = 0
|
||||
1:2/0/terrains_peering_bit/top_left_corner = 0
|
||||
1:2/0/terrains_peering_bit/top_side = 0
|
||||
1:2/0/terrains_peering_bit/top_right_corner = 0
|
||||
2:0/0 = 0
|
||||
2:0/0/terrain_set = 0
|
||||
2:0/0/terrain = 0
|
||||
2:0/0/terrains_peering_bit/bottom_side = 0
|
||||
2:0/0/terrains_peering_bit/bottom_left_corner = 0
|
||||
2:0/0/terrains_peering_bit/left_side = 0
|
||||
2:1/0 = 0
|
||||
2:1/0/terrain_set = 0
|
||||
2:1/0/terrain = 0
|
||||
2:1/0/terrains_peering_bit/bottom_side = 0
|
||||
2:1/0/terrains_peering_bit/bottom_left_corner = 0
|
||||
2:1/0/terrains_peering_bit/left_side = 0
|
||||
2:1/0/terrains_peering_bit/top_left_corner = 0
|
||||
2:1/0/terrains_peering_bit/top_side = 0
|
||||
2:2/0 = 0
|
||||
2:2/0/terrain_set = 0
|
||||
2:2/0/terrain = 0
|
||||
2:2/0/terrains_peering_bit/left_side = 0
|
||||
2:2/0/terrains_peering_bit/top_left_corner = 0
|
||||
2:2/0/terrains_peering_bit/top_side = 0
|
||||
|
||||
[sub_resource type="TileSet" id="TileSet_wjxv1"]
|
||||
tile_size = Vector2i(32, 32)
|
||||
terrain_set_0/mode = 0
|
||||
terrain_set_0/terrain_0/name = "Grass"
|
||||
terrain_set_0/terrain_0/color = Color(0.376471, 0.784314, 0.207843, 1)
|
||||
sources/0 = SubResource("TileSetAtlasSource_6qmhn")
|
||||
|
||||
[node name="World1" type="Node2D"]
|
||||
|
||||
[node name="TileMapLayer" type="TileMapLayer" parent="."]
|
||||
tile_map_data = PackedByteArray("AAAAAAAAAAABAAEAAAAAAAEAAAABAAEAAAABAAEAAAABAAEAAAACAAIAAAABAAEAAAADAAIAAAABAAEAAAAEAAMAAAABAAEAAAAFAAMAAAABAAEAAAAFAAQAAAABAAEAAAAGAAQAAAABAAEAAAAHAAUAAAABAAEAAAAIAAUAAAACAAIAAAAIAAQAAAACAAEAAAAHAAMAAAABAAEAAAAGAAMAAAABAAEAAAAHAAQAAAABAAEAAAAGAAUAAAABAAEAAAAGAAYAAAAAAAIAAAAHAAYAAAACAAIAAAAFAAUAAAABAAIAAAAHAAIAAAABAAEAAAAIAAIAAAACAAEAAAAIAAEAAAACAAAAAAAHAAEAAAABAAEAAAAGAAIAAAABAAEAAAAEAAQAAAABAAEAAAADAAUAAAABAAIAAAAEAAUAAAABAAIAAAAGAAEAAAABAAEAAAAFAAEAAAABAAEAAAAEAAEAAAABAAEAAAADAAEAAAABAAEAAAABAAIAAAABAAEAAAAAAAMAAAABAAEAAAAAAAQAAAABAAEAAAABAAQAAAABAAEAAAACAAQAAAABAAEAAAADAAQAAAABAAEAAAAFAAIAAAABAAEAAAACAAMAAAABAAEAAAADAAMAAAABAAEAAAAHAAAAAAACAAEAAAAGAP//AAABAAEAAAAFAP//AAABAAEAAAAEAP//AAABAAEAAAACAAAAAAABAAEAAAADAP//AAABAAAAAAABAAAAAAABAAEAAAD//wIAAAABAAEAAAD//wMAAAABAAEAAAABAAMAAAABAAEAAAAEAAIAAAABAAEAAAAFAAAAAAABAAEAAAACAAEAAAABAAEAAAD//wQAAAABAAEAAAAAAAUAAAABAAEAAAD//wUAAAABAAEAAAD+/wYAAAABAAEAAAD+/wcAAAAAAAIAAAD//wcAAAABAAIAAAAAAAcAAAACAAIAAAABAAYAAAACAAIAAAACAAUAAAABAAIAAAAEAAAAAAABAAEAAAAFAP7/AAABAAEAAAAEAP7/AAAAAAAAAAACAP//AAABAAAAAAD+/wIAAAABAAEAAAAAAAIAAAABAAEAAAD+/wQAAAABAAEAAAD+/wMAAAABAAEAAAD//wEAAAABAAEAAAD//wAAAAABAAEAAAD+/wEAAAABAAEAAAD9/wQAAAABAAEAAAD9/wUAAAAAAAIAAAD+/wUAAAABAAEAAAABAAUAAAABAAEAAAAAAAYAAAABAAEAAAD//wYAAAABAAEAAAAIAAMAAAACAAEAAAAGAAAAAAABAAEAAAADAAAAAAABAAEAAAAGAP7/AAABAAEAAAAHAP7/AAACAAAAAAAHAP//AAACAAEAAAAGAP3/AAACAAAAAAAFAP3/AAAAAAAAAAD9/wMAAAABAAEAAAD9/wIAAAABAAEAAAD9/wEAAAABAAEAAAD9/wAAAAABAAEAAAD+/wAAAAABAAEAAAD9////AAABAAEAAAD+////AAABAAEAAAD/////AAABAAEAAAAAAP//AAABAAAAAAABAP//AAABAAAAAAD8////AAAAAAAAAAD8/wAAAAAAAAEAAAD8/wEAAAABAAEAAAD8/wIAAAABAAEAAAD8/wMAAAABAAEAAAD8/wQAAAABAAIAAAD+//7/AAABAAAAAAD///7/AAACAAAAAAD7/wIAAAABAAEAAAD6/wIAAAABAAEAAAD5/wIAAAAAAAEAAAD5/wMAAAAAAAIAAAD6/wMAAAABAAIAAAD7/wMAAAABAAEAAAD7/wEAAAABAAAAAAD6/wEAAAABAAAAAAD5/wEAAAAAAAAAAAD7/wQAAAAAAAIAAAD9//7/AAAAAAAAAAA=")
|
||||
tile_set = SubResource("TileSet_wjxv1")
|
||||
|
||||
[node name="Player" parent="." instance=ExtResource("2_8t5gy")]
|
||||
Reference in New Issue
Block a user