Compare commits
90 commits
30eed8ad19
...
544535ede7
Author | SHA1 | Date | |
---|---|---|---|
544535ede7 | |||
616bd91c49 | |||
a698668930 | |||
59254b943f | |||
033238b787 | |||
d4c3ae3dc6 | |||
b4fbb1df75 | |||
360aa250c4 | |||
80e93629d1 | |||
5a17117d59 | |||
3edb37168c | |||
0f53fc32fe | |||
618d5f4916 | |||
a8392fb14f | |||
91caefee4a | |||
7de7f3fab8 | |||
2d81bd495a | |||
11ae7c007b | |||
fede8c3e49 | |||
1710294c9f | |||
8ab4af1047 | |||
8bd31112be | |||
d980eec683 | |||
39c85c3079 | |||
8ceeba76c0 | |||
93eab6929b | |||
1d9b9fda1e | |||
482dbecd13 | |||
cf8dacf0f5 | |||
048058bd35 | |||
605bff7324 | |||
206773c4e7 | |||
b5d5c44c4b | |||
8e55b91a39 | |||
0fad406967 | |||
4b12f764b5 | |||
02d1900dc2 | |||
18e9e2ec56 | |||
6100bdc8df | |||
5ec316a50f | |||
a7a76ff9f0 | |||
11e05ac471 | |||
3c3de7823a | |||
aa5c1d3182 | |||
3022963b84 | |||
0873a7bf70 | |||
0d2b579afe | |||
9c628c403b | |||
d41037d2df | |||
65e907e1d2 | |||
e5c7d46997 | |||
b7c34f4233 | |||
1ea80a342a | |||
f91bb18fb2 | |||
b48bb4a094 | |||
ff5713ae91 | |||
55f83a3367 | |||
c72a1f4b90 | |||
be8b7728e4 | |||
31981a1def | |||
7f4c7a6bf1 | |||
41ea287df4 | |||
a3e372c5d8 | |||
6224d06428 | |||
6459151549 | |||
fe6efb0f1d | |||
8b1367494d | |||
01b55aca07 | |||
5f53a3eb7b | |||
658477ed4b | |||
3fb8484ac5 | |||
0484b1012f | |||
b87a8e2ab8 | |||
e6c4a72158 | |||
1dcb696c35 | |||
d580523c6e | |||
11da07b9ac | |||
7cf87ac142 | |||
bd7d3b19cc | |||
996a234d0d | |||
77f6bcf07b | |||
fede2d8e6f | |||
d0ecf3b03d | |||
3d65e48c92 | |||
aa1731b701 | |||
558140b309 | |||
849b815562 | |||
3615ce087c | |||
2d28136449 | |||
74d95c2424 |
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -15,6 +15,10 @@ https://github.com/Orama-Interactive/Pixelorama/issues?q=is%3Aissue
|
|||
<!-- Specify commit hash if using a non-official build. -->
|
||||
|
||||
|
||||
**Where did you download Pixelorama from?**
|
||||
<!-- Specify where you downloaded Pixelorama from. GitHub Releases, itch.io, Steam, Flatpak, self-built, somewhere else? -->
|
||||
|
||||
|
||||
**OS/device including version:**
|
||||
<!-- Specify GPU model and drivers if graphics-related. -->
|
||||
|
||||
|
|
32
CHANGELOG.md
|
@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). All the dates are in YYYY-MM-DD format.
|
||||
<br><br>
|
||||
|
||||
## [v1.1] - Unreleased
|
||||
This update has been brought to you by the contributions of:
|
||||
Fayez Akhtar ([@Variable-ind](https://github.com/Variable-ind)), Spencer Beckwith ([@spencerjbeckwith](https://github.com/spencerjbeckwith))
|
||||
|
||||
Built using Godot 4.3
|
||||
|
||||
### Added
|
||||
- Tilemap layers have arrived! Tilemap layers allow artists to create tiles, and easily preview and dynamically modify them within Pixelorama. [#1146](https://github.com/Orama-Interactive/Pixelorama/pull/1146)
|
||||
- Indexed mode has finally been implemented! [#1136](https://github.com/Orama-Interactive/Pixelorama/pull/1136)
|
||||
- Audio layers have been added, allowing artists to easily synchronize their animations with audio. [#1149](https://github.com/Orama-Interactive/Pixelorama/pull/1149)
|
||||
- Added a new text tool. Destructive only for now, meaning that once the text is confirmed, it cannot be changed later. [#1134](https://github.com/Orama-Interactive/Pixelorama/pull/1134)
|
||||
- A color curves image and layer effect has been added.
|
||||
- It is now possible to load custom Godot shaders as image and layer effects.
|
||||
- Implemented support for multiple grids. [#1122](https://github.com/Orama-Interactive/Pixelorama/pull/1122)
|
||||
|
||||
### Changed
|
||||
- System font names are now sorted by alphabetical order.
|
||||
|
||||
### Fixed
|
||||
- Fixed crash when Pixelorama starts without a palette.
|
||||
- Undo/redo now works again when the cursor is hovering over the timeline.
|
||||
- The first frame is no longer exported twice when using ping-pong loop.
|
||||
- Fixed pencil/eraser/shading previews turning white for a brief moment when changing image brushes, and when switching between tools.
|
||||
- Dialogs that are children of other dialogs now always appear on top, to avoid issues where they could hide behind their parents and causing confusion that made Pixelorama seem unresponsive.
|
||||
- Palette swatches now get deleted when the user removes all palettes.
|
||||
- The CLI's output option now works with filepaths instead of just filenames. [#1145](https://github.com/Orama-Interactive/Pixelorama/pull/1145)
|
||||
- Fixed the Palettize effect and palette exporting to images storing slightly wrong color values. [77f6bcf](https://github.com/Orama-Interactive/Pixelorama/commit/77f6bcf07bd80bc042e478bb883d05900cebe436)
|
||||
- Fixed some issues with the Palettize effect where the output would be different if the palette size changed and empty swatches were added, even if the colors themselves stayed the same. Initially fixed by [bd7d3b1](https://github.com/Orama-Interactive/Pixelorama/commit/bd7d3b19cc98804e9b99754153c4d553d2048ee3), but [1dcb696](https://github.com/Orama-Interactive/Pixelorama/commit/1dcb696c35121f8208bde699f87bb75deff99d13) is the proper fix.
|
||||
- Fixed recorder label not updating when project is changed. [#1139](https://github.com/Orama-Interactive/Pixelorama/pull/1139)
|
||||
|
||||
## [v1.0.5] - 2024-11-18
|
||||
This update has been brought to you by the contributions of:
|
||||
Fayez Akhtar ([@Variable-ind](https://github.com/Variable-ind))
|
||||
|
@ -28,7 +58,7 @@ Built using Godot 4.3
|
|||
- Fixed layer effect slider values being rounded to the nearest integer.
|
||||
- Fixed memory leak where the project remained referenced by a drawing tool, even when its tab was closed.
|
||||
- Fixed memory leak where the first project remained forever references in memory by the Recorder panel.
|
||||
- Slightly optimize circle brushes by only calling the ellipse algorithms once while drawing
|
||||
- Slightly optimize circle brushes by only calling the ellipse algorithms once while drawing.
|
||||
|
||||
### Removed
|
||||
- The Recorder panel has been removed from the Web version. It wasn't functional anyway in a way that was useful, and it's unsure if we can find a way to make it work.
|
||||
|
|
|
@ -3,16 +3,23 @@ Name=Pixelorama
|
|||
GenericName=2D sprite editor
|
||||
GenericName[el]=Επεξεργαστής δισδιάστατων εικόνων
|
||||
GenericName[fr]=Éditeur de sprites 2D
|
||||
GenericName[ru]=2Д редактор спрайтов
|
||||
GenericName[pt_BR]=Editor de sprites 2D
|
||||
GenericName[uk]=2Д редактор спрайтів
|
||||
GenericName[zh_CN]=2D 精灵编辑器
|
||||
Comment=Create and edit static or animated 2D sprites
|
||||
Comment[el]=Δημιουργήστε και επεξεργαστείτε στατικές ή κινούμενες δισδιάστατες εικόνες
|
||||
Comment[fr]=Créez et modifiez des sprites 2D statiques ou animées
|
||||
Comment[ru]=Создавайте и редактируйте статичные и анимированные 2Д спрайты
|
||||
Comment[pt_BR]=Crie e edite sprites 2D estáticos ou animados
|
||||
Comment[uk]=Створюйте та редагуйте статичні та анімовані 2Д спрайти
|
||||
Comment[zh_CN]=创建并编辑 2D 精灵图片或动画
|
||||
Exec=pixelorama
|
||||
Icon=pixelorama
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Graphics;2DGraphics;RasterGraphics;
|
||||
Keywords=pixel;retro;animation;art;image;2d;sprite;graphics;drawing;editor;
|
||||
Keywords[ru]=pixel;retro;animation;art;image;2d;sprite;graphics;drawing;editor;пиксель;ретро;анимация;арт;изображение;2д;спрайт;графика;рисование;редактор;
|
||||
Keywords[uk]=pixel;retro;animation;art;image;2d;sprite;graphics;drawing;editor;піксель;ретро;анімація;арт;зображення;2д;спрайт;графіка;малювання;редактор;
|
||||
MimeType=image/pxo;image/png;image/bmp;image/vnd.radiance;image/jpeg;image/svg+xml;image/x-tga;image/webp;
|
||||
|
|
|
@ -156,6 +156,18 @@ msgstr ""
|
|||
msgid "Percentage"
|
||||
msgstr ""
|
||||
|
||||
#. Found in the create new image dialog. Allows users to change the color mode of the new project, such as RGBA or indexed mode.
|
||||
msgid "Color mode:"
|
||||
msgstr ""
|
||||
|
||||
#. Found in the image menu. A submenu that allows users to change the color mode of the project, such as RGBA or indexed mode.
|
||||
msgid "Color Mode"
|
||||
msgstr ""
|
||||
|
||||
#. Found in the image menu, under the "Color Mode" submenu. Refers to the indexed color mode. See this wikipedia page for more information: https://en.wikipedia.org/wiki/Indexed_color
|
||||
msgid "Indexed"
|
||||
msgstr ""
|
||||
|
||||
#. Found in the image menu. Sets the size of the project to be the same as the size of the active selection.
|
||||
msgid "Crop to Selection"
|
||||
msgstr ""
|
||||
|
@ -199,6 +211,10 @@ msgstr ""
|
|||
msgid "Initial angle:"
|
||||
msgstr ""
|
||||
|
||||
#. Found under the Select menu, It's a checkbox that, if enabled, wraps around brush strokes if some part of them goes out of selection bounds.
|
||||
msgid "Wrap Strokes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1145,6 +1161,27 @@ msgstr ""
|
|||
msgid "Tint effect factor:"
|
||||
msgstr ""
|
||||
|
||||
#. An image effect that adjusts the colors of the image by using curves.
|
||||
msgid "Color Curves"
|
||||
msgstr ""
|
||||
|
||||
#. Refers to a color channel, such as the red, green, blue or alpha channels.
|
||||
msgid "Channel:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Red"
|
||||
msgstr ""
|
||||
|
||||
msgid "Green"
|
||||
msgstr ""
|
||||
|
||||
#. Refers to the value (as in HSV) of the colors of an image.
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
msgid "Presets"
|
||||
msgstr ""
|
||||
|
||||
msgid "Apply"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1494,7 +1531,7 @@ msgstr ""
|
|||
|
||||
msgid "Text\n\n"
|
||||
"%s for left mouse button\n"
|
||||
"%s for right mouse button\n\n"
|
||||
"%s for right mouse button"
|
||||
msgstr ""
|
||||
|
||||
msgid "Rectangle"
|
||||
|
@ -1761,6 +1798,10 @@ msgstr ""
|
|||
msgid "If enabled, the application window can become transparent. This affects performance, so keep it off if you don't need it."
|
||||
msgstr ""
|
||||
|
||||
#. An option found in the preferences, under the Performance section. A dummy driver is basically a driver that doesn't do anything. When this option is enabled, audio does not play, but it can help save some performance.
|
||||
msgid "Use dummy audio driver"
|
||||
msgstr ""
|
||||
|
||||
#. Found in the Preferences, under Drivers. Specifies the renderer/video driver being used.
|
||||
msgid "Renderer:"
|
||||
msgstr ""
|
||||
|
@ -2191,6 +2232,10 @@ msgstr ""
|
|||
msgid "Unlink Cels"
|
||||
msgstr ""
|
||||
|
||||
#. An option found in the right click menu of an audio cel. If selected, the audio of the audio layer will start playing from this frame.
|
||||
msgid "Play audio here"
|
||||
msgstr ""
|
||||
|
||||
msgid "Properties"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2227,6 +2272,13 @@ msgstr ""
|
|||
msgid "Group"
|
||||
msgstr ""
|
||||
|
||||
#. A tilemap is a type of layer, which is divided by grid cells, the size of which is determined by the tileset it uses. Each grid cell is mapped to a tile in the tileset. Tilemaps can be used to create game levels and layouts.
|
||||
msgid "Tilemap"
|
||||
msgstr ""
|
||||
|
||||
msgid "Audio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Layers"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2235,33 +2287,52 @@ msgid "Clipping mask"
|
|||
msgstr ""
|
||||
|
||||
#. Hint tooltip of the create new layer button, found on the left side of the timeline.
|
||||
#: src/UI/Timeline/AnimationTimeline.tscn
|
||||
msgid "Create a new layer"
|
||||
msgstr ""
|
||||
|
||||
#. One of the options of the create new layer button.
|
||||
#: src/UI/Timeline/AnimationTimeline.tscn
|
||||
msgid "Add Pixel Layer"
|
||||
msgstr ""
|
||||
|
||||
#. One of the options of the create new layer button.
|
||||
#: src/UI/Timeline/AnimationTimeline.tscn
|
||||
msgid "Add Group Layer"
|
||||
msgstr ""
|
||||
|
||||
#. One of the options of the create new layer button.
|
||||
#: src/UI/Timeline/AnimationTimeline.tscn
|
||||
msgid "Add 3D Layer"
|
||||
msgstr ""
|
||||
|
||||
#. One of the options of the create new layer button.
|
||||
#: src/UI/Timeline/AnimationTimeline.tscn
|
||||
msgid "Add Tilemap Layer"
|
||||
msgstr ""
|
||||
|
||||
#. One of the options of the create new layer button.
|
||||
#: src/UI/Timeline/AnimationTimeline.tscn
|
||||
msgid "Add Audio Layer"
|
||||
msgstr ""
|
||||
|
||||
#: src/UI/Timeline/AnimationTimeline.tscn
|
||||
msgid "Remove current layer"
|
||||
msgstr ""
|
||||
|
||||
#: src/UI/Timeline/AnimationTimeline.tscn
|
||||
msgid "Move up the current layer"
|
||||
msgstr ""
|
||||
|
||||
#: src/UI/Timeline/AnimationTimeline.tscn
|
||||
msgid "Move down the current layer"
|
||||
msgstr ""
|
||||
|
||||
#: src/UI/Timeline/AnimationTimeline.tscn
|
||||
msgid "Clone current layer"
|
||||
msgstr ""
|
||||
|
||||
#: src/UI/Timeline/AnimationTimeline.tscn
|
||||
msgid "Merge current layer with the one below"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2375,6 +2446,17 @@ msgstr ""
|
|||
msgid "Expand/collapse group"
|
||||
msgstr ""
|
||||
|
||||
#. Refers to the audio file of an audio layer.
|
||||
msgid "Audio file:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Load file"
|
||||
msgstr ""
|
||||
|
||||
#. An option in the audio layer properties, allows users to play the audio starting from a specific frame.
|
||||
msgid "Play at frame:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Palette"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2948,6 +3030,10 @@ msgstr ""
|
|||
msgid "Recorder"
|
||||
msgstr ""
|
||||
|
||||
#. Tiles are images of a specific shape, usually rectangular, that are laid out in a grid. They are used in tile-based video games. https://en.wikipedia.org/wiki/Tile-based_video_game
|
||||
msgid "Tiles"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crop"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3349,3 +3435,51 @@ msgstr ""
|
|||
#. Text from a confirmation dialog that appears when the user is attempting to drag and drop an image directly from the browser into Pixelorama.
|
||||
msgid "Do you want to download the image from %s?"
|
||||
msgstr ""
|
||||
|
||||
#. A tileset is a collection of tiles.
|
||||
#: src/Classes/TileSetCustom.gd
|
||||
#: src/UI/Dialogs/ImportPreviewDialog.gd
|
||||
msgid "Tileset"
|
||||
msgstr ""
|
||||
|
||||
#. A tileset is a collection of tiles.
|
||||
#: src/UI/Timeline/NewTileMapLayerDialog.tscn
|
||||
msgid "Tileset:"
|
||||
msgstr ""
|
||||
|
||||
#. A tileset is a collection of tiles.
|
||||
#: src/UI/Dialogs/ProjectProperties.tscn
|
||||
msgid "Tilesets"
|
||||
msgstr ""
|
||||
|
||||
#: src/UI/Timeline/NewTileMapLayerDialog.tscn
|
||||
msgid "New tileset"
|
||||
msgstr ""
|
||||
|
||||
#: src/UI/Timeline/NewTileMapLayerDialog.tscn
|
||||
msgid "Tileset name:"
|
||||
msgstr ""
|
||||
|
||||
#: src/UI/Timeline/NewTileMapLayerDialog.tscn
|
||||
msgid "Tile size:"
|
||||
msgstr ""
|
||||
|
||||
#: src/UI/TilesPanel.tscn
|
||||
msgid "Draw tiles"
|
||||
msgstr ""
|
||||
|
||||
#: src/UI/TilesPanel.tscn
|
||||
msgid "Rotate tile left (counterclockwise)"
|
||||
msgstr ""
|
||||
|
||||
#: src/UI/TilesPanel.tscn
|
||||
msgid "Rotate tile right (clockwise)"
|
||||
msgstr ""
|
||||
|
||||
#: src/UI/TilesPanel.tscn
|
||||
msgid "Flip tile horizontally"
|
||||
msgstr ""
|
||||
|
||||
#: src/UI/TilesPanel.tscn
|
||||
msgid "Flip tile vertically"
|
||||
msgstr ""
|
||||
|
|
1
assets/graphics/misc/mirror_x.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="none" stroke="#e0e0e0" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6 2 8l2 2M2 8h11m-1-2 2 2-2 2"/></svg>
|
After Width: | Height: | Size: 206 B |
37
assets/graphics/misc/mirror_x.svg.import
Normal file
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bpsfilx47bw3r"
|
||||
path="res://.godot/imported/mirror_x.svg-16a0646fb607af92a2ccf231dd0f1d98.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/graphics/misc/mirror_x.svg"
|
||||
dest_files=["res://.godot/imported/mirror_x.svg-16a0646fb607af92a2ccf231dd0f1d98.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
1
assets/graphics/misc/mirror_y.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="none" stroke="#e0e0e0" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 4 8 2 6 4m2-2v11m2-1-2 2-2-2"/></svg>
|
After Width: | Height: | Size: 206 B |
37
assets/graphics/misc/mirror_y.svg.import
Normal file
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bk6iaxiyl74ih"
|
||||
path="res://.godot/imported/mirror_y.svg-47cb90f0f94e4ed7c37f151a9ddbaab0.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/graphics/misc/mirror_y.svg"
|
||||
dest_files=["res://.godot/imported/mirror_y.svg-47cb90f0f94e4ed7c37f151a9ddbaab0.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
BIN
assets/graphics/misc/musical_note.png
Normal file
After Width: | Height: | Size: 192 B |
34
assets/graphics/misc/musical_note.png.import
Normal file
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dfjd72smxp6ma"
|
||||
path="res://.godot/imported/musical_note.png-f1be7cc6341733e6ffe2fa5b650b80c2.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/graphics/misc/musical_note.png"
|
||||
dest_files=["res://.godot/imported/musical_note.png-f1be7cc6341733e6ffe2fa5b650b80c2.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
BIN
assets/graphics/misc/x_minus_y_mirror_off.png
Normal file
After Width: | Height: | Size: 218 B |
34
assets/graphics/misc/x_minus_y_mirror_off.png.import
Normal file
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://1kj5gcswa3t2"
|
||||
path="res://.godot/imported/x_minus_y_mirror_off.png-da237e3b5b7ad1dfef1c935385f53dc5.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/graphics/misc/x_minus_y_mirror_off.png"
|
||||
dest_files=["res://.godot/imported/x_minus_y_mirror_off.png-da237e3b5b7ad1dfef1c935385f53dc5.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
BIN
assets/graphics/misc/x_minus_y_mirror_on.png
Normal file
After Width: | Height: | Size: 187 B |
34
assets/graphics/misc/x_minus_y_mirror_on.png.import
Normal file
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dn14bkxwdqsfk"
|
||||
path="res://.godot/imported/x_minus_y_mirror_on.png-0e9186904d8241facc4a0c1190f32c53.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/graphics/misc/x_minus_y_mirror_on.png"
|
||||
dest_files=["res://.godot/imported/x_minus_y_mirror_on.png-0e9186904d8241facc4a0c1190f32c53.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
BIN
assets/graphics/misc/xy_mirror_off.png
Normal file
After Width: | Height: | Size: 183 B |
34
assets/graphics/misc/xy_mirror_off.png.import
Normal file
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dlxhm0ronna25"
|
||||
path="res://.godot/imported/xy_mirror_off.png-8d2fd9ebdf350f0cd384fdf39fed4ec1.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/graphics/misc/xy_mirror_off.png"
|
||||
dest_files=["res://.godot/imported/xy_mirror_off.png-8d2fd9ebdf350f0cd384fdf39fed4ec1.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
BIN
assets/graphics/misc/xy_mirror_on.png
Normal file
After Width: | Height: | Size: 185 B |
34
assets/graphics/misc/xy_mirror_on.png.import
Normal file
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cu2uqp5oupt80"
|
||||
path="res://.godot/imported/xy_mirror_on.png-95d443df3b6d17add41283bdd720ea7e.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/graphics/misc/xy_mirror_on.png"
|
||||
dest_files=["res://.godot/imported/xy_mirror_on.png-95d443df3b6d17add41283bdd720ea7e.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
Before Width: | Height: | Size: 218 B After Width: | Height: | Size: 374 B |
Before Width: | Height: | Size: 162 B After Width: | Height: | Size: 136 B |
BIN
assets/graphics/tileset/place_tiles_disabled.png
Normal file
After Width: | Height: | Size: 204 B |
34
assets/graphics/tileset/place_tiles_disabled.png.import
Normal file
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bqr3n3tm8b6w2"
|
||||
path="res://.godot/imported/place_tiles_disabled.png-f43e25b0863e3eedf3c6fc7ef902127f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/graphics/tileset/place_tiles_disabled.png"
|
||||
dest_files=["res://.godot/imported/place_tiles_disabled.png-f43e25b0863e3eedf3c6fc7ef902127f.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
BIN
assets/graphics/tileset/place_tiles_enabled.png
Normal file
After Width: | Height: | Size: 185 B |
34
assets/graphics/tileset/place_tiles_enabled.png.import
Normal file
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dga8nirhhgyc4"
|
||||
path="res://.godot/imported/place_tiles_enabled.png-845e4dd5c3bbd38cc7bf5ee82ed05667.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/graphics/tileset/place_tiles_enabled.png"
|
||||
dest_files=["res://.godot/imported/place_tiles_enabled.png-845e4dd5c3bbd38cc7bf5ee82ed05667.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
|
@ -1,30 +1,44 @@
|
|||
[gd_resource type="Resource" script_class="DockableLayout" load_steps=27 format=3 uid="uid://4xtpiowddm7p"]
|
||||
[gd_resource type="Resource" script_class="DockableLayout" load_steps=29 format=3 uid="uid://4xtpiowddm7p"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_panel.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_split.gd" id="2"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout.gd" id="3"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_panel.gd" id="1_jxh43"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_split.gd" id="2_lw52w"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout.gd" id="3_4h5wj"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_atmme"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Tools")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_4b0py"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Tiles")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_epagr"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 1
|
||||
percent = 0.5
|
||||
first = SubResource("Resource_atmme")
|
||||
second = SubResource("Resource_4b0py")
|
||||
|
||||
[sub_resource type="Resource" id="Resource_ouvfk"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Main Canvas")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_an0ef"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Perspective Editor")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_xgnjk"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 0
|
||||
percent = 0.5
|
||||
first = SubResource("Resource_ouvfk")
|
||||
|
@ -32,13 +46,13 @@ second = SubResource("Resource_an0ef")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_o7cqb"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Second Canvas")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_ataha"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 0
|
||||
percent = 0.980952
|
||||
first = SubResource("Resource_xgnjk")
|
||||
|
@ -46,13 +60,13 @@ second = SubResource("Resource_o7cqb")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_8y4au"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Animation Timeline")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_q2jwk"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 1
|
||||
percent = 0.75578
|
||||
first = SubResource("Resource_ataha")
|
||||
|
@ -60,19 +74,19 @@ second = SubResource("Resource_8y4au")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_5r0ap"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Canvas Preview")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_6pqxe"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Recorder")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_ln20x"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 1
|
||||
percent = 0.911765
|
||||
first = SubResource("Resource_5r0ap")
|
||||
|
@ -80,39 +94,39 @@ second = SubResource("Resource_6pqxe")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_dksrd"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Global Tool Options")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_kmey0"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Color Picker", "Reference Images")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_1tm61"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 1
|
||||
percent = 0.134307
|
||||
percent = 0.0499712
|
||||
first = SubResource("Resource_dksrd")
|
||||
second = SubResource("Resource_kmey0")
|
||||
|
||||
[sub_resource type="Resource" id="Resource_btl4b"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Left Tool Options")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_eu0mc"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Right Tool Options")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_8ff4m"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 0
|
||||
percent = 0.5
|
||||
first = SubResource("Resource_btl4b")
|
||||
|
@ -120,21 +134,21 @@ second = SubResource("Resource_eu0mc")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_e72nu"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 1
|
||||
percent = 0.660142
|
||||
percent = 0.643859
|
||||
first = SubResource("Resource_1tm61")
|
||||
second = SubResource("Resource_8ff4m")
|
||||
|
||||
[sub_resource type="Resource" id="Resource_sg54a"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Palettes")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_gdwmg"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 1
|
||||
percent = 0.82948
|
||||
first = SubResource("Resource_e72nu")
|
||||
|
@ -142,7 +156,7 @@ second = SubResource("Resource_sg54a")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_acda3"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 1
|
||||
percent = 0.0549133
|
||||
first = SubResource("Resource_ln20x")
|
||||
|
@ -150,30 +164,31 @@ second = SubResource("Resource_gdwmg")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_2qk0j"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 0
|
||||
percent = 0.731967
|
||||
percent = 0.704098
|
||||
first = SubResource("Resource_q2jwk")
|
||||
second = SubResource("Resource_acda3")
|
||||
|
||||
[sub_resource type="Resource" id="Resource_msuil"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 0
|
||||
percent = 0.0
|
||||
first = SubResource("Resource_atmme")
|
||||
first = SubResource("Resource_epagr")
|
||||
second = SubResource("Resource_2qk0j")
|
||||
|
||||
[resource]
|
||||
resource_name = "Default"
|
||||
script = ExtResource("3")
|
||||
script = ExtResource("3_4h5wj")
|
||||
root = SubResource("Resource_msuil")
|
||||
hidden_tabs = {
|
||||
"Canvas Preview": true,
|
||||
"Color Picker Sliders": true,
|
||||
"Perspective Editor": true,
|
||||
"Recorder": true,
|
||||
"Second Canvas": true
|
||||
"Second Canvas": true,
|
||||
"Tiles": true
|
||||
}
|
||||
windows = {}
|
||||
save_on_change = false
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
[gd_resource type="Resource" script_class="DockableLayout" load_steps=23 format=3 uid="uid://brcnmadkdaqok"]
|
||||
[gd_resource type="Resource" script_class="DockableLayout" load_steps=25 format=3 uid="uid://brcnmadkdaqok"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_panel.gd" id="1_nokpu"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_split.gd" id="2_q5vl6"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout.gd" id="3_ox7l5"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_panel.gd" id="1_t44r1"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_split.gd" id="2_rngtv"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout.gd" id="3_v86xb"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_kn4x4"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Main Canvas")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_btw27"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Second Canvas")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_bp28t"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 0
|
||||
percent = 0.829091
|
||||
first = SubResource("Resource_kn4x4")
|
||||
|
@ -26,13 +26,13 @@ second = SubResource("Resource_btw27")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_10g0s"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Perspective Editor")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_otntk"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 0
|
||||
percent = 0.8625
|
||||
first = SubResource("Resource_bp28t")
|
||||
|
@ -40,25 +40,25 @@ second = SubResource("Resource_10g0s")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_12axs"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Tools")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_1omiw"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Left Tool Options", "Right Tool Options")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_p32ds"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Color Picker")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_n6xyc"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 0
|
||||
percent = 0.5
|
||||
first = SubResource("Resource_1omiw")
|
||||
|
@ -66,19 +66,19 @@ second = SubResource("Resource_p32ds")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_1dcep"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Canvas Preview", "Reference Images", "Recorder")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_hc3ve"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Global Tool Options")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_nppps"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 1
|
||||
percent = 0.729839
|
||||
first = SubResource("Resource_1dcep")
|
||||
|
@ -86,13 +86,13 @@ second = SubResource("Resource_hc3ve")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_d54jb"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Palettes")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_f6rik"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 0
|
||||
percent = 0.5
|
||||
first = SubResource("Resource_nppps")
|
||||
|
@ -100,7 +100,7 @@ second = SubResource("Resource_d54jb")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_26vov"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 0
|
||||
percent = 0.501251
|
||||
first = SubResource("Resource_n6xyc")
|
||||
|
@ -108,21 +108,35 @@ second = SubResource("Resource_f6rik")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_m3axb"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Animation Timeline")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_8dhxy"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Tiles")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_j3q3h"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 0
|
||||
percent = 0.5
|
||||
first = SubResource("Resource_m3axb")
|
||||
second = SubResource("Resource_8dhxy")
|
||||
|
||||
[sub_resource type="Resource" id="Resource_af0bk"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 1
|
||||
percent = 0.5
|
||||
first = SubResource("Resource_26vov")
|
||||
second = SubResource("Resource_m3axb")
|
||||
second = SubResource("Resource_j3q3h")
|
||||
|
||||
[sub_resource type="Resource" id="Resource_1xpva"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 0
|
||||
percent = 0.03125
|
||||
first = SubResource("Resource_12axs")
|
||||
|
@ -130,7 +144,7 @@ second = SubResource("Resource_af0bk")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_6dytr"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 1
|
||||
percent = 0.459538
|
||||
first = SubResource("Resource_otntk")
|
||||
|
@ -138,12 +152,13 @@ second = SubResource("Resource_1xpva")
|
|||
|
||||
[resource]
|
||||
resource_name = "Tallscreen"
|
||||
script = ExtResource("3_ox7l5")
|
||||
script = ExtResource("3_v86xb")
|
||||
root = SubResource("Resource_6dytr")
|
||||
hidden_tabs = {
|
||||
"Perspective Editor": true,
|
||||
"Recorder": true,
|
||||
"Second Canvas": true
|
||||
"Second Canvas": true,
|
||||
"Tiles": true
|
||||
}
|
||||
windows = {}
|
||||
save_on_change = false
|
||||
|
|
|
@ -27,10 +27,6 @@ config/windows_native_icon="res://assets/graphics/icons/icon.ico"
|
|||
config/ExtensionsAPI_Version=5
|
||||
config/Pxo_Version=4
|
||||
|
||||
[audio]
|
||||
|
||||
driver/driver="Dummy"
|
||||
|
||||
[autoload]
|
||||
|
||||
Global="*res://src/Autoload/Global.gd"
|
||||
|
@ -689,6 +685,10 @@ adjust_brightness_contrast={
|
|||
"deadzone": 0.5,
|
||||
"events": []
|
||||
}
|
||||
color_curves={
|
||||
"deadzone": 0.5,
|
||||
"events": []
|
||||
}
|
||||
gradient={
|
||||
"deadzone": 0.5,
|
||||
"events": []
|
||||
|
@ -908,7 +908,7 @@ previous_project={
|
|||
}
|
||||
center_canvas={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":67,"physical_keycode":0,"key_label":0,"unicode":67,"location":0,"echo":false,"script":null)
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":66,"physical_keycode":0,"key_label":0,"unicode":66,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
left_text_tool={
|
||||
|
@ -921,6 +921,50 @@ right_text_tool={
|
|||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
show_pixel_indices={
|
||||
"deadzone": 0.5,
|
||||
"events": []
|
||||
}
|
||||
toggle_draw_tiles_mode={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
tile_edit_mode_manual={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":49,"key_label":0,"unicode":33,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
tile_edit_mode_auto={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":50,"key_label":0,"unicode":64,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
tile_edit_mode_stack={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":51,"key_label":0,"unicode":35,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
tile_rotate_left={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":90,"key_label":0,"unicode":90,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
tile_rotate_right={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":88,"key_label":0,"unicode":88,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
tile_flip_horizontal={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":67,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
tile_flip_vertical={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":86,"key_label":0,"unicode":86,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[input_devices]
|
||||
|
||||
|
|
|
@ -127,7 +127,7 @@ func get_ellipse_points(pos: Vector2i, size: Vector2i) -> Array[Vector2i]:
|
|||
var y0 := pos.y
|
||||
var y1 := pos.y + (size.y - 1)
|
||||
var a := absi(x1 - x0)
|
||||
var b := absi(y1 - x0)
|
||||
var b := absi(y1 - y0)
|
||||
var b1 := b & 1
|
||||
var dx := 4 * (1 - a) * b * b
|
||||
var dy := 4 * (b1 + 1) * a * a
|
||||
|
@ -217,7 +217,7 @@ func get_ellipse_points_filled(pos: Vector2i, size: Vector2i, thickness := 1) ->
|
|||
|
||||
func scale_3x(sprite: Image, tol := 0.196078) -> Image:
|
||||
var scaled := Image.create(
|
||||
sprite.get_width() * 3, sprite.get_height() * 3, false, Image.FORMAT_RGBA8
|
||||
sprite.get_width() * 3, sprite.get_height() * 3, sprite.has_mipmaps(), sprite.get_format()
|
||||
)
|
||||
var width_minus_one := sprite.get_width() - 1
|
||||
var height_minus_one := sprite.get_height() - 1
|
||||
|
@ -509,6 +509,8 @@ func similar_colors(c1: Color, c2: Color, tol := 0.392157) -> bool:
|
|||
func center(indices: Array) -> void:
|
||||
var project := Global.current_project
|
||||
Global.canvas.selection.transform_content_confirm()
|
||||
var redo_data := {}
|
||||
var undo_data := {}
|
||||
project.undos += 1
|
||||
project.undo_redo.create_action("Center Frames")
|
||||
for frame in indices:
|
||||
|
@ -528,62 +530,88 @@ func center(indices: Array) -> void:
|
|||
for cel in project.frames[frame].cels:
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
var sprite := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
|
||||
sprite.blend_rect(cel.image, used_rect, offset)
|
||||
Global.undo_redo_compress_images({cel.image: sprite.data}, {cel.image: cel.image.data})
|
||||
var cel_image := (cel as PixelCel).get_image()
|
||||
var tmp_centered := project.new_empty_image()
|
||||
tmp_centered.blend_rect(cel.image, used_rect, offset)
|
||||
var centered := ImageExtended.new()
|
||||
centered.copy_from_custom(tmp_centered, cel_image.is_indexed)
|
||||
if cel is CelTileMap:
|
||||
(cel as CelTileMap).serialize_undo_data_source_image(centered, redo_data, undo_data)
|
||||
centered.add_data_to_dictionary(redo_data, cel_image)
|
||||
cel_image.add_data_to_dictionary(undo_data)
|
||||
project.deserialize_cel_undo_data(redo_data, undo_data)
|
||||
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
|
||||
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
|
||||
project.undo_redo.commit_action()
|
||||
|
||||
|
||||
func scale_image(width: int, height: int, interpolation: int) -> void:
|
||||
func scale_project(width: int, height: int, interpolation: int) -> void:
|
||||
var redo_data := {}
|
||||
var undo_data := {}
|
||||
for f in Global.current_project.frames:
|
||||
for i in range(f.cels.size() - 1, -1, -1):
|
||||
var cel := f.cels[i]
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
var sprite := Image.new()
|
||||
sprite.copy_from(cel.get_image())
|
||||
if interpolation == Interpolation.SCALE3X:
|
||||
var times := Vector2i(
|
||||
ceili(width / (3.0 * sprite.get_width())),
|
||||
ceili(height / (3.0 * sprite.get_height()))
|
||||
)
|
||||
for _j in range(maxi(times.x, times.y)):
|
||||
sprite.copy_from(scale_3x(sprite))
|
||||
sprite.resize(width, height, Image.INTERPOLATE_NEAREST)
|
||||
elif interpolation == Interpolation.CLEANEDGE:
|
||||
var gen := ShaderImageEffect.new()
|
||||
gen.generate_image(sprite, clean_edge_shader, {}, Vector2i(width, height))
|
||||
elif interpolation == Interpolation.OMNISCALE and omniscale_shader:
|
||||
var gen := ShaderImageEffect.new()
|
||||
gen.generate_image(sprite, omniscale_shader, {}, Vector2i(width, height))
|
||||
else:
|
||||
sprite.resize(width, height, interpolation)
|
||||
redo_data[cel.image] = sprite.data
|
||||
undo_data[cel.image] = cel.image.data
|
||||
for cel in Global.current_project.get_all_pixel_cels():
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
var cel_image := (cel as PixelCel).get_image()
|
||||
var sprite := _resize_image(cel_image, width, height, interpolation) as ImageExtended
|
||||
if cel is CelTileMap:
|
||||
(cel as CelTileMap).serialize_undo_data_source_image(sprite, redo_data, undo_data)
|
||||
sprite.add_data_to_dictionary(redo_data, cel_image)
|
||||
cel_image.add_data_to_dictionary(undo_data)
|
||||
|
||||
general_do_and_undo_scale(width, height, redo_data, undo_data)
|
||||
|
||||
|
||||
func _resize_image(
|
||||
image: Image, width: int, height: int, interpolation: Image.Interpolation
|
||||
) -> Image:
|
||||
var new_image: Image
|
||||
if image is ImageExtended:
|
||||
new_image = ImageExtended.new()
|
||||
new_image.is_indexed = image.is_indexed
|
||||
new_image.copy_from(image)
|
||||
new_image.select_palette("", false)
|
||||
else:
|
||||
new_image = Image.new()
|
||||
new_image.copy_from(image)
|
||||
if interpolation == Interpolation.SCALE3X:
|
||||
var times := Vector2i(
|
||||
ceili(width / (3.0 * new_image.get_width())),
|
||||
ceili(height / (3.0 * new_image.get_height()))
|
||||
)
|
||||
for _j in range(maxi(times.x, times.y)):
|
||||
new_image.copy_from(scale_3x(new_image))
|
||||
new_image.resize(width, height, Image.INTERPOLATE_NEAREST)
|
||||
elif interpolation == Interpolation.CLEANEDGE:
|
||||
var gen := ShaderImageEffect.new()
|
||||
gen.generate_image(new_image, clean_edge_shader, {}, Vector2i(width, height), false)
|
||||
elif interpolation == Interpolation.OMNISCALE and omniscale_shader:
|
||||
var gen := ShaderImageEffect.new()
|
||||
gen.generate_image(new_image, omniscale_shader, {}, Vector2i(width, height), false)
|
||||
else:
|
||||
new_image.resize(width, height, interpolation)
|
||||
if new_image is ImageExtended:
|
||||
new_image.on_size_changed()
|
||||
return new_image
|
||||
|
||||
|
||||
## Sets the size of the project to be the same as the size of the active selection.
|
||||
func crop_to_selection() -> void:
|
||||
if not Global.current_project.has_selection:
|
||||
return
|
||||
Global.canvas.selection.transform_content_confirm()
|
||||
var redo_data := {}
|
||||
var undo_data := {}
|
||||
Global.canvas.selection.transform_content_confirm()
|
||||
var rect: Rect2i = Global.canvas.selection.big_bounding_rectangle
|
||||
# Loop through all the cels to crop them
|
||||
for f in Global.current_project.frames:
|
||||
for cel in f.cels:
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
var sprite := cel.get_image().get_region(rect)
|
||||
redo_data[cel.image] = sprite.data
|
||||
undo_data[cel.image] = cel.image.data
|
||||
for cel in Global.current_project.get_all_pixel_cels():
|
||||
var cel_image := cel.get_image()
|
||||
var tmp_cropped := cel_image.get_region(rect)
|
||||
var cropped := ImageExtended.new()
|
||||
cropped.copy_from_custom(tmp_cropped, cel_image.is_indexed)
|
||||
if cel is CelTileMap:
|
||||
(cel as CelTileMap).serialize_undo_data_source_image(cropped, redo_data, undo_data)
|
||||
cropped.add_data_to_dictionary(redo_data, cel_image)
|
||||
cel_image.add_data_to_dictionary(undo_data)
|
||||
|
||||
general_do_and_undo_scale(rect.size.x, rect.size.y, redo_data, undo_data)
|
||||
|
||||
|
@ -593,18 +621,17 @@ func crop_to_selection() -> void:
|
|||
func crop_to_content() -> void:
|
||||
Global.canvas.selection.transform_content_confirm()
|
||||
var used_rect := Rect2i()
|
||||
for f in Global.current_project.frames:
|
||||
for cel in f.cels:
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
var cel_used_rect := cel.get_image().get_used_rect()
|
||||
if cel_used_rect == Rect2i(0, 0, 0, 0): # If the cel has no content
|
||||
continue
|
||||
for cel in Global.current_project.get_all_pixel_cels():
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
var cel_used_rect := cel.get_image().get_used_rect()
|
||||
if cel_used_rect == Rect2i(0, 0, 0, 0): # If the cel has no content
|
||||
continue
|
||||
|
||||
if used_rect == Rect2i(0, 0, 0, 0): # If we still haven't found the first cel with content
|
||||
used_rect = cel_used_rect
|
||||
else:
|
||||
used_rect = used_rect.merge(cel_used_rect)
|
||||
if used_rect == Rect2i(0, 0, 0, 0): # If we still haven't found the first cel with content
|
||||
used_rect = cel_used_rect
|
||||
else:
|
||||
used_rect = used_rect.merge(cel_used_rect)
|
||||
|
||||
# If no layer has any content, just return
|
||||
if used_rect == Rect2i(0, 0, 0, 0):
|
||||
|
@ -615,13 +642,15 @@ func crop_to_content() -> void:
|
|||
var redo_data := {}
|
||||
var undo_data := {}
|
||||
# Loop through all the cels to trim them
|
||||
for f in Global.current_project.frames:
|
||||
for cel in f.cels:
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
var sprite := cel.get_image().get_region(used_rect)
|
||||
redo_data[cel.image] = sprite.data
|
||||
undo_data[cel.image] = cel.image.data
|
||||
for cel in Global.current_project.get_all_pixel_cels():
|
||||
var cel_image := cel.get_image()
|
||||
var tmp_cropped := cel_image.get_region(used_rect)
|
||||
var cropped := ImageExtended.new()
|
||||
cropped.copy_from_custom(tmp_cropped, cel_image.is_indexed)
|
||||
if cel is CelTileMap:
|
||||
(cel as CelTileMap).serialize_undo_data_source_image(cropped, redo_data, undo_data)
|
||||
cropped.add_data_to_dictionary(redo_data, cel_image)
|
||||
cel_image.add_data_to_dictionary(undo_data)
|
||||
|
||||
general_do_and_undo_scale(width, height, redo_data, undo_data)
|
||||
|
||||
|
@ -629,18 +658,19 @@ func crop_to_content() -> void:
|
|||
func resize_canvas(width: int, height: int, offset_x: int, offset_y: int) -> void:
|
||||
var redo_data := {}
|
||||
var undo_data := {}
|
||||
for f in Global.current_project.frames:
|
||||
for cel in f.cels:
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
var sprite := Image.create(width, height, false, Image.FORMAT_RGBA8)
|
||||
sprite.blend_rect(
|
||||
cel.get_image(),
|
||||
Rect2i(Vector2i.ZERO, Global.current_project.size),
|
||||
Vector2i(offset_x, offset_y)
|
||||
)
|
||||
redo_data[cel.image] = sprite.data
|
||||
undo_data[cel.image] = cel.image.data
|
||||
for cel in Global.current_project.get_all_pixel_cels():
|
||||
var cel_image := cel.get_image()
|
||||
var resized := ImageExtended.create_custom(
|
||||
width, height, cel_image.has_mipmaps(), cel_image.get_format(), cel_image.is_indexed
|
||||
)
|
||||
resized.blend_rect(
|
||||
cel_image, Rect2i(Vector2i.ZERO, cel_image.get_size()), Vector2i(offset_x, offset_y)
|
||||
)
|
||||
resized.convert_rgb_to_indexed()
|
||||
if cel is CelTileMap:
|
||||
(cel as CelTileMap).serialize_undo_data_source_image(resized, redo_data, undo_data)
|
||||
resized.add_data_to_dictionary(redo_data, cel_image)
|
||||
cel_image.add_data_to_dictionary(undo_data)
|
||||
|
||||
general_do_and_undo_scale(width, height, redo_data, undo_data)
|
||||
|
||||
|
@ -675,7 +705,7 @@ func general_do_and_undo_scale(
|
|||
project.undo_redo.add_do_property(project, "y_symmetry_point", new_y_symmetry_point)
|
||||
project.undo_redo.add_do_property(project.x_symmetry_axis, "points", new_x_symmetry_axis_points)
|
||||
project.undo_redo.add_do_property(project.y_symmetry_axis, "points", new_y_symmetry_axis_points)
|
||||
Global.undo_redo_compress_images(redo_data, undo_data)
|
||||
project.deserialize_cel_undo_data(redo_data, undo_data)
|
||||
project.undo_redo.add_undo_property(project, "size", project.size)
|
||||
project.undo_redo.add_undo_property(project, "x_symmetry_point", project.x_symmetry_point)
|
||||
project.undo_redo.add_undo_property(project, "y_symmetry_point", project.y_symmetry_point)
|
||||
|
|
|
@ -161,7 +161,7 @@ func cache_blended_frames(project := Global.current_project) -> void:
|
|||
blended_frames.clear()
|
||||
var frames := _calculate_frames(project)
|
||||
for frame in frames:
|
||||
var image := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
|
||||
var image := project.new_empty_image()
|
||||
_blend_layers(image, frame)
|
||||
blended_frames[frame] = image
|
||||
|
||||
|
@ -208,7 +208,7 @@ func process_spritesheet(project := Global.current_project) -> void:
|
|||
spritesheet_columns = temp
|
||||
var width := project.size.x * spritesheet_columns
|
||||
var height := project.size.y * spritesheet_rows
|
||||
var whole_image := Image.create(width, height, false, Image.FORMAT_RGBA8)
|
||||
var whole_image := Image.create(width, height, false, project.get_image_format())
|
||||
var origin := Vector2i.ZERO
|
||||
var hh := 0
|
||||
var vv := 0
|
||||
|
@ -282,15 +282,15 @@ func process_animation(project := Global.current_project) -> void:
|
|||
for cel in frame.cels:
|
||||
var image := Image.new()
|
||||
image.copy_from(cel.get_image())
|
||||
var duration := frame.duration * (1.0 / project.fps)
|
||||
var duration := frame.get_duration_in_seconds(project.fps)
|
||||
processed_images.append(
|
||||
ProcessedImage.new(image, project.frames.find(frame), duration)
|
||||
)
|
||||
else:
|
||||
var image := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
|
||||
var image := project.new_empty_image()
|
||||
image.copy_from(blended_frames[frame])
|
||||
if erase_unselected_area and project.has_selection:
|
||||
var crop := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
|
||||
var crop := project.new_empty_image()
|
||||
var selection_image = project.selection_map.return_cropped_copy(project.size)
|
||||
crop.blit_rect_mask(
|
||||
image, selection_image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO
|
||||
|
@ -298,7 +298,7 @@ func process_animation(project := Global.current_project) -> void:
|
|||
image.copy_from(crop)
|
||||
if trim_images:
|
||||
image = image.get_region(image.get_used_rect())
|
||||
var duration := frame.duration * (1.0 / project.fps)
|
||||
var duration := frame.get_duration_in_seconds(project.fps)
|
||||
processed_images.append(ProcessedImage.new(image, project.frames.find(frame), duration))
|
||||
|
||||
|
||||
|
@ -325,6 +325,8 @@ func _calculate_frames(project := Global.current_project) -> Array[Frame]:
|
|||
var inverted_frames := frames.duplicate()
|
||||
inverted_frames.reverse()
|
||||
inverted_frames.remove_at(0)
|
||||
if inverted_frames.size() > 0:
|
||||
inverted_frames.remove_at(inverted_frames.size() - 1)
|
||||
frames.append_array(inverted_frames)
|
||||
return frames
|
||||
|
||||
|
@ -425,7 +427,7 @@ func export_processed_images(
|
|||
|
||||
if is_single_file_format(project):
|
||||
if is_using_ffmpeg(project.file_format):
|
||||
var video_exported := export_video(export_paths)
|
||||
var video_exported := export_video(export_paths, project)
|
||||
if not video_exported:
|
||||
Global.popup_error(
|
||||
tr("Video failed to export. Ensure that FFMPEG is installed correctly.")
|
||||
|
@ -503,8 +505,9 @@ func export_processed_images(
|
|||
|
||||
|
||||
## Uses FFMPEG to export a video
|
||||
func export_video(export_paths: PackedStringArray) -> bool:
|
||||
func export_video(export_paths: PackedStringArray, project: Project) -> bool:
|
||||
DirAccess.make_dir_absolute(TEMP_PATH)
|
||||
var video_duration := 0
|
||||
var temp_path_real := ProjectSettings.globalize_path(TEMP_PATH)
|
||||
var input_file_path := temp_path_real.path_join("input.txt")
|
||||
var input_file := FileAccess.open(input_file_path, FileAccess.WRITE)
|
||||
|
@ -514,25 +517,80 @@ func export_video(export_paths: PackedStringArray) -> bool:
|
|||
processed_images[i].image.save_png(temp_file_path)
|
||||
input_file.store_line("file '" + temp_file_name + "'")
|
||||
input_file.store_line("duration %s" % processed_images[i].duration)
|
||||
video_duration += processed_images[i].duration
|
||||
input_file.close()
|
||||
|
||||
# ffmpeg -y -f concat -i input.txt output_path
|
||||
var ffmpeg_execute: PackedStringArray = [
|
||||
"-y", "-f", "concat", "-i", input_file_path, export_paths[0]
|
||||
]
|
||||
var output := []
|
||||
var success := OS.execute(Global.ffmpeg_path, ffmpeg_execute, output, true)
|
||||
print(output)
|
||||
var temp_dir := DirAccess.open(TEMP_PATH)
|
||||
for file in temp_dir.get_files():
|
||||
temp_dir.remove(file)
|
||||
DirAccess.remove_absolute(TEMP_PATH)
|
||||
var success := OS.execute(Global.ffmpeg_path, ffmpeg_execute, [], true)
|
||||
if success < 0 or success > 1:
|
||||
var fail_text := """Video failed to export. Make sure you have FFMPEG installed
|
||||
and have set the correct path in the preferences."""
|
||||
Global.popup_error(tr(fail_text))
|
||||
_clear_temp_folder()
|
||||
return false
|
||||
# Find audio layers
|
||||
var ffmpeg_combine_audio: PackedStringArray = ["-y"]
|
||||
var audio_layer_count := 0
|
||||
var max_audio_duration := 0
|
||||
var adelay_string := ""
|
||||
for layer in project.get_all_audio_layers():
|
||||
if layer.audio is AudioStreamMP3:
|
||||
var temp_file_name := str(audio_layer_count + 1).pad_zeros(number_of_digits) + ".mp3"
|
||||
var temp_file_path := temp_path_real.path_join(temp_file_name)
|
||||
var temp_audio_file := FileAccess.open(temp_file_path, FileAccess.WRITE)
|
||||
temp_audio_file.store_buffer(layer.audio.data)
|
||||
ffmpeg_combine_audio.append("-i")
|
||||
ffmpeg_combine_audio.append(temp_file_path)
|
||||
var delay := floori(layer.playback_position * 1000)
|
||||
# [n]adelay=delay_in_ms:all=1[na]
|
||||
adelay_string += (
|
||||
"[%s]adelay=%s:all=1[%sa];" % [audio_layer_count, delay, audio_layer_count]
|
||||
)
|
||||
audio_layer_count += 1
|
||||
if layer.get_audio_length() >= max_audio_duration:
|
||||
max_audio_duration = layer.get_audio_length()
|
||||
if audio_layer_count > 0:
|
||||
# If we have audio layers, merge them all into one file.
|
||||
for i in audio_layer_count:
|
||||
adelay_string += "[%sa]" % i
|
||||
var amix_inputs_string := "amix=inputs=%s[a]" % audio_layer_count
|
||||
var final_filter_string := adelay_string + amix_inputs_string
|
||||
var audio_file_path := temp_path_real.path_join("audio.mp3")
|
||||
ffmpeg_combine_audio.append_array(
|
||||
PackedStringArray(
|
||||
["-filter_complex", final_filter_string, "-map", '"[a]"', audio_file_path]
|
||||
)
|
||||
)
|
||||
# ffmpeg -i input1 -i input2 ... -i inputn -filter_complex amix=inputs=n output_path
|
||||
var combined_audio_success := OS.execute(Global.ffmpeg_path, ffmpeg_combine_audio, [], true)
|
||||
if combined_audio_success == 0 or combined_audio_success == 1:
|
||||
var copied_video := temp_path_real.path_join("video." + export_paths[0].get_extension())
|
||||
# Then mix the audio file with the video.
|
||||
DirAccess.copy_absolute(export_paths[0], copied_video)
|
||||
# ffmpeg -y -i video_file -i input_audio -c:v copy -map 0:v:0 -map 1:a:0 video_file
|
||||
var ffmpeg_final_video: PackedStringArray = [
|
||||
"-y", "-i", copied_video, "-i", audio_file_path
|
||||
]
|
||||
if max_audio_duration > video_duration:
|
||||
ffmpeg_final_video.append("-shortest")
|
||||
ffmpeg_final_video.append_array(
|
||||
["-c:v", "copy", "-map", "0:v:0", "-map", "1:a:0", export_paths[0]]
|
||||
)
|
||||
OS.execute(Global.ffmpeg_path, ffmpeg_final_video, [], true)
|
||||
_clear_temp_folder()
|
||||
return true
|
||||
|
||||
|
||||
func _clear_temp_folder() -> void:
|
||||
var temp_dir := DirAccess.open(TEMP_PATH)
|
||||
for file in temp_dir.get_files():
|
||||
temp_dir.remove(file)
|
||||
DirAccess.remove_absolute(TEMP_PATH)
|
||||
|
||||
|
||||
func export_animated(args: Dictionary) -> void:
|
||||
var project: Project = args["project"]
|
||||
var exporter: AImgIOBaseExporter = args["exporter"]
|
||||
|
|
|
@ -168,11 +168,11 @@ class GeneralAPI:
|
|||
|
||||
## Returns a new ValueSliderV2. Useful for editing 2D vectors.
|
||||
func create_value_slider_v2() -> ValueSliderV2:
|
||||
return preload("res://src/UI/Nodes/ValueSliderV2.tscn").instantiate()
|
||||
return preload("res://src/UI/Nodes/Sliders/ValueSliderV2.tscn").instantiate()
|
||||
|
||||
## Returns a new ValueSliderV3. Useful for editing 3D vectors.
|
||||
func create_value_slider_v3() -> ValueSliderV3:
|
||||
return preload("res://src/UI/Nodes/ValueSliderV3.tscn").instantiate()
|
||||
return preload("res://src/UI/Nodes/Sliders/ValueSliderV3.tscn").instantiate()
|
||||
|
||||
|
||||
## Gives ability to add/remove items from menus in the top bar.
|
||||
|
@ -631,7 +631,7 @@ class ProjectAPI:
|
|||
|
||||
## Returns the current cel.
|
||||
## Cel type can be checked using function [method get_class_name] inside the cel
|
||||
## type can be GroupCel, PixelCel, Cel3D, or BaseCel.
|
||||
## type can be GroupCel, PixelCel, Cel3D, CelTileMap, AudioCel or BaseCel.
|
||||
func get_current_cel() -> BaseCel:
|
||||
return current_project.get_current_cel()
|
||||
|
||||
|
@ -896,7 +896,7 @@ class SignalsAPI:
|
|||
# TOOL RELATED SIGNALS
|
||||
## Connects/disconnects a signal to [param callable], that emits
|
||||
## whenever a tool changes color.[br]
|
||||
## [b]Binds: [/b] It has two bind of type [Color] (indicating new color)
|
||||
## [b]Binds: [/b] It has two bind of type [Color] (a dictionary with keys "color" and "index")
|
||||
## and [int] (Indicating button that tool is assigned to, see [enum @GlobalScope.MouseButton])
|
||||
func signal_tool_color_changed(callable: Callable, is_disconnecting := false) -> void:
|
||||
_connect_disconnect(Tools.color_changed, callable, is_disconnecting)
|
||||
|
|
|
@ -14,7 +14,7 @@ signal cel_switched ## Emitted whenever you select a different cel.
|
|||
signal project_data_changed(project: Project) ## Emitted when project data is modified.
|
||||
signal font_loaded ## Emitted when a new font has been loaded, or an old one gets unloaded.
|
||||
|
||||
enum LayerTypes { PIXEL, GROUP, THREE_D }
|
||||
enum LayerTypes { PIXEL, GROUP, THREE_D, TILEMAP, AUDIO }
|
||||
enum GridTypes { CARTESIAN, ISOMETRIC, ALL }
|
||||
## ## Used to tell whether a color is being taken from the current theme,
|
||||
## or if it is a custom color.
|
||||
|
@ -35,6 +35,7 @@ enum ViewMenu {
|
|||
MIRROR_VIEW,
|
||||
SHOW_GRID,
|
||||
SHOW_PIXEL_GRID,
|
||||
SHOW_PIXEL_INDICES,
|
||||
SHOW_RULERS,
|
||||
SHOW_GUIDES,
|
||||
SHOW_MOUSE_GUIDES,
|
||||
|
@ -46,6 +47,7 @@ enum WindowMenu { WINDOW_OPACITY, PANELS, LAYOUTS, MOVABLE_PANELS, ZEN_MODE, FUL
|
|||
## Enumeration of items present in the Image Menu.
|
||||
enum ImageMenu {
|
||||
PROJECT_PROPERTIES,
|
||||
COLOR_MODE,
|
||||
RESIZE_CANVAS,
|
||||
SCALE_IMAGE,
|
||||
CROP_TO_SELECTION,
|
||||
|
@ -62,16 +64,17 @@ enum EffectsMenu {
|
|||
DESATURATION,
|
||||
HSV,
|
||||
BRIGHTNESS_SATURATION,
|
||||
COLOR_CURVES,
|
||||
PALETTIZE,
|
||||
PIXELIZE,
|
||||
POSTERIZE,
|
||||
GAUSSIAN_BLUR,
|
||||
GRADIENT,
|
||||
GRADIENT_MAP,
|
||||
SHADER
|
||||
LOADED_EFFECTS
|
||||
}
|
||||
## Enumeration of items present in the Select Menu.
|
||||
enum SelectMenu { SELECT_ALL, CLEAR_SELECTION, INVERT, TILE_MODE, MODIFY }
|
||||
enum SelectMenu { SELECT_ALL, CLEAR_SELECTION, INVERT, WRAP_STROKES, MODIFY }
|
||||
## Enumeration of items present in the Help Menu.
|
||||
enum HelpMenu {
|
||||
VIEW_SPLASH_SCREEN,
|
||||
|
@ -178,10 +181,14 @@ var can_draw := true
|
|||
var move_guides_on_canvas := true
|
||||
|
||||
var play_only_tags := true ## If [code]true[/code], animation plays only on frames of the same tag.
|
||||
## (Intended to be used as getter only) Tells if the x-symmetry guide ( -- ) is visible.
|
||||
## If true, the x symmetry guide ( -- ) is visible.
|
||||
var show_x_symmetry_axis := false
|
||||
## (Intended to be used as getter only) Tells if the y-symmetry guide ( | ) is visible.
|
||||
## If true, the y symmetry guide ( | ) is visible.
|
||||
var show_y_symmetry_axis := false
|
||||
## If true, the x=y symmetry guide ( / ) is visible.
|
||||
var show_xy_symmetry_axis := false
|
||||
## If true, the x==y symmetry guide ( \ ) is visible.
|
||||
var show_x_minus_y_symmetry_axis := false
|
||||
|
||||
# Preferences
|
||||
## Found in Preferences. If [code]true[/code], the last saved project will open on startup.
|
||||
|
@ -332,55 +339,8 @@ var default_height := 64 ## Found in Preferences. The default height of startup
|
|||
var default_fill_color := Color(0, 0, 0, 0)
|
||||
## Found in Preferences. The distance to the guide or grig below which cursor snapping activates.
|
||||
var snapping_distance := 32.0
|
||||
## Found in Preferences. The grid type defined by [enum GridTypes] enum.
|
||||
var grid_type := GridTypes.CARTESIAN:
|
||||
set(value):
|
||||
if value == grid_type:
|
||||
return
|
||||
grid_type = value
|
||||
if is_instance_valid(canvas.grid):
|
||||
canvas.grid.queue_redraw()
|
||||
## Found in Preferences. The size of rectangular grid.
|
||||
var grid_size := Vector2i(2, 2):
|
||||
set(value):
|
||||
if value == grid_size:
|
||||
return
|
||||
grid_size = value
|
||||
if is_instance_valid(canvas.grid):
|
||||
canvas.grid.queue_redraw()
|
||||
## Found in Preferences. The size of isometric grid.
|
||||
var isometric_grid_size := Vector2i(16, 8):
|
||||
set(value):
|
||||
if value == isometric_grid_size:
|
||||
return
|
||||
isometric_grid_size = value
|
||||
if is_instance_valid(canvas.grid):
|
||||
canvas.grid.queue_redraw()
|
||||
## Found in Preferences. The grid offset from top-left corner of the canvas.
|
||||
var grid_offset := Vector2i.ZERO:
|
||||
set(value):
|
||||
if value == grid_offset:
|
||||
return
|
||||
grid_offset = value
|
||||
if is_instance_valid(canvas.grid):
|
||||
canvas.grid.queue_redraw()
|
||||
## Found in Preferences. If [code]true[/code], The grid draws over the area extended by
|
||||
## tile-mode as well.
|
||||
var grid_draw_over_tile_mode := false:
|
||||
set(value):
|
||||
if value == grid_draw_over_tile_mode:
|
||||
return
|
||||
grid_draw_over_tile_mode = value
|
||||
if is_instance_valid(canvas.grid):
|
||||
canvas.grid.queue_redraw()
|
||||
## Found in Preferences. The color of grid.
|
||||
var grid_color := Color.BLACK:
|
||||
set(value):
|
||||
if value == grid_color:
|
||||
return
|
||||
grid_color = value
|
||||
if is_instance_valid(canvas.grid):
|
||||
canvas.grid.queue_redraw()
|
||||
## Contains dictionaries of individual grids.
|
||||
var grids: Array[Grid] = []
|
||||
## Found in Preferences. The minimum zoom after which pixel grid gets drawn if enabled.
|
||||
var pixel_grid_show_at_zoom := 1500.0: # percentage
|
||||
set(value):
|
||||
|
@ -531,6 +491,11 @@ var window_transparency := false:
|
|||
return
|
||||
window_transparency = value
|
||||
_save_to_override_file()
|
||||
var dummy_audio_driver := false:
|
||||
set(value):
|
||||
if value != dummy_audio_driver:
|
||||
dummy_audio_driver = value
|
||||
_save_to_override_file()
|
||||
|
||||
## Found in Preferences. The time (in minutes) after which backup is created (if enabled).
|
||||
var autosave_interval := 1.0:
|
||||
|
@ -597,6 +562,12 @@ var show_rulers := true:
|
|||
var show_guides := true
|
||||
## If [code]true[/code], the mouse guides are visible.
|
||||
var show_mouse_guides := false
|
||||
## If [code]true[/code], the indices of color are shown.
|
||||
var show_pixel_indices := false:
|
||||
set(value):
|
||||
show_pixel_indices = value
|
||||
if is_instance_valid(canvas.color_index):
|
||||
canvas.color_index.enabled = value
|
||||
var display_layer_effects := true:
|
||||
set(value):
|
||||
if value == display_layer_effects:
|
||||
|
@ -672,6 +643,62 @@ var cel_button_scene: PackedScene = load("res://src/UI/Timeline/CelButton.tscn")
|
|||
@onready var error_dialog: AcceptDialog = control.find_child("ErrorDialog")
|
||||
|
||||
|
||||
class Grid:
|
||||
var grid_type := GridTypes.CARTESIAN:
|
||||
set(value):
|
||||
if value == grid_type:
|
||||
return
|
||||
grid_type = value
|
||||
if is_instance_valid(Global.canvas.grid):
|
||||
Global.canvas.grid.queue_redraw()
|
||||
## Found in Preferences. The size of rectangular grid.
|
||||
var grid_size := Vector2i(2, 2):
|
||||
set(value):
|
||||
if value == grid_size:
|
||||
return
|
||||
grid_size = value
|
||||
if is_instance_valid(Global.canvas.grid):
|
||||
Global.canvas.grid.queue_redraw()
|
||||
## Found in Preferences. The size of isometric grid.
|
||||
var isometric_grid_size := Vector2i(16, 8):
|
||||
set(value):
|
||||
if value == isometric_grid_size:
|
||||
return
|
||||
isometric_grid_size = value
|
||||
if is_instance_valid(Global.canvas.grid):
|
||||
Global.canvas.grid.queue_redraw()
|
||||
## Found in Preferences. The grid offset from top-left corner of the canvas.
|
||||
var grid_offset := Vector2i.ZERO:
|
||||
set(value):
|
||||
if value == grid_offset:
|
||||
return
|
||||
grid_offset = value
|
||||
if is_instance_valid(Global.canvas.grid):
|
||||
Global.canvas.grid.queue_redraw()
|
||||
## Found in Preferences. If [code]true[/code], The grid draws over the area extended by
|
||||
## tile-mode as well.
|
||||
var grid_draw_over_tile_mode := false:
|
||||
set(value):
|
||||
if value == grid_draw_over_tile_mode:
|
||||
return
|
||||
grid_draw_over_tile_mode = value
|
||||
if is_instance_valid(Global.canvas.grid):
|
||||
Global.canvas.grid.queue_redraw()
|
||||
## Found in Preferences. The color of grid.
|
||||
var grid_color := Color.BLACK:
|
||||
set(value):
|
||||
if value == grid_color:
|
||||
return
|
||||
grid_color = value
|
||||
if is_instance_valid(Global.canvas.grid):
|
||||
Global.canvas.grid.queue_redraw()
|
||||
|
||||
func _init(properties := {}) -> void:
|
||||
Global.grids.append(self)
|
||||
for prop in properties.keys():
|
||||
set(prop, properties[prop])
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
# Load settings from the config file
|
||||
config_cache.load(CONFIG_PATH)
|
||||
|
@ -705,9 +732,12 @@ func _init() -> void:
|
|||
window_transparency = ProjectSettings.get_setting(
|
||||
"display/window/per_pixel_transparency/allowed"
|
||||
)
|
||||
dummy_audio_driver = ProjectSettings.get_setting("audio/driver/driver") == "Dummy"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Initialize Grid
|
||||
Grid.new() # gets auto added to grids array
|
||||
_initialize_keychain()
|
||||
default_width = config_cache.get_value("preferences", "default_width", default_width)
|
||||
default_height = config_cache.get_value("preferences", "default_height", default_height)
|
||||
|
@ -724,11 +754,26 @@ func _ready() -> void:
|
|||
if get(pref) == null:
|
||||
continue
|
||||
var value = config_cache.get_value("preferences", pref)
|
||||
set(pref, value)
|
||||
if pref == "grids":
|
||||
if value:
|
||||
update_grids(value)
|
||||
else:
|
||||
set(pref, value)
|
||||
if OS.is_sandboxed():
|
||||
Global.use_native_file_dialogs = true
|
||||
await get_tree().process_frame
|
||||
project_switched.emit()
|
||||
canvas.color_index.enabled = show_pixel_indices # Initialize color index preview
|
||||
|
||||
|
||||
func update_grids(grids_data: Dictionary):
|
||||
# Remove old grids
|
||||
grids.clear()
|
||||
if is_instance_valid(Global.canvas.grid):
|
||||
Global.canvas.grid.queue_redraw()
|
||||
# ADD new ones
|
||||
for grid_idx in grids_data.size():
|
||||
Grid.new(grids_data[grid_idx]) # gets auto added to grids array
|
||||
|
||||
|
||||
func _initialize_keychain() -> void:
|
||||
|
@ -765,6 +810,7 @@ func _initialize_keychain() -> void:
|
|||
&"drop_shadow": Keychain.InputAction.new("", "Effects menu", true),
|
||||
&"adjust_hsv": Keychain.InputAction.new("", "Effects menu", true),
|
||||
&"adjust_brightness_contrast": Keychain.InputAction.new("", "Effects menu", true),
|
||||
&"color_curves": Keychain.InputAction.new("", "Effects menu", true),
|
||||
&"gaussian_blur": Keychain.InputAction.new("", "Effects menu", true),
|
||||
&"gradient": Keychain.InputAction.new("", "Effects menu", true),
|
||||
&"gradient_map": Keychain.InputAction.new("", "Effects menu", true),
|
||||
|
@ -859,12 +905,17 @@ func _initialize_keychain() -> void:
|
|||
&"reference_rotate": Keychain.InputAction.new("", "Reference images", false),
|
||||
&"reference_scale": Keychain.InputAction.new("", "Reference images", false),
|
||||
&"reference_quick_menu": Keychain.InputAction.new("", "Reference images", false),
|
||||
&"cancel_reference_transform": Keychain.InputAction.new("", "Reference images", false)
|
||||
&"cancel_reference_transform": Keychain.InputAction.new("", "Reference images", false),
|
||||
&"tile_rotate_left": Keychain.InputAction.new("", "Tileset panel", false),
|
||||
&"tile_rotate_right": Keychain.InputAction.new("", "Tileset panel", false),
|
||||
&"tile_flip_horizontal": Keychain.InputAction.new("", "Tileset panel", false),
|
||||
&"tile_flip_vertical": Keychain.InputAction.new("", "Tileset panel", false)
|
||||
}
|
||||
|
||||
Keychain.groups = {
|
||||
"Canvas": Keychain.InputGroup.new("", false),
|
||||
"Cursor movement": Keychain.InputGroup.new("Canvas"),
|
||||
"Reference images": Keychain.InputGroup.new("Canvas"),
|
||||
"Buttons": Keychain.InputGroup.new(),
|
||||
"Tools": Keychain.InputGroup.new(),
|
||||
"Left": Keychain.InputGroup.new("Tools"),
|
||||
|
@ -883,7 +934,7 @@ func _initialize_keychain() -> void:
|
|||
"Shape tools": Keychain.InputGroup.new("Tool modifiers"),
|
||||
"Selection tools": Keychain.InputGroup.new("Tool modifiers"),
|
||||
"Transformation tools": Keychain.InputGroup.new("Tool modifiers"),
|
||||
"Reference images": Keychain.InputGroup.new("Canvas")
|
||||
"Tileset panel": Keychain.InputGroup.new()
|
||||
}
|
||||
Keychain.ignore_actions = ["left_mouse", "right_mouse", "middle_mouse", "shift", "ctrl"]
|
||||
|
||||
|
@ -916,7 +967,7 @@ func general_redo(project := current_project) -> void:
|
|||
## Performs actions done after an undo or redo is done. this takes [member general_undo] and
|
||||
## [member general_redo] a step further. Does further work if the current action requires it
|
||||
## like refreshing textures, redraw UI elements etc...[br]
|
||||
## [param frame_index] and [param layer_index] are there for optimizzation. if the undo or redo
|
||||
## [param frame_index] and [param layer_index] are there for optimization. if the undo or redo
|
||||
## happens only in one cel then the cel's frame and layer should be passed to [param frame_index]
|
||||
## and [param layer_index] respectively, otherwise the entire timeline will be refreshed.
|
||||
func undo_or_redo(
|
||||
|
@ -942,20 +993,24 @@ func undo_or_redo(
|
|||
]
|
||||
):
|
||||
if layer_index > -1 and frame_index > -1:
|
||||
canvas.update_texture(layer_index, frame_index, project)
|
||||
var cel := project.frames[frame_index].cels[layer_index]
|
||||
if action_name == "Scale":
|
||||
cel.size_changed(project.size)
|
||||
canvas.update_texture(layer_index, frame_index, project, undo)
|
||||
else:
|
||||
for i in project.frames.size():
|
||||
for j in project.layers.size():
|
||||
canvas.update_texture(j, i, project)
|
||||
var cel := project.frames[i].cels[j]
|
||||
if action_name == "Scale":
|
||||
cel.size_changed(project.size)
|
||||
canvas.update_texture(j, i, project, undo)
|
||||
|
||||
canvas.selection.queue_redraw()
|
||||
if action_name == "Scale":
|
||||
for i in project.frames.size():
|
||||
for j in project.layers.size():
|
||||
var current_cel := project.frames[i].cels[j]
|
||||
if current_cel is Cel3D:
|
||||
current_cel.size_changed(project.size)
|
||||
else:
|
||||
if current_cel is not Cel3D:
|
||||
current_cel.image_texture.set_image(current_cel.get_image())
|
||||
canvas.camera_zoom()
|
||||
canvas.grid.queue_redraw()
|
||||
|
@ -1061,7 +1116,9 @@ func get_available_font_names() -> PackedStringArray:
|
|||
if font_name in font_names:
|
||||
continue
|
||||
font_names.append(font_name)
|
||||
for system_font_name in OS.get_system_fonts():
|
||||
var system_fonts := OS.get_system_fonts()
|
||||
system_fonts.sort()
|
||||
for system_font_name in system_fonts:
|
||||
if system_font_name in font_names:
|
||||
continue
|
||||
font_names.append(system_font_name)
|
||||
|
@ -1113,8 +1170,17 @@ func undo_redo_compress_images(
|
|||
func undo_redo_draw_op(
|
||||
image: Image, new_size: Vector2i, compressed_image_data: PackedByteArray, buffer_size: int
|
||||
) -> void:
|
||||
var decompressed := compressed_image_data.decompress(buffer_size)
|
||||
image.set_data(new_size.x, new_size.y, image.has_mipmaps(), image.get_format(), decompressed)
|
||||
if image is ImageExtended and image.is_indexed:
|
||||
# If using indexed mode,
|
||||
# just convert the indices to RGB instead of setting the image data directly.
|
||||
if image.get_size() != new_size:
|
||||
image.crop(new_size.x, new_size.y)
|
||||
image.convert_indexed_to_rgb()
|
||||
else:
|
||||
var decompressed := compressed_image_data.decompress(buffer_size)
|
||||
image.set_data(
|
||||
new_size.x, new_size.y, image.has_mipmaps(), image.get_format(), decompressed
|
||||
)
|
||||
|
||||
|
||||
## This method is used to write project setting overrides to the override.cfg file, located
|
||||
|
@ -1129,3 +1195,6 @@ func _save_to_override_file() -> void:
|
|||
file.store_line("[display]\n")
|
||||
file.store_line("window/subwindows/embed_subwindows=%s" % single_window_mode)
|
||||
file.store_line("window/per_pixel_transparency/allowed=%s" % window_transparency)
|
||||
if dummy_audio_driver:
|
||||
file.store_line("[audio]\n")
|
||||
file.store_line('driver/driver="Dummy"')
|
||||
|
|
|
@ -90,9 +90,9 @@ func get_brush_files_from_directory(directory: String): # -> Array
|
|||
func add_randomised_brush(fpaths: Array, tooltip_name: String) -> void:
|
||||
# Attempt to load the images from the file paths.
|
||||
var loaded_images: Array = []
|
||||
for filen in fpaths:
|
||||
for file in fpaths:
|
||||
var image := Image.new()
|
||||
var err := image.load(filen)
|
||||
var err := image.load(file)
|
||||
if err == OK:
|
||||
image.convert(Image.FORMAT_RGBA8)
|
||||
loaded_images.append(image)
|
||||
|
|
|
@ -3,6 +3,9 @@ extends Node
|
|||
|
||||
signal project_saved
|
||||
signal reference_image_imported
|
||||
signal shader_copied(file_path: String)
|
||||
|
||||
const SHADERS_DIRECTORY := "user://shaders"
|
||||
|
||||
var preview_dialog_tscn := preload("res://src/UI/Dialogs/ImportPreviewDialog.tscn")
|
||||
var preview_dialogs := [] ## Array of preview dialogs
|
||||
|
@ -29,7 +32,11 @@ func handle_loading_file(file: String) -> void:
|
|||
open_pxo_file(file)
|
||||
|
||||
elif file_ext == "tres": # Godot resource file
|
||||
return
|
||||
var resource := load(file)
|
||||
if resource is VisualShader:
|
||||
var new_path := SHADERS_DIRECTORY.path_join(file.get_file())
|
||||
DirAccess.copy_absolute(file, new_path)
|
||||
shader_copied.emit(new_path)
|
||||
elif file_ext == "tscn": # Godot scene file
|
||||
return
|
||||
|
||||
|
@ -39,12 +46,15 @@ func handle_loading_file(file: String) -> void:
|
|||
elif file_ext in ["pck", "zip"]: # Godot resource pack file
|
||||
Global.control.get_node("Extensions").install_extension(file)
|
||||
|
||||
elif file_ext == "shader" or file_ext == "gdshader": # Godot shader file
|
||||
elif file_ext == "gdshader": # Godot shader file
|
||||
var shader := load(file)
|
||||
if not shader is Shader:
|
||||
return
|
||||
var file_name: String = file.get_file().get_basename()
|
||||
Global.control.find_child("ShaderEffect").change_shader(shader, file_name)
|
||||
var new_path := SHADERS_DIRECTORY.path_join(file.get_file())
|
||||
DirAccess.copy_absolute(file, new_path)
|
||||
shader_copied.emit(new_path)
|
||||
elif file_ext == "mp3": # Audio file
|
||||
open_audio_file(file)
|
||||
|
||||
else: # Image files
|
||||
# Attempt to load as APNG.
|
||||
|
@ -150,7 +160,7 @@ func handle_loading_aimg(path: String, frames: Array) -> void:
|
|||
if not frames_agree:
|
||||
frame.duration = aimg_frame.duration * project.fps
|
||||
var content := aimg_frame.content
|
||||
content.convert(Image.FORMAT_RGBA8)
|
||||
content.convert(project.get_image_format())
|
||||
frame.cels.append(PixelCel.new(content, 1))
|
||||
project.frames.append(frame)
|
||||
|
||||
|
@ -185,8 +195,8 @@ func handle_loading_video(file: String) -> bool:
|
|||
project_size.x = temp_image.get_width()
|
||||
if temp_image.get_height() > project_size.y:
|
||||
project_size.y = temp_image.get_height()
|
||||
DirAccess.remove_absolute(Export.TEMP_PATH)
|
||||
if images_to_import.size() == 0 or project_size == Vector2i.ZERO:
|
||||
DirAccess.remove_absolute(Export.TEMP_PATH)
|
||||
return false # We didn't find any images, return
|
||||
# If we found images, create a new project out of them
|
||||
var new_project := Project.new([], file.get_basename().get_file(), project_size)
|
||||
|
@ -196,6 +206,14 @@ func handle_loading_video(file: String) -> bool:
|
|||
Global.projects.append(new_project)
|
||||
Global.tabs.current_tab = Global.tabs.get_tab_count() - 1
|
||||
Global.canvas.camera_zoom()
|
||||
var output_audio_file := temp_path_real.path_join("audio.mp3")
|
||||
# ffmpeg -y -i input_file -vn audio.mp3
|
||||
var ffmpeg_execute_audio: PackedStringArray = ["-y", "-i", file, "-vn", output_audio_file]
|
||||
OS.execute(Global.ffmpeg_path, ffmpeg_execute_audio, [], true)
|
||||
if FileAccess.file_exists(output_audio_file):
|
||||
open_audio_file(output_audio_file)
|
||||
temp_dir.remove("audio.mp3")
|
||||
DirAccess.remove_absolute(Export.TEMP_PATH)
|
||||
return true
|
||||
|
||||
|
||||
|
@ -258,6 +276,18 @@ func open_pxo_file(path: String, is_backup := false, replace_empty := true) -> v
|
|||
new_project.tiles.tile_mask = image
|
||||
else:
|
||||
new_project.tiles.reset_mask()
|
||||
if result.has("tilesets"):
|
||||
for i in result.tilesets.size():
|
||||
var tileset_dict: Dictionary = result.tilesets[i]
|
||||
var tileset := new_project.tilesets[i]
|
||||
var tile_size := tileset.tile_size
|
||||
var tile_amount: int = tileset_dict.tile_amount
|
||||
for j in tile_amount:
|
||||
var image_data := zip_reader.read_file("tilesets/%s/%s" % [i, j])
|
||||
var image := Image.create_from_data(
|
||||
tile_size.x, tile_size.y, false, new_project.get_image_format(), image_data
|
||||
)
|
||||
tileset.add_tile(image, null)
|
||||
zip_reader.close()
|
||||
new_project.export_directory_path = path.get_base_dir()
|
||||
|
||||
|
@ -389,18 +419,23 @@ func save_pxo_file(
|
|||
var frame_index := 1
|
||||
for frame in project.frames:
|
||||
if not autosave and include_blended:
|
||||
var blended := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
|
||||
var blended := project.new_empty_image()
|
||||
DrawingAlgos.blend_layers(blended, frame, Vector2i.ZERO, project)
|
||||
zip_packer.start_file("image_data/final_images/%s" % frame_index)
|
||||
zip_packer.write_file(blended.get_data())
|
||||
zip_packer.close_file()
|
||||
var cel_index := 1
|
||||
for cel in frame.cels:
|
||||
var cel_image := cel.get_image()
|
||||
var cel_image := cel.get_image() as ImageExtended
|
||||
if is_instance_valid(cel_image) and cel is PixelCel:
|
||||
zip_packer.start_file("image_data/frames/%s/layer_%s" % [frame_index, cel_index])
|
||||
zip_packer.write_file(cel_image.get_data())
|
||||
zip_packer.close_file()
|
||||
zip_packer.start_file(
|
||||
"image_data/frames/%s/indices_layer_%s" % [frame_index, cel_index]
|
||||
)
|
||||
zip_packer.write_file(cel_image.indices_image.get_data())
|
||||
zip_packer.close_file()
|
||||
cel_index += 1
|
||||
frame_index += 1
|
||||
var brush_index := 0
|
||||
|
@ -413,6 +448,22 @@ func save_pxo_file(
|
|||
zip_packer.start_file("image_data/tile_map")
|
||||
zip_packer.write_file(project.tiles.tile_mask.get_data())
|
||||
zip_packer.close_file()
|
||||
for i in project.tilesets.size():
|
||||
var tileset := project.tilesets[i]
|
||||
var tileset_path := "tilesets/%s" % i
|
||||
for j in tileset.tiles.size():
|
||||
var tile := tileset.tiles[j]
|
||||
zip_packer.start_file(tileset_path.path_join(str(j)))
|
||||
zip_packer.write_file(tile.image.get_data())
|
||||
zip_packer.close_file()
|
||||
var audio_layers := project.get_all_audio_layers()
|
||||
for i in audio_layers.size():
|
||||
var layer := audio_layers[i]
|
||||
var audio_path := "audio/%s" % i
|
||||
if layer.audio is AudioStreamMP3:
|
||||
zip_packer.start_file(audio_path)
|
||||
zip_packer.write_file(layer.audio.data)
|
||||
zip_packer.close_file()
|
||||
zip_packer.close()
|
||||
|
||||
if temp_path != path:
|
||||
|
@ -457,12 +508,13 @@ func save_pxo_file(
|
|||
|
||||
func open_image_as_new_tab(path: String, image: Image) -> void:
|
||||
var project := Project.new([], path.get_file(), image.get_size())
|
||||
project.layers.append(PixelLayer.new(project))
|
||||
var layer := PixelLayer.new(project)
|
||||
project.layers.append(layer)
|
||||
Global.projects.append(project)
|
||||
|
||||
var frame := Frame.new()
|
||||
image.convert(Image.FORMAT_RGBA8)
|
||||
frame.cels.append(PixelCel.new(image, 1))
|
||||
image.convert(project.get_image_format())
|
||||
frame.cels.append(layer.new_cel_from_image(image))
|
||||
|
||||
project.frames.append(frame)
|
||||
set_new_imported_tab(project, path)
|
||||
|
@ -475,15 +527,18 @@ func open_image_as_spritesheet_tab_smart(
|
|||
frame_size = image.get_size()
|
||||
sliced_rects.append(Rect2i(Vector2i.ZERO, frame_size))
|
||||
var project := Project.new([], path.get_file(), frame_size)
|
||||
project.layers.append(PixelLayer.new(project))
|
||||
var layer := PixelLayer.new(project)
|
||||
project.layers.append(layer)
|
||||
Global.projects.append(project)
|
||||
for rect in sliced_rects:
|
||||
var offset: Vector2 = (0.5 * (frame_size - rect.size)).floor()
|
||||
var frame := Frame.new()
|
||||
var cropped_image := Image.create(frame_size.x, frame_size.y, false, Image.FORMAT_RGBA8)
|
||||
image.convert(Image.FORMAT_RGBA8)
|
||||
var cropped_image := Image.create(
|
||||
frame_size.x, frame_size.y, false, project.get_image_format()
|
||||
)
|
||||
image.convert(project.get_image_format())
|
||||
cropped_image.blit_rect(image, rect, offset)
|
||||
frame.cels.append(PixelCel.new(cropped_image, 1))
|
||||
frame.cels.append(layer.new_cel_from_image(cropped_image))
|
||||
project.frames.append(frame)
|
||||
set_new_imported_tab(project, path)
|
||||
|
||||
|
@ -494,7 +549,8 @@ func open_image_as_spritesheet_tab(path: String, image: Image, horiz: int, vert:
|
|||
var frame_width := image.get_size().x / horiz
|
||||
var frame_height := image.get_size().y / vert
|
||||
var project := Project.new([], path.get_file(), Vector2(frame_width, frame_height))
|
||||
project.layers.append(PixelLayer.new(project))
|
||||
var layer := PixelLayer.new(project)
|
||||
project.layers.append(layer)
|
||||
Global.projects.append(project)
|
||||
for yy in range(vert):
|
||||
for xx in range(horiz):
|
||||
|
@ -503,8 +559,8 @@ func open_image_as_spritesheet_tab(path: String, image: Image, horiz: int, vert:
|
|||
Rect2i(frame_width * xx, frame_height * yy, frame_width, frame_height)
|
||||
)
|
||||
project.size = cropped_image.get_size()
|
||||
cropped_image.convert(Image.FORMAT_RGBA8)
|
||||
frame.cels.append(PixelCel.new(cropped_image, 1))
|
||||
cropped_image.convert(project.get_image_format())
|
||||
frame.cels.append(layer.new_cel_from_image(cropped_image))
|
||||
project.frames.append(frame)
|
||||
set_new_imported_tab(project, path)
|
||||
|
||||
|
@ -562,12 +618,12 @@ func open_image_as_spritesheet_layer_smart(
|
|||
if f >= start_frame and f < (start_frame + sliced_rects.size()):
|
||||
# Slice spritesheet
|
||||
var offset: Vector2 = (0.5 * (frame_size - sliced_rects[f - start_frame].size)).floor()
|
||||
image.convert(Image.FORMAT_RGBA8)
|
||||
image.convert(project.get_image_format())
|
||||
var cropped_image := Image.create(
|
||||
project_width, project_height, false, Image.FORMAT_RGBA8
|
||||
project_width, project_height, false, project.get_image_format()
|
||||
)
|
||||
cropped_image.blit_rect(image, sliced_rects[f - start_frame], offset)
|
||||
cels.append(PixelCel.new(cropped_image))
|
||||
cels.append(layer.new_cel_from_image(cropped_image))
|
||||
else:
|
||||
cels.append(layer.new_empty_cel())
|
||||
|
||||
|
@ -644,16 +700,16 @@ func open_image_as_spritesheet_layer(
|
|||
# Slice spritesheet
|
||||
var xx := (f - start_frame) % horizontal
|
||||
var yy := (f - start_frame) / horizontal
|
||||
image.convert(Image.FORMAT_RGBA8)
|
||||
image.convert(project.get_image_format())
|
||||
var cropped_image := Image.create(
|
||||
project_width, project_height, false, Image.FORMAT_RGBA8
|
||||
project_width, project_height, false, project.get_image_format()
|
||||
)
|
||||
cropped_image.blit_rect(
|
||||
image,
|
||||
Rect2i(frame_width * xx, frame_height * yy, frame_width, frame_height),
|
||||
Vector2i.ZERO
|
||||
)
|
||||
cels.append(PixelCel.new(cropped_image))
|
||||
cels.append(layer.new_cel_from_image(cropped_image))
|
||||
else:
|
||||
cels.append(layer.new_empty_cel())
|
||||
|
||||
|
@ -687,13 +743,20 @@ func open_image_at_cel(image: Image, layer_index := 0, frame_index := 0) -> void
|
|||
var cel := project.frames[frame_index].cels[layer_index]
|
||||
if not cel is PixelCel:
|
||||
return
|
||||
image.convert(Image.FORMAT_RGBA8)
|
||||
var cel_image := Image.create(project_width, project_height, false, Image.FORMAT_RGBA8)
|
||||
image.convert(project.get_image_format())
|
||||
var cel_image := (cel as PixelCel).get_image()
|
||||
var undo_data := {}
|
||||
if cel is CelTileMap:
|
||||
undo_data[cel] = (cel as CelTileMap).serialize_undo_data()
|
||||
cel_image.add_data_to_dictionary(undo_data)
|
||||
cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO)
|
||||
Global.undo_redo_compress_images(
|
||||
{cel.image: cel_image.data}, {cel.image: cel.image.data}, project
|
||||
)
|
||||
|
||||
cel_image.convert_rgb_to_indexed()
|
||||
var redo_data := {}
|
||||
if cel is CelTileMap:
|
||||
(cel as CelTileMap).update_tilemap()
|
||||
redo_data[cel] = (cel as CelTileMap).serialize_undo_data()
|
||||
cel_image.add_data_to_dictionary(redo_data)
|
||||
project.deserialize_cel_undo_data(redo_data, undo_data)
|
||||
project.undo_redo.add_do_property(project, "selected_cels", [])
|
||||
project.undo_redo.add_do_method(project.change_cel.bind(frame_index, layer_index))
|
||||
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
|
||||
|
@ -716,11 +779,14 @@ func open_image_as_new_frame(
|
|||
|
||||
var frame := Frame.new()
|
||||
for i in project.layers.size():
|
||||
if i == layer_index:
|
||||
image.convert(Image.FORMAT_RGBA8)
|
||||
var cel_image := Image.create(project_width, project_height, false, Image.FORMAT_RGBA8)
|
||||
var layer := project.layers[i]
|
||||
if i == layer_index and layer is PixelLayer:
|
||||
image.convert(project.get_image_format())
|
||||
var cel_image := Image.create(
|
||||
project_width, project_height, false, project.get_image_format()
|
||||
)
|
||||
cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO)
|
||||
frame.cels.append(PixelCel.new(cel_image, 1))
|
||||
frame.cels.append(layer.new_cel_from_image(cel_image))
|
||||
else:
|
||||
frame.cels.append(project.layers[i].new_empty_cel())
|
||||
if not undo:
|
||||
|
@ -753,10 +819,12 @@ func open_image_as_new_layer(image: Image, file_name: String, frame_index := 0)
|
|||
Global.current_project.undo_redo.create_action("Add Layer")
|
||||
for i in project.frames.size():
|
||||
if i == frame_index:
|
||||
image.convert(Image.FORMAT_RGBA8)
|
||||
var cel_image := Image.create(project_width, project_height, false, Image.FORMAT_RGBA8)
|
||||
image.convert(project.get_image_format())
|
||||
var cel_image := Image.create(
|
||||
project_width, project_height, false, project.get_image_format()
|
||||
)
|
||||
cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO)
|
||||
cels.append(PixelCel.new(cel_image, 1))
|
||||
cels.append(layer.new_cel_from_image(cel_image))
|
||||
else:
|
||||
cels.append(layer.new_empty_cel())
|
||||
|
||||
|
@ -794,6 +862,49 @@ func import_reference_image_from_image(image: Image) -> void:
|
|||
reference_image_imported.emit()
|
||||
|
||||
|
||||
func open_image_as_tileset(
|
||||
path: String, image: Image, horiz: int, vert: int, project := Global.current_project
|
||||
) -> void:
|
||||
image.convert(project.get_image_format())
|
||||
horiz = mini(horiz, image.get_size().x)
|
||||
vert = mini(vert, image.get_size().y)
|
||||
var frame_width := image.get_size().x / horiz
|
||||
var frame_height := image.get_size().y / vert
|
||||
var tile_size := Vector2i(frame_width, frame_height)
|
||||
var tileset := TileSetCustom.new(tile_size, path.get_basename().get_file())
|
||||
for yy in range(vert):
|
||||
for xx in range(horiz):
|
||||
var cropped_image := image.get_region(
|
||||
Rect2i(frame_width * xx, frame_height * yy, frame_width, frame_height)
|
||||
)
|
||||
@warning_ignore("int_as_enum_without_cast")
|
||||
tileset.add_tile(cropped_image, null)
|
||||
project.tilesets.append(tileset)
|
||||
|
||||
|
||||
func open_image_as_tileset_smart(
|
||||
path: String,
|
||||
image: Image,
|
||||
sliced_rects: Array[Rect2i],
|
||||
tile_size: Vector2i,
|
||||
project := Global.current_project
|
||||
) -> void:
|
||||
image.convert(project.get_image_format())
|
||||
if sliced_rects.size() == 0: # Image is empty sprite (manually set data to be consistent)
|
||||
tile_size = image.get_size()
|
||||
sliced_rects.append(Rect2i(Vector2i.ZERO, tile_size))
|
||||
var tileset := TileSetCustom.new(tile_size, path.get_basename().get_file())
|
||||
for rect in sliced_rects:
|
||||
var offset: Vector2 = (0.5 * (tile_size - rect.size)).floor()
|
||||
var cropped_image := Image.create(
|
||||
tile_size.x, tile_size.y, false, project.get_image_format()
|
||||
)
|
||||
cropped_image.blit_rect(image, rect, offset)
|
||||
@warning_ignore("int_as_enum_without_cast")
|
||||
tileset.add_tile(cropped_image, null)
|
||||
project.tilesets.append(tileset)
|
||||
|
||||
|
||||
func set_new_imported_tab(project: Project, path: String) -> void:
|
||||
var prev_project_empty := Global.current_project.is_empty()
|
||||
var prev_project_pos := Global.current_project_index
|
||||
|
@ -817,6 +928,23 @@ func set_new_imported_tab(project: Project, path: String) -> void:
|
|||
Global.tabs.delete_tab(prev_project_pos)
|
||||
|
||||
|
||||
func open_audio_file(path: String) -> void:
|
||||
var audio_stream: AudioStream
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
audio_stream = AudioStreamMP3.new()
|
||||
audio_stream.data = file.get_buffer(file.get_length())
|
||||
if not is_instance_valid(audio_stream):
|
||||
return
|
||||
var project := Global.current_project
|
||||
for layer in project.layers:
|
||||
if layer is AudioLayer and not is_instance_valid(layer.audio):
|
||||
layer.audio = audio_stream
|
||||
return
|
||||
var new_layer := AudioLayer.new(project, path.get_basename().get_file())
|
||||
new_layer.audio = audio_stream
|
||||
Global.animation_timeline.add_layer(new_layer, project)
|
||||
|
||||
|
||||
func update_autosave() -> void:
|
||||
if not is_instance_valid(autosave_timer):
|
||||
return
|
||||
|
|
|
@ -36,9 +36,10 @@ func does_palette_exist(palette_name: String) -> bool:
|
|||
|
||||
|
||||
func select_palette(palette_name: String) -> void:
|
||||
current_palette = palettes.get(palette_name)
|
||||
current_palette = palettes.get(palette_name, null)
|
||||
_clear_selected_colors()
|
||||
Global.config_cache.set_value("data", "last_palette", current_palette.name)
|
||||
if is_instance_valid(current_palette):
|
||||
Global.config_cache.set_value("data", "last_palette", current_palette.name)
|
||||
palette_selected.emit(palette_name)
|
||||
|
||||
|
||||
|
@ -224,6 +225,7 @@ func current_palete_delete(permanent := true) -> void:
|
|||
select_palette(palettes.keys()[0])
|
||||
else:
|
||||
current_palette = null
|
||||
select_palette("")
|
||||
|
||||
|
||||
func current_palette_add_color(mouse_button: int, start_index := 0) -> void:
|
||||
|
@ -294,13 +296,13 @@ func current_palette_select_color(mouse_button: int, index: int) -> void:
|
|||
if color == null:
|
||||
return
|
||||
|
||||
_select_color(mouse_button, index)
|
||||
|
||||
match mouse_button:
|
||||
MOUSE_BUTTON_LEFT:
|
||||
Tools.assign_color(color, mouse_button)
|
||||
Tools.assign_color(color, mouse_button, true, left_selected_color)
|
||||
MOUSE_BUTTON_RIGHT:
|
||||
Tools.assign_color(color, mouse_button)
|
||||
|
||||
_select_color(mouse_button, index)
|
||||
Tools.assign_color(color, mouse_button, true, right_selected_color)
|
||||
|
||||
|
||||
func _select_color(mouse_button: int, index: int) -> void:
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# gdlint: ignore=max-public-methods
|
||||
extends Node
|
||||
|
||||
signal color_changed(color: Color, button: int)
|
||||
signal color_changed(color_info: Dictionary, button: int)
|
||||
@warning_ignore("unused_signal")
|
||||
signal selected_tile_index_changed(tile_index: int)
|
||||
signal config_changed(slot_idx: int, config: Dictionary)
|
||||
@warning_ignore("unused_signal")
|
||||
signal flip_rotated(flip_x, flip_y, rotate_90, rotate_180, rotate_270)
|
||||
|
@ -9,9 +11,15 @@ signal options_reset
|
|||
|
||||
enum Dynamics { NONE, PRESSURE, VELOCITY }
|
||||
|
||||
const XY_LINE := Vector2(-0.707107, 0.707107)
|
||||
const X_MINUS_Y_LINE := Vector2(0.707107, 0.707107)
|
||||
|
||||
var active_button := -1
|
||||
var picking_color_for := MOUSE_BUTTON_LEFT
|
||||
var horizontal_mirror := false
|
||||
var vertical_mirror := false
|
||||
var diagonal_xy_mirror := false
|
||||
var diagonal_x_minus_y_mirror := false
|
||||
var pixel_perfect := false
|
||||
var alpha_locked := false
|
||||
|
||||
|
@ -82,7 +90,11 @@ var tools := {
|
|||
),
|
||||
"Move":
|
||||
Tool.new(
|
||||
"Move", "Move", "move", "res://src/Tools/UtilityTools/Move.tscn", [Global.LayerTypes.PIXEL]
|
||||
"Move",
|
||||
"Move",
|
||||
"move",
|
||||
"res://src/Tools/UtilityTools/Move.tscn",
|
||||
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP]
|
||||
),
|
||||
"Zoom": Tool.new("Zoom", "Zoom", "zoom", "res://src/Tools/UtilityTools/Zoom.tscn"),
|
||||
"Pan": Tool.new("Pan", "Pan", "pan", "res://src/Tools/UtilityTools/Pan.tscn"),
|
||||
|
@ -110,7 +122,7 @@ var tools := {
|
|||
"Pencil",
|
||||
"pencil",
|
||||
"res://src/Tools/DesignTools/Pencil.tscn",
|
||||
[Global.LayerTypes.PIXEL],
|
||||
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
|
||||
"Hold %s to make a line",
|
||||
["draw_create_line"]
|
||||
),
|
||||
|
@ -120,7 +132,7 @@ var tools := {
|
|||
"Eraser",
|
||||
"eraser",
|
||||
"res://src/Tools/DesignTools/Eraser.tscn",
|
||||
[Global.LayerTypes.PIXEL],
|
||||
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
|
||||
"Hold %s to make a line",
|
||||
["draw_create_line"]
|
||||
),
|
||||
|
@ -130,7 +142,7 @@ var tools := {
|
|||
"Bucket",
|
||||
"fill",
|
||||
"res://src/Tools/DesignTools/Bucket.tscn",
|
||||
[Global.LayerTypes.PIXEL]
|
||||
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP]
|
||||
),
|
||||
"Shading":
|
||||
Tool.new(
|
||||
|
@ -138,7 +150,7 @@ var tools := {
|
|||
"Shading Tool",
|
||||
"shading",
|
||||
"res://src/Tools/DesignTools/Shading.tscn",
|
||||
[Global.LayerTypes.PIXEL]
|
||||
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP]
|
||||
),
|
||||
"LineTool":
|
||||
(
|
||||
|
@ -148,7 +160,7 @@ var tools := {
|
|||
"Line Tool",
|
||||
"linetool",
|
||||
"res://src/Tools/DesignTools/LineTool.tscn",
|
||||
[Global.LayerTypes.PIXEL],
|
||||
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
|
||||
"""Hold %s to snap the angle of the line
|
||||
Hold %s to center the shape on the click origin
|
||||
Hold %s to displace the shape's origin""",
|
||||
|
@ -163,7 +175,7 @@ Hold %s to displace the shape's origin""",
|
|||
"Curve Tool",
|
||||
"curvetool",
|
||||
"res://src/Tools/DesignTools/CurveTool.tscn",
|
||||
[Global.LayerTypes.PIXEL],
|
||||
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
|
||||
"""Draws bezier curves
|
||||
Press %s/%s to add new points
|
||||
Press and drag to control the curvature
|
||||
|
@ -179,7 +191,7 @@ Press %s to remove the last added point""",
|
|||
"Rectangle Tool",
|
||||
"rectangletool",
|
||||
"res://src/Tools/DesignTools/RectangleTool.tscn",
|
||||
[Global.LayerTypes.PIXEL],
|
||||
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
|
||||
"""Hold %s to create a 1:1 shape
|
||||
Hold %s to center the shape on the click origin
|
||||
Hold %s to displace the shape's origin""",
|
||||
|
@ -194,7 +206,7 @@ Hold %s to displace the shape's origin""",
|
|||
"Ellipse Tool",
|
||||
"ellipsetool",
|
||||
"res://src/Tools/DesignTools/EllipseTool.tscn",
|
||||
[Global.LayerTypes.PIXEL],
|
||||
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
|
||||
"""Hold %s to create a 1:1 shape
|
||||
Hold %s to center the shape on the click origin
|
||||
Hold %s to displace the shape's origin""",
|
||||
|
@ -207,7 +219,7 @@ Hold %s to displace the shape's origin""",
|
|||
"Text",
|
||||
"text",
|
||||
"res://src/Tools/UtilityTools/Text.tscn",
|
||||
[Global.LayerTypes.PIXEL],
|
||||
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
|
||||
""
|
||||
),
|
||||
"3DShapeEdit":
|
||||
|
@ -226,14 +238,15 @@ var _panels := {}
|
|||
var _curr_layer_type := Global.LayerTypes.PIXEL
|
||||
var _left_tools_per_layer_type := {
|
||||
Global.LayerTypes.PIXEL: "Pencil",
|
||||
Global.LayerTypes.TILEMAP: "Pencil",
|
||||
Global.LayerTypes.THREE_D: "3DShapeEdit",
|
||||
}
|
||||
var _right_tools_per_layer_type := {
|
||||
Global.LayerTypes.PIXEL: "Eraser",
|
||||
Global.LayerTypes.TILEMAP: "Eraser",
|
||||
Global.LayerTypes.THREE_D: "Pan",
|
||||
}
|
||||
var _tool_buttons: Node
|
||||
var _active_button := -1
|
||||
var _last_position := Vector2i(Vector2.INF)
|
||||
|
||||
|
||||
|
@ -504,7 +517,7 @@ func swap_color() -> void:
|
|||
assign_color(left, MOUSE_BUTTON_RIGHT, false)
|
||||
|
||||
|
||||
func assign_color(color: Color, button: int, change_alpha := true) -> void:
|
||||
func assign_color(color: Color, button: int, change_alpha := true, index: int = -1) -> void:
|
||||
var c: Color = _slots[button].color
|
||||
# This was requested by Issue #54 on GitHub
|
||||
if color.a == 0 and change_alpha:
|
||||
|
@ -512,7 +525,8 @@ func assign_color(color: Color, button: int, change_alpha := true) -> void:
|
|||
color.a = 1
|
||||
_slots[button].color = color
|
||||
Global.config_cache.set_value(_slots[button].kname, "color", color)
|
||||
color_changed.emit(color, button)
|
||||
var color_info := {"color": color, "index": index}
|
||||
color_changed.emit(color_info, button)
|
||||
|
||||
|
||||
func get_assigned_color(button: int) -> Color:
|
||||
|
@ -524,20 +538,132 @@ func get_mirrored_positions(
|
|||
) -> Array[Vector2i]:
|
||||
var positions: Array[Vector2i] = []
|
||||
if horizontal_mirror:
|
||||
var mirror_x := pos
|
||||
mirror_x.x = project.x_symmetry_point - pos.x + offset
|
||||
var mirror_x := calculate_mirror_horizontal(pos, project, offset)
|
||||
positions.append(mirror_x)
|
||||
if vertical_mirror:
|
||||
var mirror_xy := mirror_x
|
||||
mirror_xy.y = project.y_symmetry_point - pos.y + offset
|
||||
positions.append(mirror_xy)
|
||||
positions.append(calculate_mirror_vertical(mirror_x, project, offset))
|
||||
else:
|
||||
if diagonal_xy_mirror:
|
||||
positions.append(calculate_mirror_xy(mirror_x, project))
|
||||
if diagonal_x_minus_y_mirror:
|
||||
positions.append(calculate_mirror_x_minus_y(mirror_x, project))
|
||||
if vertical_mirror:
|
||||
var mirror_y := pos
|
||||
mirror_y.y = project.y_symmetry_point - pos.y + offset
|
||||
var mirror_y := calculate_mirror_vertical(pos, project, offset)
|
||||
positions.append(mirror_y)
|
||||
if diagonal_xy_mirror:
|
||||
positions.append(calculate_mirror_xy(mirror_y, project))
|
||||
if diagonal_x_minus_y_mirror:
|
||||
positions.append(calculate_mirror_x_minus_y(mirror_y, project))
|
||||
if diagonal_xy_mirror:
|
||||
var mirror_diagonal := calculate_mirror_xy(pos, project)
|
||||
positions.append(mirror_diagonal)
|
||||
if not horizontal_mirror and not vertical_mirror and diagonal_x_minus_y_mirror:
|
||||
positions.append(calculate_mirror_x_minus_y(mirror_diagonal, project))
|
||||
if diagonal_x_minus_y_mirror:
|
||||
positions.append(calculate_mirror_x_minus_y(pos, project))
|
||||
return positions
|
||||
|
||||
|
||||
func calculate_mirror_horizontal(pos: Vector2i, project: Project, offset := 0) -> Vector2i:
|
||||
return Vector2i(project.x_symmetry_point - pos.x + offset, pos.y)
|
||||
|
||||
|
||||
func calculate_mirror_vertical(pos: Vector2i, project: Project, offset := 0) -> Vector2i:
|
||||
return Vector2i(pos.x, project.y_symmetry_point - pos.y + offset)
|
||||
|
||||
|
||||
func calculate_mirror_xy(pos: Vector2i, project: Project) -> Vector2i:
|
||||
return Vector2i(Vector2(pos).reflect(XY_LINE).round()) + Vector2i(project.xy_symmetry_point)
|
||||
|
||||
|
||||
func calculate_mirror_x_minus_y(pos: Vector2i, project: Project) -> Vector2i:
|
||||
return (
|
||||
Vector2i(Vector2(pos).reflect(X_MINUS_Y_LINE).round())
|
||||
+ Vector2i(project.x_minus_y_symmetry_point)
|
||||
)
|
||||
|
||||
|
||||
func is_placing_tiles() -> bool:
|
||||
if Global.current_project.frames.size() == 0 or Global.current_project.layers.size() == 0:
|
||||
return false
|
||||
return Global.current_project.get_current_cel() is CelTileMap and TileSetPanel.placing_tiles
|
||||
|
||||
|
||||
func _get_closest_point_to_grid(pos: Vector2, distance: float, grid_pos: Vector2) -> Vector2:
|
||||
# If the cursor is close to the start/origin of a grid cell, snap to that
|
||||
var snap_distance := distance * Vector2.ONE
|
||||
var closest_point := Vector2.INF
|
||||
var rect := Rect2()
|
||||
rect.position = pos - (snap_distance / 4.0)
|
||||
rect.end = pos + (snap_distance / 4.0)
|
||||
if rect.has_point(grid_pos):
|
||||
closest_point = grid_pos
|
||||
return closest_point
|
||||
# If the cursor is far from the grid cell origin but still close to a grid line
|
||||
# Look for a point close to a horizontal grid line
|
||||
var grid_start_hor := Vector2(0, grid_pos.y)
|
||||
var grid_end_hor := Vector2(Global.current_project.size.x, grid_pos.y)
|
||||
var closest_point_hor := get_closest_point_to_segment(
|
||||
pos, distance, grid_start_hor, grid_end_hor
|
||||
)
|
||||
# Look for a point close to a vertical grid line
|
||||
var grid_start_ver := Vector2(grid_pos.x, 0)
|
||||
var grid_end_ver := Vector2(grid_pos.x, Global.current_project.size.y)
|
||||
var closest_point_ver := get_closest_point_to_segment(
|
||||
pos, distance, grid_start_ver, grid_end_ver
|
||||
)
|
||||
# Snap to the closest point to the closest grid line
|
||||
var horizontal_distance := (closest_point_hor - pos).length()
|
||||
var vertical_distance := (closest_point_ver - pos).length()
|
||||
if horizontal_distance < vertical_distance:
|
||||
closest_point = closest_point_hor
|
||||
elif horizontal_distance > vertical_distance:
|
||||
closest_point = closest_point_ver
|
||||
elif horizontal_distance == vertical_distance and closest_point_hor != Vector2.INF:
|
||||
closest_point = grid_pos
|
||||
return closest_point
|
||||
|
||||
|
||||
func get_closest_point_to_segment(
|
||||
pos: Vector2, distance: float, s1: Vector2, s2: Vector2
|
||||
) -> Vector2:
|
||||
var test_line := (s2 - s1).rotated(deg_to_rad(90)).normalized()
|
||||
var from_a := pos - test_line * distance
|
||||
var from_b := pos + test_line * distance
|
||||
var closest_point := Vector2.INF
|
||||
if Geometry2D.segment_intersects_segment(from_a, from_b, s1, s2):
|
||||
closest_point = Geometry2D.get_closest_point_to_segment(pos, s1, s2)
|
||||
return closest_point
|
||||
|
||||
|
||||
func snap_to_rectangular_grid_boundary(
|
||||
pos: Vector2, grid_size: Vector2i, grid_offset := Vector2i.ZERO, snapping_distance := 9999.0
|
||||
) -> Vector2:
|
||||
var grid_pos := pos.snapped(grid_size)
|
||||
grid_pos += Vector2(grid_offset)
|
||||
# keeping grid_pos as is would have been fine but this adds extra accuracy as to
|
||||
# which snap point (from the list below) is closest to mouse and occupy THAT point
|
||||
# t_l is for "top left" and so on
|
||||
var t_l := grid_pos + Vector2(-grid_size.x, -grid_size.y)
|
||||
var t_c := grid_pos + Vector2(0, -grid_size.y)
|
||||
var t_r := grid_pos + Vector2(grid_size.x, -grid_size.y)
|
||||
var m_l := grid_pos + Vector2(-grid_size.x, 0)
|
||||
var m_c := grid_pos
|
||||
var m_r := grid_pos + Vector2(grid_size.x, 0)
|
||||
var b_l := grid_pos + Vector2(-grid_size.x, grid_size.y)
|
||||
var b_c := grid_pos + Vector2(0, grid_size.y)
|
||||
var b_r := grid_pos + Vector2(grid_size)
|
||||
var vec_arr: PackedVector2Array = [t_l, t_c, t_r, m_l, m_c, m_r, b_l, b_c, b_r]
|
||||
for vec in vec_arr:
|
||||
if vec.distance_to(pos) < grid_pos.distance_to(pos):
|
||||
grid_pos = vec
|
||||
|
||||
var grid_point := _get_closest_point_to_grid(pos, snapping_distance, grid_pos)
|
||||
if grid_point != Vector2.INF:
|
||||
pos = grid_point.floor()
|
||||
return pos
|
||||
|
||||
|
||||
func set_button_size(button_size: int) -> void:
|
||||
var size := Vector2(24, 24) if button_size == Global.ButtonSize.SMALL else Vector2(32, 32)
|
||||
if not is_instance_valid(_tool_buttons):
|
||||
|
@ -591,32 +717,28 @@ func handle_draw(position: Vector2i, event: InputEvent) -> void:
|
|||
change_layer_automatically(draw_pos)
|
||||
return
|
||||
|
||||
if event.is_action_pressed(&"activate_left_tool") and _active_button == -1 and not pen_inverted:
|
||||
_active_button = MOUSE_BUTTON_LEFT
|
||||
_slots[_active_button].tool_node.draw_start(draw_pos)
|
||||
elif event.is_action_released(&"activate_left_tool") and _active_button == MOUSE_BUTTON_LEFT:
|
||||
_slots[_active_button].tool_node.draw_end(draw_pos)
|
||||
_active_button = -1
|
||||
if event.is_action_pressed(&"activate_left_tool") and active_button == -1 and not pen_inverted:
|
||||
active_button = MOUSE_BUTTON_LEFT
|
||||
_slots[active_button].tool_node.draw_start(draw_pos)
|
||||
elif event.is_action_released(&"activate_left_tool") and active_button == MOUSE_BUTTON_LEFT:
|
||||
_slots[active_button].tool_node.draw_end(draw_pos)
|
||||
active_button = -1
|
||||
elif (
|
||||
(
|
||||
event.is_action_pressed(&"activate_right_tool")
|
||||
and _active_button == -1
|
||||
and active_button == -1
|
||||
and not pen_inverted
|
||||
)
|
||||
or (
|
||||
event.is_action_pressed(&"activate_left_tool") and _active_button == -1 and pen_inverted
|
||||
)
|
||||
or event.is_action_pressed(&"activate_left_tool") and active_button == -1 and pen_inverted
|
||||
):
|
||||
_active_button = MOUSE_BUTTON_RIGHT
|
||||
_slots[_active_button].tool_node.draw_start(draw_pos)
|
||||
active_button = MOUSE_BUTTON_RIGHT
|
||||
_slots[active_button].tool_node.draw_start(draw_pos)
|
||||
elif (
|
||||
(event.is_action_released(&"activate_right_tool") and _active_button == MOUSE_BUTTON_RIGHT)
|
||||
or (
|
||||
event.is_action_released(&"activate_left_tool") and _active_button == MOUSE_BUTTON_RIGHT
|
||||
)
|
||||
(event.is_action_released(&"activate_right_tool") and active_button == MOUSE_BUTTON_RIGHT)
|
||||
or event.is_action_released(&"activate_left_tool") and active_button == MOUSE_BUTTON_RIGHT
|
||||
):
|
||||
_slots[_active_button].tool_node.draw_end(draw_pos)
|
||||
_active_button = -1
|
||||
_slots[active_button].tool_node.draw_end(draw_pos)
|
||||
active_button = -1
|
||||
|
||||
if event is InputEventMouseMotion:
|
||||
pen_pressure = event.pressure
|
||||
|
@ -647,8 +769,8 @@ func handle_draw(position: Vector2i, event: InputEvent) -> void:
|
|||
_last_position = position
|
||||
_slots[MOUSE_BUTTON_LEFT].tool_node.cursor_move(position)
|
||||
_slots[MOUSE_BUTTON_RIGHT].tool_node.cursor_move(position)
|
||||
if _active_button != -1:
|
||||
_slots[_active_button].tool_node.draw_move(draw_pos)
|
||||
if active_button != -1:
|
||||
_slots[active_button].tool_node.draw_move(draw_pos)
|
||||
|
||||
var project := Global.current_project
|
||||
var text := "[%s×%s]" % [project.size.x, project.size.y]
|
||||
|
@ -678,7 +800,10 @@ func _cel_switched() -> void:
|
|||
var layer: BaseLayer = Global.current_project.layers[Global.current_project.current_layer]
|
||||
var layer_type := layer.get_layer_type()
|
||||
# Do not make any changes when its the same type of layer, or a group layer
|
||||
if layer_type == _curr_layer_type or layer_type == Global.LayerTypes.GROUP:
|
||||
if (
|
||||
layer_type == _curr_layer_type
|
||||
or layer_type in [Global.LayerTypes.GROUP, Global.LayerTypes.AUDIO]
|
||||
):
|
||||
return
|
||||
_show_relevant_tools(layer_type)
|
||||
|
||||
|
|
18
src/Classes/Cels/AudioCel.gd
Normal file
|
@ -0,0 +1,18 @@
|
|||
class_name AudioCel
|
||||
extends BaseCel
|
||||
## A class for the properties of cels in AudioLayers.
|
||||
## The term "cel" comes from "celluloid" (https://en.wikipedia.org/wiki/Cel).
|
||||
|
||||
|
||||
func _init(_opacity := 1.0) -> void:
|
||||
opacity = _opacity
|
||||
image_texture = ImageTexture.new()
|
||||
|
||||
|
||||
func get_image() -> Image:
|
||||
var image := Global.current_project.new_empty_image()
|
||||
return image
|
||||
|
||||
|
||||
func get_class_name() -> String:
|
||||
return "AudioCel"
|
|
@ -24,6 +24,13 @@ func get_final_opacity(layer: BaseLayer) -> float:
|
|||
return layer.opacity * opacity
|
||||
|
||||
|
||||
func get_frame(project: Project) -> Frame:
|
||||
for frame in project.frames:
|
||||
if frame.cels.has(self):
|
||||
return frame
|
||||
return null
|
||||
|
||||
|
||||
# Methods to Override:
|
||||
|
||||
|
||||
|
@ -67,7 +74,7 @@ func get_image() -> Image:
|
|||
|
||||
|
||||
## Used to update the texture of the cel.
|
||||
func update_texture() -> void:
|
||||
func update_texture(_undo := false) -> void:
|
||||
texture_changed.emit()
|
||||
if link_set != null:
|
||||
var frame := Global.current_project.current_frame
|
||||
|
@ -92,6 +99,10 @@ func deserialize(dict: Dictionary) -> void:
|
|||
user_data = dict.get("user_data", user_data)
|
||||
|
||||
|
||||
func size_changed(_new_size: Vector2i) -> void:
|
||||
pass
|
||||
|
||||
|
||||
## Used to perform cleanup after a cel is removed.
|
||||
func on_remove() -> void:
|
||||
pass
|
||||
|
|
710
src/Classes/Cels/CelTileMap.gd
Normal file
|
@ -0,0 +1,710 @@
|
|||
# gdlint: ignore=max-public-methods
|
||||
class_name CelTileMap
|
||||
extends PixelCel
|
||||
|
||||
## A cel type for 2D tile-based maps.
|
||||
## A Tilemap cel uses a [TileSetCustom], which it inherits from its [LayerTileMap].
|
||||
## Extending from [PixelCel], it contains an internal [Image], which is divided in
|
||||
## grid cells, the size of which comes from [member TileSetCustom.tile_size].
|
||||
## Each cell contains an index, which is an integer used to map that portion of the
|
||||
## internal [member PixelCel.image] to a tile in [member tileset], as well as
|
||||
## information that specifies if that cell has a transformation applied to it,
|
||||
## such as horizontal flipping, vertical flipping, or if it's transposed.
|
||||
|
||||
## The [TileSetCustom] that this cel uses, passed down from the cel's [LayerTileMap].
|
||||
var tileset: TileSetCustom
|
||||
|
||||
## The [Array] of type [CelTileMap.Cell] that contains data for each cell of the tilemap.
|
||||
## The array's size is equal to [member horizontal_cells] * [member vertical_cells].
|
||||
var cells: Array[Cell]
|
||||
## The amount of horizontal cells.
|
||||
var horizontal_cells: int
|
||||
## The amount of vertical cells.
|
||||
var vertical_cells: int
|
||||
## Dictionary of [int] and [Array].
|
||||
## The key is the index of the tile in the tileset,
|
||||
## and the value is the index of the tilemap tile that changed first, along with
|
||||
## its image that is being changed when manual mode is enabled.
|
||||
## Gets reset on [method update_tilemap].
|
||||
var editing_images := {}
|
||||
|
||||
|
||||
## An internal class of [CelTIleMap], which contains data used by individual cells of the tilemap.
|
||||
class Cell:
|
||||
## The index of the [TileSetCustom] tile that the cell is mapped to.
|
||||
var index := 0
|
||||
## If [code]true[/code], the tile is flipped horizontally in this cell.
|
||||
var flip_h := false
|
||||
## If [code]true[/code], the tile is flipped vertically in this cell.
|
||||
var flip_v := false
|
||||
## If [code]true[/code], the tile is rotated 90 degrees counter-clockwise,
|
||||
## and then flipped vertically in this cell.
|
||||
var transpose := false
|
||||
|
||||
func _to_string() -> String:
|
||||
var text := str(index)
|
||||
if flip_h:
|
||||
text += "H"
|
||||
if flip_v:
|
||||
text += "V"
|
||||
if transpose:
|
||||
text += "T"
|
||||
return text
|
||||
|
||||
func remove_transformations() -> void:
|
||||
flip_h = false
|
||||
flip_v = false
|
||||
transpose = false
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
return {"index": index, "flip_h": flip_h, "flip_v": flip_v, "transpose": transpose}
|
||||
|
||||
func deserialize(dict: Dictionary) -> void:
|
||||
index = dict.get("index", index)
|
||||
flip_h = dict.get("flip_h", flip_h)
|
||||
flip_v = dict.get("flip_v", flip_v)
|
||||
transpose = dict.get("transpose", transpose)
|
||||
|
||||
|
||||
func _init(_tileset: TileSetCustom, _image := ImageExtended.new(), _opacity := 1.0) -> void:
|
||||
super._init(_image, _opacity)
|
||||
set_tileset(_tileset)
|
||||
|
||||
|
||||
func set_tileset(new_tileset: TileSetCustom, reset_indices := true) -> void:
|
||||
if tileset == new_tileset:
|
||||
return
|
||||
if is_instance_valid(tileset):
|
||||
if tileset.updated.is_connected(_on_tileset_updated):
|
||||
tileset.updated.disconnect(_on_tileset_updated)
|
||||
tileset = new_tileset
|
||||
if is_instance_valid(tileset):
|
||||
_resize_cells(get_image().get_size(), reset_indices)
|
||||
if not tileset.updated.is_connected(_on_tileset_updated):
|
||||
tileset.updated.connect(_on_tileset_updated)
|
||||
|
||||
|
||||
## Maps the cell at position [param cell_position] to
|
||||
## the [member tileset]'s tile of index [param index].
|
||||
func set_index(cell_position: int, index: int) -> void:
|
||||
index = clampi(index, 0, tileset.tiles.size() - 1)
|
||||
var previous_index := cells[cell_position].index
|
||||
|
||||
if previous_index != index:
|
||||
if previous_index > 0 and previous_index < tileset.tiles.size():
|
||||
tileset.tiles[previous_index].times_used -= 1
|
||||
tileset.tiles[index].times_used += 1
|
||||
cells[cell_position].index = index
|
||||
cells[cell_position].flip_h = TileSetPanel.is_flipped_h
|
||||
cells[cell_position].flip_v = TileSetPanel.is_flipped_v
|
||||
cells[cell_position].transpose = TileSetPanel.is_transposed
|
||||
_update_cell(cell_position)
|
||||
Global.canvas.queue_redraw()
|
||||
|
||||
|
||||
## Returns the pixel coordinates of the tilemap's cell
|
||||
## at position [cell_position] in the cel's image.
|
||||
## The reverse of [method get_cell_position].
|
||||
func get_cell_coords_in_image(cell_position: int) -> Vector2i:
|
||||
var x_coord := float(tileset.tile_size.x) * (cell_position % horizontal_cells)
|
||||
@warning_ignore("integer_division")
|
||||
var y_coord := float(tileset.tile_size.y) * (cell_position / horizontal_cells)
|
||||
return Vector2i(x_coord, y_coord)
|
||||
|
||||
|
||||
## Returns the position of a cell in the tilemap
|
||||
## at pixel coordinates [param coords] in the cel's image.
|
||||
## The reverse of [method get_cell_coords_in_image].
|
||||
func get_cell_position(coords: Vector2i) -> int:
|
||||
@warning_ignore("integer_division")
|
||||
var x := coords.x / tileset.tile_size.x
|
||||
x = clampi(x, 0, horizontal_cells - 1)
|
||||
@warning_ignore("integer_division")
|
||||
var y := coords.y / tileset.tile_size.y
|
||||
y = clampi(y, 0, vertical_cells - 1)
|
||||
y *= horizontal_cells
|
||||
return x + y
|
||||
|
||||
|
||||
## Returns the position of a cell in the tilemap
|
||||
## at tilemap coordinates [param coords] in the cel's image.
|
||||
func get_cell_position_in_tilemap_space(coords: Vector2i) -> int:
|
||||
var x := coords.x
|
||||
x = clampi(x, 0, horizontal_cells - 1)
|
||||
var y := coords.y
|
||||
y = clampi(y, 0, vertical_cells - 1)
|
||||
y *= horizontal_cells
|
||||
return x + y
|
||||
|
||||
|
||||
## Returns the index of a cell in the tilemap
|
||||
## at pixel coordinates [param coords] in the cel's image.
|
||||
func get_cell_index_at_coords(coords: Vector2i) -> int:
|
||||
return cells[get_cell_position(coords)].index
|
||||
|
||||
|
||||
## Returns the index of a cell in the tilemap
|
||||
## at tilemap coordinates [param coords] in the cel's image.
|
||||
func get_cell_index_at_coords_in_tilemap_space(coords: Vector2i) -> int:
|
||||
return cells[get_cell_position_in_tilemap_space(coords)].index
|
||||
|
||||
|
||||
## Returns [code]true[/code] if the tile at cell position [param cell_position]
|
||||
## with image [param image_portion] is equal to [param tile_image].
|
||||
func _tiles_equal(cell_position: int, image_portion: Image, tile_image: Image) -> bool:
|
||||
var cell_data := cells[cell_position]
|
||||
var final_image_portion := transform_tile(
|
||||
tile_image, cell_data.flip_h, cell_data.flip_v, cell_data.transpose
|
||||
)
|
||||
return image_portion.get_data() == final_image_portion.get_data()
|
||||
|
||||
|
||||
## Applies transformations to [param tile_image] based on [param flip_h],
|
||||
## [param flip_v] and [param transpose], and returns the transformed image.
|
||||
## If [param reverse] is [code]true[/code], the transposition is applied the reverse way.
|
||||
func transform_tile(
|
||||
tile_image: Image, flip_h: bool, flip_v: bool, transpose: bool, reverse := false
|
||||
) -> Image:
|
||||
var transformed_tile := Image.new()
|
||||
transformed_tile.copy_from(tile_image)
|
||||
if transpose:
|
||||
var tmp_image := Image.new()
|
||||
tmp_image.copy_from(transformed_tile)
|
||||
if reverse:
|
||||
tmp_image.rotate_90(CLOCKWISE)
|
||||
else:
|
||||
tmp_image.rotate_90(COUNTERCLOCKWISE)
|
||||
transformed_tile.blit_rect(
|
||||
tmp_image, Rect2i(Vector2i.ZERO, transformed_tile.get_size()), Vector2i.ZERO
|
||||
)
|
||||
if reverse and not (flip_h != flip_v):
|
||||
transformed_tile.flip_x()
|
||||
else:
|
||||
transformed_tile.flip_y()
|
||||
if flip_h:
|
||||
transformed_tile.flip_x()
|
||||
if flip_v:
|
||||
transformed_tile.flip_y()
|
||||
return transformed_tile
|
||||
|
||||
|
||||
## Given a [param selection_map] and a [param selection_rect],
|
||||
## the method finds the cells that are currently selected and returns them
|
||||
## in the form of a 2D array that contains the serialiazed data
|
||||
##of the selected cells in the form of [Dictionary].
|
||||
func get_selected_cells(selection_map: SelectionMap, selection_rect: Rect2i) -> Array[Array]:
|
||||
var selected_cells: Array[Array] = []
|
||||
for x in range(0, selection_rect.size.x, tileset.tile_size.x):
|
||||
selected_cells.append([])
|
||||
for y in range(0, selection_rect.size.y, tileset.tile_size.y):
|
||||
var pos := Vector2i(x, y) + selection_rect.position
|
||||
var x_index := x / tileset.tile_size.x
|
||||
if selection_map.is_pixel_selected(pos):
|
||||
var cell_pos := get_cell_position(pos)
|
||||
selected_cells[x_index].append(cells[cell_pos].serialize())
|
||||
else:
|
||||
# If it's not selected, append the transparent tile 0.
|
||||
selected_cells[x_index].append(
|
||||
{"index": 0, "flip_h": false, "flip_v": false, "transpose": false}
|
||||
)
|
||||
return selected_cells
|
||||
|
||||
|
||||
## Resizes [param selected_indices], which is an array of arrays of [Dictionary],
|
||||
## to [param horizontal_size] and [param vertical_size].
|
||||
## This method is used when resizing a selection and draw tiles mode is enabled.
|
||||
func resize_selection(
|
||||
selected_cells: Array[Array], horizontal_size: int, vertical_size: int
|
||||
) -> Array[Array]:
|
||||
var resized_cells: Array[Array] = []
|
||||
var current_columns := selected_cells.size()
|
||||
if current_columns == 0:
|
||||
return resized_cells
|
||||
var current_rows := selected_cells[0].size()
|
||||
if current_rows == 0:
|
||||
return resized_cells
|
||||
resized_cells.resize(horizontal_size)
|
||||
for x in horizontal_size:
|
||||
resized_cells[x] = []
|
||||
resized_cells[x].resize(vertical_size)
|
||||
var column_middles := current_columns - 2
|
||||
if current_columns == 1:
|
||||
for x in horizontal_size:
|
||||
_resize_rows(selected_cells[0], resized_cells[x], current_rows, vertical_size)
|
||||
else:
|
||||
for x in horizontal_size:
|
||||
if x == 0:
|
||||
_resize_rows(selected_cells[0], resized_cells[x], current_rows, vertical_size)
|
||||
elif x == horizontal_size - 1:
|
||||
_resize_rows(selected_cells[-1], resized_cells[x], current_rows, vertical_size)
|
||||
else:
|
||||
if x < current_columns - 1:
|
||||
_resize_rows(selected_cells[x], resized_cells[x], current_rows, vertical_size)
|
||||
else:
|
||||
if column_middles == 0:
|
||||
_resize_rows(
|
||||
selected_cells[-1], resized_cells[x], current_rows, vertical_size
|
||||
)
|
||||
else:
|
||||
var x_index := x - (column_middles * ((x - 1) / column_middles))
|
||||
_resize_rows(
|
||||
selected_cells[x_index], resized_cells[x], current_rows, vertical_size
|
||||
)
|
||||
return resized_cells
|
||||
|
||||
|
||||
## Helper method of [method resize_selection].
|
||||
func _resize_rows(
|
||||
selected_cells: Array, resized_cells: Array, current_rows: int, vertical_size: int
|
||||
) -> void:
|
||||
var row_middles := current_rows - 2
|
||||
if current_rows == 1:
|
||||
for y in vertical_size:
|
||||
resized_cells[y] = selected_cells[0]
|
||||
else:
|
||||
for y in vertical_size:
|
||||
if y == 0:
|
||||
resized_cells[y] = selected_cells[0]
|
||||
elif y == vertical_size - 1:
|
||||
resized_cells[y] = selected_cells[-1]
|
||||
else:
|
||||
if y < current_rows - 1:
|
||||
resized_cells[y] = selected_cells[y]
|
||||
else:
|
||||
if row_middles == 0:
|
||||
resized_cells[y] = selected_cells[-1]
|
||||
else:
|
||||
var y_index := y - (row_middles * ((y - 1) / row_middles))
|
||||
resized_cells[y] = selected_cells[y_index]
|
||||
|
||||
|
||||
## Applies the [param selected_cells] data to [param target_image] data,
|
||||
## offset by [param selection_rect]. The target image needs to be resized first.
|
||||
## This method is used when resizing a selection and draw tiles mode is enabled.
|
||||
func apply_resizing_to_image(
|
||||
target_image: Image, selected_cells: Array[Array], selection_rect: Rect2i
|
||||
) -> void:
|
||||
for x in selected_cells.size():
|
||||
for y in selected_cells[x].size():
|
||||
var pos := Vector2i(x, y) * tileset.tile_size + selection_rect.position
|
||||
var cell_pos := get_cell_position(pos)
|
||||
var coords := get_cell_coords_in_image(cell_pos) - selection_rect.position
|
||||
var rect := Rect2i(coords, tileset.tile_size)
|
||||
var image_portion := target_image.get_region(rect)
|
||||
var cell_data := Cell.new()
|
||||
cell_data.deserialize(selected_cells[x][y])
|
||||
var index := cell_data.index
|
||||
if index >= tileset.tiles.size():
|
||||
index = 0
|
||||
var current_tile := tileset.tiles[index].image
|
||||
var transformed_tile := transform_tile(
|
||||
current_tile, cell_data.flip_h, cell_data.flip_v, cell_data.transpose
|
||||
)
|
||||
if image_portion.get_data() != transformed_tile.get_data():
|
||||
var tile_size := transformed_tile.get_size()
|
||||
target_image.blit_rect(transformed_tile, Rect2i(Vector2i.ZERO, tile_size), coords)
|
||||
if target_image is ImageExtended:
|
||||
target_image.convert_rgb_to_indexed()
|
||||
|
||||
|
||||
## Appends data to a [Dictionary] to be used for undo/redo.
|
||||
func serialize_undo_data() -> Dictionary:
|
||||
var dict := {}
|
||||
var cell_indices := []
|
||||
cell_indices.resize(cells.size())
|
||||
for i in cell_indices.size():
|
||||
cell_indices[i] = cells[i].serialize()
|
||||
dict["cell_indices"] = cell_indices
|
||||
dict["tileset"] = tileset.serialize_undo_data()
|
||||
dict["resize"] = false
|
||||
return dict
|
||||
|
||||
|
||||
## Same purpose as [method serialize_undo_data], but for when the image resource
|
||||
## ([param source_image]) we want to store to the undo/redo stack
|
||||
## is not the same as [member image]. This method also handles the resizing logic for undo/redo.
|
||||
func serialize_undo_data_source_image(
|
||||
source_image: ImageExtended, redo_data: Dictionary, undo_data: Dictionary
|
||||
) -> void:
|
||||
undo_data[self] = serialize_undo_data()
|
||||
if source_image.get_size() != image.get_size():
|
||||
undo_data[self]["resize"] = true
|
||||
_resize_cells(source_image.get_size())
|
||||
tileset.clear_tileset(self)
|
||||
var tile_editing_mode := TileSetPanel.tile_editing_mode
|
||||
if tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL:
|
||||
tile_editing_mode = TileSetPanel.TileEditingMode.AUTO
|
||||
update_tilemap(tile_editing_mode, source_image)
|
||||
redo_data[self] = serialize_undo_data()
|
||||
redo_data[self]["resize"] = undo_data[self]["resize"]
|
||||
|
||||
|
||||
## Reads data from a [param dict] [Dictionary], and uses them to add methods to [param undo_redo].
|
||||
func deserialize_undo_data(dict: Dictionary, undo_redo: UndoRedo, undo: bool) -> void:
|
||||
var cell_indices = dict.cell_indices
|
||||
if undo:
|
||||
undo_redo.add_undo_method(_deserialize_cell_data.bind(cell_indices, dict.resize))
|
||||
if dict.has("tileset"):
|
||||
undo_redo.add_undo_method(tileset.deserialize_undo_data.bind(dict.tileset, self))
|
||||
else:
|
||||
undo_redo.add_do_method(_deserialize_cell_data.bind(cell_indices, dict.resize))
|
||||
if dict.has("tileset"):
|
||||
undo_redo.add_do_method(tileset.deserialize_undo_data.bind(dict.tileset, self))
|
||||
|
||||
|
||||
## Gets called every time a change is being applied to the [param image],
|
||||
## such as when finishing drawing with a draw tool, or when applying an image effect.
|
||||
## This method responsible for updating the indices of the [member cells], as well as
|
||||
## updating the [member tileset] with the incoming changes.
|
||||
## The updating behavior depends on the current tile editing mode
|
||||
## by [member TileSetPanel.tile_editing_mode].
|
||||
## If a [param source_image] is provided, that image is being used instead of [member image].
|
||||
func update_tilemap(
|
||||
tile_editing_mode := TileSetPanel.tile_editing_mode, source_image := image
|
||||
) -> void:
|
||||
editing_images.clear()
|
||||
var tileset_size_before_update := tileset.tiles.size()
|
||||
for i in cells.size():
|
||||
var coords := get_cell_coords_in_image(i)
|
||||
var rect := Rect2i(coords, tileset.tile_size)
|
||||
var image_portion := source_image.get_region(rect)
|
||||
var index := cells[i].index
|
||||
if index >= tileset.tiles.size():
|
||||
index = 0
|
||||
var current_tile := tileset.tiles[index]
|
||||
if tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL:
|
||||
if image_portion.is_invisible():
|
||||
continue
|
||||
if index == 0:
|
||||
# If the tileset is empty, only then add a new tile.
|
||||
if tileset.tiles.size() <= 1:
|
||||
tileset.add_tile(image_portion, self)
|
||||
cells[i].index = tileset.tiles.size() - 1
|
||||
continue
|
||||
if not _tiles_equal(i, image_portion, current_tile.image):
|
||||
tileset.replace_tile_at(image_portion, index, self)
|
||||
elif tile_editing_mode == TileSetPanel.TileEditingMode.AUTO:
|
||||
_handle_auto_editing_mode(i, image_portion, tileset_size_before_update)
|
||||
else: # Stack
|
||||
if image_portion.is_invisible():
|
||||
continue
|
||||
var found_tile := false
|
||||
for j in range(1, tileset.tiles.size()):
|
||||
var tile := tileset.tiles[j]
|
||||
if _tiles_equal(i, image_portion, tile.image):
|
||||
if cells[i].index != j:
|
||||
cells[i].index = j
|
||||
tileset.tiles[j].times_used += 1
|
||||
cells[i].remove_transformations()
|
||||
found_tile = true
|
||||
break
|
||||
if not found_tile:
|
||||
tileset.add_tile(image_portion, self)
|
||||
cells[i].index = tileset.tiles.size() - 1
|
||||
cells[i].remove_transformations()
|
||||
# Updates transparent cells that have indices higher than 0.
|
||||
# This can happen when switching to another tileset which has less tiles
|
||||
# than the previous one.
|
||||
for i in cells.size():
|
||||
var coords := get_cell_coords_in_image(i)
|
||||
var rect := Rect2i(coords, tileset.tile_size)
|
||||
var image_portion := source_image.get_region(rect)
|
||||
if not image_portion.is_invisible():
|
||||
continue
|
||||
var index := cells[i].index
|
||||
if index == 0:
|
||||
continue
|
||||
if index >= tileset.tiles.size():
|
||||
index = 0
|
||||
var current_tile := tileset.tiles[index]
|
||||
if not _tiles_equal(i, image_portion, current_tile.image):
|
||||
set_index(i, cells[i].index)
|
||||
|
||||
|
||||
## Gets called by [method update_tilemap]. This method is responsible for handling
|
||||
## the tilemap updating behavior for the auto tile editing mode.[br]
|
||||
## Cases:[br]
|
||||
## 0) Cell is transparent. Set its index to 0.
|
||||
## [br]
|
||||
## 0.5) Cell is transparent and mapped.
|
||||
## Set its index to 0 and unuse the mapped tile.
|
||||
## If the mapped tile is removed, reduce the index of all cells that have
|
||||
## indices greater or equal than the existing tile's index.
|
||||
## [br]
|
||||
## 1) Cell not mapped, exists in the tileset.
|
||||
## Map the cell to the existing tile and increase its times_used by one.
|
||||
## [br]
|
||||
## 2) Cell not mapped, does not exist in the tileset.
|
||||
## Add the cell as a tile in the tileset, set its index to be the tileset's tile size - 1.
|
||||
## [br]
|
||||
## 3) Cell mapped, tile did not change. Do nothing.
|
||||
## [br]
|
||||
## 4) Cell mapped, exists in the tileset.
|
||||
## The mapped tile still exists in the tileset.
|
||||
## Map the cell to the existing tile, increase its times_used by one,
|
||||
## and reduce the previously mapped tile's times_used by 1.
|
||||
## [br]
|
||||
## 5) Cell mapped, exists in the tileset.
|
||||
## The mapped tile does not exist in the tileset anymore.
|
||||
## Map the cell to the existing tile and increase its times_used by one.
|
||||
## Remove the previously mapped tile,
|
||||
## and reduce the index of all cells that have indices greater or equal
|
||||
## than the existing tile's index.
|
||||
## [br]
|
||||
## 6) Cell mapped, does not exist in the tileset.
|
||||
## The mapped tile still exists in the tileset.
|
||||
## Add the cell as a tile in the tileset, set its index to be the tileset's tile size - 1.
|
||||
## Reduce the previously mapped tile's times_used by 1.
|
||||
## [br]
|
||||
## 7) Cell mapped, does not exist in the tileset.
|
||||
## The mapped tile does not exist in the tileset anymore.
|
||||
## Simply replace the old tile with the new one, do not change its index.
|
||||
func _handle_auto_editing_mode(
|
||||
i: int, image_portion: Image, tileset_size_before_update: int
|
||||
) -> void:
|
||||
var index := cells[i].index
|
||||
if index >= tileset.tiles.size():
|
||||
index = 0
|
||||
var current_tile := tileset.tiles[index]
|
||||
if image_portion.is_invisible():
|
||||
# Case 0: The cell is transparent.
|
||||
if cells[i].index >= tileset_size_before_update:
|
||||
return
|
||||
cells[i].index = 0
|
||||
cells[i].remove_transformations()
|
||||
if index > 0:
|
||||
# Case 0.5: The cell is transparent and mapped to a tile.
|
||||
var is_removed := tileset.unuse_tile_at_index(index, self)
|
||||
if is_removed:
|
||||
# Re-index all indices that are after the deleted one.
|
||||
_re_index_cells_after_index(index)
|
||||
return
|
||||
var index_in_tileset := tileset.find_tile(image_portion)
|
||||
if index == 0: # If the cell is not mapped to a tile.
|
||||
if index_in_tileset > -1:
|
||||
# Case 1: The cell is not mapped already,
|
||||
# and it exists in the tileset as a tile.
|
||||
tileset.tiles[index_in_tileset].times_used += 1
|
||||
cells[i].index = index_in_tileset
|
||||
else:
|
||||
# Case 2: The cell is not mapped already,
|
||||
# and it does not exist in the tileset.
|
||||
tileset.add_tile(image_portion, self)
|
||||
cells[i].index = tileset.tiles.size() - 1
|
||||
else: # If the cell is already mapped.
|
||||
if _tiles_equal(i, image_portion, current_tile.image):
|
||||
# Case 3: The cell is mapped and it did not change.
|
||||
# Do nothing and move on to the next cell.
|
||||
return
|
||||
if index_in_tileset > -1: # If the cell exists in the tileset as a tile.
|
||||
if current_tile.times_used > 1:
|
||||
# Case 4: The cell is mapped and it exists in the tileset as a tile,
|
||||
# and the currently mapped tile still exists in the tileset.
|
||||
tileset.tiles[index_in_tileset].times_used += 1
|
||||
cells[i].index = index_in_tileset
|
||||
tileset.unuse_tile_at_index(index, self)
|
||||
else:
|
||||
# Case 5: The cell is mapped and it exists in the tileset as a tile,
|
||||
# and the currently mapped tile no longer exists in the tileset.
|
||||
tileset.tiles[index_in_tileset].times_used += 1
|
||||
cells[i].index = index_in_tileset
|
||||
tileset.remove_tile_at_index(index, self)
|
||||
# Re-index all indices that are after the deleted one.
|
||||
_re_index_cells_after_index(index)
|
||||
else: # If the cell does not exist in the tileset as a tile.
|
||||
if current_tile.times_used > 1:
|
||||
# Case 6: The cell is mapped and it does not
|
||||
# exist in the tileset as a tile,
|
||||
# and the currently mapped tile still exists in the tileset.
|
||||
tileset.unuse_tile_at_index(index, self)
|
||||
tileset.add_tile(image_portion, self)
|
||||
cells[i].index = tileset.tiles.size() - 1
|
||||
else:
|
||||
# Case 7: The cell is mapped and it does not
|
||||
# exist in the tileset as a tile,
|
||||
# and the currently mapped tile no longer exists in the tileset.
|
||||
tileset.replace_tile_at(image_portion, index, self)
|
||||
cells[i].remove_transformations()
|
||||
|
||||
|
||||
## Re-indexes all [member cells] that are larger or equal to [param index],
|
||||
## by reducing their value by one.
|
||||
func _re_index_cells_after_index(index: int) -> void:
|
||||
for i in cells.size():
|
||||
var tmp_index := cells[i].index
|
||||
if tmp_index >= index:
|
||||
cells[i].index -= 1
|
||||
|
||||
|
||||
## Updates the [member image] data of the cell of the tilemap in [param cell_position],
|
||||
## to ensure that it is the same as its mapped tile in the [member tileset].
|
||||
func _update_cell(cell_position: int) -> void:
|
||||
var coords := get_cell_coords_in_image(cell_position)
|
||||
var rect := Rect2i(coords, tileset.tile_size)
|
||||
var image_portion := image.get_region(rect)
|
||||
var cell_data := cells[cell_position]
|
||||
var index := cell_data.index
|
||||
if index >= tileset.tiles.size():
|
||||
index = 0
|
||||
var current_tile := tileset.tiles[index].image
|
||||
var transformed_tile := transform_tile(
|
||||
current_tile, cell_data.flip_h, cell_data.flip_v, cell_data.transpose
|
||||
)
|
||||
if image_portion.get_data() != transformed_tile.get_data():
|
||||
var tile_size := transformed_tile.get_size()
|
||||
image.blit_rect(transformed_tile, Rect2i(Vector2i.ZERO, tile_size), coords)
|
||||
image.convert_rgb_to_indexed()
|
||||
|
||||
|
||||
## Calls [method _update_cell] for all [member cells].
|
||||
func update_cel_portions() -> void:
|
||||
for i in cells.size():
|
||||
_update_cell(i)
|
||||
|
||||
|
||||
## Loops through all [member cells] of the tilemap and updates their indices,
|
||||
## so they can remain mapped to the [member tileset]'s tiles.
|
||||
func _re_index_all_cells() -> void:
|
||||
for i in cells.size():
|
||||
var coords := get_cell_coords_in_image(i)
|
||||
var rect := Rect2i(coords, tileset.tile_size)
|
||||
var image_portion := image.get_region(rect)
|
||||
if image_portion.is_invisible():
|
||||
var index := cells[i].index
|
||||
if index > 0 and index < tileset.tiles.size():
|
||||
var current_tile := tileset.tiles[index]
|
||||
if not _tiles_equal(i, image_portion, current_tile.image):
|
||||
set_index(i, cells[i].index)
|
||||
continue
|
||||
for j in range(1, tileset.tiles.size()):
|
||||
var tile := tileset.tiles[j]
|
||||
if _tiles_equal(i, image_portion, tile.image):
|
||||
cells[i].index = j
|
||||
break
|
||||
|
||||
|
||||
## Resizes the [member cells] array based on [param new_size].
|
||||
func _resize_cells(new_size: Vector2i, reset_indices := true) -> void:
|
||||
horizontal_cells = ceili(float(new_size.x) / tileset.tile_size.x)
|
||||
vertical_cells = ceili(float(new_size.y) / tileset.tile_size.y)
|
||||
cells.resize(horizontal_cells * vertical_cells)
|
||||
for i in cells.size():
|
||||
if reset_indices:
|
||||
cells[i] = Cell.new()
|
||||
else:
|
||||
if not is_instance_valid(cells[i]):
|
||||
cells[i] = Cell.new()
|
||||
|
||||
|
||||
## Returns [code]true[/code] if the user just did a Redo.
|
||||
func _is_redo() -> bool:
|
||||
return Global.control.redone
|
||||
|
||||
|
||||
## If the tileset has been modified by another [param cel],
|
||||
## make sure to also update it here.
|
||||
## If [param replace_index] is larger than -1, it means that manual mode
|
||||
## has been used to replace a tile in the tileset in another cel,
|
||||
## so call [method update_cel_portions] to update it in this cel as well.
|
||||
## Otherwise, call [method _re_index_all_cells] to ensure that the cells have correct indices.
|
||||
func _on_tileset_updated(cel: CelTileMap, replace_index: int) -> void:
|
||||
if cel == self or not is_instance_valid(cel):
|
||||
return
|
||||
if link_set != null and cel in link_set["cels"]:
|
||||
return
|
||||
if replace_index > -1: # Manual mode
|
||||
update_cel_portions()
|
||||
else:
|
||||
_re_index_all_cells()
|
||||
Global.canvas.update_all_layers = true
|
||||
Global.canvas.queue_redraw()
|
||||
|
||||
|
||||
func _deserialize_cell_data(cell_indices: Array, resize: bool) -> void:
|
||||
if resize:
|
||||
_resize_cells(image.get_size())
|
||||
for i in cell_indices.size():
|
||||
var cell_data: Dictionary = cell_indices[i]
|
||||
cells[i].deserialize(cell_data)
|
||||
|
||||
|
||||
# Overridden Methods:
|
||||
func set_content(content, texture: ImageTexture = null) -> void:
|
||||
super.set_content(content, texture)
|
||||
_resize_cells(image.get_size())
|
||||
_re_index_all_cells()
|
||||
|
||||
|
||||
func update_texture(undo := false) -> void:
|
||||
var tile_editing_mode := TileSetPanel.tile_editing_mode
|
||||
if undo or _is_redo() or tile_editing_mode != TileSetPanel.TileEditingMode.MANUAL:
|
||||
super.update_texture(undo)
|
||||
editing_images.clear()
|
||||
return
|
||||
|
||||
for i in cells.size():
|
||||
var cell_data := cells[i]
|
||||
var index := cell_data.index
|
||||
if index >= tileset.tiles.size():
|
||||
index = 0
|
||||
var coords := get_cell_coords_in_image(i)
|
||||
var rect := Rect2i(coords, tileset.tile_size)
|
||||
var image_portion := image.get_region(rect)
|
||||
var current_tile := tileset.tiles[index]
|
||||
if index == 0:
|
||||
if tileset.tiles.size() > 1:
|
||||
# Prevent from drawing on empty image portions.
|
||||
var tile_size := current_tile.image.get_size()
|
||||
image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords)
|
||||
continue
|
||||
if not editing_images.has(index):
|
||||
if not _tiles_equal(i, image_portion, current_tile.image):
|
||||
var transformed_image := transform_tile(
|
||||
image_portion, cell_data.flip_h, cell_data.flip_v, cell_data.transpose, true
|
||||
)
|
||||
editing_images[index] = [i, transformed_image]
|
||||
|
||||
for i in cells.size():
|
||||
var cell_data := cells[i]
|
||||
var index := cell_data.index
|
||||
if index >= tileset.tiles.size():
|
||||
index = 0
|
||||
var coords := get_cell_coords_in_image(i)
|
||||
var rect := Rect2i(coords, tileset.tile_size)
|
||||
var image_portion := image.get_region(rect)
|
||||
if editing_images.has(index):
|
||||
var editing_portion := editing_images[index][0] as int
|
||||
if i == editing_portion:
|
||||
var transformed_image := transform_tile(
|
||||
image_portion, cell_data.flip_h, cell_data.flip_v, cell_data.transpose, true
|
||||
)
|
||||
editing_images[index] = [i, transformed_image]
|
||||
var editing_image := editing_images[index][1] as Image
|
||||
var transformed_editing_image := transform_tile(
|
||||
editing_image, cell_data.flip_h, cell_data.flip_v, cell_data.transpose
|
||||
)
|
||||
if not image_portion.get_data() == transformed_editing_image.get_data():
|
||||
var tile_size := image_portion.get_size()
|
||||
image.blit_rect(transformed_editing_image, Rect2i(Vector2i.ZERO, tile_size), coords)
|
||||
super.update_texture(undo)
|
||||
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
var dict := super.serialize()
|
||||
var cell_indices := []
|
||||
cell_indices.resize(cells.size())
|
||||
for i in cell_indices.size():
|
||||
cell_indices[i] = cells[i].serialize()
|
||||
dict["cell_indices"] = cell_indices
|
||||
return dict
|
||||
|
||||
|
||||
func deserialize(dict: Dictionary) -> void:
|
||||
super.deserialize(dict)
|
||||
var cell_indices = dict.get("cell_indices")
|
||||
for i in cell_indices.size():
|
||||
cells[i].deserialize(cell_indices[i])
|
||||
|
||||
|
||||
func get_class_name() -> String:
|
||||
return "CelTileMap"
|
|
@ -10,9 +10,7 @@ func _init(_opacity := 1.0) -> void:
|
|||
|
||||
|
||||
func get_image() -> Image:
|
||||
var image := Image.create(
|
||||
Global.current_project.size.x, Global.current_project.size.y, false, Image.FORMAT_RGBA8
|
||||
)
|
||||
var image := Global.current_project.new_empty_image()
|
||||
return image
|
||||
|
||||
|
||||
|
|
|
@ -4,28 +4,34 @@ extends BaseCel
|
|||
## The term "cel" comes from "celluloid" (https://en.wikipedia.org/wiki/Cel).
|
||||
|
||||
## This variable is where the image data of the cel are.
|
||||
var image: Image:
|
||||
var image: ImageExtended:
|
||||
set = image_changed
|
||||
|
||||
|
||||
func _init(_image := Image.new(), _opacity := 1.0) -> void:
|
||||
func _init(_image := ImageExtended.new(), _opacity := 1.0) -> void:
|
||||
image_texture = ImageTexture.new()
|
||||
image = _image # Set image and call setter
|
||||
opacity = _opacity
|
||||
|
||||
|
||||
func image_changed(value: Image) -> void:
|
||||
func image_changed(value: ImageExtended) -> void:
|
||||
image = value
|
||||
if not image.is_empty() and is_instance_valid(image_texture):
|
||||
image_texture.set_image(image)
|
||||
|
||||
|
||||
func get_content():
|
||||
func get_content() -> ImageExtended:
|
||||
return image
|
||||
|
||||
|
||||
func set_content(content, texture: ImageTexture = null) -> void:
|
||||
image = content
|
||||
var proper_content: ImageExtended
|
||||
if content is not ImageExtended:
|
||||
proper_content = ImageExtended.new()
|
||||
proper_content.copy_from_custom(content, image.is_indexed)
|
||||
else:
|
||||
proper_content = content
|
||||
image = proper_content
|
||||
if is_instance_valid(texture) and is_instance_valid(texture.get_image()):
|
||||
image_texture = texture
|
||||
if image_texture.get_image().get_size() != image.get_size():
|
||||
|
@ -34,27 +40,29 @@ func set_content(content, texture: ImageTexture = null) -> void:
|
|||
image_texture.update(image)
|
||||
|
||||
|
||||
func create_empty_content():
|
||||
var empty_image := Image.create(
|
||||
image.get_size().x, image.get_size().y, false, Image.FORMAT_RGBA8
|
||||
)
|
||||
return empty_image
|
||||
func create_empty_content() -> ImageExtended:
|
||||
var empty := Image.create(image.get_width(), image.get_height(), false, image.get_format())
|
||||
var new_image := ImageExtended.new()
|
||||
new_image.copy_from_custom(empty, image.is_indexed)
|
||||
return new_image
|
||||
|
||||
|
||||
func copy_content():
|
||||
var copy_image := Image.create_from_data(
|
||||
image.get_width(), image.get_height(), false, Image.FORMAT_RGBA8, image.get_data()
|
||||
func copy_content() -> ImageExtended:
|
||||
var tmp_image := Image.create_from_data(
|
||||
image.get_width(), image.get_height(), false, image.get_format(), image.get_data()
|
||||
)
|
||||
var copy_image := ImageExtended.new()
|
||||
copy_image.copy_from_custom(tmp_image, image.is_indexed)
|
||||
return copy_image
|
||||
|
||||
|
||||
func get_image() -> Image:
|
||||
func get_image() -> ImageExtended:
|
||||
return image
|
||||
|
||||
|
||||
func update_texture() -> void:
|
||||
func update_texture(undo := false) -> void:
|
||||
image_texture.set_image(image)
|
||||
super.update_texture()
|
||||
super.update_texture(undo)
|
||||
|
||||
|
||||
func get_class_name() -> String:
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
class_name Drawer
|
||||
|
||||
const NUMBER_OF_DRAWERS := 8
|
||||
|
||||
var pixel_perfect := false:
|
||||
set(value):
|
||||
pixel_perfect = value
|
||||
if pixel_perfect:
|
||||
drawers = pixel_perfect_drawers.duplicate()
|
||||
else:
|
||||
drawers = [simple_drawer, simple_drawer, simple_drawer, simple_drawer]
|
||||
_create_simple_drawers()
|
||||
var color_op := ColorOp.new()
|
||||
|
||||
var simple_drawer := SimpleDrawer.new()
|
||||
var pixel_perfect_drawers: Array[PixelPerfectDrawer] = [
|
||||
PixelPerfectDrawer.new(),
|
||||
PixelPerfectDrawer.new(),
|
||||
PixelPerfectDrawer.new(),
|
||||
PixelPerfectDrawer.new()
|
||||
]
|
||||
var drawers := [simple_drawer, simple_drawer, simple_drawer, simple_drawer]
|
||||
var pixel_perfect_drawers: Array[PixelPerfectDrawer] = []
|
||||
var drawers := []
|
||||
|
||||
|
||||
class ColorOp:
|
||||
|
@ -27,12 +24,14 @@ class ColorOp:
|
|||
|
||||
|
||||
class SimpleDrawer:
|
||||
func set_pixel(image: Image, position: Vector2i, color: Color, op: ColorOp) -> void:
|
||||
func set_pixel(image: ImageExtended, position: Vector2i, color: Color, op: ColorOp) -> void:
|
||||
var color_old := image.get_pixelv(position)
|
||||
var color_str := color.to_html()
|
||||
var color_new := op.process(Color(color_str), color_old)
|
||||
if not color_new.is_equal_approx(color_old):
|
||||
image.set_pixelv(position, color_new)
|
||||
image.set_pixelv_custom(position, color_new)
|
||||
else:
|
||||
image.set_pixelv_custom(position, color_new, image.is_indexed)
|
||||
|
||||
|
||||
class PixelPerfectDrawer:
|
||||
|
@ -43,11 +42,11 @@ class PixelPerfectDrawer:
|
|||
func reset() -> void:
|
||||
last_pixels = [null, null]
|
||||
|
||||
func set_pixel(image: Image, position: Vector2i, color: Color, op: ColorOp) -> void:
|
||||
func set_pixel(image: ImageExtended, position: Vector2i, color: Color, op: ColorOp) -> void:
|
||||
var color_old := image.get_pixelv(position)
|
||||
var color_str := color.to_html()
|
||||
last_pixels.push_back([position, color_old])
|
||||
image.set_pixelv(position, op.process(Color(color_str), color_old))
|
||||
image.set_pixelv_custom(position, op.process(Color(color_str), color_old))
|
||||
|
||||
var corner = last_pixels.pop_front()
|
||||
var neighbour = last_pixels[0]
|
||||
|
@ -56,10 +55,25 @@ class PixelPerfectDrawer:
|
|||
return
|
||||
|
||||
if position - corner[0] in CORNERS and position - neighbour[0] in NEIGHBOURS:
|
||||
image.set_pixel(neighbour[0].x, neighbour[0].y, neighbour[1])
|
||||
image.set_pixel_custom(neighbour[0].x, neighbour[0].y, neighbour[1])
|
||||
last_pixels[0] = corner
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
drawers.resize(NUMBER_OF_DRAWERS)
|
||||
pixel_perfect_drawers.resize(NUMBER_OF_DRAWERS)
|
||||
for i in NUMBER_OF_DRAWERS:
|
||||
drawers[i] = simple_drawer
|
||||
pixel_perfect_drawers[i] = PixelPerfectDrawer.new()
|
||||
|
||||
|
||||
func _create_simple_drawers() -> void:
|
||||
drawers = []
|
||||
drawers.resize(NUMBER_OF_DRAWERS)
|
||||
for i in NUMBER_OF_DRAWERS:
|
||||
drawers[i] = simple_drawer
|
||||
|
||||
|
||||
func reset() -> void:
|
||||
for drawer in pixel_perfect_drawers:
|
||||
drawer.reset()
|
||||
|
@ -72,7 +86,12 @@ func set_pixel(image: Image, position: Vector2i, color: Color, ignore_mirroring
|
|||
SteamManager.set_achievement("ACH_FIRST_PIXEL")
|
||||
if ignore_mirroring:
|
||||
return
|
||||
if not Tools.horizontal_mirror and not Tools.vertical_mirror:
|
||||
if (
|
||||
not Tools.horizontal_mirror
|
||||
and not Tools.vertical_mirror
|
||||
and not Tools.diagonal_xy_mirror
|
||||
and not Tools.diagonal_x_minus_y_mirror
|
||||
):
|
||||
return
|
||||
# Handle mirroring
|
||||
var mirrored_positions := Tools.get_mirrored_positions(position, project)
|
||||
|
|
|
@ -11,3 +11,26 @@ var user_data := "" ## User defined data, set in the frame properties.
|
|||
func _init(_cels: Array[BaseCel] = [], _duration := 1.0) -> void:
|
||||
cels = _cels
|
||||
duration = _duration
|
||||
|
||||
|
||||
func get_duration_in_seconds(fps: float) -> float:
|
||||
return duration * (1.0 / fps)
|
||||
|
||||
|
||||
func position_in_seconds(project: Project, start_from := 0) -> float:
|
||||
var pos := 0.0
|
||||
var index := project.frames.find(self)
|
||||
if index > start_from:
|
||||
for i in range(start_from, index):
|
||||
if i >= 0:
|
||||
var frame := project.frames[i]
|
||||
pos += frame.get_duration_in_seconds(project.fps)
|
||||
else:
|
||||
pos += 1.0 / project.fps
|
||||
else:
|
||||
if start_from >= project.frames.size():
|
||||
return -1.0
|
||||
for i in range(start_from, index, -1):
|
||||
var frame := project.frames[i]
|
||||
pos -= frame.get_duration_in_seconds(project.fps)
|
||||
return pos
|
||||
|
|
|
@ -144,8 +144,10 @@ func set_nodes() -> void:
|
|||
selection_checkbox = $VBoxContainer/OptionsContainer/SelectionCheckBox
|
||||
affect_option_button = $VBoxContainer/OptionsContainer/AffectOptionButton
|
||||
animate_panel = $"%AnimatePanel"
|
||||
animate_panel.image_effect_node = self
|
||||
live_checkbox.button_pressed = live_preview
|
||||
if is_instance_valid(animate_panel):
|
||||
animate_panel.image_effect_node = self
|
||||
if is_instance_valid(live_checkbox):
|
||||
live_checkbox.button_pressed = live_preview
|
||||
|
||||
|
||||
func display_animate_dialog() -> void:
|
||||
|
@ -157,10 +159,14 @@ func display_animate_dialog() -> void:
|
|||
|
||||
|
||||
func _commit_undo(action: String, undo_data: Dictionary, project: Project) -> void:
|
||||
var tile_editing_mode := TileSetPanel.tile_editing_mode
|
||||
if tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL:
|
||||
tile_editing_mode = TileSetPanel.TileEditingMode.AUTO
|
||||
project.update_tilemaps(undo_data, tile_editing_mode)
|
||||
var redo_data := _get_undo_data(project)
|
||||
project.undos += 1
|
||||
project.undo_redo.create_action(action)
|
||||
Global.undo_redo_compress_images(redo_data, undo_data, project)
|
||||
project.deserialize_cel_undo_data(redo_data, undo_data)
|
||||
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false, -1, -1, project))
|
||||
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true, -1, -1, project))
|
||||
project.undo_redo.commit_action()
|
||||
|
@ -168,24 +174,22 @@ func _commit_undo(action: String, undo_data: Dictionary, project: Project) -> vo
|
|||
|
||||
func _get_undo_data(project: Project) -> Dictionary:
|
||||
var data := {}
|
||||
var images := _get_selected_draw_images(project)
|
||||
for image in images:
|
||||
data[image] = image.data
|
||||
project.serialize_cel_undo_data(_get_selected_draw_cels(project), data)
|
||||
return data
|
||||
|
||||
|
||||
func _get_selected_draw_images(project: Project) -> Array[Image]:
|
||||
var images: Array[Image] = []
|
||||
func _get_selected_draw_cels(project: Project) -> Array[BaseCel]:
|
||||
var images: Array[BaseCel] = []
|
||||
if affect == SELECTED_CELS:
|
||||
for cel_index in project.selected_cels:
|
||||
var cel: BaseCel = project.frames[cel_index[0]].cels[cel_index[1]]
|
||||
if cel is PixelCel:
|
||||
images.append(cel.get_image())
|
||||
images.append(cel)
|
||||
else:
|
||||
for frame in project.frames:
|
||||
for cel in frame.cels:
|
||||
if cel is PixelCel:
|
||||
images.append(cel.get_image())
|
||||
images.append(cel)
|
||||
return images
|
||||
|
||||
|
||||
|
|
193
src/Classes/ImageExtended.gd
Normal file
|
@ -0,0 +1,193 @@
|
|||
class_name ImageExtended
|
||||
extends Image
|
||||
|
||||
## A custom [Image] class that implements support for indexed mode.
|
||||
## Before implementing indexed mode, we just used the [Image] class.
|
||||
## In indexed mode, each pixel is assigned to a number that references a palette color.
|
||||
## This essentially means that the colors of the image are restricted to a specific palette,
|
||||
## and they will automatically get updated when you make changes to that palette, or when
|
||||
## you switch to a different one.
|
||||
|
||||
const TRANSPARENT := Color(0)
|
||||
const SET_INDICES := preload("res://src/Shaders/SetIndices.gdshader")
|
||||
const INDEXED_TO_RGB := preload("res://src/Shaders/IndexedToRGB.gdshader")
|
||||
|
||||
## If [code]true[/code], the image uses indexed mode.
|
||||
var is_indexed := false
|
||||
## The [Palette] the image is currently using for indexed mode.
|
||||
var current_palette := Palettes.current_palette
|
||||
## An [Image] that contains the index of each pixel of the main image for indexed mode.
|
||||
## The indices are stored in the red channel of this image, by diving each index by 255.
|
||||
## This means that there can be a maximum index size of 255. 0 means that the pixel is transparent.
|
||||
var indices_image := Image.create_empty(1, 1, false, Image.FORMAT_R8)
|
||||
## A [PackedColorArray] containing all of the colors of the [member current_palette].
|
||||
var palette := PackedColorArray()
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
indices_image.fill(TRANSPARENT)
|
||||
Palettes.palette_selected.connect(select_palette)
|
||||
|
||||
|
||||
## Equivalent of [method Image.create_empty], but returns [ImageExtended] instead.
|
||||
## If [param _is_indexed] is [code]true[/code], the image that is being returned uses indexed mode.
|
||||
static func create_custom(
|
||||
width: int, height: int, mipmaps: bool, format: Image.Format, _is_indexed := false
|
||||
) -> ImageExtended:
|
||||
var new_image := ImageExtended.new()
|
||||
new_image.crop(width, height)
|
||||
if mipmaps:
|
||||
new_image.generate_mipmaps()
|
||||
new_image.convert(format)
|
||||
new_image.fill(TRANSPARENT)
|
||||
new_image.is_indexed = _is_indexed
|
||||
if new_image.is_indexed:
|
||||
new_image.resize_indices()
|
||||
new_image.select_palette("", false)
|
||||
return new_image
|
||||
|
||||
|
||||
## Equivalent of [method Image.copy_from], but also handles the logic necessary for indexed mode.
|
||||
## If [param _is_indexed] is [code]true[/code], the image is set to be using indexed mode.
|
||||
func copy_from_custom(image: Image, indexed := is_indexed) -> void:
|
||||
is_indexed = indexed
|
||||
copy_from(image)
|
||||
if is_indexed:
|
||||
resize_indices()
|
||||
select_palette("", false)
|
||||
convert_rgb_to_indexed()
|
||||
|
||||
|
||||
## Selects a new palette to use in indexed mode.
|
||||
func select_palette(_name: String, convert_to_rgb := true) -> void:
|
||||
current_palette = Palettes.current_palette
|
||||
if not is_instance_valid(current_palette) or not is_indexed:
|
||||
return
|
||||
update_palette()
|
||||
if not current_palette.data_changed.is_connected(update_palette):
|
||||
current_palette.data_changed.connect(update_palette)
|
||||
if not current_palette.data_changed.is_connected(convert_indexed_to_rgb):
|
||||
current_palette.data_changed.connect(convert_indexed_to_rgb)
|
||||
if convert_to_rgb:
|
||||
convert_indexed_to_rgb()
|
||||
|
||||
|
||||
## Updates [member palette] to contain the colors of [member current_palette].
|
||||
func update_palette() -> void:
|
||||
if not is_instance_valid(current_palette):
|
||||
return
|
||||
if palette.size() != current_palette.colors_max:
|
||||
palette.resize(current_palette.colors_max)
|
||||
palette.fill(TRANSPARENT)
|
||||
for i in current_palette.colors:
|
||||
# Due to the decimal nature of the color values, some values get rounded off
|
||||
# unintentionally.
|
||||
# Even though the decimal values change, the HTML code remains the same after the change.
|
||||
# So we're using this trick to convert the values back to how they are shown in
|
||||
# the palette.
|
||||
palette[i] = Color(current_palette.colors[i].color.to_html())
|
||||
|
||||
|
||||
## Displays the actual RGBA values of each pixel in the image from indexed mode.
|
||||
func convert_indexed_to_rgb() -> void:
|
||||
if not is_indexed or not is_instance_valid(current_palette):
|
||||
return
|
||||
var palette_image := current_palette.convert_to_image(false)
|
||||
var palette_texture := ImageTexture.create_from_image(palette_image)
|
||||
var shader_image_effect := ShaderImageEffect.new()
|
||||
var indices_texture := ImageTexture.create_from_image(indices_image)
|
||||
var params := {"palette_texture": palette_texture, "indices_texture": indices_texture}
|
||||
shader_image_effect.generate_image(self, INDEXED_TO_RGB, params, get_size(), false)
|
||||
Global.canvas.queue_redraw()
|
||||
|
||||
|
||||
## Automatically maps each color of the image's pixel to the closest color of the palette,
|
||||
## by finding the palette color's index and storing it in [member indices_image].
|
||||
func convert_rgb_to_indexed() -> void:
|
||||
if not is_indexed or not is_instance_valid(current_palette):
|
||||
return
|
||||
var palette_image := current_palette.convert_to_image(false)
|
||||
var palette_texture := ImageTexture.create_from_image(palette_image)
|
||||
var params := {
|
||||
"palette_texture": palette_texture, "rgb_texture": ImageTexture.create_from_image(self)
|
||||
}
|
||||
var shader_image_effect := ShaderImageEffect.new()
|
||||
shader_image_effect.generate_image(
|
||||
indices_image, SET_INDICES, params, indices_image.get_size(), false
|
||||
)
|
||||
convert_indexed_to_rgb()
|
||||
|
||||
|
||||
## Resizes indices and calls [method convert_rgb_to_indexed] when the image's size changes
|
||||
## and indexed mode is enabled.
|
||||
func on_size_changed() -> void:
|
||||
if is_indexed:
|
||||
resize_indices()
|
||||
convert_rgb_to_indexed()
|
||||
|
||||
|
||||
## Resizes [indices_image] to the image's size.
|
||||
func resize_indices() -> void:
|
||||
indices_image.crop(get_width(), get_height())
|
||||
|
||||
|
||||
## Equivalent of [method Image.set_pixel],
|
||||
## but also handles the logic necessary for indexed mode.
|
||||
func set_pixel_custom(x: int, y: int, color: Color) -> void:
|
||||
set_pixelv_custom(Vector2i(x, y), color)
|
||||
|
||||
|
||||
## Equivalent of [method Image.set_pixelv],
|
||||
## but also handles the logic necessary for indexed mode.
|
||||
func set_pixelv_custom(point: Vector2i, color: Color, index_image_only := false) -> void:
|
||||
var new_color := color
|
||||
if is_indexed:
|
||||
var color_to_fill := TRANSPARENT
|
||||
var color_index := 0
|
||||
if not color.is_equal_approx(TRANSPARENT):
|
||||
if palette.has(color):
|
||||
color_index = palette.find(color)
|
||||
# If the color selected in the palette is the same then it should take prioity.
|
||||
var selected_index = Palettes.current_palette_get_selected_color_index(
|
||||
Tools.active_button
|
||||
)
|
||||
if selected_index != -1:
|
||||
if palette[selected_index].is_equal_approx(color):
|
||||
color_index = selected_index
|
||||
else: # Find the most similar color
|
||||
var smaller_distance := color_distance(color, palette[0])
|
||||
for i in palette.size():
|
||||
var swatch := palette[i]
|
||||
if is_zero_approx(swatch.a): # Skip transparent colors
|
||||
continue
|
||||
var dist := color_distance(color, swatch)
|
||||
if dist < smaller_distance:
|
||||
smaller_distance = dist
|
||||
color_index = i
|
||||
if not indices_image.get_pixelv(point).r8 == color_index + 1:
|
||||
indices_image.set_pixelv(point, Color((color_index + 1) / 255.0, 0, 0, 0))
|
||||
color_to_fill = palette[color_index]
|
||||
new_color = color_to_fill
|
||||
else:
|
||||
indices_image.set_pixelv(point, TRANSPARENT)
|
||||
new_color = TRANSPARENT
|
||||
if not index_image_only:
|
||||
set_pixelv(point, new_color)
|
||||
|
||||
|
||||
## Finds the distance between colors [param c1] and [param c2].
|
||||
func color_distance(c1: Color, c2: Color) -> float:
|
||||
var v1 := Vector4(c1.r, c1.g, c1.b, c1.a)
|
||||
var v2 := Vector4(c2.r, c2.g, c2.b, c2.a)
|
||||
return v2.distance_to(v1)
|
||||
|
||||
|
||||
## Adds image data to a [param dict] [Dictionary]. Used for undo/redo.
|
||||
func add_data_to_dictionary(dict: Dictionary, other_image: ImageExtended = null) -> void:
|
||||
# The order matters! Setting self's data first would make undo/redo appear to work incorrectly.
|
||||
if is_instance_valid(other_image):
|
||||
dict[other_image.indices_image] = indices_image.data
|
||||
dict[other_image] = data
|
||||
else:
|
||||
dict[indices_image] = indices_image.data
|
||||
dict[self] = data
|
74
src/Classes/Layers/AudioLayer.gd
Normal file
|
@ -0,0 +1,74 @@
|
|||
class_name AudioLayer
|
||||
extends BaseLayer
|
||||
## A unique type of layer which acts as an audio track for the timeline.
|
||||
## Each audio layer has one audio stream, and its starting position can be
|
||||
## in any point during the animation.
|
||||
|
||||
signal audio_changed
|
||||
signal playback_frame_changed
|
||||
|
||||
var audio: AudioStream: ## The audio stream of the layer.
|
||||
set(value):
|
||||
audio = value
|
||||
audio_changed.emit()
|
||||
var playback_position := 0.0: ## The time in seconds where the audio stream starts playing.
|
||||
get():
|
||||
if playback_frame >= 0:
|
||||
var frame := project.frames[playback_frame]
|
||||
return frame.position_in_seconds(project)
|
||||
var pos := 0.0
|
||||
for i in absi(playback_frame):
|
||||
pos -= 1.0 / project.fps
|
||||
return pos
|
||||
var playback_frame := 0: ## The frame where the audio stream starts playing.
|
||||
set(value):
|
||||
playback_frame = value
|
||||
playback_frame_changed.emit()
|
||||
|
||||
|
||||
func _init(_project: Project, _name := "") -> void:
|
||||
project = _project
|
||||
name = _name
|
||||
|
||||
|
||||
## Returns the length of the audio stream.
|
||||
func get_audio_length() -> float:
|
||||
if is_instance_valid(audio):
|
||||
return audio.get_length()
|
||||
else:
|
||||
return -1.0
|
||||
|
||||
|
||||
## Returns the class name of the audio stream. E.g. "AudioStreamMP3".
|
||||
func get_audio_type() -> String:
|
||||
if not is_instance_valid(audio):
|
||||
return ""
|
||||
return audio.get_class()
|
||||
|
||||
|
||||
# Overridden Methods:
|
||||
func serialize() -> Dictionary:
|
||||
var data := {
|
||||
"name": name,
|
||||
"type": get_layer_type(),
|
||||
"playback_frame": playback_frame,
|
||||
"audio_type": get_audio_type()
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
func deserialize(dict: Dictionary) -> void:
|
||||
super.deserialize(dict)
|
||||
playback_frame = dict.get("playback_frame", playback_frame)
|
||||
|
||||
|
||||
func get_layer_type() -> int:
|
||||
return Global.LayerTypes.AUDIO
|
||||
|
||||
|
||||
func new_empty_cel() -> AudioCel:
|
||||
return AudioCel.new()
|
||||
|
||||
|
||||
func set_name_to_default(number: int) -> void:
|
||||
name = tr("Audio") + " %s" % number
|
|
@ -218,19 +218,28 @@ func link_cel(cel: BaseCel, link_set = null) -> void:
|
|||
## This method is not destructive as it does NOT change the data of the image,
|
||||
## it just returns a copy.
|
||||
func display_effects(cel: BaseCel, image_override: Image = null) -> Image:
|
||||
var image := Image.new()
|
||||
var image := ImageExtended.new()
|
||||
if is_instance_valid(image_override):
|
||||
image.copy_from(image_override)
|
||||
if image_override is ImageExtended:
|
||||
image.is_indexed = image_override.is_indexed
|
||||
image.copy_from_custom(image_override)
|
||||
else:
|
||||
image.copy_from(cel.get_image())
|
||||
var cel_image := cel.get_image()
|
||||
if cel_image is ImageExtended:
|
||||
image.is_indexed = cel_image.is_indexed
|
||||
image.copy_from_custom(cel_image)
|
||||
if not effects_enabled:
|
||||
return image
|
||||
var image_size := image.get_size()
|
||||
for effect in effects:
|
||||
if not effect.enabled:
|
||||
if not effect.enabled or not is_instance_valid(effect.shader):
|
||||
continue
|
||||
var params := effect.params
|
||||
params["PXO_time"] = cel.get_frame(project).position_in_seconds(project)
|
||||
params["PXO_frame_index"] = project.frames.find(cel.get_frame(project))
|
||||
params["PXO_layer_index"] = index
|
||||
var shader_image_effect := ShaderImageEffect.new()
|
||||
shader_image_effect.generate_image(image, effect.shader, effect.params, image_size)
|
||||
shader_image_effect.generate_image(image, effect.shader, params, image_size)
|
||||
# Inherit effects from the parents, if their blend mode is set to pass through
|
||||
for ancestor in get_ancestors():
|
||||
if ancestor.blend_mode != BlendModes.PASS_THROUGH:
|
||||
|
|
|
@ -13,7 +13,9 @@ func _init(_project: Project, _name := "") -> void:
|
|||
|
||||
## Blends all of the images of children layer of the group layer into a single image.
|
||||
func blend_children(frame: Frame, origin := Vector2i.ZERO, apply_effects := true) -> Image:
|
||||
var image := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
|
||||
var image := ImageExtended.create_custom(
|
||||
project.size.x, project.size.y, false, project.get_image_format(), project.is_indexed()
|
||||
)
|
||||
var children := get_children(false)
|
||||
if children.size() <= 0:
|
||||
return image
|
||||
|
@ -66,7 +68,7 @@ func blend_children(frame: Frame, origin := Vector2i.ZERO, apply_effects := true
|
|||
|
||||
|
||||
func _include_child_in_blending(
|
||||
image: Image,
|
||||
image: ImageExtended,
|
||||
layer: BaseLayer,
|
||||
frame: Frame,
|
||||
textures: Array[Image],
|
||||
|
@ -100,7 +102,7 @@ func _include_child_in_blending(
|
|||
## Gets called recursively if the child group has children groups of its own,
|
||||
## and they are also set to pass through mode.
|
||||
func _blend_child_group(
|
||||
image: Image,
|
||||
image: ImageExtended,
|
||||
layer: BaseLayer,
|
||||
frame: Frame,
|
||||
textures: Array[Image],
|
||||
|
|
57
src/Classes/Layers/LayerTileMap.gd
Normal file
|
@ -0,0 +1,57 @@
|
|||
class_name LayerTileMap
|
||||
extends PixelLayer
|
||||
|
||||
## A layer type for 2D tile-based maps.
|
||||
## A LayerTileMap uses a [TileSetCustom], which is then by all of its [CelTileMap]s.
|
||||
## This class doesn't hold any actual tilemap data, as they are different in each cel.
|
||||
## For this reason, that data is being handled by the [CelTileMap] class.
|
||||
## Not to be confused with [TileMapLayer], which is a Godot node.
|
||||
|
||||
## The [TileSetCustom] that this layer uses.
|
||||
## Internally, this class doesn't make much use of this.
|
||||
## It's mostly only used to be passed down to the layer's [CelTileMap]s.
|
||||
var tileset: TileSetCustom
|
||||
|
||||
|
||||
func _init(_project: Project, _tileset: TileSetCustom, _name := "") -> void:
|
||||
super._init(_project, _name)
|
||||
tileset = _tileset
|
||||
if not project.tilesets.has(tileset) and is_instance_valid(tileset):
|
||||
project.add_tileset(tileset)
|
||||
|
||||
|
||||
# Overridden Methods:
|
||||
func serialize() -> Dictionary:
|
||||
var dict := super.serialize()
|
||||
dict["tileset_index"] = project.tilesets.find(tileset)
|
||||
return dict
|
||||
|
||||
|
||||
func deserialize(dict: Dictionary) -> void:
|
||||
super.deserialize(dict)
|
||||
new_cels_linked = dict.new_cels_linked
|
||||
var tileset_index = dict.get("tileset_index")
|
||||
tileset = project.tilesets[tileset_index]
|
||||
|
||||
|
||||
func get_layer_type() -> int:
|
||||
return Global.LayerTypes.TILEMAP
|
||||
|
||||
|
||||
func new_empty_cel() -> BaseCel:
|
||||
var format := project.get_image_format()
|
||||
var is_indexed := project.is_indexed()
|
||||
var image := ImageExtended.create_custom(
|
||||
project.size.x, project.size.y, false, format, is_indexed
|
||||
)
|
||||
return CelTileMap.new(tileset, image)
|
||||
|
||||
|
||||
func new_cel_from_image(image: Image) -> PixelCel:
|
||||
var image_extended := ImageExtended.new()
|
||||
image_extended.copy_from_custom(image, project.is_indexed())
|
||||
return CelTileMap.new(tileset, image_extended)
|
||||
|
||||
|
||||
func set_name_to_default(number: int) -> void:
|
||||
name = tr("Tilemap") + " %s" % number
|
|
@ -28,9 +28,19 @@ func get_layer_type() -> int:
|
|||
|
||||
|
||||
func new_empty_cel() -> BaseCel:
|
||||
var image := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
|
||||
var format := project.get_image_format()
|
||||
var is_indexed := project.is_indexed()
|
||||
var image := ImageExtended.create_custom(
|
||||
project.size.x, project.size.y, false, format, is_indexed
|
||||
)
|
||||
return PixelCel.new(image)
|
||||
|
||||
|
||||
func new_cel_from_image(image: Image) -> PixelCel:
|
||||
var pixelorama_image := ImageExtended.new()
|
||||
pixelorama_image.copy_from_custom(image, project.is_indexed())
|
||||
return PixelCel.new(pixelorama_image)
|
||||
|
||||
|
||||
func can_layer_get_drawn() -> bool:
|
||||
return is_visible_in_hierarchy() && !is_locked_in_hierarchy()
|
||||
|
|
|
@ -8,6 +8,9 @@ signal serialized(dict: Dictionary)
|
|||
signal about_to_deserialize(dict: Dictionary)
|
||||
signal resized
|
||||
signal timeline_updated
|
||||
signal fps_changed
|
||||
|
||||
const INDEXED_MODE := Image.FORMAT_MAX + 1
|
||||
|
||||
var name := "":
|
||||
set(value):
|
||||
|
@ -21,6 +24,18 @@ var undo_redo := UndoRedo.new()
|
|||
var tiles: Tiles
|
||||
var undos := 0 ## The number of times we added undo properties
|
||||
var can_undo := true
|
||||
var color_mode: int = Image.FORMAT_RGBA8:
|
||||
set(value):
|
||||
if color_mode != value:
|
||||
color_mode = value
|
||||
for cel in get_all_pixel_cels():
|
||||
var image := cel.get_image()
|
||||
image.is_indexed = is_indexed()
|
||||
if image.is_indexed:
|
||||
image.resize_indices()
|
||||
image.select_palette("", false)
|
||||
image.convert_rgb_to_indexed()
|
||||
Global.canvas.color_index.queue_redraw()
|
||||
var fill_color := Color(0)
|
||||
var has_changed := false:
|
||||
set(value):
|
||||
|
@ -51,13 +66,20 @@ var brushes: Array[Image] = []
|
|||
var reference_images: Array[ReferenceImage] = []
|
||||
var reference_index: int = -1 # The currently selected index ReferenceImage
|
||||
var vanishing_points := [] ## Array of Vanishing Points
|
||||
var fps := 6.0
|
||||
var fps := 6.0:
|
||||
set(value):
|
||||
fps = value
|
||||
fps_changed.emit()
|
||||
var user_data := "" ## User defined data, set in the project properties.
|
||||
|
||||
var x_symmetry_point: float
|
||||
var y_symmetry_point: float
|
||||
var xy_symmetry_point: Vector2
|
||||
var x_minus_y_symmetry_point: Vector2
|
||||
var x_symmetry_axis := SymmetryGuide.new()
|
||||
var y_symmetry_axis := SymmetryGuide.new()
|
||||
var diagonal_xy_symmetry_axis := SymmetryGuide.new()
|
||||
var diagonal_x_minus_y_symmetry_axis := SymmetryGuide.new()
|
||||
|
||||
var selection_map := SelectionMap.new()
|
||||
## This is useful for when the selection is outside of the canvas boundaries,
|
||||
|
@ -67,6 +89,7 @@ var selection_offset := Vector2i.ZERO:
|
|||
selection_offset = value
|
||||
Global.canvas.selection.marching_ants_outline.offset = selection_offset
|
||||
var has_selection := false
|
||||
var tilesets: Array[TileSetCustom]
|
||||
|
||||
## For every camera (currently there are 3)
|
||||
var cameras_rotation: PackedFloat32Array = [0.0, 0.0, 0.0]
|
||||
|
@ -98,17 +121,32 @@ func _init(_frames: Array[Frame] = [], _name := tr("untitled"), _size := Vector2
|
|||
|
||||
x_symmetry_point = size.x - 1
|
||||
y_symmetry_point = size.y - 1
|
||||
x_symmetry_axis.type = x_symmetry_axis.Types.HORIZONTAL
|
||||
xy_symmetry_point = Vector2i(size.y, size.x) - Vector2i.ONE
|
||||
x_minus_y_symmetry_point = Vector2(maxi(size.x - size.y, 0), maxi(size.y - size.x, 0))
|
||||
x_symmetry_axis.type = Guide.Types.HORIZONTAL
|
||||
x_symmetry_axis.project = self
|
||||
x_symmetry_axis.add_point(Vector2(-19999, y_symmetry_point / 2 + 0.5))
|
||||
x_symmetry_axis.add_point(Vector2(19999, y_symmetry_point / 2 + 0.5))
|
||||
Global.canvas.add_child(x_symmetry_axis)
|
||||
y_symmetry_axis.type = y_symmetry_axis.Types.VERTICAL
|
||||
|
||||
y_symmetry_axis.type = Guide.Types.VERTICAL
|
||||
y_symmetry_axis.project = self
|
||||
y_symmetry_axis.add_point(Vector2(x_symmetry_point / 2 + 0.5, -19999))
|
||||
y_symmetry_axis.add_point(Vector2(x_symmetry_point / 2 + 0.5, 19999))
|
||||
Global.canvas.add_child(y_symmetry_axis)
|
||||
|
||||
diagonal_xy_symmetry_axis.type = Guide.Types.XY
|
||||
diagonal_xy_symmetry_axis.project = self
|
||||
diagonal_xy_symmetry_axis.add_point(Vector2(19999, -19999))
|
||||
diagonal_xy_symmetry_axis.add_point(Vector2(-19999, 19999) + xy_symmetry_point + Vector2.ONE)
|
||||
Global.canvas.add_child(diagonal_xy_symmetry_axis)
|
||||
|
||||
diagonal_x_minus_y_symmetry_axis.type = Guide.Types.X_MINUS_Y
|
||||
diagonal_x_minus_y_symmetry_axis.project = self
|
||||
diagonal_x_minus_y_symmetry_axis.add_point(Vector2(-19999, -19999))
|
||||
diagonal_x_minus_y_symmetry_axis.add_point(Vector2(19999, 19999) + x_minus_y_symmetry_point)
|
||||
Global.canvas.add_child(diagonal_x_minus_y_symmetry_axis)
|
||||
|
||||
if OS.get_name() == "Web":
|
||||
export_directory_path = "user://"
|
||||
else:
|
||||
|
@ -176,11 +214,26 @@ func new_empty_frame() -> Frame:
|
|||
return frame
|
||||
|
||||
|
||||
## Returns a new [Image] of size [member size] and format [method get_image_format].
|
||||
func new_empty_image() -> Image:
|
||||
return Image.create(size.x, size.y, false, get_image_format())
|
||||
|
||||
|
||||
## Returns the currently selected [BaseCel].
|
||||
func get_current_cel() -> BaseCel:
|
||||
return frames[current_frame].cels[current_layer]
|
||||
|
||||
|
||||
func get_image_format() -> Image.Format:
|
||||
if color_mode == INDEXED_MODE:
|
||||
return Image.FORMAT_RGBA8
|
||||
return color_mode
|
||||
|
||||
|
||||
func is_indexed() -> bool:
|
||||
return color_mode == INDEXED_MODE
|
||||
|
||||
|
||||
func selection_map_changed() -> void:
|
||||
var image_texture: ImageTexture
|
||||
has_selection = !selection_map.is_invisible()
|
||||
|
@ -247,6 +300,9 @@ func serialize() -> Dictionary:
|
|||
var reference_image_data := []
|
||||
for reference_image in reference_images:
|
||||
reference_image_data.append(reference_image.serialize())
|
||||
var tileset_data := []
|
||||
for tileset in tilesets:
|
||||
tileset_data.append(tileset.serialize())
|
||||
|
||||
var metadata := _serialize_metadata(self)
|
||||
|
||||
|
@ -255,6 +311,7 @@ func serialize() -> Dictionary:
|
|||
"pxo_version": ProjectSettings.get_setting("application/config/Pxo_Version"),
|
||||
"size_x": size.x,
|
||||
"size_y": size.y,
|
||||
"color_mode": color_mode,
|
||||
"tile_mode_x_basis_x": tiles.x_basis.x,
|
||||
"tile_mode_x_basis_y": tiles.x_basis.y,
|
||||
"tile_mode_y_basis_x": tiles.y_basis.x,
|
||||
|
@ -266,6 +323,7 @@ func serialize() -> Dictionary:
|
|||
"frames": frame_data,
|
||||
"brushes": brush_data,
|
||||
"reference_images": reference_image_data,
|
||||
"tilesets": tileset_data,
|
||||
"vanishing_points": vanishing_points,
|
||||
"export_file_name": file_name,
|
||||
"export_file_format": file_format,
|
||||
|
@ -288,13 +346,21 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces
|
|||
size.y = dict.size_y
|
||||
tiles.tile_size = size
|
||||
selection_map.crop(size.x, size.y)
|
||||
color_mode = dict.get("color_mode", color_mode)
|
||||
if dict.has("tile_mode_x_basis_x") and dict.has("tile_mode_x_basis_y"):
|
||||
tiles.x_basis.x = dict.tile_mode_x_basis_x
|
||||
tiles.x_basis.y = dict.tile_mode_x_basis_y
|
||||
if dict.has("tile_mode_y_basis_x") and dict.has("tile_mode_y_basis_y"):
|
||||
tiles.y_basis.x = dict.tile_mode_y_basis_x
|
||||
tiles.y_basis.y = dict.tile_mode_y_basis_y
|
||||
if dict.has("tilesets"):
|
||||
for saved_tileset in dict["tilesets"]:
|
||||
var tile_size = str_to_var("Vector2i" + saved_tileset.get("tile_size"))
|
||||
var tileset := TileSetCustom.new(tile_size, "", false)
|
||||
tileset.deserialize(saved_tileset)
|
||||
tilesets.append(tileset)
|
||||
if dict.has("frames") and dict.has("layers"):
|
||||
var audio_layers := 0
|
||||
for saved_layer in dict.layers:
|
||||
match int(saved_layer.get("type", Global.LayerTypes.PIXEL)):
|
||||
Global.LayerTypes.PIXEL:
|
||||
|
@ -303,27 +369,30 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces
|
|||
layers.append(GroupLayer.new(self))
|
||||
Global.LayerTypes.THREE_D:
|
||||
layers.append(Layer3D.new(self))
|
||||
Global.LayerTypes.TILEMAP:
|
||||
layers.append(LayerTileMap.new(self, null))
|
||||
Global.LayerTypes.AUDIO:
|
||||
var layer := AudioLayer.new(self)
|
||||
var audio_path := "audio/%s" % audio_layers
|
||||
if zip_reader.file_exists(audio_path):
|
||||
var audio_data := zip_reader.read_file(audio_path)
|
||||
var stream: AudioStream
|
||||
if saved_layer.get("audio_type", "") == "AudioStreamMP3":
|
||||
stream = AudioStreamMP3.new()
|
||||
stream.data = audio_data
|
||||
layer.audio = stream
|
||||
layers.append(layer)
|
||||
audio_layers += 1
|
||||
|
||||
var frame_i := 0
|
||||
for frame in dict.frames:
|
||||
var cels: Array[BaseCel] = []
|
||||
var cel_i := 0
|
||||
for cel in frame.cels:
|
||||
match int(dict.layers[cel_i].get("type", Global.LayerTypes.PIXEL)):
|
||||
var layer := layers[cel_i]
|
||||
match layer.get_layer_type():
|
||||
Global.LayerTypes.PIXEL:
|
||||
var image := Image.new()
|
||||
if is_instance_valid(zip_reader): # For pxo files saved in 1.0+
|
||||
var image_data := zip_reader.read_file(
|
||||
"image_data/frames/%s/layer_%s" % [frame_i + 1, cel_i + 1]
|
||||
)
|
||||
image = Image.create_from_data(
|
||||
size.x, size.y, false, Image.FORMAT_RGBA8, image_data
|
||||
)
|
||||
elif is_instance_valid(file): # For pxo files saved in 0.x
|
||||
var buffer := file.get_buffer(size.x * size.y * 4)
|
||||
image = Image.create_from_data(
|
||||
size.x, size.y, false, Image.FORMAT_RGBA8, buffer
|
||||
)
|
||||
var image := _load_image_from_pxo(frame_i, cel_i, zip_reader, file)
|
||||
cels.append(PixelCel.new(image))
|
||||
Global.LayerTypes.GROUP:
|
||||
cels.append(GroupCel.new())
|
||||
|
@ -332,6 +401,14 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces
|
|||
# Don't do anything with it, just read it so that the file can move on
|
||||
file.get_buffer(size.x * size.y * 4)
|
||||
cels.append(Cel3D.new(size, true))
|
||||
Global.LayerTypes.TILEMAP:
|
||||
var image := _load_image_from_pxo(frame_i, cel_i, zip_reader, file)
|
||||
var tileset_index = dict.layers[cel_i].tileset_index
|
||||
var tileset := tilesets[tileset_index]
|
||||
var new_cel := CelTileMap.new(tileset, image)
|
||||
cels.append(new_cel)
|
||||
Global.LayerTypes.AUDIO:
|
||||
cels.append(AudioCel.new())
|
||||
cel["pxo_version"] = pxo_version
|
||||
cels[cel_i].deserialize(cel)
|
||||
_deserialize_metadata(cels[cel_i], cel)
|
||||
|
@ -420,6 +497,37 @@ func _deserialize_metadata(object: Object, dict: Dictionary) -> void:
|
|||
object.set_meta(meta, metadata[meta])
|
||||
|
||||
|
||||
## Called by [method deserialize], this method loads an image at
|
||||
## a given [param frame_i] frame index and a [param cel_i] cel index from a pxo file,
|
||||
## and returns it as an [ImageExtended].
|
||||
## If the pxo file is saved with Pixelorama version 1.0 and on,
|
||||
## the [param zip_reader] is used to load the image. Otherwise, [param file] is used.
|
||||
func _load_image_from_pxo(
|
||||
frame_i: int, cel_i: int, zip_reader: ZIPReader, file: FileAccess
|
||||
) -> ImageExtended:
|
||||
var image: Image
|
||||
var indices_data := PackedByteArray()
|
||||
if is_instance_valid(zip_reader): # For pxo files saved in 1.0+
|
||||
var path := "image_data/frames/%s/layer_%s" % [frame_i + 1, cel_i + 1]
|
||||
var image_data := zip_reader.read_file(path)
|
||||
image = Image.create_from_data(size.x, size.y, false, get_image_format(), image_data)
|
||||
var indices_path := "image_data/frames/%s/indices_layer_%s" % [frame_i + 1, cel_i + 1]
|
||||
if zip_reader.file_exists(indices_path):
|
||||
indices_data = zip_reader.read_file(indices_path)
|
||||
elif is_instance_valid(file): # For pxo files saved in 0.x
|
||||
var buffer := file.get_buffer(size.x * size.y * 4)
|
||||
image = Image.create_from_data(size.x, size.y, false, get_image_format(), buffer)
|
||||
var pixelorama_image := ImageExtended.new()
|
||||
pixelorama_image.is_indexed = is_indexed()
|
||||
if not indices_data.is_empty() and is_indexed():
|
||||
pixelorama_image.indices_image = Image.create_from_data(
|
||||
size.x, size.y, false, Image.FORMAT_R8, indices_data
|
||||
)
|
||||
pixelorama_image.copy_from(image)
|
||||
pixelorama_image.select_palette("", true)
|
||||
return pixelorama_image
|
||||
|
||||
|
||||
func _size_changed(value: Vector2i) -> void:
|
||||
if not is_instance_valid(tiles):
|
||||
size = value
|
||||
|
@ -551,14 +659,87 @@ func find_first_drawable_cel(frame := frames[current_frame]) -> BaseCel:
|
|||
var result: BaseCel
|
||||
var cel := frame.cels[0]
|
||||
var i := 0
|
||||
while cel is GroupCel and i < layers.size():
|
||||
while (cel is GroupCel or cel is AudioCel) and i < layers.size():
|
||||
cel = frame.cels[i]
|
||||
i += 1
|
||||
if not cel is GroupCel:
|
||||
if cel is not GroupCel and cel is not AudioCel:
|
||||
result = cel
|
||||
return result
|
||||
|
||||
|
||||
## Returns an [Array] of type [PixelCel] containing all of the pixel cels of the project.
|
||||
func get_all_pixel_cels() -> Array[PixelCel]:
|
||||
var cels: Array[PixelCel]
|
||||
for frame in frames:
|
||||
for cel in frame.cels:
|
||||
if cel is PixelCel:
|
||||
cels.append(cel)
|
||||
return cels
|
||||
|
||||
|
||||
func get_all_audio_layers(only_valid_streams := true) -> Array[AudioLayer]:
|
||||
var audio_layers: Array[AudioLayer]
|
||||
for layer in layers:
|
||||
if layer is AudioLayer:
|
||||
if only_valid_streams:
|
||||
if is_instance_valid(layer.audio):
|
||||
audio_layers.append(layer)
|
||||
else:
|
||||
audio_layers.append(layer)
|
||||
return audio_layers
|
||||
|
||||
|
||||
## Reads data from [param cels] and appends them to [param data],
|
||||
## to be used for the undo/redo system.
|
||||
## It adds data such as the images of [PixelCel]s,
|
||||
## and calls [method CelTileMap.serialize_undo_data] for [CelTileMap]s.
|
||||
func serialize_cel_undo_data(cels: Array[BaseCel], data: Dictionary) -> void:
|
||||
var cels_to_serialize := cels
|
||||
if not TileSetPanel.placing_tiles:
|
||||
cels_to_serialize = find_same_tileset_tilemap_cels(cels)
|
||||
for cel in cels_to_serialize:
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
var image := (cel as PixelCel).get_image()
|
||||
image.add_data_to_dictionary(data)
|
||||
if cel is CelTileMap:
|
||||
data[cel] = (cel as CelTileMap).serialize_undo_data()
|
||||
|
||||
|
||||
## Loads data from [param redo_data] and param [undo_data],
|
||||
## to be used for the undo/redo system.
|
||||
## It calls [method Global.undo_redo_compress_images], and
|
||||
## [method CelTileMap.deserialize_undo_data] for [CelTileMap]s.
|
||||
func deserialize_cel_undo_data(redo_data: Dictionary, undo_data: Dictionary) -> void:
|
||||
Global.undo_redo_compress_images(redo_data, undo_data, self)
|
||||
for cel in redo_data:
|
||||
if cel is CelTileMap:
|
||||
(cel as CelTileMap).deserialize_undo_data(redo_data[cel], undo_redo, false)
|
||||
for cel in undo_data:
|
||||
if cel is CelTileMap:
|
||||
(cel as CelTileMap).deserialize_undo_data(undo_data[cel], undo_redo, true)
|
||||
|
||||
|
||||
## Returns all [BaseCel]s in [param cels], and for every [CelTileMap],
|
||||
## this methods finds all other [CelTileMap]s that share the same [TileSetCustom],
|
||||
## and appends them in the array that is being returned by this method.
|
||||
func find_same_tileset_tilemap_cels(cels: Array[BaseCel]) -> Array[BaseCel]:
|
||||
var tilemap_cels: Array[BaseCel]
|
||||
var current_tilesets: Array[TileSetCustom]
|
||||
for cel in cels:
|
||||
tilemap_cels.append(cel)
|
||||
if cel is not CelTileMap:
|
||||
continue
|
||||
current_tilesets.append((cel as CelTileMap).tileset)
|
||||
for cel in get_all_pixel_cels():
|
||||
if cel is not CelTileMap:
|
||||
continue
|
||||
if (cel as CelTileMap).tileset in current_tilesets:
|
||||
if cel not in cels:
|
||||
tilemap_cels.append(cel)
|
||||
return tilemap_cels
|
||||
|
||||
|
||||
## Re-order layers to take each cel's z-index into account. If all z-indexes are 0,
|
||||
## then the order of drawing is the same as the order of the layers itself.
|
||||
func order_layers(frame_index := current_frame) -> void:
|
||||
|
@ -858,3 +1039,18 @@ func reorder_reference_image(from: int, to: int) -> void:
|
|||
var ri: ReferenceImage = reference_images.pop_at(from)
|
||||
reference_images.insert(to, ri)
|
||||
Global.canvas.reference_image_container.move_child(ri, to)
|
||||
|
||||
|
||||
## Adds a new [param tileset] to [member tilesets].
|
||||
func add_tileset(tileset: TileSetCustom) -> void:
|
||||
tilesets.append(tileset)
|
||||
|
||||
|
||||
## Loops through all cels in [param cel_dictionary], and for [CelTileMap]s,
|
||||
## it calls [method CelTileMap.update_tilemap].
|
||||
func update_tilemaps(
|
||||
cel_dictionary: Dictionary, tile_editing_mode := TileSetPanel.tile_editing_mode
|
||||
) -> void:
|
||||
for cel in cel_dictionary:
|
||||
if cel is CelTileMap:
|
||||
(cel as CelTileMap).update_tilemap(tile_editing_mode)
|
||||
|
|
|
@ -77,6 +77,13 @@ func select_pixel(pixel: Vector2i, select := true) -> void:
|
|||
set_pixelv(pixel, Color(0))
|
||||
|
||||
|
||||
func select_rect(rect: Rect2i, select := true) -> void:
|
||||
if select:
|
||||
fill_rect(rect, Color(1, 1, 1, 1))
|
||||
else:
|
||||
fill_rect(rect, Color(0))
|
||||
|
||||
|
||||
func select_all() -> void:
|
||||
fill(Color(1, 1, 1, 1))
|
||||
|
||||
|
|
|
@ -5,7 +5,9 @@ extends RefCounted
|
|||
signal done
|
||||
|
||||
|
||||
func generate_image(img: Image, shader: Shader, params: Dictionary, size: Vector2i) -> void:
|
||||
func generate_image(
|
||||
img: Image, shader: Shader, params: Dictionary, size: Vector2i, respect_indexed := true
|
||||
) -> void:
|
||||
# duplicate shader before modifying code to avoid affecting original resource
|
||||
var resized_width := false
|
||||
var resized_height := false
|
||||
|
@ -60,4 +62,6 @@ func generate_image(img: Image, shader: Shader, params: Dictionary, size: Vector
|
|||
img.crop(img.get_width() - 1, img.get_height())
|
||||
if resized_height:
|
||||
img.crop(img.get_width(), img.get_height() - 1)
|
||||
if img is ImageExtended and respect_indexed:
|
||||
img.convert_rgb_to_indexed()
|
||||
done.emit()
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
class_name ShaderLoader
|
||||
extends RefCounted
|
||||
|
||||
const VALUE_SLIDER_V2_TSCN := preload("res://src/UI/Nodes/ValueSliderV2.tscn")
|
||||
const BASIS_SLIDERS_TSCN := preload("res://src/UI/Nodes/BasisSliders.tscn")
|
||||
const VALUE_SLIDER_V2_TSCN := preload("res://src/UI/Nodes/Sliders/ValueSliderV2.tscn")
|
||||
const BASIS_SLIDERS_TSCN := preload("res://src/UI/Nodes/Sliders/BasisSliders.tscn")
|
||||
const GRADIENT_EDIT_TSCN := preload("res://src/UI/Nodes/GradientEdit.tscn")
|
||||
const NOISE_GENERATOR := preload("res://src/UI/Nodes/NoiseGeneratorDialog.tscn")
|
||||
|
||||
|
||||
static func create_ui_for_shader_uniforms(
|
||||
|
@ -62,6 +63,8 @@ static func create_ui_for_shader_uniforms(
|
|||
var u_init := u_left_side[0].split(" ")
|
||||
var u_type := u_init[1]
|
||||
var u_name := u_init[2]
|
||||
if u_name in ["PXO_time", "PXO_frame_index", "PXO_layer_index"]:
|
||||
continue
|
||||
# Find custom data of the uniform, if any exists
|
||||
# Right now it only checks if a uniform should have another type of node
|
||||
# Such as integers having OptionButtons
|
||||
|
@ -238,47 +241,34 @@ static func create_ui_for_shader_uniforms(
|
|||
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
var hbox := HBoxContainer.new()
|
||||
hbox.add_child(label)
|
||||
if u_name.begins_with("gradient_"):
|
||||
var gradient_edit := GRADIENT_EDIT_TSCN.instantiate() as GradientEditNode
|
||||
gradient_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
if params.has(u_name) and params[u_name] is GradientTexture2D:
|
||||
gradient_edit.set_gradient_texture(params[u_name])
|
||||
else:
|
||||
params[u_name] = gradient_edit.texture
|
||||
# This needs to be call_deferred because GradientTexture2D gets updated next frame.
|
||||
# Without this, the texture is purple.
|
||||
value_changed.call_deferred(gradient_edit.texture, u_name)
|
||||
gradient_edit.updated.connect(
|
||||
func(_gradient, _cc): value_changed.call(gradient_edit.texture, u_name)
|
||||
if shader is VisualShader and u_name.begins_with("tex_frg_"):
|
||||
var node_id := int(u_name.replace("tex_frg_", ""))
|
||||
var shader_node := (shader as VisualShader).get_node(
|
||||
VisualShader.TYPE_FRAGMENT, node_id
|
||||
)
|
||||
hbox.add_child(gradient_edit)
|
||||
else: ## Simple texture
|
||||
var file_dialog := FileDialog.new()
|
||||
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||
file_dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
file_dialog.size = Vector2(384, 281)
|
||||
file_dialog.file_selected.connect(file_selected.bind(u_name))
|
||||
file_dialog.use_native_dialog = Global.use_native_file_dialogs
|
||||
var button := Button.new()
|
||||
button.text = "Load texture"
|
||||
button.pressed.connect(file_dialog.popup_centered)
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
var mod_button := Button.new()
|
||||
mod_button.text = "Modify"
|
||||
mod_button.pressed.connect(
|
||||
func():
|
||||
_modify_texture_resource(
|
||||
_get_loaded_texture(params, u_name),
|
||||
u_name,
|
||||
_shader_update_texture.bind(value_changed, u_name)
|
||||
if shader_node is VisualShaderNodeTexture:
|
||||
var texture := (shader_node as VisualShaderNodeTexture).texture
|
||||
params[u_name] = texture
|
||||
if texture is GradientTexture1D or texture is GradientTexture2D:
|
||||
_create_gradient_texture_ui(params, u_name, hbox, value_changed)
|
||||
elif texture is CurveTexture:
|
||||
_create_curve_texture_ui(params, u_name, hbox, value_changed)
|
||||
elif texture is NoiseTexture2D:
|
||||
_create_noise_texture_ui(params, u_name, hbox, value_changed, parent_node)
|
||||
else: # Simple texture
|
||||
_create_simple_texture_ui(
|
||||
params, u_name, hbox, value_changed, parent_node, file_selected
|
||||
)
|
||||
elif u_name.begins_with("gradient_"):
|
||||
_create_gradient_texture_ui(params, u_name, hbox, value_changed)
|
||||
elif u_name.begins_with("curve_"):
|
||||
_create_curve_texture_ui(params, u_name, hbox, value_changed)
|
||||
elif u_name.begins_with("noise_"):
|
||||
_create_noise_texture_ui(params, u_name, hbox, value_changed, parent_node)
|
||||
else: # Simple texture
|
||||
_create_simple_texture_ui(
|
||||
params, u_name, hbox, value_changed, parent_node, file_selected
|
||||
)
|
||||
mod_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
mod_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
hbox.add_child(button)
|
||||
hbox.add_child(mod_button)
|
||||
parent_node.add_child(file_dialog)
|
||||
parent_node.add_child(hbox)
|
||||
|
||||
elif u_type == "bool":
|
||||
|
@ -374,6 +364,106 @@ static func _mat3str_to_basis(mat3: String) -> Basis:
|
|||
return basis
|
||||
|
||||
|
||||
static func _create_simple_texture_ui(
|
||||
params: Dictionary,
|
||||
u_name: String,
|
||||
hbox: BoxContainer,
|
||||
value_changed: Callable,
|
||||
parent_node: Control,
|
||||
file_selected: Callable
|
||||
) -> void:
|
||||
var file_dialog := FileDialog.new()
|
||||
file_dialog.always_on_top = true
|
||||
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||
file_dialog.access = FileDialog.ACCESS_FILESYSTEM
|
||||
file_dialog.size = Vector2(384, 281)
|
||||
file_dialog.file_selected.connect(file_selected.bind(u_name))
|
||||
file_dialog.use_native_dialog = Global.use_native_file_dialogs
|
||||
var button := Button.new()
|
||||
button.text = "Load texture"
|
||||
button.pressed.connect(file_dialog.popup_centered)
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
var mod_button := Button.new()
|
||||
mod_button.text = "Modify"
|
||||
mod_button.pressed.connect(
|
||||
func():
|
||||
_modify_texture_resource(
|
||||
_get_loaded_texture(params, u_name),
|
||||
u_name,
|
||||
_shader_update_texture.bind(value_changed, u_name)
|
||||
)
|
||||
)
|
||||
mod_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
mod_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
hbox.add_child(button)
|
||||
hbox.add_child(mod_button)
|
||||
parent_node.add_child(file_dialog)
|
||||
|
||||
|
||||
static func _create_gradient_texture_ui(
|
||||
params: Dictionary, u_name: String, hbox: BoxContainer, value_changed: Callable
|
||||
) -> void:
|
||||
var gradient_edit := GRADIENT_EDIT_TSCN.instantiate() as GradientEditNode
|
||||
gradient_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
if params.has(u_name):
|
||||
var texture = params[u_name]
|
||||
if texture is GradientTexture2D:
|
||||
gradient_edit.set_gradient_texture(texture)
|
||||
elif texture is GradientTexture1D:
|
||||
gradient_edit.set_gradient_texture_1d(texture)
|
||||
else:
|
||||
params[u_name] = gradient_edit.texture
|
||||
# This needs to be call_deferred because GradientTexture2D gets updated next frame.
|
||||
# Without this, the texture is purple.
|
||||
value_changed.call_deferred(gradient_edit.texture, u_name)
|
||||
gradient_edit.updated.connect(
|
||||
func(_gradient, _cc): value_changed.call(gradient_edit.texture, u_name)
|
||||
)
|
||||
hbox.add_child(gradient_edit)
|
||||
|
||||
|
||||
static func _create_curve_texture_ui(
|
||||
params: Dictionary, u_name: String, hbox: BoxContainer, value_changed: Callable
|
||||
) -> void:
|
||||
var curve_edit := CurveEdit.new()
|
||||
curve_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
if params.has(u_name) and params[u_name] is CurveTexture:
|
||||
curve_edit.curve = params[u_name].curve
|
||||
else:
|
||||
curve_edit.set_default_curve()
|
||||
params[u_name] = CurveEdit.to_texture(curve_edit.curve)
|
||||
curve_edit.value_changed.connect(
|
||||
func(curve: Curve): value_changed.call(CurveEdit.to_texture(curve), u_name)
|
||||
)
|
||||
hbox.add_child(curve_edit)
|
||||
|
||||
|
||||
static func _create_noise_texture_ui(
|
||||
params: Dictionary,
|
||||
u_name: String,
|
||||
hbox: BoxContainer,
|
||||
value_changed: Callable,
|
||||
parent_node: Control
|
||||
) -> void:
|
||||
var noise_generator_dialog := NOISE_GENERATOR.instantiate() as AcceptDialog
|
||||
var noise_generator := noise_generator_dialog.get_child(0) as NoiseGenerator
|
||||
if params.has(u_name) and params[u_name] is NoiseTexture2D:
|
||||
noise_generator.noise_texture = params[u_name]
|
||||
else:
|
||||
params[u_name] = noise_generator.noise_texture
|
||||
noise_generator.value_changed.connect(
|
||||
func(noise_texture: NoiseTexture2D): value_changed.call(noise_texture, u_name)
|
||||
)
|
||||
parent_node.add_child(noise_generator_dialog)
|
||||
var button := Button.new()
|
||||
button.text = "Generate noise"
|
||||
button.pressed.connect(noise_generator_dialog.popup_centered)
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
hbox.add_child(button)
|
||||
|
||||
|
||||
static func _shader_change_palette(value_changed: Callable, parameter_name: String) -> void:
|
||||
var palette := Palettes.current_palette
|
||||
_shader_update_palette_texture(palette, value_changed, parameter_name)
|
||||
|
|
186
src/Classes/TileSetCustom.gd
Normal file
|
@ -0,0 +1,186 @@
|
|||
class_name TileSetCustom
|
||||
extends RefCounted
|
||||
|
||||
## A Tileset is a collection of tiles, used by [LayerTileMap]s and [CelTileMap]s.
|
||||
## The tileset contains its [member name], the size of each individual tile,
|
||||
## and the collection of [TileSetCustom.Tile]s itself.
|
||||
## Not to be confused with [TileSet], which is a Godot class.
|
||||
|
||||
## Emitted every time the tileset changes, such as when a tile is added, removed or replaced.
|
||||
## The [CelTileMap] that the changes are coming from is referenced in the [param cel] parameter.
|
||||
signal updated(cel: CelTileMap, replace_index: int)
|
||||
|
||||
## The tileset's name.
|
||||
var name := ""
|
||||
## The size of each individual tile.
|
||||
var tile_size: Vector2i
|
||||
## The collection of tiles in the form of an [Array] of type [TileSetCustom.Tile].
|
||||
var tiles: Array[Tile] = []
|
||||
## If [code]true[/code], the code in [method clear_tileset] does not execute.
|
||||
## This variable is used to prevent multiple cels from clearing the tileset at the same time.
|
||||
## In [method clear_tileset], the variable is set to [code]true[/code], and then
|
||||
## immediately set to [code]false[/code] in the next frame using [method Object.set_deferred].
|
||||
var _tileset_has_been_cleared := false
|
||||
|
||||
|
||||
## An internal class of [TileSetCustom], which contains data used by individual tiles of a tileset.
|
||||
class Tile:
|
||||
## The [Image] tile itself.
|
||||
var image: Image
|
||||
## The amount of tiles this tile is being used in tilemaps.
|
||||
var times_used := 1
|
||||
|
||||
func _init(_image: Image) -> void:
|
||||
image = _image
|
||||
|
||||
## A method that checks if the tile should be removed from the tileset.
|
||||
## Returns [code]true[/code] if the amount of [member times_used] is 0.
|
||||
func can_be_removed() -> bool:
|
||||
return times_used <= 0
|
||||
|
||||
|
||||
func _init(_tile_size: Vector2i, _name := "", add_empty_tile := true) -> void:
|
||||
tile_size = _tile_size
|
||||
name = _name
|
||||
if add_empty_tile:
|
||||
var empty_image := Image.create_empty(tile_size.x, tile_size.y, false, Image.FORMAT_RGBA8)
|
||||
tiles.append(Tile.new(empty_image))
|
||||
|
||||
|
||||
## Adds a new [param image] as a tile to the tileset.
|
||||
## The [param cel] parameter references the [CelTileMap] that this change is coming from,
|
||||
## and the [param edit_mode] parameter contains the tile editing mode at the time of this change.
|
||||
func add_tile(image: Image, cel: CelTileMap) -> void:
|
||||
var tile := Tile.new(image)
|
||||
tiles.append(tile)
|
||||
updated.emit(cel, -1)
|
||||
|
||||
|
||||
## Adds a new [param image] as a tile in a given [param position] in the tileset.
|
||||
## The [param cel] parameter references the [CelTileMap] that this change is coming from,
|
||||
## and the [param edit_mode] parameter contains the tile editing mode at the time of this change.
|
||||
func insert_tile(image: Image, position: int, cel: CelTileMap) -> void:
|
||||
var tile := Tile.new(image)
|
||||
tiles.insert(position, tile)
|
||||
updated.emit(cel, -1)
|
||||
|
||||
|
||||
## Reduces a tile's [member TileSetCustom.Tile.times_used] by one,
|
||||
## in a given [param index] in the tileset.
|
||||
## If the times that tile is used reaches 0 and it can be removed,
|
||||
## it is being removed from the tileset by calling [method remove_tile_at_index].
|
||||
## Returns [code]true[/code] if the tile has been removed.
|
||||
## The [param cel] parameter references the [CelTileMap] that this change is coming from.
|
||||
func unuse_tile_at_index(index: int, cel: CelTileMap) -> bool:
|
||||
tiles[index].times_used -= 1
|
||||
if tiles[index].can_be_removed():
|
||||
remove_tile_at_index(index, cel)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
## Removes a tile in a given [param index] from the tileset.
|
||||
## The [param cel] parameter references the [CelTileMap] that this change is coming from.
|
||||
func remove_tile_at_index(index: int, cel: CelTileMap) -> void:
|
||||
tiles.remove_at(index)
|
||||
updated.emit(cel, -1)
|
||||
|
||||
|
||||
## Replaces a tile in a given [param index] in the tileset with a [param new_tile].
|
||||
## The [param cel] parameter references the [CelTileMap] that this change is coming from.
|
||||
func replace_tile_at(new_tile: Image, index: int, cel: CelTileMap) -> void:
|
||||
tiles[index].image.copy_from(new_tile)
|
||||
updated.emit(cel, index)
|
||||
|
||||
|
||||
## Finds and returns the position of a tile [param image] inside the tileset.
|
||||
func find_tile(image: Image) -> int:
|
||||
for i in tiles.size():
|
||||
var tile := tiles[i]
|
||||
if image.get_data() == tile.image.get_data():
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
## Loops through the array of tiles, and automatically removes any tile that can be removed.
|
||||
## Returns [code]true[/code] if at least one tile has been removed.
|
||||
## The [param cel] parameter references the [CelTileMap] that this change is coming from.
|
||||
func remove_unused_tiles(cel: CelTileMap) -> bool:
|
||||
var tile_removed := false
|
||||
for i in range(tiles.size() - 1, 0, -1):
|
||||
var tile := tiles[i]
|
||||
if tile.can_be_removed():
|
||||
remove_tile_at_index(i, cel)
|
||||
tile_removed = true
|
||||
return tile_removed
|
||||
|
||||
|
||||
## Clears the tileset. Usually called when the project gets resized,
|
||||
## and tilemap cels are updating their size and clearing the tileset to re-create it.
|
||||
func clear_tileset(cel: CelTileMap) -> void:
|
||||
if _tileset_has_been_cleared:
|
||||
return
|
||||
tiles.clear()
|
||||
var empty_image := Image.create_empty(tile_size.x, tile_size.y, false, Image.FORMAT_RGBA8)
|
||||
tiles.append(Tile.new(empty_image))
|
||||
updated.emit(cel, -1)
|
||||
_tileset_has_been_cleared = true
|
||||
set_deferred("_tileset_has_been_cleared", false)
|
||||
|
||||
|
||||
## Returns the tilemap's info, such as its name and tile size and with a given
|
||||
## [param tile_index], in the form of text.
|
||||
func get_text_info(tile_index: int) -> String:
|
||||
var item_string := " %s (%s×%s)" % [tile_index, tile_size.x, tile_size.y]
|
||||
if not name.is_empty():
|
||||
item_string += ": " + name
|
||||
return tr("Tileset") + item_string
|
||||
|
||||
|
||||
## Finds and returns all of the [LayerTileMap]s that use this tileset.
|
||||
func find_using_layers(project: Project) -> Array[LayerTileMap]:
|
||||
var tilemaps: Array[LayerTileMap]
|
||||
for layer in project.layers:
|
||||
if layer is not LayerTileMap:
|
||||
continue
|
||||
if layer.tileset == self:
|
||||
tilemaps.append(layer)
|
||||
return tilemaps
|
||||
|
||||
|
||||
## Serializes the data of this class into the form of a [Dictionary],
|
||||
## which is used so the data can be stored in pxo files.
|
||||
func serialize() -> Dictionary:
|
||||
return {"name": name, "tile_size": tile_size, "tile_amount": tiles.size()}
|
||||
|
||||
|
||||
## Deserializes the data of a given [member dict] [Dictionary] into class data,
|
||||
## which is used so data can be loaded from pxo files.
|
||||
func deserialize(dict: Dictionary) -> void:
|
||||
name = dict.get("name", name)
|
||||
tile_size = str_to_var("Vector2i" + dict.get("tile_size"))
|
||||
|
||||
|
||||
## Serializes the data of each tile in [member tiles] into the form of a [Dictionary],
|
||||
## which is used by the undo/redo system.
|
||||
func serialize_undo_data() -> Dictionary:
|
||||
var dict := {}
|
||||
for tile in tiles:
|
||||
var image_data := tile.image.get_data()
|
||||
dict[tile.image] = [image_data.compress(), image_data.size(), tile.times_used]
|
||||
return dict
|
||||
|
||||
|
||||
## Deserializes the data of each tile in [param dict], which is used by the undo/redo system.
|
||||
func deserialize_undo_data(dict: Dictionary, cel: CelTileMap) -> void:
|
||||
tiles.resize(dict.size())
|
||||
var i := 0
|
||||
for image: Image in dict:
|
||||
var tile_data = dict[image]
|
||||
var buffer_size := tile_data[1] as int
|
||||
var image_data := (tile_data[0] as PackedByteArray).decompress(buffer_size)
|
||||
image.set_data(tile_size.x, tile_size.y, false, image.get_format(), image_data)
|
||||
tiles[i] = Tile.new(image)
|
||||
tiles[i].times_used = tile_data[2]
|
||||
i += 1
|
||||
updated.emit(cel, -1)
|
21
src/Main.gd
|
@ -5,6 +5,7 @@ const SPLASH_DIALOG_SCENE_PATH := "res://src/UI/Dialogs/SplashDialog.tscn"
|
|||
var opensprite_file_selected := false
|
||||
var redone := false
|
||||
var is_quitting_on_save := false
|
||||
var is_writing_text := false
|
||||
var changed_projects_on_quit: Array[Project]
|
||||
var cursor_image := preload("res://assets/graphics/cursor.png")
|
||||
## Used to download an image when dragged and dropped directly from a browser into Pixelorama
|
||||
|
@ -104,7 +105,10 @@ some useful [SYSTEM OPTIONS] are:
|
|||
|
||||
static func set_output(project: Project, next_arg: String) -> void:
|
||||
if not next_arg.is_empty():
|
||||
project.file_name = next_arg.get_basename()
|
||||
project.file_name = next_arg.get_file().get_basename()
|
||||
var directory_path = next_arg.get_base_dir()
|
||||
if directory_path != ".":
|
||||
project.export_directory_path = directory_path
|
||||
var extension := next_arg.get_extension()
|
||||
project.file_format = Export.get_file_format_from_extension(extension)
|
||||
|
||||
|
@ -180,13 +184,6 @@ func _ready() -> void:
|
|||
Import.import_patterns(Global.path_join_array(Global.data_directories, "Patterns"))
|
||||
|
||||
quit_and_save_dialog.add_button("Exit without saving", false, "ExitWithoutSaving")
|
||||
|
||||
open_sprite_dialog.current_dir = Global.config_cache.get_value(
|
||||
"data", "current_dir", OS.get_system_dir(OS.SYSTEM_DIR_DESKTOP)
|
||||
)
|
||||
save_sprite_dialog.current_dir = Global.config_cache.get_value(
|
||||
"data", "current_dir", OS.get_system_dir(OS.SYSTEM_DIR_DESKTOP)
|
||||
)
|
||||
_handle_cmdline_arguments()
|
||||
get_tree().root.files_dropped.connect(_on_files_dropped)
|
||||
if OS.get_name() == "Android":
|
||||
|
@ -199,7 +196,7 @@ func _ready() -> void:
|
|||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and is_instance_valid(Global.main_viewport):
|
||||
if is_writing_text and event is InputEventKey and is_instance_valid(Global.main_viewport):
|
||||
Global.main_viewport.get_child(0).push_input(event)
|
||||
left_cursor.position = get_global_mouse_position() + Vector2(-32, 32)
|
||||
right_cursor.position = get_global_mouse_position() + Vector2(32, 32)
|
||||
|
@ -211,8 +208,8 @@ func _input(event: InputEvent) -> void:
|
|||
|
||||
func _project_switched() -> void:
|
||||
if Global.current_project.export_directory_path != "":
|
||||
open_sprite_dialog.current_path = Global.current_project.export_directory_path
|
||||
save_sprite_dialog.current_path = Global.current_project.export_directory_path
|
||||
open_sprite_dialog.current_dir = Global.current_project.export_directory_path
|
||||
save_sprite_dialog.current_dir = Global.current_project.export_directory_path
|
||||
|
||||
|
||||
# Taken from https://github.com/godotengine/godot/blob/3.x/editor/editor_settings.cpp#L1474
|
||||
|
@ -423,6 +420,7 @@ func load_last_project() -> void:
|
|||
# Check if file still exists on disk
|
||||
var file_path = Global.config_cache.get_value("data", "last_project_path")
|
||||
load_recent_project_file(file_path)
|
||||
(func(): Global.cel_switched.emit()).call_deferred()
|
||||
|
||||
|
||||
func load_recent_project_file(path: String) -> void:
|
||||
|
@ -601,6 +599,7 @@ func _exit_tree() -> void:
|
|||
Global.config_cache.set_value("window", "size", get_window().size)
|
||||
Global.config_cache.set_value("view_menu", "draw_grid", Global.draw_grid)
|
||||
Global.config_cache.set_value("view_menu", "draw_pixel_grid", Global.draw_pixel_grid)
|
||||
Global.config_cache.set_value("view_menu", "show_pixel_indices", Global.show_pixel_indices)
|
||||
Global.config_cache.set_value("view_menu", "show_rulers", Global.show_rulers)
|
||||
Global.config_cache.set_value("view_menu", "show_guides", Global.show_guides)
|
||||
Global.config_cache.set_value("view_menu", "show_mouse_guides", Global.show_mouse_guides)
|
||||
|
|
|
@ -25,7 +25,7 @@ var colors_max := 0
|
|||
|
||||
|
||||
class PaletteColor:
|
||||
var color := Color.TRANSPARENT
|
||||
var color := Color(0, 0, 0, 0)
|
||||
var index := -1
|
||||
|
||||
func _init(init_color := Color.BLACK, init_index := -1) -> void:
|
||||
|
@ -358,9 +358,11 @@ static func strip_unvalid_characters(string_to_strip: String) -> String:
|
|||
return regex.sub(string_to_strip, "", true)
|
||||
|
||||
|
||||
func convert_to_image() -> Image:
|
||||
func convert_to_image(crop_image := true) -> Image:
|
||||
var image := Image.create(colors_max, 1, false, Image.FORMAT_RGBA8)
|
||||
for i in colors_max:
|
||||
if colors.has(i):
|
||||
image.set_pixel(i, 0, colors[i].color)
|
||||
image.set_pixel(i, 0, Color(colors[i].color.to_html()))
|
||||
if crop_image:
|
||||
image.copy_from(image.get_region(image.get_used_rect()))
|
||||
return image
|
||||
|
|
|
@ -23,10 +23,6 @@ func _ready() -> void:
|
|||
|
||||
|
||||
func set_palette(new_palette: Palette) -> void:
|
||||
# Only display valid palette objects
|
||||
if not new_palette:
|
||||
return
|
||||
|
||||
current_palette = new_palette
|
||||
grid_window_origin = Vector2.ZERO
|
||||
|
||||
|
@ -68,6 +64,7 @@ func draw_palette() -> void:
|
|||
var grid_index := i + grid_size.x * j
|
||||
var index := convert_grid_index_to_palette_index(grid_index)
|
||||
var swatch := swatches[grid_index]
|
||||
swatch.color_index = index
|
||||
swatch.show_left_highlight = Palettes.left_selected_color == index
|
||||
swatch.show_right_highlight = Palettes.right_selected_color == index
|
||||
var color = current_palette.get_color(index)
|
||||
|
@ -86,21 +83,39 @@ func scroll_palette(origin: Vector2i) -> void:
|
|||
|
||||
## Called when the color changes, either the left or the right, determined by [param mouse_button].
|
||||
## If current palette has [param target_color] as a [Color], then select it.
|
||||
func find_and_select_color(target_color: Color, mouse_button: int) -> void:
|
||||
var old_index := Palettes.current_palette_get_selected_color_index(mouse_button)
|
||||
for color_ind in swatches.size():
|
||||
if (
|
||||
target_color.is_equal_approx(swatches[color_ind].color)
|
||||
or target_color.to_html() == swatches[color_ind].color.to_html()
|
||||
):
|
||||
var index := convert_grid_index_to_palette_index(color_ind)
|
||||
select_swatch(mouse_button, index, old_index)
|
||||
match mouse_button:
|
||||
MOUSE_BUTTON_LEFT:
|
||||
Palettes.left_selected_color = index
|
||||
MOUSE_BUTTON_RIGHT:
|
||||
Palettes.right_selected_color = index
|
||||
## This is helpful when we select color indirectly (e.g through colorpicker)
|
||||
func find_and_select_color(color_info: Dictionary, mouse_button: int) -> void:
|
||||
var target_color: Color = color_info.get("color", Color(0, 0, 0, 0))
|
||||
var palette_color_index: int = color_info.get("index", -1)
|
||||
if not is_instance_valid(current_palette):
|
||||
return
|
||||
var selected_index := Palettes.current_palette_get_selected_color_index(mouse_button)
|
||||
if palette_color_index != -1: # If color has a defined index in palette then priortize index
|
||||
if selected_index == palette_color_index: # Index already selected
|
||||
return
|
||||
select_swatch(mouse_button, palette_color_index, selected_index)
|
||||
match mouse_button:
|
||||
MOUSE_BUTTON_LEFT:
|
||||
Palettes.left_selected_color = palette_color_index
|
||||
MOUSE_BUTTON_RIGHT:
|
||||
Palettes.right_selected_color = palette_color_index
|
||||
return
|
||||
else: # If it doesn't then select the first match in the palette
|
||||
if get_swatch_color(selected_index) == target_color: # Color already selected
|
||||
return
|
||||
for color_ind in swatches.size():
|
||||
if (
|
||||
target_color.is_equal_approx(swatches[color_ind].color)
|
||||
or target_color.to_html() == swatches[color_ind].color.to_html()
|
||||
):
|
||||
var index := convert_grid_index_to_palette_index(color_ind)
|
||||
select_swatch(mouse_button, index, selected_index)
|
||||
match mouse_button:
|
||||
MOUSE_BUTTON_LEFT:
|
||||
Palettes.left_selected_color = index
|
||||
MOUSE_BUTTON_RIGHT:
|
||||
Palettes.right_selected_color = index
|
||||
return
|
||||
# Unselect swatches when tools color is changed
|
||||
var swatch_to_unselect := -1
|
||||
if mouse_button == MOUSE_BUTTON_LEFT:
|
||||
|
@ -115,6 +130,8 @@ func find_and_select_color(target_color: Color, mouse_button: int) -> void:
|
|||
|
||||
## Displays a left/right highlight over a swatch
|
||||
func select_swatch(mouse_button: int, palette_index: int, old_palette_index: int) -> void:
|
||||
if not is_instance_valid(current_palette):
|
||||
return
|
||||
var index := convert_palette_index_to_grid_index(palette_index)
|
||||
var old_index := convert_palette_index_to_grid_index(old_palette_index)
|
||||
if index >= 0 and index < swatches.size():
|
||||
|
@ -159,16 +176,17 @@ func convert_palette_index_to_grid_index(palette_index: int) -> int:
|
|||
|
||||
|
||||
func resize_grid(new_rect_size: Vector2) -> void:
|
||||
if not is_instance_valid(current_palette):
|
||||
return
|
||||
var grid_x: int = (
|
||||
new_rect_size.x / (swatch_size.x + get("theme_override_constants/h_separation"))
|
||||
)
|
||||
var grid_y: int = (
|
||||
new_rect_size.y / (swatch_size.y + get("theme_override_constants/v_separation"))
|
||||
)
|
||||
grid_size.x = mini(grid_x, current_palette.width)
|
||||
grid_size.y = mini(grid_y, current_palette.height)
|
||||
if is_instance_valid(current_palette):
|
||||
grid_size.x = mini(grid_x, current_palette.width)
|
||||
grid_size.y = mini(grid_y, current_palette.height)
|
||||
else:
|
||||
grid_size = Vector2i.ZERO
|
||||
setup_swatches()
|
||||
draw_palette()
|
||||
|
||||
|
|
|
@ -89,16 +89,16 @@ func select_palette(palette_name: String) -> void:
|
|||
var palette_id = palettes_path_id.get(palette_name)
|
||||
if palette_id != null:
|
||||
palette_select.selected = palette_id
|
||||
palette_grid.set_palette(Palettes.current_palette)
|
||||
palette_scroll.resize_grid()
|
||||
palette_scroll.set_sliders(Palettes.current_palette, palette_grid.grid_window_origin)
|
||||
palette_grid.set_palette(Palettes.current_palette)
|
||||
palette_scroll.resize_grid()
|
||||
palette_scroll.set_sliders(Palettes.current_palette, palette_grid.grid_window_origin)
|
||||
|
||||
var left_selected := Palettes.current_palette_get_selected_color_index(MOUSE_BUTTON_LEFT)
|
||||
var right_selected := Palettes.current_palette_get_selected_color_index(MOUSE_BUTTON_RIGHT)
|
||||
palette_grid.select_swatch(MOUSE_BUTTON_LEFT, left_selected, left_selected)
|
||||
palette_grid.select_swatch(MOUSE_BUTTON_RIGHT, right_selected, right_selected)
|
||||
var left_selected := Palettes.current_palette_get_selected_color_index(MOUSE_BUTTON_LEFT)
|
||||
var right_selected := Palettes.current_palette_get_selected_color_index(MOUSE_BUTTON_RIGHT)
|
||||
palette_grid.select_swatch(MOUSE_BUTTON_LEFT, left_selected, left_selected)
|
||||
palette_grid.select_swatch(MOUSE_BUTTON_RIGHT, right_selected, right_selected)
|
||||
|
||||
toggle_add_delete_buttons()
|
||||
toggle_add_delete_buttons()
|
||||
|
||||
|
||||
## Select and display current palette
|
||||
|
@ -115,6 +115,8 @@ func redraw_current_palette() -> void:
|
|||
|
||||
|
||||
func toggle_add_delete_buttons() -> void:
|
||||
if not is_instance_valid(Palettes.current_palette):
|
||||
return
|
||||
add_color_button.disabled = Palettes.current_palette.is_full()
|
||||
if add_color_button.disabled:
|
||||
add_color_button.mouse_default_cursor_shape = CURSOR_FORBIDDEN
|
||||
|
@ -252,6 +254,7 @@ func _on_ColorPicker_color_changed(color: Color) -> void:
|
|||
== Palettes.current_palette_get_selected_color_index(MOUSE_BUTTON_RIGHT)
|
||||
):
|
||||
Tools.assign_color(color, MOUSE_BUTTON_RIGHT)
|
||||
Palettes.current_palette_set_color(edited_swatch_index, edited_swatch_color)
|
||||
|
||||
|
||||
## Saves edited swatch to palette file when color selection dialog is closed
|
||||
|
|
|
@ -4,9 +4,9 @@ var scroll := Vector2i.ZERO
|
|||
var drag_started := false
|
||||
var drag_start_position := Vector2i.ZERO
|
||||
|
||||
@onready var h_slider := %HScrollBar
|
||||
@onready var v_slider := %VScrollBar
|
||||
@onready var palette_grid := %PaletteGrid
|
||||
@onready var h_slider := %HScrollBar as HScrollBar
|
||||
@onready var v_slider := %VScrollBar as VScrollBar
|
||||
@onready var palette_grid := %PaletteGrid as PaletteGrid
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
|
@ -17,16 +17,21 @@ func _input(event: InputEvent) -> void:
|
|||
|
||||
|
||||
func set_sliders(palette: Palette, origin: Vector2i) -> void:
|
||||
if not is_instance_valid(palette):
|
||||
return
|
||||
h_slider.value = origin.x
|
||||
h_slider.max_value = palette.width
|
||||
h_slider.page = palette_grid.grid_size.x
|
||||
if is_instance_valid(palette):
|
||||
h_slider.value = origin.x
|
||||
h_slider.max_value = palette.width
|
||||
h_slider.page = palette_grid.grid_size.x
|
||||
v_slider.value = origin.y
|
||||
v_slider.max_value = palette.height
|
||||
v_slider.page = palette_grid.grid_size.y
|
||||
else:
|
||||
h_slider.value = 0
|
||||
h_slider.max_value = 0
|
||||
h_slider.page = 0
|
||||
v_slider.value = 0
|
||||
v_slider.max_value = 0
|
||||
v_slider.page = 0
|
||||
h_slider.visible = false if h_slider.max_value <= palette_grid.grid_size.x else true
|
||||
|
||||
v_slider.value = origin.y
|
||||
v_slider.max_value = palette.height
|
||||
v_slider.page = palette_grid.grid_size.y
|
||||
v_slider.visible = false if v_slider.max_value <= palette_grid.grid_size.y else true
|
||||
|
||||
|
||||
|
@ -58,7 +63,7 @@ func _on_PaletteGrid_gui_input(event: InputEvent) -> void:
|
|||
drag_started = true
|
||||
# Keeps position where the dragging started
|
||||
drag_start_position = (
|
||||
event.position + Vector2i(h_slider.value, v_slider.value) * palette_grid.swatch_size
|
||||
event.position + Vector2(h_slider.value, v_slider.value) * palette_grid.swatch_size
|
||||
)
|
||||
|
||||
if event is InputEventMouseMotion and drag_started:
|
||||
|
|
|
@ -8,6 +8,7 @@ signal dropped(source_index: int, new_index: int)
|
|||
const DEFAULT_COLOR := Color(0.0, 0.0, 0.0, 0.0)
|
||||
|
||||
var index := -1
|
||||
var color_index := -1
|
||||
var show_left_highlight := false
|
||||
var show_right_highlight := false
|
||||
var empty := true:
|
||||
|
@ -48,6 +49,23 @@ func _draw() -> void:
|
|||
draw_rect(
|
||||
Rect2(margin - Vector2.ONE, size - margin * 2 + Vector2(2, 2)), Color.WHITE, false, 1
|
||||
)
|
||||
if Global.show_pixel_indices:
|
||||
var font := Themes.get_font()
|
||||
var str_pos := Vector2(size.x / 2, size.y - 2)
|
||||
var text_color := Global.control.theme.get_color(&"font_color", &"Label")
|
||||
draw_string_outline(
|
||||
font,
|
||||
str_pos,
|
||||
str(color_index),
|
||||
HORIZONTAL_ALIGNMENT_RIGHT,
|
||||
-1,
|
||||
size.x / 2,
|
||||
1,
|
||||
text_color.inverted()
|
||||
)
|
||||
draw_string(
|
||||
font, str_pos, str(color_index), HORIZONTAL_ALIGNMENT_RIGHT, -1, size.x / 2, text_color
|
||||
)
|
||||
|
||||
|
||||
## Enables drawing of highlights which indicate selected swatches
|
||||
|
|
200
src/Preferences/GridPreferences.gd
Normal file
|
@ -0,0 +1,200 @@
|
|||
extends GridContainer
|
||||
|
||||
# We should use pre defined initial grid colors instead of random colors
|
||||
const INITIAL_GRID_COLORS := [
|
||||
Color.BLACK,
|
||||
Color.WHITE,
|
||||
Color.YELLOW,
|
||||
Color.GREEN,
|
||||
Color.BLUE,
|
||||
Color.GRAY,
|
||||
Color.ORANGE,
|
||||
Color.PINK,
|
||||
Color.SIENNA,
|
||||
Color.CORAL,
|
||||
]
|
||||
|
||||
var grid_preferences: Array[GridPreference] = [
|
||||
GridPreference.new("grid_type", "GridType", "selected", Global.GridTypes.CARTESIAN),
|
||||
GridPreference.new("grid_size", "GridSizeValue", "value", Vector2i(2, 2)),
|
||||
GridPreference.new("isometric_grid_size", "IsometricGridSizeValue", "value", Vector2i(16, 8)),
|
||||
GridPreference.new("grid_offset", "GridOffsetValue", "value", Vector2i.ZERO),
|
||||
GridPreference.new("grid_draw_over_tile_mode", "GridDrawOverTileMode", "button_pressed", false),
|
||||
GridPreference.new("grid_color", "GridColor", "color", Color.BLACK),
|
||||
]
|
||||
|
||||
var grid_selected: int = 0:
|
||||
set(key):
|
||||
grid_selected = key
|
||||
for child: BaseButton in grids_select_container.get_children():
|
||||
if child.get_index() == grid_selected:
|
||||
child.self_modulate = Color.WHITE
|
||||
else:
|
||||
child.self_modulate = Color.DIM_GRAY
|
||||
var grids: Dictionary = Global.config_cache.get_value(
|
||||
"preferences", "grids", {0: create_default_properties()}
|
||||
)
|
||||
if grids.has(key):
|
||||
update_pref_ui(grids[key])
|
||||
|
||||
@onready var grids_select_container: HFlowContainer = $GridsSelectContainer
|
||||
|
||||
|
||||
class GridPreference:
|
||||
var prop_name: String
|
||||
var node_path: String
|
||||
var value_type: String
|
||||
var default_value
|
||||
|
||||
func _init(
|
||||
_prop_name: String,
|
||||
_node_path: String,
|
||||
_value_type: String,
|
||||
_default_value = null,
|
||||
_require_restart := false
|
||||
) -> void:
|
||||
prop_name = _prop_name
|
||||
node_path = _node_path
|
||||
value_type = _value_type
|
||||
if _default_value != null:
|
||||
default_value = _default_value
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var grids = Global.config_cache.get_value(
|
||||
"preferences", "grids", {0: create_default_properties()}
|
||||
)
|
||||
Global.config_cache.set_value("preferences", "grids", grids)
|
||||
$GridsCount.value = grids.size()
|
||||
if grids.size() == 1:
|
||||
add_remove_select_button(0)
|
||||
for pref in grid_preferences:
|
||||
if not has_node(pref.node_path):
|
||||
continue
|
||||
var node := get_node(pref.node_path)
|
||||
var restore_default_button := RestoreDefaultButton.new()
|
||||
restore_default_button.pressed.connect(
|
||||
_on_grid_pref_value_changed.bind(pref.default_value, pref, restore_default_button)
|
||||
)
|
||||
restore_default_button.setting_name = pref.prop_name
|
||||
restore_default_button.value_type = pref.value_type
|
||||
restore_default_button.default_value = pref.default_value
|
||||
restore_default_button.node = node
|
||||
|
||||
var node_position := node.get_index()
|
||||
node.get_parent().add_child(restore_default_button)
|
||||
node.get_parent().move_child(restore_default_button, node_position)
|
||||
|
||||
match pref.value_type:
|
||||
"button_pressed":
|
||||
node.toggled.connect(_on_grid_pref_value_changed.bind(pref, restore_default_button))
|
||||
"value":
|
||||
node.value_changed.connect(
|
||||
_on_grid_pref_value_changed.bind(pref, restore_default_button)
|
||||
)
|
||||
"color":
|
||||
node.get_picker().presets_visible = false
|
||||
node.color_changed.connect(
|
||||
_on_grid_pref_value_changed.bind(pref, restore_default_button)
|
||||
)
|
||||
"selected":
|
||||
node.item_selected.connect(
|
||||
_on_grid_pref_value_changed.bind(pref, restore_default_button)
|
||||
)
|
||||
grid_selected = 0
|
||||
|
||||
|
||||
func _on_grid_pref_value_changed(value, pref: GridPreference, button: RestoreDefaultButton) -> void:
|
||||
var grids: Dictionary = Global.config_cache.get_value(
|
||||
"preferences", "grids", {0: create_default_properties()}
|
||||
)
|
||||
if grids.has(grid_selected): # Failsafe (Always true)
|
||||
var grid_info: Dictionary = grids[grid_selected]
|
||||
var prop := pref.prop_name
|
||||
grid_info[prop] = value
|
||||
grids[grid_selected] = grid_info
|
||||
Global.update_grids(grids)
|
||||
var default_value = pref.default_value
|
||||
var disable: bool = Global.grids[grid_selected].get(prop) == default_value
|
||||
if typeof(value) == TYPE_COLOR:
|
||||
disable = value.is_equal_approx(default_value)
|
||||
disable_restore_default_button(button, disable)
|
||||
Global.config_cache.set_value("preferences", "grids", grids)
|
||||
|
||||
|
||||
func _on_grids_count_value_changed(value: float) -> void:
|
||||
var new_grids: Dictionary = Global.config_cache.get_value(
|
||||
"preferences", "grids", {0: create_default_properties()}
|
||||
)
|
||||
var last_grid_idx = int(value - 1)
|
||||
if last_grid_idx >= grids_select_container.get_child_count():
|
||||
# Add missing grids
|
||||
for key in range(grids_select_container.get_child_count(), value):
|
||||
if not new_grids.has(key):
|
||||
var new_grid := create_default_properties()
|
||||
if new_grids.has(key - 1): # Failsafe
|
||||
var last_grid = new_grids[key - 1]
|
||||
# This small bit of code is there to make ui look a little neater
|
||||
# Reasons:
|
||||
# - Usually user intends to make the next grid twice the size.
|
||||
# - Having all grids being same size initially may cause confusion for some
|
||||
# users when they try to change color of a middle grid not seeing it's changing
|
||||
# (due to being covered by grids above it).
|
||||
if (
|
||||
new_grid.has("grid_size")
|
||||
and new_grid.has("isometric_grid_size")
|
||||
and new_grid.has("grid_color")
|
||||
):
|
||||
new_grid["grid_size"] = last_grid["grid_size"] * 2
|
||||
new_grid["isometric_grid_size"] = last_grid["isometric_grid_size"] * 2
|
||||
if key < INITIAL_GRID_COLORS.size():
|
||||
new_grid["grid_color"] = INITIAL_GRID_COLORS[key]
|
||||
new_grids[key] = new_grid
|
||||
add_remove_select_button(key)
|
||||
else:
|
||||
# Remove extra grids
|
||||
for key: int in range(value, new_grids.size()):
|
||||
new_grids.erase(key)
|
||||
add_remove_select_button(key, true)
|
||||
grid_selected = min(grid_selected, last_grid_idx)
|
||||
Global.update_grids(new_grids)
|
||||
Global.config_cache.set_value("preferences", "grids", new_grids)
|
||||
|
||||
|
||||
func create_default_properties() -> Dictionary:
|
||||
var grid_info = {}
|
||||
for pref in grid_preferences:
|
||||
grid_info[pref.prop_name] = pref.default_value
|
||||
return grid_info
|
||||
|
||||
|
||||
func disable_restore_default_button(button: RestoreDefaultButton, disable: bool) -> void:
|
||||
button.disabled = disable
|
||||
if disable:
|
||||
button.mouse_default_cursor_shape = Control.CURSOR_ARROW
|
||||
button.tooltip_text = ""
|
||||
else:
|
||||
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||
button.tooltip_text = "Restore default value"
|
||||
|
||||
|
||||
func add_remove_select_button(grid_idx: int, remove := false):
|
||||
if not remove:
|
||||
var select_button = Button.new()
|
||||
select_button.text = str(grid_idx)
|
||||
grids_select_container.add_child(select_button)
|
||||
select_button.pressed.connect(func(): grid_selected = grid_idx)
|
||||
else:
|
||||
if grid_idx < grids_select_container.get_child_count():
|
||||
grids_select_container.get_child(grid_idx).queue_free()
|
||||
|
||||
|
||||
func update_pref_ui(grid_data: Dictionary):
|
||||
for pref in grid_preferences:
|
||||
var key = pref.prop_name
|
||||
if grid_data.has(key):
|
||||
var node := get_node(pref.node_path)
|
||||
node.set(pref.value_type, grid_data[key])
|
||||
if pref.value_type == "color":
|
||||
# the signal doesn't seem to be emitted automatically
|
||||
node.color_changed.emit(grid_data[key])
|
|
@ -94,21 +94,6 @@ var preferences: Array[Preference] = [
|
|||
Preference.new("smooth_zoom", "Canvas/ZoomOptions/SmoothZoom", "button_pressed", true),
|
||||
Preference.new("integer_zoom", "Canvas/ZoomOptions/IntegerZoom", "button_pressed", false),
|
||||
Preference.new("snapping_distance", "Canvas/SnappingOptions/DistanceValue", "value", 32.0),
|
||||
Preference.new(
|
||||
"grid_type", "Canvas/GridOptions/GridType", "selected", Global.GridTypes.CARTESIAN
|
||||
),
|
||||
Preference.new("grid_size", "Canvas/GridOptions/GridSizeValue", "value", Vector2i(2, 2)),
|
||||
Preference.new(
|
||||
"isometric_grid_size", "Canvas/GridOptions/IsometricGridSizeValue", "value", Vector2i(16, 8)
|
||||
),
|
||||
Preference.new("grid_offset", "Canvas/GridOptions/GridOffsetValue", "value", Vector2i.ZERO),
|
||||
Preference.new(
|
||||
"grid_draw_over_tile_mode",
|
||||
"Canvas/GridOptions/GridDrawOverTileMode",
|
||||
"button_pressed",
|
||||
false
|
||||
),
|
||||
Preference.new("grid_color", "Canvas/GridOptions/GridColor", "color", Color.BLACK),
|
||||
Preference.new(
|
||||
"pixel_grid_show_at_zoom", "Canvas/PixelGridOptions/ShowAtZoom", "value", 1500.0
|
||||
),
|
||||
|
@ -196,6 +181,13 @@ var preferences: Array[Preference] = [
|
|||
false,
|
||||
true
|
||||
),
|
||||
Preference.new(
|
||||
"dummy_audio_driver",
|
||||
"Performance/PerformanceContainer/DummyAudioDriver",
|
||||
"button_pressed",
|
||||
false,
|
||||
true
|
||||
),
|
||||
Preference.new("tablet_driver", "Drivers/DriversContainer/TabletDriver", "selected", 0)
|
||||
]
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
[gd_scene load_steps=9 format=3 uid="uid://b3hkjj3s6pe4x"]
|
||||
[gd_scene load_steps=11 format=3 uid="uid://b3hkjj3s6pe4x"]
|
||||
|
||||
[ext_resource type="Script" path="res://src/Preferences/PreferencesDialog.gd" id="1"]
|
||||
[ext_resource type="PackedScene" uid="uid://bq7ibhm0txl5p" path="res://addons/keychain/ShortcutEdit.tscn" id="3"]
|
||||
[ext_resource type="Script" path="res://src/Preferences/ThemesPreferences.gd" id="3_nvl8k"]
|
||||
[ext_resource type="PackedScene" path="res://src/UI/Nodes/ValueSliderV2.tscn" id="7"]
|
||||
[ext_resource type="Script" path="res://src/Preferences/GridPreferences.gd" id="4_76iff"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/Sliders/ValueSlider.tscn" id="5_rlmsh"]
|
||||
[ext_resource type="PackedScene" path="res://src/UI/Nodes/Sliders/ValueSliderV2.tscn" id="7"]
|
||||
[ext_resource type="Script" path="res://src/Preferences/ExtensionsPreferences.gd" id="7_8ume5"]
|
||||
[ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="8"]
|
||||
[ext_resource type="Script" path="res://src/UI/Nodes/Sliders/ValueSlider.gd" id="8"]
|
||||
[ext_resource type="PackedScene" uid="uid://chy5d42l72crk" path="res://src/UI/ExtensionExplorer/Store.tscn" id="8_jmnx8"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_8vsfb"]
|
||||
|
@ -482,6 +484,30 @@ layout_mode = 2
|
|||
theme_override_constants/h_separation = 4
|
||||
theme_override_constants/v_separation = 4
|
||||
columns = 3
|
||||
script = ExtResource("4_76iff")
|
||||
|
||||
[node name="GridsCountLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Canvas/GridOptions"]
|
||||
layout_mode = 2
|
||||
text = "Grids Visible:"
|
||||
|
||||
[node name="Spacer" type="Control" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Canvas/GridOptions"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="GridsCount" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Canvas/GridOptions" instance=ExtResource("5_rlmsh")]
|
||||
layout_mode = 2
|
||||
min_value = 1.0
|
||||
max_value = 10.0
|
||||
value = 1.0
|
||||
|
||||
[node name="GridsSelectLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Canvas/GridOptions"]
|
||||
layout_mode = 2
|
||||
text = "Editing Grid:"
|
||||
|
||||
[node name="Spacer2" type="Control" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Canvas/GridOptions"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="GridsSelectContainer" type="HFlowContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Canvas/GridOptions"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="GridTypeLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Canvas/GridOptions"]
|
||||
layout_mode = 2
|
||||
|
@ -1116,18 +1142,28 @@ mouse_default_cursor_shape = 2
|
|||
button_pressed = true
|
||||
text = "On"
|
||||
|
||||
[node name="WindowTransparencyLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer"]
|
||||
[node name="WindowTransparencyLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer" groups=["DesktopOnly"]]
|
||||
layout_mode = 2
|
||||
tooltip_text = "If enabled, the application window can become transparent. This affects performance, so keep it off if you don't need it."
|
||||
mouse_filter = 0
|
||||
text = "Enable window transparency"
|
||||
|
||||
[node name="WindowTransparency" type="CheckBox" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer"]
|
||||
[node name="WindowTransparency" type="CheckBox" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer" groups=["DesktopOnly"]]
|
||||
layout_mode = 2
|
||||
tooltip_text = "If enabled, the application window can become transparent. This affects performance, so keep it off if you don't need it."
|
||||
mouse_default_cursor_shape = 2
|
||||
text = "On"
|
||||
|
||||
[node name="DummyAudioDriverLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer" groups=["DesktopOnly"]]
|
||||
layout_mode = 2
|
||||
mouse_filter = 0
|
||||
text = "Use dummy audio driver"
|
||||
|
||||
[node name="DummyAudioDriver" type="CheckBox" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer" groups=["DesktopOnly"]]
|
||||
layout_mode = 2
|
||||
mouse_default_cursor_shape = 2
|
||||
text = "On"
|
||||
|
||||
[node name="Drivers" type="VBoxContainer" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
|
@ -1447,6 +1483,7 @@ text = "Pixelorama must be restarted for changes to take effect."
|
|||
mode = 1
|
||||
title = "Open File(s)"
|
||||
size = Vector2i(560, 400)
|
||||
always_on_top = true
|
||||
ok_button_text = "Open"
|
||||
file_mode = 1
|
||||
access = 2
|
||||
|
@ -1458,6 +1495,7 @@ transient = true
|
|||
|
||||
[node name="EnableExtensionConfirmation" type="ConfirmationDialog" parent="."]
|
||||
unique_name_in_owner = true
|
||||
always_on_top = true
|
||||
dialog_text = "Are you sure you want to enable this extension? Make sure to only enable extensions from sources that you trust."
|
||||
dialog_autowrap = true
|
||||
|
||||
|
@ -1465,11 +1503,13 @@ dialog_autowrap = true
|
|||
unique_name_in_owner = true
|
||||
position = Vector2i(0, 36)
|
||||
size = Vector2i(400, 100)
|
||||
always_on_top = true
|
||||
ok_button_text = "Delete Permanently"
|
||||
dialog_text = "Are you sure you want to delete this extension?"
|
||||
dialog_autowrap = true
|
||||
|
||||
[node name="ResetOptionsConfirmation" type="ConfirmationDialog" parent="."]
|
||||
always_on_top = true
|
||||
dialog_text = "Are you sure you want to reset the selected options? There will be no way to undo this."
|
||||
|
||||
[connection signal="about_to_popup" from="." to="." method="_on_PreferencesDialog_about_to_show"]
|
||||
|
@ -1478,6 +1518,7 @@ dialog_text = "Are you sure you want to reset the selected options? There will b
|
|||
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Language/System Language" to="." method="_on_language_pressed" binds= [1]]
|
||||
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface/InterfaceOptions/ShrinkContainer/ShrinkApplyButton" to="." method="_on_shrink_apply_button_pressed"]
|
||||
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Interface/InterfaceOptions/FontSizeContainer/FontSizeApplyButton" to="." method="_on_font_size_apply_button_pressed"]
|
||||
[connection signal="value_changed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Canvas/GridOptions/GridsCount" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Canvas/GridOptions" method="_on_grids_count_value_changed"]
|
||||
[connection signal="pressed" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/ExtensionsHeader/Explore" to="Store" method="_on_explore_pressed"]
|
||||
[connection signal="empty_clicked" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/InstalledExtensions" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions" method="_on_InstalledExtensions_empty_clicked"]
|
||||
[connection signal="item_selected" from="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions/InstalledExtensions" to="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Extensions" method="_on_InstalledExtensions_item_selected"]
|
||||
|
|
67
src/Shaders/Effects/ColorCurves.gdshader
Normal file
|
@ -0,0 +1,67 @@
|
|||
shader_type canvas_item;
|
||||
|
||||
// CurveTexture(s)
|
||||
uniform sampler2D curve_rgb;
|
||||
uniform sampler2D curve_red;
|
||||
uniform sampler2D curve_green;
|
||||
uniform sampler2D curve_blue;
|
||||
uniform sampler2D curve_alpha;
|
||||
uniform sampler2D curve_hue;
|
||||
uniform sampler2D curve_sat;
|
||||
uniform sampler2D curve_value;
|
||||
uniform sampler2D selection : filter_nearest;
|
||||
|
||||
|
||||
vec3 rgb2hsb(vec3 c) {
|
||||
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
||||
vec4 p = mix(vec4(c.bg, K.wz),
|
||||
vec4(c.gb, K.xy),
|
||||
step(c.b, c.g));
|
||||
vec4 q = mix(vec4(p.xyw, c.r),
|
||||
vec4(c.r, p.yzx),
|
||||
step(p.x, c.r));
|
||||
float d = q.x - min(q.w, q.y);
|
||||
float e = 1.0e-10;
|
||||
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)),
|
||||
d / (q.x + e),
|
||||
q.x);
|
||||
}
|
||||
|
||||
vec3 hsb2rgb(vec3 c)
|
||||
{
|
||||
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
|
||||
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
|
||||
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
|
||||
}
|
||||
|
||||
|
||||
void fragment() {
|
||||
vec4 original_color = texture(TEXTURE, UV);
|
||||
vec4 selection_color = texture(selection, UV);
|
||||
vec4 col;
|
||||
float red_curve_color = texture(curve_red, vec2(COLOR.r, 0.0)).r;
|
||||
float green_curve_color = texture(curve_green, vec2(COLOR.g, 0.0)).r;
|
||||
float blue_curve_color = texture(curve_blue, vec2(COLOR.b, 0.0)).r;
|
||||
float alpha_curve_color = texture(curve_alpha, vec2(COLOR.a, 0.0)).r;
|
||||
col.r = red_curve_color;
|
||||
col.g = green_curve_color;
|
||||
col.b = blue_curve_color;
|
||||
col.a = alpha_curve_color;
|
||||
|
||||
vec3 hsb = rgb2hsb(col.rgb);
|
||||
float hue_curve_color = texture(curve_hue, vec2(hsb.r, 0.0)).r;
|
||||
float sat_curve_color = texture(curve_sat, vec2(hsb.g, 0.0)).r;
|
||||
float value_curve_color = texture(curve_value, vec2(hsb.b, 0.0)).r;
|
||||
hsb.r = hue_curve_color;
|
||||
hsb.g = sat_curve_color;
|
||||
hsb.b = value_curve_color;
|
||||
|
||||
col.rgb = hsb2rgb(hsb);
|
||||
|
||||
float rgb_curve_color_r = texture(curve_rgb, vec2(col.r, 0.0)).r;
|
||||
float rgb_curve_color_g = texture(curve_rgb, vec2(col.g, 0.0)).r;
|
||||
float rgb_curve_color_b = texture(curve_rgb, vec2(col.b, 0.0)).r;
|
||||
col.rgb = vec3(rgb_curve_color_r, rgb_curve_color_g, rgb_curve_color_b);
|
||||
vec4 output = mix(original_color.rgba, col, selection_color.a);
|
||||
COLOR = output;
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
shader_type canvas_item;
|
||||
render_mode unshaded;
|
||||
|
||||
#include "res://src/Shaders/FindPaletteColorIndex.gdshaderinc"
|
||||
uniform sampler2D palette_texture : filter_nearest;
|
||||
uniform sampler2D selection : filter_nearest;
|
||||
|
||||
|
@ -10,19 +11,10 @@ vec4 swap_color(vec4 color) {
|
|||
if (color.a <= 0.01) {
|
||||
return color;
|
||||
}
|
||||
int color_index = 0;
|
||||
|
||||
int n_of_colors = textureSize(palette_texture, 0).x;
|
||||
float smaller_distance = distance(color, texture(palette_texture, vec2(0.0)));
|
||||
for (int i = 0; i <= n_of_colors; i++) {
|
||||
vec2 uv = vec2(float(i) / float(n_of_colors), 0.0);
|
||||
vec4 palette_color = texture(palette_texture, uv);
|
||||
float dist = distance(color, palette_color);
|
||||
if (dist < smaller_distance) {
|
||||
smaller_distance = dist;
|
||||
color_index = i;
|
||||
}
|
||||
}
|
||||
return texture(palette_texture, vec2(float(color_index) / float(n_of_colors), 0.0));
|
||||
int color_index = find_index(color, n_of_colors, palette_texture);
|
||||
return texelFetch(palette_texture, ivec2(color_index, 0), 0);
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
|
|
13
src/Shaders/FindPaletteColorIndex.gdshaderinc
Normal file
|
@ -0,0 +1,13 @@
|
|||
int find_index(vec4 color, int n_of_colors, sampler2D palette_texture) {
|
||||
int color_index = 0;
|
||||
float smaller_distance = distance(color, texture(palette_texture, vec2(0.0)));
|
||||
for (int i = 0; i <= n_of_colors; i++) {
|
||||
vec4 palette_color = texelFetch(palette_texture, ivec2(i, 0), 0);
|
||||
float dist = distance(color, palette_color);
|
||||
if (dist < smaller_distance) {
|
||||
smaller_distance = dist;
|
||||
color_index = i;
|
||||
}
|
||||
}
|
||||
return color_index;
|
||||
}
|
28
src/Shaders/IndexedToRGB.gdshader
Normal file
|
@ -0,0 +1,28 @@
|
|||
shader_type canvas_item;
|
||||
render_mode unshaded;
|
||||
|
||||
const float EPSILON = 0.0001;
|
||||
|
||||
uniform sampler2D palette_texture : filter_nearest;
|
||||
uniform sampler2D indices_texture : filter_nearest;
|
||||
|
||||
void fragment() {
|
||||
float index = texture(indices_texture, UV).r * 255.0;
|
||||
if (index <= EPSILON) { // If index is zero, make it transparent
|
||||
COLOR = vec4(0.0);
|
||||
}
|
||||
else {
|
||||
float n_of_colors = float(textureSize(palette_texture, 0).x);
|
||||
index -= 1.0;
|
||||
float index_normalized = index / n_of_colors;
|
||||
if (index < n_of_colors) {
|
||||
COLOR = texelFetch(palette_texture, ivec2(int(index), 0), 0);
|
||||
}
|
||||
else {
|
||||
// If index is bigger than the size of the palette, make it transparent.
|
||||
// This happens when switching to a palette, where the previous palette was bigger
|
||||
// than the newer one, and the current index is out of bounds of the new one.
|
||||
COLOR = vec4(0.0);
|
||||
}
|
||||
}
|
||||
}
|
18
src/Shaders/SetIndices.gdshader
Normal file
|
@ -0,0 +1,18 @@
|
|||
shader_type canvas_item;
|
||||
render_mode unshaded;
|
||||
|
||||
#include "res://src/Shaders/FindPaletteColorIndex.gdshaderinc"
|
||||
uniform sampler2D rgb_texture : filter_nearest;
|
||||
uniform sampler2D palette_texture : filter_nearest;
|
||||
|
||||
|
||||
void fragment() {
|
||||
vec4 color = texture(rgb_texture, UV);
|
||||
if (color.a <= 0.0001) {
|
||||
COLOR.r = 0.0;
|
||||
}
|
||||
else {
|
||||
int color_index = find_index(color, textureSize(palette_texture, 0).x, palette_texture);
|
||||
COLOR.r = float(color_index + 1) / 255.0;
|
||||
}
|
||||
}
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
[ext_resource type="PackedScene" uid="uid://ctfgfelg0sho8" path="res://src/Tools/BaseTool.tscn" id="1"]
|
||||
[ext_resource type="Script" path="res://src/Tools/3DTools/3DShapeEdit.gd" id="2"]
|
||||
[ext_resource type="PackedScene" path="res://src/UI/Nodes/ValueSliderV2.tscn" id="3"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/ValueSlider.tscn" id="4"]
|
||||
[ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="5"]
|
||||
[ext_resource type="PackedScene" path="res://src/UI/Nodes/Sliders/ValueSliderV2.tscn" id="3"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/Sliders/ValueSlider.tscn" id="4"]
|
||||
[ext_resource type="Script" path="res://src/UI/Nodes/Sliders/ValueSlider.gd" id="5"]
|
||||
[ext_resource type="Script" path="res://src/UI/Nodes/CollapsibleContainer.gd" id="6"]
|
||||
[ext_resource type="PackedScene" uid="uid://dpoteid430evf" path="res://src/UI/Nodes/ValueSliderV3.tscn" id="7"]
|
||||
[ext_resource type="PackedScene" uid="uid://dpoteid430evf" path="res://src/UI/Nodes/Sliders/ValueSliderV3.tscn" id="7"]
|
||||
|
||||
[sub_resource type="InputEventAction" id="InputEventAction_8sqgw"]
|
||||
action = &"delete"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
class_name BaseDrawTool
|
||||
extends BaseTool
|
||||
|
||||
const IMAGE_BRUSHES := [Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM]
|
||||
|
@ -17,6 +18,7 @@ var _brush_image := Image.new()
|
|||
var _orignal_brush_image := Image.new() ## Contains the original _brush_image, without resizing
|
||||
var _brush_texture := ImageTexture.new()
|
||||
var _strength := 1.0
|
||||
var _is_eraser := false
|
||||
@warning_ignore("unused_private_class_variable")
|
||||
var _picking_color := false
|
||||
|
||||
|
@ -35,13 +37,14 @@ var _line_polylines := []
|
|||
|
||||
# Memorize some stuff when doing brush strokes
|
||||
var _stroke_project: Project
|
||||
var _stroke_images: Array[Image] = []
|
||||
var _stroke_images: Array[ImageExtended] = []
|
||||
var _is_mask_size_zero := true
|
||||
var _circle_tool_shortcut: Array[Vector2i]
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
super._ready()
|
||||
Global.cel_switched.connect(update_brush)
|
||||
Global.global_tool_options.dynamics_panel.dynamics_changed.connect(_reset_dynamics)
|
||||
Tools.color_changed.connect(_on_Color_changed)
|
||||
Global.brushes_popup.brush_removed.connect(_on_Brush_removed)
|
||||
|
@ -104,7 +107,7 @@ func _on_InterpolateFactor_value_changed(value: float) -> void:
|
|||
save_config()
|
||||
|
||||
|
||||
func _on_Color_changed(_color: Color, _button: int) -> void:
|
||||
func _on_Color_changed(_color_info: Dictionary, _button: int) -> void:
|
||||
update_brush()
|
||||
|
||||
|
||||
|
@ -160,34 +163,48 @@ func update_config() -> void:
|
|||
|
||||
func update_brush() -> void:
|
||||
$Brush/BrushSize.suffix = "px" # Assume we are using default brushes
|
||||
match _brush.type:
|
||||
Brushes.PIXEL:
|
||||
_brush_texture = ImageTexture.create_from_image(
|
||||
load("res://assets/graphics/pixel_image.png")
|
||||
)
|
||||
_stroke_dimensions = Vector2.ONE * _brush_size
|
||||
Brushes.CIRCLE:
|
||||
_brush_texture = ImageTexture.create_from_image(
|
||||
load("res://assets/graphics/circle_9x9.png")
|
||||
)
|
||||
_stroke_dimensions = Vector2.ONE * _brush_size
|
||||
Brushes.FILLED_CIRCLE:
|
||||
_brush_texture = ImageTexture.create_from_image(
|
||||
load("res://assets/graphics/circle_filled_9x9.png")
|
||||
)
|
||||
_stroke_dimensions = Vector2.ONE * _brush_size
|
||||
Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM:
|
||||
$Brush/BrushSize.suffix = "00 %" # Use a different size convention on images
|
||||
if _brush.random.size() <= 1:
|
||||
_orignal_brush_image = _brush.image
|
||||
else:
|
||||
var random := randi() % _brush.random.size()
|
||||
_orignal_brush_image = _brush.random[random]
|
||||
_brush_image = _create_blended_brush_image(_orignal_brush_image)
|
||||
update_brush_image_flip_and_rotate()
|
||||
_brush_texture = ImageTexture.create_from_image(_brush_image)
|
||||
update_mirror_brush()
|
||||
_stroke_dimensions = _brush_image.get_size()
|
||||
if Tools.is_placing_tiles():
|
||||
var tilemap_cel := Global.current_project.get_current_cel() as CelTileMap
|
||||
var tileset := tilemap_cel.tileset
|
||||
var tile_index := clampi(TileSetPanel.selected_tile_index, 0, tileset.tiles.size() - 1)
|
||||
var tile_image := tileset.tiles[tile_index].image
|
||||
tile_image = tilemap_cel.transform_tile(
|
||||
tile_image,
|
||||
TileSetPanel.is_flipped_h,
|
||||
TileSetPanel.is_flipped_v,
|
||||
TileSetPanel.is_transposed
|
||||
)
|
||||
_brush_image.copy_from(tile_image)
|
||||
_brush_texture = ImageTexture.create_from_image(_brush_image)
|
||||
else:
|
||||
match _brush.type:
|
||||
Brushes.PIXEL:
|
||||
_brush_texture = ImageTexture.create_from_image(
|
||||
load("res://assets/graphics/pixel_image.png")
|
||||
)
|
||||
_stroke_dimensions = Vector2.ONE * _brush_size
|
||||
Brushes.CIRCLE:
|
||||
_brush_texture = ImageTexture.create_from_image(
|
||||
load("res://assets/graphics/circle_9x9.png")
|
||||
)
|
||||
_stroke_dimensions = Vector2.ONE * _brush_size
|
||||
Brushes.FILLED_CIRCLE:
|
||||
_brush_texture = ImageTexture.create_from_image(
|
||||
load("res://assets/graphics/circle_filled_9x9.png")
|
||||
)
|
||||
_stroke_dimensions = Vector2.ONE * _brush_size
|
||||
Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM:
|
||||
$Brush/BrushSize.suffix = "00 %" # Use a different size convention on images
|
||||
if _brush.random.size() <= 1:
|
||||
_orignal_brush_image = _brush.image
|
||||
else:
|
||||
var random := randi() % _brush.random.size()
|
||||
_orignal_brush_image = _brush.random[random]
|
||||
_brush_image = _create_blended_brush_image(_orignal_brush_image)
|
||||
update_brush_image_flip_and_rotate()
|
||||
_brush_texture = ImageTexture.create_from_image(_brush_image)
|
||||
update_mirror_brush()
|
||||
_stroke_dimensions = _brush_image.get_size()
|
||||
_circle_tool_shortcut = []
|
||||
_indicator = _create_brush_indicator()
|
||||
_polylines = _create_polylines(_indicator)
|
||||
|
@ -195,6 +212,7 @@ func update_brush() -> void:
|
|||
$DensityValueSlider.visible = _brush.type not in IMAGE_BRUSHES
|
||||
$ColorInterpolation.visible = _brush.type in IMAGE_BRUSHES
|
||||
$RotationOptions.visible = _brush.type in IMAGE_BRUSHES
|
||||
Global.canvas.indicators.queue_redraw()
|
||||
|
||||
|
||||
func update_random_image() -> void:
|
||||
|
@ -256,8 +274,10 @@ func prepare_undo(action: String) -> void:
|
|||
|
||||
|
||||
func commit_undo() -> void:
|
||||
var redo_data := _get_undo_data()
|
||||
var project := Global.current_project
|
||||
Global.canvas.update_selected_cels_textures(project)
|
||||
project.update_tilemaps(_undo_data)
|
||||
var redo_data := _get_undo_data()
|
||||
var frame := -1
|
||||
var layer := -1
|
||||
if Global.animation_timeline.animation_timer.is_stopped() and project.selected_cels.size() == 1:
|
||||
|
@ -265,7 +285,7 @@ func commit_undo() -> void:
|
|||
layer = project.current_layer
|
||||
|
||||
project.undos += 1
|
||||
Global.undo_redo_compress_images(redo_data, _undo_data, project)
|
||||
project.deserialize_cel_undo_data(redo_data, _undo_data)
|
||||
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false, frame, layer))
|
||||
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true, frame, layer))
|
||||
project.undo_redo.commit_action()
|
||||
|
@ -303,6 +323,22 @@ func draw_end(pos: Vector2i) -> void:
|
|||
_polylines = _create_polylines(_indicator)
|
||||
|
||||
|
||||
func draw_tile(pos: Vector2i) -> void:
|
||||
var tile_index := 0 if _is_eraser else TileSetPanel.selected_tile_index
|
||||
var mirrored_positions := Tools.get_mirrored_positions(pos, Global.current_project)
|
||||
var tile_positions := PackedInt32Array()
|
||||
tile_positions.resize(mirrored_positions.size() + 1)
|
||||
tile_positions[0] = get_cell_position(pos)
|
||||
for i in mirrored_positions.size():
|
||||
var mirrored_position := mirrored_positions[i]
|
||||
tile_positions[i + 1] = get_cell_position(mirrored_position)
|
||||
for cel in _get_selected_draw_cels():
|
||||
if cel is not CelTileMap:
|
||||
return
|
||||
for tile_position in tile_positions:
|
||||
(cel as CelTileMap).set_index(tile_position, tile_index)
|
||||
|
||||
|
||||
func _prepare_tool() -> void:
|
||||
if !Global.current_project.layers[Global.current_project.current_layer].can_layer_get_drawn():
|
||||
return
|
||||
|
@ -341,6 +377,8 @@ func _prepare_tool() -> void:
|
|||
func _draw_tool(pos: Vector2) -> PackedVector2Array:
|
||||
if !Global.current_project.layers[Global.current_project.current_layer].can_layer_get_drawn():
|
||||
return PackedVector2Array() # empty fallback
|
||||
if Tools.is_placing_tiles():
|
||||
return _compute_draw_tool_pixel(pos)
|
||||
match _brush.type:
|
||||
Brushes.PIXEL:
|
||||
return _compute_draw_tool_pixel(pos)
|
||||
|
@ -391,9 +429,12 @@ func draw_fill_gap(start: Vector2i, end: Vector2i) -> void:
|
|||
|
||||
## Compute the array of coordinates that should be drawn
|
||||
func _compute_draw_tool_pixel(pos: Vector2) -> PackedVector2Array:
|
||||
var brush_size := _brush_size_dynamics
|
||||
if Tools.is_placing_tiles():
|
||||
brush_size = 1
|
||||
var result := PackedVector2Array()
|
||||
var start := pos - Vector2.ONE * (_brush_size_dynamics >> 1)
|
||||
var end := start + Vector2.ONE * _brush_size_dynamics
|
||||
var start := pos - Vector2.ONE * (brush_size >> 1)
|
||||
var end := start + Vector2.ONE * brush_size
|
||||
for y in range(start.y, end.y):
|
||||
for x in range(start.x, end.x):
|
||||
result.append(Vector2(x, y))
|
||||
|
@ -482,7 +523,14 @@ func remove_unselected_parts_of_brush(brush: Image, dst: Vector2i) -> Image:
|
|||
|
||||
func draw_indicator(left: bool) -> void:
|
||||
var color := Global.left_tool_color if left else Global.right_tool_color
|
||||
draw_indicator_at(snap_position(_cursor), Vector2i.ZERO, color)
|
||||
var snapped_position := snap_position(_cursor)
|
||||
if Tools.is_placing_tiles():
|
||||
var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset
|
||||
var grid_size := tileset.tile_size
|
||||
snapped_position = _snap_to_rectangular_grid_center(
|
||||
snapped_position, grid_size, Vector2i.ZERO, -1
|
||||
)
|
||||
draw_indicator_at(snapped_position, Vector2i.ZERO, color)
|
||||
if (
|
||||
Global.current_project.has_selection
|
||||
and Global.current_project.tiles.mode == Tiles.MODE.NONE
|
||||
|
@ -491,7 +539,7 @@ func draw_indicator(left: bool) -> void:
|
|||
var nearest_pos := Global.current_project.selection_map.get_nearest_position(pos)
|
||||
if nearest_pos != Vector2i.ZERO:
|
||||
var offset := nearest_pos
|
||||
draw_indicator_at(snap_position(_cursor), offset, Color.GREEN)
|
||||
draw_indicator_at(snapped_position, offset, Color.GREEN)
|
||||
return
|
||||
|
||||
if Global.current_project.tiles.mode and Global.current_project.tiles.has_point(_cursor):
|
||||
|
@ -499,12 +547,12 @@ func draw_indicator(left: bool) -> void:
|
|||
var nearest_tile := Global.current_project.tiles.get_nearest_tile(pos)
|
||||
if nearest_tile.position != Vector2i.ZERO:
|
||||
var offset := nearest_tile.position
|
||||
draw_indicator_at(snap_position(_cursor), offset, Color.GREEN)
|
||||
draw_indicator_at(snapped_position, offset, Color.GREEN)
|
||||
|
||||
|
||||
func draw_indicator_at(pos: Vector2i, offset: Vector2i, color: Color) -> void:
|
||||
var canvas: Node2D = Global.canvas.indicators
|
||||
if _brush.type in IMAGE_BRUSHES and not _draw_line:
|
||||
if _brush.type in IMAGE_BRUSHES and not _draw_line or Tools.is_placing_tiles():
|
||||
pos -= _brush_image.get_size() / 2
|
||||
pos -= offset
|
||||
canvas.draw_texture(_brush_texture, pos)
|
||||
|
@ -539,6 +587,9 @@ func _set_pixel_no_cache(pos: Vector2i, ignore_mirroring := false) -> void:
|
|||
pos = _stroke_project.tiles.get_canon_position(pos)
|
||||
if Global.current_project.has_selection:
|
||||
pos = Global.current_project.selection_map.get_canon_position(pos)
|
||||
if Tools.is_placing_tiles():
|
||||
draw_tile(pos)
|
||||
return
|
||||
if !_stroke_project.can_pixel_get_drawn(pos):
|
||||
return
|
||||
|
||||
|
@ -727,11 +778,7 @@ func _get_undo_data() -> Dictionary:
|
|||
if not cel is PixelCel:
|
||||
continue
|
||||
cels.append(cel)
|
||||
for cel in cels:
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
var image := cel.get_image()
|
||||
data[image] = image.data
|
||||
project.serialize_cel_undo_data(cels, data)
|
||||
return data
|
||||
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
[gd_scene load_steps=9 format=3 uid="uid://ubyatap3sylf"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/ValueSlider.tscn" id="1"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/Sliders/ValueSlider.tscn" id="1"]
|
||||
[ext_resource type="PackedScene" uid="uid://ctfgfelg0sho8" path="res://src/Tools/BaseTool.tscn" id="2"]
|
||||
[ext_resource type="Script" path="res://src/Tools/BaseDraw.gd" id="3"]
|
||||
[ext_resource type="Script" path="res://src/UI/Nodes/CollapsibleContainer.gd" id="3_76bek"]
|
||||
[ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="5_kdxku"]
|
||||
[ext_resource type="Script" path="res://src/UI/Nodes/Sliders/ValueSlider.gd" id="5_kdxku"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_7u3x0"]
|
||||
resource_name = "rotate"
|
||||
|
|
|
@ -152,6 +152,10 @@ func draw_move(pos: Vector2i) -> void:
|
|||
if not _move:
|
||||
return
|
||||
|
||||
if Tools.is_placing_tiles():
|
||||
var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset
|
||||
var grid_size := tileset.tile_size
|
||||
pos = Tools.snap_to_rectangular_grid_boundary(pos, grid_size)
|
||||
if Input.is_action_pressed("transform_snap_axis"): # Snap to axis
|
||||
var angle := Vector2(pos).angle_to_point(_start_pos)
|
||||
if absf(angle) <= PI / 4 or absf(angle) >= 3 * PI / 4:
|
||||
|
@ -159,16 +163,17 @@ func draw_move(pos: Vector2i) -> void:
|
|||
else:
|
||||
pos.x = _start_pos.x
|
||||
if Input.is_action_pressed("transform_snap_grid"):
|
||||
_offset = _offset.snapped(Global.grid_size)
|
||||
_offset = _offset.snapped(Global.grids[0].grid_size)
|
||||
var prev_pos: Vector2i = selection_node.big_bounding_rectangle.position
|
||||
selection_node.big_bounding_rectangle.position = prev_pos.snapped(Global.grid_size)
|
||||
selection_node.big_bounding_rectangle.position = prev_pos.snapped(Global.grids[0].grid_size)
|
||||
selection_node.marching_ants_outline.offset += Vector2(
|
||||
selection_node.big_bounding_rectangle.position - prev_pos
|
||||
)
|
||||
pos = pos.snapped(Global.grid_size)
|
||||
var grid_offset := Global.grid_offset
|
||||
pos = pos.snapped(Global.grids[0].grid_size)
|
||||
var grid_offset := Global.grids[0].grid_offset
|
||||
grid_offset = Vector2i(
|
||||
fmod(grid_offset.x, Global.grid_size.x), fmod(grid_offset.y, Global.grid_size.y)
|
||||
fmod(grid_offset.x, Global.grids[0].grid_size.x),
|
||||
fmod(grid_offset.y, Global.grids[0].grid_size.y)
|
||||
)
|
||||
pos += grid_offset
|
||||
|
||||
|
@ -210,6 +215,13 @@ func apply_selection(_position: Vector2i) -> void:
|
|||
_intersect = true
|
||||
|
||||
|
||||
func select_tilemap_cell(
|
||||
cel: CelTileMap, cell_position: int, selection: SelectionMap, select: bool
|
||||
) -> void:
|
||||
var rect := Rect2i(cel.get_cell_coords_in_image(cell_position), cel.tileset.tile_size)
|
||||
selection.select_rect(rect, select)
|
||||
|
||||
|
||||
func _on_confirm_button_pressed() -> void:
|
||||
if selection_node.is_moving_content:
|
||||
selection_node.transform_content_confirm()
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
[ext_resource type="PackedScene" uid="uid://ctfgfelg0sho8" path="res://src/Tools/BaseTool.tscn" id="1"]
|
||||
[ext_resource type="Script" path="res://src/Tools/BaseSelectionTool.gd" id="2"]
|
||||
[ext_resource type="Texture2D" uid="uid://d267xalp3p7ru" path="res://assets/graphics/misc/check_plain.png" id="3_mtv71"]
|
||||
[ext_resource type="PackedScene" path="res://src/UI/Nodes/ValueSliderV2.tscn" id="4"]
|
||||
[ext_resource type="PackedScene" path="res://src/UI/Nodes/Sliders/ValueSliderV2.tscn" id="4"]
|
||||
[ext_resource type="Texture2D" uid="uid://bnc78807k1xjv" path="res://assets/graphics/misc/close.png" id="4_ad04n"]
|
||||
|
||||
[sub_resource type="InputEventAction" id="InputEventAction_gfv4x"]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends "res://src/Tools/BaseDraw.gd"
|
||||
extends BaseDrawTool
|
||||
|
||||
var _start := Vector2i.ZERO
|
||||
var _offset := Vector2i.ZERO
|
||||
|
@ -128,8 +128,8 @@ func draw_move(pos: Vector2i) -> void:
|
|||
|
||||
func draw_end(pos: Vector2i) -> void:
|
||||
pos = snap_position(pos)
|
||||
super.draw_end(pos)
|
||||
if _picking_color:
|
||||
super.draw_end(pos)
|
||||
return
|
||||
|
||||
if _drawing:
|
||||
|
@ -148,8 +148,10 @@ func draw_end(pos: Vector2i) -> void:
|
|||
_start = Vector2i.ZERO
|
||||
_dest = Vector2i.ZERO
|
||||
_drawing = false
|
||||
Global.canvas.previews_sprite.texture = null
|
||||
_displace_origin = false
|
||||
cursor_text = ""
|
||||
super.draw_end(pos)
|
||||
|
||||
|
||||
func draw_preview() -> void:
|
||||
|
@ -168,22 +170,11 @@ func draw_preview() -> void:
|
|||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(points[i]):
|
||||
image.set_pixelv(points[i], Color.WHITE)
|
||||
# Handle mirroring
|
||||
if Tools.horizontal_mirror:
|
||||
for point in mirror_array(points, true, false):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
if Tools.vertical_mirror:
|
||||
for point in mirror_array(points, true, true):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
if Tools.vertical_mirror:
|
||||
for point in mirror_array(points, false, true):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
for point in mirror_array(points):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
var texture := ImageTexture.create_from_image(image)
|
||||
canvas.texture = texture
|
||||
else:
|
||||
canvas.texture = null
|
||||
|
||||
|
||||
func _draw_shape(origin: Vector2i, dest: Vector2i) -> void:
|
||||
|
@ -197,9 +188,12 @@ func _draw_shape(origin: Vector2i, dest: Vector2i) -> void:
|
|||
_drawer.reset()
|
||||
# Draw each point offsetted based on the shape's thickness
|
||||
var draw_pos := point + thickness_vector
|
||||
if Global.current_project.can_pixel_get_drawn(draw_pos):
|
||||
for image in images:
|
||||
_drawer.set_pixel(image, draw_pos, tool_slot.color)
|
||||
if Tools.is_placing_tiles():
|
||||
draw_tile(draw_pos)
|
||||
else:
|
||||
if Global.current_project.can_pixel_get_drawn(draw_pos):
|
||||
for image in images:
|
||||
_drawer.set_pixel(image, draw_pos, tool_slot.color)
|
||||
|
||||
commit_undo()
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
[ext_resource type="Script" path="res://src/Tools/BaseShapeDrawer.gd" id="1"]
|
||||
[ext_resource type="PackedScene" uid="uid://ubyatap3sylf" path="res://src/Tools/BaseDraw.tscn" id="2"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/ValueSlider.tscn" id="3"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/Sliders/ValueSlider.tscn" id="3"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_7w1wt"]
|
||||
resource_name = "rotate"
|
||||
|
|
|
@ -75,7 +75,17 @@ func draw_move(pos: Vector2i) -> void:
|
|||
func draw_end(_pos: Vector2i) -> void:
|
||||
is_moving = false
|
||||
_draw_cache = []
|
||||
Global.current_project.can_undo = true
|
||||
var project := Global.current_project
|
||||
project.can_undo = true
|
||||
|
||||
|
||||
func get_cell_position(pos: Vector2i) -> int:
|
||||
var tile_pos := 0
|
||||
if Global.current_project.get_current_cel() is not CelTileMap:
|
||||
return tile_pos
|
||||
var cel := Global.current_project.get_current_cel() as CelTileMap
|
||||
tile_pos = cel.get_cell_position(pos)
|
||||
return tile_pos
|
||||
|
||||
|
||||
func cursor_move(pos: Vector2i) -> void:
|
||||
|
@ -129,48 +139,14 @@ func draw_preview() -> void:
|
|||
func snap_position(pos: Vector2) -> Vector2:
|
||||
var snapping_distance := Global.snapping_distance / Global.camera.zoom.x
|
||||
if Global.snap_to_rectangular_grid_boundary:
|
||||
var grid_pos := pos.snapped(Global.grid_size)
|
||||
grid_pos += Vector2(Global.grid_offset)
|
||||
# keeping grid_pos as is would have been fine but this adds extra accuracy as to
|
||||
# which snap point (from the list below) is closest to mouse and occupy THAT point
|
||||
var t_l := grid_pos + Vector2(-Global.grid_size.x, -Global.grid_size.y)
|
||||
var t_c := grid_pos + Vector2(0, -Global.grid_size.y) # t_c is for "top centre" and so on
|
||||
var t_r := grid_pos + Vector2(Global.grid_size.x, -Global.grid_size.y)
|
||||
var m_l := grid_pos + Vector2(-Global.grid_size.x, 0)
|
||||
var m_c := grid_pos
|
||||
var m_r := grid_pos + Vector2(Global.grid_size.x, 0)
|
||||
var b_l := grid_pos + Vector2(-Global.grid_size.x, Global.grid_size.y)
|
||||
var b_c := grid_pos + Vector2(0, Global.grid_size.y)
|
||||
var b_r := grid_pos + Vector2(Global.grid_size)
|
||||
var vec_arr: PackedVector2Array = [t_l, t_c, t_r, m_l, m_c, m_r, b_l, b_c, b_r]
|
||||
for vec in vec_arr:
|
||||
if vec.distance_to(pos) < grid_pos.distance_to(pos):
|
||||
grid_pos = vec
|
||||
|
||||
var grid_point := _get_closest_point_to_grid(pos, snapping_distance, grid_pos)
|
||||
if grid_point != Vector2.INF:
|
||||
pos = grid_point.floor()
|
||||
pos = Tools.snap_to_rectangular_grid_boundary(
|
||||
pos, Global.grids[0].grid_size, Global.grids[0].grid_offset, snapping_distance
|
||||
)
|
||||
|
||||
if Global.snap_to_rectangular_grid_center:
|
||||
var grid_center := pos.snapped(Global.grid_size) + Vector2(Global.grid_size / 2)
|
||||
grid_center += Vector2(Global.grid_offset)
|
||||
# keeping grid_center as is would have been fine but this adds extra accuracy as to
|
||||
# which snap point (from the list below) is closest to mouse and occupy THAT point
|
||||
var t_l := grid_center + Vector2(-Global.grid_size.x, -Global.grid_size.y)
|
||||
var t_c := grid_center + Vector2(0, -Global.grid_size.y) # t_c is for "top centre" and so on
|
||||
var t_r := grid_center + Vector2(Global.grid_size.x, -Global.grid_size.y)
|
||||
var m_l := grid_center + Vector2(-Global.grid_size.x, 0)
|
||||
var m_c := grid_center
|
||||
var m_r := grid_center + Vector2(Global.grid_size.x, 0)
|
||||
var b_l := grid_center + Vector2(-Global.grid_size.x, Global.grid_size.y)
|
||||
var b_c := grid_center + Vector2(0, Global.grid_size.y)
|
||||
var b_r := grid_center + Vector2(Global.grid_size)
|
||||
var vec_arr := [t_l, t_c, t_r, m_l, m_c, m_r, b_l, b_c, b_r]
|
||||
for vec in vec_arr:
|
||||
if vec.distance_to(pos) < grid_center.distance_to(pos):
|
||||
grid_center = vec
|
||||
if grid_center.distance_to(pos) <= snapping_distance:
|
||||
pos = grid_center.floor()
|
||||
pos = _snap_to_rectangular_grid_center(
|
||||
pos, Global.grids[0].grid_size, Global.grids[0].grid_offset, snapping_distance
|
||||
)
|
||||
|
||||
var snap_to := Vector2.INF
|
||||
if Global.snap_to_guides:
|
||||
|
@ -205,73 +181,70 @@ func snap_position(pos: Vector2) -> Vector2:
|
|||
return pos
|
||||
|
||||
|
||||
func mirror_array(array: Array[Vector2i], h: bool, v: bool) -> Array[Vector2i]:
|
||||
## Returns an array that mirrors each point of the [param array].
|
||||
## An optional [param callable] can be passed, which gets called for each type of symmetry.
|
||||
func mirror_array(array: Array[Vector2i], callable := func(_array): pass) -> Array[Vector2i]:
|
||||
var new_array: Array[Vector2i] = []
|
||||
var project := Global.current_project
|
||||
for point in array:
|
||||
if h and v:
|
||||
new_array.append(
|
||||
Vector2i(project.x_symmetry_point - point.x, project.y_symmetry_point - point.y)
|
||||
)
|
||||
elif h:
|
||||
new_array.append(Vector2i(project.x_symmetry_point - point.x, point.y))
|
||||
elif v:
|
||||
new_array.append(Vector2i(point.x, project.y_symmetry_point - point.y))
|
||||
if Tools.horizontal_mirror and Tools.vertical_mirror:
|
||||
var hv_array: Array[Vector2i] = []
|
||||
for point in array:
|
||||
var mirror_x := Tools.calculate_mirror_horizontal(point, project)
|
||||
hv_array.append(Tools.calculate_mirror_vertical(mirror_x, project))
|
||||
if callable.is_valid():
|
||||
callable.call(hv_array)
|
||||
new_array += hv_array
|
||||
if Tools.horizontal_mirror:
|
||||
var h_array: Array[Vector2i] = []
|
||||
for point in array:
|
||||
h_array.append(Tools.calculate_mirror_horizontal(point, project))
|
||||
if callable.is_valid():
|
||||
callable.call(h_array)
|
||||
new_array += h_array
|
||||
if Tools.vertical_mirror:
|
||||
var v_array: Array[Vector2i] = []
|
||||
for point in array:
|
||||
v_array.append(Tools.calculate_mirror_vertical(point, project))
|
||||
if callable.is_valid():
|
||||
callable.call(v_array)
|
||||
new_array += v_array
|
||||
|
||||
return new_array
|
||||
|
||||
|
||||
func _get_closest_point_to_grid(pos: Vector2, distance: float, grid_pos: Vector2) -> Vector2:
|
||||
# If the cursor is close to the start/origin of a grid cell, snap to that
|
||||
var snap_distance := distance * Vector2.ONE
|
||||
var closest_point := Vector2.INF
|
||||
var rect := Rect2()
|
||||
rect.position = pos - (snap_distance / 4.0)
|
||||
rect.end = pos + (snap_distance / 4.0)
|
||||
if rect.has_point(grid_pos):
|
||||
closest_point = grid_pos
|
||||
return closest_point
|
||||
# If the cursor is far from the grid cell origin but still close to a grid line
|
||||
# Look for a point close to a horizontal grid line
|
||||
var grid_start_hor := Vector2(0, grid_pos.y)
|
||||
var grid_end_hor := Vector2(Global.current_project.size.x, grid_pos.y)
|
||||
var closest_point_hor := _get_closest_point_to_segment(
|
||||
pos, distance, grid_start_hor, grid_end_hor
|
||||
)
|
||||
# Look for a point close to a vertical grid line
|
||||
var grid_start_ver := Vector2(grid_pos.x, 0)
|
||||
var grid_end_ver := Vector2(grid_pos.x, Global.current_project.size.y)
|
||||
var closest_point_ver := _get_closest_point_to_segment(
|
||||
pos, distance, grid_start_ver, grid_end_ver
|
||||
)
|
||||
# Snap to the closest point to the closest grid line
|
||||
var horizontal_distance := (closest_point_hor - pos).length()
|
||||
var vertical_distance := (closest_point_ver - pos).length()
|
||||
if horizontal_distance < vertical_distance:
|
||||
closest_point = closest_point_hor
|
||||
elif horizontal_distance > vertical_distance:
|
||||
closest_point = closest_point_ver
|
||||
elif horizontal_distance == vertical_distance and closest_point_hor != Vector2.INF:
|
||||
closest_point = grid_pos
|
||||
return closest_point
|
||||
|
||||
|
||||
func _get_closest_point_to_segment(
|
||||
pos: Vector2, distance: float, s1: Vector2, s2: Vector2
|
||||
func _snap_to_rectangular_grid_center(
|
||||
pos: Vector2, grid_size: Vector2i, grid_offset: Vector2i, snapping_distance: float
|
||||
) -> Vector2:
|
||||
var test_line := (s2 - s1).rotated(deg_to_rad(90)).normalized()
|
||||
var from_a := pos - test_line * distance
|
||||
var from_b := pos + test_line * distance
|
||||
var closest_point := Vector2.INF
|
||||
if Geometry2D.segment_intersects_segment(from_a, from_b, s1, s2):
|
||||
closest_point = Geometry2D.get_closest_point_to_segment(pos, s1, s2)
|
||||
return closest_point
|
||||
var grid_center := pos.snapped(grid_size) + Vector2(grid_size / 2)
|
||||
grid_center += Vector2(grid_offset)
|
||||
# keeping grid_center as is would have been fine but this adds extra accuracy as to
|
||||
# which snap point (from the list below) is closest to mouse and occupy THAT point
|
||||
# t_l is for "top left" and so on
|
||||
var t_l := grid_center + Vector2(-grid_size.x, -grid_size.y)
|
||||
var t_c := grid_center + Vector2(0, -grid_size.y)
|
||||
var t_r := grid_center + Vector2(grid_size.x, -grid_size.y)
|
||||
var m_l := grid_center + Vector2(-grid_size.x, 0)
|
||||
var m_c := grid_center
|
||||
var m_r := grid_center + Vector2(grid_size.x, 0)
|
||||
var b_l := grid_center + Vector2(-grid_size.x, grid_size.y)
|
||||
var b_c := grid_center + Vector2(0, grid_size.y)
|
||||
var b_r := grid_center + Vector2(grid_size)
|
||||
var vec_arr := [t_l, t_c, t_r, m_l, m_c, m_r, b_l, b_c, b_r]
|
||||
for vec in vec_arr:
|
||||
if vec.distance_to(pos) < grid_center.distance_to(pos):
|
||||
grid_center = vec
|
||||
if snapping_distance < 0:
|
||||
pos = grid_center.floor()
|
||||
else:
|
||||
if grid_center.distance_to(pos) <= snapping_distance:
|
||||
pos = grid_center.floor()
|
||||
return pos
|
||||
|
||||
|
||||
func _snap_to_guide(
|
||||
snap_to: Vector2, pos: Vector2, distance: float, s1: Vector2, s2: Vector2
|
||||
) -> Vector2:
|
||||
var closest_point := _get_closest_point_to_segment(pos, distance, s1, s2)
|
||||
var closest_point := Tools.get_closest_point_to_segment(pos, distance, s1, s2)
|
||||
if closest_point == Vector2.INF: # Is not close to a guide
|
||||
return Vector2.INF
|
||||
# Snap to the closest guide
|
||||
|
@ -299,12 +272,23 @@ func _get_draw_rect() -> Rect2i:
|
|||
return Rect2i(Vector2i.ZERO, Global.current_project.size)
|
||||
|
||||
|
||||
func _get_draw_image() -> Image:
|
||||
func _get_draw_image() -> ImageExtended:
|
||||
return Global.current_project.get_current_cel().get_image()
|
||||
|
||||
|
||||
func _get_selected_draw_images() -> Array[Image]:
|
||||
var images: Array[Image] = []
|
||||
func _get_selected_draw_cels() -> Array[BaseCel]:
|
||||
var cels: Array[BaseCel]
|
||||
var project := Global.current_project
|
||||
for cel_index in project.selected_cels:
|
||||
var cel: BaseCel = project.frames[cel_index[0]].cels[cel_index[1]]
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
cels.append(cel)
|
||||
return cels
|
||||
|
||||
|
||||
func _get_selected_draw_images() -> Array[ImageExtended]:
|
||||
var images: Array[ImageExtended] = []
|
||||
var project := Global.current_project
|
||||
for cel_index in project.selected_cels:
|
||||
var cel: BaseCel = project.frames[cel_index[0]].cels[cel_index[1]]
|
||||
|
@ -321,22 +305,31 @@ func _pick_color(pos: Vector2i) -> void:
|
|||
|
||||
if pos.x < 0 or pos.y < 0:
|
||||
return
|
||||
|
||||
if Tools.is_placing_tiles():
|
||||
var cel := Global.current_project.get_current_cel() as CelTileMap
|
||||
Tools.selected_tile_index_changed.emit(cel.get_cell_index_at_coords(pos))
|
||||
return
|
||||
var image := Image.new()
|
||||
image.copy_from(_get_draw_image())
|
||||
if pos.x > image.get_width() - 1 or pos.y > image.get_height() - 1:
|
||||
return
|
||||
|
||||
var color := Color(0, 0, 0, 0)
|
||||
var palette_index = -1
|
||||
var curr_frame: Frame = project.frames[project.current_frame]
|
||||
for layer in project.layers.size():
|
||||
var idx := (project.layers.size() - 1) - layer
|
||||
if project.layers[idx].is_visible_in_hierarchy():
|
||||
image = curr_frame.cels[idx].get_image()
|
||||
var cel := curr_frame.cels[idx]
|
||||
image = cel.get_image()
|
||||
color = image.get_pixelv(pos)
|
||||
if not is_zero_approx(color.a):
|
||||
# If image is indexed then get index as well
|
||||
if cel is PixelCel:
|
||||
if cel.image.is_indexed:
|
||||
palette_index = cel.image.indices_image.get_pixel(pos.x, pos.y).r8 - 1
|
||||
if not is_zero_approx(color.a) or palette_index > -1:
|
||||
break
|
||||
Tools.assign_color(color, tool_slot.button, false)
|
||||
Tools.assign_color(color, tool_slot.button, false, palette_index)
|
||||
|
||||
|
||||
func _flip_rect(rect: Rect2, rect_size: Vector2, horiz: bool, vert: bool) -> Rect2:
|
||||
|
@ -402,3 +395,4 @@ func _exit_tree() -> void:
|
|||
if is_moving:
|
||||
draw_end(Global.canvas.current_pixel.floor())
|
||||
Global.canvas.previews_sprite.texture = null
|
||||
Global.canvas.indicators.queue_redraw()
|
||||
|
|
|
@ -186,6 +186,11 @@ func draw_end(pos: Vector2i) -> void:
|
|||
commit_undo()
|
||||
|
||||
|
||||
func draw_tile(pos: Vector2i, cel: CelTileMap) -> void:
|
||||
var tile_position := get_cell_position(pos)
|
||||
cel.set_index(tile_position, TileSetPanel.selected_tile_index)
|
||||
|
||||
|
||||
func fill(pos: Vector2i) -> void:
|
||||
match _fill_area:
|
||||
FillArea.AREA:
|
||||
|
@ -199,6 +204,17 @@ func fill(pos: Vector2i) -> void:
|
|||
|
||||
func fill_in_color(pos: Vector2i) -> void:
|
||||
var project := Global.current_project
|
||||
if Tools.is_placing_tiles():
|
||||
for cel in _get_selected_draw_cels():
|
||||
if cel is not CelTileMap:
|
||||
continue
|
||||
var tilemap_cel := cel as CelTileMap
|
||||
var tile_index := tilemap_cel.get_cell_index_at_coords(pos)
|
||||
for i in tilemap_cel.cells.size():
|
||||
var cell := tilemap_cel.cells[i]
|
||||
if cell.index == tile_index:
|
||||
tilemap_cel.set_index(i, TileSetPanel.selected_tile_index)
|
||||
return
|
||||
var color := project.get_current_cel().get_image().get_pixelv(pos)
|
||||
var images := _get_selected_draw_images()
|
||||
for image in images:
|
||||
|
@ -220,7 +236,7 @@ func fill_in_color(pos: Vector2i) -> void:
|
|||
if project.has_selection:
|
||||
selection = project.selection_map.return_cropped_copy(project.size)
|
||||
else:
|
||||
selection = Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
|
||||
selection = project.new_empty_image()
|
||||
selection.fill(Color(1, 1, 1, 1))
|
||||
|
||||
selection_tex = ImageTexture.create_from_image(selection)
|
||||
|
@ -263,15 +279,17 @@ func fill_in_selection() -> void:
|
|||
var images := _get_selected_draw_images()
|
||||
if _fill_with == FillWith.COLOR or _pattern == null:
|
||||
if project.has_selection:
|
||||
var filler := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
|
||||
var filler := project.new_empty_image()
|
||||
filler.fill(tool_slot.color)
|
||||
var rect: Rect2i = Global.canvas.selection.big_bounding_rectangle
|
||||
var selection_map_copy := project.selection_map.return_cropped_copy(project.size)
|
||||
for image in images:
|
||||
image.blit_rect_mask(filler, selection_map_copy, rect, rect.position)
|
||||
image.convert_rgb_to_indexed()
|
||||
else:
|
||||
for image in images:
|
||||
image.fill(tool_slot.color)
|
||||
image.convert_rgb_to_indexed()
|
||||
else:
|
||||
# End early if we are filling with an empty pattern
|
||||
var pattern_image: Image = _pattern.image
|
||||
|
@ -284,7 +302,7 @@ func fill_in_selection() -> void:
|
|||
if project.has_selection:
|
||||
selection = project.selection_map.return_cropped_copy(project.size)
|
||||
else:
|
||||
selection = Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
|
||||
selection = project.new_empty_image()
|
||||
selection.fill(Color(1, 1, 1, 1))
|
||||
|
||||
selection_tex = ImageTexture.create_from_image(selection)
|
||||
|
@ -309,6 +327,74 @@ func fill_in_selection() -> void:
|
|||
gen.generate_image(image, PATTERN_FILL_SHADER, params, project.size)
|
||||
|
||||
|
||||
func _flood_fill(pos: Vector2i) -> void:
|
||||
# implements the floodfill routine by Shawn Hargreaves
|
||||
# from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c
|
||||
var project := Global.current_project
|
||||
if Tools.is_placing_tiles():
|
||||
for cel in _get_selected_draw_cels():
|
||||
if cel is not CelTileMap:
|
||||
continue
|
||||
var tile_index := (cel as CelTileMap).get_cell_index_at_coords(pos)
|
||||
# init flood data structures
|
||||
_allegro_flood_segments = []
|
||||
_allegro_image_segments = []
|
||||
_compute_segments_for_tilemap(pos, cel, tile_index)
|
||||
_color_segments_tilemap(cel)
|
||||
return
|
||||
|
||||
var images := _get_selected_draw_images()
|
||||
for image in images:
|
||||
if Tools.check_alpha_lock(image, pos):
|
||||
continue
|
||||
var color: Color = image.get_pixelv(pos)
|
||||
if _fill_with == FillWith.COLOR or _pattern == null:
|
||||
# end early if we are filling with the same color
|
||||
if tool_slot.color.is_equal_approx(color):
|
||||
continue
|
||||
else:
|
||||
# end early if we are filling with an empty pattern
|
||||
var pattern_size := _pattern.image.get_size()
|
||||
if pattern_size.x == 0 or pattern_size.y == 0:
|
||||
return
|
||||
# init flood data structures
|
||||
_allegro_flood_segments = []
|
||||
_allegro_image_segments = []
|
||||
_compute_segments_for_image(pos, project, image, color)
|
||||
# now actually color the image: since we have already checked a few things for the points
|
||||
# we'll process here, we're going to skip a bunch of safety checks to speed things up.
|
||||
_color_segments(image)
|
||||
|
||||
|
||||
func _compute_segments_for_image(
|
||||
pos: Vector2i, project: Project, image: Image, src_color: Color
|
||||
) -> void:
|
||||
# initially allocate at least 1 segment per line of image
|
||||
for j in image.get_height():
|
||||
_add_new_segment(j)
|
||||
# start flood algorithm
|
||||
_flood_line_around_point(pos, project, image, src_color)
|
||||
# test all segments while also discovering more
|
||||
var done := false
|
||||
while not done:
|
||||
done = true
|
||||
var max_index := _allegro_flood_segments.size()
|
||||
for c in max_index:
|
||||
var p := _allegro_flood_segments[c]
|
||||
if p.todo_below: # check below the segment?
|
||||
p.todo_below = false
|
||||
if _check_flooded_segment(
|
||||
p.y + 1, p.left_position, p.right_position, project, image, src_color
|
||||
):
|
||||
done = false
|
||||
if p.todo_above: # check above the segment?
|
||||
p.todo_above = false
|
||||
if _check_flooded_segment(
|
||||
p.y - 1, p.left_position, p.right_position, project, image, src_color
|
||||
):
|
||||
done = false
|
||||
|
||||
|
||||
## Add a new segment to the array
|
||||
func _add_new_segment(y := 0) -> void:
|
||||
_allegro_flood_segments.append(Segment.new(y))
|
||||
|
@ -405,63 +491,7 @@ func _check_flooded_segment(
|
|||
return ret
|
||||
|
||||
|
||||
func _flood_fill(pos: Vector2i) -> void:
|
||||
# implements the floodfill routine by Shawn Hargreaves
|
||||
# from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c
|
||||
var project := Global.current_project
|
||||
var images := _get_selected_draw_images()
|
||||
for image in images:
|
||||
if Tools.check_alpha_lock(image, pos):
|
||||
continue
|
||||
var color: Color = image.get_pixelv(pos)
|
||||
if _fill_with == FillWith.COLOR or _pattern == null:
|
||||
# end early if we are filling with the same color
|
||||
if tool_slot.color.is_equal_approx(color):
|
||||
continue
|
||||
else:
|
||||
# end early if we are filling with an empty pattern
|
||||
var pattern_size := _pattern.image.get_size()
|
||||
if pattern_size.x == 0 or pattern_size.y == 0:
|
||||
return
|
||||
# init flood data structures
|
||||
_allegro_flood_segments = []
|
||||
_allegro_image_segments = []
|
||||
_compute_segments_for_image(pos, project, image, color)
|
||||
# now actually color the image: since we have already checked a few things for the points
|
||||
# we'll process here, we're going to skip a bunch of safety checks to speed things up.
|
||||
_color_segments(image)
|
||||
|
||||
|
||||
func _compute_segments_for_image(
|
||||
pos: Vector2i, project: Project, image: Image, src_color: Color
|
||||
) -> void:
|
||||
# initially allocate at least 1 segment per line of image
|
||||
for j in image.get_height():
|
||||
_add_new_segment(j)
|
||||
# start flood algorithm
|
||||
_flood_line_around_point(pos, project, image, src_color)
|
||||
# test all segments while also discovering more
|
||||
var done := false
|
||||
while not done:
|
||||
done = true
|
||||
var max_index := _allegro_flood_segments.size()
|
||||
for c in max_index:
|
||||
var p := _allegro_flood_segments[c]
|
||||
if p.todo_below: # check below the segment?
|
||||
p.todo_below = false
|
||||
if _check_flooded_segment(
|
||||
p.y + 1, p.left_position, p.right_position, project, image, src_color
|
||||
):
|
||||
done = false
|
||||
if p.todo_above: # check above the segment?
|
||||
p.todo_above = false
|
||||
if _check_flooded_segment(
|
||||
p.y - 1, p.left_position, p.right_position, project, image, src_color
|
||||
):
|
||||
done = false
|
||||
|
||||
|
||||
func _color_segments(image: Image) -> void:
|
||||
func _color_segments(image: ImageExtended) -> void:
|
||||
if _fill_with == FillWith.COLOR or _pattern == null:
|
||||
# This is needed to ensure that the color used to fill is not wrong, due to float
|
||||
# rounding issues.
|
||||
|
@ -472,7 +502,7 @@ func _color_segments(image: Image) -> void:
|
|||
var p := _allegro_image_segments[c]
|
||||
for px in range(p.left_position, p.right_position + 1):
|
||||
# We don't have to check again whether the point being processed is within the bounds
|
||||
image.set_pixel(px, p.y, color)
|
||||
image.set_pixel_custom(px, p.y, color)
|
||||
else:
|
||||
# shortcircuit tests for patternfills
|
||||
var pattern_size := _pattern.image.get_size()
|
||||
|
@ -484,16 +514,126 @@ func _color_segments(image: Image) -> void:
|
|||
_set_pixel_pattern(image, px, p.y, pattern_size)
|
||||
|
||||
|
||||
func _set_pixel_pattern(image: Image, x: int, y: int, pattern_size: Vector2i) -> void:
|
||||
func _set_pixel_pattern(image: ImageExtended, x: int, y: int, pattern_size: Vector2i) -> void:
|
||||
var px := (x + _offset_x) % pattern_size.x
|
||||
var py := (y + _offset_y) % pattern_size.y
|
||||
var pc := _pattern.image.get_pixel(px, py)
|
||||
image.set_pixel(x, y, pc)
|
||||
image.set_pixel_custom(x, y, pc)
|
||||
|
||||
|
||||
func _compute_segments_for_tilemap(pos: Vector2i, cel: CelTileMap, src_index: int) -> void:
|
||||
# initially allocate at least 1 segment per line of the tilemap
|
||||
for j in cel.vertical_cells:
|
||||
_add_new_segment(j)
|
||||
pos /= cel.tileset.tile_size
|
||||
# start flood algorithm
|
||||
_flood_line_around_point_tilemap(pos, cel, src_index)
|
||||
# test all segments while also discovering more
|
||||
var done := false
|
||||
while not done:
|
||||
done = true
|
||||
var max_index := _allegro_flood_segments.size()
|
||||
for c in max_index:
|
||||
var p := _allegro_flood_segments[c]
|
||||
if p.todo_below: # check below the segment?
|
||||
p.todo_below = false
|
||||
if _check_flooded_segment_tilemap(
|
||||
p.y + 1, p.left_position, p.right_position, cel, src_index
|
||||
):
|
||||
done = false
|
||||
if p.todo_above: # check above the segment?
|
||||
p.todo_above = false
|
||||
if _check_flooded_segment_tilemap(
|
||||
p.y - 1, p.left_position, p.right_position, cel, src_index
|
||||
):
|
||||
done = false
|
||||
|
||||
|
||||
## Fill an horizontal segment around the specified position, and adds it to the
|
||||
## list of segments filled. Returns the first x coordinate after the part of the
|
||||
## line that has been filled.
|
||||
## Τhis method is called by [method _flood_fill] after the required data structures
|
||||
## have been initialized.
|
||||
func _flood_line_around_point_tilemap(pos: Vector2i, cel: CelTileMap, src_index: int) -> int:
|
||||
if cel.get_cell_index_at_coords_in_tilemap_space(pos) != src_index:
|
||||
return pos.x + 1
|
||||
var west := pos
|
||||
var east := pos
|
||||
while west.x >= 0 && cel.get_cell_index_at_coords_in_tilemap_space(west) == src_index:
|
||||
west += Vector2i.LEFT
|
||||
while (
|
||||
east.x < cel.horizontal_cells
|
||||
&& cel.get_cell_index_at_coords_in_tilemap_space(east) == src_index
|
||||
):
|
||||
east += Vector2i.RIGHT
|
||||
# Make a note of the stuff we processed
|
||||
var c := pos.y
|
||||
var segment := _allegro_flood_segments[c]
|
||||
# we may have already processed some segments on this y coordinate
|
||||
if segment.flooding:
|
||||
while segment.next > 0:
|
||||
c = segment.next # index of next segment in this line of image
|
||||
segment = _allegro_flood_segments[c]
|
||||
# found last current segment on this line
|
||||
c = _allegro_flood_segments.size()
|
||||
segment.next = c
|
||||
_add_new_segment(pos.y)
|
||||
segment = _allegro_flood_segments[c]
|
||||
# set the values for the current segment
|
||||
segment.flooding = true
|
||||
segment.left_position = west.x + 1
|
||||
segment.right_position = east.x - 1
|
||||
segment.y = pos.y
|
||||
segment.next = 0
|
||||
# Should we process segments above or below this one?
|
||||
# when there is a selected area, the pixels above and below the one we started creating this
|
||||
# segment from may be outside it. It's easier to assume we should be checking for segments
|
||||
# above and below this one than to specifically check every single pixel in it, because that
|
||||
# test will be performed later anyway.
|
||||
# On the other hand, this test we described is the same `project.can_pixel_get_drawn` does if
|
||||
# there is no selection, so we don't need branching here.
|
||||
segment.todo_above = pos.y > 0
|
||||
segment.todo_below = pos.y < cel.vertical_cells - 1
|
||||
# this is an actual segment we should be coloring, so we add it to the results for the
|
||||
# current image
|
||||
if segment.right_position >= segment.left_position:
|
||||
_allegro_image_segments.append(segment)
|
||||
# we know the point just east of the segment is not part of a segment that should be
|
||||
# processed, else it would be part of this segment
|
||||
return east.x + 1
|
||||
|
||||
|
||||
func _check_flooded_segment_tilemap(
|
||||
y: int, left: int, right: int, cel: CelTileMap, src_index: int
|
||||
) -> bool:
|
||||
var ret := false
|
||||
var c := 0
|
||||
while left <= right:
|
||||
c = y
|
||||
while true:
|
||||
var segment := _allegro_flood_segments[c]
|
||||
if left >= segment.left_position and left <= segment.right_position:
|
||||
left = segment.right_position + 2
|
||||
break
|
||||
c = segment.next
|
||||
if c == 0: # couldn't find a valid segment, so we draw a new one
|
||||
left = _flood_line_around_point_tilemap(Vector2i(left, y), cel, src_index)
|
||||
ret = true
|
||||
break
|
||||
return ret
|
||||
|
||||
|
||||
func _color_segments_tilemap(cel: CelTileMap) -> void:
|
||||
for c in _allegro_image_segments.size():
|
||||
var p := _allegro_image_segments[c]
|
||||
for px in range(p.left_position, p.right_position + 1):
|
||||
draw_tile(Vector2i(px, p.y) * cel.tileset.tile_size, cel)
|
||||
|
||||
|
||||
func commit_undo() -> void:
|
||||
var redo_data := _get_undo_data()
|
||||
var project := Global.current_project
|
||||
project.update_tilemaps(_undo_data)
|
||||
var redo_data := _get_undo_data()
|
||||
var frame := -1
|
||||
var layer := -1
|
||||
if Global.animation_timeline.animation_timer.is_stopped() and project.selected_cels.size() == 1:
|
||||
|
@ -502,7 +642,7 @@ func commit_undo() -> void:
|
|||
|
||||
project.undos += 1
|
||||
project.undo_redo.create_action("Draw")
|
||||
Global.undo_redo_compress_images(redo_data, _undo_data, project)
|
||||
project.deserialize_cel_undo_data(redo_data, _undo_data)
|
||||
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false, frame, layer))
|
||||
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true, frame, layer))
|
||||
project.undo_redo.commit_action()
|
||||
|
@ -512,14 +652,13 @@ func commit_undo() -> void:
|
|||
func _get_undo_data() -> Dictionary:
|
||||
var data := {}
|
||||
if Global.animation_timeline.animation_timer.is_stopped():
|
||||
var images := _get_selected_draw_images()
|
||||
for image in images:
|
||||
data[image] = image.data
|
||||
Global.current_project.serialize_cel_undo_data(_get_selected_draw_cels(), data)
|
||||
else:
|
||||
var cels: Array[BaseCel]
|
||||
for frame in Global.current_project.frames:
|
||||
var cel := frame.cels[Global.current_project.current_layer]
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
var image := cel.get_image()
|
||||
data[image] = image.data
|
||||
cels.append(cel)
|
||||
Global.current_project.serialize_cel_undo_data(cels, data)
|
||||
return data
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
[gd_scene load_steps=7 format=3 uid="uid://bbvvkrrjyxugo"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/ValueSlider.tscn" id="1"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/Sliders/ValueSlider.tscn" id="1"]
|
||||
[ext_resource type="PackedScene" uid="uid://ctfgfelg0sho8" path="res://src/Tools/BaseTool.tscn" id="2"]
|
||||
[ext_resource type="Script" path="res://src/Tools/DesignTools/Bucket.gd" id="3"]
|
||||
[ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="4_1pngp"]
|
||||
[ext_resource type="Script" path="res://src/UI/Nodes/Sliders/ValueSlider.gd" id="4_1pngp"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="2"]
|
||||
bg_color = Color(1, 1, 1, 1)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends "res://src/Tools/BaseDraw.gd"
|
||||
extends BaseDrawTool
|
||||
|
||||
var _curve := Curve2D.new() ## The [Curve2D] responsible for the shape of the curve being drawn.
|
||||
var _drawing := false ## Set to true when a curve is being drawn.
|
||||
|
@ -128,7 +128,6 @@ func draw_end(pos: Vector2i) -> void:
|
|||
func draw_preview() -> void:
|
||||
var previews := Global.canvas.previews_sprite
|
||||
if not _drawing:
|
||||
previews.texture = null
|
||||
return
|
||||
var points := _bezier()
|
||||
var image := Image.create(
|
||||
|
@ -141,18 +140,9 @@ func draw_preview() -> void:
|
|||
image.set_pixelv(points[i], Color.WHITE)
|
||||
|
||||
# Handle mirroring
|
||||
if Tools.horizontal_mirror:
|
||||
for point in mirror_array(points, true, false):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
if Tools.vertical_mirror:
|
||||
for point in mirror_array(points, true, true):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
if Tools.vertical_mirror:
|
||||
for point in mirror_array(points, false, true):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
for point in mirror_array(points):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
var texture := ImageTexture.create_from_image(image)
|
||||
previews.texture = texture
|
||||
|
||||
|
@ -203,16 +193,20 @@ func _draw_shape() -> void:
|
|||
commit_undo()
|
||||
|
||||
|
||||
func _draw_pixel(point: Vector2i, images: Array[Image]) -> void:
|
||||
if Global.current_project.can_pixel_get_drawn(point):
|
||||
for image in images:
|
||||
_drawer.set_pixel(image, point, tool_slot.color)
|
||||
func _draw_pixel(point: Vector2i, images: Array[ImageExtended]) -> void:
|
||||
if Tools.is_placing_tiles():
|
||||
draw_tile(point)
|
||||
else:
|
||||
if Global.current_project.can_pixel_get_drawn(point):
|
||||
for image in images:
|
||||
_drawer.set_pixel(image, point, tool_slot.color)
|
||||
|
||||
|
||||
func _clear() -> void:
|
||||
_curve.clear_points()
|
||||
_fill_inside_rect = Rect2i()
|
||||
_drawing = false
|
||||
Global.canvas.previews_sprite.texture = null
|
||||
_editing_out_control_point = false
|
||||
Global.canvas.previews.queue_redraw()
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
[ext_resource type="PackedScene" uid="uid://ubyatap3sylf" path="res://src/Tools/BaseDraw.tscn" id="1_rvuea"]
|
||||
[ext_resource type="Script" path="res://src/Tools/DesignTools/CurveTool.gd" id="2_tjnp6"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/ValueSlider.tscn" id="3_g0nav"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/Sliders/ValueSlider.tscn" id="3_g0nav"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_drx24"]
|
||||
resource_name = "rotate"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends "res://src/Tools/BaseDraw.gd"
|
||||
extends BaseDrawTool
|
||||
|
||||
var _last_position := Vector2.INF
|
||||
var _clear_image: Image
|
||||
|
@ -19,6 +19,7 @@ class EraseOp:
|
|||
|
||||
func _init() -> void:
|
||||
_drawer.color_op = EraseOp.new()
|
||||
_is_eraser = true
|
||||
_clear_image = Image.create(1, 1, false, Image.FORMAT_RGBA8)
|
||||
_clear_image.fill(Color(0, 0, 0, 0))
|
||||
|
||||
|
@ -42,13 +43,11 @@ func draw_start(pos: Vector2i) -> void:
|
|||
_pick_color(pos)
|
||||
return
|
||||
_picking_color = false
|
||||
|
||||
Global.canvas.selection.transform_content_confirm()
|
||||
prepare_undo("Draw")
|
||||
update_mask(_strength == 1)
|
||||
_changed = false
|
||||
_drawer.color_op.changed = false
|
||||
|
||||
prepare_undo("Draw")
|
||||
_drawer.reset()
|
||||
|
||||
_draw_line = Input.is_action_pressed("draw_create_line")
|
||||
|
@ -122,6 +121,7 @@ func _draw_brush_image(image: Image, src_rect: Rect2i, dst: Vector2i) -> void:
|
|||
var images := _get_selected_draw_images()
|
||||
for draw_image in images:
|
||||
draw_image.blit_rect_mask(_clear_image, image, src_rect, dst)
|
||||
draw_image.convert_rgb_to_indexed()
|
||||
else:
|
||||
for xx in image.get_size().x:
|
||||
for yy in image.get_size().y:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
[ext_resource type="PackedScene" uid="uid://ubyatap3sylf" path="res://src/Tools/BaseDraw.tscn" id="1"]
|
||||
[ext_resource type="Script" path="res://src/Tools/DesignTools/Eraser.gd" id="2"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/ValueSlider.tscn" id="3"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/Sliders/ValueSlider.tscn" id="3"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_7k1sb"]
|
||||
resource_name = "rotate"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends "res://src/Tools/BaseDraw.gd"
|
||||
extends BaseDrawTool
|
||||
|
||||
var _original_pos := Vector2i.ZERO
|
||||
var _start := Vector2i.ZERO
|
||||
|
@ -120,8 +120,8 @@ func draw_move(pos: Vector2i) -> void:
|
|||
|
||||
func draw_end(pos: Vector2i) -> void:
|
||||
pos = snap_position(pos)
|
||||
super.draw_end(pos)
|
||||
if _picking_color:
|
||||
super.draw_end(pos)
|
||||
return
|
||||
|
||||
if _drawing:
|
||||
|
@ -142,8 +142,10 @@ func draw_end(pos: Vector2i) -> void:
|
|||
_start = Vector2.ZERO
|
||||
_dest = Vector2.ZERO
|
||||
_drawing = false
|
||||
Global.canvas.previews_sprite.texture = null
|
||||
_displace_origin = false
|
||||
cursor_text = ""
|
||||
super.draw_end(pos)
|
||||
|
||||
|
||||
func draw_preview() -> void:
|
||||
|
@ -157,22 +159,11 @@ func draw_preview() -> void:
|
|||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
# Handle mirroring
|
||||
if Tools.horizontal_mirror:
|
||||
for point in mirror_array(points, true, false):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
if Tools.vertical_mirror:
|
||||
for point in mirror_array(points, true, true):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
if Tools.vertical_mirror:
|
||||
for point in mirror_array(points, false, true):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
for point in mirror_array(points):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
var texture := ImageTexture.create_from_image(image)
|
||||
canvas.texture = texture
|
||||
else:
|
||||
canvas.texture = null
|
||||
|
||||
|
||||
func _draw_shape() -> void:
|
||||
|
@ -182,10 +173,13 @@ func _draw_shape() -> void:
|
|||
for point in points:
|
||||
# Reset drawer every time because pixel perfect sometimes breaks the tool
|
||||
_drawer.reset()
|
||||
# Draw each point offsetted based on the shape's thickness
|
||||
if Global.current_project.can_pixel_get_drawn(point):
|
||||
for image in images:
|
||||
_drawer.set_pixel(image, point, tool_slot.color)
|
||||
if Tools.is_placing_tiles():
|
||||
draw_tile(point)
|
||||
else:
|
||||
# Draw each point offsetted based on the shape's thickness
|
||||
if Global.current_project.can_pixel_get_drawn(point):
|
||||
for image in images:
|
||||
_drawer.set_pixel(image, point, tool_slot.color)
|
||||
|
||||
commit_undo()
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
[ext_resource type="PackedScene" uid="uid://ubyatap3sylf" path="res://src/Tools/BaseDraw.tscn" id="1"]
|
||||
[ext_resource type="Script" path="res://src/Tools/DesignTools/LineTool.gd" id="2"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/ValueSlider.tscn" id="3"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/Sliders/ValueSlider.tscn" id="3"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_o5212"]
|
||||
resource_name = "rotate"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends "res://src/Tools/BaseDraw.gd"
|
||||
extends BaseDrawTool
|
||||
|
||||
var _prev_mode := false
|
||||
var _last_position := Vector2i(Vector2.INF)
|
||||
|
@ -103,6 +103,7 @@ func draw_start(pos: Vector2i) -> void:
|
|||
_picking_color = false
|
||||
|
||||
Global.canvas.selection.transform_content_confirm()
|
||||
prepare_undo("Draw")
|
||||
var can_skip_mask := true
|
||||
if tool_slot.color.a < 1 and !_overwrite:
|
||||
can_skip_mask = false
|
||||
|
@ -112,7 +113,6 @@ func draw_start(pos: Vector2i) -> void:
|
|||
_drawer.color_op.overwrite = _overwrite
|
||||
_draw_points = []
|
||||
|
||||
prepare_undo("Draw")
|
||||
_drawer.reset()
|
||||
|
||||
_draw_line = Input.is_action_pressed("draw_create_line")
|
||||
|
@ -211,6 +211,7 @@ func _draw_brush_image(brush_image: Image, src_rect: Rect2i, dst: Vector2i) -> v
|
|||
draw_image.blit_rect_mask(brush_image, mask, src_rect, dst)
|
||||
else:
|
||||
draw_image.blit_rect(brush_image, src_rect, dst)
|
||||
draw_image.convert_rgb_to_indexed()
|
||||
else:
|
||||
for draw_image in images:
|
||||
if Tools.alpha_locked:
|
||||
|
@ -218,3 +219,4 @@ func _draw_brush_image(brush_image: Image, src_rect: Rect2i, dst: Vector2i) -> v
|
|||
draw_image.blend_rect_mask(brush_image, mask, src_rect, dst)
|
||||
else:
|
||||
draw_image.blend_rect(brush_image, src_rect, dst)
|
||||
draw_image.convert_rgb_to_indexed()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[gd_scene load_steps=5 format=3 uid="uid://cul5mpy17ebfl"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://ubyatap3sylf" path="res://src/Tools/BaseDraw.tscn" id="1"]
|
||||
[ext_resource type="PackedScene" path="res://src/UI/Nodes/ValueSliderV2.tscn" id="2"]
|
||||
[ext_resource type="PackedScene" path="res://src/UI/Nodes/Sliders/ValueSliderV2.tscn" id="2"]
|
||||
[ext_resource type="Script" path="res://src/Tools/DesignTools/Pencil.gd" id="3"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_e3rs3"]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends "res://src/Tools/BaseDraw.gd"
|
||||
extends BaseDrawTool
|
||||
|
||||
enum ShadingMode { SIMPLE, HUE_SHIFTING, COLOR_REPLACE }
|
||||
enum LightenDarken { LIGHTEN, DARKEN }
|
||||
|
@ -324,8 +324,8 @@ func update_brush() -> void:
|
|||
$ColorInterpolation.visible = false
|
||||
|
||||
|
||||
## this function is also used by a signal, this is why there is _color = Color.TRANSPARENT in here.
|
||||
func _refresh_colors_array(_color = Color.TRANSPARENT, mouse_button := tool_slot.button) -> void:
|
||||
## this function is also used by a signal, this is why there is _color_info = {} in here.
|
||||
func _refresh_colors_array(_color_info = {}, mouse_button := tool_slot.button) -> void:
|
||||
if mouse_button != tool_slot.button:
|
||||
return
|
||||
if _shading_mode == ShadingMode.COLOR_REPLACE:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
[ext_resource type="PackedScene" uid="uid://ubyatap3sylf" path="res://src/Tools/BaseDraw.tscn" id="1"]
|
||||
[ext_resource type="Script" path="res://src/Tools/DesignTools/Shading.gd" id="2"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/ValueSlider.tscn" id="3"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/Sliders/ValueSlider.tscn" id="3"]
|
||||
|
||||
[sub_resource type="ButtonGroup" id="ButtonGroup_lvcwb"]
|
||||
resource_name = "rotate"
|
||||
|
|
|
@ -32,23 +32,46 @@ func apply_selection(pos: Vector2i) -> void:
|
|||
if pos.x > project.size.x - 1 or pos.y > project.size.y - 1:
|
||||
return
|
||||
|
||||
var cel_image := Image.new()
|
||||
cel_image.copy_from(_get_draw_image())
|
||||
var color := cel_image.get_pixelv(pos)
|
||||
var operation := 0
|
||||
if _subtract:
|
||||
operation = 1
|
||||
elif _intersect:
|
||||
operation = 2
|
||||
|
||||
var params := {"color": color, "tolerance": _tolerance, "operation": operation}
|
||||
if _add or _subtract or _intersect:
|
||||
var selection_tex := ImageTexture.create_from_image(project.selection_map)
|
||||
params["selection"] = selection_tex
|
||||
var gen := ShaderImageEffect.new()
|
||||
gen.generate_image(cel_image, shader, params, project.size)
|
||||
cel_image.convert(Image.FORMAT_LA8)
|
||||
if Tools.is_placing_tiles():
|
||||
var prev_selection_map := SelectionMap.new() # Used for intersect
|
||||
prev_selection_map.copy_from(project.selection_map)
|
||||
if !_add and !_subtract and !_intersect:
|
||||
Global.canvas.selection.clear_selection()
|
||||
if _intersect:
|
||||
project.selection_map.clear()
|
||||
for cel in _get_selected_draw_cels():
|
||||
if cel is not CelTileMap:
|
||||
continue
|
||||
var tilemap_cel := cel as CelTileMap
|
||||
var tile_index := tilemap_cel.get_cell_index_at_coords(pos)
|
||||
for i in tilemap_cel.cells.size():
|
||||
var cell := tilemap_cel.cells[i]
|
||||
if cell.index == tile_index:
|
||||
if _intersect:
|
||||
var p := (cel as CelTileMap).get_cell_coords_in_image(i)
|
||||
select_tilemap_cell(
|
||||
cel, i, project.selection_map, prev_selection_map.is_pixel_selected(p)
|
||||
)
|
||||
else:
|
||||
select_tilemap_cell(cel, i, project.selection_map, !_subtract)
|
||||
else:
|
||||
var cel_image := Image.new()
|
||||
cel_image.copy_from(_get_draw_image())
|
||||
var color := cel_image.get_pixelv(pos)
|
||||
var params := {"color": color, "tolerance": _tolerance, "operation": operation}
|
||||
if _add or _subtract or _intersect:
|
||||
var selection_tex := ImageTexture.create_from_image(project.selection_map)
|
||||
params["selection"] = selection_tex
|
||||
var gen := ShaderImageEffect.new()
|
||||
gen.generate_image(cel_image, shader, params, project.size)
|
||||
cel_image.convert(Image.FORMAT_LA8)
|
||||
|
||||
project.selection_map.copy_from(cel_image)
|
||||
project.selection_map.copy_from(cel_image)
|
||||
Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect()
|
||||
Global.canvas.selection.commit_undo("Select", undo_data)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
[ext_resource type="PackedScene" uid="uid://bd62qfjn380wf" path="res://src/Tools/BaseSelectionTool.tscn" id="1"]
|
||||
[ext_resource type="Script" path="res://src/Tools/SelectionTools/ColorSelect.gd" id="2"]
|
||||
[ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="3_44rxy"]
|
||||
[ext_resource type="Script" path="res://src/UI/Nodes/Sliders/ValueSlider.gd" id="3_44rxy"]
|
||||
|
||||
[node name="ToolOptions" instance=ExtResource("1")]
|
||||
script = ExtResource("2")
|
||||
|
|
|
@ -63,22 +63,11 @@ func draw_preview() -> void:
|
|||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(points[i]):
|
||||
image.set_pixelv(points[i], Color.WHITE)
|
||||
# Handle mirroring
|
||||
if Tools.horizontal_mirror:
|
||||
for point in mirror_array(points, true, false):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
if Tools.vertical_mirror:
|
||||
for point in mirror_array(points, true, true):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
if Tools.vertical_mirror:
|
||||
for point in mirror_array(points, false, true):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
for point in mirror_array(points):
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
||||
image.set_pixelv(point, Color.WHITE)
|
||||
var texture := ImageTexture.create_from_image(image)
|
||||
canvas.texture = texture
|
||||
else:
|
||||
canvas.texture = null
|
||||
|
||||
|
||||
func apply_selection(_position: Vector2i) -> void:
|
||||
|
@ -90,18 +79,37 @@ func apply_selection(_position: Vector2i) -> void:
|
|||
Global.canvas.selection.commit_undo("Select", undo_data)
|
||||
if _rect.size == Vector2i.ZERO:
|
||||
return
|
||||
set_ellipse(project.selection_map, _rect.position)
|
||||
# Handle mirroring
|
||||
var mirror_positions := Tools.get_mirrored_positions(_rect.position, project, 1)
|
||||
var mirror_ends := Tools.get_mirrored_positions(_rect.end, project, 1)
|
||||
for i in mirror_positions.size():
|
||||
var mirror_rect := Rect2i()
|
||||
mirror_rect.position = mirror_positions[i]
|
||||
mirror_rect.end = mirror_ends[i]
|
||||
set_ellipse(project.selection_map, mirror_rect.abs().position)
|
||||
if Tools.is_placing_tiles():
|
||||
var operation := 0
|
||||
if _subtract:
|
||||
operation = 1
|
||||
elif _intersect:
|
||||
operation = 2
|
||||
Global.canvas.selection.select_rect(_rect, operation)
|
||||
# Handle mirroring
|
||||
var mirror_positions := Tools.get_mirrored_positions(_rect.position, project, 1)
|
||||
var mirror_ends := Tools.get_mirrored_positions(_rect.end, project, 1)
|
||||
for i in mirror_positions.size():
|
||||
var mirror_rect := Rect2i()
|
||||
mirror_rect.position = mirror_positions[i]
|
||||
mirror_rect.end = mirror_ends[i]
|
||||
Global.canvas.selection.select_rect(mirror_rect.abs(), operation)
|
||||
|
||||
Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect()
|
||||
Global.canvas.selection.commit_undo("Select", undo_data)
|
||||
Global.canvas.selection.commit_undo("Select", undo_data)
|
||||
else:
|
||||
set_ellipse(project.selection_map, _rect.position)
|
||||
# Handle mirroring
|
||||
var mirror_positions := Tools.get_mirrored_positions(_rect.position, project, 1)
|
||||
var mirror_ends := Tools.get_mirrored_positions(_rect.end, project, 1)
|
||||
for i in mirror_positions.size():
|
||||
var mirror_rect := Rect2i()
|
||||
mirror_rect.position = mirror_positions[i]
|
||||
mirror_rect.end = mirror_ends[i]
|
||||
set_ellipse(project.selection_map, mirror_rect.abs().position)
|
||||
|
||||
Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect()
|
||||
Global.canvas.selection.commit_undo("Select", undo_data)
|
||||
Global.canvas.previews_sprite.texture = null
|
||||
|
||||
|
||||
func set_ellipse(selection_map: SelectionMap, pos: Vector2i) -> void:
|
||||
|
@ -125,8 +133,12 @@ func set_ellipse(selection_map: SelectionMap, pos: Vector2i) -> void:
|
|||
# Given an origin point and destination point, returns a rect representing
|
||||
# where the shape will be drawn and what is its size
|
||||
func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i:
|
||||
if Tools.is_placing_tiles():
|
||||
var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset
|
||||
var grid_size := tileset.tile_size
|
||||
origin = Tools.snap_to_rectangular_grid_boundary(origin, grid_size)
|
||||
dest = Tools.snap_to_rectangular_grid_boundary(dest, grid_size)
|
||||
var rect := Rect2i()
|
||||
|
||||
# Center the rect on the mouse
|
||||
if _expand_from_center:
|
||||
var new_size := dest - origin
|
||||
|
@ -149,6 +161,7 @@ func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i:
|
|||
rect.position = Vector2i(mini(origin.x, dest.x), mini(origin.y, dest.y))
|
||||
rect.size = (origin - dest).abs()
|
||||
|
||||
rect.size += Vector2i.ONE
|
||||
if not Tools.is_placing_tiles():
|
||||
rect.size += Vector2i.ONE
|
||||
|
||||
return rect
|
||||
|
|
|
@ -46,31 +46,14 @@ func draw_preview() -> void:
|
|||
image.set_pixelv(draw_point, Color.WHITE)
|
||||
|
||||
# Handle mirroring
|
||||
if Tools.horizontal_mirror:
|
||||
for point in mirror_array(_draw_points, true, false):
|
||||
var draw_point := point
|
||||
if Global.mirror_view: # This fixes previewing in mirror mode
|
||||
draw_point.x = image.get_width() - draw_point.x - 1
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(draw_point):
|
||||
image.set_pixelv(draw_point, Color.WHITE)
|
||||
if Tools.vertical_mirror:
|
||||
for point in mirror_array(_draw_points, true, true):
|
||||
var draw_point := point
|
||||
if Global.mirror_view: # This fixes previewing in mirror mode
|
||||
draw_point.x = image.get_width() - draw_point.x - 1
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(draw_point):
|
||||
image.set_pixelv(draw_point, Color.WHITE)
|
||||
if Tools.vertical_mirror:
|
||||
for point in mirror_array(_draw_points, false, true):
|
||||
var draw_point := point
|
||||
if Global.mirror_view: # This fixes previewing in mirror mode
|
||||
draw_point.x = image.get_width() - draw_point.x - 1
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(draw_point):
|
||||
image.set_pixelv(draw_point, Color.WHITE)
|
||||
for point in mirror_array(_draw_points):
|
||||
var draw_point := point
|
||||
if Global.mirror_view: # This fixes previewing in mirror mode
|
||||
draw_point.x = image.get_width() - draw_point.x - 1
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(draw_point):
|
||||
image.set_pixelv(draw_point, Color.WHITE)
|
||||
var texture := ImageTexture.create_from_image(image)
|
||||
canvas.texture = texture
|
||||
else:
|
||||
canvas.texture = null
|
||||
|
||||
|
||||
func apply_selection(_position) -> void:
|
||||
|
@ -85,19 +68,10 @@ func apply_selection(_position) -> void:
|
|||
if _draw_points.size() > 3:
|
||||
if _intersect:
|
||||
project.selection_map.clear()
|
||||
lasso_selection(project.selection_map, previous_selection_map, _draw_points)
|
||||
|
||||
lasso_selection(_draw_points, project, previous_selection_map)
|
||||
# Handle mirroring
|
||||
if Tools.horizontal_mirror:
|
||||
var mirror_x := mirror_array(_draw_points, true, false)
|
||||
lasso_selection(project.selection_map, previous_selection_map, mirror_x)
|
||||
if Tools.vertical_mirror:
|
||||
var mirror_xy := mirror_array(_draw_points, true, true)
|
||||
lasso_selection(project.selection_map, previous_selection_map, mirror_xy)
|
||||
if Tools.vertical_mirror:
|
||||
var mirror_y := mirror_array(_draw_points, false, true)
|
||||
lasso_selection(project.selection_map, previous_selection_map, mirror_y)
|
||||
|
||||
var callable := lasso_selection.bind(project, previous_selection_map)
|
||||
mirror_array(_draw_points, callable)
|
||||
Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect()
|
||||
else:
|
||||
if !cleared:
|
||||
|
@ -106,11 +80,13 @@ func apply_selection(_position) -> void:
|
|||
Global.canvas.selection.commit_undo("Select", undo_data)
|
||||
_draw_points.clear()
|
||||
_last_position = Vector2.INF
|
||||
Global.canvas.previews_sprite.texture = null
|
||||
|
||||
|
||||
func lasso_selection(
|
||||
selection_map: SelectionMap, previous_selection_map: SelectionMap, points: Array[Vector2i]
|
||||
points: Array[Vector2i], project: Project, previous_selection_map: SelectionMap
|
||||
) -> void:
|
||||
var selection_map := project.selection_map
|
||||
var selection_size := selection_map.get_size()
|
||||
var bounding_rect := Rect2i(points[0], Vector2i.ZERO)
|
||||
for point in points:
|
||||
|
@ -119,9 +95,9 @@ func lasso_selection(
|
|||
bounding_rect = bounding_rect.expand(point)
|
||||
if _intersect:
|
||||
if previous_selection_map.is_pixel_selected(point):
|
||||
selection_map.select_pixel(point, true)
|
||||
select_pixel(point, project, true)
|
||||
else:
|
||||
selection_map.select_pixel(point, !_subtract)
|
||||
select_pixel(point, project, !_subtract)
|
||||
|
||||
var v := Vector2i()
|
||||
for x in bounding_rect.size.x:
|
||||
|
@ -131,9 +107,17 @@ func lasso_selection(
|
|||
if Geometry2D.is_point_in_polygon(v, points):
|
||||
if _intersect:
|
||||
if previous_selection_map.is_pixel_selected(v):
|
||||
selection_map.select_pixel(v, true)
|
||||
select_pixel(v, project, true)
|
||||
else:
|
||||
selection_map.select_pixel(v, !_subtract)
|
||||
select_pixel(v, project, !_subtract)
|
||||
|
||||
|
||||
func select_pixel(point: Vector2i, project: Project, select: bool) -> void:
|
||||
if Tools.is_placing_tiles():
|
||||
var tilemap := project.get_current_cel() as CelTileMap
|
||||
var cell_position := tilemap.get_cell_position(point)
|
||||
select_tilemap_cell(tilemap, cell_position, project.selection_map, select)
|
||||
project.selection_map.select_pixel(point, select)
|
||||
|
||||
|
||||
# Bresenham's Algorithm
|
||||
|
|
|
@ -34,10 +34,10 @@ func apply_selection(pos: Vector2i) -> void:
|
|||
|
||||
var cel_image := Image.new()
|
||||
cel_image.copy_from(_get_draw_image())
|
||||
_flood_fill(pos, cel_image, project.selection_map, previous_selection_map)
|
||||
_flood_fill(pos, cel_image, project, previous_selection_map)
|
||||
# Handle mirroring
|
||||
for mirror_pos in Tools.get_mirrored_positions(pos):
|
||||
_flood_fill(mirror_pos, cel_image, project.selection_map, previous_selection_map)
|
||||
_flood_fill(mirror_pos, cel_image, project, previous_selection_map)
|
||||
|
||||
Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect()
|
||||
Global.canvas.selection.commit_undo("Select", undo_data)
|
||||
|
@ -59,6 +59,39 @@ func update_config() -> void:
|
|||
$ToleranceSlider.value = _tolerance * 255.0
|
||||
|
||||
|
||||
func _on_tolerance_slider_value_changed(value: float) -> void:
|
||||
_tolerance = value / 255.0
|
||||
update_config()
|
||||
save_config()
|
||||
|
||||
|
||||
func _flood_fill(
|
||||
pos: Vector2i, image: Image, project: Project, previous_selection_map: SelectionMap
|
||||
) -> void:
|
||||
# implements the floodfill routine by Shawn Hargreaves
|
||||
# from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c
|
||||
var selection_map := project.selection_map
|
||||
if Tools.is_placing_tiles():
|
||||
for cel in _get_selected_draw_cels():
|
||||
if cel is not CelTileMap:
|
||||
continue
|
||||
var tile_index := (cel as CelTileMap).get_cell_index_at_coords(pos)
|
||||
# init flood data structures
|
||||
_allegro_flood_segments = []
|
||||
_allegro_image_segments = []
|
||||
_compute_segments_for_tilemap(pos, cel, tile_index)
|
||||
_select_segments_tilemap(project, previous_selection_map)
|
||||
return
|
||||
var color := image.get_pixelv(pos)
|
||||
# init flood data structures
|
||||
_allegro_flood_segments = []
|
||||
_allegro_image_segments = []
|
||||
_compute_segments_for_image(pos, project, image, color)
|
||||
# now actually color the image: since we have already checked a few things for the points
|
||||
# we'll process here, we're going to skip a bunch of safety checks to speed things up.
|
||||
_select_segments(selection_map, previous_selection_map)
|
||||
|
||||
|
||||
# Add a new segment to the array
|
||||
func _add_new_segment(y := 0) -> void:
|
||||
_allegro_flood_segments.append(Segment.new(y))
|
||||
|
@ -140,22 +173,6 @@ func _check_flooded_segment(
|
|||
return ret
|
||||
|
||||
|
||||
func _flood_fill(
|
||||
pos: Vector2i, image: Image, selection_map: SelectionMap, previous_selection_map: SelectionMap
|
||||
) -> void:
|
||||
# implements the floodfill routine by Shawn Hargreaves
|
||||
# from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c
|
||||
var project := Global.current_project
|
||||
var color := image.get_pixelv(pos)
|
||||
# init flood data structures
|
||||
_allegro_flood_segments = []
|
||||
_allegro_image_segments = []
|
||||
_compute_segments_for_image(pos, project, image, color)
|
||||
# now actually color the image: since we have already checked a few things for the points
|
||||
# we'll process here, we're going to skip a bunch of safety checks to speed things up.
|
||||
_select_segments(selection_map, previous_selection_map)
|
||||
|
||||
|
||||
func _compute_segments_for_image(
|
||||
pos: Vector2i, project: Project, image: Image, src_color: Color
|
||||
) -> void:
|
||||
|
@ -201,7 +218,128 @@ func _set_bit(p: Vector2i, selection_map: SelectionMap, prev_selection_map: Sele
|
|||
selection_map.select_pixel(p, !_subtract)
|
||||
|
||||
|
||||
func _on_tolerance_slider_value_changed(value: float) -> void:
|
||||
_tolerance = value / 255.0
|
||||
update_config()
|
||||
save_config()
|
||||
func _compute_segments_for_tilemap(pos: Vector2i, cel: CelTileMap, src_index: int) -> void:
|
||||
# initially allocate at least 1 segment per line of the tilemap
|
||||
for j in cel.vertical_cells:
|
||||
_add_new_segment(j)
|
||||
pos /= cel.tileset.tile_size
|
||||
# start flood algorithm
|
||||
_flood_line_around_point_tilemap(pos, cel, src_index)
|
||||
# test all segments while also discovering more
|
||||
var done := false
|
||||
while not done:
|
||||
done = true
|
||||
var max_index := _allegro_flood_segments.size()
|
||||
for c in max_index:
|
||||
var p := _allegro_flood_segments[c]
|
||||
if p.todo_below: # check below the segment?
|
||||
p.todo_below = false
|
||||
if _check_flooded_segment_tilemap(
|
||||
p.y + 1, p.left_position, p.right_position, cel, src_index
|
||||
):
|
||||
done = false
|
||||
if p.todo_above: # check above the segment?
|
||||
p.todo_above = false
|
||||
if _check_flooded_segment_tilemap(
|
||||
p.y - 1, p.left_position, p.right_position, cel, src_index
|
||||
):
|
||||
done = false
|
||||
|
||||
|
||||
## Fill an horizontal segment around the specified position, and adds it to the
|
||||
## list of segments filled. Returns the first x coordinate after the part of the
|
||||
## line that has been filled.
|
||||
## Τhis method is called by [method _flood_fill] after the required data structures
|
||||
## have been initialized.
|
||||
func _flood_line_around_point_tilemap(pos: Vector2i, cel: CelTileMap, src_index: int) -> int:
|
||||
if cel.get_cell_index_at_coords_in_tilemap_space(pos) != src_index:
|
||||
return pos.x + 1
|
||||
var west := pos
|
||||
var east := pos
|
||||
while west.x >= 0 && cel.get_cell_index_at_coords_in_tilemap_space(west) == src_index:
|
||||
west += Vector2i.LEFT
|
||||
while (
|
||||
east.x < cel.horizontal_cells
|
||||
&& cel.get_cell_index_at_coords_in_tilemap_space(east) == src_index
|
||||
):
|
||||
east += Vector2i.RIGHT
|
||||
# Make a note of the stuff we processed
|
||||
var c := pos.y
|
||||
var segment := _allegro_flood_segments[c]
|
||||
# we may have already processed some segments on this y coordinate
|
||||
if segment.flooding:
|
||||
while segment.next > 0:
|
||||
c = segment.next # index of next segment in this line of image
|
||||
segment = _allegro_flood_segments[c]
|
||||
# found last current segment on this line
|
||||
c = _allegro_flood_segments.size()
|
||||
segment.next = c
|
||||
_add_new_segment(pos.y)
|
||||
segment = _allegro_flood_segments[c]
|
||||
# set the values for the current segment
|
||||
segment.flooding = true
|
||||
segment.left_position = west.x + 1
|
||||
segment.right_position = east.x - 1
|
||||
segment.y = pos.y
|
||||
segment.next = 0
|
||||
# Should we process segments above or below this one?
|
||||
# when there is a selected area, the pixels above and below the one we started creating this
|
||||
# segment from may be outside it. It's easier to assume we should be checking for segments
|
||||
# above and below this one than to specifically check every single pixel in it, because that
|
||||
# test will be performed later anyway.
|
||||
# On the other hand, this test we described is the same `project.can_pixel_get_drawn` does if
|
||||
# there is no selection, so we don't need branching here.
|
||||
segment.todo_above = pos.y > 0
|
||||
segment.todo_below = pos.y < cel.vertical_cells - 1
|
||||
# this is an actual segment we should be coloring, so we add it to the results for the
|
||||
# current image
|
||||
if segment.right_position >= segment.left_position:
|
||||
_allegro_image_segments.append(segment)
|
||||
# we know the point just east of the segment is not part of a segment that should be
|
||||
# processed, else it would be part of this segment
|
||||
return east.x + 1
|
||||
|
||||
|
||||
func _check_flooded_segment_tilemap(
|
||||
y: int, left: int, right: int, cel: CelTileMap, src_index: int
|
||||
) -> bool:
|
||||
var ret := false
|
||||
var c := 0
|
||||
while left <= right:
|
||||
c = y
|
||||
while true:
|
||||
var segment := _allegro_flood_segments[c]
|
||||
if left >= segment.left_position and left <= segment.right_position:
|
||||
left = segment.right_position + 2
|
||||
break
|
||||
c = segment.next
|
||||
if c == 0: # couldn't find a valid segment, so we draw a new one
|
||||
left = _flood_line_around_point_tilemap(Vector2i(left, y), cel, src_index)
|
||||
ret = true
|
||||
break
|
||||
return ret
|
||||
|
||||
|
||||
func _select_segments_tilemap(project: Project, previous_selection_map: SelectionMap) -> void:
|
||||
# short circuit for flat colors
|
||||
for c in _allegro_image_segments.size():
|
||||
var p := _allegro_image_segments[c]
|
||||
for px in range(p.left_position, p.right_position + 1):
|
||||
# We don't have to check again whether the point being processed is within the bounds
|
||||
_set_bit_rect(Vector2i(px, p.y), project, previous_selection_map)
|
||||
|
||||
|
||||
func _set_bit_rect(p: Vector2i, project: Project, prev_selection_map: SelectionMap) -> void:
|
||||
var selection_map := project.selection_map
|
||||
var tilemap := project.get_current_cel() as CelTileMap
|
||||
var cell_position := tilemap.get_cell_position_in_tilemap_space(p)
|
||||
if _intersect:
|
||||
var image_coords := tilemap.get_cell_coords_in_image(cell_position)
|
||||
select_tilemap_cell(
|
||||
tilemap,
|
||||
cell_position,
|
||||
project.selection_map,
|
||||
prev_selection_map.is_pixel_selected(image_coords)
|
||||
)
|
||||
else:
|
||||
select_tilemap_cell(tilemap, cell_position, project.selection_map, !_subtract)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
[ext_resource type="PackedScene" uid="uid://bd62qfjn380wf" path="res://src/Tools/BaseSelectionTool.tscn" id="1"]
|
||||
[ext_resource type="Script" path="res://src/Tools/SelectionTools/MagicWand.gd" id="2"]
|
||||
[ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="3_4ed6a"]
|
||||
[ext_resource type="Script" path="res://src/UI/Nodes/Sliders/ValueSlider.gd" id="3_4ed6a"]
|
||||
|
||||
[node name="ToolOptions" instance=ExtResource("1")]
|
||||
script = ExtResource("2")
|
||||
|
|
|
@ -74,31 +74,14 @@ func draw_preview() -> void:
|
|||
image.set_pixelv(draw_point, Color.WHITE)
|
||||
|
||||
# Handle mirroring
|
||||
if Tools.horizontal_mirror:
|
||||
for point in mirror_array(_draw_points, true, false):
|
||||
var draw_point := point
|
||||
if Global.mirror_view: # This fixes previewing in mirror mode
|
||||
draw_point.x = image.get_width() - draw_point.x - 1
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(draw_point):
|
||||
image.set_pixelv(draw_point, Color.WHITE)
|
||||
if Tools.vertical_mirror:
|
||||
for point in mirror_array(_draw_points, true, true):
|
||||
var draw_point := point
|
||||
if Global.mirror_view: # This fixes previewing in mirror mode
|
||||
draw_point.x = image.get_width() - draw_point.x - 1
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(draw_point):
|
||||
image.set_pixelv(draw_point, Color.WHITE)
|
||||
if Tools.vertical_mirror:
|
||||
for point in mirror_array(_draw_points, false, true):
|
||||
var draw_point := point
|
||||
if Global.mirror_view: # This fixes previewing in mirror mode
|
||||
draw_point.x = image.get_width() - draw_point.x - 1
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(draw_point):
|
||||
image.set_pixelv(draw_point, Color.WHITE)
|
||||
for point in mirror_array(_draw_points):
|
||||
var draw_point := point
|
||||
if Global.mirror_view: # This fixes previewing in mirror mode
|
||||
draw_point.x = image.get_width() - draw_point.x - 1
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(draw_point):
|
||||
image.set_pixelv(draw_point, Color.WHITE)
|
||||
var texture := ImageTexture.create_from_image(image)
|
||||
canvas.texture = texture
|
||||
else:
|
||||
canvas.texture = null
|
||||
|
||||
|
||||
func apply_selection(pos: Vector2i) -> void:
|
||||
|
@ -114,19 +97,10 @@ func apply_selection(pos: Vector2i) -> void:
|
|||
if _draw_points.size() >= 1:
|
||||
if _intersect:
|
||||
project.selection_map.clear()
|
||||
paint_selection(project.selection_map, previous_selection_map, _draw_points)
|
||||
|
||||
paint_selection(project, previous_selection_map, _draw_points)
|
||||
# Handle mirroring
|
||||
if Tools.horizontal_mirror:
|
||||
var mirror_x := mirror_array(_draw_points, true, false)
|
||||
paint_selection(project.selection_map, previous_selection_map, mirror_x)
|
||||
if Tools.vertical_mirror:
|
||||
var mirror_xy := mirror_array(_draw_points, true, true)
|
||||
paint_selection(project.selection_map, previous_selection_map, mirror_xy)
|
||||
if Tools.vertical_mirror:
|
||||
var mirror_y := mirror_array(_draw_points, false, true)
|
||||
paint_selection(project.selection_map, previous_selection_map, mirror_y)
|
||||
|
||||
var mirror := mirror_array(_draw_points)
|
||||
paint_selection(project, previous_selection_map, mirror)
|
||||
Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect()
|
||||
else:
|
||||
if !cleared:
|
||||
|
@ -135,20 +109,30 @@ func apply_selection(pos: Vector2i) -> void:
|
|||
Global.canvas.selection.commit_undo("Select", undo_data)
|
||||
_draw_points.clear()
|
||||
_last_position = Vector2.INF
|
||||
Global.canvas.previews_sprite.texture = null
|
||||
|
||||
|
||||
func paint_selection(
|
||||
selection_map: SelectionMap, previous_selection_map: SelectionMap, points: Array[Vector2i]
|
||||
project: Project, previous_selection_map: SelectionMap, points: Array[Vector2i]
|
||||
) -> void:
|
||||
var selection_map := project.selection_map
|
||||
var selection_size := selection_map.get_size()
|
||||
for point in points:
|
||||
if point.x < 0 or point.y < 0 or point.x >= selection_size.x or point.y >= selection_size.y:
|
||||
continue
|
||||
if _intersect:
|
||||
if previous_selection_map.is_pixel_selected(point):
|
||||
selection_map.select_pixel(point, true)
|
||||
select_pixel(point, project, true)
|
||||
else:
|
||||
selection_map.select_pixel(point, !_subtract)
|
||||
select_pixel(point, project, !_subtract)
|
||||
|
||||
|
||||
func select_pixel(point: Vector2i, project: Project, select: bool) -> void:
|
||||
if Tools.is_placing_tiles():
|
||||
var tilemap := project.get_current_cel() as CelTileMap
|
||||
var cell_position := tilemap.get_cell_position(point)
|
||||
select_tilemap_cell(tilemap, cell_position, project.selection_map, select)
|
||||
project.selection_map.select_pixel(point, select)
|
||||
|
||||
|
||||
# Bresenham's Algorithm
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
[ext_resource type="PackedScene" uid="uid://bd62qfjn380wf" path="res://src/Tools/BaseSelectionTool.tscn" id="1"]
|
||||
[ext_resource type="Script" path="res://src/Tools/SelectionTools/PaintSelect.gd" id="2"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/ValueSlider.tscn" id="3"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/Sliders/ValueSlider.tscn" id="3"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="1"]
|
||||
bg_color = Color(1, 1, 1, 1)
|
||||
|
|
|
@ -81,31 +81,14 @@ func draw_preview() -> void:
|
|||
)
|
||||
|
||||
# Handle mirroring
|
||||
if Tools.horizontal_mirror:
|
||||
for point in mirror_array(preview_draw_points, true, false):
|
||||
var draw_point := point
|
||||
if Global.mirror_view: # This fixes previewing in mirror mode
|
||||
draw_point.x = image.get_width() - draw_point.x - 1
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(draw_point):
|
||||
image.set_pixelv(draw_point, Color.WHITE)
|
||||
if Tools.vertical_mirror:
|
||||
for point in mirror_array(preview_draw_points, true, true):
|
||||
var draw_point := point
|
||||
if Global.mirror_view: # This fixes previewing in mirror mode
|
||||
draw_point.x = image.get_width() - draw_point.x - 1
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(draw_point):
|
||||
image.set_pixelv(draw_point, Color.WHITE)
|
||||
if Tools.vertical_mirror:
|
||||
for point in mirror_array(preview_draw_points, false, true):
|
||||
var draw_point := point
|
||||
if Global.mirror_view: # This fixes previewing in mirror mode
|
||||
draw_point.x = image.get_width() - draw_point.x - 1
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(draw_point):
|
||||
image.set_pixelv(draw_point, Color.WHITE)
|
||||
for point in mirror_array(preview_draw_points):
|
||||
var draw_point := point
|
||||
if Global.mirror_view: # This fixes previewing in mirror mode
|
||||
draw_point.x = image.get_width() - draw_point.x - 1
|
||||
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(draw_point):
|
||||
image.set_pixelv(draw_point, Color.WHITE)
|
||||
var texture := ImageTexture.create_from_image(image)
|
||||
previews.texture = texture
|
||||
else:
|
||||
previews.texture = null
|
||||
|
||||
|
||||
func apply_selection(pos: Vector2i) -> void:
|
||||
|
@ -122,19 +105,10 @@ func apply_selection(pos: Vector2i) -> void:
|
|||
if _draw_points.size() > 3:
|
||||
if _intersect:
|
||||
project.selection_map.clear()
|
||||
lasso_selection(project.selection_map, previous_selection_map, _draw_points)
|
||||
|
||||
lasso_selection(_draw_points, project, previous_selection_map)
|
||||
# Handle mirroring
|
||||
if Tools.horizontal_mirror:
|
||||
var mirror_x := mirror_array(_draw_points, true, false)
|
||||
lasso_selection(project.selection_map, previous_selection_map, mirror_x)
|
||||
if Tools.vertical_mirror:
|
||||
var mirror_xy := mirror_array(_draw_points, true, true)
|
||||
lasso_selection(project.selection_map, previous_selection_map, mirror_xy)
|
||||
if Tools.vertical_mirror:
|
||||
var mirror_y := mirror_array(_draw_points, false, true)
|
||||
lasso_selection(project.selection_map, previous_selection_map, mirror_y)
|
||||
|
||||
var callable := lasso_selection.bind(project, previous_selection_map)
|
||||
mirror_array(_draw_points, callable)
|
||||
Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect()
|
||||
else:
|
||||
if !cleared:
|
||||
|
@ -146,14 +120,16 @@ func apply_selection(pos: Vector2i) -> void:
|
|||
|
||||
func _clear() -> void:
|
||||
_ongoing_selection = false
|
||||
Global.canvas.previews_sprite.texture = null
|
||||
_draw_points.clear()
|
||||
_ready_to_apply = false
|
||||
Global.canvas.previews.queue_redraw()
|
||||
|
||||
|
||||
func lasso_selection(
|
||||
selection_map: SelectionMap, previous_selection_map: SelectionMap, points: Array[Vector2i]
|
||||
points: Array[Vector2i], project: Project, previous_selection_map: SelectionMap
|
||||
) -> void:
|
||||
var selection_map := project.selection_map
|
||||
var selection_size := selection_map.get_size()
|
||||
var bounding_rect := Rect2i(points[0], Vector2i.ZERO)
|
||||
for point in points:
|
||||
|
@ -162,9 +138,9 @@ func lasso_selection(
|
|||
bounding_rect = bounding_rect.expand(point)
|
||||
if _intersect:
|
||||
if previous_selection_map.is_pixel_selected(point):
|
||||
selection_map.select_pixel(point, true)
|
||||
select_pixel(point, project, true)
|
||||
else:
|
||||
selection_map.select_pixel(point, !_subtract)
|
||||
select_pixel(point, project, !_subtract)
|
||||
|
||||
var v := Vector2i()
|
||||
for x in bounding_rect.size.x:
|
||||
|
@ -174,9 +150,17 @@ func lasso_selection(
|
|||
if Geometry2D.is_point_in_polygon(v, points):
|
||||
if _intersect:
|
||||
if previous_selection_map.is_pixel_selected(v):
|
||||
selection_map.select_pixel(v, true)
|
||||
select_pixel(v, project, true)
|
||||
else:
|
||||
selection_map.select_pixel(v, !_subtract)
|
||||
select_pixel(v, project, !_subtract)
|
||||
|
||||
|
||||
func select_pixel(point: Vector2i, project: Project, select: bool) -> void:
|
||||
if Tools.is_placing_tiles():
|
||||
var tilemap := project.get_current_cel() as CelTileMap
|
||||
var cell_position := tilemap.get_cell_position(point)
|
||||
select_tilemap_cell(tilemap, cell_position, project.selection_map, select)
|
||||
project.selection_map.select_pixel(point, select)
|
||||
|
||||
|
||||
# Bresenham's Algorithm
|
||||
|
|
|
@ -101,6 +101,11 @@ func apply_selection(pos: Vector2i) -> void:
|
|||
## Given an origin point and destination point, returns a rect representing
|
||||
## where the shape will be drawn and what is its size
|
||||
func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i:
|
||||
if Tools.is_placing_tiles():
|
||||
var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset
|
||||
var grid_size := tileset.tile_size
|
||||
origin = Tools.snap_to_rectangular_grid_boundary(origin, grid_size)
|
||||
dest = Tools.snap_to_rectangular_grid_boundary(dest, grid_size)
|
||||
var rect := Rect2i()
|
||||
|
||||
# Center the rect on the mouse
|
||||
|
@ -125,6 +130,7 @@ func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i:
|
|||
rect.position = Vector2i(mini(origin.x, dest.x), mini(origin.y, dest.y))
|
||||
rect.size = (origin - dest).abs()
|
||||
|
||||
rect.size += Vector2i.ONE
|
||||
if not Tools.is_placing_tiles():
|
||||
rect.size += Vector2i.ONE
|
||||
|
||||
return rect
|
||||
|
|
|
@ -63,26 +63,39 @@ func draw_end(pos: Vector2i) -> void:
|
|||
func _pick_color(pos: Vector2i) -> void:
|
||||
var project := Global.current_project
|
||||
pos = project.tiles.get_canon_position(pos)
|
||||
|
||||
if pos.x < 0 or pos.y < 0:
|
||||
return
|
||||
|
||||
var color := Color(0, 0, 0, 0)
|
||||
if Tools.is_placing_tiles():
|
||||
var cel := Global.current_project.get_current_cel() as CelTileMap
|
||||
Tools.selected_tile_index_changed.emit(cel.get_cell_index_at_coords(pos))
|
||||
return
|
||||
var image := Image.new()
|
||||
image.copy_from(_get_draw_image())
|
||||
if pos.x > image.get_width() - 1 or pos.y > image.get_height() - 1:
|
||||
return
|
||||
|
||||
var color := Color(0, 0, 0, 0)
|
||||
var palette_index = -1
|
||||
match _mode:
|
||||
TOP_COLOR:
|
||||
var curr_frame := project.frames[project.current_frame]
|
||||
for layer in project.layers.size():
|
||||
var idx := (project.layers.size() - 1) - layer
|
||||
if project.layers[idx].is_visible_in_hierarchy():
|
||||
image = curr_frame.cels[idx].get_image()
|
||||
var cel := curr_frame.cels[idx]
|
||||
image = cel.get_image()
|
||||
color = image.get_pixelv(pos)
|
||||
# If image is indexed then get index as well
|
||||
if cel is PixelCel:
|
||||
if cel.image.is_indexed:
|
||||
palette_index = cel.image.indices_image.get_pixel(pos.x, pos.y).r8 - 1
|
||||
if not is_zero_approx(color.a):
|
||||
break
|
||||
CURRENT_LAYER:
|
||||
color = image.get_pixelv(pos)
|
||||
var current_cel = Global.current_project.get_current_cel()
|
||||
if current_cel is PixelCel:
|
||||
if current_cel.image.is_indexed:
|
||||
palette_index = current_cel.image.index_image.get_pixel(pos.x, pos.y).r8 - 1
|
||||
var button := MOUSE_BUTTON_LEFT if _color_slot == 0 else MOUSE_BUTTON_RIGHT
|
||||
Tools.assign_color(color, button, false)
|
||||
Tools.assign_color(color, button, false, palette_index)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
[gd_scene load_steps=6 format=3 uid="uid://c35n21ii7onhe"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://ctfgfelg0sho8" path="res://src/Tools/BaseTool.tscn" id="1"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/ValueSlider.tscn" id="2"]
|
||||
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/Sliders/ValueSlider.tscn" id="2"]
|
||||
[ext_resource type="Script" path="res://src/Tools/UtilityTools/CropTool.gd" id="3"]
|
||||
[ext_resource type="Texture2D" uid="uid://bgrq56ndc4ydj" path="res://assets/graphics/misc/unlocked_size.png" id="4"]
|
||||
[ext_resource type="PackedScene" path="res://src/UI/Nodes/ValueSliderV2.tscn" id="5"]
|
||||
[ext_resource type="PackedScene" path="res://src/UI/Nodes/Sliders/ValueSliderV2.tscn" id="5"]
|
||||
|
||||
[node name="ToolOptions" instance=ExtResource("1")]
|
||||
script = ExtResource("3")
|
||||
|
|