├── .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 | [](README.md)
4 | [](README.ru.md)
5 |
6 | 
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 |
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 | [](README.md)
4 | [](README.ru.md)
5 |
6 | 
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 |
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 "%s>" % [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
--------------------------------------------------------------------------------