├── .gitignore ├── LICENSE ├── README.md ├── README.ru.md ├── addons └── nklbdev.importality │ ├── combined_editor_import_plugin.gd │ ├── command_line_image_format_loader_extension.gd │ ├── common.gd │ ├── dir_access_ext.gd │ ├── editor_plugin.gd │ ├── export │ ├── _.gd │ ├── _pxo_v2.gd │ ├── _pxo_v3.gd │ ├── aseprite.gd │ ├── krita.gd │ ├── pencil2d.gd │ ├── piskel.gd │ └── pixelorama.gd │ ├── external_scripts │ ├── _.gd │ ├── middle_import_script_base.gd │ ├── middle_import_script_example.gd │ ├── post_import_script_base.gd │ └── post_import_script_example.gd │ ├── import │ ├── _.gd │ ├── _node.gd │ ├── _node_with_animation_player.gd │ ├── _sprite_with_animation_player.gd │ ├── animated_sprite_2d.gd │ ├── animated_sprite_3d.gd │ ├── sprite_2d_with_animation_player.gd │ ├── sprite_3d_with_animation_player.gd │ ├── sprite_frames.gd │ ├── sprite_sheet.gd │ └── texture_rect_with_animation_player.gd │ ├── options.gd │ ├── plugin.cfg │ ├── rect_packer.gd │ ├── result.gd │ ├── setting.gd │ ├── sprite_sheet_builder │ ├── _.gd │ ├── grid_based.gd │ └── packed.gd │ ├── standalone_image_format_loader_extension.gd │ ├── uuid.gd │ └── xml.gd ├── icon.png └── icon.svg /.gitignore: -------------------------------------------------------------------------------- 1 | *.import -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 nklbdev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Importality 2 | 3 | [![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) 4 | [![en](https://img.shields.io/badge/lang-ru-green.svg)](README.ru.md) 5 | 6 | ![art for repo 2](https://github.com/nklbdev/godot-4-importality/assets/7024016/f44d98b1-116c-493e-8108-2138b1bddd61) 7 | 8 | **Importality - is an add-on for [Godot](https://godotengine.org) engine for importing graphics and animations from popular formats.** 9 | 10 | ### ATTENTION! 11 | The latest DEV-versions of Godot have broken backward compatibility, and when importing `SpriteFrames` you may see that all frames are empty. In this case, try using the plugin from the [fix_empty_frames](https://github.com/nklbdev/godot-4-importality/tree/fix_empty_frames) branch. It uses `PortableCompressedTexture2D` to save the atlas embedded right in `SpriteFrames` resource instead of saving a separated PNG-file. 12 | Please let me know about any problems with this. 13 | 14 | ### ATTENTION! 15 | In version 0.3.0, the plugin settings were moved from the ProjectSettings to the EditorSettings! This may come as a surprise and may break some of your configured processes! But this will allow you to avoid publishing your local settings along with the project file to the Git repository, and will make CI/CD easier. 16 | 17 | ## 📜 Table of contents 18 | 19 | - [Introduction](#introduction) 20 | - [Features](#features) 21 | - [How to install](#how-to-install) 22 | - [How to use](#how-to-use) 23 | - [How to help the project](#how-to-help-the-project) 24 | 25 | ## 📝 Introduction 26 | 27 | I previously published an [add-on for importing Aseprite files](https://github.com/nklbdev/godot-4-aseprite-importers). After that, I started developing a similar add-on for importing Krita files. During the development process, these projects turned out to have a lot in common, and I decided to combine them into one. Importality contains scripts for exporting data from source files to a common internal format, and scripts for importing data from an internal format into Godot resources. After that, I decide to add new export scripts for other graphic applications. 28 | 29 |

30 | 31 | Watch the demo video 32 | 33 |

34 | 35 | ## 🎯 Features 36 | 37 | - Adding recognition of source graphic files as images to Godot with all the standard features for importing them (for animated files, only the first frame will be imported). 38 | - Support for Aseprite (and LibreSprite), Krita, Pencil2D, Piskel and Pixelorama files. Other formats may be supported in the future. 39 | - Import files as: 40 | - Atlas of sprites (sprite sheet) - texture with metadata; 41 | - `SpriteFrames` resource to create your own `AnimatedSprite2D` and `AnimatedSprite3D` based on it; 42 | - `PackedScene`'s with ready-to-use `Node`'s: 43 | - `AnimatedSprite2D` and `AnimatedSprite3D` 44 | - `Sprite2D`, `Sprite3D` and `TextureRect` animated with `AnimationPlayer` 45 | - Several artifacts avoiding methods on the edges of sprites. 46 | - Grid-based and packaged layout options for sprite sheets. 47 | - Several node animation strategies with `AnimationPlayer`. 48 | - Importing any other graphics formats as regular images with external command line utilities. 49 | 50 | ## 🥁 Upcoming updates by [voting Reddit users](https://www.reddit.com/r/godot/comments/160hnuj/what_features_should_i_add_to_importality_first) 51 | 52 | 1. [Layers names filters (for layers visibility overriding)](https://github.com/nklbdev/godot-4-importality/issues/11) 53 | 1. [Linux and MacOS scripts to run Krita as different user](https://github.com/nklbdev/godot-4-importality/issues/6) (to resolve import hanging while Krita instance is running) 54 | 1. Something else (what?) - users are undecided 55 | 1. [New target resource-types](https://github.com/nklbdev/godot-4-importality/issues/14) 56 | 1. [More flexible definition of borders around sprites](https://github.com/nklbdev/godot-4-importality/issues/12) 57 | 1. [Ability to specify normal-map layer name](https://github.com/nklbdev/godot-4-importality/issues/9) 58 | 59 | ## 💽 How to install 60 | 61 | 1. Install it from [Godot Asset Library](https://godotengine.org/asset-library/asset/2025) or: 62 | - Clone this repository or download its contents as an archive. 63 | - Place the contents of the `addons` folder of the repository into the `addons` folder of your project. 64 | 1. Adjust the settings in `Editor Settings` -> `Importality` 65 | - [Specify a directory for temporary files](https://github.com/nklbdev/godot-4-importality/wiki/about-temporary-files-and-ram_drives-(en)). 66 | - Specify the command and its parameters to launch your editor in data export mode, if necessary. How to configure settings for your graphical application, see the corresponding article on the [wiki](https://github.com/nklbdev/godot-4-importality/wiki). 67 | 68 | ## 👷 How to use 69 | 70 | **Be sure to read the wiki article about the editor you are using! These articles describe the important nuances of configuring the integration!** 71 | - [Aseprite/LibreSprite](https://github.com/nklbdev/godot-4-importality/wiki/exporting-data-from-aseprite-(en)) (Important) 72 | - [Krita](https://github.com/nklbdev/godot-4-importality/wiki/exporting-data-from-krita-(en)) (**Critical!**) 73 | - [Pencil2D](https://github.com/nklbdev/godot-4-importality/wiki/exporting-data-from-pencil_2d-(en)) (Important) 74 | - [Piskel](https://github.com/nklbdev/godot-4-importality/wiki/exporting-data-from-piskel-(en)) (No integration with the application. The plugin uses its own source file parser) 75 | - [Pixelorama](https://github.com/nklbdev/godot-4-importality/wiki/exporting-data-from-pixelorama-(en)) (No integration with the application. The plugin uses its own source file parser) 76 | - [Other graphics formats](https://github.com/nklbdev/godot-4-importality/wiki/importing-as-regular-images-(en)) (Important) 77 | 78 | Then: 79 | 80 | 1. Save the files of your favorite graphics editor to the Godot project folder. 81 | 1. Select them in the Godot file system tree. They are already imported as a `Texture2D` resource. 82 | 1. Select the import method you want in the "Import" panel. 83 | 1. Customize its settings. 84 | 1. If necessary, save your settings as a default preset for this import method. 85 | 1. Click the "Reimport" button (you may need to restart the engine). 86 | 1. In the future, if you change the source files, Godot will automatically repeat the import. 87 | 88 | ## 💪 How to help the project 89 | 90 | If you know how another graphics format works, or how to use the CLI of another application, graphics and animation from which can be imported in this way - please offer your help in any way. It could be: 91 | 92 | - An [issue](https://github.com/nklbdev/godot-4-importality/issues) describing the bug, problem, or improvement for the add-on. (Please attach screenshots and other data to help reproduce your issue.) 93 | - Textual description of the format or CLI operation. 94 | - [Pull request](https://github.com/nklbdev/godot-4-importality/pulls) with new exporter. 95 | - A temporary or permanent license for paid software to be able to study it and create an exporter. For example for: 96 | - [Adobe Photoshop](https://www.adobe.com/products/photoshop.html) 97 | - [Adobe Animate](https://www.adobe.com/products/animate.html) 98 | - [Adobe Character Animator](https://www.adobe.com/products/character-animator.html) 99 | - [Affinity Photo](https://affinity.serif.com/photo) 100 | - [Moho Debut](https://moho.lostmarble.com/products/moho-debut) / [Moho Pro](https://moho.lostmarble.com/products/moho-pro) 101 | - [Toon Boom Harmony](https://www.toonboom.com/products/harmony) 102 | - [PyxelEdit](https://pyxeledit.com) 103 | - and others 104 | -------------------------------------------------------------------------------- /README.ru.md: -------------------------------------------------------------------------------- 1 | # Importality 2 | 3 | [![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) 4 | [![en](https://img.shields.io/badge/lang-ru-green.svg)](README.ru.md) 5 | 6 | ![art for repo 2](https://github.com/nklbdev/godot-4-importality/assets/7024016/f44d98b1-116c-493e-8108-2138b1bddd61) 7 | 8 | **Importality - это дополнение (addon) для движка [Godot](https://godotengine.org) для импорта графики и анимации из популярных форматов.** 9 | 10 | ### ВНИМАНИЕ! 11 | В версии 0.3.0 настройки плагина были перенесены из настроек проекта (ProjectSettings) в настройки редактора (EditorSettings)! Это может стать неожиданностью и может сломать некоторые ваши настроенные процессы! Но это позволит вам не публиковать ваши локальные настройки вместе с файлом проекта в репозиторий Git, и облегчит CI/CD. 12 | 13 | ## 📜 Содержание 14 | 15 | - [Вступление](#вступление) 16 | - [Возможности](#возможности) 17 | - [Как установить](#как-установить) 18 | - [Как использовать](#как-использовать) 19 | - [Как помочь проекту](#как-помочь-проекту) 20 | 21 | ## 📝 Вступление 22 | 23 | Ранее я уже публиковал [дополнение для импорта файлов Aseprite](https://github.com/nklbdev/godot-4-aseprite-importers). После него я начал разработку аналогичного дополнения для импорта файлов Krita. В процессе разработки у этих проектов оказалось много общего, и я решил объединить их в один. Importality содержит скрипты экспорта данных из исходных файлов в общий внутренний формат, и скрипты импорта из внутреннего формата в ресурсы Godot. После этого было решено добавить новые скрипты экспорта для других графических приложений. 24 | 25 |

26 | 27 | Watch the demo video 28 | 29 |

30 | 31 | ## 🎯 Возможности 32 | 33 | - Добавление в Godot распознавания исходных графических файлов как изображений со всеми штатными возможностями их импорта (для анимированных файлов импортируется только первый кадр). 34 | - Поддержка файлов Aseprite (и LibreSprite), Krita, Pencil2D, Piskel и Pixelorama. В будущем возможна поддержка других форматов. 35 | - Импорт файлов в качестве: 36 | - Атласа спрайтов (sprite sheet) - текстуры с метаданными; 37 | - Ресурса `SpriteFrames` для создания собственных `AnimatedSprite2D` и `AnimatedSprite3D` на его основе; 38 | - Запакованных сцен (`PackedScene`) с готовыми для использования узлами (`Node`): 39 | - `AnimatedSprite2D` и `AnimatedSprite3D` 40 | - `Sprite2D`, `Sprite3D` и `TextureRect`, анимированных с помощью `AnimationPlayer` 41 | - Несколько методов борьбы с артефактами по краям спрайтов. 42 | - Табличный и упакованный варианты раскладки атласа спрайтов. 43 | - Несколько стратегий анимации узлов с помощью `AnimationPlayer`. 44 | - Импорт любых других графических форматов как обычных изображений с помощью внешних утилит командной строки 45 | 46 | ## 🥁 Ближайшие нововведения по [просьбам пользователей Reddit](https://www.reddit.com/r/godot/comments/160hnuj/what_features_should_i_add_to_importality_first) 47 | 48 | 1. [Фильтры имен слоёв (для переопределения видимости слоёв)](https://github.com/nklbdev/godot-4-importality/issues/11) 49 | 1. [Скрипты под Linux и MacOS для запуска Krita от имени другого пользователя](https://github.com/nklbdev/godot-4-importality/issues/6) (для того, чтобы импорт не "зависал", пока запущено окно Krita) 50 | 1. Что-то еще (что именно?) - пользователи не определились 51 | 1. [Новые целевые типы ресурсов](https://github.com/nklbdev/godot-4-importality/issues/14) 52 | 1. [Более гибкая настройка рамок вокруг спрайтов](https://github.com/nklbdev/godot-4-importality/issues/12) 53 | 1. [Возможность указать имя слоя с картой нормалей](https://github.com/nklbdev/godot-4-importality/issues/9) 54 | 55 | ## 💽 Как установить 56 | 57 | 1. Установите его из [Библиотеки Ассетов Godot](https://godotengine.org/asset-library/asset/2025) или: 58 | - Склонируйте этот репозиторий или скачайте его содержимое в виде архива. 59 | - Поместите содержимое папки `addons` репозитория в папку `addons` вашего проекта. 60 | 1. Настройте параметры в `Editor Settings` -> `Importality` 61 | - [Укажите директорию для временных файлов](https://github.com/nklbdev/godot-4-importality/wiki/about-temporary-files-and-ram_drives-(ru)). 62 | - Укажите команду и её параметры для запуска вашего редактора в режиме экспорта данных, если это необходимо. Как настроить параметры для вашего графического приложения читайте в соответствующей статье [вики](https://github.com/nklbdev/godot-4-importality/wiki), посвящённой ему. 63 | 64 | ## 👷 Как использовать 65 | 66 | **Обязательно прочитайте статью на вики про редактор, который вы используете! В этих статьях описаны важные нюансы настройки интеграции!** 67 | - [Aseprite/LibreSprite](https://github.com/nklbdev/godot-4-importality/wiki/exporting-data-from-aseprite-(ru)) (Важно) 68 | - [Krita](https://github.com/nklbdev/godot-4-importality/wiki/exporting-data-from-krita-(ru)) (Критически важно!) 69 | - [Pencil2D](https://github.com/nklbdev/godot-4-importality/wiki/exporting-data-from-pencil_2d-(ru)) (Важно) 70 | - [Piskel](https://github.com/nklbdev/godot-4-importality/wiki/exporting-data-from-piskel-(ru)) (Интеграции с приложением нет. Используется собственный парсер исходных файлов) 71 | - [Pixelorama](https://github.com/nklbdev/godot-4-importality/wiki/exporting-data-from-pixelorama-(ru)) (Интеграции с приложением нет. Используется собственный парсер исходных файлов) 72 | - [Другие графические форматы](https://github.com/nklbdev/godot-4-importality/wiki/importing-as-regular-images-(ru)) (Важно!) 73 | 74 | Затем: 75 | 76 | 1. Сохраните файлы вашего любимого редактора в папку проекта Godot. 77 | 1. Выберите их в дереве файловой системы Godot. Скорее всего они уже импортированы как ресурс `Texture2D`. 78 | 1. Выберите нужный вам вариант импорта на панели "Import". 79 | 1. Настройте его параметры. 80 | 1. Если нужно, сохраните ваш вариант настройки параметров как пресет по умолчанию. 81 | 1. Нажмите кнопку "Reimport" (может понадобиться перезапуск движка). 82 | 1. В дальнейшем при изменении исходных файлов Godot автоматически повторит импорт. 83 | 84 | ## 💪 Как помочь проекту 85 | 86 | Если вы знаете, как устроен еще один формат, или как работать с CLI очередного приложения, графику и анимацию из которого можно импортировать подобным образом - пожалуйста, предложите свою помощь в любом виде. Это может быть: 87 | 88 | - [Тикет](https://github.com/nklbdev/godot-4-importality/issues) с описанием ошибки, проблемы или варианта улучшения дополнения. (Пожалуйста, приложите скриншоты и другие данные, которые помогут воспроизвести вашу проблему.) 89 | - Текстовое описание формата или работы с CLI. 90 | - [Пулл-реквест](https://github.com/nklbdev/godot-4-importality/pulls) с новым экспортером. 91 | - Временная или постоянная лицензия на платное ПО для возможности изучить его и создать экспортер. Например для: 92 | - [Adobe Photoshop](https://www.adobe.com/products/photoshop.html) 93 | - [Adobe Animate](https://www.adobe.com/products/animate.html) 94 | - [Adobe Character Animator](https://www.adobe.com/products/character-animator.html) 95 | - [Affinity Photo](https://affinity.serif.com/photo) 96 | - [Moho Debut](https://moho.lostmarble.com/products/moho-debut) / [Moho Pro](https://moho.lostmarble.com/products/moho-pro) 97 | - [Toon Boom Harmony](https://www.toonboom.com/products/harmony) 98 | - [PyxelEdit](https://pyxeledit.com) 99 | - и других 100 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/combined_editor_import_plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorImportPlugin 3 | 4 | const _Common = preload("common.gd") 5 | const _Options = preload("options.gd") 6 | const _Exporter = preload("export/_.gd") 7 | const _Importer = preload("import/_.gd") 8 | const _MiddleImportScript = preload("external_scripts/middle_import_script_base.gd") 9 | const _PostImportScript = preload("external_scripts/post_import_script_base.gd") 10 | 11 | const __empty_callable: Callable = Callable() 12 | 13 | var __exporter: _Exporter 14 | var __importer: _Importer 15 | var __import_order: int = 0 16 | var __importer_name: String 17 | var __priority: float = 1 18 | var __resource_type: StringName 19 | var __save_extension: String 20 | var __visible_name: String 21 | var __options: Array[Dictionary] 22 | var __options_visibility_checkers: Dictionary 23 | 24 | func _init(exporter: _Exporter, importer: _Importer) -> void: 25 | __importer = importer 26 | __exporter = exporter 27 | __import_order = 1 28 | __importer_name = "%s %s" % [exporter.get_name(), importer.get_name()] 29 | __priority = 1 30 | __resource_type = importer.get_resource_type() 31 | __save_extension = importer.get_save_extension() 32 | __visible_name = "%s -> %s" % [exporter.get_name(), importer.get_name()] 33 | var options: Array[Dictionary] 34 | __options.append_array(importer.get_options()) 35 | __options.append(_Options.create_option( 36 | _Options.MIDDLE_IMPORT_SCRIPT_PATH, "", PROPERTY_HINT_FILE, "*.gd", PROPERTY_USAGE_DEFAULT)) 37 | __options.append(_Options.create_option( 38 | _Options.POST_IMPORT_SCRIPT_PATH, "", PROPERTY_HINT_FILE, "*.gd", PROPERTY_USAGE_DEFAULT)) 39 | __options.append_array(exporter.get_options()) 40 | for option in __options: 41 | if option.has(&"get_is_visible"): 42 | __options_visibility_checkers[option.name] = option.get_is_visible 43 | 44 | func _import( 45 | res_source_file_path: String, 46 | res_save_file_path: String, 47 | options: Dictionary, 48 | platform_variants: Array[String], 49 | gen_files: Array[String] 50 | ) -> Error: 51 | var error: Error 52 | 53 | var export_result: _Exporter.ExportResult = \ 54 | __exporter.export(res_source_file_path, options, self) 55 | if export_result.error: 56 | push_error("Export is failed. Errors chain:\n%s" % [export_result]) 57 | return export_result.error 58 | 59 | var middle_import_script_context: _MiddleImportScript.Context = _MiddleImportScript.Context.new() 60 | middle_import_script_context.atlas_image = export_result.atlas_image 61 | middle_import_script_context.sprite_sheet = export_result.sprite_sheet 62 | middle_import_script_context.animation_library = export_result.animation_library 63 | 64 | 65 | # -------- MIDDLE IMPORT BEGIN -------- 66 | var middle_import_script_path: String = options[_Options.MIDDLE_IMPORT_SCRIPT_PATH].strip_edges() 67 | if middle_import_script_path: 68 | if not (middle_import_script_path.is_absolute_path() and middle_import_script_path.begins_with("res://")): 69 | push_error("Middle import script path is not valid: %s" % [middle_import_script_path]) 70 | return ERR_FILE_BAD_PATH 71 | var middle_import_script: Script = ResourceLoader \ 72 | .load(middle_import_script_path, "Script") as Script 73 | if middle_import_script == null: 74 | push_error("Failed to load middle import script: %s" % [middle_import_script_path]) 75 | return ERR_FILE_CORRUPT 76 | if not __is_script_inherited_from(middle_import_script, _MiddleImportScript): 77 | 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]) 78 | return ERR_INVALID_DECLARATION 79 | error = middle_import_script.modify_context( 80 | res_source_file_path, 81 | res_save_file_path, 82 | self, 83 | options, 84 | middle_import_script_context) 85 | if error: 86 | push_error("Failed to perform middle-import-script") 87 | return error 88 | error = __append_gen_files(gen_files, middle_import_script_context.gen_files_to_add) 89 | if error: 90 | push_error("Failed to add gen files from middle-import-script context") 91 | return error 92 | # -------- MIDDLE IMPORT END -------- 93 | 94 | 95 | 96 | var portableCompressedTexture: PortableCompressedTexture2D = PortableCompressedTexture2D.new() 97 | portableCompressedTexture.create_from_image(middle_import_script_context.atlas_image, PortableCompressedTexture2D.COMPRESSION_MODE_LOSSLESS) 98 | 99 | var import_result: _Importer.ImportResult = __importer.import( 100 | res_source_file_path, 101 | portableCompressedTexture, 102 | middle_import_script_context.sprite_sheet, 103 | middle_import_script_context.animation_library, 104 | options, 105 | res_save_file_path) 106 | if import_result.error: 107 | push_error("Import is failed. Errors chain:\n%s" % [import_result]) 108 | return import_result.error 109 | 110 | var post_import_script_context: _PostImportScript.Context = _PostImportScript.Context.new() 111 | post_import_script_context.resource = import_result.resource 112 | post_import_script_context.resource_saver_flags = import_result.resource_saver_flags 113 | post_import_script_context.save_extension = _get_save_extension() 114 | 115 | 116 | 117 | # -------- POST IMPORT BEGIN -------- 118 | var post_import_script_path: String = options[_Options.POST_IMPORT_SCRIPT_PATH].strip_edges() 119 | if post_import_script_path: 120 | if not (post_import_script_path.is_absolute_path() and post_import_script_path.begins_with("res://")): 121 | push_error("Post import script path is not valid: %s" % [post_import_script_path]) 122 | return ERR_FILE_BAD_PATH 123 | var post_import_script: Script = ResourceLoader \ 124 | .load(post_import_script_path, "Script") as Script 125 | if post_import_script == null: 126 | push_error("Failed to load post import script: %s" % [post_import_script_path]) 127 | return ERR_FILE_CORRUPT 128 | if not __is_script_inherited_from(post_import_script, _PostImportScript): 129 | 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]) 130 | return ERR_INVALID_DECLARATION 131 | error = post_import_script.modify_context( 132 | res_source_file_path, 133 | res_save_file_path, 134 | self, 135 | options, 136 | middle_import_script_context.middle_import_data, 137 | post_import_script_context) 138 | if error: 139 | push_error("Failed to perform post-import-script") 140 | return error 141 | error = __append_gen_files(gen_files, post_import_script_context.gen_files_to_add) 142 | if error: 143 | push_error("Failed to add gen files from post-import-script context") 144 | return error 145 | # -------- POST IMPORT END -------- 146 | 147 | 148 | error = ResourceSaver.save( 149 | post_import_script_context.resource, 150 | "%s.%s" % [res_save_file_path, post_import_script_context.save_extension], 151 | post_import_script_context.resource_saver_flags) 152 | if error: 153 | push_error("Failed to save the new resource via ResourceSaver") 154 | return error 155 | 156 | func _get_import_options(path: String, preset_index: int) -> Array[Dictionary]: 157 | return __options 158 | 159 | func _get_option_visibility(path: String, option_name: StringName, options: Dictionary) -> bool: 160 | if __options_visibility_checkers.has(option_name): 161 | return __options_visibility_checkers[option_name].call(options) 162 | return true 163 | 164 | func _get_import_order() -> int: 165 | return __import_order 166 | 167 | func _get_importer_name() -> String: 168 | return __importer_name 169 | 170 | func _get_preset_count() -> int: 171 | return 1 172 | 173 | func _get_preset_name(preset_index: int) -> String: 174 | return "Default" 175 | 176 | func _get_priority() -> float: 177 | return __priority 178 | 179 | func _get_recognized_extensions() -> PackedStringArray: 180 | return __exporter.get_recognized_extensions() 181 | 182 | func _get_resource_type() -> String: 183 | return __importer.get_resource_type() 184 | 185 | func _get_save_extension() -> String: 186 | return __importer.get_save_extension() 187 | 188 | func _get_visible_name() -> String: 189 | return __visible_name 190 | 191 | func __is_script_inherited_from(script: Script, base_script: Script) -> bool: 192 | while script != null: 193 | if script == base_script: 194 | return true 195 | script = script.get_base_script() 196 | return false 197 | 198 | func __append_gen_files(gen_files: PackedStringArray, gen_files_to_add: PackedStringArray) -> Error: 199 | for gen_file_path in gen_files_to_add: 200 | gen_file_path = gen_file_path.strip_edges() 201 | if gen_files.has(gen_file_path): 202 | continue 203 | if not gen_file_path.is_absolute_path(): 204 | push_error("Gen-file-path is not valid path: %s" % [gen_file_path]) 205 | return ERR_FILE_BAD_PATH 206 | if not gen_file_path.begins_with("res://"): 207 | push_error("Gen-file-path is not a resource file system path (res://): %s" % [gen_file_path]) 208 | return ERR_FILE_BAD_PATH 209 | if not FileAccess.file_exists(gen_file_path): 210 | push_error("The file at the gen-file-path was not found: %s" % [gen_file_path]) 211 | return ERR_FILE_NOT_FOUND 212 | gen_files.push_back(gen_file_path) 213 | return OK 214 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/command_line_image_format_loader_extension.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "standalone_image_format_loader_extension.gd" 3 | 4 | const _Common = preload("common.gd") 5 | 6 | static var command_building_rules_for_custom_image_loader_setting: _Setting = _Setting.new( 7 | "command_building_rules_for_custom_image_loader", PackedStringArray(), TYPE_PACKED_STRING_ARRAY, PROPERTY_HINT_NONE) 8 | 9 | func _get_recognized_extensions() -> PackedStringArray: 10 | var rules_by_extensions_result: _Setting.GettingValueResult = command_building_rules_for_custom_image_loader_setting.get_value() 11 | if rules_by_extensions_result.error: 12 | push_error("Failed to get command building rules for custom image loader setting") 13 | return PackedStringArray() 14 | var extensions: PackedStringArray 15 | for rule_string in rules_by_extensions_result.value: 16 | var parsed_rule: Dictionary = _parse_rule(rule_string) 17 | if parsed_rule.is_empty(): 18 | push_error("Failed to parse command building rule") 19 | return PackedStringArray() 20 | for extension in parsed_rule.extensions as PackedStringArray: 21 | if extensions.has(extension): 22 | push_error("There are duplicated file extensions found in command building rules") 23 | return PackedStringArray() 24 | extensions.push_back(extension) 25 | return extensions 26 | 27 | func get_settings() -> Array[_Setting]: 28 | return [command_building_rules_for_custom_image_loader_setting] 29 | 30 | static var regex_middle_spaces: RegEx = RegEx.create_from_string("(?<=\\S)\\s(?>=\\S)") 31 | static func normalize_string(source: String) -> String: 32 | return regex_middle_spaces.sub(source.strip_edges(), " ", true) 33 | 34 | func _parse_rule(rule_string: String) -> Dictionary: 35 | var parts: PackedStringArray = rule_string.split(":", false, 1) 36 | if parts.size() != 2: 37 | push_error("Failed to find colon (:) delimiter in command building rule between file extensions and command template") 38 | return {} 39 | var extensions: PackedStringArray 40 | for extensions_splitted_by_spaces in normalize_string(parts[0]).split(" ", false): 41 | extensions.append_array(extensions_splitted_by_spaces.split(",", false)) 42 | if extensions.is_empty(): 43 | push_error("Extensions list in command building rule is empty") 44 | return {} 45 | var command_template: String = parts[1].strip_edges() 46 | if command_template.is_empty(): 47 | push_error("Command template in command building rule is empty") 48 | return {} 49 | return { 50 | extensions = extensions, 51 | command_template = command_template, 52 | } 53 | 54 | func _load_image( 55 | image: Image, 56 | file_access: FileAccess, 57 | flags, 58 | scale: float 59 | ) -> Error: 60 | 61 | var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value() 62 | if temp_dir_path_result.error: 63 | push_error("Failed to get Temporary Files Directory Path to export image from source file: %s" % [temp_dir_path_result]) 64 | return ERR_UNCONFIGURED 65 | 66 | var rules_by_extensions_result: _Setting.GettingValueResult = command_building_rules_for_custom_image_loader_setting.get_value() 67 | if rules_by_extensions_result.error: 68 | push_error("Failed to get command building rules for custom image loader setting") 69 | return ERR_UNCONFIGURED 70 | 71 | var command_templates_by_extensions: Dictionary 72 | for rule_string in rules_by_extensions_result.value: 73 | var parsed_rule: Dictionary = _parse_rule(rule_string) 74 | if parsed_rule.is_empty(): 75 | push_error("Failed to parse command building rule") 76 | return ERR_UNCONFIGURED 77 | for extension in parsed_rule.extensions as PackedStringArray: 78 | if command_templates_by_extensions.has(extension): 79 | push_error("There are duplicated file extensions found in command building rules") 80 | return ERR_UNCONFIGURED 81 | command_templates_by_extensions[extension] = \ 82 | parsed_rule.command_template 83 | 84 | var global_input_path: String = file_access.get_path_absolute() 85 | var extension = global_input_path.get_extension() 86 | var global_output_path: String = ProjectSettings.globalize_path( 87 | temp_dir_path_result.value.path_join("temp.png")) 88 | 89 | var command_template: String = command_templates_by_extensions.get(extension, "") as String 90 | if command_template.is_empty(): 91 | push_error("Failed to find command template for file extension: " + extension) 92 | return ERR_UNCONFIGURED 93 | 94 | var command_template_parts: PackedStringArray = _Common.split_words_with_quotes(command_template) 95 | if command_template_parts.is_empty(): 96 | push_error("Failed to recognize command template parts for extension: %s" % [extension]) 97 | return ERR_UNCONFIGURED 98 | 99 | for command_template_part_index in command_template_parts.size(): 100 | var command_template_part: String = command_template_parts[command_template_part_index] 101 | command_template_parts[command_template_part_index] = \ 102 | command_template_parts[command_template_part_index] \ 103 | .replace("{in_path}", global_input_path) \ 104 | .replace("{in_path_b}", global_input_path.replace("/", "\\")) \ 105 | .replace("{in_path_base}", global_input_path.get_basename()) \ 106 | .replace("{in_path_base_b}", global_input_path.get_basename().replace("/", "\\")) \ 107 | .replace("{in_file}", global_input_path.get_file()) \ 108 | .replace("{in_file_base}", global_input_path.get_file().get_basename()) \ 109 | .replace("{in_dir}", global_input_path.get_base_dir()) \ 110 | .replace("{in_dir_b}", global_input_path.get_base_dir().replace("/", "\\")) \ 111 | .replace("{in_ext}", extension) \ 112 | .replace("{out_path}", global_output_path) \ 113 | .replace("{out_path_b}", global_output_path.replace("/", "\\")) \ 114 | .replace("{out_path_base}", global_output_path.get_basename()) \ 115 | .replace("{out_path_base_b}", global_output_path.get_basename().replace("/", "\\")) \ 116 | .replace("{out_file}", global_output_path.get_file()) \ 117 | .replace("{out_file_base}", global_output_path.get_file().get_basename()) \ 118 | .replace("{out_dir}", global_output_path.get_base_dir()) \ 119 | .replace("{out_dir_b}", global_output_path.get_base_dir().replace("/", "\\")) \ 120 | .replace("{out_ext}", "png") 121 | 122 | var command: String = command_template_parts[0] 123 | var arguments: PackedStringArray = command_template_parts.slice(1) 124 | 125 | var output: Array 126 | var exit_code: int = OS.execute(command, arguments, output, true, false) 127 | if exit_code: 128 | for arg_index in arguments.size(): 129 | arguments[arg_index] = "\nArgument: " + arguments[arg_index] 130 | push_error(" ".join([ 131 | "An error occurred while executing", 132 | "the external image converting utility command.", 133 | "Process exited with code %s:\nCommand: %s%s" 134 | ]) % [exit_code, command, "".join(arguments)]) 135 | return ERR_QUERY_FAILED 136 | 137 | if not FileAccess.file_exists(global_output_path): 138 | push_error("The output temporary PNG file is not found: %s" % [global_output_path]) 139 | return ERR_UNCONFIGURED 140 | 141 | var err: Error = image.load_png_from_buffer(FileAccess.get_file_as_bytes(global_output_path)) 142 | if err: 143 | push_error("Failed to load temporary PNG file as image: %s" % [global_output_path]) 144 | return err 145 | 146 | err = DirAccess.remove_absolute(global_output_path) 147 | if err: 148 | push_warning("Failed to remove temporary file \"%s\". Continuing..." % [global_output_path]) 149 | 150 | return OK 151 | 152 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/common.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | 3 | const _Setting = preload("setting.gd") 4 | 5 | const SPRITE_SHEET_LAYOUTS_NAMES: PackedStringArray = [ 6 | "Packed", 7 | "Horizontal strips", 8 | "Vertical strips", 9 | ] 10 | enum SpriteSheetLayout { 11 | PACKED = 0, 12 | HORIZONTAL_STRIPS = 1, 13 | VERTICAL_STRIPS = 2, 14 | } 15 | 16 | const EDGES_ARTIFACTS_AVOIDANCE_METHODS_NAMES: PackedStringArray = [ 17 | "None", 18 | "Transparent spacing", 19 | "Solid color surrounding", 20 | "Borders extrusion", 21 | "Transparent expansion", 22 | ] 23 | enum EdgesArtifactsAvoidanceMethod { 24 | NONE = 0, 25 | TRANSPARENT_SPACING = 1, 26 | SOLID_COLOR_SURROUNDING = 2, 27 | BORDERS_EXTRUSION = 3, 28 | TRANSPARENT_EXPANSION = 4, 29 | } 30 | 31 | const ANIMATION_DIRECTIONS_NAMES: PackedStringArray = [ 32 | "Forward", 33 | "Reverse", 34 | "Ping-pong", 35 | "Ping-pong reverse", 36 | ] 37 | enum AnimationDirection { 38 | FORWARD = 0, 39 | REVERSE = 1, 40 | PING_PONG = 2, 41 | PING_PONG_REVERSE = 3, 42 | } 43 | 44 | const ANIMATION_STRATEGIES_NAMES: PackedStringArray = [ 45 | "Animate sprite's region and offset", 46 | "Animate single atlas texture's region and margin", 47 | "Animate multiple atlas textures instances", 48 | ] 49 | 50 | enum AnimationStrategy { 51 | SPRITE_REGION_AND_OFFSET = 0, 52 | SINGLE_ATLAS_TEXTURE_REGION_AND_MARGIN = 1, 53 | MULTIPLE_ATLAS_TEXTURES_INSTANCES = 2, 54 | MAX_ALL = 5, 55 | MAX_WITH_BORDERS = 3, 56 | } 57 | 58 | class SpriteInfo: 59 | extends RefCounted 60 | var region: Rect2i 61 | var offset: Vector2i 62 | 63 | class SpriteSheetInfo: 64 | extends RefCounted 65 | var source_image_size: Vector2i 66 | var sprites: Array[SpriteInfo] 67 | 68 | class FrameInfo: 69 | extends RefCounted 70 | var sprite: SpriteInfo 71 | var duration: float 72 | 73 | class AnimationInfo: 74 | extends RefCounted 75 | var name: String 76 | var direction: AnimationDirection 77 | var repeat_count: int 78 | var frames: Array[FrameInfo] 79 | func get_flatten_frames() -> Array[FrameInfo]: 80 | var iteration_frames: Array[FrameInfo] = frames.duplicate() 81 | if direction == AnimationDirection.REVERSE or direction == AnimationDirection.PING_PONG_REVERSE: 82 | iteration_frames.reverse() 83 | if direction == AnimationDirection.PING_PONG or direction == AnimationDirection.PING_PONG_REVERSE: 84 | var returning_frames: Array[FrameInfo] = iteration_frames.duplicate() 85 | returning_frames.pop_back() 86 | returning_frames.reverse() 87 | returning_frames.pop_back() 88 | iteration_frames.append_array(returning_frames) 89 | if repeat_count <= 1: 90 | return iteration_frames 91 | var output_frames: Array[FrameInfo] 92 | var iteration_frames_count: int = iteration_frames.size() 93 | output_frames.resize(iteration_frames_count * repeat_count) 94 | for iteration_number in repeat_count: 95 | for frame_index in iteration_frames_count: 96 | output_frames[iteration_number * iteration_frames_count + frame_index] = \ 97 | iteration_frames[frame_index] 98 | return output_frames 99 | 100 | class AnimationLibraryInfo: 101 | extends RefCounted 102 | var animations: Array[AnimationInfo] 103 | var autoplay_index: int = -1 104 | 105 | static func get_vector2i(dict: Dictionary, x_key: String, y_key: String) -> Vector2i: 106 | return Vector2i(int(dict[x_key]), int(dict[y_key])) 107 | 108 | static var common_temporary_files_directory_path_setting: _Setting = _Setting.new( 109 | &"temporary_files_directory_path", "", TYPE_STRING, PROPERTY_HINT_GLOBAL_DIR, 110 | "", true, func(v: String): return v.is_empty()) 111 | 112 | const __backslash: String = "\\" 113 | const __quote: String = "\"" 114 | const __space: String = " " 115 | const __tab: String = "\t" 116 | const __empty: String = "" 117 | static func split_words_with_quotes(source: String) -> PackedStringArray: 118 | var parts: PackedStringArray 119 | if source.is_empty(): 120 | return parts 121 | 122 | var quotation: bool 123 | 124 | var previous: String 125 | var current: String 126 | var next: String = source[0] 127 | var chars_count = source.length() 128 | 129 | var part: String 130 | for char_idx in chars_count: 131 | previous = current 132 | current = next 133 | next = source[char_idx + 1] if char_idx < chars_count - 1 else "" 134 | if quotation: 135 | # seek for quotation end 136 | if previous != __backslash and current == __quote: 137 | if next == __space or next == __tab or next == __empty: 138 | quotation = false 139 | parts.push_back(part) 140 | part = "" 141 | continue 142 | else: 143 | push_error("Invalid quotation start at %s:\n%s\n%s" % [char_idx, source, " ".repeat(char_idx) + "^"]) 144 | return PackedStringArray() 145 | else: 146 | # seek for quotation start 147 | if current == __space or current == __tab: 148 | if not part.is_empty(): 149 | parts.push_back(part) 150 | part = "" 151 | continue 152 | else: 153 | if previous != __backslash and current == __quote: 154 | if previous == __space or previous == __tab or previous == __empty: 155 | quotation = true 156 | continue 157 | else: 158 | push_error("Invalid quotation end at %s:\n%s\n%s" % [char_idx, source, " ".repeat(char_idx) + "^"]) 159 | return PackedStringArray() 160 | part += current 161 | if quotation: 162 | push_error("Invalid quotation end at %s:\n%s\n%s" % [chars_count - 1, source, " ".repeat(chars_count - 1) + "^"]) 163 | return PackedStringArray() 164 | if not part.is_empty(): 165 | parts.push_back(part) 166 | return parts 167 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/dir_access_ext.gd: -------------------------------------------------------------------------------- 1 | extends Object 2 | 3 | const _Result = preload("result.gd").Class 4 | 5 | class CreationResult: 6 | extends _Result 7 | var path: String 8 | func success(path: String) -> void: 9 | super._success() 10 | self.path = path 11 | 12 | class RemovalResult: 13 | extends _Result 14 | 15 | static func create_directory_with_unique_name(base_directory_path: String) -> CreationResult: 16 | const error_description: String = "Failed to create a directory with unique name" 17 | var name: String 18 | var path: String 19 | var result = CreationResult.new() 20 | 21 | var error = DirAccess.make_dir_recursive_absolute(base_directory_path) 22 | match error: 23 | OK, ERR_ALREADY_EXISTS: 24 | pass 25 | _: 26 | var inner_result: CreationResult = CreationResult.new() 27 | inner_result.fail(ERR_QUERY_FAILED, "Failed to create base directory recursive") 28 | result.fail( 29 | ERR_CANT_CREATE, 30 | "%s: %s \"%s\"" % 31 | [error_description, error, error_string(error)], 32 | inner_result) 33 | return result 34 | 35 | while true: 36 | name = "%d" % (Time.get_unix_time_from_system() * 1000) 37 | path = base_directory_path.path_join(name) 38 | if not DirAccess.dir_exists_absolute(path): 39 | error = DirAccess.make_dir_absolute(path) 40 | match error: 41 | ERR_ALREADY_EXISTS: 42 | pass 43 | OK: 44 | result.success(path) 45 | break 46 | _: 47 | result.fail( 48 | ERR_CANT_CREATE, 49 | "%s: %s \"%s\"" % 50 | [error_description, error, error_string(error)]) 51 | break 52 | return result 53 | 54 | static func remove_dir_recursive(dir_path: String) -> RemovalResult: 55 | const error_description: String = "Failed to remove a directory with contents recursive" 56 | var result: RemovalResult = RemovalResult.new() 57 | for child_file_name in DirAccess.get_files_at(dir_path): 58 | var child_file_path = dir_path.path_join(child_file_name) 59 | var error: Error = DirAccess.remove_absolute(child_file_path) 60 | if error: 61 | var inner_result: RemovalResult = RemovalResult.new() 62 | inner_result.fail( 63 | ERR_QUERY_FAILED, 64 | "Failed to remove a file: \"%s\". Error: %s \"%s\"" % 65 | [child_file_path, error, error_string(error)]) 66 | result.fail(ERR_QUERY_FAILED, "%s: \"%s\"" % [error_description, dir_path], inner_result) 67 | return result 68 | for child_dir_name in DirAccess.get_directories_at(dir_path): 69 | var child_dir_path = dir_path.path_join(child_dir_name) 70 | var inner_result: RemovalResult = remove_dir_recursive(child_dir_path) 71 | if inner_result.error: 72 | result.fail(ERR_QUERY_FAILED, "%s: \"%s\"" % [error_description, dir_path], inner_result) 73 | return result 74 | var error: Error = DirAccess.remove_absolute(dir_path) 75 | if error: 76 | result.fail( 77 | ERR_QUERY_FAILED, 78 | "%s: \"%s\". Error: %s \"%s\"" % 79 | [error_description, dir_path, error, error_string(error)]) 80 | return result 81 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/editor_plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | const ExporterBase = preload("export/_.gd") 5 | const EXPORTERS_SCRIPTS: Array[GDScript] = [ 6 | preload("export/aseprite.gd"), 7 | preload("export/krita.gd"), 8 | preload("export/pencil2d.gd"), 9 | preload("export/piskel.gd"), 10 | preload("export/pixelorama.gd"), 11 | ] 12 | 13 | const ImporterBase = preload("import/_.gd") 14 | const IMPORTERS_SCRIPTS: Array[GDScript] = [ 15 | preload("import/animated_sprite_2d.gd"), 16 | preload("import/animated_sprite_3d.gd"), 17 | preload("import/sprite_2d_with_animation_player.gd"), 18 | preload("import/sprite_3d_with_animation_player.gd"), 19 | preload("import/sprite_frames.gd"), 20 | preload("import/texture_rect_with_animation_player.gd"), 21 | preload("import/sprite_sheet.gd"), 22 | ] 23 | 24 | const StandaloneImageFormatLoaderExtension = preload("standalone_image_format_loader_extension.gd") 25 | const STANDALONE_IMAGE_FORMAT_LOADER_EXTENSIONS: Array[GDScript] = [ 26 | preload("command_line_image_format_loader_extension.gd") 27 | ] 28 | 29 | const CombinedEditorImportPlugin = preload("combined_editor_import_plugin.gd") 30 | 31 | var __editor_import_plugins: Array[EditorImportPlugin] 32 | var __image_format_loader_extensions: Array[ImageFormatLoaderExtension] 33 | 34 | func _enter_tree() -> void: 35 | var editor_interface: EditorInterface = get_editor_interface() 36 | var editor_settings: EditorSettings = editor_interface.get_editor_settings() 37 | 38 | var exporters: Array[ExporterBase] 39 | for Exporter in EXPORTERS_SCRIPTS: 40 | var exporter: ExporterBase = Exporter.new() 41 | for setting in exporter.get_settings(): 42 | setting.register(editor_settings) 43 | exporters.push_back(exporter) 44 | var image_format_loader_extension: ImageFormatLoaderExtension = \ 45 | exporter.get_image_format_loader_extension() 46 | if image_format_loader_extension: 47 | __image_format_loader_extensions.push_back(image_format_loader_extension) 48 | image_format_loader_extension.add_format_loader() 49 | var importers: Array[ImporterBase] 50 | for Importer in IMPORTERS_SCRIPTS: 51 | importers.push_back(Importer.new()) 52 | for exporter in exporters: 53 | for importer in importers: 54 | var editor_import_plugin: EditorImportPlugin = \ 55 | CombinedEditorImportPlugin.new(exporter, importer) 56 | __editor_import_plugins.push_back(editor_import_plugin) 57 | add_import_plugin(editor_import_plugin) 58 | for Extension in STANDALONE_IMAGE_FORMAT_LOADER_EXTENSIONS: 59 | var image_format_loader_extension: StandaloneImageFormatLoaderExtension = \ 60 | Extension.new() as StandaloneImageFormatLoaderExtension 61 | for setting in image_format_loader_extension.get_settings(): 62 | setting.register(editor_settings) 63 | __image_format_loader_extensions.push_back(image_format_loader_extension) 64 | image_format_loader_extension.add_format_loader() 65 | 66 | func _exit_tree() -> void: 67 | for editor_import_plugin in __editor_import_plugins: 68 | remove_import_plugin(editor_import_plugin) 69 | __editor_import_plugins.clear() 70 | for image_format_loader_extension in __image_format_loader_extensions: 71 | image_format_loader_extension.remove_format_loader() 72 | __image_format_loader_extensions.clear() 73 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/export/_.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends RefCounted 3 | 4 | const _Result = preload("../result.gd").Class 5 | const _Common = preload("../common.gd") 6 | const _Options = preload("../options.gd") 7 | const _Setting = preload("../setting.gd") 8 | const _DirAccessExtensions = preload("../dir_access_ext.gd") 9 | 10 | const _SpriteSheetBuilderBase = preload("../sprite_sheet_builder/_.gd") 11 | const _GridBasedSpriteSheetBuilder = preload("../sprite_sheet_builder/grid_based.gd") 12 | const _PackedSpriteSheetBuilder = preload("../sprite_sheet_builder/packed.gd") 13 | 14 | const ATLAS_TEXTURE_RESOURCE_TYPE_NAMES: PackedStringArray = [ 15 | "Embedded PortableCompressedTexture2D (compact)", 16 | "Embedded ImageTexture (large)", 17 | "Separated image (custom)", 18 | ] 19 | enum AtlasResourceType { 20 | EMBEDDED_PORTABLE_COMPRESSED_TEXTURE_2D = 0, 21 | EMBEDDED_IMAGE_TEXTURE = 1, 22 | SEPARATED_IMAGE = 2, 23 | } 24 | 25 | class ExportResult: 26 | extends _Result 27 | var atlas_image: Image 28 | var sprite_sheet: _Common.SpriteSheetInfo 29 | var animation_library: _Common.AnimationLibraryInfo 30 | func _get_result_type_description() -> String: 31 | return "Export" 32 | func success( 33 | atlas_image: Image, 34 | sprite_sheet: _Common.SpriteSheetInfo, 35 | animation_library: _Common.AnimationLibraryInfo 36 | ) -> void: 37 | _success() 38 | self.atlas_image = atlas_image 39 | self.sprite_sheet = sprite_sheet 40 | self.animation_library = animation_library 41 | 42 | var __name: String 43 | var __recognized_extensions: PackedStringArray 44 | var __settings: Array[_Setting] = [_Common.common_temporary_files_directory_path_setting] 45 | var __options: Array[Dictionary] = [ 46 | _Options.create_option(_Options.EDGES_ARTIFACTS_AVOIDANCE_METHOD, _Common.EdgesArtifactsAvoidanceMethod.NONE, 47 | PROPERTY_HINT_ENUM, ",".join(_Common.EDGES_ARTIFACTS_AVOIDANCE_METHODS_NAMES), 48 | PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 49 | _Options.create_option(_Options.SPRITES_SURROUNDING_COLOR, Color.TRANSPARENT, 50 | PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, 51 | func(o): return o[_Options.EDGES_ARTIFACTS_AVOIDANCE_METHOD] == \ 52 | _Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING), 53 | _Options.create_option(_Options.SPRITE_SHEET_LAYOUT, _Common.SpriteSheetLayout.PACKED, 54 | PROPERTY_HINT_ENUM, ",".join(_Common.SPRITE_SHEET_LAYOUTS_NAMES), 55 | PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 56 | _Options.create_option(_Options.MAX_CELLS_IN_STRIP, 0, 57 | PROPERTY_HINT_RANGE, "0,,1,or_greater", PROPERTY_USAGE_DEFAULT, 58 | func(o): return o[_Options.SPRITE_SHEET_LAYOUT] != \ 59 | _Common.SpriteSheetLayout.PACKED), 60 | _Options.create_option(_Options.TRIM_SPRITES_TO_OVERALL_MIN_SIZE, true, 61 | PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, 62 | func(o): return o[_Options.SPRITE_SHEET_LAYOUT] != \ 63 | _Common.SpriteSheetLayout.PACKED), 64 | _Options.create_option(_Options.COLLAPSE_TRANSPARENT_SPRITES, true, 65 | PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, 66 | func(o): return o[_Options.SPRITE_SHEET_LAYOUT] != \ 67 | _Common.SpriteSheetLayout.PACKED), 68 | _Options.create_option(_Options.MERGE_DUPLICATED_SPRITES, true, 69 | PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, 70 | func(o): return o[_Options.SPRITE_SHEET_LAYOUT] != \ 71 | _Common.SpriteSheetLayout.PACKED), 72 | ] 73 | var __image_format_loader_extension: ImageFormatLoaderExtension 74 | 75 | func _init( 76 | name: String, 77 | recognized_extensions: PackedStringArray, 78 | options: Array[Dictionary], 79 | settings: Array[_Setting], 80 | image_format_loader_extension: ImageFormatLoaderExtension = null 81 | ) -> void: 82 | __name = name 83 | __recognized_extensions = recognized_extensions 84 | __options.append_array(options) 85 | __settings.append_array(settings) 86 | __image_format_loader_extension = image_format_loader_extension 87 | 88 | func get_recognized_extensions() -> PackedStringArray: 89 | return __recognized_extensions 90 | 91 | func get_options() -> Array[Dictionary]: 92 | return __options 93 | 94 | func get_name() -> String: 95 | return __name 96 | 97 | func get_settings() -> Array[_Setting]: 98 | return __settings 99 | 100 | func get_image_format_loader_extension() -> ImageFormatLoaderExtension: 101 | return __image_format_loader_extension 102 | 103 | func export( 104 | res_source_file_path: String, 105 | options: Dictionary, 106 | editor_import_plugin: EditorImportPlugin 107 | ) -> ExportResult: 108 | return _export( 109 | res_source_file_path, 110 | options) 111 | 112 | func _export(source_file: String, options: Dictionary) -> ExportResult: 113 | assert(false, "This method is abstract and must be overriden.") 114 | var result: ExportResult = ExportResult.new() 115 | result.fail(ERR_UNCONFIGURED) 116 | return result 117 | 118 | enum AnimationOptions { 119 | FramesCount = 1, 120 | Direction = 2, 121 | RepeatCount = 4, 122 | } 123 | 124 | static var __option_regex: RegEx = RegEx.create_from_string("\\s-\\p{L}:\\s*\\S+") 125 | static var __natural_number_regex: RegEx = RegEx.create_from_string("\\A\\d+\\z") 126 | 127 | class AnimationParamsParsingResult: 128 | extends _Result 129 | var name: String 130 | var first_frame_index: int 131 | var frames_count: int 132 | var direction: _Common.AnimationDirection 133 | var repeat_count: int 134 | func _get_result_type_description() -> String: 135 | return "Animation parameters parsing" 136 | 137 | static func _parse_animation_params( 138 | raw_animation_params: String, 139 | animation_options: AnimationOptions, 140 | first_frame_index: int, 141 | frames_count: int = 0 142 | ) -> AnimationParamsParsingResult: 143 | var result = AnimationParamsParsingResult.new() 144 | if first_frame_index < 0: 145 | result.fail(ERR_INVALID_DATA, "Wrong value for animation first frame index. Expected natural number, got: %s" % [first_frame_index]) 146 | return result 147 | result.first_frame_index = first_frame_index 148 | result.frames_count = frames_count 149 | result.direction = -1 150 | result.repeat_count = -1 151 | raw_animation_params = raw_animation_params.strip_edges() 152 | var options_matches: Array[RegExMatch] = __option_regex.search_all(raw_animation_params) 153 | var first_match_position: int = raw_animation_params.length() 154 | for option_match in options_matches: 155 | var match_position: int = option_match.get_start() 156 | assert(match_position >= 0) 157 | if match_position < first_match_position: 158 | first_match_position = match_position 159 | var raw_option: String = option_match.get_string().strip_edges() 160 | var raw_value = raw_option.substr(3).strip_edges() 161 | match raw_option.substr(0, 3): 162 | "-f:": 163 | if animation_options & AnimationOptions.FramesCount: 164 | if result.frames_count == 0: 165 | if __natural_number_regex.search(raw_value): 166 | result.frames_count = raw_value.to_int() 167 | if result.frames_count <= 0: 168 | result.fail(ERR_INVALID_DATA, "Wrong value format for frames count. Expected positive integer number, got: \"%s\"" % [raw_value]) 169 | return result 170 | "-d:": 171 | if animation_options & AnimationOptions.Direction: 172 | if result.direction < 0: 173 | match raw_value: 174 | "f": result.direction = _Common.AnimationDirection.FORWARD 175 | "r": result.direction = _Common.AnimationDirection.REVERSE 176 | "pp": result.direction = _Common.AnimationDirection.PING_PONG 177 | "ppr": result.direction = _Common.AnimationDirection.PING_PONG_REVERSE 178 | _: 179 | result.fail(ERR_INVALID_DATA, "Wrong value format for animation direction. Expected one of: [\"f\", \"r\", \"pp\", \"ppr\"], got: \"%s\"" % [raw_value]) 180 | return result 181 | "-r:": 182 | if animation_options & AnimationOptions.RepeatCount: 183 | if result.repeat_count < 0: 184 | if __natural_number_regex.search(raw_value): 185 | result.repeat_count = raw_value.to_int() 186 | else: 187 | result.fail(ERR_INVALID_DATA, "Wrong value format for repeat count. Expected positive integer number or zero, got: \"%s\"" % [raw_value]) 188 | return result 189 | _: pass # Ignore unknown parameter 190 | result.name = raw_animation_params.left(first_match_position).strip_edges() 191 | if result.frames_count <= 0: 192 | result.fail(ERR_UNCONFIGURED, "Animation frames count is required but not specified") 193 | return result 194 | return result 195 | 196 | static func _create_sprite_sheet_builder(options: Dictionary) -> _SpriteSheetBuilderBase: 197 | var sprite_sheet_layout: _Common.SpriteSheetLayout = options[_Options.SPRITE_SHEET_LAYOUT] 198 | return \ 199 | _PackedSpriteSheetBuilder.new( 200 | options[_Options.EDGES_ARTIFACTS_AVOIDANCE_METHOD], 201 | options[_Options.SPRITES_SURROUNDING_COLOR]) \ 202 | if sprite_sheet_layout == _Common.SpriteSheetLayout.PACKED else \ 203 | _GridBasedSpriteSheetBuilder.new( 204 | options[_Options.EDGES_ARTIFACTS_AVOIDANCE_METHOD], 205 | _GridBasedSpriteSheetBuilder.StripDirection.HORIZONTAL 206 | if sprite_sheet_layout == _Common.SpriteSheetLayout.HORIZONTAL_STRIPS else 207 | _GridBasedSpriteSheetBuilder.StripDirection.HORIZONTAL, 208 | options[_Options.MAX_CELLS_IN_STRIP], 209 | options[_Options.TRIM_SPRITES_TO_OVERALL_MIN_SIZE], 210 | options[_Options.COLLAPSE_TRANSPARENT_SPRITES], 211 | options[_Options.MERGE_DUPLICATED_SPRITES], 212 | options[_Options.SPRITES_SURROUNDING_COLOR]) 213 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/export/_pxo_v2.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | 3 | const _Common = preload("../common.gd") 4 | const _Export = preload("_.gd") 5 | const _Options = preload("../options.gd") 6 | 7 | enum PxoLayerType { 8 | PIXEL_LAYER = 0, 9 | GROUP_LAYER = 1, 10 | LAYER_3D = 2, 11 | } 12 | 13 | static func export(res_source_file_path: String, options: Dictionary) -> _Export.ExportResult: 14 | var result := _Export.ExportResult.new() 15 | 16 | var file: FileAccess = FileAccess.open_compressed(res_source_file_path, FileAccess.READ, FileAccess.COMPRESSION_ZSTD) 17 | if file == null or file.get_open_error() == ERR_FILE_UNRECOGNIZED: 18 | file = FileAccess.open(res_source_file_path, FileAccess.READ) 19 | if file == null: 20 | result.fail(ERR_FILE_CANT_OPEN, "Failed to open file with unknown error") 21 | return result 22 | var open_error: Error = file.get_open_error() 23 | if open_error: 24 | result.fail(ERR_FILE_CANT_OPEN, "Failed to open file with error: %s \"%s\"" % [open_error, error_string(open_error)]) 25 | return result 26 | 27 | var first_line: String = file.get_line() 28 | var images_data: PackedByteArray = file.get_buffer(file.get_length() - file.get_position()) 29 | file.close() 30 | 31 | var pxo_project: Dictionary = JSON.parse_string(first_line) 32 | var image_size: Vector2i = Vector2i(pxo_project.size_x, pxo_project.size_y) 33 | var pxo_cel_image_buffer_size: int = image_size.x * image_size.y * 4 34 | var pxo_cel_image_buffer_offset: int 35 | var pxo_cel_image: Image = Image.create(image_size.x, image_size.y, false, Image.FORMAT_RGBA8) 36 | var pixel_layers_count: int 37 | for pxo_layer in pxo_project.layers: 38 | if pxo_layer.type == PxoLayerType.PIXEL_LAYER: 39 | pixel_layers_count += 1 40 | 41 | var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges() 42 | var unique_frames_indices_by_frame_index: Dictionary 43 | var unique_frames: Array[_Common.FrameInfo] 44 | var unique_frames_images: Array[Image] 45 | var unique_frames_count: int 46 | var pixel_layer_index: int 47 | var image_rect: Rect2i = Rect2i(Vector2i.ZERO, image_size) 48 | var frame: _Common.FrameInfo 49 | 50 | var is_animation_default: bool = pxo_project.tags.is_empty() 51 | if is_animation_default: 52 | var default_animation_name: String = options[_Options.DEFAULT_ANIMATION_NAME].strip_edges() 53 | if default_animation_name.is_empty(): 54 | default_animation_name = "default" 55 | pxo_project.tags.push_back({ 56 | name = default_animation_name, 57 | from = 1, 58 | to = pxo_project.frames.size()}) 59 | var animations_count: int = pxo_project.tags.size() 60 | 61 | var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new() 62 | animation_library.animations.resize(animations_count) 63 | var pxo_cel_opacity: float 64 | var unique_animations_names: PackedStringArray 65 | for animation_index in animations_count: 66 | var pxo_tag: Dictionary = pxo_project.tags[animation_index] 67 | var animation: _Common.AnimationInfo = _Common.AnimationInfo.new() 68 | animation_library.animations[animation_index] = animation 69 | var animation_frames_count: int = pxo_tag.to + 1 - pxo_tag.from 70 | if is_animation_default: 71 | animation.name = pxo_tag.name 72 | if animation.name == autoplay_animation_name: 73 | animation_library.autoplay_index = animation_index 74 | animation.direction = options[_Options.DEFAULT_ANIMATION_DIRECTION] 75 | animation.repeat_count = options[_Options.DEFAULT_ANIMATION_REPEAT_COUNT] 76 | else: 77 | var animation_params_parsing_result := _Export._parse_animation_params( 78 | pxo_tag.name, _Export.AnimationOptions.Direction | _Export.AnimationOptions.RepeatCount, 79 | pxo_tag.from, animation_frames_count) 80 | if animation_params_parsing_result.error: 81 | result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters", 82 | animation_params_parsing_result) 83 | return result 84 | if unique_animations_names.has(animation_params_parsing_result.name): 85 | result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" % 86 | [animation_params_parsing_result.name, animation_index]) 87 | return result 88 | unique_animations_names.push_back(animation_params_parsing_result.name) 89 | animation.name = animation_params_parsing_result.name 90 | if animation.name == autoplay_animation_name: 91 | animation_library.autoplay_index = animation_index 92 | animation.direction = animation_params_parsing_result.direction 93 | if animation.direction < 0: 94 | animation.direction = _Common.AnimationDirection.FORWARD 95 | animation.repeat_count = animation_params_parsing_result.repeat_count 96 | if animation.repeat_count < 0: 97 | animation.repeat_count = 1 98 | 99 | animation.frames.resize(animation_frames_count) 100 | 101 | var frame_image: Image 102 | for animation_frame_index in animation_frames_count: 103 | var frame_index: int = pxo_tag.from - 1 + animation_frame_index 104 | var unique_frame_index: int = unique_frames_indices_by_frame_index.get(frame_index, -1) 105 | if unique_frame_index >= 0: 106 | frame = unique_frames[unique_frame_index] 107 | else: 108 | frame = _Common.FrameInfo.new() 109 | unique_frames.push_back(frame) 110 | frame_image = Image.create(image_size.x, image_size.y, false, Image.FORMAT_RGBA8) 111 | unique_frames_images.push_back(frame_image) 112 | unique_frame_index = unique_frames_count 113 | unique_frames_count += 1 114 | pixel_layer_index = -1 115 | var pxo_frame: Dictionary = pxo_project.frames[frame_index] 116 | frame.duration = pxo_frame.duration / pxo_project.fps 117 | for cel_index in pxo_frame.cels.size(): 118 | var pxo_cel = pxo_frame.cels[cel_index] 119 | var pxo_layer = pxo_project.layers[cel_index] 120 | if pxo_layer.type == PxoLayerType.PIXEL_LAYER: 121 | pixel_layer_index += 1 122 | var l: Dictionary = pxo_layer 123 | while l.parent >= 0 and pxo_layer.visible: 124 | if not l.visible: 125 | pxo_layer.visible = false 126 | break 127 | l = pxo_project.layers[l.parent] 128 | pxo_cel_opacity = pxo_cel.opacity 129 | if not pxo_layer.visible or pxo_cel_opacity == 0: 130 | continue 131 | pxo_cel_image_buffer_offset = pxo_cel_image_buffer_size * \ 132 | (pixel_layers_count * frame_index + pixel_layer_index) 133 | var pxo_cel_image_buffer: PackedByteArray = images_data.slice( 134 | pxo_cel_image_buffer_offset, 135 | pxo_cel_image_buffer_offset + pxo_cel_image_buffer_size) 136 | for alpha_index in range(3, pxo_cel_image_buffer_size, 4): 137 | pxo_cel_image_buffer[alpha_index] = roundi(pxo_cel_image_buffer[alpha_index] * pxo_cel_opacity) 138 | pxo_cel_image.set_data(image_size.x, image_size.y, false, Image.FORMAT_RGBA8, pxo_cel_image_buffer) 139 | frame_image.blend_rect(pxo_cel_image, image_rect, Vector2i.ZERO) 140 | unique_frames_indices_by_frame_index[frame_index] = unique_frame_index 141 | animation.frames[animation_frame_index] = frame 142 | if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0: 143 | push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name]) 144 | 145 | var sprite_sheet_builder := _Export._create_sprite_sheet_builder(options) 146 | 147 | var sprite_sheet_building_result := sprite_sheet_builder.build_sprite_sheet(unique_frames_images) 148 | if sprite_sheet_building_result.error: 149 | result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result) 150 | return result 151 | var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet 152 | 153 | for unique_frame_index in unique_frames_count: 154 | var unique_frame: _Common.FrameInfo = unique_frames[unique_frame_index] 155 | unique_frame.sprite = sprite_sheet.sprites[unique_frame_index] 156 | 157 | result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library) 158 | return result 159 | 160 | static func load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error: 161 | var file: FileAccess = FileAccess.open_compressed(file_access.get_path(), FileAccess.READ, FileAccess.COMPRESSION_ZSTD) 162 | if file == null or file.get_open_error() == ERR_FILE_UNRECOGNIZED: 163 | file = FileAccess.open(file_access.get_path(), FileAccess.READ) 164 | if file == null: 165 | push_error("Failed to open file with unknown error") 166 | return ERR_FILE_CANT_OPEN 167 | var open_error: Error = file.get_open_error() 168 | if open_error: 169 | push_error("Failed to open file with error: %s \"%s\"" % [open_error, error_string(open_error)]) 170 | return ERR_FILE_CANT_OPEN 171 | 172 | var first_line: String = file.get_line() 173 | 174 | var pxo_project: Dictionary = JSON.parse_string(first_line) 175 | var image_size: Vector2i = Vector2i(pxo_project.size_x, pxo_project.size_y) 176 | var pxo_cel_image_buffer_size: int = image_size.x * image_size.y * 4 177 | var pxo_cel_image_buffer_offset: int 178 | var pxo_cel_image: Image = Image.create(image_size.x, image_size.y, false, Image.FORMAT_RGBA8) 179 | var pixel_layer_index: int = -1 180 | image.set_data(1, 1, false, Image.FORMAT_RGBA8, [0, 0, 0, 0]) 181 | image.resize(image_size.x, image_size.y) 182 | var image_rect: Rect2i = Rect2i(Vector2i.ZERO, image_size) 183 | for layer_index in pxo_project.layers.size(): 184 | var pxo_layer: Dictionary = pxo_project.layers[layer_index] 185 | if pxo_layer.type != PxoLayerType.PIXEL_LAYER: 186 | continue 187 | pixel_layer_index += 1 188 | var l: Dictionary = pxo_layer 189 | while l.parent >= 0 and pxo_layer.visible: 190 | if not l.visible: 191 | pxo_layer.visible = false 192 | break 193 | l = pxo_project.layers[l.parent] 194 | if not pxo_layer.visible: 195 | continue 196 | var pxo_cel: Dictionary = pxo_project.frames[0].cels[layer_index] 197 | var pxo_cel_opacity = pxo_cel.opacity 198 | if pxo_cel_opacity == 0: 199 | continue 200 | pxo_cel_image_buffer_offset = pxo_cel_image_buffer_size * layer_index 201 | var pxo_cel_image_buffer: PackedByteArray = file.get_buffer(pxo_cel_image_buffer_size) 202 | for alpha_index in range(3, pxo_cel_image_buffer_size, 4): 203 | pxo_cel_image_buffer[alpha_index] = roundi(pxo_cel_image_buffer[alpha_index] * pxo_cel_opacity) 204 | pxo_cel_image.set_data(image_size.x, image_size.y, false, Image.FORMAT_RGBA8, pxo_cel_image_buffer) 205 | image.blend_rect(pxo_cel_image, image_rect, Vector2i.ZERO) 206 | file.close() 207 | return OK 208 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/export/_pxo_v3.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends RefCounted 3 | ## Represents a Pixelorama project stored in a pxo v3 file. 4 | 5 | enum _PxoLayerType { 6 | PIXEL_LAYER = 0, 7 | GROUP_LAYER = 1, 8 | LAYER_3D = 2, 9 | } 10 | 11 | const _Common = preload("../common.gd") 12 | const _Export = preload("_.gd") 13 | const _Result = preload("../result.gd").Class 14 | const _Options = preload("../options.gd") 15 | 16 | var _animation_library: _Common.AnimationLibraryInfo 17 | var _atlas_image: Image 18 | var _data: Dictionary 19 | var _empty_image: Image 20 | var _error: _Export.ExportResult 21 | var _has_composited_frames: bool 22 | var _frame_images: Array[Image] 23 | var _frame_rect: Rect2i 24 | var _options: Dictionary 25 | var _path: String 26 | var _sprite_sheet: _Common.SpriteSheetInfo 27 | var _zip_reader: ZIPReader 28 | 29 | func _init(path: String) -> void: 30 | self._path = path 31 | 32 | ## Export the project. 33 | func export(options: Dictionary) -> _Export.ExportResult: 34 | self._options = options 35 | self._load() 36 | 37 | if not self._error: 38 | self._load_animation_images() 39 | 40 | if not self._error: 41 | self._build_sprite_sheet() 42 | 43 | if not self._error: 44 | self._create_animations() 45 | 46 | if self._error: 47 | return self._error 48 | 49 | var result := _Export.ExportResult.new() 50 | 51 | result.success(self._atlas_image, self._sprite_sheet, self._animation_library) 52 | 53 | return result 54 | 55 | ## Replace an image's data with the first frame in the project. 56 | func set_image_data(image: Image) -> Error: 57 | self._load() 58 | 59 | if self._error: 60 | return self._error.error 61 | 62 | self._load_frame_image(0) 63 | 64 | image.set_data( 65 | self._frame_images[0].get_width(), 66 | self._frame_images[0].get_height(), 67 | false, 68 | Image.FORMAT_RGBA8, 69 | self._frame_images[0].get_data(), 70 | ) 71 | 72 | return OK 73 | 74 | ## Build a sprite sheet containing all loaded frame images. 75 | func _build_sprite_sheet() -> void: 76 | if not self._options: 77 | self._error = _Export.ExportResult.new() 78 | self._error.fail(ERR_BUG, "Building a sprite sheet requires export options.") 79 | 80 | return 81 | 82 | var sprite_sheet_builder := _Export._create_sprite_sheet_builder(self._options) 83 | var sprite_sheet_result := sprite_sheet_builder.build_sprite_sheet(self._frame_images) 84 | 85 | if sprite_sheet_result.error: 86 | self._error = _Export.ExportResult.new() 87 | self._error.fail(ERR_BUG, "Failed to build sprite sheet", sprite_sheet_result) 88 | 89 | return 90 | 91 | self._atlas_image = sprite_sheet_result.atlas_image 92 | self._sprite_sheet = sprite_sheet_result.sprite_sheet 93 | 94 | ## Compare two frame cels by z-index. 95 | ## 96 | ## [br] 97 | ## If both cels have the same z-index, they are sorted by their layer index 98 | ## instead (i.e. their order is preserved). 99 | static func _compare_cels(a: Dictionary, b: Dictionary) -> bool: 100 | if a.z_index > b.z_index: 101 | return true 102 | 103 | if a.z_index < b.z_index: 104 | return false 105 | 106 | if a.layer_index > b.layer_index: 107 | return true 108 | 109 | assert(a.layer_index != b.layer_index, "Can't happen; layer indices should be unique.") 110 | 111 | return false 112 | 113 | ## Create a single animation. 114 | ## 115 | ## [br] 116 | ## [param start] The zero-based index of the first frame to include in the 117 | ## animation. If creating an animation from a tag, this should be 118 | ## [code]tag.from - 1[/code]. 119 | ## [br] 120 | ## [param end] The zero-based index of the first frame to [b]not[/b] include 121 | ## in the animation. If creating an animation from a tag, this should be 122 | ## [code]tag.to[/code]. 123 | func _create_animation( 124 | name: String, 125 | direction: _Common.AnimationDirection, 126 | repeat_count: int, 127 | start: int, 128 | end: int, 129 | ) -> _Common.AnimationInfo: 130 | var animation := _Common.AnimationInfo.new() 131 | 132 | animation.name = name 133 | animation.direction = direction 134 | animation.repeat_count = repeat_count 135 | animation.frames.resize(end - start) 136 | 137 | for animation_frame_index in end - start: 138 | var project_frame_index := start + animation_frame_index 139 | var frame := _Common.FrameInfo.new() 140 | 141 | frame.duration = self._data.frames[project_frame_index].duration / self._data.fps 142 | frame.sprite = self._sprite_sheet.sprites[project_frame_index] 143 | 144 | animation.frames[animation_frame_index] = frame 145 | 146 | return animation 147 | 148 | ## Create an animation for each tag in the project. 149 | ## 150 | ## If the project doesn't contain any tags, a single animation will be created 151 | ## which contains every frame in the project. 152 | func _create_animations() -> void: 153 | if self._animation_library: 154 | return 155 | 156 | if not self._options: 157 | self._error = _Export.ExportResult.new() 158 | self._error.fail(ERR_BUG, "Creating animations requires export options.") 159 | 160 | return 161 | 162 | self._animation_library = _Common.AnimationLibraryInfo.new() 163 | 164 | var autoplay_animation_name := self._get_string_option(_Options.AUTOPLAY_ANIMATION_NAME) 165 | 166 | # If no tags are defined, create a single animation with the default name and 167 | # containing all frames. 168 | if self._data.tags.is_empty(): 169 | var default_animation_name := self._get_string_option(_Options.DEFAULT_ANIMATION_NAME) 170 | var name := "default" if default_animation_name.is_empty() else default_animation_name 171 | 172 | self._animation_library.animations.resize(1) 173 | 174 | self._animation_library.animations[0] = self._create_animation( 175 | name, 176 | self._options[_Options.DEFAULT_ANIMATION_DIRECTION] as _Common.AnimationDirection, 177 | self._options[_Options.DEFAULT_ANIMATION_REPEAT_COUNT] as int, 178 | 0, 179 | (self._data.frames as Array[Dictionary]).size() 180 | ) 181 | 182 | if name == autoplay_animation_name: 183 | self._animation_library.autoplay_index = 0 184 | 185 | return 186 | 187 | self._animation_library.animations.resize(self._data.tags.size()) 188 | 189 | for tag_index in self._data.tags.size(): 190 | var tag := self._data.tags[tag_index] as Dictionary 191 | 192 | var animation_params_result := _Export._parse_animation_params( 193 | tag.name, 194 | _Export.AnimationOptions.Direction | _Export.AnimationOptions.RepeatCount, 195 | tag.from - 1, 196 | tag.to - tag.from + 1, 197 | ) 198 | 199 | self._animation_library.animations[tag_index] = self._create_animation( 200 | animation_params_result.name, 201 | animation_params_result.direction, 202 | animation_params_result.repeat_count, 203 | animation_params_result.first_frame_index, 204 | animation_params_result.first_frame_index + animation_params_result.frames_count, 205 | ) 206 | 207 | if animation_params_result.name == autoplay_animation_name: 208 | self._animation_library.autoplay_index = tag_index 209 | 210 | ## Get the specified option as a string. 211 | ## 212 | ## [br] 213 | ## This both ensures that the value is, in fact, a string and trims its ends 214 | ## of whitespace. 215 | func _get_string_option(name: String) -> String: 216 | return (self._options[name] as String).strip_edges() 217 | 218 | ## Load the project from its pxo file and initialize it. 219 | func _load() -> void: 220 | if self._zip_reader: 221 | return 222 | 223 | self._zip_reader = ZIPReader.new() 224 | 225 | var open_error := self._zip_reader.open(self._path) 226 | 227 | if open_error != OK: 228 | self._error = _Export.ExportResult.new() 229 | self._error.fail(open_error, "Could not open pxo file") 230 | 231 | return 232 | 233 | var raw_data := self._zip_reader.read_file("data.json") 234 | 235 | if raw_data.is_empty(): 236 | self._error = _Export.ExportResult.new() 237 | self._error.fail(ERR_DOES_NOT_EXIST, "Invalid Pixelorama project: data.json missing or empty") 238 | 239 | return 240 | 241 | var json := JSON.new() 242 | var json_error := json.parse(raw_data.get_string_from_utf8()) 243 | 244 | if json_error != OK: 245 | self._error = _Export.ExportResult.new() 246 | self._error.fail( 247 | ERR_PARSE_ERROR, 248 | "Invalid Pixelorama project: could not parse data.json (%s on line %d)" % [ 249 | json.get_error_message(), 250 | json.get_error_line(), 251 | ], 252 | ) 253 | 254 | return 255 | 256 | if typeof(json.data) != TYPE_DICTIONARY: 257 | self._error = _Export.ExportResult.new() 258 | self._error.fail(ERR_INVALID_DATA, "Invalid Pixelorama project: data.json is not a dictionary") 259 | 260 | return 261 | 262 | if json.data.pxo_version != 3: 263 | push_warning( 264 | "This project uses version " + str(json.data.pxo_version) + 265 | " of the pxo file format, which is not currently supported by Importality." 266 | ) 267 | 268 | self._data = json.data 269 | self._empty_image = Image.create_empty(1, 1, false, Image.FORMAT_RGBA8) 270 | self._frame_images = [] 271 | self._frame_images.resize(self._data.frames.size()) 272 | self._frame_images.fill(self._empty_image) 273 | self._frame_rect = Rect2i(Vector2i.ZERO, Vector2i(self._data.size_x, self._data.size_y)) 274 | self._has_composited_frames = self._zip_reader.file_exists("image_data/final_images/1") 275 | 276 | if not self._has_composited_frames: 277 | push_warning( 278 | "The Pixelorama project '%s' does not contain blended/precomposited frame images." %[self._path] + 279 | " Not all Pixelorama compositing features are supported by Importality, so frames may not" + 280 | " look the way you expect unless you enable \"Include blended images\" in Pixelorama." 281 | ) 282 | 283 | ## Preload the frame images for all animations. 284 | ## 285 | ## [br] 286 | ## This allows the sprite sheet to be built before the animations, which can 287 | ## then be created in a single step. 288 | func _load_animation_images() -> void: 289 | if self._data.tags.is_empty(): 290 | for frame_index in self._data.frames.size(): 291 | self._load_frame_image(frame_index) 292 | else: 293 | for tag in self._data.tags: 294 | for frame_index in range(tag.from - 1, tag.to): 295 | self._load_frame_image(frame_index) 296 | 297 | ## Ensure a frame's image has been loaded. 298 | ## 299 | ## [br] 300 | ## If the image for the frame is not already loaded, it will either load the 301 | ## frame's precomposited image (a.k.a. blended image) if availble or attempt 302 | ## to composite the frame layers into an image itself otherwise. 303 | func _load_frame_image(frame_index: int) -> void: 304 | if self._frame_images[frame_index] != self._empty_image: 305 | return 306 | 307 | if self._has_composited_frames: 308 | # Having precomposited frames available greatly simplifies things. 309 | 310 | self._frame_images[frame_index] = Image.create_from_data( 311 | self._data.size_x, 312 | self._data.size_y, 313 | false, 314 | Image.FORMAT_RGBA8, 315 | self._zip_reader.read_file("image_data/final_images/%d" % [frame_index + 1]), 316 | ) 317 | 318 | return 319 | 320 | # Blended images aren't available, so the layers need composited. 321 | 322 | var frame: Dictionary = self._data.frames[frame_index] 323 | var cels := (frame.cels as Array[Dictionary]).duplicate(true) 324 | 325 | for index in range(cels.size()): 326 | cels[index].layer_index = index 327 | 328 | cels.sort_custom(_compare_cels) 329 | 330 | var frame_image := Image.create_empty(self._data.size_x, self._data.size_y, false, Image.FORMAT_RGBA8) 331 | var cel_image := Image.create_empty(self._data.size_x, self._data.size_y, false, Image.FORMAT_RGBA8) 332 | 333 | for cel in cels: 334 | var layer: Dictionary = self._data.layers[cel.layer_index] 335 | 336 | if layer.type != _PxoLayerType.PIXEL_LAYER: 337 | continue 338 | 339 | var opacity: float = cel.opacity 340 | 341 | while opacity > 0.0: 342 | if layer.visible: 343 | opacity *= layer.opacity 344 | 345 | if layer.parent < 0: 346 | break 347 | 348 | layer = self._data.layers[layer.parent] 349 | else: 350 | opacity = 0.0 351 | 352 | if is_equal_approx(opacity, 0.0): 353 | continue 354 | 355 | var cel_data := self._zip_reader.read_file("image_data/frames/%d/layer_%d" % [frame_index + 1, cel.layer_index + 1]) 356 | 357 | if not is_equal_approx(opacity, 1.0): 358 | # Apply the cel opacity by scaling all the alpha-channel bytes. 359 | 360 | for alpha_index in range(3, cel_data.size(), 4): 361 | cel_data[alpha_index] *= opacity 362 | 363 | cel_image.set_data(self._data.size_x, self._data.size_y, false, Image.FORMAT_RGBA8, cel_data) 364 | frame_image.blend_rect(cel_image, self._frame_rect, Vector2i.ZERO) 365 | 366 | self._frame_images[frame_index] = frame_image 367 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/export/aseprite.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_.gd" 3 | 4 | const __aseprite_sheet_types_by_sprite_sheet_layout: PackedStringArray = \ 5 | [ "packed", "rows", "columns" ] 6 | const __aseprite_animation_directions: PackedStringArray = \ 7 | [ "forward", "reverse", "pingpong", "pingpong_reverse" ] 8 | 9 | var __os_command_setting: _Setting = _Setting.new( 10 | "aseprite_or_libre_sprite_command", "", TYPE_STRING, PROPERTY_HINT_NONE, 11 | "", true, func(v: String): return v.is_empty()) 12 | 13 | var __os_command_arguments_setting: _Setting = _Setting.new( 14 | "aseprite_or_libre_sprite_command_arguments", PackedStringArray(), TYPE_PACKED_STRING_ARRAY, PROPERTY_HINT_NONE, 15 | "", true, func(v: PackedStringArray): return false) 16 | 17 | func _init() -> void: 18 | var recognized_extensions: PackedStringArray = ["ase", "aseprite"] 19 | super("Aseprite", recognized_extensions, [], 20 | [__os_command_setting, __os_command_arguments_setting], 21 | CustomImageFormatLoaderExtension.new( 22 | recognized_extensions, 23 | __os_command_setting, 24 | __os_command_arguments_setting, 25 | _Common.common_temporary_files_directory_path_setting)) 26 | 27 | func _export(res_source_file_path: String, options: Dictionary) -> ExportResult: 28 | var result: ExportResult = ExportResult.new() 29 | var err: Error 30 | 31 | var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value() 32 | if os_command_result.error: 33 | result.fail(ERR_UNCONFIGURED, "Failed to get Aseprite Command to export spritesheet", os_command_result) 34 | return result 35 | 36 | var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value() 37 | if os_command_arguments_result.error: 38 | result.fail(ERR_UNCONFIGURED, "Failed to get Aseprite Command Arguments to export spritesheet", os_command_arguments_result) 39 | return result 40 | 41 | var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value() 42 | if temp_dir_path_result.error: 43 | result.fail(ERR_UNCONFIGURED, "Failed to get Temporary Files Directory Path to export spritesheet", temp_dir_path_result) 44 | return result 45 | var global_temp_dir_path: String = ProjectSettings.globalize_path( 46 | temp_dir_path_result.value.strip_edges()) 47 | var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \ 48 | _DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path) 49 | if unique_temp_dir_creation_result.error: 50 | result.fail(ERR_QUERY_FAILED, "Failed to create unique temporary directory to export spritesheet", unique_temp_dir_creation_result) 51 | return result 52 | var unique_temp_dir_path: String = unique_temp_dir_creation_result.path 53 | 54 | var global_source_file_path: String = ProjectSettings.globalize_path(res_source_file_path) 55 | 56 | var global_png_path: String = unique_temp_dir_path.path_join("temp.png") 57 | var global_json_path: String = unique_temp_dir_path.path_join("temp.json") 58 | 59 | var command: String = os_command_result.value.strip_edges() 60 | var arguments: PackedStringArray = \ 61 | os_command_arguments_result.value + \ 62 | PackedStringArray([ 63 | "--batch", 64 | "--format", "json-array", 65 | "--list-tags", 66 | "--sheet", global_png_path, 67 | "--data", global_json_path, 68 | global_source_file_path]) 69 | 70 | var output: Array = [] 71 | var exit_code: int = OS.execute(command, arguments, output, true, false) 72 | if exit_code: 73 | for arg_index in arguments.size(): 74 | arguments[arg_index] = "\nArgument: " + arguments[arg_index] 75 | result.fail(ERR_QUERY_FAILED, " ".join([ 76 | "An error occurred while executing the Aseprite command.", 77 | "Process exited with code %s:\nCommand: %s%s" 78 | ]) % [exit_code, command, "".join(arguments)]) 79 | return result 80 | var raw_atlas_image: Image = Image.load_from_file(global_png_path) 81 | var json = JSON.new() 82 | err = json.parse(FileAccess.get_file_as_string(global_json_path)) 83 | if err: 84 | result.fail(ERR_INVALID_DATA, "Failed to parse sprite sheet json data with error %s \"%s\"" % [err, error_string(err)]) 85 | return result 86 | var raw_sprite_sheet_data: Dictionary = json.data 87 | 88 | var sprite_sheet_layout: _Common.SpriteSheetLayout = options[_Options.SPRITE_SHEET_LAYOUT] 89 | var source_image_size: Vector2i = _Common.get_vector2i( 90 | raw_sprite_sheet_data.frames[0].sourceSize, "w", "h") 91 | 92 | var frames_images_by_indices: Dictionary 93 | var tags_data: Array = raw_sprite_sheet_data.meta.frameTags 94 | var frames_data: Array = raw_sprite_sheet_data.frames 95 | var frames_count: int = frames_data.size() 96 | if tags_data.is_empty(): 97 | var default_animation_name: String = options[_Options.DEFAULT_ANIMATION_NAME].strip_edges() 98 | if default_animation_name.is_empty(): 99 | default_animation_name = "default" 100 | tags_data.push_back({ 101 | name = default_animation_name, 102 | from = 0, 103 | to = frames_count - 1, 104 | direction = __aseprite_animation_directions[options[_Options.DEFAULT_ANIMATION_DIRECTION]], 105 | repeat = options[_Options.DEFAULT_ANIMATION_REPEAT_COUNT] 106 | }) 107 | var animations_count: int = tags_data.size() 108 | for tag_data in tags_data: 109 | for frame_index in range(tag_data.from, tag_data.to + 1): 110 | if frames_images_by_indices.has(frame_index): 111 | continue 112 | var frame_data: Dictionary = frames_data[frame_index] 113 | frames_images_by_indices[frame_index] = raw_atlas_image.get_region(Rect2i( 114 | _Common.get_vector2i(frame_data.frame, "x", "y"), 115 | source_image_size)) 116 | var used_frames_indices: PackedInt32Array = PackedInt32Array(frames_images_by_indices.keys()) 117 | used_frames_indices.sort() 118 | var used_frames_count: int = used_frames_indices.size() 119 | var sprite_sheet_frames_indices_by_global_frame_indices: Dictionary 120 | for sprite_sheet_frame_index in used_frames_indices.size(): 121 | sprite_sheet_frames_indices_by_global_frame_indices[ 122 | used_frames_indices[sprite_sheet_frame_index]] = \ 123 | sprite_sheet_frame_index 124 | var used_frames_images: Array[Image] 125 | used_frames_images.resize(used_frames_count) 126 | for i in used_frames_count: 127 | used_frames_images[i] = frames_images_by_indices[used_frames_indices[i]] 128 | 129 | var sprite_sheet_builder: _SpriteSheetBuilderBase = _create_sprite_sheet_builder(options) 130 | 131 | var sprite_sheet_building_result: _SpriteSheetBuilderBase.SpriteSheetBuildingResult = sprite_sheet_builder.build_sprite_sheet(used_frames_images) 132 | if sprite_sheet_building_result.error: 133 | result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result) 134 | return result 135 | var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet 136 | 137 | var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new() 138 | var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges() 139 | 140 | var all_frames: Array[_Common.FrameInfo] 141 | all_frames.resize(used_frames_count) 142 | var unique_animations_names: PackedStringArray 143 | for animation_index in animations_count: 144 | var tag_data: Dictionary = tags_data[animation_index] 145 | 146 | var animation_params_parsing_result: AnimationParamsParsingResult = _parse_animation_params( 147 | tag_data.name.strip_edges(), 148 | AnimationOptions.Direction | AnimationOptions.RepeatCount, 149 | tag_data.from, 150 | tag_data.to - tag_data.from + 1) 151 | if animation_params_parsing_result.error: 152 | result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters", 153 | animation_params_parsing_result) 154 | return result 155 | if unique_animations_names.has(animation_params_parsing_result.name): 156 | result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" % 157 | [animation_params_parsing_result.name, animation_index]) 158 | return result 159 | unique_animations_names.push_back(animation_params_parsing_result.name) 160 | var animation = _Common.AnimationInfo.new() 161 | animation.name = animation_params_parsing_result.name 162 | if animation.name.is_empty(): 163 | result.fail(ERR_INVALID_DATA, "A tag with empty name found") 164 | return result 165 | if animation.name == autoplay_animation_name: 166 | animation_library.autoplay_index = animation_index 167 | animation.direction = __aseprite_animation_directions.find(tag_data.direction) 168 | if animation_params_parsing_result.direction >= 0: 169 | animation.direction = animation_params_parsing_result.direction 170 | animation.repeat_count = int(tag_data.get("repeat", "0")) 171 | if animation_params_parsing_result.repeat_count >= 0: 172 | animation.repeat_count = animation_params_parsing_result.repeat_count 173 | for global_frame_index in range(tag_data.from, tag_data.to + 1): 174 | var sprite_sheet_frame_index: int = \ 175 | sprite_sheet_frames_indices_by_global_frame_indices[global_frame_index] 176 | var frame: _Common.FrameInfo = all_frames[sprite_sheet_frame_index] 177 | if frame == null: 178 | frame = _Common.FrameInfo.new() 179 | frame.sprite = sprite_sheet.sprites[sprite_sheet_frame_index] 180 | frame.duration = frames_data[global_frame_index].duration * 0.001 181 | all_frames[sprite_sheet_frame_index] = frame 182 | animation.frames.push_back(frame) 183 | animation_library.animations.push_back(animation) 184 | 185 | if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0: 186 | push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name]) 187 | 188 | if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error: 189 | push_warning( 190 | "Failed to remove unique temporary directory: \"%s\"" % 191 | [unique_temp_dir_path]) 192 | 193 | result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library) 194 | return result 195 | 196 | class CustomImageFormatLoaderExtension: 197 | extends ImageFormatLoaderExtension 198 | 199 | var __recognized_extensions: PackedStringArray 200 | var __os_command_setting: _Setting 201 | var __os_command_arguments_setting: _Setting 202 | var __common_temporary_files_directory_path_setting: _Setting 203 | 204 | func _init(recognized_extensions: PackedStringArray, 205 | os_command_setting: _Setting, 206 | os_command_arguments_setting: _Setting, 207 | common_temporary_files_directory_path_setting: _Setting 208 | ) -> void: 209 | __recognized_extensions = recognized_extensions 210 | __os_command_setting = os_command_setting 211 | __os_command_arguments_setting = os_command_arguments_setting 212 | __common_temporary_files_directory_path_setting = \ 213 | common_temporary_files_directory_path_setting 214 | 215 | func _get_recognized_extensions() -> PackedStringArray: 216 | return __recognized_extensions 217 | 218 | func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error: 219 | var global_source_file_path: String = file_access.get_path_absolute() 220 | var err: Error 221 | 222 | var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value() 223 | if os_command_result.error: 224 | push_error(os_command_result.error_description) 225 | return os_command_result.error 226 | 227 | var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value() 228 | if os_command_arguments_result.error: 229 | push_error(os_command_arguments_result.error_description) 230 | return os_command_arguments_result.error 231 | 232 | var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value() 233 | if temp_dir_path_result.error: 234 | push_error("Failed to get Temporary Files Directory Path to export spritesheet") 235 | return temp_dir_path_result.error 236 | var global_temp_dir_path: String = ProjectSettings.globalize_path( 237 | temp_dir_path_result.value.strip_edges()) 238 | var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \ 239 | _DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path) 240 | if unique_temp_dir_creation_result.error: 241 | push_error("Failed to create unique temporary directory to export spritesheet") 242 | return unique_temp_dir_creation_result.error 243 | var unique_temp_dir_path: String = unique_temp_dir_creation_result.path 244 | 245 | var global_png_path: String = unique_temp_dir_path.path_join("temp.png") 246 | var global_json_path: String = unique_temp_dir_path.path_join("temp.json") 247 | 248 | var command: String = os_command_result.value.strip_edges() 249 | var arguments: PackedStringArray = \ 250 | os_command_arguments_result.value + \ 251 | PackedStringArray([ 252 | "--batch", 253 | "--format", "json-array", 254 | "--list-tags", 255 | "--sheet", global_png_path, 256 | "--data", global_json_path, 257 | global_source_file_path, 258 | ]) 259 | 260 | var output: Array = [] 261 | var exit_code: int = OS.execute(command, arguments, output, true, false) 262 | if exit_code: 263 | for arg_index in arguments.size(): 264 | arguments[arg_index] = "\nArgument: " + arguments[arg_index] 265 | push_error(" ".join([ 266 | "An error occurred while executing the Aseprite command.", 267 | "Process exited with code %s:\nCommand: %s%s" 268 | ]) % [exit_code, command, "".join(arguments)]) 269 | return ERR_QUERY_FAILED 270 | 271 | var raw_atlas_image: Image = Image.load_from_file(global_png_path) 272 | var json = JSON.new() 273 | err = json.parse(FileAccess.get_file_as_string(global_json_path)) 274 | if err: 275 | push_error("Failed to parse sprite sheet json data with error %s \"%s\"" % [err, error_string(err)]) 276 | return ERR_INVALID_DATA 277 | var raw_sprite_sheet_data: Dictionary = json.data 278 | 279 | var source_image_size: Vector2i = _Common.get_vector2i( 280 | raw_sprite_sheet_data.frames[0].sourceSize, "w", "h") 281 | 282 | if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error: 283 | push_warning( 284 | "Failed to remove unique temporary directory: \"%s\"" % 285 | [unique_temp_dir_path]) 286 | 287 | image.copy_from(raw_atlas_image.get_region(Rect2i(Vector2i.ZERO, source_image_size))) 288 | return OK 289 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/export/krita.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_.gd" 3 | 4 | const _XML = preload("../xml.gd") 5 | 6 | var __os_command_setting: _Setting = _Setting.new( 7 | "krita_command", "", TYPE_STRING, PROPERTY_HINT_NONE, 8 | "", true, func(v: String): return v.is_empty()) 9 | 10 | var __os_command_arguments_setting: _Setting = _Setting.new( 11 | "krita_command_arguments", PackedStringArray(), TYPE_PACKED_STRING_ARRAY, PROPERTY_HINT_NONE, 12 | "", true, func(v: PackedStringArray): return false) 13 | 14 | func _init() -> void: 15 | var recognized_extensions: PackedStringArray = ["kra", "krita"] 16 | super("Krita", recognized_extensions, [], [ 17 | __os_command_setting, 18 | __os_command_arguments_setting, 19 | ], CustomImageFormatLoaderExtension.new(recognized_extensions)) 20 | 21 | func __validate_image_name(image_name: String) -> _Result: 22 | var result: _Result = _Result.new() 23 | var image_name_with_underscored_invalid_characters: String = image_name.validate_filename() 24 | var unsupported_characters: PackedStringArray 25 | for character_index in image_name.length(): 26 | var validated_character = image_name_with_underscored_invalid_characters[character_index] 27 | if validated_character == "_": 28 | var original_character = image_name[character_index] 29 | if original_character != "_": 30 | if not unsupported_characters.has(original_character): 31 | unsupported_characters.push_back(original_character) 32 | if not unsupported_characters.is_empty(): 33 | result.fail(ERR_FILE_BAD_PATH, "There are unsupported characters in Krita Document Title: \"%s\"" % ["".join(unsupported_characters)]) 34 | return result 35 | 36 | func _export(res_source_file_path: String, options: Dictionary) -> ExportResult: 37 | var result: ExportResult = ExportResult.new() 38 | var err: Error 39 | 40 | var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value() 41 | if os_command_result.error: 42 | result.fail(ERR_UNCONFIGURED, "Failed to get Krita Command to export spritesheet", os_command_result) 43 | return result 44 | 45 | var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value() 46 | if os_command_arguments_result.error: 47 | result.fail(ERR_UNCONFIGURED, "Failed to get Krita Command Arguments to export spritesheet", os_command_arguments_result) 48 | return result 49 | 50 | var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value() 51 | if temp_dir_path_result.error: 52 | result.fail(ERR_UNCONFIGURED, "Failed to get Temporary Files Directory Path to export spritesheet", temp_dir_path_result) 53 | return result 54 | var global_temp_dir_path: String = ProjectSettings.globalize_path( 55 | temp_dir_path_result.value.strip_edges()) 56 | var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \ 57 | _DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path) 58 | if unique_temp_dir_creation_result.error: 59 | result.fail(ERR_QUERY_FAILED, "Failed to create unique temporary directory to export spritesheet", unique_temp_dir_creation_result) 60 | return result 61 | var unique_temp_dir_path: String = unique_temp_dir_creation_result.path 62 | 63 | var global_source_file_path: String = ProjectSettings.globalize_path(res_source_file_path) 64 | 65 | var zip_reader: ZIPReader = ZIPReader.new() 66 | var zip_error: Error = zip_reader.open(global_source_file_path) 67 | if zip_error: 68 | 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)]) 69 | return result 70 | 71 | var files_names_in_zip: PackedStringArray = zip_reader.get_files() 72 | 73 | var maindoc_filename: String = "maindoc.xml" 74 | var maindoc_buffer: PackedByteArray = zip_reader.read_file(maindoc_filename) 75 | var maindoc_xml_root: _XML.XMLNodeRoot = _XML.parse_buffer(maindoc_buffer) 76 | var maindoc_doc_xml_element: _XML.XMLNodeElement = maindoc_xml_root.get_elements("DOC").front() 77 | 78 | var image_xml_element: _XML.XMLNodeElement = maindoc_doc_xml_element.get_elements("IMAGE").front() 79 | var image_name: String = image_xml_element.get_string("name") 80 | var image_size: Vector2i = image_xml_element.get_vector2i("width", "height") 81 | var image_name_validation_result: _Result = __validate_image_name(image_name) 82 | if image_name_validation_result.error: 83 | result.fail(ERR_INVALID_DATA, 84 | "Krita Document Title have unsupported format", 85 | image_name_validation_result) 86 | return result 87 | 88 | var has_keyframes: bool 89 | for layer_xml_element in image_xml_element.get_elements("layers").front().get_elements("layer"): 90 | if layer_xml_element.attributes.has("keyframes"): 91 | has_keyframes = true 92 | break 93 | if not has_keyframes: 94 | result.fail(ERR_INVALID_DATA, "Source file has no keyframes") 95 | return result 96 | 97 | var animation_xml_element: _XML.XMLNodeElement = image_xml_element.get_elements("animation").front() 98 | var animation_framerate: int = max(1, animation_xml_element.get_elements("framerate").front().get_int("value")) 99 | var animation_range_xml_element: _XML.XMLNodeElement = animation_xml_element.get_elements("range").front() 100 | 101 | var animation_index_filename: String = "%s/animation/index.xml" % image_name 102 | var animation_index_buffer: PackedByteArray = zip_reader.read_file(animation_index_filename) 103 | var animation_index_xml_root: _XML.XMLNodeRoot = _XML.parse_buffer(animation_index_buffer) 104 | var animation_index_animation_metadata_xml_element: _XML.XMLNodeElement = animation_index_xml_root.get_elements("animation-metadata").front() 105 | var animation_index_animation_metadata_range_xml_element: _XML.XMLNodeElement = animation_index_animation_metadata_xml_element.get_elements("range").front() 106 | var export_settings_xml_element: _XML.XMLNodeElement = animation_index_animation_metadata_xml_element.get_elements("export-settings").front() 107 | var sequence_file_path_xml_element: _XML.XMLNodeElement = export_settings_xml_element.get_elements("sequenceFilePath").front() 108 | var sequence_base_name_xml_element: _XML.XMLNodeElement = export_settings_xml_element.get_elements("sequenceBaseName").front() 109 | 110 | var animations_parameters_parsing_results: Array[AnimationParamsParsingResult] 111 | var total_animations_frames_count: int 112 | var first_animations_frame_index: int = -1 113 | var last_animations_frame_index: int = -1 114 | var global_temp_kra_path: String 115 | var temp_file_base_name: String = "img" 116 | var temp_kra_file_name: String = temp_file_base_name + ".kra" 117 | var temp_png_file_name_pattern: String = temp_file_base_name + ".png" 118 | 119 | var storyboard_index_file_name: String = "%s/storyboard/index.xml" % image_name 120 | if storyboard_index_file_name in files_names_in_zip: 121 | var storyboard_index_xml_root: _XML.XMLNodeRoot = _XML.parse_buffer(zip_reader.read_file("%s/storyboard/index.xml" % image_name)) 122 | var storyboard_info_xml_element: _XML.XMLNodeElement = storyboard_index_xml_root.get_elements("storyboard-info").front() 123 | var storyboard_item_list_xml_element: _XML.XMLNodeElement = storyboard_info_xml_element.get_elements("StoryboardItemList").front() 124 | var storyboard_item_xml_elements: Array[_XML.XMLNodeElement] = storyboard_item_list_xml_element.get_elements("storyboarditem") 125 | var unique_animations_names: PackedStringArray 126 | 127 | for animation_index in storyboard_item_xml_elements.size(): 128 | var story_xml_element: _XML.XMLNodeElement = storyboard_item_xml_elements[animation_index] 129 | var animation_first_frame: int = story_xml_element.get_int("frame") 130 | var animation_params_parsing_result: AnimationParamsParsingResult = _parse_animation_params( 131 | story_xml_element.get_string("item-name").strip_edges(), 132 | AnimationOptions.Direction | AnimationOptions.RepeatCount, 133 | animation_first_frame, 134 | story_xml_element.get_int("duration-frame") + \ 135 | animation_framerate * story_xml_element.get_int("duration-second")) 136 | if animation_params_parsing_result.error: 137 | result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters", 138 | animation_params_parsing_result) 139 | return result 140 | if unique_animations_names.has(animation_params_parsing_result.name): 141 | result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" % 142 | [animation_params_parsing_result.name, animation_index]) 143 | return result 144 | unique_animations_names.push_back(animation_params_parsing_result.name) 145 | animations_parameters_parsing_results.push_back(animation_params_parsing_result) 146 | total_animations_frames_count += animation_params_parsing_result.frames_count 147 | if first_animations_frame_index < 0 or animation_params_parsing_result.first_frame_index < first_animations_frame_index: 148 | first_animations_frame_index = animation_params_parsing_result.first_frame_index 149 | var animation_last_frame_index: int = animation_params_parsing_result.first_frame_index + animation_params_parsing_result.frames_count - 1 150 | if last_animations_frame_index < 0 or animation_last_frame_index > last_animations_frame_index: 151 | last_animations_frame_index = animation_last_frame_index 152 | 153 | animation_range_xml_element.attributes["from"] = str(first_animations_frame_index) 154 | animation_range_xml_element.attributes["to"] = str(last_animations_frame_index) 155 | 156 | global_temp_kra_path = unique_temp_dir_path.path_join(temp_kra_file_name) 157 | 158 | animation_index_animation_metadata_range_xml_element.attributes["from"] = str(first_animations_frame_index) 159 | animation_index_animation_metadata_range_xml_element.attributes["to"] = str(last_animations_frame_index) 160 | 161 | var zip_writer = ZIPPacker.new() 162 | zip_writer.open(global_temp_kra_path, ZIPPacker.APPEND_CREATE) 163 | for filename in zip_reader.get_files(): 164 | zip_writer.start_file(filename) 165 | match filename: 166 | maindoc_filename: 167 | zip_writer.write_file(maindoc_xml_root.dump_to_buffer()) 168 | animation_index_filename: 169 | zip_writer.write_file(animation_index_xml_root.dump_to_buffer()) 170 | _: zip_writer.write_file(zip_reader.read_file(filename)) 171 | zip_writer.close_file() 172 | zip_writer.close() 173 | else: 174 | first_animations_frame_index = animation_range_xml_element.get_int("from") 175 | last_animations_frame_index = animation_range_xml_element.get_int("to") 176 | total_animations_frames_count = last_animations_frame_index - first_animations_frame_index + 1 177 | var default_animation_params_parsing_result: AnimationParamsParsingResult = AnimationParamsParsingResult.new() 178 | default_animation_params_parsing_result.name = options[_Options.DEFAULT_ANIMATION_NAME].strip_edges() 179 | if not default_animation_params_parsing_result.name: 180 | default_animation_params_parsing_result.name = "default" 181 | default_animation_params_parsing_result.first_frame_index = first_animations_frame_index 182 | default_animation_params_parsing_result.frames_count = last_animations_frame_index - first_animations_frame_index + 1 183 | default_animation_params_parsing_result.direction = options[_Options.DEFAULT_ANIMATION_DIRECTION] 184 | default_animation_params_parsing_result.repeat_count = options[_Options.DEFAULT_ANIMATION_REPEAT_COUNT] 185 | animations_parameters_parsing_results.push_back(default_animation_params_parsing_result) 186 | global_temp_kra_path = global_source_file_path 187 | 188 | zip_reader.close() 189 | 190 | var global_temp_png_path_pattern: String = unique_temp_dir_path.path_join(temp_png_file_name_pattern) 191 | 192 | var command: String = os_command_result.value.strip_edges() 193 | var arguments: PackedStringArray = \ 194 | os_command_arguments_result.value + \ 195 | PackedStringArray([ 196 | "--export-sequence", 197 | "--export-filename", global_temp_png_path_pattern, 198 | global_temp_kra_path]) 199 | 200 | var output: Array 201 | var exit_code: int = OS.execute(command, arguments, output, true, false) 202 | if exit_code: 203 | for arg_index in arguments.size(): 204 | arguments[arg_index] = "\nArgument: " + arguments[arg_index] 205 | result.fail(ERR_QUERY_FAILED, " ".join([ 206 | "An error occurred while executing the Krita command.", 207 | "Process exited with code %s:\nCommand: %s%s" 208 | ]) % [exit_code, command, "".join(arguments)]) 209 | return result 210 | 211 | var unique_frames_count: int = last_animations_frame_index + 1 # - first_stories_frame 212 | var frames_images: Array[Image] 213 | for image_idx in unique_frames_count: 214 | var global_frame_png_path: String = unique_temp_dir_path \ 215 | .path_join("%s%04d.png" % [temp_file_base_name, image_idx]) 216 | if FileAccess.file_exists(global_frame_png_path): 217 | var image: Image = Image.load_from_file(global_frame_png_path) 218 | frames_images.push_back(image) 219 | else: 220 | frames_images.push_back(frames_images.back()) 221 | 222 | var sprite_sheet_builder: _SpriteSheetBuilderBase = _create_sprite_sheet_builder(options) 223 | 224 | var sprite_sheet_building_result: _SpriteSheetBuilderBase.SpriteSheetBuildingResult = sprite_sheet_builder.build_sprite_sheet(frames_images) 225 | if sprite_sheet_building_result.error: 226 | result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result) 227 | return result 228 | var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet 229 | 230 | var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new() 231 | var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges() 232 | 233 | var frames_duration: float = 1.0 / animation_framerate 234 | var all_frames: Array[_Common.FrameInfo] 235 | all_frames.resize(unique_frames_count) 236 | for animation_index in animations_parameters_parsing_results.size(): 237 | var animation_params_parsing_result: AnimationParamsParsingResult = animations_parameters_parsing_results[animation_index] 238 | var animation = _Common.AnimationInfo.new() 239 | animation.name = animation_params_parsing_result.name 240 | if animation.name == autoplay_animation_name: 241 | animation_library.autoplay_index = animation_index 242 | animation.direction = animation_params_parsing_result.direction 243 | if animation.direction < 0: 244 | animation.direction = _Common.AnimationDirection.FORWARD 245 | animation.repeat_count = animation_params_parsing_result.repeat_count 246 | if animation.repeat_count < 0: 247 | animation.repeat_count = 1 248 | for animation_frame_index in animation_params_parsing_result.frames_count: 249 | var global_frame_index: int = animation_params_parsing_result.first_frame_index + animation_frame_index 250 | var frame: _Common.FrameInfo = all_frames[global_frame_index] 251 | if frame == null: 252 | frame = _Common.FrameInfo.new() 253 | frame.sprite = sprite_sheet.sprites[global_frame_index] 254 | frame.duration = frames_duration 255 | all_frames[global_frame_index] = frame 256 | animation.frames.push_back(frame) 257 | animation_library.animations.push_back(animation) 258 | 259 | if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0: 260 | push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name]) 261 | 262 | if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error: 263 | push_warning( 264 | "Failed to remove unique temporary directory: \"%s\"" % 265 | [unique_temp_dir_path]) 266 | 267 | result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library) 268 | return result 269 | 270 | class CustomImageFormatLoaderExtension: 271 | extends ImageFormatLoaderExtension 272 | 273 | var __recognized_extensions: PackedStringArray 274 | 275 | func _init(recognized_extensions: PackedStringArray) -> void: 276 | __recognized_extensions = recognized_extensions 277 | 278 | func _get_recognized_extensions() -> PackedStringArray: 279 | return __recognized_extensions 280 | 281 | func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error: 282 | var zip_reader := ZIPReader.new() 283 | zip_reader.open(file_access.get_path_absolute()) 284 | image.load_png_from_buffer(zip_reader.read_file("mergedimage.png")) 285 | zip_reader.close() 286 | return OK 287 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/export/pencil2d.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_.gd" 3 | 4 | const _XML = preload("../xml.gd") 5 | 6 | var __os_command_setting: _Setting = _Setting.new( 7 | "pencil2d_command", "", TYPE_STRING, PROPERTY_HINT_NONE, 8 | "", true, func(v: String): return v.is_empty()) 9 | 10 | var __os_command_arguments_setting: _Setting = _Setting.new( 11 | "pencil2d_command_arguments", PackedStringArray(), TYPE_PACKED_STRING_ARRAY, PROPERTY_HINT_NONE, 12 | "", true, func(v: PackedStringArray): return false) 13 | 14 | const __ANIMATIONS_PARAMETERS_OPTION: StringName = &"pencil2d/animations_parameters" 15 | 16 | func _init() -> void: 17 | var recognized_extensions: PackedStringArray = ["pclx"] 18 | super("Pencil2D", recognized_extensions, [ 19 | _Options.create_option(__ANIMATIONS_PARAMETERS_OPTION, PackedStringArray(), 20 | PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT)], 21 | [ __os_command_setting, __os_command_arguments_setting ], 22 | CustomImageFormatLoaderExtension.new( 23 | recognized_extensions, 24 | __os_command_setting, 25 | __os_command_arguments_setting, 26 | _Common.common_temporary_files_directory_path_setting)) 27 | 28 | func _export(res_source_file_path: String, options: Dictionary) -> ExportResult: 29 | var result: ExportResult = ExportResult.new() 30 | var err: Error 31 | 32 | var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value() 33 | if os_command_result.error: 34 | result.fail(ERR_UNCONFIGURED, "Failed to get Pencil2D Command to export spritesheet", os_command_result) 35 | return result 36 | 37 | var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value() 38 | if os_command_arguments_result.error: 39 | result.fail(ERR_UNCONFIGURED, "Failed to get Pencil2D Command Arguments to export spritesheet", os_command_arguments_result) 40 | return result 41 | 42 | var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value() 43 | if temp_dir_path_result.error: 44 | result.fail(ERR_UNCONFIGURED, "Failed to get Temporary Files Directory Path to export spritesheet", temp_dir_path_result) 45 | return result 46 | var global_temp_dir_path: String = ProjectSettings.globalize_path( 47 | temp_dir_path_result.value.strip_edges()) 48 | var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \ 49 | _DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path) 50 | if unique_temp_dir_creation_result.error: 51 | result.fail(ERR_QUERY_FAILED, "Failed to create unique temporary directory to export spritesheet", unique_temp_dir_creation_result) 52 | return result 53 | var unique_temp_dir_path: String = unique_temp_dir_creation_result.path 54 | 55 | var global_source_file_path: String = ProjectSettings.globalize_path(res_source_file_path) 56 | 57 | var zip_reader: ZIPReader = ZIPReader.new() 58 | var zip_error: Error = zip_reader.open(global_source_file_path) 59 | if zip_error: 60 | 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)]) 61 | return result 62 | var buffer: PackedByteArray = zip_reader.read_file("main.xml") 63 | var main_xml_root: _XML.XMLNodeRoot = _XML.parse_buffer(buffer) 64 | zip_reader.close() 65 | var animation_framerate: int = main_xml_root \ 66 | .get_elements("document").front() \ 67 | .get_elements("projectdata").front() \ 68 | .get_elements("fps").front() \ 69 | .get_int("value") 70 | 71 | var raw_animations_params_list: PackedStringArray = options[__ANIMATIONS_PARAMETERS_OPTION] 72 | var animations_params_parsing_results: Array[AnimationParamsParsingResult] 73 | animations_params_parsing_results.resize(raw_animations_params_list.size()) 74 | var unique_animations_names: PackedStringArray 75 | var frame_indices_to_export 76 | var unique_frames_count: int = 0 77 | var animation_first_frame_index: int = 0 78 | for animation_index in raw_animations_params_list.size(): 79 | var raw_animation_params: String = raw_animations_params_list[animation_index] 80 | var animation_params_parsing_result: AnimationParamsParsingResult = _parse_animation_params( 81 | raw_animation_params, 82 | AnimationOptions.FramesCount | AnimationOptions.Direction | AnimationOptions.RepeatCount, 83 | animation_first_frame_index) 84 | if animation_params_parsing_result.error: 85 | result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters", animation_params_parsing_result) 86 | return result 87 | if unique_animations_names.has(animation_params_parsing_result.name): 88 | result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" % 89 | [animation_params_parsing_result.name, animation_index]) 90 | return result 91 | unique_animations_names.push_back(animation_params_parsing_result.name) 92 | unique_frames_count += animation_params_parsing_result.frames_count 93 | animation_first_frame_index += animation_params_parsing_result.frames_count 94 | animations_params_parsing_results[animation_index] = animation_params_parsing_result 95 | 96 | # -o --export Render the file to 97 | # --camera Name of the camera layer to use 98 | # --width Width of the output frames 99 | # --height Height of the output frames 100 | # --start The first frame you want to include in the exported movie 101 | # --end The last frame you want to include in the exported movie. Can also be last or last-sound to automatically use the last frame containing animation or sound respectively 102 | # --transparency Render transparency when possible 103 | # input Path to input pencil file 104 | var png_base_name: String = "temp" 105 | var global_temp_png_path: String = unique_temp_dir_path.path_join("%s.png" % png_base_name) 106 | 107 | var command: String = os_command_result.value.strip_edges() 108 | var arguments: PackedStringArray = \ 109 | os_command_arguments_result.value + \ 110 | PackedStringArray([ 111 | "--export", global_temp_png_path, 112 | "--start", 1, 113 | "--end", unique_frames_count, 114 | "--transparency", 115 | global_source_file_path]) 116 | 117 | var output: Array 118 | var exit_code: int = OS.execute(command, arguments, output, true, false) 119 | if exit_code: 120 | for arg_index in arguments.size(): 121 | arguments[arg_index] = "\nArgument: " + arguments[arg_index] 122 | result.fail(ERR_QUERY_FAILED, " ".join([ 123 | "An error occurred while executing the Pencil2D command.", 124 | "Process exited with code %s:\nCommand: %s%s" 125 | ]) % [exit_code, command, "".join(arguments)]) 126 | return result 127 | 128 | var frames_images: Array[Image] 129 | for image_idx in unique_frames_count: 130 | var global_frame_png_path: String = unique_temp_dir_path \ 131 | .path_join("%s%04d.png" % [png_base_name, image_idx + 1]) 132 | frames_images.push_back(Image.load_from_file(global_frame_png_path)) 133 | 134 | var sprite_sheet_builder: _SpriteSheetBuilderBase = _create_sprite_sheet_builder(options) 135 | 136 | var sprite_sheet_building_result: _SpriteSheetBuilderBase.SpriteSheetBuildingResult = sprite_sheet_builder.build_sprite_sheet(frames_images) 137 | if sprite_sheet_building_result.error: 138 | result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result) 139 | return result 140 | var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet 141 | 142 | var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new() 143 | var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges() 144 | 145 | var frames_duration: float = 1.0 / animation_framerate 146 | var all_frames: Array[_Common.FrameInfo] 147 | all_frames.resize(unique_frames_count) 148 | for animation_index in animations_params_parsing_results.size(): 149 | var animation_params_parsing_result: AnimationParamsParsingResult = animations_params_parsing_results[animation_index] 150 | var animation = _Common.AnimationInfo.new() 151 | animation.name = animation_params_parsing_result.name 152 | if animation.name == autoplay_animation_name: 153 | animation_library.autoplay_index = animation_index 154 | animation.direction = animation_params_parsing_result.direction 155 | if animation.direction < 0: 156 | animation.direction = _Common.AnimationDirection.FORWARD 157 | animation.repeat_count = animation_params_parsing_result.repeat_count 158 | if animation.repeat_count < 0: 159 | animation.repeat_count = 1 160 | for animation_frame_index in animation_params_parsing_result.frames_count: 161 | var global_frame_index: int = animation_params_parsing_result.first_frame_index + animation_frame_index 162 | var frame: _Common.FrameInfo = all_frames[global_frame_index] 163 | if frame == null: 164 | frame = _Common.FrameInfo.new() 165 | frame.sprite = sprite_sheet.sprites[global_frame_index] 166 | frame.duration = frames_duration 167 | all_frames[global_frame_index] = frame 168 | animation.frames.push_back(frame) 169 | animation_library.animations.push_back(animation) 170 | if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0: 171 | push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name]) 172 | 173 | if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error: 174 | push_warning( 175 | "Failed to remove unique temporary directory: \"%s\"" % 176 | [unique_temp_dir_path]) 177 | 178 | result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library) 179 | return result 180 | 181 | class CustomImageFormatLoaderExtension: 182 | extends ImageFormatLoaderExtension 183 | 184 | var __recognized_extensions: PackedStringArray 185 | var __os_command_setting: _Setting 186 | var __os_command_arguments_setting: _Setting 187 | var __common_temporary_files_directory_path_setting: _Setting 188 | 189 | func _init(recognized_extensions: PackedStringArray, 190 | os_command_setting: _Setting, 191 | os_command_arguments_setting: _Setting, 192 | common_temporary_files_directory_path: _Setting, 193 | ) -> void: 194 | __recognized_extensions = recognized_extensions 195 | __os_command_setting = os_command_setting 196 | __os_command_arguments_setting = os_command_arguments_setting 197 | __common_temporary_files_directory_path_setting = common_temporary_files_directory_path 198 | 199 | func _get_recognized_extensions() -> PackedStringArray: 200 | return __recognized_extensions 201 | 202 | func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error: 203 | var err: Error 204 | 205 | var os_command_result: _Setting.GettingValueResult = __os_command_setting.get_value() 206 | if os_command_result.error: 207 | push_error(os_command_result.error_description) 208 | return os_command_result.error 209 | 210 | var os_command_arguments_result: _Setting.GettingValueResult = __os_command_arguments_setting.get_value() 211 | if os_command_arguments_result.error: 212 | push_error(os_command_arguments_result.error_description) 213 | return os_command_arguments_result.error 214 | 215 | var temp_dir_path_result: _Setting.GettingValueResult = _Common.common_temporary_files_directory_path_setting.get_value() 216 | if temp_dir_path_result.error: 217 | push_error("Failed to get Temporary Files Directory Path to export spritesheet") 218 | return temp_dir_path_result.error 219 | var global_temp_dir_path: String = ProjectSettings.globalize_path( 220 | temp_dir_path_result.value.strip_edges()) 221 | var unique_temp_dir_creation_result: _DirAccessExtensions.CreationResult = \ 222 | _DirAccessExtensions.create_directory_with_unique_name(global_temp_dir_path) 223 | if unique_temp_dir_creation_result.error: 224 | push_error("Failed to create unique temporary directory to export spritesheet") 225 | return unique_temp_dir_creation_result.error 226 | var unique_temp_dir_path: String = unique_temp_dir_creation_result.path 227 | 228 | var global_source_file_path: String = ProjectSettings.globalize_path(file_access.get_path()) 229 | 230 | const png_base_name: String = "img" 231 | var global_temp_png_path: String = unique_temp_dir_path.path_join("%s.png" % png_base_name) 232 | 233 | var command: String = os_command_result.value.strip_edges() 234 | var arguments: PackedStringArray = \ 235 | os_command_arguments_result.value + \ 236 | PackedStringArray([ 237 | "--export", global_temp_png_path, 238 | "--start", 1, 239 | "--end", 1, 240 | "--transparency", 241 | global_source_file_path]) 242 | 243 | var output: Array 244 | var exit_code: int = OS.execute(command, arguments, output, true, false) 245 | if exit_code: 246 | for arg_index in arguments.size(): 247 | arguments[arg_index] = "\nArgument: " + arguments[arg_index] 248 | push_error(" ".join([ 249 | "An error occurred while executing the Pencil2D command.", 250 | "Process exited with code %s:\nCommand: %s%s" 251 | ]) % [exit_code, command, "".join(arguments)]) 252 | return ERR_QUERY_FAILED 253 | 254 | var global_frame_png_path: String = unique_temp_dir_path \ 255 | .path_join("%s0001.png" % [png_base_name]) 256 | err = image.load_png_from_buffer(FileAccess.get_file_as_bytes(global_frame_png_path)) 257 | if err: 258 | push_error("An error occurred while image loading") 259 | return err 260 | 261 | if _DirAccessExtensions.remove_dir_recursive(unique_temp_dir_path).error: 262 | push_warning( 263 | "Failed to remove unique temporary directory: \"%s\"" % 264 | [unique_temp_dir_path]) 265 | 266 | return OK 267 | 268 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/export/piskel.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_.gd" 3 | 4 | const __ANIMATIONS_PARAMETERS_OPTION: StringName = &"piskel/animations_parameters" 5 | 6 | func _init() -> void: 7 | var recognized_extensions: PackedStringArray = ["piskel"] 8 | super("Piskel", recognized_extensions, [ 9 | _Options.create_option(__ANIMATIONS_PARAMETERS_OPTION, PackedStringArray(), 10 | PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT)], 11 | [], 12 | CustomImageFormatLoaderExtension.new(recognized_extensions)) 13 | 14 | func _export(res_source_file_path: String, options: Dictionary) -> ExportResult: 15 | var result: ExportResult = ExportResult.new() 16 | 17 | var raw_animations_params_list: PackedStringArray = options[__ANIMATIONS_PARAMETERS_OPTION] 18 | var animations_params_parsing_results: Array[AnimationParamsParsingResult] 19 | animations_params_parsing_results.resize(raw_animations_params_list.size()) 20 | var unique_animations_names: PackedStringArray 21 | var frame_indices_to_export 22 | var unique_frames_count: int = 0 23 | var animation_first_frame_index: int = 0 24 | for animation_index in raw_animations_params_list.size(): 25 | var raw_animation_params: String = raw_animations_params_list[animation_index] 26 | var animation_params_parsing_result: AnimationParamsParsingResult = _parse_animation_params( 27 | raw_animation_params, 28 | AnimationOptions.FramesCount | AnimationOptions.Direction | AnimationOptions.RepeatCount, 29 | animation_first_frame_index) 30 | if animation_params_parsing_result.error: 31 | result.fail(ERR_CANT_RESOLVE, "Failed to parse animation parameters", animation_params_parsing_result) 32 | return result 33 | if unique_animations_names.has(animation_params_parsing_result.name): 34 | result.fail(ERR_INVALID_DATA, "Duplicated animation name \"%s\" at index: %s" % 35 | [animation_params_parsing_result.name, animation_index]) 36 | return result 37 | unique_animations_names.push_back(animation_params_parsing_result.name) 38 | unique_frames_count += animation_params_parsing_result.frames_count 39 | animation_first_frame_index += animation_params_parsing_result.frames_count 40 | animations_params_parsing_results[animation_index] = animation_params_parsing_result 41 | 42 | var document: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(res_source_file_path)) 43 | document.modelVersion #int 2 44 | var piskel: Dictionary = document.piskel 45 | piskel.name #string New Piskel 46 | piskel.description #string asdfasdfasdf 47 | piskel.fps #int 12, 48 | var image_size: Vector2i = Vector2i(piskel.width, piskel.height) 49 | # piskel.hiddenFrames#Array may absend 50 | var blended_layers: Image 51 | var layer_image: Image = Image.new() 52 | var frames_count: int 53 | var layer_image_size: Vector2i = image_size 54 | for layer_string in piskel.layers: #Array 55 | var layer: Dictionary = JSON.parse_string(layer_string) 56 | layer.name #string layer 1 57 | layer.opacity #float 1 58 | if frames_count == 0: 59 | frames_count = layer.frameCount 60 | layer_image_size.x = image_size.x * frames_count 61 | else: 62 | assert(frames_count == layer.frameCount) 63 | for chunk in layer.chunks: 64 | # chunk.layout # array [ [ 0 ], [ 1 ], [ 2 ] ] 65 | layer_image.load_png_from_buffer(Marshalls.base64_to_raw(chunk.base64PNG.trim_prefix("data:image/png;base64,"))) 66 | assert(layer_image.get_size() == layer_image_size) 67 | if blended_layers == null: 68 | blended_layers = layer_image 69 | layer_image = Image.new() 70 | else: 71 | blended_layers.blend_rect(layer_image, Rect2i(Vector2i.ZERO, layer_image.get_size()), Vector2i.ZERO) 72 | 73 | var frames_images: Array[Image] 74 | frames_images.resize(frames_count) 75 | for frame_index in frames_count: 76 | frames_images[frame_index] = blended_layers.get_region( 77 | Rect2i(Vector2i(frame_index * image_size.x, 0), image_size)) 78 | 79 | var sprite_sheet_builder: _SpriteSheetBuilderBase = _create_sprite_sheet_builder(options) 80 | 81 | var sprite_sheet_building_result: _SpriteSheetBuilderBase.SpriteSheetBuildingResult = \ 82 | sprite_sheet_builder.build_sprite_sheet(frames_images) 83 | if sprite_sheet_building_result.error: 84 | result.fail(ERR_BUG, "Sprite sheet building failed", sprite_sheet_building_result) 85 | return result 86 | var sprite_sheet: _Common.SpriteSheetInfo = sprite_sheet_building_result.sprite_sheet 87 | 88 | var animation_library: _Common.AnimationLibraryInfo = _Common.AnimationLibraryInfo.new() 89 | var autoplay_animation_name: String = options[_Options.AUTOPLAY_ANIMATION_NAME].strip_edges() 90 | 91 | var frames_duration: float = 1.0 / piskel.fps 92 | var all_frames: Array[_Common.FrameInfo] 93 | all_frames.resize(unique_frames_count) 94 | for animation_index in animations_params_parsing_results.size(): 95 | var animation_params_parsing_result: AnimationParamsParsingResult = animations_params_parsing_results[animation_index] 96 | var animation = _Common.AnimationInfo.new() 97 | animation.name = animation_params_parsing_result.name 98 | if animation.name == autoplay_animation_name: 99 | animation_library.autoplay_index = animation_index 100 | animation.direction = animation_params_parsing_result.direction 101 | if animation.direction < 0: 102 | animation.direction = _Common.AnimationDirection.FORWARD 103 | animation.repeat_count = animation_params_parsing_result.repeat_count 104 | if animation.repeat_count < 0: 105 | animation.repeat_count = 1 106 | for animation_frame_index in animation_params_parsing_result.frames_count: 107 | var global_frame_index: int = animation_params_parsing_result.first_frame_index + animation_frame_index 108 | var frame: _Common.FrameInfo = all_frames[global_frame_index] 109 | if frame == null: 110 | frame = _Common.FrameInfo.new() 111 | frame.sprite = sprite_sheet.sprites[global_frame_index] 112 | frame.duration = frames_duration 113 | all_frames[global_frame_index] = frame 114 | animation.frames.push_back(frame) 115 | animation_library.animations.push_back(animation) 116 | if not autoplay_animation_name.is_empty() and animation_library.autoplay_index < 0: 117 | push_warning("Autoplay animation name not found: \"%s\". Continuing..." % [autoplay_animation_name]) 118 | 119 | result.success(sprite_sheet_building_result.atlas_image, sprite_sheet, animation_library) 120 | return result 121 | 122 | class CustomImageFormatLoaderExtension: 123 | extends ImageFormatLoaderExtension 124 | 125 | var __recognized_extensions: PackedStringArray 126 | 127 | func _init(recognized_extensions: PackedStringArray) -> void: 128 | __recognized_extensions = recognized_extensions 129 | 130 | func _get_recognized_extensions() -> PackedStringArray: 131 | return __recognized_extensions 132 | 133 | func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error: 134 | 135 | var document: Dictionary = JSON.parse_string(file_access.get_as_text()) 136 | var piskel: Dictionary = document.piskel 137 | var image_size: Vector2i = Vector2i(piskel.width, piskel.height) 138 | var image_rect: Rect2i = Rect2i(Vector2i.ZERO, image_size) 139 | image.set_data(1, 1, false, Image.FORMAT_RGBA8, [0, 0, 0, 0]) 140 | image.resize(image_size.x, image_size.y) 141 | var layer_image: Image = Image.new() 142 | for layer_string in piskel.layers: #Array 143 | var layer: Dictionary = JSON.parse_string(layer_string) 144 | layer.opacity #float 1 145 | for chunk in layer.chunks: 146 | # chunk.layout # array [ [ 0 ], [ 1 ], [ 2 ] ] 147 | layer_image.load_png_from_buffer(Marshalls.base64_to_raw(chunk.base64PNG.trim_prefix("data:image/png;base64,"))) 148 | image.blend_rect(layer_image, image_rect, Vector2i.ZERO) 149 | return OK 150 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/export/pixelorama.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_.gd" 3 | 4 | const _PxoV2 = preload("_pxo_v2.gd") 5 | const _PxoV3 = preload("_pxo_v3.gd") 6 | 7 | func _init() -> void: 8 | var recognized_extensions: PackedStringArray = ["pxo"] 9 | super("Pixelorama", recognized_extensions, [ 10 | ], [ 11 | # settings 12 | ], CustomImageFormatLoaderExtension.new(recognized_extensions)) 13 | 14 | func _export(res_source_file_path: String, options: Dictionary) -> ExportResult: 15 | var v2_result: ExportResult 16 | var v3_result := _PxoV3.new(res_source_file_path).export(options) 17 | 18 | if v3_result.error: 19 | v2_result = _PxoV2.export(res_source_file_path, options) 20 | 21 | # Only return the v2 result if it succeeded; if both failed, the v3 result 22 | # is returned on the theory that the more recent format is more likely to 23 | # be what was intended. 24 | if not v2_result.error: 25 | return v2_result 26 | 27 | return v2_result if v2_result else v3_result 28 | 29 | class CustomImageFormatLoaderExtension: 30 | extends ImageFormatLoaderExtension 31 | 32 | var __recognized_extensions: PackedStringArray 33 | 34 | func _init(recognized_extensions: PackedStringArray) -> void: 35 | __recognized_extensions = recognized_extensions 36 | 37 | func _get_recognized_extensions() -> PackedStringArray: 38 | return __recognized_extensions 39 | 40 | func _load_image(image: Image, file_access: FileAccess, flags: int, scale: float) -> Error: 41 | var v3_error: int = _PxoV3.new(file_access.get_path()).set_image_data(image) 42 | 43 | if v3_error != OK: 44 | if OK == _PxoV2.load_image(image, file_access, flags, scale): 45 | return OK 46 | 47 | return v3_error 48 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/external_scripts/_.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | 3 | const SpriteSheetLayout = preload("../common.gd").SpriteSheetLayout 4 | const EdgesArtifactsAvoidanceMethod = preload("../common.gd").EdgesArtifactsAvoidanceMethod 5 | const AnimationDirection = preload("../common.gd").AnimationDirection 6 | const SpriteInfo = preload("../common.gd").SpriteInfo 7 | const SpriteSheetInfo = preload("../common.gd").SpriteSheetInfo 8 | const FrameInfo = preload("../common.gd").FrameInfo 9 | const AnimationInfo = preload("../common.gd").AnimationInfo 10 | const AnimationLibraryInfo = preload("../common.gd").AnimationLibraryInfo 11 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/external_scripts/middle_import_script_base.gd: -------------------------------------------------------------------------------- 1 | extends "_.gd" 2 | 3 | class Context: 4 | extends RefCounted 5 | var atlas_image: Image 6 | var sprite_sheet: SpriteSheetInfo 7 | var animation_library: AnimationLibraryInfo 8 | var gen_files_to_add: PackedStringArray 9 | var middle_import_data: Variant 10 | 11 | static func modify_context( 12 | res_source_file_path: String, 13 | res_save_file_path: String, 14 | editor_import_plugin: EditorImportPlugin, 15 | options: Dictionary, 16 | context: Context) -> Error: 17 | return OK 18 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/external_scripts/middle_import_script_example.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/nklbdev.importality/external_scripts/middle_import_script_base.gd" 2 | 3 | static func modify_context( 4 | # Path to the source file from which the import is performed 5 | res_source_file_path: String, 6 | # Path to save imported resource file 7 | res_save_file_path: String, 8 | # EditorImportPlugin instance to call append_import_external_resource 9 | # or other methods 10 | editor_import_plugin: EditorImportPlugin, 11 | # Import options 12 | options: Dictionary, 13 | # Context-object to modify 14 | context: Context) -> Error: 15 | # ------------------------------------------------ 16 | # You can modify or replace objects in context fields. 17 | # (Be careful not to shoot yourself in the foot!) 18 | # ------------------------------------------------ 19 | # 20 | # context.atlas_image: Image 21 | # The image that will be saved as a PNG file next to the original file 22 | # and automatically imported by the engine into a resource 23 | # that will be used as an atlas 24 | # 25 | # context.sprite_sheet: SpriteSheetInfo 26 | # Sprite sheet data. Stores source image size and sprites data (SpriteInfo) 27 | # 28 | # context.animation_library: AnimationLibraryInfo 29 | # Animations data. Uses sprites data (SpriteInfo) stored in context.sprite_sheet 30 | # 31 | # gen_files_to_add: PackedStringArray 32 | # Gen-files paths to add to gen_files array of import-function 33 | # 34 | # context.middle_import_data: Variant 35 | # Your custom data to use in the post-import script 36 | 37 | # You can save your new resources directly in .godot/import folder 38 | # in *.res or *.tres formats. 39 | # 40 | # If you want to save an image as Texture resource, 41 | # use PortableCompressedTexture2D resource. It has almost the same file 42 | # structure as CompressedTexture2D (engine internal *.ctex - files). 43 | # And you can embed this resource into another resources! 44 | # You cannot save an image in *.ctex format yourself. Sad but true. 45 | 46 | box_blur(context.atlas_image) 47 | grayscale(context.atlas_image) 48 | return OK 49 | 50 | static func box_blur(image: Image) -> void: 51 | var image_copy = image.duplicate() 52 | var image_size: Vector2i = image.get_size() 53 | for y in range(1, image_size.y - 1): for x in range(1, image_size.x - 1): 54 | # Set P to the average of 9 pixels: 55 | # X X X 56 | # X P X 57 | # X X X 58 | image.set_pixel(x, y, ( 59 | image_copy.get_pixel(x - 1, y + 1) + # Top left 60 | image_copy.get_pixel(x + 0, y + 1) + # Top center 61 | image_copy.get_pixel(x + 1, y + 1) + # Top right 62 | image_copy.get_pixel(x - 1, y + 0) + # Mid left 63 | image_copy.get_pixel(x + 0, y + 0) + # Current pixel 64 | image_copy.get_pixel(x + 1, y + 0) + # Mid right 65 | image_copy.get_pixel(x - 1, y - 1) + # Low left 66 | image_copy.get_pixel(x + 0, y - 1) + # Low center 67 | image_copy.get_pixel(x + 1, y - 1) # Low right 68 | ) / 9.0) 69 | 70 | static func grayscale(image: Image) -> void: 71 | var image_size: Vector2i = image.get_size() 72 | for y in image_size.y: for x in image_size.x: 73 | var pixel_color: Color = image.get_pixel(x, y) 74 | var luminance: float = pixel_color.get_luminance() 75 | image.set_pixel(x, y, Color(luminance, luminance, luminance, pixel_color.a)); 76 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/external_scripts/post_import_script_base.gd: -------------------------------------------------------------------------------- 1 | extends "_.gd" 2 | 3 | class Context: 4 | extends RefCounted 5 | var resource: Resource 6 | var resource_saver_flags: ResourceSaver.SaverFlags 7 | var save_extension: String 8 | var gen_files_to_add: PackedStringArray 9 | 10 | static func modify_context( 11 | res_source_file_path: String, 12 | res_save_file_path: String, 13 | editor_import_plugin: EditorImportPlugin, 14 | options: Dictionary, 15 | middle_import_data: Variant, 16 | context: Context, 17 | ) -> Error: 18 | return OK 19 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/external_scripts/post_import_script_example.gd: -------------------------------------------------------------------------------- 1 | extends "res://addons/nklbdev.importality/external_scripts/post_import_script_base.gd" 2 | 3 | static func modify_context( 4 | # Path to the source file from which the import is performed 5 | res_source_file_path: String, 6 | # Path to save imported resource file 7 | res_save_file_path: String, 8 | # EditorImportPlugin instance to call append_import_external_resource 9 | # or other methods 10 | editor_import_plugin: EditorImportPlugin, 11 | # Import options 12 | options: Dictionary, 13 | # Your custom data from middle-import script 14 | middle_import_data: Variant, 15 | # Context-object to modify 16 | context: Context, 17 | ) -> Error: 18 | # ------------------------------------------------ 19 | # You can modify or replace objects in context fields. 20 | # (Be careful not to shoot yourself in the foot!) 21 | # ------------------------------------------------ 22 | # 23 | # resource: Resource 24 | # A save-ready resource that you can modify or replace as you wish 25 | # 26 | # resource_saver_flags: ResourceSaver.SaverFlags 27 | # Resource save flags for use in ResourceSaver.save method 28 | # 29 | # gen_files_to_add: PackedStringArray 30 | # Gen-files paths to add to gen_files array of import-function 31 | # 32 | # save_extension: String 33 | # Save resource file extension 34 | 35 | var animated_sprite_2d: AnimatedSprite2D = (context.resource as PackedScene).instantiate() as AnimatedSprite2D 36 | animated_sprite_2d.modulate = Color.RED 37 | var packed_scene = PackedScene.new() 38 | packed_scene.pack(animated_sprite_2d) 39 | context.resource = packed_scene 40 | return OK 41 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/import/_.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends RefCounted 3 | 4 | const _Result = preload("../result.gd").Class 5 | const _Common = preload("../common.gd") 6 | const _Options = preload("../options.gd") 7 | 8 | class ImportResult: 9 | extends _Result 10 | var resource: Resource 11 | var resource_saver_flags: ResourceSaver.SaverFlags 12 | func _get_result_type_description() -> String: 13 | return "Import" 14 | func success( 15 | resource: Resource, 16 | resource_saver_flags: ResourceSaver.SaverFlags = ResourceSaver.FLAG_NONE 17 | ) -> void: 18 | _success() 19 | self.resource = resource 20 | self.resource_saver_flags = resource_saver_flags 21 | 22 | var __options: Array[Dictionary] = [ 23 | _Options.create_option(_Options.DEFAULT_ANIMATION_NAME, "default", 24 | PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), 25 | _Options.create_option(_Options.DEFAULT_ANIMATION_DIRECTION, _Common.AnimationDirection.FORWARD, 26 | PROPERTY_HINT_ENUM, ",".join(_Common.ANIMATION_DIRECTIONS_NAMES), PROPERTY_USAGE_DEFAULT), 27 | _Options.create_option(_Options.DEFAULT_ANIMATION_REPEAT_COUNT, 0, 28 | PROPERTY_HINT_RANGE, "0,,1,or_greater", PROPERTY_USAGE_DEFAULT), 29 | _Options.create_option(_Options.AUTOPLAY_ANIMATION_NAME, "", 30 | PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), 31 | _Options.create_option(_Options.ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED, false, 32 | PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), 33 | ] 34 | var __name: String 35 | var __resource_type: StringName 36 | var __save_extension: String 37 | 38 | func _init( 39 | name: String, 40 | resource_type: String, 41 | save_extension: String, 42 | options: Array[Dictionary] = []) -> void: 43 | __name = name 44 | __resource_type = resource_type 45 | __save_extension = save_extension 46 | __options.append_array(options) 47 | 48 | func get_name() -> String: 49 | return __name 50 | 51 | func get_resource_type() -> StringName: 52 | return __resource_type 53 | 54 | func get_save_extension() -> String: 55 | return __save_extension 56 | 57 | func get_options() -> Array[Dictionary]: 58 | return __options 59 | 60 | func import( 61 | source_file_path: String, 62 | atlas: Texture2D, 63 | sprite_sheet: _Common.SpriteSheetInfo, 64 | animation_library: _Common.AnimationLibraryInfo, 65 | options: Dictionary, 66 | save_path: String) -> ImportResult: 67 | assert(false, "This method is abstract and must be overriden.") 68 | var result: ImportResult = ImportResult.new() 69 | result.fail(ERR_UNCONFIGURED, "This method is abstract and must be overriden.") 70 | return result 71 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/import/_node.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_.gd" 3 | 4 | func _init( 5 | name: String, 6 | resource_type: String, 7 | save_extension: String, 8 | options: Array[Dictionary] = [] 9 | ) -> void: 10 | options.append_array([ 11 | _Options.create_option(_Options.ROOT_NODE_NAME, "", 12 | PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), 13 | ]) 14 | super(name, resource_type, save_extension, options) 15 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/import/_node_with_animation_player.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_node.gd" 3 | 4 | class TrackFrame: 5 | extends RefCounted 6 | var duration: float 7 | var value: Variant 8 | func _init(duration: float, value: Variant) -> void: 9 | self.duration = duration 10 | self.value = value 11 | 12 | static func _create_animation_player( 13 | animation_library_info: _Common.AnimationLibraryInfo, 14 | track_value_getters_by_property_path: Dictionary 15 | ) -> AnimationPlayer: 16 | var animation_player: AnimationPlayer = AnimationPlayer.new() 17 | animation_player.name = &"AnimationPlayer" 18 | var animation_library: AnimationLibrary = AnimationLibrary.new() 19 | 20 | for animation_info in animation_library_info.animations: 21 | var animation: Animation = Animation.new() 22 | var frames: Array[_Common.FrameInfo] = animation_info.get_flatten_frames() 23 | for property_path in track_value_getters_by_property_path.keys(): 24 | __create_track(animation, property_path, 25 | frames, track_value_getters_by_property_path[property_path]) 26 | 27 | animation.length = 0 28 | for frame in frames: 29 | animation.length += frame.duration 30 | 31 | animation.loop_mode = Animation.LOOP_LINEAR if animation_info.repeat_count == 0 else Animation.LOOP_NONE 32 | animation_library.add_animation(animation_info.name, animation) 33 | animation_player.add_animation_library("", animation_library) 34 | 35 | if animation_library_info.autoplay_index >= 0: 36 | animation_player.autoplay = animation_library_info \ 37 | .animations[animation_library_info.autoplay_index].name 38 | 39 | return animation_player 40 | 41 | static func __create_track( 42 | animation: Animation, 43 | property_path: NodePath, 44 | frames: Array[_Common.FrameInfo], 45 | track_value_getter: Callable # func(f: FrameModel) -> Variant for each f in frames 46 | ) -> int: 47 | var track_index = animation.add_track(Animation.TYPE_VALUE) 48 | animation.track_set_path(track_index, property_path) 49 | animation.value_track_set_update_mode(track_index, Animation.UPDATE_DISCRETE) 50 | animation.track_set_interpolation_loop_wrap(track_index, false) 51 | animation.track_set_interpolation_type(track_index, Animation.INTERPOLATION_NEAREST) 52 | var track_frames = frames.map(func (frame: _Common.FrameInfo): 53 | return TrackFrame.new(frame.duration, track_value_getter.call(frame))) 54 | 55 | var transition: float = 1 56 | var track_length: float = 0 57 | var previous_track_frame: TrackFrame = null 58 | for track_frame in track_frames: 59 | if previous_track_frame == null or track_frame.value != previous_track_frame.value: 60 | animation.track_insert_key(track_index, track_length, track_frame.value, transition) 61 | previous_track_frame = track_frame 62 | track_length += track_frame.duration 63 | 64 | return track_index 65 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/import/_sprite_with_animation_player.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_node_with_animation_player.gd" 3 | 4 | func _init( 5 | name: String, 6 | resource_type: String, 7 | save_extension: String, 8 | options: Array[Dictionary] = [] 9 | ) -> void: 10 | options.append_array([ 11 | _Options.create_option(_Options.ANIMATION_STRATEGY, _Common.AnimationStrategy.SPRITE_REGION_AND_OFFSET, 12 | PROPERTY_HINT_ENUM, ",".join(_Common.ANIMATION_STRATEGIES_NAMES), PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 13 | _Options.create_option(_Options.SPRITE_CENTERED, false, 14 | PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), 15 | ]) 16 | super(name, resource_type, save_extension, options) 17 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/import/animated_sprite_2d.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_node.gd" 3 | 4 | const _SpriteFramesImporter = preload("sprite_frames.gd") 5 | 6 | var __sprite_frames_importer: _SpriteFramesImporter 7 | 8 | func _init() -> void: 9 | super("AnimatedSprite2D", "PackedScene", "scn") 10 | __sprite_frames_importer = _SpriteFramesImporter.new() 11 | 12 | func import( 13 | res_source_file_path: String, 14 | atlas: Texture2D, 15 | sprite_sheet: _Common.SpriteSheetInfo, 16 | animation_library: _Common.AnimationLibraryInfo, 17 | options: Dictionary, 18 | save_path: String 19 | ) -> ImportResult: 20 | var result: ImportResult = ImportResult.new() 21 | 22 | var sprite_frames_import_result: ImportResult = __sprite_frames_importer \ 23 | .import(res_source_file_path, atlas, sprite_sheet, animation_library, options, save_path) 24 | if sprite_frames_import_result.error: 25 | return sprite_frames_import_result 26 | var sprite_frames: SpriteFrames = sprite_frames_import_result.resource 27 | 28 | var animated_sprite: AnimatedSprite2D = AnimatedSprite2D.new() 29 | var node_name: String = options[_Options.ROOT_NODE_NAME].strip_edges() 30 | animated_sprite.name = res_source_file_path.get_file().get_basename() \ 31 | if node_name.is_empty() else node_name 32 | animated_sprite.sprite_frames = sprite_frames 33 | 34 | if animation_library.autoplay_index >= 0: 35 | if animation_library.autoplay_index >= animation_library.animations.size(): 36 | result.fail(ERR_INVALID_DATA, "Autoplay animation index overflow") 37 | return result 38 | animated_sprite.autoplay = animation_library \ 39 | .animations[animation_library.autoplay_index].name 40 | 41 | var packed_scene: PackedScene = PackedScene.new() 42 | packed_scene.pack(animated_sprite) 43 | result.success(packed_scene, 44 | ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES) 45 | return result 46 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/import/animated_sprite_3d.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_node.gd" 3 | 4 | const _SpriteFramesImporter = preload("sprite_frames.gd") 5 | 6 | var __sprite_frames_importer: _SpriteFramesImporter 7 | 8 | func _init() -> void: 9 | super("AnimatedSprite3D", "PackedScene", "scn") 10 | __sprite_frames_importer = _SpriteFramesImporter.new() 11 | 12 | func import( 13 | res_source_file_path: String, 14 | atlas: Texture2D, 15 | sprite_sheet: _Common.SpriteSheetInfo, 16 | animation_library: _Common.AnimationLibraryInfo, 17 | options: Dictionary, 18 | save_path: String 19 | ) -> ImportResult: 20 | var result: ImportResult = ImportResult.new() 21 | 22 | var sprite_frames_import_result: ImportResult = __sprite_frames_importer \ 23 | .import(res_source_file_path, atlas, sprite_sheet, animation_library, options, save_path) 24 | if sprite_frames_import_result.error: 25 | return sprite_frames_import_result 26 | var sprite_frames: SpriteFrames = sprite_frames_import_result.resource 27 | 28 | var animated_sprite: AnimatedSprite3D = AnimatedSprite3D.new() 29 | var node_name: String = options[_Options.ROOT_NODE_NAME].strip_edges() 30 | animated_sprite.name = res_source_file_path.get_file().get_basename() \ 31 | if node_name.is_empty() else node_name 32 | animated_sprite.sprite_frames = sprite_frames 33 | 34 | if animation_library.autoplay_index >= 0: 35 | animated_sprite.autoplay = animation_library \ 36 | .animations[animation_library.autoplay_index].name 37 | 38 | var packed_scene: PackedScene = PackedScene.new() 39 | packed_scene.pack(animated_sprite) 40 | result.success(packed_scene, 41 | ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES) 42 | return result 43 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/import/sprite_2d_with_animation_player.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_sprite_with_animation_player.gd" 3 | 4 | func _init() -> void: 5 | super("Sprite2D with AnimationPlayer", "PackedScene", "scn") 6 | 7 | func import( 8 | res_source_file_path: String, 9 | atlas: Texture2D, 10 | sprite_sheet: _Common.SpriteSheetInfo, 11 | animation_library: _Common.AnimationLibraryInfo, 12 | options: Dictionary, 13 | save_path: String 14 | ) -> ImportResult: 15 | var result: ImportResult = ImportResult.new() 16 | 17 | var sprite_size: Vector2i = sprite_sheet.source_image_size 18 | 19 | var sprite: Sprite2D = Sprite2D.new() 20 | var node_name: String = options[_Options.ROOT_NODE_NAME].strip_edges() 21 | sprite.name = res_source_file_path.get_file().get_basename() \ 22 | if node_name.is_empty() else node_name 23 | sprite.centered = options[_Options.SPRITE_CENTERED] 24 | 25 | var filter_clip_enabled: bool = options[_Options.ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED] 26 | 27 | var animation_player: AnimationPlayer 28 | match options[_Options.ANIMATION_STRATEGY]: 29 | 30 | _Common.AnimationStrategy.SPRITE_REGION_AND_OFFSET: 31 | sprite.texture = atlas 32 | sprite.region_enabled = true 33 | animation_player = _create_animation_player(animation_library, { 34 | ".:offset": func(frame: _Common.FrameInfo) -> Vector2: 35 | return \ 36 | Vector2(frame.sprite.offset) - 0.5 * (frame.sprite.region.size - sprite_size) \ 37 | if sprite.centered else \ 38 | frame.sprite.offset, 39 | ".:region_rect": func(frame: _Common.FrameInfo) -> Rect2: 40 | return Rect2(frame.sprite.region) }) 41 | 42 | _Common.AnimationStrategy.SINGLE_ATLAS_TEXTURE_REGION_AND_MARGIN: 43 | var atlas_texture: AtlasTexture = AtlasTexture.new() 44 | atlas_texture.filter_clip = filter_clip_enabled 45 | atlas_texture.resource_local_to_scene = true 46 | atlas_texture.atlas = atlas 47 | atlas_texture.region = Rect2(0, 0, 1, 1) 48 | atlas_texture.margin = Rect2(2, 2, 0, 0) 49 | sprite.texture = atlas_texture 50 | animation_player = _create_animation_player(animation_library, { 51 | ".:texture:margin": func(frame: _Common.FrameInfo) -> Rect2: 52 | return \ 53 | Rect2(frame.sprite.offset, 54 | sprite_size - frame.sprite.region.size) \ 55 | if frame.sprite.region.has_area() else \ 56 | Rect2(2, 2, 0, 0), 57 | ".:texture:region": func(frame: _Common.FrameInfo) -> Rect2: 58 | return Rect2(frame.sprite.region) if frame.sprite.region.has_area() else Rect2(0, 0, 1, 1) }) 59 | 60 | _Common.AnimationStrategy.MULTIPLE_ATLAS_TEXTURES_INSTANCES: 61 | var atlas_textures: Array[AtlasTexture] 62 | var empty_atlas_texture: AtlasTexture = AtlasTexture.new() 63 | empty_atlas_texture.filter_clip = filter_clip_enabled 64 | empty_atlas_texture.atlas = atlas 65 | empty_atlas_texture.region = Rect2(0, 0, 1, 1) 66 | empty_atlas_texture.margin = Rect2(2, 2, 0, 0) 67 | animation_player = _create_animation_player(animation_library, { 68 | ".:texture": func(frame: _Common.FrameInfo) -> Texture2D: 69 | if not frame.sprite.region.has_area(): 70 | return empty_atlas_texture 71 | var region: Rect2 = frame.sprite.region 72 | var margin: Rect2 = Rect2( 73 | frame.sprite.offset, 74 | sprite_size - frame.sprite.region.size) 75 | var equivalent_atlas_textures: Array = atlas_textures.filter( 76 | func(t: AtlasTexture) -> bool: return t.margin == margin and t.region == region) 77 | if not equivalent_atlas_textures.is_empty(): 78 | return equivalent_atlas_textures.front() 79 | var atlas_texture: AtlasTexture = AtlasTexture.new() 80 | atlas_texture.atlas = atlas 81 | atlas_texture.filter_clip = filter_clip_enabled 82 | atlas_texture.region = region 83 | atlas_texture.margin = margin 84 | atlas_textures.append(atlas_texture) 85 | return atlas_texture}) 86 | 87 | sprite.add_child(animation_player) 88 | animation_player.owner = sprite 89 | 90 | var packed_scene: PackedScene = PackedScene.new() 91 | packed_scene.pack(sprite) 92 | result.success(packed_scene, 93 | ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES) 94 | return result 95 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/import/sprite_3d_with_animation_player.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_sprite_with_animation_player.gd" 3 | 4 | func _init() -> void: 5 | super("Sprite3D with AnimationPlayer", "PackedScene", "scn") 6 | 7 | func import( 8 | res_source_file_path: String, 9 | atlas: Texture2D, 10 | sprite_sheet: _Common.SpriteSheetInfo, 11 | animation_library: _Common.AnimationLibraryInfo, 12 | options: Dictionary, 13 | save_path: String 14 | ) -> ImportResult: 15 | var result: ImportResult = ImportResult.new() 16 | 17 | var sprite_size: Vector2i = sprite_sheet.source_image_size 18 | 19 | var sprite: Sprite3D = Sprite3D.new() 20 | var node_name: String = options[_Options.ROOT_NODE_NAME].strip_edges() 21 | sprite.name = res_source_file_path.get_file().get_basename() \ 22 | if node_name.is_empty() else node_name 23 | sprite.centered = options[_Options.SPRITE_CENTERED] 24 | 25 | var filter_clip_enabled: bool = options[_Options.ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED] 26 | 27 | var animation_player: AnimationPlayer 28 | match options[_Options.ANIMATION_STRATEGY]: 29 | 30 | _Common.AnimationStrategy.SPRITE_REGION_AND_OFFSET: 31 | sprite.texture = atlas 32 | sprite.region_enabled = true 33 | animation_player = _create_animation_player(animation_library, { 34 | ".:offset": func(frame: _Common.FrameInfo) -> Vector2: 35 | return Vector2( # spatial sprite offset (the Y-axis is Up-directed) 36 | frame.sprite.offset.x, 37 | sprite_size.y - frame.sptite.offset.y - 38 | frame.sprite.region.size.y) + \ 39 | # add center correction 40 | ((Vector2(frame.sprite.region.size - sprite_size) * 0.5) 41 | if sprite.centered else Vector2.ZERO), 42 | ".:region_rect": func(frame: _Common.FrameInfo) -> Rect2: 43 | return Rect2(frame.sprite.region) }) 44 | 45 | _Common.AnimationStrategy.SINGLE_ATLAS_TEXTURE_REGION_AND_MARGIN: 46 | var atlas_texture: AtlasTexture = AtlasTexture.new() 47 | atlas_texture.filter_clip = filter_clip_enabled 48 | atlas_texture.resource_local_to_scene = true 49 | atlas_texture.atlas = atlas 50 | atlas_texture.region = Rect2(0, 0, 1, 1) 51 | atlas_texture.margin = Rect2(2, 2, 0, 0) 52 | sprite.texture = atlas_texture 53 | animation_player = _create_animation_player(animation_library, { 54 | ".:texture:margin": func(frame: _Common.FrameInfo) -> Rect2: 55 | return \ 56 | Rect2(frame.sprite.offset, 57 | sprite_size - frame.sprite.region.size) \ 58 | if frame.sprite.region.has_area() else \ 59 | Rect2(2, 2, 0, 0), 60 | ".:texture:region": func(frame: _Common.FrameInfo) -> Rect2: 61 | return Rect2(frame.sprite.region) if frame.sprite.region.has_area() else Rect2(0, 0, 1, 1) }) 62 | 63 | _Common.AnimationStrategy.MULTIPLE_ATLAS_TEXTURES_INSTANCES: 64 | var atlas_textures: Array[AtlasTexture] 65 | var empty_atlas_texture: AtlasTexture = AtlasTexture.new() 66 | empty_atlas_texture.filter_clip = filter_clip_enabled 67 | empty_atlas_texture.atlas = atlas 68 | empty_atlas_texture.region = Rect2(0, 0, 1, 1) 69 | empty_atlas_texture.margin = Rect2(2, 2, 0, 0) 70 | animation_player = _create_animation_player(animation_library, { 71 | ".:texture": func(frame: _Common.FrameInfo) -> Texture2D: 72 | if not frame.sprite.region.has_area(): 73 | return empty_atlas_texture 74 | var region: Rect2 = frame.sprite.region 75 | var margin: Rect2 = Rect2( 76 | frame.sprite.offset, 77 | sprite_size - frame.sprite.region.size) 78 | var equivalent_atlas_textures: Array = atlas_textures.filter( 79 | func(t: AtlasTexture) -> bool: return t.margin == margin and t.region == region) 80 | if not equivalent_atlas_textures.is_empty(): 81 | return equivalent_atlas_textures.front() 82 | var atlas_texture: AtlasTexture = AtlasTexture.new() 83 | atlas_texture.atlas = atlas 84 | atlas_texture.filter_clip = filter_clip_enabled 85 | atlas_texture.region = region 86 | atlas_texture.margin = margin 87 | atlas_textures.append(atlas_texture) 88 | return atlas_texture}) 89 | 90 | sprite.add_child(animation_player) 91 | animation_player.owner = sprite 92 | 93 | var packed_scene: PackedScene = PackedScene.new() 94 | packed_scene.pack(sprite) 95 | result.success(packed_scene, 96 | ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES) 97 | return result 98 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/import/sprite_frames.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_.gd" 3 | 4 | func _init() -> void: super("SpriteFrames", "SpriteFrames", "res") 5 | 6 | func import( 7 | res_source_file_path: String, 8 | atlas: Texture2D, 9 | sprite_sheet: _Common.SpriteSheetInfo, 10 | animation_library: _Common.AnimationLibraryInfo, 11 | options: Dictionary, 12 | save_path: String 13 | ) -> ImportResult: 14 | var result: ImportResult = ImportResult.new() 15 | 16 | var sprite_frames: SpriteFrames = SpriteFrames.new() 17 | for animation_name in sprite_frames.get_animation_names(): 18 | sprite_frames.remove_animation(animation_name) 19 | 20 | var filter_clip_enabled: bool = options[_Options.ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED] 21 | var atlas_textures: Array[AtlasTexture] 22 | var empty_atlas_texture: AtlasTexture 23 | for animation in animation_library.animations: 24 | sprite_frames.add_animation(animation.name) 25 | sprite_frames.set_animation_loop(animation.name, animation.repeat_count == 0) 26 | sprite_frames.set_animation_speed(animation.name, 1) 27 | var previous_texture: Texture2D 28 | for frame in animation.get_flatten_frames(): 29 | var atlas_texture: AtlasTexture 30 | if frame.sprite.region.has_area(): 31 | var region: Rect2 = frame.sprite.region 32 | var margin: Rect2 = Rect2( 33 | frame.sprite.offset, 34 | sprite_sheet.source_image_size - frame.sprite.region.size) 35 | var equivalent_atlas_textures: Array = atlas_textures.filter( 36 | func(t: AtlasTexture) -> bool: return t.margin == margin and t.region == region) 37 | if not equivalent_atlas_textures.is_empty(): 38 | atlas_texture = equivalent_atlas_textures.front() 39 | if atlas_texture == null: 40 | atlas_texture = AtlasTexture.new() 41 | atlas_texture.filter_clip = filter_clip_enabled 42 | atlas_texture.atlas = atlas 43 | atlas_texture.region = region 44 | atlas_texture.margin = margin 45 | atlas_textures.push_back(atlas_texture) 46 | else: 47 | if empty_atlas_texture == null: 48 | empty_atlas_texture = AtlasTexture.new() 49 | empty_atlas_texture.filter_clip = filter_clip_enabled 50 | empty_atlas_texture.atlas = atlas 51 | empty_atlas_texture.region = Rect2(0, 0, 1, 1) 52 | empty_atlas_texture.margin = Rect2(2, 2, 0, 0) 53 | atlas_texture = empty_atlas_texture 54 | if atlas_texture == previous_texture: 55 | var last_frame_index: int = sprite_frames.get_frame_count(animation.name) - 1 56 | sprite_frames.set_frame(animation.name, last_frame_index, atlas_texture, 57 | sprite_frames.get_frame_duration(animation.name, last_frame_index) + frame.duration) 58 | continue 59 | sprite_frames.add_frame(animation.name, atlas_texture, frame.duration) 60 | previous_texture = atlas_texture 61 | 62 | result.success(sprite_frames, 63 | ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES) 64 | return result 65 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/import/sprite_sheet.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_.gd" 3 | 4 | const __ANIMATION_MERGE_EQUAL_CONSEQUENT_FRAMES_OPTION: StringName = &"animation/merge_equal_sonsequent_frames" 5 | const __ANIMATION_FLATTEN_REPETITION_OPTION: StringName = &"animation/flatten_repetition" 6 | 7 | func _init() -> void: super("Sprite sheet (PortableCompressedTexture2D)", "PortableCompressedTexture2D", "res", [ 8 | _Options.create_option(__ANIMATION_MERGE_EQUAL_CONSEQUENT_FRAMES_OPTION, true, 9 | PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), 10 | _Options.create_option(__ANIMATION_FLATTEN_REPETITION_OPTION, true, 11 | PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT), 12 | ]) 13 | 14 | func import( 15 | res_source_file_path: String, 16 | atlas: Texture2D, 17 | sprite_sheet: _Common.SpriteSheetInfo, 18 | animation_library: _Common.AnimationLibraryInfo, 19 | options: Dictionary, 20 | save_path: String 21 | ) -> ImportResult: 22 | var result: ImportResult = ImportResult.new() 23 | 24 | var unique_indixes_by_sprites: Dictionary 25 | var unique_sprite_index: int = 0 26 | var sprites: Array[Dictionary] 27 | for sprite in sprite_sheet.sprites: 28 | if not unique_indixes_by_sprites.has(sprite): 29 | unique_indixes_by_sprites[sprite] = unique_sprite_index 30 | sprites.push_back({ 31 | region = sprite.region, 32 | offset = sprite.offset 33 | }) 34 | unique_sprite_index += 1 35 | 36 | var flatten_animation_repetition: bool = options[__ANIMATION_FLATTEN_REPETITION_OPTION] 37 | var merge_equal_consequent_frames: bool = options[__ANIMATION_MERGE_EQUAL_CONSEQUENT_FRAMES_OPTION] 38 | var animations: Array[Dictionary] 39 | for animation in animation_library.animations: 40 | var frames_data: Array[Dictionary] 41 | var frames: Array[_Common.FrameInfo] = \ 42 | animation.get_flatten_frames() \ 43 | if flatten_animation_repetition else \ 44 | animation.frames 45 | var previous_sprite_index: int = -1 46 | for frame in frames: 47 | var sprite_index: int = unique_indixes_by_sprites[frame.sprite] 48 | if merge_equal_consequent_frames and sprite_index == previous_sprite_index: 49 | frames_data.back().duration += frame.duration 50 | else: 51 | frames_data.push_back({ 52 | sprite_index = sprite_index, 53 | duration = frame.duration, 54 | }) 55 | previous_sprite_index = sprite_index 56 | animations.push_back({ 57 | name = animation.name, 58 | direction = 59 | _Common.AnimationDirection.FORWARD 60 | if flatten_animation_repetition else 61 | animation.direction, 62 | repeat_count = 63 | mini(1, animation.repeat_count) 64 | if flatten_animation_repetition else 65 | animation.repeat_count, 66 | frames = frames_data, 67 | }) 68 | 69 | var portable_compressed_texture: PortableCompressedTexture2D = PortableCompressedTexture2D.new() 70 | portable_compressed_texture.create_from_image( 71 | atlas.get_image(), 72 | PortableCompressedTexture2D.COMPRESSION_MODE_LOSSLESS) 73 | portable_compressed_texture.set_meta(&"source_image_size", sprite_sheet.source_image_size); 74 | portable_compressed_texture.set_meta(&"sprites", sprites); 75 | portable_compressed_texture.set_meta(&"animations", animations); 76 | portable_compressed_texture.set_meta(&"source_image_size", sprite_sheet.source_image_size); 77 | portable_compressed_texture.set_meta(&"autoplay_index", animation_library.autoplay_index); 78 | 79 | result.success(portable_compressed_texture, ResourceSaver.FLAG_COMPRESS) 80 | return result 81 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/import/texture_rect_with_animation_player.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_node_with_animation_player.gd" 3 | 4 | const ANIMATION_STRATEGIES_NAMES: PackedStringArray = [ 5 | "Animate single atlas texture's region and margin", 6 | "Animate multiple atlas textures instances", 7 | ] 8 | enum AnimationStrategy { 9 | SINGLE_ATLAS_TEXTURE_REGION_AND_MARGIN = 1, 10 | MULTIPLE_ATLAS_TEXTURES_INSTANCES = 2 11 | } 12 | 13 | func _init() -> void: 14 | super("TextureRect with AnimationPlayer", "PackedScene", "scn", [ 15 | _Options.create_option(_Options.ANIMATION_STRATEGY, AnimationStrategy.SINGLE_ATLAS_TEXTURE_REGION_AND_MARGIN, 16 | PROPERTY_HINT_ENUM, ",".join(ANIMATION_STRATEGIES_NAMES), PROPERTY_USAGE_DEFAULT), 17 | ]) 18 | 19 | func import( 20 | res_source_file_path: String, 21 | atlas: Texture2D, 22 | sprite_sheet: _Common.SpriteSheetInfo, 23 | animation_library: _Common.AnimationLibraryInfo, 24 | options: Dictionary, 25 | save_path: String 26 | ) -> ImportResult: 27 | var result: ImportResult = ImportResult.new() 28 | 29 | var texture_rect: TextureRect = TextureRect.new() 30 | var node_name: String = options[_Options.ROOT_NODE_NAME].strip_edges() 31 | texture_rect.name = res_source_file_path.get_file().get_basename() \ 32 | if node_name.is_empty() else node_name 33 | texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE 34 | var sprite_size: Vector2i = sprite_sheet.source_image_size 35 | texture_rect.size = sprite_size 36 | 37 | var filter_clip_enabled: bool = options[_Options.ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED] 38 | 39 | var animation_player: AnimationPlayer 40 | match options[_Options.ANIMATION_STRATEGY]: 41 | 42 | AnimationStrategy.SINGLE_ATLAS_TEXTURE_REGION_AND_MARGIN: 43 | var atlas_texture: AtlasTexture = AtlasTexture.new() 44 | atlas_texture.atlas = atlas 45 | atlas_texture.filter_clip = filter_clip_enabled 46 | atlas_texture.resource_local_to_scene = true 47 | atlas_texture.region = Rect2(0, 0, 1, 1) 48 | atlas_texture.margin = Rect2(2, 2, 0, 0) 49 | texture_rect.texture = atlas_texture 50 | 51 | animation_player = _create_animation_player(animation_library, { 52 | ".:texture:margin": func(frame: _Common.FrameInfo) -> Rect2: 53 | return \ 54 | Rect2(frame.sprite.offset, 55 | sprite_size - frame.sprite.region.size) \ 56 | if frame.sprite.region.has_area() else \ 57 | Rect2(2, 2, 0, 0), 58 | ".:texture:region" : func(frame: _Common.FrameInfo) -> Rect2: 59 | return \ 60 | Rect2(frame.sprite.region) \ 61 | if frame.sprite.region.has_area() else \ 62 | Rect2(0, 0, 1, 1) }) 63 | 64 | AnimationStrategy.MULTIPLE_ATLAS_TEXTURES_INSTANCES: 65 | var atlas_textures: Array[AtlasTexture] 66 | var empty_atlas_texture: AtlasTexture = AtlasTexture.new() 67 | empty_atlas_texture.filter_clip = filter_clip_enabled 68 | empty_atlas_texture.atlas = atlas 69 | empty_atlas_texture.region = Rect2(0, 0, 1, 1) 70 | empty_atlas_texture.margin = Rect2(2, 2, 0, 0) 71 | animation_player = _create_animation_player(animation_library, { 72 | ".:texture": func(frame: _Common.FrameInfo) -> Texture2D: 73 | if not frame.sprite.region.has_area(): 74 | return empty_atlas_texture 75 | var region: Rect2 = frame.sprite.region 76 | var margin: Rect2 = Rect2( 77 | frame.sprite.offset, 78 | sprite_size - frame.sprite.region.size) 79 | var equivalent_atlas_textures: Array = atlas_textures.filter( 80 | func(t: AtlasTexture) -> bool: return t.margin == margin and t.region == region) 81 | if not equivalent_atlas_textures.is_empty(): 82 | return equivalent_atlas_textures.front() 83 | var atlas_texture: AtlasTexture = AtlasTexture.new() 84 | atlas_texture.atlas = atlas 85 | atlas_texture.filter_clip = filter_clip_enabled 86 | atlas_texture.region = region 87 | atlas_texture.margin = margin 88 | atlas_textures.append(atlas_texture) 89 | return atlas_texture}) 90 | 91 | texture_rect.add_child(animation_player) 92 | animation_player.owner = texture_rect 93 | 94 | var packed_scene: PackedScene = PackedScene.new() 95 | packed_scene.pack(texture_rect) 96 | result.success(packed_scene, 97 | ResourceSaver.FLAG_COMPRESS | ResourceSaver.FLAG_BUNDLE_RESOURCES) 98 | return result 99 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/options.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | 3 | const _Common = preload("common.gd") 4 | const __empty_callable: Callable = Callable() 5 | 6 | const SPRITE_SHEET_LAYOUT: StringName = &"sprite_sheet/layout" 7 | const MAX_CELLS_IN_STRIP: StringName = &"sprite_sheet/max_cells_in_strip" 8 | const EDGES_ARTIFACTS_AVOIDANCE_METHOD: StringName = &"sprite_sheet/edges_artifacts_avoidance_method" 9 | const SPRITES_SURROUNDING_COLOR: StringName = &"sprite_sheet/sprites_surrounding_color" 10 | const TRIM_SPRITES_TO_OVERALL_MIN_SIZE: StringName = &"sprite_sheet/trim_sprites_to_overall_min_size" 11 | const COLLAPSE_TRANSPARENT_SPRITES: StringName = &"sprite_sheet/collapse_transparent_sprites" 12 | const MERGE_DUPLICATED_SPRITES: StringName = &"sprite_sheet/merge_duplicated_sprites" 13 | const DEFAULT_ANIMATION_NAME: StringName = &"animation/default/name" 14 | const DEFAULT_ANIMATION_DIRECTION: StringName = &"animation/default/direction" 15 | const DEFAULT_ANIMATION_REPEAT_COUNT: StringName = &"animation/default/repeat_count" 16 | const AUTOPLAY_ANIMATION_NAME: StringName = &"animation/autoplay_name" 17 | const ROOT_NODE_NAME: StringName = &"root_node_name" 18 | const ANIMATION_STRATEGY: StringName = &"sprite/animation_strategy" 19 | const SPRITE_CENTERED: StringName = &"sprite/centered" 20 | const ATLAS_TEXTURES_REGION_FILTER_CLIP_ENABLED: StringName = &"atlas_textures/region_filter_clip_enabled" 21 | const MIDDLE_IMPORT_SCRIPT_PATH: StringName = &"middle_import_script" 22 | const POST_IMPORT_SCRIPT_PATH: StringName = &"post_import_script" 23 | 24 | static func create_option( 25 | name: StringName, 26 | default_value: Variant, 27 | property_hint: PropertyHint = PROPERTY_HINT_NONE, 28 | hint_string: String = "", 29 | usage: PropertyUsageFlags = PROPERTY_USAGE_NONE, 30 | get_is_visible: Callable = __empty_callable 31 | ) -> Dictionary: 32 | var option_data: Dictionary = { 33 | name = name, 34 | default_value = default_value, 35 | } 36 | if hint_string: option_data[&"hint_string"] = hint_string 37 | if property_hint: option_data[&"property_hint"] = property_hint 38 | if usage: option_data[&"usage"] = usage 39 | if get_is_visible != __empty_callable: 40 | option_data[&"get_is_visible"] = get_is_visible 41 | return option_data 42 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Importality" 4 | description="Universal raster graphics and animations importers pack" 5 | author="Nikolay Lebedev aka nklbdev" 6 | version="0.3.0" 7 | script="editor_plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/rect_packer.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | # This code is taken from: https://github.com/semibran/pack/blob/master/lib/pack.js 3 | # Copyright (c) 2018 Brandon Semilla (MIT License) - original author 4 | # Copyright (c) 2023 Nikolay Lebedev (MIT License) - porting to gdscript, refactoring and optimization 5 | 6 | const _Result = preload("result.gd").Class 7 | const _Common = preload("common.gd") 8 | 9 | const __WHITESPACE_WEIGHT: float = 1 10 | const __SIDE_LENGTH_WEIGHT: float = 10 11 | 12 | class RectPackingResult: 13 | extends _Result 14 | # Total size of the entire layout of rectangles. 15 | var bounds: Vector2i 16 | # Computed positions of the input rectangles 17 | # in the same order as their sizes were passed in. 18 | var rects_positions: Array[Vector2i] 19 | func _get_result_type_description() -> String: 20 | return "Rect packing" 21 | func success(bounds: Vector2i, rects_positions: Array[Vector2i]) -> void: 22 | _success() 23 | self.bounds = bounds 24 | self.rects_positions = rects_positions 25 | 26 | static func __add_rect_to_cache(rect: Rect2i, cache: Dictionary, cache_grid_size: Vector2i) -> void: 27 | var left_top_cell: Vector2i = rect.position / cache_grid_size 28 | var right_bottom_cell: Vector2i = rect.end / cache_grid_size + (rect.end % cache_grid_size).sign() 29 | for y in range(left_top_cell.y, right_bottom_cell.y): 30 | for x in range(left_top_cell.x, right_bottom_cell.x): 31 | var cell: Vector2i = Vector2i(x, y) 32 | if cache.has(cell): 33 | cache[cell].push_back(rect) 34 | else: 35 | cache[cell] = [rect] as Array[Rect2i] 36 | 37 | const __empty_rect_array: Array[Rect2i] = [] 38 | static func __has_intersection(rect: Rect2i, cache: Dictionary, cache_grid_size: Vector2i) -> bool: 39 | var left_top_cell: Vector2i = rect.position / cache_grid_size 40 | var right_bottom_cell: Vector2i = rect.end / cache_grid_size + (rect.end % cache_grid_size).sign() 41 | for y in range(left_top_cell.y, right_bottom_cell.y): 42 | for x in range(left_top_cell.x, right_bottom_cell.x): 43 | for cached_rect in cache.get(Vector2i(x, y), __empty_rect_array): 44 | if cached_rect.intersects(rect): 45 | return true 46 | return false 47 | 48 | # The function takes an array of rectangle sizes as input and compactly packs them. 49 | static func pack(rects_sizes: Array[Vector2i]) -> RectPackingResult: 50 | var result: RectPackingResult = RectPackingResult.new() 51 | var rects_count: int = rects_sizes.size() 52 | if rects_count == 0: 53 | result.success(Vector2i.ZERO, []) 54 | return result 55 | var rects_positions: Array[Vector2i] 56 | rects_positions.resize(rects_count) 57 | var min_area: int 58 | var rect_sizes_sum: Vector2i 59 | for size in rects_sizes: 60 | if size.x < 0 or size.y < 0: 61 | result.fail(ERR_INVALID_DATA, "Negative rect size found") 62 | return result 63 | min_area += size.x * size.y 64 | rect_sizes_sum += size 65 | if min_area == 0: 66 | result.success(Vector2i.ZERO, rects_positions) 67 | return result 68 | var average_rect_size: Vector2 = Vector2(rect_sizes_sum) / rects_count 69 | var rect_cache_grid_size: Vector2i = average_rect_size.ceil() * 2 70 | var average_squared_rect_side_length: float = sqrt(min_area / float(rects_count)) 71 | 72 | var rect_cache: Dictionary 73 | 74 | var possible_bounds_side_length: int = ceili(sqrt(rects_count)) 75 | nearest_po2(possible_bounds_side_length) 76 | 77 | var rects_order_arr: Array = PackedInt32Array(range(0, rects_count)) 78 | rects_order_arr.sort_custom(func(a: int, b: int) -> bool: 79 | return rects_sizes[a].x * rects_sizes[a].y > rects_sizes[b].x * rects_sizes[b].y) 80 | var rects_order: PackedInt32Array = PackedInt32Array(rects_order_arr) 81 | 82 | var bounds: Vector2i = rects_sizes[rects_order[0]] 83 | var utilized_area: int = bounds.x * bounds.y 84 | 85 | var splits_by_axis: Array[PackedInt32Array] = [[0, bounds.x], [0, bounds.y]] 86 | __add_rect_to_cache(Rect2i(Vector2i.ZERO, rects_sizes[rects_order[0]]), rect_cache, rect_cache_grid_size) 87 | 88 | for rect_index in range(1, rects_count): # skip first rect at (0, 0) 89 | var ordered_rect_index: int = rects_order[rect_index] 90 | var rect: Rect2i = Rect2i(Vector2i.ZERO, rects_sizes[ordered_rect_index]) 91 | var rect_area: int = rect.get_area() 92 | if rect_area == 0: 93 | continue 94 | utilized_area += rect_area 95 | 96 | var best_score: float = INF 97 | var best_new_bounds: Vector2i = bounds 98 | for landing_rect_index in rect_index: 99 | var ordered_landing_rect_index: int = rects_order[landing_rect_index] 100 | var landing_rect: Rect2i = Rect2i( 101 | rects_positions[ordered_landing_rect_index], 102 | rects_sizes[ordered_landing_rect_index]) 103 | for split_axis_index in 2: 104 | var orthogonal_asis_index: int = (split_axis_index + 1) % 2 105 | var splits: PackedInt32Array = splits_by_axis[split_axis_index] 106 | rect.position[orthogonal_asis_index] = landing_rect.end[orthogonal_asis_index] 107 | for split_index in range( 108 | splits.bsearch(landing_rect.position[split_axis_index]), 109 | splits.bsearch(landing_rect.end[split_axis_index])): 110 | rect.position[split_axis_index] = splits[split_index] 111 | if __has_intersection(rect, rect_cache, rect_cache_grid_size): 112 | continue 113 | var new_bounds: Vector2i = Vector2i( 114 | maxi(bounds.x, rect.end.x), 115 | maxi(bounds.y, rect.end.y)) 116 | var score: float = \ 117 | __WHITESPACE_WEIGHT * (new_bounds.x * new_bounds.y - utilized_area) + \ 118 | __SIDE_LENGTH_WEIGHT * average_squared_rect_side_length * maxf(new_bounds.x, new_bounds.y) 119 | if score < best_score: 120 | best_score = score 121 | rects_positions[ordered_rect_index] = rect.position 122 | best_new_bounds = new_bounds 123 | bounds = best_new_bounds 124 | rect.position = rects_positions[ordered_rect_index] 125 | 126 | __add_rect_to_cache(rect, rect_cache, rect_cache_grid_size) 127 | # Add new splits at rect.end if they dot't already exist 128 | for split_axis_index in 2: 129 | var splits: PackedInt32Array = splits_by_axis[split_axis_index] 130 | var position: int = rect.end[split_axis_index] 131 | var split_index: int = splits.bsearch(position) 132 | if split_index == splits.size(): 133 | splits.append(position) 134 | elif splits[split_index] != position: 135 | splits.insert(split_index, position) 136 | 137 | result.success(bounds, rects_positions) 138 | return result 139 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/result.gd: -------------------------------------------------------------------------------- 1 | class Class: 2 | extends RefCounted 3 | 4 | var error: Error 5 | var error_description: String 6 | var inner_result: Class 7 | func _get_result_type_description() -> String: 8 | return "Operation" 9 | func fail(error: Error, error_description: String = "", inner_result: Class = null) -> void: 10 | assert(error != OK) 11 | self.error = error 12 | self.error_description = error_description 13 | self.inner_result = inner_result 14 | func _success(): 15 | error = OK 16 | error_description = "" 17 | inner_result = null 18 | func _to_string() -> String: 19 | return "%s error: %s (%s)%s%s" % [ 20 | _get_result_type_description(), 21 | error, 22 | error_string(error), 23 | (", description: \"%s\"" % [error_description]) if error_description else "", 24 | (", inner error:\n%s" % [inner_result]) if inner_result else "", 25 | ] if error else "%s(success)" 26 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/setting.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends RefCounted 3 | 4 | const _Result = preload("result.gd").Class 5 | 6 | var __editor_settings: EditorSettings 7 | 8 | var __name: StringName 9 | var __initial_value: Variant 10 | var __type: Variant.Type 11 | var __hint: PropertyHint 12 | var __hint_string: String 13 | var __is_required: bool 14 | var __is_value_empty_func: Callable 15 | 16 | func __default_is_value_empty_func(value: Variant) -> bool: 17 | if value: return false 18 | return true 19 | 20 | func _init( 21 | name: String, 22 | initial_value: Variant, 23 | type: int, 24 | hint: int, 25 | hint_string: String = "", 26 | is_required: bool = false, 27 | is_value_empty_func: Callable = __default_is_value_empty_func 28 | ) -> void: 29 | __name = "importality/" + name 30 | __initial_value = initial_value 31 | __type = type 32 | __hint = hint 33 | __hint_string = hint_string 34 | __is_required = is_required 35 | __is_value_empty_func = is_value_empty_func 36 | 37 | func register(editor_settings: EditorSettings) -> void: 38 | __editor_settings = editor_settings 39 | if not __editor_settings.has_setting(__name): 40 | __editor_settings.set_setting(__name, __initial_value) 41 | __editor_settings.set_initial_value(__name, __initial_value, false) 42 | var property_info: Dictionary = { 43 | &"name": __name, 44 | &"type": __type, 45 | &"hint": __hint, } 46 | if __hint_string: 47 | property_info[&"hint_string"] = __hint_string 48 | __editor_settings.add_property_info(property_info) 49 | 50 | class GettingValueResult: 51 | extends _Result 52 | var value: Variant 53 | func success(value: Variant) -> void: 54 | _success() 55 | self.value = value 56 | 57 | func get_value() -> GettingValueResult: 58 | var result = GettingValueResult.new() 59 | var value = __editor_settings.get_setting(__name) 60 | if __is_required: 61 | if __is_value_empty_func.call(value): 62 | result.fail(ERR_UNCONFIGURED, 63 | "The editor setting \"%s\" is not specified! " % [__name] + \ 64 | "Specify it in Editor Settings -> Importality.") 65 | return result 66 | result.success(value) 67 | return result 68 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/sprite_sheet_builder/_.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends RefCounted 3 | 4 | const _Result = preload("../result.gd").Class 5 | const _Common = preload("../common.gd") 6 | 7 | var _edges_artifacts_avoidance_method: _Common.EdgesArtifactsAvoidanceMethod 8 | var _sprites_surrounding_color: Color 9 | 10 | func _init( 11 | edges_artifacts_avoidance_method: _Common.EdgesArtifactsAvoidanceMethod, 12 | sprites_surrounding_color: Color = Color.TRANSPARENT 13 | ) -> void: 14 | _edges_artifacts_avoidance_method = edges_artifacts_avoidance_method 15 | _sprites_surrounding_color = sprites_surrounding_color 16 | 17 | class SpriteSheetBuildingResult: 18 | extends _Result 19 | var sprite_sheet: _Common.SpriteSheetInfo 20 | var atlas_image: Image 21 | func _get_result_type_description() -> String: 22 | return "Sprite sheet building" 23 | func success(sprite_sheet: _Common.SpriteSheetInfo, atlas_image: Image) -> void: 24 | _success() 25 | self.sprite_sheet = sprite_sheet 26 | self.atlas_image = atlas_image 27 | 28 | func build_sprite_sheet(images: Array[Image]) -> SpriteSheetBuildingResult: 29 | assert(false, "This method is abstract and must be overriden.") 30 | return null 31 | 32 | static func __hash_combine(a: int, b: int) -> int: 33 | return a ^ (b + 0x9E3779B9 + (a<<6) + (a>>2)) 34 | 35 | const __hash_precision: int = 5 36 | static func _get_image_hash(image: Image) -> int: 37 | var image_size: Vector2i = image.get_size() 38 | if image_size.x * image_size.y == 0: 39 | return 0 40 | var hash: int = 0 41 | hash = __hash_combine(hash, image_size.x) 42 | hash = __hash_combine(hash, image_size.y) 43 | var grid_cell_size: Vector2i = image_size / __hash_precision 44 | for y in range(0, image_size.y, grid_cell_size.y): 45 | for x in range(0, image_size.x, grid_cell_size.x): 46 | var pixel: Color = image.get_pixel(x, y) 47 | hash = __hash_combine(hash, pixel.r8) 48 | hash = __hash_combine(hash, pixel.g8) 49 | hash = __hash_combine(hash, pixel.b8) 50 | hash = __hash_combine(hash, pixel.a8) 51 | return hash 52 | 53 | static func _extrude_borders(image: Image, rect: Rect2i) -> void: 54 | if not rect.has_area(): 55 | return 56 | # extrude borders 57 | # left border 58 | image.blit_rect(image, 59 | rect.grow_side(SIDE_RIGHT, 1 - rect.size.x), 60 | rect.position + Vector2i.LEFT) 61 | # top border 62 | image.blit_rect(image, 63 | rect.grow_side(SIDE_BOTTOM, 1 - rect.size.y), 64 | rect.position + Vector2i.UP) 65 | # right border 66 | image.blit_rect(image, 67 | rect.grow_side(SIDE_LEFT, 1 - rect.size.x), 68 | rect.position + Vector2i(rect.size.x, 0)) 69 | # bottom border 70 | image.blit_rect(image, 71 | rect.grow_side(SIDE_TOP, 1 - rect.size.y), 72 | rect.position + Vector2i(0, rect.size.y)) 73 | 74 | # corner pixels 75 | # top left corner 76 | image.set_pixelv(rect.position - Vector2i.ONE, 77 | image.get_pixelv(rect.position)) 78 | # top right corner 79 | image.set_pixelv(rect.position + Vector2i(rect.size.x, -1), 80 | image.get_pixelv(rect.position + Vector2i(rect.size.x - 1, 0))) 81 | # bottom right corner 82 | image.set_pixelv(rect.end, 83 | image.get_pixelv(rect.end - Vector2i.ONE)) 84 | # bottom left corner 85 | image.set_pixelv(rect.position + Vector2i(-1, rect.size.y), 86 | image.get_pixelv(rect.position + Vector2i(0, rect.size.y -1))) 87 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/sprite_sheet_builder/grid_based.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_.gd" 3 | 4 | const _RectPacker = preload("../rect_packer.gd") 5 | 6 | enum StripDirection { 7 | HORIZONTAL = 0, 8 | VERTICAL = 1, 9 | } 10 | 11 | var _strips_direction: StripDirection 12 | var _max_cells_in_strip: int 13 | var _trim_sprites_to_overall_min_size: bool 14 | var _collapse_transparent: bool 15 | var _merge_duplicates: bool 16 | 17 | func _init( 18 | edges_artifacts_avoidance_method: _Common.EdgesArtifactsAvoidanceMethod, 19 | strips_direction: StripDirection, 20 | max_cells_in_strip: int, 21 | trim_sprites_to_overall_min_size: bool, 22 | collapse_transparent: bool, 23 | merge_duplicates: bool, 24 | sprites_surrounding_color: Color = Color.TRANSPARENT 25 | ) -> void: 26 | super(edges_artifacts_avoidance_method, sprites_surrounding_color) 27 | _strips_direction = strips_direction 28 | _max_cells_in_strip = max_cells_in_strip 29 | _trim_sprites_to_overall_min_size = trim_sprites_to_overall_min_size 30 | _collapse_transparent = collapse_transparent 31 | _merge_duplicates = merge_duplicates 32 | 33 | func build_sprite_sheet(images: Array[Image]) -> SpriteSheetBuildingResult: 34 | var result: SpriteSheetBuildingResult = SpriteSheetBuildingResult.new() 35 | var images_count: int = images.size() 36 | 37 | var sprite_sheet: _Common.SpriteSheetInfo = _Common.SpriteSheetInfo.new() 38 | 39 | if images_count == 0: 40 | var atlas_image = Image.new() 41 | atlas_image.set_data(1, 1, false, Image.FORMAT_RGBA8, PackedByteArray([0, 0, 0, 0])) 42 | result.success(sprite_sheet, atlas_image) 43 | return result 44 | 45 | sprite_sheet.source_image_size = images.front().get_size() 46 | if not images.all(func(i: Image) -> bool: return i.get_size() == sprite_sheet.source_image_size): 47 | result.fail(ERR_INVALID_DATA, "Input images have different sizes") 48 | return result 49 | 50 | sprite_sheet.sprites.resize(images_count) 51 | 52 | var first_axis: int = _strips_direction 53 | var second_axis: int = 1 - first_axis 54 | 55 | var max_image_used_rect: Rect2i 56 | var images_infos_cache: Dictionary # of arrays of images indices by image hashes 57 | 58 | var unique_sprites_indices: Array[int] 59 | var collapsed_sprite: _Common.SpriteInfo = _Common.SpriteInfo.new() 60 | var images_used_rects: Array[Rect2i] 61 | 62 | for image_index in images_count: 63 | var image: Image = images[image_index] 64 | var image_used_rect: Rect2i = image.get_used_rect() 65 | var is_image_invisible: bool = not image_used_rect.has_area() 66 | 67 | if _collapse_transparent and is_image_invisible: 68 | sprite_sheet.sprites[image_index] = collapsed_sprite 69 | continue 70 | elif _merge_duplicates: 71 | var image_hash: int = _get_image_hash(image) 72 | var similar_images_indices: PackedInt32Array = \ 73 | images_infos_cache.get(image_hash, PackedInt32Array()) 74 | var is_duplicate_found: bool = false 75 | for similar_image_index in similar_images_indices: 76 | var similar_image: Image = images[similar_image_index] 77 | if image == similar_image or image.get_data() == similar_image.get_data(): 78 | sprite_sheet.sprites[image_index] = \ 79 | sprite_sheet.sprites[similar_image_index] 80 | is_duplicate_found = true 81 | break 82 | if similar_images_indices.is_empty(): 83 | images_infos_cache[image_hash] = similar_images_indices 84 | similar_images_indices.push_back(image_index) 85 | if is_duplicate_found: 86 | continue 87 | 88 | var sprite: _Common.SpriteInfo = _Common.SpriteInfo.new() 89 | sprite.region = image_used_rect 90 | sprite_sheet.sprites[image_index] = sprite 91 | unique_sprites_indices.push_back(image_index) 92 | if not is_image_invisible: 93 | max_image_used_rect = \ 94 | image_used_rect.merge(max_image_used_rect) \ 95 | if max_image_used_rect.has_area() else \ 96 | image_used_rect 97 | 98 | var unique_sprites_count: int = unique_sprites_indices.size() 99 | 100 | if _edges_artifacts_avoidance_method == _Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION: 101 | sprite_sheet.source_image_size += Vector2i.ONE * 2 102 | 103 | var grid_size: Vector2i 104 | grid_size[second_axis] = \ 105 | unique_sprites_count / _max_cells_in_strip + \ 106 | sign(unique_sprites_count % _max_cells_in_strip) \ 107 | if _max_cells_in_strip > 0 else sign(unique_sprites_count) 108 | grid_size[first_axis] = \ 109 | _max_cells_in_strip \ 110 | if grid_size[second_axis] > 1 else \ 111 | unique_sprites_count 112 | 113 | var image_region: Rect2i = \ 114 | max_image_used_rect \ 115 | if _trim_sprites_to_overall_min_size else \ 116 | Rect2i(Vector2i.ZERO, sprite_sheet.source_image_size) 117 | if _edges_artifacts_avoidance_method == _Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION: 118 | image_region = image_region.grow(1) 119 | 120 | var atlas_size: Vector2i = grid_size * image_region.size 121 | match _edges_artifacts_avoidance_method: 122 | _Common.EdgesArtifactsAvoidanceMethod.NONE: 123 | pass 124 | _Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_SPACING: 125 | atlas_size += grid_size - Vector2i.ONE 126 | _Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING: 127 | atlas_size += grid_size + Vector2i.ONE 128 | _Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION: 129 | atlas_size += grid_size * 2 130 | _Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION: 131 | pass 132 | 133 | var atlas_image = Image.create(atlas_size.x, atlas_size.y, false, Image.FORMAT_RGBA8) 134 | 135 | if _edges_artifacts_avoidance_method == _Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING: 136 | atlas_image.fill(_sprites_surrounding_color) 137 | 138 | var cell: Vector2i 139 | var cell_index: int 140 | for sprite_index in unique_sprites_indices: 141 | # calculate cell 142 | var sprite: _Common.SpriteInfo = sprite_sheet.sprites[sprite_index] 143 | if sprite == collapsed_sprite: 144 | continue 145 | sprite.region.size = image_region.size 146 | sprite.offset = image_region.position 147 | var image: Image = images[sprite_index] 148 | cell[first_axis] = cell_index % _max_cells_in_strip if _max_cells_in_strip > 0 else cell_index 149 | cell[second_axis] = cell_index / _max_cells_in_strip if _max_cells_in_strip > 0 else 0 150 | sprite.region.position = cell * image_region.size 151 | match _edges_artifacts_avoidance_method: 152 | _Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_SPACING: 153 | sprite.region.position += cell 154 | _Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING: 155 | sprite.region.position += cell + Vector2i.ONE 156 | _Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION: 157 | sprite.region.position += cell * 2 + Vector2i.ONE 158 | _Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION: 159 | pass 160 | atlas_image.blit_rect(image, image_region, sprite.region.position)# + 161 | match _edges_artifacts_avoidance_method: 162 | _Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION: 163 | _extrude_borders(atlas_image, sprite.region) 164 | cell_index += 1 165 | 166 | result.success(sprite_sheet, atlas_image) 167 | return result 168 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/sprite_sheet_builder/packed.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends "_.gd" 3 | 4 | const _RectPacker = preload("../rect_packer.gd") 5 | 6 | class SpriteProps: 7 | extends RefCounted 8 | var images_props: Array[ImageProps] 9 | var atlas_region_props: AtlasRegionProps 10 | 11 | var offset: Vector2i 12 | 13 | func create_sprite(atlas_region_position: Vector2i) -> _Common.SpriteInfo: 14 | var sprite = _Common.SpriteInfo.new() 15 | sprite.region = Rect2i(atlas_region_position, atlas_region_props.size) 16 | sprite.offset = offset 17 | return sprite 18 | 19 | class AtlasRegionProps: 20 | extends RefCounted 21 | var sprites_props: Array[SpriteProps] 22 | var images_props: Array[ImageProps] 23 | 24 | var size: Vector2i 25 | 26 | class ImageProps: 27 | extends RefCounted 28 | var sprite_props: SpriteProps 29 | var atlas_region_props: AtlasRegionProps 30 | 31 | var image: Image 32 | var used_rect: Rect2i 33 | var used_fragment: Image 34 | var used_fragment_data: PackedByteArray 35 | var used_fragment_data_hash: int 36 | 37 | func _init(image: Image) -> void: 38 | self.image = image 39 | used_rect = image.get_used_rect() 40 | if used_rect.has_area(): 41 | used_fragment = image.get_region(used_rect) 42 | used_fragment_data = used_fragment.get_data() 43 | used_fragment_data_hash = hash(used_fragment_data) 44 | 45 | class SpriteSheetBuildingContext: 46 | extends RefCounted 47 | var images_props: Array[ImageProps] 48 | var sprites_props: Array[SpriteProps] 49 | var atlas_regions_props: Array[AtlasRegionProps] 50 | var _similar_images_props_by_used_fragment_data_hash: Dictionary 51 | var _collapsed_image_props: ImageProps 52 | 53 | func _init(images: Array[Image]) -> void: 54 | var images_count: int = images.size() 55 | images_props.resize(images_count) 56 | for image_index in images_count: 57 | images_props[image_index] = _process_image_props(ImageProps.new(images[image_index])) 58 | 59 | func _process_image_props(image_props: ImageProps) -> ImageProps: 60 | if not image_props.used_rect.has_area(): 61 | if _collapsed_image_props == null: 62 | _collapsed_image_props = image_props 63 | return _collapsed_image_props 64 | 65 | var similar_images_props: Array[ImageProps] 66 | if not _similar_images_props_by_used_fragment_data_hash.has(image_props.used_fragment_data_hash): 67 | _similar_images_props_by_used_fragment_data_hash[image_props.used_fragment_data_hash] = similar_images_props 68 | else: 69 | similar_images_props = _similar_images_props_by_used_fragment_data_hash[image_props.used_fragment_data_hash] 70 | for similar_image_props in similar_images_props: 71 | if image_props.image == similar_image_props.image: 72 | # The same image found. 73 | return similar_image_props 74 | elif image_props.used_rect.size == similar_image_props.used_rect.size: 75 | if image_props.used_fragment_data == similar_image_props.used_fragment_data: 76 | if image_props.used_rect.position == similar_image_props.used_rect.position: 77 | # An image with equal content found. 78 | return similar_image_props 79 | else: 80 | # An image with equal, but offsetted content found. 81 | # It will have the same region, but new sprite. 82 | image_props.atlas_region_props = similar_image_props.atlas_region_props 83 | image_props.sprite_props = SpriteProps.new() 84 | image_props.sprite_props.offset = image_props.used_rect.position 85 | image_props.sprite_props.images_props.push_back(image_props) 86 | image_props.sprite_props.atlas_region_props = similar_image_props.atlas_region_props 87 | sprites_props.push_back(image_props.sprite_props) 88 | return image_props 89 | # A new unique image found. 90 | # It will have new region and sprite. 91 | image_props.atlas_region_props = AtlasRegionProps.new() 92 | image_props.atlas_region_props.size = image_props.used_rect.size 93 | image_props.sprite_props = SpriteProps.new() 94 | image_props.sprite_props.offset = image_props.used_rect.position 95 | image_props.sprite_props.images_props.push_back(image_props) 96 | image_props.sprite_props.atlas_region_props = image_props.atlas_region_props 97 | image_props.atlas_region_props.sprites_props.push_back(image_props.sprite_props) 98 | image_props.atlas_region_props.images_props.push_back(image_props) 99 | sprites_props.push_back(image_props.sprite_props) 100 | atlas_regions_props.push_back(image_props.atlas_region_props) 101 | similar_images_props.push_back(image_props) 102 | return image_props 103 | 104 | func build_sprite_sheet(images: Array[Image]) -> SpriteSheetBuildingResult: 105 | var result: SpriteSheetBuildingResult = SpriteSheetBuildingResult.new() 106 | var images_count: int = images.size() 107 | 108 | var sprite_sheet: _Common.SpriteSheetInfo = _Common.SpriteSheetInfo.new() 109 | 110 | if images_count == 0: 111 | var atlas_image = Image.new() 112 | atlas_image.set_data(1, 1, false, Image.FORMAT_RGBA8, PackedByteArray([0, 0, 0, 0])) 113 | result.success(sprite_sheet, atlas_image) 114 | return result 115 | 116 | sprite_sheet.source_image_size = images.front().get_size() 117 | if not images.all(func(i: Image) -> bool: 118 | return i.get_size() == sprite_sheet.source_image_size): 119 | result.fail(ERR_INVALID_DATA, "Input images have different sizes") 120 | return result 121 | 122 | sprite_sheet.sprites.resize(images_count) 123 | 124 | var context: SpriteSheetBuildingContext = SpriteSheetBuildingContext.new(images) 125 | var atlas_regions_count: int = context.atlas_regions_props.size() 126 | if atlas_regions_count == 0: 127 | # All sprites are collapsed 128 | var collapsed_sprite: _Common.SpriteInfo = _Common.SpriteInfo.new() 129 | for image_index in images_count: 130 | sprite_sheet.sprites[image_index] = collapsed_sprite 131 | var atlas_image = Image.new() 132 | atlas_image.set_data(1, 1, false, Image.FORMAT_RGBA8, PackedByteArray([0, 0, 0, 0])) 133 | result.success(sprite_sheet, atlas_image) 134 | return result 135 | 136 | var atlas_regions_sizes: Array[Vector2i] 137 | atlas_regions_sizes.resize(atlas_regions_count) 138 | for atlas_region_index in atlas_regions_count: 139 | atlas_regions_sizes[atlas_region_index] = \ 140 | context.atlas_regions_props[atlas_region_index].size 141 | 142 | match _edges_artifacts_avoidance_method: 143 | _Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_SPACING, \ 144 | _Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING: 145 | for atlas_region_index in atlas_regions_count: 146 | atlas_regions_sizes[atlas_region_index] += Vector2i.ONE 147 | _Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION, \ 148 | _Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION: 149 | for atlas_region_index in atlas_regions_count: 150 | atlas_regions_sizes[atlas_region_index] += Vector2i.ONE * 2 151 | 152 | var packing_result: _RectPacker.RectPackingResult = _RectPacker.pack(atlas_regions_sizes) 153 | if packing_result.error: 154 | result.fail(ERR_BUG, "Rect packing failed", packing_result) 155 | return result 156 | 157 | match _edges_artifacts_avoidance_method: 158 | _Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_SPACING: 159 | packing_result.bounds -= Vector2i.ONE 160 | _Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING: 161 | packing_result.bounds += Vector2i.ONE 162 | for atlas_region_index in atlas_regions_count: 163 | packing_result.rects_positions[atlas_region_index] += Vector2i.ONE 164 | _Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION: 165 | for atlas_region_index in atlas_regions_count: 166 | packing_result.rects_positions[atlas_region_index] += Vector2i.ONE 167 | 168 | var atlas_image: Image = Image.create( 169 | packing_result.bounds.x, packing_result.bounds.y, false, Image.FORMAT_RGBA8) 170 | 171 | if _edges_artifacts_avoidance_method == _Common.EdgesArtifactsAvoidanceMethod.SOLID_COLOR_SURROUNDING: 172 | atlas_image.fill(_sprites_surrounding_color) 173 | 174 | var extrude_sprites_borders: bool = _edges_artifacts_avoidance_method == \ 175 | _Common.EdgesArtifactsAvoidanceMethod.BORDERS_EXTRUSION 176 | var expand_sprites: bool = _edges_artifacts_avoidance_method == \ 177 | _Common.EdgesArtifactsAvoidanceMethod.TRANSPARENT_EXPANSION 178 | 179 | var atlas_regions_positions_by_atlas_regions_props: Dictionary 180 | for atlas_region_index in atlas_regions_count: 181 | var atlas_region_props: AtlasRegionProps = context.atlas_regions_props[atlas_region_index] 182 | packing_result.rects_positions[atlas_region_index] += \ 183 | Vector2i.ONE if expand_sprites else Vector2i.ZERO 184 | var image_props: ImageProps = atlas_region_props.images_props.front() 185 | atlas_image.blit_rect(image_props.used_fragment, 186 | Rect2i(Vector2i.ZERO, atlas_region_props.size), 187 | packing_result.rects_positions[atlas_region_index]) 188 | if extrude_sprites_borders: 189 | _extrude_borders(atlas_image, Rect2i( 190 | packing_result.rects_positions[atlas_region_index], 191 | atlas_region_props.size)) 192 | atlas_regions_positions_by_atlas_regions_props[atlas_region_props] = \ 193 | packing_result.rects_positions[atlas_region_index] 194 | 195 | var sprites_by_sprites_props: Dictionary 196 | for sprite_props in context.sprites_props: 197 | var sprite: _Common.SpriteInfo = sprite_props.create_sprite( 198 | atlas_regions_positions_by_atlas_regions_props[sprite_props.atlas_region_props]) 199 | if expand_sprites: 200 | sprite.region = sprite.region.grow(1) 201 | sprite.offset -= Vector2i.ONE 202 | sprites_by_sprites_props[sprite_props] = sprite 203 | 204 | var collapsed_sprite: _Common.SpriteInfo 205 | for image_index in images_count: 206 | var image_props: ImageProps = context.images_props[image_index] 207 | var sprite: _Common.SpriteInfo = sprites_by_sprites_props.get(image_props.sprite_props, null) 208 | if sprite == null: 209 | if collapsed_sprite == null: 210 | collapsed_sprite = _Common.SpriteInfo.new() 211 | sprite = collapsed_sprite 212 | sprite_sheet.sprites[image_index] = sprite 213 | 214 | if expand_sprites: 215 | sprite_sheet.source_image_size += Vector2i.ONE * 2 216 | result.success(sprite_sheet, atlas_image) 217 | return result 218 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/standalone_image_format_loader_extension.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ImageFormatLoaderExtension 3 | 4 | const _Setting = preload("setting.gd") 5 | 6 | func get_settings() -> Array[_Setting]: 7 | assert(false, "This method is abstract and must be overriden.") 8 | return [] 9 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/uuid.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | 3 | const __BYTE_MASK: int = 0b11111111 4 | static var __default_rng: RandomNumberGenerator = RandomNumberGenerator.new() 5 | 6 | var __bytes: PackedByteArray 7 | 8 | func _init(rng: RandomNumberGenerator = null) -> void: 9 | if rng == null: 10 | rng = __default_rng 11 | rng.randomize() 12 | const size: int = 16 13 | __bytes.resize(size) 14 | for i in size: 15 | __bytes[i] = rng.randi() & __BYTE_MASK 16 | __bytes[6] = __bytes[6] & 0x0f | 0x40 17 | __bytes[8] = __bytes[8] & 0x3f | 0x80 18 | 19 | func to_bytes() -> PackedByteArray: 20 | return __bytes.duplicate() 21 | 22 | func is_equal(other: Object) -> bool: 23 | return \ 24 | other != null and \ 25 | get_script() == other.get_script() and \ 26 | __bytes == other.__bytes 27 | 28 | func _to_string() -> String: 29 | return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % Array(__bytes) 30 | -------------------------------------------------------------------------------- /addons/nklbdev.importality/xml.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | 3 | class XMLNode: 4 | extends RefCounted 5 | var text: String 6 | func _init(text: String) -> void: 7 | self.text = text 8 | func _get_solid_text() -> String: 9 | assert(false, "This method is abstract and must be overriden in derived class") 10 | return "" 11 | func get_elements(text: String) -> Array[XMLNodeElement]: 12 | assert(false, "This method is abstract and must be overriden in derived class") 13 | return [] 14 | func _dump(target: PackedStringArray, indent: String, level: int) -> void: 15 | target.append(indent.repeat(level) + _get_solid_text()) 16 | 17 | class XMLNodeParent: 18 | extends XMLNode 19 | var children: Array[XMLNode] 20 | func _init(text: String) -> void: 21 | super(text) 22 | func _get_opening_tag() -> String: return "" 23 | func _get_closing_tag() -> String: return "" 24 | func _get_solid_text() -> String: 25 | assert(false, "This method is abstract and must be overriden in derived class") 26 | return "" 27 | func _dump_children(target: PackedStringArray, indent: String, level: int) -> void: 28 | for child in children: 29 | child._dump(target, indent, level) 30 | func _dump(target: PackedStringArray, indent: String, level: int) -> void: 31 | var tag_indent: String = indent.repeat(level) 32 | if children.is_empty(): 33 | target.append(tag_indent + _get_solid_text()) 34 | else: 35 | target.append(tag_indent + _get_opening_tag()) 36 | _dump_children(target, indent, level + 1) 37 | target.append(tag_indent + _get_closing_tag()) 38 | func get_elements(text: String) -> Array[XMLNodeElement]: 39 | var result: Array[XMLNodeElement] 40 | result.append_array(children.filter(func(n): return n is XMLNodeElement and n.text == text)) 41 | return result 42 | 43 | class XMLNodeRoot: 44 | extends XMLNodeParent 45 | func _init() -> void: 46 | super("") 47 | func dump_to_string(indent: String = " ", new_line: String = "\n") -> String: 48 | var target: PackedStringArray 49 | _dump_children(target, indent, 0) 50 | return new_line.join(target) 51 | func dump_to_buffer(indent: String = " ", new_line: String = "\n") -> PackedByteArray: 52 | return dump_to_string(indent, new_line).to_utf8_buffer() 53 | func dump_to_file(absolute_file_path: String, indent: String = " ", new_line: String = "\n") -> void: 54 | DirAccess.make_dir_recursive_absolute(absolute_file_path.get_base_dir()) 55 | var file: FileAccess = FileAccess.open(absolute_file_path, FileAccess.WRITE) 56 | file.store_string(dump_to_string(indent, new_line)) 57 | file.close() 58 | 59 | class XMLNodeElement: 60 | extends XMLNodeParent 61 | var attributes: Dictionary 62 | var closed: bool 63 | func _init(text: String, closed: bool = false) -> void: 64 | super(text) 65 | self.closed = closed 66 | func _get_attributes_string() -> String: 67 | return "".join(attributes.keys().map(func(k): return " %s=\"%s\"" % [k, attributes[k]])) 68 | func _get_opening_tag() -> String: return "<%s%s>" % [text, _get_attributes_string()] 69 | func _get_closing_tag() -> String:return "" % [text] 70 | func _get_solid_text() -> String: return "<%s%s/>" % [text, _get_attributes_string()] 71 | func get_string(attribute: String) -> String: 72 | return attributes[attribute] 73 | func get_int(attribute: String) -> int: 74 | return attributes[attribute].to_int() 75 | func get_int_encoded_hex_color(attribute: String, with_alpha: bool = false) -> Color: 76 | var arr: PackedByteArray 77 | arr.resize(4) 78 | arr.encode_u32(0, attributes[attribute].to_int()) 79 | if not with_alpha: 80 | arr.resize(3) 81 | return Color(arr.hex_encode()) 82 | func get_vector2i(attribute_x: String, attribute_y: String) -> Vector2i: 83 | return Vector2i(attributes[attribute_x].to_int(), attributes[attribute_y].to_int()) 84 | func get_rect2i(attribute_position_x: String, attribute_position_y: String, attribute_size_x: String, attribute_size_y: String) -> Rect2i: 85 | return Rect2i( 86 | attributes[attribute_position_x].to_int(), 87 | attributes[attribute_position_y].to_int(), 88 | attributes[attribute_size_x].to_int(), 89 | attributes[attribute_size_y].to_int()) 90 | func get_bool(attribute: String) -> bool: 91 | var raw_value: String = attributes[attribute] 92 | if raw_value.is_empty(): 93 | return false 94 | if raw_value.is_valid_int(): 95 | return bool(raw_value.to_int()) 96 | if raw_value.nocasecmp_to("True") == 0: 97 | return true 98 | if raw_value.nocasecmp_to("False") == 0: 99 | return false 100 | push_warning("Failed to parse bool value from string: \"%s\", returning false..." % [raw_value]) 101 | return false 102 | 103 | class XMLNodeText: 104 | extends XMLNode 105 | func _init(text: String) -> void: 106 | super(text) 107 | func _get_solid_text() -> String: return text.strip_edges() 108 | func _dump(target: PackedStringArray, indent: String, level: int) -> void: 109 | var text: String = _get_solid_text() 110 | if not text.is_empty(): 111 | target.append(indent.repeat(level) + text) 112 | 113 | class XMLNodeCData: 114 | extends XMLNode 115 | func _init(text: String) -> void: 116 | super(text) 117 | func _get_solid_text() -> String: return "" % [text] 118 | 119 | class XMLNodeComment: 120 | extends XMLNode 121 | func _init(text: String) -> void: 122 | super(text) 123 | func _get_solid_text() -> String: return "" % [text] 124 | 125 | class XMLNodeUnknown: 126 | extends XMLNode 127 | func _init(text: String) -> void: 128 | super(text) 129 | func _get_solid_text() -> String: return "<%s>" % [text] 130 | 131 | static func parse_file(path: String) -> XMLNodeRoot: 132 | var parser = XMLParser.new() 133 | parser.open(path) 134 | return __parse_xml(parser) 135 | 136 | static func parse_buffer(buffer: PackedByteArray) -> XMLNodeRoot: 137 | var parser = XMLParser.new() 138 | parser.open_buffer(buffer) 139 | return __parse_xml(parser) 140 | 141 | static func parse_string(xml_string: String) -> XMLNodeRoot: 142 | return parse_buffer(xml_string.to_utf8_buffer()) 143 | 144 | static func __parse_xml(parser: XMLParser) -> XMLNodeRoot: 145 | var root = XMLNodeRoot.new() 146 | var stack: Array[XMLNode] = [root] 147 | while parser.read() != ERR_FILE_EOF: 148 | match parser.get_node_type(): 149 | XMLParser.NODE_ELEMENT: 150 | var node: XMLNode = XMLNodeElement.new(parser.get_node_name()) 151 | for attr_idx in parser.get_attribute_count(): 152 | node.attributes[parser.get_attribute_name(attr_idx)] = \ 153 | parser.get_attribute_value(attr_idx) 154 | stack.back().children.push_back(node) 155 | if not parser.is_empty(): 156 | stack.push_back(node) 157 | XMLParser.NODE_ELEMENT_END: 158 | if stack.size() < 2: 159 | push_warning("Extra end tag found") 160 | else: 161 | stack.pop_back() 162 | XMLParser.NODE_TEXT: 163 | var text: String = parser.get_node_data().strip_edges() 164 | if not text.is_empty(): 165 | stack.back().children.push_back(XMLNodeText.new(text)) 166 | XMLParser.NODE_CDATA: 167 | stack.back().children.push_back(XMLNodeCData.new(parser.get_node_data())) 168 | XMLParser.NODE_NONE: 169 | push_error("Incorrect XML node found") 170 | XMLParser.NODE_UNKNOWN: 171 | stack.back().children.push_back(XMLNodeUnknown.new(parser.get_node_name())) 172 | XMLParser.NODE_COMMENT: 173 | stack.back().children.push_back(XMLNodeComment.new(parser.get_node_name())) 174 | return root 175 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nklbdev/godot-4-importality/7bada9964ada65a9abe26971795705c316730930/icon.png --------------------------------------------------------------------------------