1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-19 01:29:49 +00:00

Compare commits

...

84 commits

Author SHA1 Message Date
Emmanouil Papadeas aa36b36a74
Merge 6a0ad2e3db into ff5713ae91 2024-12-03 15:56:48 +00:00
Emmanouil Papadeas 6a0ad2e3db Add a grid_offset parameter for the rectangular grid snap methods 2024-12-03 17:55:36 +02:00
Emmanouil Papadeas 8bdad90c30 Add a _snap_to_rectangular_grid_boundary() method to BaseTool
And rename _snap_to_grid_center() to _snap_to_rectangular_grid_center()
2024-12-03 17:42:23 +02:00
Emmanouil Papadeas ad118a048b The bucket tool now works with draw tiles mode 2024-12-03 02:59:01 +02:00
Emmanouil Papadeas 9dbea81d0a Add more keyboard shortcuts 2024-12-03 00:40:02 +02:00
Emmanouil Papadeas 985fa5c9e5 Use the AutoInvertColors shader for when showing the tile mode indices 2024-12-03 00:33:18 +02:00
Emmanouil Papadeas 9e1bc5646e Support mirroring when using draw tiles mode 2024-12-03 00:31:34 +02:00
Emmanouil Papadeas 7b3474a58c Add support for the draw tile mode for the rest of the draw tools, except bucket
Also fixes issues with the draw tile mode with the pencil and eraser tools, such as leaving gaps if the mouse is moving fast, and support for spacing and fill inside tool options.
2024-12-02 23:42:08 +02:00
Emmanouil Papadeas f9bde9dcc3 Change tileset in a layer from the project properties 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 40f56c3b08 Update the default layouts to include the Tiles panel 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 1cd2159e28 Duplicate tilesets from the project properties 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas d881e9f605 Add tilesets in the project properties and a button to delete them
Can only delete tilesets that are not currently used by a tilemap layer
2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 6b6606005d Add get_text_info() in TileSetCustom 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 1336819760 Disable draw tiles mode when pressing one of the tile edit mode buttons 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas f5428952b2 Add rotate left and right buttons in the tiles panel instead of transpose 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas c4a5b3b380 Update Translations.pot 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas fdd3e613cb Add documentation for CelTileMap and rename update_tileset to update_tilemap 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas aa4ca7a422 Add documentation for Project and TileSetCustom 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 2fb4af09d5 Include all cels that share the same tileset in undo/redo if manual mode is enabled. 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 08de3cec45 Make drawing on multiple selected tilemap cels that share the same tileset and applied a layer effect to all cels of the layer work
This now creates an issue when manual mode is used and we undo. Other cels don't get updated, even though they were changed by manual mode.
2024-12-02 19:51:36 +02:00
Emmanouil Papadeas d35b78f013 Fix layer effect applying not updating the tilesets properly 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 6243d1dc3d Only resize cells on undo/redo when needed 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 99e8cfa602 Resizing should now work
Also fixes cel replacing
2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 8110442ca1 Revert 3f39dbf3 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 7b6f70e999 Almost make cel replacing work
Needs to fix image resizing in order for this to work properly with undo/redo.
2024-12-02 19:51:36 +02:00
Emmanouil Papadeas db73d40550 Merge layers into tilemap layers 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas de6784202e Layer/frame cloning works. 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 0997fa8536 Linked tilemap cels should now work 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas d2cfe72c16 Draw tiles on all selected draw cels
Not working properly yet
2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 815388f2fa Don't execute update_tileset is we are on draw tiles mode 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 9e86492bfe Continue with the undo/redo rewrite
Works everywhere except image resizing, replacing cels and merging layers
2024-12-02 19:51:36 +02:00
Emmanouil Papadeas b3a429466d Fixed bugs when placing a transformed tile over a non-transformed tile 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas d1bcab4bf9 Fix placing tiles not working when switching to indexed mode 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 29b281f7ba Format TileModeIndices 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 50b4a8428f Fix issues with transposed tiles 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 5425275e9c Some improvements to TileModeIndices 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 8ab71490cf Fix variable name shadowing in TopMenuContainer 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 13070b6244 Remove transformations from cells when using auto or stack mode 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 903ea5134a Make undo/redo store tilemap cell indices and tileset tiles
Fixes issues with cases 0.5 and 5 of auto mode, and should be a better system overall. Only works with BaseDraw tools, needs to be applied everywhere as well.
2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 8d1652dc09 Write some documentation in CelTIleMap
WIP
2024-12-02 19:51:36 +02:00
Emmanouil Papadeas e870679869 Rename some variables and methods in CelTileMap 2024-12-02 19:51:36 +02:00
Emmanouil Papadeas 6be273d098 Add documentation for LayerTileMap and TileSetCustom, along with a class description for CelTileMap 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas a10a0a9bda Experimental undo redo for tileset tiles 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas ad919a2a10 Fix manual mode when the tilemap is empty. 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 2ccb9dd6f7 Support tile transformation, no undo/redo yet 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 8be3a1a54f Add logic for checking if two tiles are equal with transformations applied to them
There is currently no exposed way to apply transformations to tiles.
2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 35f78cf02c Refactor CelTileMap to eventually support alternative tiles
Such as rotated and flipped tiles
2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 74a40be7f7 Add smart tileset importing 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 62882cb8b1 Load images as tilesets 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 5e5789752d Prevent users from editing tileset name and size if they choose to not create a new tileset 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 281c205290 Automatically hide and show the tiles panel when the current cel is a tilemap cel 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 020be20566 Preview tiles when using tools and draw tiles mode is enabled. 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas f410cf8917 Manual mode should update other cels that have the same tileset 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 6ac98e1bc6 Make manual tile editing mode automatically update all other image portions that have the same tile index 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas a617039967 Fix tileset panel updating when undoing and the wrong tilemap cel is selected 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas e84e9d46f3 Resize tileset buttons 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 5c90501293 Enable tile drawing mode when clicking on a tile button 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 6f3e3c8566 When a tilemap cel is selected, force the first grid to have the same size as the tile size 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 24af6573e6 Fix out of bounds issues when undoing/redoing when the place tiles mode is enabled 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 46ebb0930c Add a dialog when creating a tilemap layer to allow users to set the tileset's settings 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas fc8f21a436 Resize indices on project resize 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 9bd2eb7f0f Save and load to/from pxo files 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 64809c64d9 Prevent from setting tile indices out of bounds of the canvas 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas 6b95908ef3 Place tiles mode works with eraser and color picker tools 2024-12-02 19:51:35 +02:00
Emmanouil Papadeas c1028131a1 Undo/redo now removes tiles and re-indexes tilemap tiles 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas 3e2587e4ca Implement placing tiles for pencil tool
Still needs undo support
2024-12-02 19:48:51 +02:00
Emmanouil Papadeas 65895374ab Tileset panel UI improvements 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas 3f26e859dc Properly update the tileset when using any tool 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas 00cd47b94e Better tile buttons 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas 7f82be13ab Improve tileset panel UI updating logic 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas 1bb908638b Prevent from drawing on empty image portions on manual mode. 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas 2ecdf023b2 Make the manual mode work, kind of 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas 29262ff7da Properly implement the auto tile editing mode
Should work well now.
2024-12-02 19:48:51 +02:00
Emmanouil Papadeas d584807c14 Show tile indices when pressing Control 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas 488c023838 Add a way to show the indices of each tile, WIP 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas a4b33ad83c Don't delete tiles that have been added using the stack mode 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas cadd7c57f1 Change tile editing mode from the UI 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas 7893ae2531 Make manual mode work when the tileset is empty 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas df56989d5f Improve tileset editing logic 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas 8abf44d65a Support ImageExtended 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas b74c3149af Add a tileset panel
Code is a bit meh, needs to be written better.
2024-12-02 19:48:51 +02:00
Emmanouil Papadeas dab8c5bed5 Add tilemap layers 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas 85854b3490 Implement all draw modes (untested) 2024-12-02 19:48:51 +02:00
Emmanouil Papadeas d562e50af7 Initial work for tilemap layers 2024-12-02 19:48:51 +02:00
54 changed files with 2703 additions and 483 deletions

View file

@ -2239,6 +2239,10 @@ msgstr ""
msgid "Group" msgid "Group"
msgstr "" 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 "Layers" msgid "Layers"
msgstr "" msgstr ""
@ -2247,33 +2251,47 @@ msgid "Clipping mask"
msgstr "" msgstr ""
#. Hint tooltip of the create new layer button, found on the left side of the timeline. #. 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" msgid "Create a new layer"
msgstr "" msgstr ""
#. One of the options of the create new layer button. #. One of the options of the create new layer button.
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Add Pixel Layer" msgid "Add Pixel Layer"
msgstr "" msgstr ""
#. One of the options of the create new layer button. #. One of the options of the create new layer button.
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Add Group Layer" msgid "Add Group Layer"
msgstr "" msgstr ""
#. One of the options of the create new layer button. #. One of the options of the create new layer button.
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Add 3D Layer" msgid "Add 3D Layer"
msgstr "" msgstr ""
#. One of the options of the create new layer button.
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Add Tilemap Layer"
msgstr ""
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Remove current layer" msgid "Remove current layer"
msgstr "" msgstr ""
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Move up the current layer" msgid "Move up the current layer"
msgstr "" msgstr ""
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Move down the current layer" msgid "Move down the current layer"
msgstr "" msgstr ""
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Clone current layer" msgid "Clone current layer"
msgstr "" msgstr ""
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Merge current layer with the one below" msgid "Merge current layer with the one below"
msgstr "" msgstr ""
@ -2960,6 +2978,10 @@ msgstr ""
msgid "Recorder" msgid "Recorder"
msgstr "" 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" msgid "Crop"
msgstr "" msgstr ""
@ -3361,3 +3383,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. #. 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?" msgid "Do you want to download the image from %s?"
msgstr "" 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 ""

View 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

View 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

View 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

View 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

View file

@ -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_panel.gd" id="1_jxh43"]
[ext_resource type="Script" path="res://addons/dockable_container/layout_split.gd" id="2"] [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"] [ext_resource type="Script" path="res://addons/dockable_container/layout.gd" id="3_4h5wj"]
[sub_resource type="Resource" id="Resource_atmme"] [sub_resource type="Resource" id="Resource_atmme"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1") script = ExtResource("1_jxh43")
names = PackedStringArray("Tools") names = PackedStringArray("Tools")
current_tab = 0 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"] [sub_resource type="Resource" id="Resource_ouvfk"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1") script = ExtResource("1_jxh43")
names = PackedStringArray("Main Canvas") names = PackedStringArray("Main Canvas")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_an0ef"] [sub_resource type="Resource" id="Resource_an0ef"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1") script = ExtResource("1_jxh43")
names = PackedStringArray("Perspective Editor") names = PackedStringArray("Perspective Editor")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_xgnjk"] [sub_resource type="Resource" id="Resource_xgnjk"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2") script = ExtResource("2_lw52w")
direction = 0 direction = 0
percent = 0.5 percent = 0.5
first = SubResource("Resource_ouvfk") first = SubResource("Resource_ouvfk")
@ -32,13 +46,13 @@ second = SubResource("Resource_an0ef")
[sub_resource type="Resource" id="Resource_o7cqb"] [sub_resource type="Resource" id="Resource_o7cqb"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1") script = ExtResource("1_jxh43")
names = PackedStringArray("Second Canvas") names = PackedStringArray("Second Canvas")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_ataha"] [sub_resource type="Resource" id="Resource_ataha"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2") script = ExtResource("2_lw52w")
direction = 0 direction = 0
percent = 0.980952 percent = 0.980952
first = SubResource("Resource_xgnjk") first = SubResource("Resource_xgnjk")
@ -46,13 +60,13 @@ second = SubResource("Resource_o7cqb")
[sub_resource type="Resource" id="Resource_8y4au"] [sub_resource type="Resource" id="Resource_8y4au"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1") script = ExtResource("1_jxh43")
names = PackedStringArray("Animation Timeline") names = PackedStringArray("Animation Timeline")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_q2jwk"] [sub_resource type="Resource" id="Resource_q2jwk"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2") script = ExtResource("2_lw52w")
direction = 1 direction = 1
percent = 0.75578 percent = 0.75578
first = SubResource("Resource_ataha") first = SubResource("Resource_ataha")
@ -60,19 +74,19 @@ second = SubResource("Resource_8y4au")
[sub_resource type="Resource" id="Resource_5r0ap"] [sub_resource type="Resource" id="Resource_5r0ap"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1") script = ExtResource("1_jxh43")
names = PackedStringArray("Canvas Preview") names = PackedStringArray("Canvas Preview")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_6pqxe"] [sub_resource type="Resource" id="Resource_6pqxe"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1") script = ExtResource("1_jxh43")
names = PackedStringArray("Recorder") names = PackedStringArray("Recorder")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_ln20x"] [sub_resource type="Resource" id="Resource_ln20x"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2") script = ExtResource("2_lw52w")
direction = 1 direction = 1
percent = 0.911765 percent = 0.911765
first = SubResource("Resource_5r0ap") first = SubResource("Resource_5r0ap")
@ -80,39 +94,39 @@ second = SubResource("Resource_6pqxe")
[sub_resource type="Resource" id="Resource_dksrd"] [sub_resource type="Resource" id="Resource_dksrd"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1") script = ExtResource("1_jxh43")
names = PackedStringArray("Global Tool Options") names = PackedStringArray("Global Tool Options")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_kmey0"] [sub_resource type="Resource" id="Resource_kmey0"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1") script = ExtResource("1_jxh43")
names = PackedStringArray("Color Picker", "Reference Images") names = PackedStringArray("Color Picker", "Reference Images")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_1tm61"] [sub_resource type="Resource" id="Resource_1tm61"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2") script = ExtResource("2_lw52w")
direction = 1 direction = 1
percent = 0.134307 percent = 0.0499712
first = SubResource("Resource_dksrd") first = SubResource("Resource_dksrd")
second = SubResource("Resource_kmey0") second = SubResource("Resource_kmey0")
[sub_resource type="Resource" id="Resource_btl4b"] [sub_resource type="Resource" id="Resource_btl4b"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1") script = ExtResource("1_jxh43")
names = PackedStringArray("Left Tool Options") names = PackedStringArray("Left Tool Options")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_eu0mc"] [sub_resource type="Resource" id="Resource_eu0mc"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1") script = ExtResource("1_jxh43")
names = PackedStringArray("Right Tool Options") names = PackedStringArray("Right Tool Options")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_8ff4m"] [sub_resource type="Resource" id="Resource_8ff4m"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2") script = ExtResource("2_lw52w")
direction = 0 direction = 0
percent = 0.5 percent = 0.5
first = SubResource("Resource_btl4b") first = SubResource("Resource_btl4b")
@ -120,21 +134,21 @@ second = SubResource("Resource_eu0mc")
[sub_resource type="Resource" id="Resource_e72nu"] [sub_resource type="Resource" id="Resource_e72nu"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2") script = ExtResource("2_lw52w")
direction = 1 direction = 1
percent = 0.660142 percent = 0.643859
first = SubResource("Resource_1tm61") first = SubResource("Resource_1tm61")
second = SubResource("Resource_8ff4m") second = SubResource("Resource_8ff4m")
[sub_resource type="Resource" id="Resource_sg54a"] [sub_resource type="Resource" id="Resource_sg54a"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1") script = ExtResource("1_jxh43")
names = PackedStringArray("Palettes") names = PackedStringArray("Palettes")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_gdwmg"] [sub_resource type="Resource" id="Resource_gdwmg"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2") script = ExtResource("2_lw52w")
direction = 1 direction = 1
percent = 0.82948 percent = 0.82948
first = SubResource("Resource_e72nu") first = SubResource("Resource_e72nu")
@ -142,7 +156,7 @@ second = SubResource("Resource_sg54a")
[sub_resource type="Resource" id="Resource_acda3"] [sub_resource type="Resource" id="Resource_acda3"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2") script = ExtResource("2_lw52w")
direction = 1 direction = 1
percent = 0.0549133 percent = 0.0549133
first = SubResource("Resource_ln20x") first = SubResource("Resource_ln20x")
@ -150,30 +164,31 @@ second = SubResource("Resource_gdwmg")
[sub_resource type="Resource" id="Resource_2qk0j"] [sub_resource type="Resource" id="Resource_2qk0j"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2") script = ExtResource("2_lw52w")
direction = 0 direction = 0
percent = 0.731967 percent = 0.704098
first = SubResource("Resource_q2jwk") first = SubResource("Resource_q2jwk")
second = SubResource("Resource_acda3") second = SubResource("Resource_acda3")
[sub_resource type="Resource" id="Resource_msuil"] [sub_resource type="Resource" id="Resource_msuil"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2") script = ExtResource("2_lw52w")
direction = 0 direction = 0
percent = 0.0 percent = 0.0
first = SubResource("Resource_atmme") first = SubResource("Resource_epagr")
second = SubResource("Resource_2qk0j") second = SubResource("Resource_2qk0j")
[resource] [resource]
resource_name = "Default" resource_name = "Default"
script = ExtResource("3") script = ExtResource("3_4h5wj")
root = SubResource("Resource_msuil") root = SubResource("Resource_msuil")
hidden_tabs = { hidden_tabs = {
"Canvas Preview": true, "Canvas Preview": true,
"Color Picker Sliders": true, "Color Picker Sliders": true,
"Perspective Editor": true, "Perspective Editor": true,
"Recorder": true, "Recorder": true,
"Second Canvas": true "Second Canvas": true,
"Tiles": true
} }
windows = {} windows = {}
save_on_change = false save_on_change = false

View file

@ -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_panel.gd" id="1_t44r1"]
[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_split.gd" id="2_rngtv"]
[ext_resource type="Script" path="res://addons/dockable_container/layout.gd" id="3_ox7l5"] [ext_resource type="Script" path="res://addons/dockable_container/layout.gd" id="3_v86xb"]
[sub_resource type="Resource" id="Resource_kn4x4"] [sub_resource type="Resource" id="Resource_kn4x4"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1_nokpu") script = ExtResource("1_t44r1")
names = PackedStringArray("Main Canvas") names = PackedStringArray("Main Canvas")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_btw27"] [sub_resource type="Resource" id="Resource_btw27"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1_nokpu") script = ExtResource("1_t44r1")
names = PackedStringArray("Second Canvas") names = PackedStringArray("Second Canvas")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_bp28t"] [sub_resource type="Resource" id="Resource_bp28t"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2_q5vl6") script = ExtResource("2_rngtv")
direction = 0 direction = 0
percent = 0.829091 percent = 0.829091
first = SubResource("Resource_kn4x4") first = SubResource("Resource_kn4x4")
@ -26,13 +26,13 @@ second = SubResource("Resource_btw27")
[sub_resource type="Resource" id="Resource_10g0s"] [sub_resource type="Resource" id="Resource_10g0s"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1_nokpu") script = ExtResource("1_t44r1")
names = PackedStringArray("Perspective Editor") names = PackedStringArray("Perspective Editor")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_otntk"] [sub_resource type="Resource" id="Resource_otntk"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2_q5vl6") script = ExtResource("2_rngtv")
direction = 0 direction = 0
percent = 0.8625 percent = 0.8625
first = SubResource("Resource_bp28t") first = SubResource("Resource_bp28t")
@ -40,25 +40,25 @@ second = SubResource("Resource_10g0s")
[sub_resource type="Resource" id="Resource_12axs"] [sub_resource type="Resource" id="Resource_12axs"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1_nokpu") script = ExtResource("1_t44r1")
names = PackedStringArray("Tools") names = PackedStringArray("Tools")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_1omiw"] [sub_resource type="Resource" id="Resource_1omiw"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1_nokpu") script = ExtResource("1_t44r1")
names = PackedStringArray("Left Tool Options", "Right Tool Options") names = PackedStringArray("Left Tool Options", "Right Tool Options")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_p32ds"] [sub_resource type="Resource" id="Resource_p32ds"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1_nokpu") script = ExtResource("1_t44r1")
names = PackedStringArray("Color Picker") names = PackedStringArray("Color Picker")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_n6xyc"] [sub_resource type="Resource" id="Resource_n6xyc"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2_q5vl6") script = ExtResource("2_rngtv")
direction = 0 direction = 0
percent = 0.5 percent = 0.5
first = SubResource("Resource_1omiw") first = SubResource("Resource_1omiw")
@ -66,19 +66,19 @@ second = SubResource("Resource_p32ds")
[sub_resource type="Resource" id="Resource_1dcep"] [sub_resource type="Resource" id="Resource_1dcep"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1_nokpu") script = ExtResource("1_t44r1")
names = PackedStringArray("Canvas Preview", "Reference Images", "Recorder") names = PackedStringArray("Canvas Preview", "Reference Images", "Recorder")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_hc3ve"] [sub_resource type="Resource" id="Resource_hc3ve"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1_nokpu") script = ExtResource("1_t44r1")
names = PackedStringArray("Global Tool Options") names = PackedStringArray("Global Tool Options")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_nppps"] [sub_resource type="Resource" id="Resource_nppps"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2_q5vl6") script = ExtResource("2_rngtv")
direction = 1 direction = 1
percent = 0.729839 percent = 0.729839
first = SubResource("Resource_1dcep") first = SubResource("Resource_1dcep")
@ -86,13 +86,13 @@ second = SubResource("Resource_hc3ve")
[sub_resource type="Resource" id="Resource_d54jb"] [sub_resource type="Resource" id="Resource_d54jb"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1_nokpu") script = ExtResource("1_t44r1")
names = PackedStringArray("Palettes") names = PackedStringArray("Palettes")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_f6rik"] [sub_resource type="Resource" id="Resource_f6rik"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2_q5vl6") script = ExtResource("2_rngtv")
direction = 0 direction = 0
percent = 0.5 percent = 0.5
first = SubResource("Resource_nppps") first = SubResource("Resource_nppps")
@ -100,7 +100,7 @@ second = SubResource("Resource_d54jb")
[sub_resource type="Resource" id="Resource_26vov"] [sub_resource type="Resource" id="Resource_26vov"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2_q5vl6") script = ExtResource("2_rngtv")
direction = 0 direction = 0
percent = 0.501251 percent = 0.501251
first = SubResource("Resource_n6xyc") first = SubResource("Resource_n6xyc")
@ -108,21 +108,35 @@ second = SubResource("Resource_f6rik")
[sub_resource type="Resource" id="Resource_m3axb"] [sub_resource type="Resource" id="Resource_m3axb"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("1_nokpu") script = ExtResource("1_t44r1")
names = PackedStringArray("Animation Timeline") names = PackedStringArray("Animation Timeline")
current_tab = 0 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"] [sub_resource type="Resource" id="Resource_af0bk"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2_q5vl6") script = ExtResource("2_rngtv")
direction = 1 direction = 1
percent = 0.5 percent = 0.5
first = SubResource("Resource_26vov") first = SubResource("Resource_26vov")
second = SubResource("Resource_m3axb") second = SubResource("Resource_j3q3h")
[sub_resource type="Resource" id="Resource_1xpva"] [sub_resource type="Resource" id="Resource_1xpva"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2_q5vl6") script = ExtResource("2_rngtv")
direction = 0 direction = 0
percent = 0.03125 percent = 0.03125
first = SubResource("Resource_12axs") first = SubResource("Resource_12axs")
@ -130,7 +144,7 @@ second = SubResource("Resource_af0bk")
[sub_resource type="Resource" id="Resource_6dytr"] [sub_resource type="Resource" id="Resource_6dytr"]
resource_name = "Split" resource_name = "Split"
script = ExtResource("2_q5vl6") script = ExtResource("2_rngtv")
direction = 1 direction = 1
percent = 0.459538 percent = 0.459538
first = SubResource("Resource_otntk") first = SubResource("Resource_otntk")
@ -138,12 +152,13 @@ second = SubResource("Resource_1xpva")
[resource] [resource]
resource_name = "Tallscreen" resource_name = "Tallscreen"
script = ExtResource("3_ox7l5") script = ExtResource("3_v86xb")
root = SubResource("Resource_6dytr") root = SubResource("Resource_6dytr")
hidden_tabs = { hidden_tabs = {
"Perspective Editor": true, "Perspective Editor": true,
"Recorder": true, "Recorder": true,
"Second Canvas": true "Second Canvas": true,
"Tiles": true
} }
windows = {} windows = {}
save_on_change = false save_on_change = false

View file

@ -908,7 +908,7 @@ previous_project={
} }
center_canvas={ center_canvas={
"deadzone": 0.5, "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={ left_text_tool={
@ -925,6 +925,46 @@ show_pixel_indices={
"deadzone": 0.5, "deadzone": 0.5,
"events": [] "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] [input_devices]

View file

@ -535,9 +535,11 @@ func center(indices: Array) -> void:
tmp_centered.blend_rect(cel.image, used_rect, offset) tmp_centered.blend_rect(cel.image, used_rect, offset)
var centered := ImageExtended.new() var centered := ImageExtended.new()
centered.copy_from_custom(tmp_centered, cel_image.is_indexed) 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) centered.add_data_to_dictionary(redo_data, cel_image)
cel_image.add_data_to_dictionary(undo_data) cel_image.add_data_to_dictionary(undo_data)
Global.undo_redo_compress_images(redo_data, 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_undo_method(Global.undo_or_redo.bind(true))
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false)) project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
project.undo_redo.commit_action() project.undo_redo.commit_action()
@ -546,15 +548,15 @@ func center(indices: Array) -> void:
func scale_project(width: int, height: int, interpolation: int) -> void: func scale_project(width: int, height: int, interpolation: int) -> void:
var redo_data := {} var redo_data := {}
var undo_data := {} var undo_data := {}
for f in Global.current_project.frames: for cel in Global.current_project.get_all_pixel_cels():
for i in range(f.cels.size() - 1, -1, -1): if not cel is PixelCel:
var cel := f.cels[i] continue
if not cel is PixelCel: var cel_image := (cel as PixelCel).get_image()
continue var sprite := _resize_image(cel_image, width, height, interpolation) as ImageExtended
var cel_image := (cel as PixelCel).get_image() if cel is CelTileMap:
var sprite := _resize_image(cel_image, width, height, interpolation) as ImageExtended (cel as CelTileMap).serialize_undo_data_source_image(sprite, redo_data, undo_data)
sprite.add_data_to_dictionary(redo_data, cel_image) sprite.add_data_to_dictionary(redo_data, cel_image)
cel_image.add_data_to_dictionary(undo_data) cel_image.add_data_to_dictionary(undo_data)
general_do_and_undo_scale(width, height, redo_data, undo_data) general_do_and_undo_scale(width, height, redo_data, undo_data)
@ -596,9 +598,9 @@ func _resize_image(
func crop_to_selection() -> void: func crop_to_selection() -> void:
if not Global.current_project.has_selection: if not Global.current_project.has_selection:
return return
Global.canvas.selection.transform_content_confirm()
var redo_data := {} var redo_data := {}
var undo_data := {} var undo_data := {}
Global.canvas.selection.transform_content_confirm()
var rect: Rect2i = Global.canvas.selection.big_bounding_rectangle var rect: Rect2i = Global.canvas.selection.big_bounding_rectangle
# Loop through all the cels to crop them # Loop through all the cels to crop them
for cel in Global.current_project.get_all_pixel_cels(): for cel in Global.current_project.get_all_pixel_cels():
@ -606,6 +608,8 @@ func crop_to_selection() -> void:
var tmp_cropped := cel_image.get_region(rect) var tmp_cropped := cel_image.get_region(rect)
var cropped := ImageExtended.new() var cropped := ImageExtended.new()
cropped.copy_from_custom(tmp_cropped, cel_image.is_indexed) 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) cropped.add_data_to_dictionary(redo_data, cel_image)
cel_image.add_data_to_dictionary(undo_data) cel_image.add_data_to_dictionary(undo_data)
@ -617,18 +621,17 @@ func crop_to_selection() -> void:
func crop_to_content() -> void: func crop_to_content() -> void:
Global.canvas.selection.transform_content_confirm() Global.canvas.selection.transform_content_confirm()
var used_rect := Rect2i() var used_rect := Rect2i()
for f in Global.current_project.frames: for cel in Global.current_project.get_all_pixel_cels():
for cel in f.cels: if not cel is PixelCel:
if not cel is PixelCel: continue
continue var cel_used_rect := cel.get_image().get_used_rect()
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
if cel_used_rect == Rect2i(0, 0, 0, 0): # If the cel has no content continue
continue
if used_rect == Rect2i(0, 0, 0, 0): # If we still haven't found the first cel with content if used_rect == Rect2i(0, 0, 0, 0): # If we still haven't found the first cel with content
used_rect = cel_used_rect used_rect = cel_used_rect
else: else:
used_rect = used_rect.merge(cel_used_rect) used_rect = used_rect.merge(cel_used_rect)
# If no layer has any content, just return # If no layer has any content, just return
if used_rect == Rect2i(0, 0, 0, 0): if used_rect == Rect2i(0, 0, 0, 0):
@ -644,6 +647,8 @@ func crop_to_content() -> void:
var tmp_cropped := cel_image.get_region(used_rect) var tmp_cropped := cel_image.get_region(used_rect)
var cropped := ImageExtended.new() var cropped := ImageExtended.new()
cropped.copy_from_custom(tmp_cropped, cel_image.is_indexed) 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) cropped.add_data_to_dictionary(redo_data, cel_image)
cel_image.add_data_to_dictionary(undo_data) cel_image.add_data_to_dictionary(undo_data)
@ -662,6 +667,8 @@ func resize_canvas(width: int, height: int, offset_x: int, offset_y: int) -> voi
cel_image, Rect2i(Vector2i.ZERO, cel_image.get_size()), Vector2i(offset_x, offset_y) cel_image, Rect2i(Vector2i.ZERO, cel_image.get_size()), Vector2i(offset_x, offset_y)
) )
resized.convert_rgb_to_indexed() 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) resized.add_data_to_dictionary(redo_data, cel_image)
cel_image.add_data_to_dictionary(undo_data) cel_image.add_data_to_dictionary(undo_data)
@ -698,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, "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.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) 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, "size", project.size)
project.undo_redo.add_undo_property(project, "x_symmetry_point", project.x_symmetry_point) 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) project.undo_redo.add_undo_property(project, "y_symmetry_point", project.y_symmetry_point)

View file

@ -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 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. 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 }
enum GridTypes { CARTESIAN, ISOMETRIC, ALL } enum GridTypes { CARTESIAN, ISOMETRIC, ALL }
## ## Used to tell whether a color is being taken from the current theme, ## ## Used to tell whether a color is being taken from the current theme,
## or if it is a custom color. ## or if it is a custom color.
@ -897,12 +897,17 @@ func _initialize_keychain() -> void:
&"reference_rotate": Keychain.InputAction.new("", "Reference images", false), &"reference_rotate": Keychain.InputAction.new("", "Reference images", false),
&"reference_scale": Keychain.InputAction.new("", "Reference images", false), &"reference_scale": Keychain.InputAction.new("", "Reference images", false),
&"reference_quick_menu": 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 = { Keychain.groups = {
"Canvas": Keychain.InputGroup.new("", false), "Canvas": Keychain.InputGroup.new("", false),
"Cursor movement": Keychain.InputGroup.new("Canvas"), "Cursor movement": Keychain.InputGroup.new("Canvas"),
"Reference images": Keychain.InputGroup.new("Canvas"),
"Buttons": Keychain.InputGroup.new(), "Buttons": Keychain.InputGroup.new(),
"Tools": Keychain.InputGroup.new(), "Tools": Keychain.InputGroup.new(),
"Left": Keychain.InputGroup.new("Tools"), "Left": Keychain.InputGroup.new("Tools"),
@ -921,7 +926,7 @@ func _initialize_keychain() -> void:
"Shape tools": Keychain.InputGroup.new("Tool modifiers"), "Shape tools": Keychain.InputGroup.new("Tool modifiers"),
"Selection tools": Keychain.InputGroup.new("Tool modifiers"), "Selection tools": Keychain.InputGroup.new("Tool modifiers"),
"Transformation 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"] Keychain.ignore_actions = ["left_mouse", "right_mouse", "middle_mouse", "shift", "ctrl"]
@ -954,7 +959,7 @@ func general_redo(project := current_project) -> void:
## Performs actions done after an undo or redo is done. this takes [member general_undo] and ## 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 ## [member general_redo] a step further. Does further work if the current action requires it
## like refreshing textures, redraw UI elements etc...[br] ## 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] ## 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. ## and [param layer_index] respectively, otherwise the entire timeline will be refreshed.
func undo_or_redo( func undo_or_redo(
@ -980,20 +985,24 @@ func undo_or_redo(
] ]
): ):
if layer_index > -1 and frame_index > -1: 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: else:
for i in project.frames.size(): for i in project.frames.size():
for j in project.layers.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() canvas.selection.queue_redraw()
if action_name == "Scale": if action_name == "Scale":
for i in project.frames.size(): for i in project.frames.size():
for j in project.layers.size(): for j in project.layers.size():
var current_cel := project.frames[i].cels[j] var current_cel := project.frames[i].cels[j]
if current_cel is Cel3D: if current_cel is not Cel3D:
current_cel.size_changed(project.size)
else:
current_cel.image_texture.set_image(current_cel.get_image()) current_cel.image_texture.set_image(current_cel.get_image())
canvas.camera_zoom() canvas.camera_zoom()
canvas.grid.queue_redraw() canvas.grid.queue_redraw()

View file

@ -258,6 +258,18 @@ func open_pxo_file(path: String, is_backup := false, replace_empty := true) -> v
new_project.tiles.tile_mask = image new_project.tiles.tile_mask = image
else: else:
new_project.tiles.reset_mask() 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() zip_reader.close()
new_project.export_directory_path = path.get_base_dir() new_project.export_directory_path = path.get_base_dir()
@ -418,6 +430,14 @@ func save_pxo_file(
zip_packer.start_file("image_data/tile_map") zip_packer.start_file("image_data/tile_map")
zip_packer.write_file(project.tiles.tile_mask.get_data()) zip_packer.write_file(project.tiles.tile_mask.get_data())
zip_packer.close_file() 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()
zip_packer.close() zip_packer.close()
if temp_path != path: if temp_path != path:
@ -699,17 +719,18 @@ func open_image_at_cel(image: Image, layer_index := 0, frame_index := 0) -> void
return return
image.convert(project.get_image_format()) image.convert(project.get_image_format())
var cel_image := (cel as PixelCel).get_image() var cel_image := (cel as PixelCel).get_image()
var new_cel_image := ImageExtended.create_custom(
project_width, project_height, false, project.get_image_format(), cel_image.is_indexed
)
new_cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO)
new_cel_image.convert_rgb_to_indexed()
var redo_data := {}
new_cel_image.add_data_to_dictionary(redo_data, cel_image)
var undo_data := {} 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.add_data_to_dictionary(undo_data)
Global.undo_redo_compress_images(redo_data, undo_data, project) cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO)
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_property(project, "selected_cels", [])
project.undo_redo.add_do_method(project.change_cel.bind(frame_index, layer_index)) 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)) project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
@ -815,6 +836,49 @@ func import_reference_image_from_image(image: Image) -> void:
reference_image_imported.emit() 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: func set_new_imported_tab(project: Project, path: String) -> void:
var prev_project_empty := Global.current_project.is_empty() var prev_project_empty := Global.current_project.is_empty()
var prev_project_pos := Global.current_project_index var prev_project_pos := Global.current_project_index

View file

@ -2,6 +2,8 @@
extends Node extends Node
signal color_changed(color_info: Dictionary, 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) signal config_changed(slot_idx: int, config: Dictionary)
@warning_ignore("unused_signal") @warning_ignore("unused_signal")
signal flip_rotated(flip_x, flip_y, rotate_90, rotate_180, rotate_270) signal flip_rotated(flip_x, flip_y, rotate_90, rotate_180, rotate_270)
@ -88,7 +90,11 @@ var tools := {
), ),
"Move": "Move":
Tool.new( 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"), "Zoom": Tool.new("Zoom", "Zoom", "zoom", "res://src/Tools/UtilityTools/Zoom.tscn"),
"Pan": Tool.new("Pan", "Pan", "pan", "res://src/Tools/UtilityTools/Pan.tscn"), "Pan": Tool.new("Pan", "Pan", "pan", "res://src/Tools/UtilityTools/Pan.tscn"),
@ -116,7 +122,7 @@ var tools := {
"Pencil", "Pencil",
"pencil", "pencil",
"res://src/Tools/DesignTools/Pencil.tscn", "res://src/Tools/DesignTools/Pencil.tscn",
[Global.LayerTypes.PIXEL], [Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
"Hold %s to make a line", "Hold %s to make a line",
["draw_create_line"] ["draw_create_line"]
), ),
@ -126,7 +132,7 @@ var tools := {
"Eraser", "Eraser",
"eraser", "eraser",
"res://src/Tools/DesignTools/Eraser.tscn", "res://src/Tools/DesignTools/Eraser.tscn",
[Global.LayerTypes.PIXEL], [Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
"Hold %s to make a line", "Hold %s to make a line",
["draw_create_line"] ["draw_create_line"]
), ),
@ -136,7 +142,7 @@ var tools := {
"Bucket", "Bucket",
"fill", "fill",
"res://src/Tools/DesignTools/Bucket.tscn", "res://src/Tools/DesignTools/Bucket.tscn",
[Global.LayerTypes.PIXEL] [Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP]
), ),
"Shading": "Shading":
Tool.new( Tool.new(
@ -144,7 +150,7 @@ var tools := {
"Shading Tool", "Shading Tool",
"shading", "shading",
"res://src/Tools/DesignTools/Shading.tscn", "res://src/Tools/DesignTools/Shading.tscn",
[Global.LayerTypes.PIXEL] [Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP]
), ),
"LineTool": "LineTool":
( (
@ -154,7 +160,7 @@ var tools := {
"Line Tool", "Line Tool",
"linetool", "linetool",
"res://src/Tools/DesignTools/LineTool.tscn", "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 snap the angle of the line
Hold %s to center the shape on the click origin Hold %s to center the shape on the click origin
Hold %s to displace the shape's origin""", Hold %s to displace the shape's origin""",
@ -169,7 +175,7 @@ Hold %s to displace the shape's origin""",
"Curve Tool", "Curve Tool",
"curvetool", "curvetool",
"res://src/Tools/DesignTools/CurveTool.tscn", "res://src/Tools/DesignTools/CurveTool.tscn",
[Global.LayerTypes.PIXEL], [Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
"""Draws bezier curves """Draws bezier curves
Press %s/%s to add new points Press %s/%s to add new points
Press and drag to control the curvature Press and drag to control the curvature
@ -185,7 +191,7 @@ Press %s to remove the last added point""",
"Rectangle Tool", "Rectangle Tool",
"rectangletool", "rectangletool",
"res://src/Tools/DesignTools/RectangleTool.tscn", "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 create a 1:1 shape
Hold %s to center the shape on the click origin Hold %s to center the shape on the click origin
Hold %s to displace the shape's origin""", Hold %s to displace the shape's origin""",
@ -200,7 +206,7 @@ Hold %s to displace the shape's origin""",
"Ellipse Tool", "Ellipse Tool",
"ellipsetool", "ellipsetool",
"res://src/Tools/DesignTools/EllipseTool.tscn", "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 create a 1:1 shape
Hold %s to center the shape on the click origin Hold %s to center the shape on the click origin
Hold %s to displace the shape's origin""", Hold %s to displace the shape's origin""",
@ -213,7 +219,7 @@ Hold %s to displace the shape's origin""",
"Text", "Text",
"text", "text",
"res://src/Tools/UtilityTools/Text.tscn", "res://src/Tools/UtilityTools/Text.tscn",
[Global.LayerTypes.PIXEL], [Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
"" ""
), ),
"3DShapeEdit": "3DShapeEdit":
@ -232,10 +238,12 @@ var _panels := {}
var _curr_layer_type := Global.LayerTypes.PIXEL var _curr_layer_type := Global.LayerTypes.PIXEL
var _left_tools_per_layer_type := { var _left_tools_per_layer_type := {
Global.LayerTypes.PIXEL: "Pencil", Global.LayerTypes.PIXEL: "Pencil",
Global.LayerTypes.TILEMAP: "Pencil",
Global.LayerTypes.THREE_D: "3DShapeEdit", Global.LayerTypes.THREE_D: "3DShapeEdit",
} }
var _right_tools_per_layer_type := { var _right_tools_per_layer_type := {
Global.LayerTypes.PIXEL: "Eraser", Global.LayerTypes.PIXEL: "Eraser",
Global.LayerTypes.TILEMAP: "Eraser",
Global.LayerTypes.THREE_D: "Pan", Global.LayerTypes.THREE_D: "Pan",
} }
var _tool_buttons: Node var _tool_buttons: Node

View file

@ -67,7 +67,7 @@ func get_image() -> Image:
## Used to update the texture of the cel. ## Used to update the texture of the cel.
func update_texture() -> void: func update_texture(_undo := false) -> void:
texture_changed.emit() texture_changed.emit()
if link_set != null: if link_set != null:
var frame := Global.current_project.current_frame var frame := Global.current_project.current_frame
@ -92,6 +92,10 @@ func deserialize(dict: Dictionary) -> void:
user_data = dict.get("user_data", user_data) 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. ## Used to perform cleanup after a cel is removed.
func on_remove() -> void: func on_remove() -> void:
pass pass

View file

@ -0,0 +1,580 @@
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
## 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
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 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)
else:
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]
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"

View file

@ -54,9 +54,9 @@ func get_image() -> ImageExtended:
return image return image
func update_texture() -> void: func update_texture(undo := false) -> void:
image_texture.set_image(image) image_texture.set_image(image)
super.update_texture() super.update_texture(undo)
func get_class_name() -> String: func get_class_name() -> String:

View file

@ -157,10 +157,11 @@ func display_animate_dialog() -> void:
func _commit_undo(action: String, undo_data: Dictionary, project: Project) -> void: func _commit_undo(action: String, undo_data: Dictionary, project: Project) -> void:
project.update_tilemaps(undo_data)
var redo_data := _get_undo_data(project) var redo_data := _get_undo_data(project)
project.undos += 1 project.undos += 1
project.undo_redo.create_action(action) 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_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.add_undo_method(Global.undo_or_redo.bind(true, -1, -1, project))
project.undo_redo.commit_action() project.undo_redo.commit_action()
@ -168,24 +169,22 @@ func _commit_undo(action: String, undo_data: Dictionary, project: Project) -> vo
func _get_undo_data(project: Project) -> Dictionary: func _get_undo_data(project: Project) -> Dictionary:
var data := {} var data := {}
var images := _get_selected_draw_images(project) project.serialize_cel_undo_data(_get_selected_draw_cels(project), data)
for image in images:
image.add_data_to_dictionary(data)
return data return data
func _get_selected_draw_images(project: Project) -> Array[ImageExtended]: func _get_selected_draw_cels(project: Project) -> Array[BaseCel]:
var images: Array[ImageExtended] = [] var images: Array[BaseCel] = []
if affect == SELECTED_CELS: if affect == SELECTED_CELS:
for cel_index in project.selected_cels: for cel_index in project.selected_cels:
var cel: BaseCel = project.frames[cel_index[0]].cels[cel_index[1]] var cel: BaseCel = project.frames[cel_index[0]].cels[cel_index[1]]
if cel is PixelCel: if cel is PixelCel:
images.append(cel.get_image()) images.append(cel)
else: else:
for frame in project.frames: for frame in project.frames:
for cel in frame.cels: for cel in frame.cels:
if cel is PixelCel: if cel is PixelCel:
images.append(cel.get_image()) images.append(cel)
return images return images

View 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):
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

View file

@ -85,6 +85,7 @@ var selection_offset := Vector2i.ZERO:
selection_offset = value selection_offset = value
Global.canvas.selection.marching_ants_outline.offset = selection_offset Global.canvas.selection.marching_ants_outline.offset = selection_offset
var has_selection := false var has_selection := false
var tilesets: Array[TileSetCustom]
## For every camera (currently there are 3) ## For every camera (currently there are 3)
var cameras_rotation: PackedFloat32Array = [0.0, 0.0, 0.0] var cameras_rotation: PackedFloat32Array = [0.0, 0.0, 0.0]
@ -295,6 +296,9 @@ func serialize() -> Dictionary:
var reference_image_data := [] var reference_image_data := []
for reference_image in reference_images: for reference_image in reference_images:
reference_image_data.append(reference_image.serialize()) reference_image_data.append(reference_image.serialize())
var tileset_data := []
for tileset in tilesets:
tileset_data.append(tileset.serialize())
var metadata := _serialize_metadata(self) var metadata := _serialize_metadata(self)
@ -315,6 +319,7 @@ func serialize() -> Dictionary:
"frames": frame_data, "frames": frame_data,
"brushes": brush_data, "brushes": brush_data,
"reference_images": reference_image_data, "reference_images": reference_image_data,
"tilesets": tileset_data,
"vanishing_points": vanishing_points, "vanishing_points": vanishing_points,
"export_file_name": file_name, "export_file_name": file_name,
"export_file_format": file_format, "export_file_format": file_format,
@ -344,6 +349,12 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces
if dict.has("tile_mode_y_basis_x") and dict.has("tile_mode_y_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.x = dict.tile_mode_y_basis_x
tiles.y_basis.y = dict.tile_mode_y_basis_y 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)
tileset.deserialize(saved_tileset)
tilesets.append(tileset)
if dict.has("frames") and dict.has("layers"): if dict.has("frames") and dict.has("layers"):
for saved_layer in dict.layers: for saved_layer in dict.layers:
match int(saved_layer.get("type", Global.LayerTypes.PIXEL)): match int(saved_layer.get("type", Global.LayerTypes.PIXEL)):
@ -353,63 +364,8 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces
layers.append(GroupLayer.new(self)) layers.append(GroupLayer.new(self))
Global.LayerTypes.THREE_D: Global.LayerTypes.THREE_D:
layers.append(Layer3D.new(self)) layers.append(Layer3D.new(self))
Global.LayerTypes.TILEMAP:
var frame_i := 0 layers.append(LayerTileMap.new(self, null))
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)):
Global.LayerTypes.PIXEL:
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)
cels.append(PixelCel.new(pixelorama_image))
Global.LayerTypes.GROUP:
cels.append(GroupCel.new())
Global.LayerTypes.THREE_D:
if is_instance_valid(file): # For pxo files saved in 0.x
# 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))
cel["pxo_version"] = pxo_version
cels[cel_i].deserialize(cel)
_deserialize_metadata(cels[cel_i], cel)
cel_i += 1
var duration := 1.0
if frame.has("duration"):
duration = frame.duration
elif dict.has("frame_duration"):
duration = dict.frame_duration[frame_i]
var frame_class := Frame.new(cels, duration)
frame_class.user_data = frame.get("user_data", "")
_deserialize_metadata(frame_class, frame)
frames.append(frame_class)
frame_i += 1
# Parent references to other layers are created when deserializing # Parent references to other layers are created when deserializing
# a layer, so loop again after creating them: # a layer, so loop again after creating them:
@ -425,6 +381,43 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces
layer_dict["blend_mode"] = blend_mode layer_dict["blend_mode"] = blend_mode
layers[layer_i].deserialize(layer_dict) layers[layer_i].deserialize(layer_dict)
_deserialize_metadata(layers[layer_i], dict.layers[layer_i]) _deserialize_metadata(layers[layer_i], dict.layers[layer_i])
var frame_i := 0
for frame in dict.frames:
var cels: Array[BaseCel] = []
var cel_i := 0
for cel in frame.cels:
var layer := layers[cel_i]
match layer.get_layer_type():
Global.LayerTypes.PIXEL:
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())
Global.LayerTypes.THREE_D:
if is_instance_valid(file): # For pxo files saved in 0.x
# 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 new_cel := (layer as LayerTileMap).new_cel_from_image(image)
cels.append(new_cel)
cel["pxo_version"] = pxo_version
cels[cel_i].deserialize(cel)
_deserialize_metadata(cels[cel_i], cel)
cel_i += 1
var duration := 1.0
if frame.has("duration"):
duration = frame.duration
elif dict.has("frame_duration"):
duration = dict.frame_duration[frame_i]
var frame_class := Frame.new(cels, duration)
frame_class.user_data = frame.get("user_data", "")
_deserialize_metadata(frame_class, frame)
frames.append(frame_class)
frame_i += 1
if dict.has("tags"): if dict.has("tags"):
for tag in dict.tags: for tag in dict.tags:
var new_tag := AnimationTag.new(tag.name, Color(tag.color), tag.from, tag.to) var new_tag := AnimationTag.new(tag.name, Color(tag.color), tag.from, tag.to)
@ -483,6 +476,37 @@ func _deserialize_metadata(object: Object, dict: Dictionary) -> void:
object.set_meta(meta, metadata[meta]) 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: func _size_changed(value: Vector2i) -> void:
if not is_instance_valid(tiles): if not is_instance_valid(tiles):
size = value size = value
@ -632,6 +656,57 @@ func get_all_pixel_cels() -> Array[PixelCel]:
return cels return cels
## 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, ## 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. ## then the order of drawing is the same as the order of the layers itself.
func order_layers(frame_index := current_frame) -> void: func order_layers(frame_index := current_frame) -> void:
@ -931,3 +1006,16 @@ func reorder_reference_image(from: int, to: int) -> void:
var ri: ReferenceImage = reference_images.pop_at(from) var ri: ReferenceImage = reference_images.pop_at(from)
reference_images.insert(to, ri) reference_images.insert(to, ri)
Global.canvas.reference_image_container.move_child(ri, to) 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) -> void:
for cel in cel_dictionary:
if cel is CelTileMap:
(cel as CelTileMap).update_tilemap()

View file

@ -0,0 +1,185 @@
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 := "") -> void:
tile_size = _tile_size
name = _name
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)

View file

@ -1,3 +1,4 @@
class_name BaseDrawTool
extends BaseTool extends BaseTool
const IMAGE_BRUSHES := [Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM] 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 _orignal_brush_image := Image.new() ## Contains the original _brush_image, without resizing
var _brush_texture := ImageTexture.new() var _brush_texture := ImageTexture.new()
var _strength := 1.0 var _strength := 1.0
var _is_eraser := false
@warning_ignore("unused_private_class_variable") @warning_ignore("unused_private_class_variable")
var _picking_color := false var _picking_color := false
@ -42,6 +44,7 @@ var _circle_tool_shortcut: Array[Vector2i]
func _ready() -> void: func _ready() -> void:
super._ready() super._ready()
Global.cel_switched.connect(update_brush)
Global.global_tool_options.dynamics_panel.dynamics_changed.connect(_reset_dynamics) Global.global_tool_options.dynamics_panel.dynamics_changed.connect(_reset_dynamics)
Tools.color_changed.connect(_on_Color_changed) Tools.color_changed.connect(_on_Color_changed)
Global.brushes_popup.brush_removed.connect(_on_Brush_removed) Global.brushes_popup.brush_removed.connect(_on_Brush_removed)
@ -160,34 +163,48 @@ func update_config() -> void:
func update_brush() -> void: func update_brush() -> void:
$Brush/BrushSize.suffix = "px" # Assume we are using default brushes $Brush/BrushSize.suffix = "px" # Assume we are using default brushes
match _brush.type: if is_placing_tiles():
Brushes.PIXEL: var tilemap_cel := Global.current_project.get_current_cel() as CelTileMap
_brush_texture = ImageTexture.create_from_image( var tileset := tilemap_cel.tileset
load("res://assets/graphics/pixel_image.png") var tile_index := clampi(TileSetPanel.selected_tile_index, 0, tileset.tiles.size() - 1)
) var tile_image := tileset.tiles[tile_index].image
_stroke_dimensions = Vector2.ONE * _brush_size tile_image = tilemap_cel.transform_tile(
Brushes.CIRCLE: tile_image,
_brush_texture = ImageTexture.create_from_image( TileSetPanel.is_flipped_h,
load("res://assets/graphics/circle_9x9.png") TileSetPanel.is_flipped_v,
) TileSetPanel.is_transposed
_stroke_dimensions = Vector2.ONE * _brush_size )
Brushes.FILLED_CIRCLE: _brush_image.copy_from(tile_image)
_brush_texture = ImageTexture.create_from_image( _brush_texture = ImageTexture.create_from_image(_brush_image)
load("res://assets/graphics/circle_filled_9x9.png") else:
) match _brush.type:
_stroke_dimensions = Vector2.ONE * _brush_size Brushes.PIXEL:
Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM: _brush_texture = ImageTexture.create_from_image(
$Brush/BrushSize.suffix = "00 %" # Use a different size convention on images load("res://assets/graphics/pixel_image.png")
if _brush.random.size() <= 1: )
_orignal_brush_image = _brush.image _stroke_dimensions = Vector2.ONE * _brush_size
else: Brushes.CIRCLE:
var random := randi() % _brush.random.size() _brush_texture = ImageTexture.create_from_image(
_orignal_brush_image = _brush.random[random] load("res://assets/graphics/circle_9x9.png")
_brush_image = _create_blended_brush_image(_orignal_brush_image) )
update_brush_image_flip_and_rotate() _stroke_dimensions = Vector2.ONE * _brush_size
_brush_texture = ImageTexture.create_from_image(_brush_image) Brushes.FILLED_CIRCLE:
update_mirror_brush() _brush_texture = ImageTexture.create_from_image(
_stroke_dimensions = _brush_image.get_size() 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 = [] _circle_tool_shortcut = []
_indicator = _create_brush_indicator() _indicator = _create_brush_indicator()
_polylines = _create_polylines(_indicator) _polylines = _create_polylines(_indicator)
@ -256,8 +273,9 @@ func prepare_undo(action: String) -> void:
func commit_undo() -> void: func commit_undo() -> void:
var redo_data := _get_undo_data()
var project := Global.current_project var project := Global.current_project
project.update_tilemaps(_undo_data)
var redo_data := _get_undo_data()
var frame := -1 var frame := -1
var layer := -1 var layer := -1
if Global.animation_timeline.animation_timer.is_stopped() and project.selected_cels.size() == 1: if Global.animation_timeline.animation_timer.is_stopped() and project.selected_cels.size() == 1:
@ -265,7 +283,7 @@ func commit_undo() -> void:
layer = project.current_layer layer = project.current_layer
project.undos += 1 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_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.add_undo_method(Global.undo_or_redo.bind(true, frame, layer))
project.undo_redo.commit_action() project.undo_redo.commit_action()
@ -303,6 +321,22 @@ func draw_end(pos: Vector2i) -> void:
_polylines = _create_polylines(_indicator) _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: func _prepare_tool() -> void:
if !Global.current_project.layers[Global.current_project.current_layer].can_layer_get_drawn(): if !Global.current_project.layers[Global.current_project.current_layer].can_layer_get_drawn():
return return
@ -482,7 +516,14 @@ func remove_unselected_parts_of_brush(brush: Image, dst: Vector2i) -> Image:
func draw_indicator(left: bool) -> void: func draw_indicator(left: bool) -> void:
var color := Global.left_tool_color if left else Global.right_tool_color 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 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 ( if (
Global.current_project.has_selection Global.current_project.has_selection
and Global.current_project.tiles.mode == Tiles.MODE.NONE and Global.current_project.tiles.mode == Tiles.MODE.NONE
@ -491,7 +532,7 @@ func draw_indicator(left: bool) -> void:
var nearest_pos := Global.current_project.selection_map.get_nearest_position(pos) var nearest_pos := Global.current_project.selection_map.get_nearest_position(pos)
if nearest_pos != Vector2i.ZERO: if nearest_pos != Vector2i.ZERO:
var offset := nearest_pos var offset := nearest_pos
draw_indicator_at(snap_position(_cursor), offset, Color.GREEN) draw_indicator_at(snapped_position, offset, Color.GREEN)
return return
if Global.current_project.tiles.mode and Global.current_project.tiles.has_point(_cursor): if Global.current_project.tiles.mode and Global.current_project.tiles.has_point(_cursor):
@ -499,12 +540,12 @@ func draw_indicator(left: bool) -> void:
var nearest_tile := Global.current_project.tiles.get_nearest_tile(pos) var nearest_tile := Global.current_project.tiles.get_nearest_tile(pos)
if nearest_tile.position != Vector2i.ZERO: if nearest_tile.position != Vector2i.ZERO:
var offset := nearest_tile.position 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: func draw_indicator_at(pos: Vector2i, offset: Vector2i, color: Color) -> void:
var canvas: Node2D = Global.canvas.indicators 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 is_placing_tiles():
pos -= _brush_image.get_size() / 2 pos -= _brush_image.get_size() / 2
pos -= offset pos -= offset
canvas.draw_texture(_brush_texture, pos) canvas.draw_texture(_brush_texture, pos)
@ -539,6 +580,9 @@ func _set_pixel_no_cache(pos: Vector2i, ignore_mirroring := false) -> void:
pos = _stroke_project.tiles.get_canon_position(pos) pos = _stroke_project.tiles.get_canon_position(pos)
if Global.current_project.has_selection: if Global.current_project.has_selection:
pos = Global.current_project.selection_map.get_canon_position(pos) pos = Global.current_project.selection_map.get_canon_position(pos)
if is_placing_tiles():
draw_tile(pos)
return
if !_stroke_project.can_pixel_get_drawn(pos): if !_stroke_project.can_pixel_get_drawn(pos):
return return
@ -727,11 +771,7 @@ func _get_undo_data() -> Dictionary:
if not cel is PixelCel: if not cel is PixelCel:
continue continue
cels.append(cel) cels.append(cel)
for cel in cels: project.serialize_cel_undo_data(cels, data)
if not cel is PixelCel:
continue
var image := (cel as PixelCel).get_image()
image.add_data_to_dictionary(data)
return data return data

View file

@ -1,4 +1,4 @@
extends "res://src/Tools/BaseDraw.gd" extends BaseDrawTool
var _start := Vector2i.ZERO var _start := Vector2i.ZERO
var _offset := Vector2i.ZERO var _offset := Vector2i.ZERO
@ -128,8 +128,8 @@ func draw_move(pos: Vector2i) -> void:
func draw_end(pos: Vector2i) -> void: func draw_end(pos: Vector2i) -> void:
pos = snap_position(pos) pos = snap_position(pos)
super.draw_end(pos)
if _picking_color: if _picking_color:
super.draw_end(pos)
return return
if _drawing: if _drawing:
@ -150,6 +150,7 @@ func draw_end(pos: Vector2i) -> void:
_drawing = false _drawing = false
_displace_origin = false _displace_origin = false
cursor_text = "" cursor_text = ""
super.draw_end(pos)
func draw_preview() -> void: func draw_preview() -> void:
@ -188,9 +189,12 @@ func _draw_shape(origin: Vector2i, dest: Vector2i) -> void:
_drawer.reset() _drawer.reset()
# Draw each point offsetted based on the shape's thickness # Draw each point offsetted based on the shape's thickness
var draw_pos := point + thickness_vector var draw_pos := point + thickness_vector
if Global.current_project.can_pixel_get_drawn(draw_pos): if is_placing_tiles():
for image in images: draw_tile(draw_pos)
_drawer.set_pixel(image, draw_pos, tool_slot.color) 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() commit_undo()

View file

@ -75,7 +75,23 @@ func draw_move(pos: Vector2i) -> void:
func draw_end(_pos: Vector2i) -> void: func draw_end(_pos: Vector2i) -> void:
is_moving = false is_moving = false
_draw_cache = [] _draw_cache = []
Global.current_project.can_undo = true var project := Global.current_project
project.can_undo = true
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_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: func cursor_move(pos: Vector2i) -> void:
@ -129,52 +145,14 @@ func draw_preview() -> void:
func snap_position(pos: Vector2) -> Vector2: func snap_position(pos: Vector2) -> Vector2:
var snapping_distance := Global.snapping_distance / Global.camera.zoom.x var snapping_distance := Global.snapping_distance / Global.camera.zoom.x
if Global.snap_to_rectangular_grid_boundary: if Global.snap_to_rectangular_grid_boundary:
var grid_pos := pos.snapped(Global.grids[0].grid_size) pos = _snap_to_rectangular_grid_boundary(
grid_pos += Vector2(Global.grids[0].grid_offset) pos, Global.grids[0].grid_size, Global.grids[0].grid_offset, snapping_distance
# 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(-Global.grids[0].grid_size.x, -Global.grids[0].grid_size.y)
var t_c := grid_pos + Vector2(0, -Global.grids[0].grid_size.y)
var t_r := grid_pos + Vector2(Global.grids[0].grid_size.x, -Global.grids[0].grid_size.y)
var m_l := grid_pos + Vector2(-Global.grids[0].grid_size.x, 0)
var m_c := grid_pos
var m_r := grid_pos + Vector2(Global.grids[0].grid_size.x, 0)
var b_l := grid_pos + Vector2(-Global.grids[0].grid_size.x, Global.grids[0].grid_size.y)
var b_c := grid_pos + Vector2(0, Global.grids[0].grid_size.y)
var b_r := grid_pos + Vector2(Global.grids[0].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()
if Global.snap_to_rectangular_grid_center: if Global.snap_to_rectangular_grid_center:
var grid_center := ( pos = _snap_to_rectangular_grid_center(
pos.snapped(Global.grids[0].grid_size) + Vector2(Global.grids[0].grid_size / 2) pos, Global.grids[0].grid_size, Global.grids[0].grid_offset, snapping_distance
) )
grid_center += Vector2(Global.grids[0].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(-Global.grids[0].grid_size.x, -Global.grids[0].grid_size.y)
var t_c := grid_center + Vector2(0, -Global.grids[0].grid_size.y)
var t_r := grid_center + Vector2(Global.grids[0].grid_size.x, -Global.grids[0].grid_size.y)
var m_l := grid_center + Vector2(-Global.grids[0].grid_size.x, 0)
var m_c := grid_center
var m_r := grid_center + Vector2(Global.grids[0].grid_size.x, 0)
var b_l := grid_center + Vector2(-Global.grids[0].grid_size.x, Global.grids[0].grid_size.y)
var b_c := grid_center + Vector2(0, Global.grids[0].grid_size.y)
var b_r := grid_center + Vector2(Global.grids[0].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()
var snap_to := Vector2.INF var snap_to := Vector2.INF
if Global.snap_to_guides: if Global.snap_to_guides:
@ -287,6 +265,63 @@ func _get_closest_point_to_segment(
return closest_point return closest_point
func _snap_to_rectangular_grid_boundary(
pos: Vector2, grid_size: Vector2i, grid_offset: Vector2i, snapping_distance: float
) -> 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 _snap_to_rectangular_grid_center(
pos: Vector2, grid_size: Vector2i, grid_offset: Vector2i, snapping_distance: float
) -> Vector2:
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( func _snap_to_guide(
snap_to: Vector2, pos: Vector2, distance: float, s1: Vector2, s2: Vector2 snap_to: Vector2, pos: Vector2, distance: float, s1: Vector2, s2: Vector2
) -> Vector2: ) -> Vector2:
@ -322,6 +357,17 @@ func _get_draw_image() -> ImageExtended:
return Global.current_project.get_current_cel().get_image() return Global.current_project.get_current_cel().get_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]: func _get_selected_draw_images() -> Array[ImageExtended]:
var images: Array[ImageExtended] = [] var images: Array[ImageExtended] = []
var project := Global.current_project var project := Global.current_project
@ -340,7 +386,10 @@ func _pick_color(pos: Vector2i) -> void:
if pos.x < 0 or pos.y < 0: if pos.x < 0 or pos.y < 0:
return return
if 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() var image := Image.new()
image.copy_from(_get_draw_image()) image.copy_from(_get_draw_image())
if pos.x > image.get_width() - 1 or pos.y > image.get_height() - 1: if pos.x > image.get_width() - 1 or pos.y > image.get_height() - 1:

View file

@ -186,6 +186,11 @@ func draw_end(pos: Vector2i) -> void:
commit_undo() 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: func fill(pos: Vector2i) -> void:
match _fill_area: match _fill_area:
FillArea.AREA: FillArea.AREA:
@ -199,6 +204,17 @@ func fill(pos: Vector2i) -> void:
func fill_in_color(pos: Vector2i) -> void: func fill_in_color(pos: Vector2i) -> void:
var project := Global.current_project var project := Global.current_project
if 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 color := project.get_current_cel().get_image().get_pixelv(pos)
var images := _get_selected_draw_images() var images := _get_selected_draw_images()
for image in images: for image in images:
@ -311,6 +327,74 @@ func fill_in_selection() -> void:
gen.generate_image(image, PATTERN_FILL_SHADER, params, project.size) 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 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 ## Add a new segment to the array
func _add_new_segment(y := 0) -> void: func _add_new_segment(y := 0) -> void:
_allegro_flood_segments.append(Segment.new(y)) _allegro_flood_segments.append(Segment.new(y))
@ -407,62 +491,6 @@ func _check_flooded_segment(
return ret 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: ImageExtended) -> void: func _color_segments(image: ImageExtended) -> void:
if _fill_with == FillWith.COLOR or _pattern == null: 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 # This is needed to ensure that the color used to fill is not wrong, due to float
@ -493,9 +521,119 @@ func _set_pixel_pattern(image: ImageExtended, x: int, y: int, pattern_size: Vect
image.set_pixel_custom(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: func commit_undo() -> void:
var redo_data := _get_undo_data()
var project := Global.current_project var project := Global.current_project
project.update_tilemaps(_undo_data)
var redo_data := _get_undo_data()
var frame := -1 var frame := -1
var layer := -1 var layer := -1
if Global.animation_timeline.animation_timer.is_stopped() and project.selected_cels.size() == 1: if Global.animation_timeline.animation_timer.is_stopped() and project.selected_cels.size() == 1:
@ -504,7 +642,7 @@ func commit_undo() -> void:
project.undos += 1 project.undos += 1
project.undo_redo.create_action("Draw") 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_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.add_undo_method(Global.undo_or_redo.bind(true, frame, layer))
project.undo_redo.commit_action() project.undo_redo.commit_action()
@ -514,14 +652,13 @@ func commit_undo() -> void:
func _get_undo_data() -> Dictionary: func _get_undo_data() -> Dictionary:
var data := {} var data := {}
if Global.animation_timeline.animation_timer.is_stopped(): if Global.animation_timeline.animation_timer.is_stopped():
var images := _get_selected_draw_images() Global.current_project.serialize_cel_undo_data(_get_selected_draw_cels(), data)
for image in images:
image.add_data_to_dictionary(data)
else: else:
var cels: Array[BaseCel]
for frame in Global.current_project.frames: for frame in Global.current_project.frames:
var cel := frame.cels[Global.current_project.current_layer] var cel := frame.cels[Global.current_project.current_layer]
if not cel is PixelCel: if not cel is PixelCel:
continue continue
var image := (cel as PixelCel).get_image() cels.append(cel)
image.add_data_to_dictionary(data) Global.current_project.serialize_cel_undo_data(cels, data)
return data return data

View file

@ -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 _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. var _drawing := false ## Set to true when a curve is being drawn.
@ -195,9 +195,12 @@ func _draw_shape() -> void:
func _draw_pixel(point: Vector2i, images: Array[ImageExtended]) -> void: func _draw_pixel(point: Vector2i, images: Array[ImageExtended]) -> void:
if Global.current_project.can_pixel_get_drawn(point): if is_placing_tiles():
for image in images: draw_tile(point)
_drawer.set_pixel(image, point, tool_slot.color) 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: func _clear() -> void:

View file

@ -1,4 +1,4 @@
extends "res://src/Tools/BaseDraw.gd" extends BaseDrawTool
var _last_position := Vector2.INF var _last_position := Vector2.INF
var _clear_image: Image var _clear_image: Image
@ -19,6 +19,7 @@ class EraseOp:
func _init() -> void: func _init() -> void:
_drawer.color_op = EraseOp.new() _drawer.color_op = EraseOp.new()
_is_eraser = true
_clear_image = Image.create(1, 1, false, Image.FORMAT_RGBA8) _clear_image = Image.create(1, 1, false, Image.FORMAT_RGBA8)
_clear_image.fill(Color(0, 0, 0, 0)) _clear_image.fill(Color(0, 0, 0, 0))
@ -42,13 +43,11 @@ func draw_start(pos: Vector2i) -> void:
_pick_color(pos) _pick_color(pos)
return return
_picking_color = false _picking_color = false
Global.canvas.selection.transform_content_confirm() Global.canvas.selection.transform_content_confirm()
prepare_undo("Draw")
update_mask(_strength == 1) update_mask(_strength == 1)
_changed = false _changed = false
_drawer.color_op.changed = false _drawer.color_op.changed = false
prepare_undo("Draw")
_drawer.reset() _drawer.reset()
_draw_line = Input.is_action_pressed("draw_create_line") _draw_line = Input.is_action_pressed("draw_create_line")

View file

@ -1,4 +1,4 @@
extends "res://src/Tools/BaseDraw.gd" extends BaseDrawTool
var _original_pos := Vector2i.ZERO var _original_pos := Vector2i.ZERO
var _start := Vector2i.ZERO var _start := Vector2i.ZERO
@ -120,8 +120,8 @@ func draw_move(pos: Vector2i) -> void:
func draw_end(pos: Vector2i) -> void: func draw_end(pos: Vector2i) -> void:
pos = snap_position(pos) pos = snap_position(pos)
super.draw_end(pos)
if _picking_color: if _picking_color:
super.draw_end(pos)
return return
if _drawing: if _drawing:
@ -144,6 +144,7 @@ func draw_end(pos: Vector2i) -> void:
_drawing = false _drawing = false
_displace_origin = false _displace_origin = false
cursor_text = "" cursor_text = ""
super.draw_end(pos)
func draw_preview() -> void: func draw_preview() -> void:
@ -173,10 +174,13 @@ func _draw_shape() -> void:
for point in points: for point in points:
# Reset drawer every time because pixel perfect sometimes breaks the tool # Reset drawer every time because pixel perfect sometimes breaks the tool
_drawer.reset() _drawer.reset()
# Draw each point offsetted based on the shape's thickness if is_placing_tiles():
if Global.current_project.can_pixel_get_drawn(point): draw_tile(point)
for image in images: else:
_drawer.set_pixel(image, point, tool_slot.color) # 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() commit_undo()

View file

@ -1,4 +1,4 @@
extends "res://src/Tools/BaseDraw.gd" extends BaseDrawTool
var _prev_mode := false var _prev_mode := false
var _last_position := Vector2i(Vector2.INF) var _last_position := Vector2i(Vector2.INF)
@ -103,6 +103,7 @@ func draw_start(pos: Vector2i) -> void:
_picking_color = false _picking_color = false
Global.canvas.selection.transform_content_confirm() Global.canvas.selection.transform_content_confirm()
prepare_undo("Draw")
var can_skip_mask := true var can_skip_mask := true
if tool_slot.color.a < 1 and !_overwrite: if tool_slot.color.a < 1 and !_overwrite:
can_skip_mask = false can_skip_mask = false
@ -112,7 +113,6 @@ func draw_start(pos: Vector2i) -> void:
_drawer.color_op.overwrite = _overwrite _drawer.color_op.overwrite = _overwrite
_draw_points = [] _draw_points = []
prepare_undo("Draw")
_drawer.reset() _drawer.reset()
_draw_line = Input.is_action_pressed("draw_create_line") _draw_line = Input.is_action_pressed("draw_create_line")

View file

@ -1,4 +1,4 @@
extends "res://src/Tools/BaseDraw.gd" extends BaseDrawTool
enum ShadingMode { SIMPLE, HUE_SHIFTING, COLOR_REPLACE } enum ShadingMode { SIMPLE, HUE_SHIFTING, COLOR_REPLACE }
enum LightenDarken { LIGHTEN, DARKEN } enum LightenDarken { LIGHTEN, DARKEN }

View file

@ -63,10 +63,12 @@ func draw_end(pos: Vector2i) -> void:
func _pick_color(pos: Vector2i) -> void: func _pick_color(pos: Vector2i) -> void:
var project := Global.current_project var project := Global.current_project
pos = project.tiles.get_canon_position(pos) pos = project.tiles.get_canon_position(pos)
if pos.x < 0 or pos.y < 0: if pos.x < 0 or pos.y < 0:
return return
if 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() var image := Image.new()
image.copy_from(_get_draw_image()) image.copy_from(_get_draw_image())
if pos.x > image.get_width() - 1 or pos.y > image.get_height() - 1: if pos.x > image.get_width() - 1 or pos.y > image.get_height() - 1:

View file

@ -70,8 +70,8 @@ func draw_move(pos: Vector2i) -> void:
func draw_end(pos: Vector2i) -> void: func draw_end(pos: Vector2i) -> void:
super.draw_end(pos)
if !Global.current_project.layers[Global.current_project.current_layer].can_layer_get_drawn(): if !Global.current_project.layers[Global.current_project.current_layer].can_layer_get_drawn():
super.draw_end(pos)
return return
if ( if (
_start_pos != Vector2i(Vector2.INF) _start_pos != Vector2i(Vector2.INF)
@ -93,6 +93,7 @@ func draw_end(pos: Vector2i) -> void:
_snap_to_grid = false _snap_to_grid = false
Global.canvas.sprite_changed_this_frame = true Global.canvas.sprite_changed_this_frame = true
Global.canvas.measurements.update_measurement(Global.MeasurementMode.NONE) Global.canvas.measurements.update_measurement(Global.MeasurementMode.NONE)
super.draw_end(pos)
func _move_image(image: Image, pixel_diff: Vector2i) -> void: func _move_image(image: Image, pixel_diff: Vector2i) -> void:
@ -129,8 +130,9 @@ func _snap_position(pos: Vector2) -> Vector2:
func _commit_undo(action: String) -> void: func _commit_undo(action: String) -> void:
var redo_data := _get_undo_data()
var project := Global.current_project var project := Global.current_project
project.update_tilemaps(_undo_data)
var redo_data := _get_undo_data()
var frame := -1 var frame := -1
var layer := -1 var layer := -1
if Global.animation_timeline.animation_timer.is_stopped() and project.selected_cels.size() == 1: if Global.animation_timeline.animation_timer.is_stopped() and project.selected_cels.size() == 1:
@ -139,7 +141,7 @@ func _commit_undo(action: String) -> void:
project.undos += 1 project.undos += 1
project.undo_redo.create_action(action) 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, frame, layer)) 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.add_undo_method(Global.undo_or_redo.bind(true, frame, layer))
project.undo_redo.commit_action() project.undo_redo.commit_action()
@ -157,9 +159,5 @@ func _get_undo_data() -> Dictionary:
for frame in project.frames: for frame in project.frames:
var cel := frame.cels[project.current_layer] var cel := frame.cels[project.current_layer]
cels.append(cel) cels.append(cel)
for cel in cels: project.serialize_cel_undo_data(cels, data)
if not cel is PixelCel:
continue
var image := (cel as PixelCel).get_image()
image.add_data_to_dictionary(data)
return data return data

View file

@ -103,8 +103,8 @@ func draw_move(pos: Vector2i) -> void:
_offset = pos _offset = pos
func draw_end(_position: Vector2i) -> void: func draw_end(pos: Vector2i) -> void:
pass super.draw_end(pos)
func text_to_pixels() -> void: func text_to_pixels() -> void:
@ -161,8 +161,9 @@ func text_to_pixels() -> void:
func commit_undo(action: String, undo_data: Dictionary) -> void: func commit_undo(action: String, undo_data: Dictionary) -> void:
var redo_data := _get_undo_data()
var project := Global.current_project var project := Global.current_project
project.update_tilemaps(undo_data)
var redo_data := _get_undo_data()
var frame := -1 var frame := -1
var layer := -1 var layer := -1
if Global.animation_timeline.animation_timer.is_stopped() and project.selected_cels.size() == 1: if Global.animation_timeline.animation_timer.is_stopped() and project.selected_cels.size() == 1:
@ -171,7 +172,7 @@ func commit_undo(action: String, undo_data: Dictionary) -> void:
project.undos += 1 project.undos += 1
project.undo_redo.create_action(action) 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, frame, layer)) 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.add_undo_method(Global.undo_or_redo.bind(true, frame, layer))
project.undo_redo.commit_action() project.undo_redo.commit_action()
@ -179,9 +180,7 @@ func commit_undo(action: String, undo_data: Dictionary) -> void:
func _get_undo_data() -> Dictionary: func _get_undo_data() -> Dictionary:
var data := {} var data := {}
var images := _get_selected_draw_images() Global.current_project.serialize_cel_undo_data(_get_selected_draw_cels(), data)
for image in images:
image.add_data_to_dictionary(data)
return data return data

View file

@ -110,13 +110,15 @@ func camera_zoom() -> void:
Global.transparent_checker.update_rect() Global.transparent_checker.update_rect()
func update_texture(layer_i: int, frame_i := -1, project := Global.current_project) -> void: func update_texture(
layer_i: int, frame_i := -1, project := Global.current_project, undo := false
) -> void:
if frame_i == -1: if frame_i == -1:
frame_i = project.current_frame frame_i = project.current_frame
if frame_i < project.frames.size() and layer_i < project.layers.size(): if frame_i < project.frames.size() and layer_i < project.layers.size():
var current_cel := project.frames[frame_i].cels[layer_i] var current_cel := project.frames[frame_i].cels[layer_i]
current_cel.update_texture() current_cel.update_texture(undo)
# Needed so that changes happening to the non-selected layer(s) are also visible # Needed so that changes happening to the non-selected layer(s) are also visible
# e.g. when undoing/redoing, when applying image effects to the entire frame, etc # e.g. when undoing/redoing, when applying image effects to the entire frame, etc
if frame_i != project.current_frame: if frame_i != project.current_frame:

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=24 format=3 uid="uid://ba24iuv55m4l3"] [gd_scene load_steps=25 format=3 uid="uid://ba24iuv55m4l3"]
[ext_resource type="Script" path="res://src/UI/Canvas/Canvas.gd" id="1"] [ext_resource type="Script" path="res://src/UI/Canvas/Canvas.gd" id="1"]
[ext_resource type="Shader" path="res://src/Shaders/BlendLayers.gdshader" id="1_253dh"] [ext_resource type="Shader" path="res://src/Shaders/BlendLayers.gdshader" id="1_253dh"]
@ -18,6 +18,7 @@
[ext_resource type="Shader" path="res://src/Shaders/AutoInvertColors.gdshader" id="17_lowhf"] [ext_resource type="Shader" path="res://src/Shaders/AutoInvertColors.gdshader" id="17_lowhf"]
[ext_resource type="Script" path="res://src/UI/Canvas/ReferenceImages.gd" id="17_qfjb4"] [ext_resource type="Script" path="res://src/UI/Canvas/ReferenceImages.gd" id="17_qfjb4"]
[ext_resource type="Script" path="res://src/UI/Canvas/color_index.gd" id="18_o3xx2"] [ext_resource type="Script" path="res://src/UI/Canvas/color_index.gd" id="18_o3xx2"]
[ext_resource type="Script" path="res://src/UI/Canvas/TileModeIndices.gd" id="19_7a6wb"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_6b0ox"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_6b0ox"]
shader = ExtResource("1_253dh") shader = ExtResource("1_253dh")
@ -113,3 +114,7 @@ script = ExtResource("16_nxilb")
[node name="ReferenceImages" type="Node2D" parent="."] [node name="ReferenceImages" type="Node2D" parent="."]
script = ExtResource("17_qfjb4") script = ExtResource("17_qfjb4")
[node name="TileModeIndices" type="Node2D" parent="."]
material = SubResource("ShaderMaterial_ascg6")
script = ExtResource("19_7a6wb")

View file

@ -6,6 +6,7 @@ var unique_iso_lines := PackedVector2Array()
func _ready() -> void: func _ready() -> void:
Global.project_switched.connect(queue_redraw) Global.project_switched.connect(queue_redraw)
Global.cel_switched.connect(queue_redraw)
func _draw() -> void: func _draw() -> void:
@ -32,28 +33,32 @@ func _draw() -> void:
func _draw_cartesian_grid(grid_index: int, target_rect: Rect2i) -> void: func _draw_cartesian_grid(grid_index: int, target_rect: Rect2i) -> void:
var grid = Global.grids[grid_index] var grid := Global.grids[grid_index]
var grid_size := grid.grid_size
var grid_offset := grid.grid_offset
var cel := Global.current_project.get_current_cel()
if cel is CelTileMap and grid_index == 0:
grid_size = (cel as CelTileMap).tileset.tile_size
grid_offset = Vector2i.ZERO
var grid_multiline_points := PackedVector2Array() var grid_multiline_points := PackedVector2Array()
var x: float = ( var x: float = (
target_rect.position.x target_rect.position.x + fposmod(grid_offset.x - target_rect.position.x, grid_size.x)
+ fposmod(grid.grid_offset.x - target_rect.position.x, grid.grid_size.x)
) )
while x <= target_rect.end.x: while x <= target_rect.end.x:
if not Vector2(x, target_rect.position.y) in unique_rect_lines: if not Vector2(x, target_rect.position.y) in unique_rect_lines:
grid_multiline_points.push_back(Vector2(x, target_rect.position.y)) grid_multiline_points.push_back(Vector2(x, target_rect.position.y))
grid_multiline_points.push_back(Vector2(x, target_rect.end.y)) grid_multiline_points.push_back(Vector2(x, target_rect.end.y))
x += grid.grid_size.x x += grid_size.x
var y: float = ( var y: float = (
target_rect.position.y target_rect.position.y + fposmod(grid_offset.y - target_rect.position.y, grid_size.y)
+ fposmod(grid.grid_offset.y - target_rect.position.y, grid.grid_size.y)
) )
while y <= target_rect.end.y: while y <= target_rect.end.y:
if not Vector2(target_rect.position.x, y) in unique_rect_lines: if not Vector2(target_rect.position.x, y) in unique_rect_lines:
grid_multiline_points.push_back(Vector2(target_rect.position.x, y)) grid_multiline_points.push_back(Vector2(target_rect.position.x, y))
grid_multiline_points.push_back(Vector2(target_rect.end.x, y)) grid_multiline_points.push_back(Vector2(target_rect.end.x, y))
y += grid.grid_size.y y += grid_size.y
unique_rect_lines.append_array(grid_multiline_points) unique_rect_lines.append_array(grid_multiline_points)
if not grid_multiline_points.is_empty(): if not grid_multiline_points.is_empty():
@ -61,7 +66,7 @@ func _draw_cartesian_grid(grid_index: int, target_rect: Rect2i) -> void:
func _draw_isometric_grid(grid_index: int, target_rect: Rect2i) -> void: func _draw_isometric_grid(grid_index: int, target_rect: Rect2i) -> void:
var grid = Global.grids[grid_index] var grid := Global.grids[grid_index]
var grid_multiline_points := PackedVector2Array() var grid_multiline_points := PackedVector2Array()
var cell_size: Vector2 = grid.isometric_grid_size var cell_size: Vector2 = grid.isometric_grid_size

View file

@ -568,12 +568,12 @@ func commit_undo(action: String, undo_data_tmp: Dictionary) -> void:
if !undo_data_tmp: if !undo_data_tmp:
print("No undo data found!") print("No undo data found!")
return return
var redo_data := get_undo_data(undo_data_tmp["undo_image"])
var project := Global.current_project var project := Global.current_project
project.update_tilemaps(undo_data_tmp)
var redo_data := get_undo_data(undo_data_tmp["undo_image"])
project.undos += 1 project.undos += 1
project.undo_redo.create_action(action) project.undo_redo.create_action(action)
Global.undo_redo_compress_images(redo_data, undo_data_tmp, project) project.deserialize_cel_undo_data(redo_data, undo_data_tmp)
project.undo_redo.add_do_property( project.undo_redo.add_do_property(
self, "big_bounding_rectangle", redo_data["big_bounding_rectangle"] self, "big_bounding_rectangle", redo_data["big_bounding_rectangle"]
) )
@ -604,15 +604,14 @@ func get_undo_data(undo_image: bool) -> Dictionary:
data["undo_image"] = undo_image data["undo_image"] = undo_image
if undo_image: if undo_image:
var images := _get_selected_draw_images() Global.current_project.serialize_cel_undo_data(_get_selected_draw_cels(), data)
for image in images:
image.add_data_to_dictionary(data)
return data return data
func _get_selected_draw_cels() -> Array[PixelCel]: # TODO: Change BaseCel to PixelCel if Godot ever fixes issues
var cels: Array[PixelCel] = [] # with typed arrays being cast into other types.
func _get_selected_draw_cels() -> Array[BaseCel]:
var cels: Array[BaseCel] = []
var project := Global.current_project var project := Global.current_project
for cel_index in project.selected_cels: for cel_index in project.selected_cels:
var cel: BaseCel = project.frames[cel_index[0]].cels[cel_index[1]] var cel: BaseCel = project.frames[cel_index[0]].cels[cel_index[1]]

View file

@ -0,0 +1,26 @@
extends Node2D
const FONT_SIZE := 16
func _input(event: InputEvent) -> void:
if event.is_action("ctrl"):
queue_redraw()
func _draw() -> void:
var current_cel := Global.current_project.get_current_cel()
draw_set_transform(position, rotation, Vector2(0.5, 0.5))
if current_cel is CelTileMap and Input.is_action_pressed("ctrl"):
var tilemap_cel := current_cel as CelTileMap
for i in tilemap_cel.cells.size():
var tile_data := tilemap_cel.cells[i]
if tile_data.index == 0:
continue
var pos := tilemap_cel.get_cell_coords_in_image(i)
pos.y += tilemap_cel.tileset.tile_size.y
var text := tile_data.to_string()
draw_multiline_string(
Themes.get_font(), pos * 2, text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE
)
draw_set_transform(position, rotation, scale)

View file

@ -231,6 +231,8 @@ func create_layer_list() -> void:
layer_name = tr("Group layer:") layer_name = tr("Group layer:")
elif layer is Layer3D: elif layer is Layer3D:
layer_name = tr("3D layer:") layer_name = tr("3D layer:")
elif layer is LayerTileMap:
layer_name = tr("Tilemap layer:")
layer_name += " %s" % layer.get_layer_path() layer_name += " %s" % layer.get_layer_path()
layers_option_button.add_item(layer_name) layers_option_button.add_item(layer_name)

View file

@ -47,11 +47,11 @@ func _flip_image(cel: Image, affect_selection: bool, project: Project) -> void:
func _commit_undo(action: String, undo_data: Dictionary, project: Project) -> void: func _commit_undo(action: String, undo_data: Dictionary, project: Project) -> void:
_flip_selection(project) _flip_selection(project)
project.update_tilemaps(undo_data)
var redo_data := _get_undo_data(project) var redo_data := _get_undo_data(project)
project.undos += 1 project.undos += 1
project.undo_redo.create_action(action) 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)
if redo_data.has("outline_offset"): if redo_data.has("outline_offset"):
project.undo_redo.add_do_property(project, "selection_offset", redo_data["outline_offset"]) project.undo_redo.add_do_property(project, "selection_offset", redo_data["outline_offset"])
project.undo_redo.add_undo_property( project.undo_redo.add_undo_property(
@ -66,14 +66,10 @@ func _commit_undo(action: String, undo_data: Dictionary, project: Project) -> vo
func _get_undo_data(project: Project) -> Dictionary: func _get_undo_data(project: Project) -> Dictionary:
var affect_selection := selection_checkbox.button_pressed and project.has_selection var affect_selection := selection_checkbox.button_pressed and project.has_selection
var data := {} var data := super._get_undo_data(project)
if affect_selection: if affect_selection:
data[project.selection_map] = project.selection_map.data data[project.selection_map] = project.selection_map.data
data["outline_offset"] = project.selection_offset data["outline_offset"] = project.selection_offset
var images := _get_selected_draw_images(project)
for image in images:
data[image] = image.data
return data return data

View file

@ -11,7 +11,8 @@ enum ImageImportOptions {
NEW_REFERENCE_IMAGE, NEW_REFERENCE_IMAGE,
PALETTE, PALETTE,
BRUSH, BRUSH,
PATTERN PATTERN,
TILESET
} }
enum BrushTypes { FILE, PROJECT, RANDOM } enum BrushTypes { FILE, PROJECT, RANDOM }
@ -75,6 +76,7 @@ func _on_ImportPreviewDialog_about_to_show() -> void:
import_option_button.add_item("New palette") import_option_button.add_item("New palette")
import_option_button.add_item("New brush") import_option_button.add_item("New brush")
import_option_button.add_item("New pattern") import_option_button.add_item("New pattern")
import_option_button.add_item("Tileset")
# adding custom importers # adding custom importers
for id in custom_importers.keys(): for id in custom_importers.keys():
@ -207,6 +209,17 @@ func _on_ImportPreviewDialog_confirmed() -> void:
var location := "Patterns".path_join(file_name_ext) var location := "Patterns".path_join(file_name_ext)
var dir := DirAccess.open(path.get_base_dir()) var dir := DirAccess.open(path.get_base_dir())
dir.copy(path, Global.home_data_directory.path_join(location)) dir.copy(path, Global.home_data_directory.path_join(location))
elif current_import_option == ImageImportOptions.TILESET:
if smart_slice:
if !recycle_last_slice_result:
obtain_sliced_data()
OpenSave.open_image_as_tileset_smart(
path, image, sliced_rects.rects, sliced_rects.frame_size
)
else:
OpenSave.open_image_as_tileset(
path, image, spritesheet_horizontal, spritesheet_vertical
)
else: else:
if current_import_option in custom_importers.keys(): if current_import_option in custom_importers.keys():
@ -250,7 +263,11 @@ func synchronize() -> void:
dialog.at_layer_option.get_node("AtLayerOption") as OptionButton dialog.at_layer_option.get_node("AtLayerOption") as OptionButton
) )
# Sync properties (if any) # Sync properties (if any)
if id == ImageImportOptions.SPRITESHEET_TAB or id == ImageImportOptions.SPRITESHEET_LAYER: if (
id == ImageImportOptions.SPRITESHEET_TAB
or id == ImageImportOptions.SPRITESHEET_LAYER
or id == ImageImportOptions.TILESET
):
var h_frames := spritesheet_options.find_child("HorizontalFrames") as SpinBox var h_frames := spritesheet_options.find_child("HorizontalFrames") as SpinBox
var v_frames := spritesheet_options.find_child("VerticalFrames") as SpinBox var v_frames := spritesheet_options.find_child("VerticalFrames") as SpinBox
var d_h_frames := dialog.spritesheet_options.find_child("HorizontalFrames") as SpinBox var d_h_frames := dialog.spritesheet_options.find_child("HorizontalFrames") as SpinBox
@ -298,7 +315,7 @@ func _on_ImportOption_item_selected(id: ImageImportOptions) -> void:
_hide_all_options() _hide_all_options()
import_options.get_parent().visible = true import_options.get_parent().visible = true
if id == ImageImportOptions.SPRITESHEET_TAB: if id == ImageImportOptions.SPRITESHEET_TAB or id == ImageImportOptions.TILESET:
frame_size_label.visible = true frame_size_label.visible = true
spritesheet_options.visible = true spritesheet_options.visible = true
texture_rect.get_child(0).visible = true texture_rect.get_child(0).visible = true
@ -505,6 +522,7 @@ func _call_queue_redraw() -> void:
if ( if (
current_import_option == ImageImportOptions.SPRITESHEET_TAB current_import_option == ImageImportOptions.SPRITESHEET_TAB
or current_import_option == ImageImportOptions.SPRITESHEET_LAYER or current_import_option == ImageImportOptions.SPRITESHEET_LAYER
or current_import_option == ImageImportOptions.TILESET
): ):
if smart_slice: if smart_slice:
if is_instance_valid(sliced_rects) and not sliced_rects.rects.is_empty(): if is_instance_valid(sliced_rects) and not sliced_rects.rects.is_empty():

View file

@ -223,10 +223,9 @@ text = "Brush type:"
[node name="BrushTypeOption" type="OptionButton" parent="VBoxContainer/ImportOptionsContainer/ImportOptions/NewBrushOptions/Type"] [node name="BrushTypeOption" type="OptionButton" parent="VBoxContainer/ImportOptionsContainer/ImportOptions/NewBrushOptions/Type"]
layout_mode = 2 layout_mode = 2
mouse_default_cursor_shape = 2 mouse_default_cursor_shape = 2
item_count = 3
selected = 0 selected = 0
item_count = 3
popup/item_0/text = "File brush" popup/item_0/text = "File brush"
popup/item_0/id = 0
popup/item_1/text = "Project brush" popup/item_1/text = "Project brush"
popup/item_1/id = 1 popup/item_1/id = 1
popup/item_2/text = "Random brush" popup/item_2/text = "Random brush"

View file

@ -1,11 +1,16 @@
extends AcceptDialog extends AcceptDialog
@onready var size_value_label := $GridContainer/SizeValueLabel as Label const DUPLICATE_TEXTURE := preload("res://assets/graphics/timeline/copy_frame.png")
@onready var color_mode_value_label := $GridContainer/ColorModeValueLabel as Label const REMOVE_TEXTURE := preload("res://assets/graphics/misc/close.png")
@onready var frames_value_label := $GridContainer/FramesValueLabel as Label
@onready var layers_value_label := $GridContainer/LayersValueLabel as Label @onready var size_value_label := $VBoxContainer/GridContainer/SizeValueLabel as Label
@onready var name_line_edit := $GridContainer/NameLineEdit as LineEdit @onready var color_mode_value_label := $VBoxContainer/GridContainer/ColorModeValueLabel as Label
@onready var user_data_text_edit := $GridContainer/UserDataTextEdit as TextEdit @onready var frames_value_label := $VBoxContainer/GridContainer/FramesValueLabel as Label
@onready var layers_value_label := $VBoxContainer/GridContainer/LayersValueLabel as Label
@onready var name_line_edit := $VBoxContainer/GridContainer/NameLineEdit as LineEdit
@onready var user_data_text_edit := $VBoxContainer/GridContainer/UserDataTextEdit as TextEdit
@onready var tilesets_container := $VBoxContainer/TilesetsContainer as VBoxContainer
@onready var tilesets_list := $VBoxContainer/TilesetsContainer/TilesetsList as Tree
func _on_visibility_changed() -> void: func _on_visibility_changed() -> void:
@ -21,6 +26,30 @@ func _on_visibility_changed() -> void:
layers_value_label.text = str(Global.current_project.layers.size()) layers_value_label.text = str(Global.current_project.layers.size())
name_line_edit.text = Global.current_project.name name_line_edit.text = Global.current_project.name
user_data_text_edit.text = Global.current_project.user_data user_data_text_edit.text = Global.current_project.user_data
tilesets_container.visible = Global.current_project.tilesets.size() > 0
tilesets_list.clear()
var root_item := tilesets_list.create_item()
for i in Global.current_project.tilesets.size():
_create_tileset_tree_item(i, root_item)
func _create_tileset_tree_item(i: int, root_item: TreeItem) -> void:
var tileset := Global.current_project.tilesets[i]
var tree_item := tilesets_list.create_item(root_item)
var item_text := tileset.get_text_info(i)
var using_layers := tileset.find_using_layers(Global.current_project)
for j in using_layers.size():
if j == 0:
item_text += " ("
item_text += using_layers[j].name
if j == using_layers.size() - 1:
item_text += ")"
else:
item_text += ", "
tree_item.set_text(0, item_text)
tree_item.set_metadata(0, i)
tree_item.add_button(0, DUPLICATE_TEXTURE, -1, false, "Duplicate")
tree_item.add_button(0, REMOVE_TEXTURE, -1, using_layers.size() > 0, "Delete")
func _on_name_line_edit_text_changed(new_text: String) -> void: func _on_name_line_edit_text_changed(new_text: String) -> void:
@ -29,3 +58,35 @@ func _on_name_line_edit_text_changed(new_text: String) -> void:
func _on_user_data_text_edit_text_changed() -> void: func _on_user_data_text_edit_text_changed() -> void:
Global.current_project.user_data = user_data_text_edit.text Global.current_project.user_data = user_data_text_edit.text
func _on_tilesets_list_button_clicked(item: TreeItem, column: int, id: int, _mbi: int) -> void:
var tileset_index: int = item.get_metadata(column)
var project := Global.current_project
var tileset := project.tilesets[tileset_index]
if id == 0: # Duplicate
var new_tileset := TileSetCustom.new(tileset.tile_size, tileset.name)
for i in range(1, tileset.tiles.size()):
var tile := tileset.tiles[i]
var new_image := Image.new()
new_image.copy_from(tile.image)
new_tileset.add_tile(new_image, null)
project.undos += 1
project.undo_redo.create_action("Duplicate tileset")
project.undo_redo.add_do_method(func(): project.tilesets.append(new_tileset))
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
project.undo_redo.add_undo_method(func(): project.tilesets.erase(new_tileset))
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
project.undo_redo.commit_action()
_create_tileset_tree_item(item.get_parent().get_child_count(), item.get_parent())
if id == 1: # Delete
if tileset.find_using_layers(project).size() > 0:
return
project.undos += 1
project.undo_redo.create_action("Delete tileset")
project.undo_redo.add_do_method(func(): project.tilesets.erase(tileset))
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
project.undo_redo.add_undo_method(func(): project.tilesets.insert(tileset_index, tileset))
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
project.undo_redo.commit_action()
item.free()

View file

@ -4,75 +4,103 @@
[node name="ProjectProperties" type="AcceptDialog"] [node name="ProjectProperties" type="AcceptDialog"]
title = "Project Properties" title = "Project Properties"
size = Vector2i(197, 235) position = Vector2i(0, 36)
size = Vector2i(300, 288)
script = ExtResource("1_0n4uc") script = ExtResource("1_0n4uc")
[node name="GridContainer" type="GridContainer" parent="."] [node name="VBoxContainer" type="VBoxContainer" parent="."]
offset_left = 8.0 offset_left = 8.0
offset_top = 8.0 offset_top = 8.0
offset_right = 189.0 offset_right = 292.0
offset_bottom = 186.0 offset_bottom = 239.0
[node name="GridContainer" type="GridContainer" parent="VBoxContainer"]
layout_mode = 2
columns = 2 columns = 2
[node name="SizeLabel" type="Label" parent="GridContainer"] [node name="SizeLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
text = "Size:" text = "Size:"
[node name="SizeValueLabel" type="Label" parent="GridContainer"] [node name="SizeValueLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
text = "64x64" text = "64x64"
[node name="ColorModeLabel" type="Label" parent="GridContainer"] [node name="ColorModeLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
text = "Color mode:" text = "Color mode:"
[node name="ColorModeValueLabel" type="Label" parent="GridContainer"] [node name="ColorModeValueLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
text = "RGBA8" text = "RGBA8"
[node name="FramesLabel" type="Label" parent="GridContainer"] [node name="FramesLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
text = "Frames:" text = "Frames:"
[node name="FramesValueLabel" type="Label" parent="GridContainer"] [node name="FramesValueLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
text = "1" text = "1"
[node name="LayersLabel" type="Label" parent="GridContainer"] [node name="LayersLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
text = "Layers:" text = "Layers:"
[node name="LayersValueLabel" type="Label" parent="GridContainer"] [node name="LayersValueLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
text = "1" text = "1"
[node name="NameLabel" type="Label" parent="GridContainer"] [node name="NameLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
text = "Name:" text = "Name:"
[node name="NameLineEdit" type="LineEdit" parent="GridContainer"] [node name="NameLineEdit" type="LineEdit" parent="VBoxContainer/GridContainer"]
layout_mode = 2 layout_mode = 2
[node name="UserDataLabel" type="Label" parent="GridContainer"] [node name="UserDataLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 0 size_flags_vertical = 0
text = "User data:" text = "User data:"
[node name="UserDataTextEdit" type="TextEdit" parent="GridContainer"] [node name="UserDataTextEdit" type="TextEdit" parent="VBoxContainer/GridContainer"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
scroll_fit_content_height = true scroll_fit_content_height = true
[node name="TilesetsContainer" type="VBoxContainer" parent="VBoxContainer"]
visible = false
layout_mode = 2
size_flags_vertical = 3
[node name="TilesetsHeader" type="HBoxContainer" parent="VBoxContainer/TilesetsContainer"]
layout_mode = 2
theme_override_constants/separation = 0
[node name="Label" type="Label" parent="VBoxContainer/TilesetsContainer/TilesetsHeader"]
layout_mode = 2
theme_type_variation = &"HeaderSmall"
text = "Tilesets"
[node name="HSeparator" type="HSeparator" parent="VBoxContainer/TilesetsContainer/TilesetsHeader"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TilesetsList" type="Tree" parent="VBoxContainer/TilesetsContainer"]
layout_mode = 2
size_flags_vertical = 3
hide_root = true
[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] [connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]
[connection signal="text_changed" from="GridContainer/NameLineEdit" to="." method="_on_name_line_edit_text_changed"] [connection signal="text_changed" from="VBoxContainer/GridContainer/NameLineEdit" to="." method="_on_name_line_edit_text_changed"]
[connection signal="text_changed" from="GridContainer/UserDataTextEdit" to="." method="_on_user_data_text_edit_text_changed"] [connection signal="text_changed" from="VBoxContainer/GridContainer/UserDataTextEdit" to="." method="_on_user_data_text_edit_text_changed"]
[connection signal="button_clicked" from="VBoxContainer/TilesetsContainer/TilesetsList" to="." method="_on_tilesets_list_button_clicked"]

207
src/UI/TilesPanel.gd Normal file
View file

@ -0,0 +1,207 @@
class_name TileSetPanel
extends PanelContainer
enum TileEditingMode { MANUAL, AUTO, STACK }
const TRANSPARENT_CHECKER := preload("res://src/UI/Nodes/TransparentChecker.tscn")
const MIN_BUTTON_SIZE := 36
const MAX_BUTTON_SIZE := 144
## A matrix with every possible flip/transpose combination,
## sorted by what comes next when you rotate.
## Taken from Godot's rotation matrix found in:
## https://github.com/godotengine/godot/blob/master/editor/plugins/tiles/tile_map_layer_editor.cpp
const ROTATION_MATRIX: Array[bool] = [
0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1
]
static var placing_tiles := false:
set(value):
placing_tiles = value
_call_update_brushes()
static var tile_editing_mode := TileEditingMode.AUTO
static var selected_tile_index := 0:
set(value):
selected_tile_index = value
_call_update_brushes()
static var is_flipped_h := false:
set(value):
is_flipped_h = value
_call_update_brushes()
static var is_flipped_v := false:
set(value):
is_flipped_v = value
_call_update_brushes()
static var is_transposed := false:
set(value):
is_transposed = value
_call_update_brushes()
var current_tileset: TileSetCustom
var button_size := 36:
set(value):
if button_size == value:
return
button_size = clampi(value, MIN_BUTTON_SIZE, MAX_BUTTON_SIZE)
update_minimum_size()
Global.config_cache.set_value("tileset_panel", "button_size", button_size)
for button: Control in tile_button_container.get_children():
button.custom_minimum_size = Vector2(button_size, button_size)
button.size = Vector2(button_size, button_size)
@onready var place_tiles: CheckBox = $VBoxContainer/PlaceTiles
@onready var transform_buttons_container: HFlowContainer = $VBoxContainer/TransformButtonsContainer
@onready var tile_button_container: HFlowContainer = %TileButtonContainer
func _ready() -> void:
Tools.selected_tile_index_changed.connect(select_tile)
Global.cel_switched.connect(_on_cel_switched)
for child: Button in transform_buttons_container.get_children():
Global.disable_button(child, true)
func _gui_input(event: InputEvent) -> void:
if Input.is_key_pressed(KEY_CTRL):
var zoom := 2 * int(event.is_action("zoom_in")) - 2 * int(event.is_action("zoom_out"))
button_size += zoom
if zoom != 0:
get_viewport().set_input_as_handled()
func set_tileset(tileset: TileSetCustom) -> void:
if tileset == current_tileset:
return
if is_instance_valid(current_tileset) and current_tileset.updated.is_connected(_update_tileset):
current_tileset.updated.disconnect(_update_tileset)
current_tileset = tileset
if (
is_instance_valid(current_tileset)
and not current_tileset.updated.is_connected(_update_tileset)
):
current_tileset.updated.connect(_update_tileset)
func _on_cel_switched() -> void:
if Global.current_project.get_current_cel() is not CelTileMap:
set_tileset(null)
_clear_tile_buttons()
return
var cel := Global.current_project.get_current_cel() as CelTileMap
set_tileset(cel.tileset)
_update_tileset(cel, -1)
func _update_tileset(cel: BaseCel, _replace_index: int) -> void:
_clear_tile_buttons()
if cel is not CelTileMap:
return
var tilemap_cel := cel as CelTileMap
var tileset := tilemap_cel.tileset
var button_group := ButtonGroup.new()
if selected_tile_index >= tileset.tiles.size():
selected_tile_index = 0
for i in tileset.tiles.size():
var tile := tileset.tiles[i]
var texture := ImageTexture.create_from_image(tile.image)
var button := _create_tile_button(texture, i, button_group)
if i == selected_tile_index:
button.set_pressed_no_signal(true)
tile_button_container.add_child(button)
func _create_tile_button(texture: Texture2D, index: int, button_group: ButtonGroup) -> Button:
var button := Button.new()
button.button_group = button_group
button.toggle_mode = true
button.custom_minimum_size = Vector2(button_size, button_size)
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
var texture_rect := TextureRect.new()
texture_rect.texture = texture
texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
texture_rect.set_anchor_and_offset(SIDE_LEFT, 0, 6)
texture_rect.set_anchor_and_offset(SIDE_RIGHT, 1, -6)
texture_rect.set_anchor_and_offset(SIDE_TOP, 0, 6)
texture_rect.set_anchor_and_offset(SIDE_BOTTOM, 1, -6)
texture_rect.grow_horizontal = Control.GROW_DIRECTION_BOTH
texture_rect.grow_vertical = Control.GROW_DIRECTION_BOTH
var transparent_checker := TRANSPARENT_CHECKER.instantiate() as ColorRect
transparent_checker.set_anchors_preset(Control.PRESET_FULL_RECT)
transparent_checker.show_behind_parent = true
texture_rect.add_child(transparent_checker)
button.add_child(texture_rect)
button.tooltip_text = str(index)
button.toggled.connect(_on_tile_button_toggled.bind(index))
return button
func select_tile(tile_index: int) -> void:
tile_button_container.get_child(tile_index).button_pressed = true
static func _call_update_brushes() -> void:
for slot in Tools._slots.values():
if slot.tool_node is BaseDrawTool:
slot.tool_node.update_brush()
func _on_tile_button_toggled(toggled_on: bool, index: int) -> void:
if toggled_on:
selected_tile_index = index
place_tiles.button_pressed = true
func _clear_tile_buttons() -> void:
for child in tile_button_container.get_children():
child.queue_free()
func _on_place_tiles_toggled(toggled_on: bool) -> void:
placing_tiles = toggled_on
for child: Button in transform_buttons_container.get_children():
Global.disable_button(child, not toggled_on)
func _on_manual_toggled(toggled_on: bool) -> void:
place_tiles.button_pressed = false
if toggled_on:
tile_editing_mode = TileEditingMode.MANUAL
func _on_auto_toggled(toggled_on: bool) -> void:
place_tiles.button_pressed = false
if toggled_on:
tile_editing_mode = TileEditingMode.AUTO
func _on_stack_toggled(toggled_on: bool) -> void:
place_tiles.button_pressed = false
if toggled_on:
tile_editing_mode = TileEditingMode.STACK
func _on_flip_horizontal_button_pressed() -> void:
is_flipped_h = not is_flipped_h
func _on_flip_vertical_button_pressed() -> void:
is_flipped_v = not is_flipped_v
func _on_rotate_pressed(clockwise: bool) -> void:
for i in ROTATION_MATRIX.size():
var final_i := i
if (
is_flipped_h == ROTATION_MATRIX[i * 3]
&& is_flipped_v == ROTATION_MATRIX[i * 3 + 1]
&& is_transposed == ROTATION_MATRIX[i * 3 + 2]
):
if clockwise:
@warning_ignore("integer_division")
final_i = i / 4 * 4 + posmod(i - 1, 4)
else:
@warning_ignore("integer_division")
final_i = i / 4 * 4 + (i + 1) % 4
is_flipped_h = ROTATION_MATRIX[final_i * 3]
is_flipped_v = ROTATION_MATRIX[final_i * 3 + 1]
is_transposed = ROTATION_MATRIX[final_i * 3 + 2]
break

189
src/UI/TilesPanel.tscn Normal file
View file

@ -0,0 +1,189 @@
[gd_scene load_steps=22 format=3 uid="uid://bfbragmmdwfbl"]
[ext_resource type="Script" path="res://src/UI/TilesPanel.gd" id="1_d2oc5"]
[ext_resource type="Texture2D" uid="uid://bv7ldl8obhawm" path="res://assets/graphics/misc/icon_reload.png" id="2_r1kie"]
[ext_resource type="Texture2D" uid="uid://bpsfilx47bw3r" path="res://assets/graphics/misc/mirror_x.svg" id="3_5o62r"]
[ext_resource type="Texture2D" uid="uid://bk6iaxiyl74ih" path="res://assets/graphics/misc/mirror_y.svg" id="4_2xhnr"]
[sub_resource type="InputEventAction" id="InputEventAction_klv67"]
action = &"toggle_draw_tiles_mode"
[sub_resource type="Shortcut" id="Shortcut_6ebuw"]
events = [SubResource("InputEventAction_klv67")]
[sub_resource type="InputEventAction" id="InputEventAction_yr0lx"]
action = &"tile_rotate_left"
[sub_resource type="Shortcut" id="Shortcut_yas23"]
events = [SubResource("InputEventAction_yr0lx")]
[sub_resource type="InputEventAction" id="InputEventAction_g6d5p"]
action = &"tile_rotate_right"
[sub_resource type="Shortcut" id="Shortcut_cmy2w"]
events = [SubResource("InputEventAction_g6d5p")]
[sub_resource type="InputEventAction" id="InputEventAction_yh67l"]
action = &"tile_flip_horizontal"
[sub_resource type="Shortcut" id="Shortcut_ouoxo"]
events = [SubResource("InputEventAction_yh67l")]
[sub_resource type="InputEventAction" id="InputEventAction_18g3a"]
action = &"tile_flip_vertical"
[sub_resource type="Shortcut" id="Shortcut_jj4yy"]
events = [SubResource("InputEventAction_18g3a")]
[sub_resource type="ButtonGroup" id="ButtonGroup_uxnt0"]
[sub_resource type="InputEventAction" id="InputEventAction_mhgo3"]
action = &"tile_edit_mode_manual"
[sub_resource type="Shortcut" id="Shortcut_pgg48"]
events = [SubResource("InputEventAction_mhgo3")]
[sub_resource type="InputEventAction" id="InputEventAction_h1wos"]
action = &"tile_edit_mode_auto"
[sub_resource type="Shortcut" id="Shortcut_a0fx5"]
events = [SubResource("InputEventAction_h1wos")]
[sub_resource type="InputEventAction" id="InputEventAction_i4ufh"]
action = &"tile_edit_mode_stack"
[sub_resource type="Shortcut" id="Shortcut_ysxej"]
events = [SubResource("InputEventAction_i4ufh")]
[node name="Tiles" type="PanelContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_d2oc5")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
[node name="PlaceTiles" type="CheckBox" parent="VBoxContainer"]
layout_mode = 2
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_6ebuw")
text = "Draw tiles"
[node name="TransformButtonsContainer" type="HFlowContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="RotateLeftButton" type="Button" parent="VBoxContainer/TransformButtonsContainer" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
tooltip_text = "Rotate tile left (counterclockwise)"
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_yas23")
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/TransformButtonsContainer/RotateLeftButton"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("2_r1kie")
stretch_mode = 3
[node name="RotateRightButton" type="Button" parent="VBoxContainer/TransformButtonsContainer" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
tooltip_text = "Rotate tile right (clockwise)"
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_cmy2w")
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/TransformButtonsContainer/RotateRightButton"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("2_r1kie")
stretch_mode = 3
flip_h = true
[node name="FlipHorizontalButton" type="Button" parent="VBoxContainer/TransformButtonsContainer" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
tooltip_text = "Flip tile horizontally"
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_ouoxo")
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/TransformButtonsContainer/FlipHorizontalButton"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("3_5o62r")
stretch_mode = 3
[node name="FlipVerticalButton" type="Button" parent="VBoxContainer/TransformButtonsContainer" groups=["UIButtons"]]
custom_minimum_size = Vector2(24, 24)
layout_mode = 2
tooltip_text = "Flip tile vertically"
mouse_default_cursor_shape = 2
shortcut = SubResource("Shortcut_jj4yy")
[node name="TextureRect" type="TextureRect" parent="VBoxContainer/TransformButtonsContainer/FlipVerticalButton"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("4_2xhnr")
stretch_mode = 3
[node name="ModeButtonsContainer" type="HFlowContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="Manual" type="CheckBox" parent="VBoxContainer/ModeButtonsContainer"]
layout_mode = 2
mouse_default_cursor_shape = 2
button_group = SubResource("ButtonGroup_uxnt0")
shortcut = SubResource("Shortcut_pgg48")
text = "Manual"
[node name="Auto" type="CheckBox" parent="VBoxContainer/ModeButtonsContainer"]
layout_mode = 2
mouse_default_cursor_shape = 2
button_pressed = true
button_group = SubResource("ButtonGroup_uxnt0")
shortcut = SubResource("Shortcut_a0fx5")
text = "Auto"
[node name="Stack" type="CheckBox" parent="VBoxContainer/ModeButtonsContainer"]
layout_mode = 2
mouse_default_cursor_shape = 2
button_group = SubResource("ButtonGroup_uxnt0")
shortcut = SubResource("Shortcut_ysxej")
text = "Stack"
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="TileButtonContainer" type="HFlowContainer" parent="VBoxContainer/ScrollContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[connection signal="toggled" from="VBoxContainer/PlaceTiles" to="." method="_on_place_tiles_toggled"]
[connection signal="pressed" from="VBoxContainer/TransformButtonsContainer/RotateLeftButton" to="." method="_on_rotate_pressed" binds= [false]]
[connection signal="pressed" from="VBoxContainer/TransformButtonsContainer/RotateRightButton" to="." method="_on_rotate_pressed" binds= [true]]
[connection signal="pressed" from="VBoxContainer/TransformButtonsContainer/FlipHorizontalButton" to="." method="_on_flip_horizontal_button_pressed"]
[connection signal="pressed" from="VBoxContainer/TransformButtonsContainer/FlipVerticalButton" to="." method="_on_flip_vertical_button_pressed"]
[connection signal="toggled" from="VBoxContainer/ModeButtonsContainer/Manual" to="." method="_on_manual_toggled"]
[connection signal="toggled" from="VBoxContainer/ModeButtonsContainer/Auto" to="." method="_on_auto_toggled"]
[connection signal="toggled" from="VBoxContainer/ModeButtonsContainer/Stack" to="." method="_on_stack_toggled"]

View file

@ -55,9 +55,10 @@ var global_layer_expand := true
@onready var play_forward := %PlayForward as Button @onready var play_forward := %PlayForward as Button
@onready var fps_spinbox := %FPSValue as ValueSlider @onready var fps_spinbox := %FPSValue as ValueSlider
@onready var onion_skinning_button := %OnionSkinning as BaseButton @onready var onion_skinning_button := %OnionSkinning as BaseButton
@onready var timeline_settings := $TimelineSettings as Popup
@onready var cel_size_slider := %CelSizeSlider as ValueSlider @onready var cel_size_slider := %CelSizeSlider as ValueSlider
@onready var loop_animation_button := %LoopAnim as BaseButton @onready var loop_animation_button := %LoopAnim as BaseButton
@onready var timeline_settings := $TimelineSettings as Popup
@onready var new_tile_map_layer_dialog := $NewTileMapLayerDialog as ConfirmationDialog
@onready var drag_highlight := $DragHighlight as ColorRect @onready var drag_highlight := $DragHighlight as ColorRect
@ -70,7 +71,7 @@ func _ready() -> void:
cel_size_slider.min_value = min_cel_size cel_size_slider.min_value = min_cel_size
cel_size_slider.max_value = max_cel_size cel_size_slider.max_value = max_cel_size
cel_size_slider.value = cel_size cel_size_slider.value = cel_size
add_layer_list.get_popup().id_pressed.connect(add_layer) add_layer_list.get_popup().id_pressed.connect(_on_add_layer_list_id_pressed)
frame_scroll_bar.value_changed.connect(_frame_scroll_changed) frame_scroll_bar.value_changed.connect(_frame_scroll_changed)
animation_timer.wait_time = 1 / Global.current_project.fps animation_timer.wait_time = 1 / Global.current_project.fps
fps_spinbox.value = Global.current_project.fps fps_spinbox.value = Global.current_project.fps
@ -475,6 +476,8 @@ func copy_frames(
) )
if src_cel.selected != null: if src_cel.selected != null:
selected_id = src_cel.selected.id selected_id = src_cel.selected.id
elif src_cel is CelTileMap:
new_cel = CelTileMap.new(src_cel.tileset)
else: else:
new_cel = src_cel.get_script().new() new_cel = src_cel.get_script().new()
@ -832,24 +835,34 @@ func _on_FuturePlacement_item_selected(index: int) -> void:
# Layer buttons # Layer buttons
func _on_add_layer_pressed() -> void:
func add_layer(type := 0) -> void:
var project := Global.current_project var project := Global.current_project
var current_layer := project.layers[project.current_layer] var layer := PixelLayer.new(project)
var l: BaseLayer add_layer(layer, project)
match type:
Global.LayerTypes.PIXEL:
l = PixelLayer.new(project)
Global.LayerTypes.GROUP:
l = GroupLayer.new(project)
Global.LayerTypes.THREE_D:
l = Layer3D.new(project)
SteamManager.set_achievement("ACH_3D_LAYER")
func _on_add_layer_list_id_pressed(id: int) -> void:
if id == Global.LayerTypes.TILEMAP:
new_tile_map_layer_dialog.popup_centered()
else:
var project := Global.current_project
var layer: BaseLayer
match id:
Global.LayerTypes.PIXEL:
layer = PixelLayer.new(project)
Global.LayerTypes.GROUP:
layer = GroupLayer.new(project)
Global.LayerTypes.THREE_D:
layer = Layer3D.new(project)
SteamManager.set_achievement("ACH_3D_LAYER")
add_layer(layer, project)
func add_layer(layer: BaseLayer, project: Project) -> void:
var current_layer := project.layers[project.current_layer]
var cels := [] var cels := []
for f in project.frames: for f in project.frames:
cels.append(l.new_empty_cel()) cels.append(layer.new_empty_cel())
var new_layer_idx := project.current_layer + 1 var new_layer_idx := project.current_layer + 1
if current_layer is GroupLayer: if current_layer is GroupLayer:
@ -862,14 +875,14 @@ func add_layer(type := 0) -> void:
layer_button.visible = expanded layer_button.visible = expanded
Global.cel_vbox.get_child(layer_button.get_index()).visible = expanded Global.cel_vbox.get_child(layer_button.get_index()).visible = expanded
# make layer child of group # make layer child of group
l.parent = Global.current_project.layers[project.current_layer] layer.parent = Global.current_project.layers[project.current_layer]
else: else:
# set the parent of layer to be the same as the layer below it # set the parent of layer to be the same as the layer below it
l.parent = Global.current_project.layers[project.current_layer].parent layer.parent = Global.current_project.layers[project.current_layer].parent
project.undos += 1 project.undos += 1
project.undo_redo.create_action("Add Layer") project.undo_redo.create_action("Add Layer")
project.undo_redo.add_do_method(project.add_layers.bind([l], [new_layer_idx], [cels])) project.undo_redo.add_do_method(project.add_layers.bind([layer], [new_layer_idx], [cels]))
project.undo_redo.add_undo_method(project.remove_layers.bind([new_layer_idx])) project.undo_redo.add_undo_method(project.remove_layers.bind([new_layer_idx]))
project.undo_redo.add_do_method(project.change_cel.bind(-1, new_layer_idx)) project.undo_redo.add_do_method(project.change_cel.bind(-1, new_layer_idx))
project.undo_redo.add_undo_method(project.change_cel.bind(-1, project.current_layer)) project.undo_redo.add_undo_method(project.change_cel.bind(-1, project.current_layer))
@ -886,7 +899,11 @@ func _on_CloneLayer_pressed() -> void:
var clones: Array[BaseLayer] = [] var clones: Array[BaseLayer] = []
var cels := [] # 2D Array of Cels var cels := [] # 2D Array of Cels
for src_layer in source_layers: for src_layer in source_layers:
var cl_layer: BaseLayer = src_layer.get_script().new(project) var cl_layer: BaseLayer
if src_layer is LayerTileMap:
cl_layer = LayerTileMap.new(project, src_layer.tileset)
else:
cl_layer = src_layer.get_script().new(project)
cl_layer.project = project cl_layer.project = project
cl_layer.index = src_layer.index cl_layer.index = src_layer.index
var src_layer_data: Dictionary = src_layer.serialize() var src_layer_data: Dictionary = src_layer.serialize()
@ -904,6 +921,8 @@ func _on_CloneLayer_pressed() -> void:
new_cel = Cel3D.new( new_cel = Cel3D.new(
src_cel.size, false, src_cel.object_properties, src_cel.scene_properties src_cel.size, false, src_cel.object_properties, src_cel.scene_properties
) )
elif src_cel is CelTileMap:
new_cel = CelTileMap.new(src_cel.tileset)
else: else:
new_cel = src_cel.get_script().new() new_cel = src_cel.get_script().new()
@ -1085,11 +1104,15 @@ func _on_MergeDownLayer_pressed() -> void:
project.undo_redo.add_do_property(bottom_cel, "image", new_bottom_image) project.undo_redo.add_do_property(bottom_cel, "image", new_bottom_image)
project.undo_redo.add_undo_property(bottom_cel, "image", bottom_cel.image) project.undo_redo.add_undo_property(bottom_cel, "image", bottom_cel.image)
else: else:
var redo_data := {}
var undo_data := {} var undo_data := {}
var redo_data := {}
if bottom_cel is CelTileMap:
(bottom_cel as CelTileMap).serialize_undo_data_source_image(
new_bottom_image, redo_data, undo_data
)
new_bottom_image.add_data_to_dictionary(redo_data, bottom_image) new_bottom_image.add_data_to_dictionary(redo_data, bottom_image)
bottom_image.add_data_to_dictionary(undo_data) bottom_image.add_data_to_dictionary(undo_data)
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(project.remove_layers.bind([top_layer.index])) project.undo_redo.add_do_method(project.remove_layers.bind([top_layer.index]))
project.undo_redo.add_undo_method( project.undo_redo.add_undo_method(

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=76 format=3 uid="uid://dbr6mulku2qju"] [gd_scene load_steps=77 format=3 uid="uid://dbr6mulku2qju"]
[ext_resource type="Script" path="res://src/UI/Timeline/AnimationTimeline.gd" id="1"] [ext_resource type="Script" path="res://src/UI/Timeline/AnimationTimeline.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://d36mlbmq06q4e" path="res://assets/graphics/layers/new.png" id="2"] [ext_resource type="Texture2D" uid="uid://d36mlbmq06q4e" path="res://assets/graphics/layers/new.png" id="2"]
@ -26,6 +26,7 @@
[ext_resource type="Texture2D" uid="uid://cerkv5yx4cqeh" path="res://assets/graphics/timeline/copy_frame.png" id="27"] [ext_resource type="Texture2D" uid="uid://cerkv5yx4cqeh" path="res://assets/graphics/timeline/copy_frame.png" id="27"]
[ext_resource type="Texture2D" uid="uid://dndlglvqc7v6a" path="res://assets/graphics/layers/group_expanded.png" id="27_lrc8y"] [ext_resource type="Texture2D" uid="uid://dndlglvqc7v6a" path="res://assets/graphics/layers/group_expanded.png" id="27_lrc8y"]
[ext_resource type="Texture2D" uid="uid://dukip7mvotxsp" path="res://assets/graphics/timeline/onion_skinning_off.png" id="29"] [ext_resource type="Texture2D" uid="uid://dukip7mvotxsp" path="res://assets/graphics/timeline/onion_skinning_off.png" id="29"]
[ext_resource type="PackedScene" uid="uid://hbgwxlin4jun" path="res://src/UI/Timeline/NewTileMapLayerDialog.tscn" id="29_t0mtf"]
[ext_resource type="Texture2D" uid="uid://dinubfua8gqhw" path="res://assets/graphics/timeline/expandable.png" id="30"] [ext_resource type="Texture2D" uid="uid://dinubfua8gqhw" path="res://assets/graphics/timeline/expandable.png" id="30"]
[ext_resource type="Texture2D" uid="uid://fbwld5ofmocm" path="res://assets/graphics/timeline/loop.png" id="31"] [ext_resource type="Texture2D" uid="uid://fbwld5ofmocm" path="res://assets/graphics/timeline/loop.png" id="31"]
@ -239,12 +240,14 @@ offset_left = -22.0
offset_top = -10.0 offset_top = -10.0
offset_bottom = 10.0 offset_bottom = 10.0
mouse_default_cursor_shape = 2 mouse_default_cursor_shape = 2
item_count = 3 item_count = 4
popup/item_0/text = "Add Pixel Layer" popup/item_0/text = "Add Pixel Layer"
popup/item_1/text = "Add Group Layer" popup/item_1/text = "Add Group Layer"
popup/item_1/id = 1 popup/item_1/id = 1
popup/item_2/text = "Add 3D Layer" popup/item_2/text = "Add 3D Layer"
popup/item_2/id = 2 popup/item_2/id = 2
popup/item_3/text = "Add Tilemap Layer"
popup/item_3/id = 3
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons/AddLayer/AddLayerList"] [node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons/AddLayer/AddLayerList"]
layout_mode = 0 layout_mode = 0
@ -1114,6 +1117,8 @@ size_flags_horizontal = 0
mouse_default_cursor_shape = 2 mouse_default_cursor_shape = 2
text = "Color mode" text = "Color mode"
[node name="NewTileMapLayerDialog" parent="." instance=ExtResource("29_t0mtf")]
[node name="DragHighlight" type="ColorRect" parent="."] [node name="DragHighlight" type="ColorRect" parent="."]
visible = false visible = false
z_index = 2 z_index = 2
@ -1123,7 +1128,7 @@ offset_bottom = 40.0
mouse_filter = 2 mouse_filter = 2
color = Color(0, 0.741176, 1, 0.501961) color = Color(0, 0.741176, 1, 0.501961)
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons/AddLayer" to="." method="add_layer"] [connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons/AddLayer" to="." method="_on_add_layer_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons/RemoveLayer" to="." method="_on_RemoveLayer_pressed"] [connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons/RemoveLayer" to="." method="_on_RemoveLayer_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons/MoveUpLayer" to="." method="change_layer_order" binds= [true]] [connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons/MoveUpLayer" to="." method="change_layer_order" binds= [true]]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons/MoveDownLayer" to="." method="change_layer_order" binds= [false]] [connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons/MoveDownLayer" to="." method="change_layer_order" binds= [false]]

View file

@ -149,34 +149,41 @@ func _delete_effect(effect: LayerEffect) -> void:
func _apply_effect(layer: BaseLayer, effect: LayerEffect) -> void: func _apply_effect(layer: BaseLayer, effect: LayerEffect) -> void:
var project := Global.current_project
var index := layer.effects.find(effect) var index := layer.effects.find(effect)
var redo_data := {} var redo_data := {}
var undo_data := {} var undo_data := {}
for frame in Global.current_project.frames: for frame in project.frames:
var cel := frame.cels[layer.index] var cel := frame.cels[layer.index]
var new_image := ImageExtended.new()
var cel_image := cel.get_image() var cel_image := cel.get_image()
if cel is CelTileMap:
undo_data[cel] = (cel as CelTileMap).serialize_undo_data()
if cel_image is ImageExtended: if cel_image is ImageExtended:
new_image.is_indexed = cel_image.is_indexed
new_image.copy_from_custom(cel_image)
var image_size := new_image.get_size()
var shader_image_effect := ShaderImageEffect.new()
shader_image_effect.generate_image(new_image, effect.shader, effect.params, image_size)
if cel_image is ImageExtended:
redo_data[cel_image.indices_image] = new_image.indices_image.data
undo_data[cel_image.indices_image] = cel_image.indices_image.data undo_data[cel_image.indices_image] = cel_image.indices_image.data
redo_data[cel_image] = new_image.data
undo_data[cel_image] = cel_image.data undo_data[cel_image] = cel_image.data
Global.current_project.undos += 1 var image_size := cel_image.get_size()
Global.current_project.undo_redo.create_action("Apply layer effect") var shader_image_effect := ShaderImageEffect.new()
Global.undo_redo_compress_images(redo_data, undo_data) shader_image_effect.generate_image(cel_image, effect.shader, effect.params, image_size)
Global.current_project.undo_redo.add_do_method(func(): layer.effects.erase(effect))
Global.current_project.undo_redo.add_do_method(Global.canvas.queue_redraw) project.update_tilemaps(undo_data)
Global.current_project.undo_redo.add_do_method(Global.undo_or_redo.bind(false)) for frame in project.frames:
Global.current_project.undo_redo.add_undo_method(func(): layer.effects.insert(index, effect)) var cel := frame.cels[layer.index]
Global.current_project.undo_redo.add_undo_method(Global.canvas.queue_redraw) var cel_image := cel.get_image()
Global.current_project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true)) if cel is CelTileMap:
Global.current_project.undo_redo.commit_action() redo_data[cel] = (cel as CelTileMap).serialize_undo_data()
if cel_image is ImageExtended:
redo_data[cel_image.indices_image] = cel_image.indices_image.data
redo_data[cel_image] = cel_image.data
project.undos += 1
project.undo_redo.create_action("Apply layer effect")
project.deserialize_cel_undo_data(redo_data, undo_data)
project.undo_redo.add_do_method(func(): layer.effects.erase(effect))
project.undo_redo.add_do_method(Global.canvas.queue_redraw)
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
project.undo_redo.add_undo_method(func(): layer.effects.insert(index, effect))
project.undo_redo.add_undo_method(Global.canvas.queue_redraw)
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
project.undo_redo.commit_action()
effect_container.get_child(index).queue_free() effect_container.get_child(index).queue_free()

View file

@ -8,13 +8,15 @@ var layer_indices: PackedInt32Array
@onready var opacity_slider := $GridContainer/OpacitySlider as ValueSlider @onready var opacity_slider := $GridContainer/OpacitySlider as ValueSlider
@onready var blend_modes_button := $GridContainer/BlendModeOptionButton as OptionButton @onready var blend_modes_button := $GridContainer/BlendModeOptionButton as OptionButton
@onready var user_data_text_edit := $GridContainer/UserDataTextEdit as TextEdit @onready var user_data_text_edit := $GridContainer/UserDataTextEdit as TextEdit
@onready var tileset_option_button := $GridContainer/TilesetOptionButton as OptionButton
func _on_visibility_changed() -> void: func _on_visibility_changed() -> void:
if layer_indices.size() == 0: if layer_indices.size() == 0:
return return
Global.dialog_open(visible) Global.dialog_open(visible)
var first_layer := Global.current_project.layers[layer_indices[0]] var project := Global.current_project
var first_layer := project.layers[layer_indices[0]]
if visible: if visible:
_fill_blend_modes_option_button() _fill_blend_modes_option_button()
name_line_edit.text = first_layer.name name_line_edit.text = first_layer.name
@ -22,6 +24,14 @@ func _on_visibility_changed() -> void:
var blend_mode_index := blend_modes_button.get_item_index(first_layer.blend_mode) var blend_mode_index := blend_modes_button.get_item_index(first_layer.blend_mode)
blend_modes_button.selected = blend_mode_index blend_modes_button.selected = blend_mode_index
user_data_text_edit.text = first_layer.user_data user_data_text_edit.text = first_layer.user_data
get_tree().set_group(&"TilemapLayers", "visible", first_layer is LayerTileMap)
tileset_option_button.clear()
if first_layer is LayerTileMap:
for i in project.tilesets.size():
var tileset := project.tilesets[i]
tileset_option_button.add_item(tileset.get_text_info(i))
if tileset == first_layer.tileset:
tileset_option_button.select(i)
else: else:
layer_indices = [] layer_indices = []
@ -86,6 +96,7 @@ func _on_blend_mode_option_button_item_selected(index: BaseLayer.BlendModes) ->
Global.canvas.update_all_layers = true Global.canvas.update_all_layers = true
var project := Global.current_project var project := Global.current_project
var current_mode := blend_modes_button.get_item_id(index) var current_mode := blend_modes_button.get_item_id(index)
project.undos += 1
project.undo_redo.create_action("Set Blend Mode") project.undo_redo.create_action("Set Blend Mode")
for layer_index in layer_indices: for layer_index in layer_indices:
var layer := project.layers[layer_index] var layer := project.layers[layer_index]
@ -109,3 +120,32 @@ func _on_user_data_text_edit_text_changed() -> void:
func _emit_layer_property_signal() -> void: func _emit_layer_property_signal() -> void:
layer_property_changed.emit() layer_property_changed.emit()
func _on_tileset_option_button_item_selected(index: int) -> void:
var project := Global.current_project
var new_tileset := project.tilesets[index]
project.undos += 1
project.undo_redo.create_action("Set Tileset")
for layer_index in layer_indices:
var layer := project.layers[layer_index]
if layer is not LayerTileMap:
continue
var previous_tileset := (layer as LayerTileMap).tileset
project.undo_redo.add_do_property(layer, "tileset", new_tileset)
project.undo_redo.add_undo_property(layer, "tileset", previous_tileset)
for frame in project.frames:
for i in frame.cels.size():
var cel := frame.cels[i]
if cel is CelTileMap and i == layer_index:
project.undo_redo.add_do_method(cel.set_tileset.bind(new_tileset, false))
project.undo_redo.add_do_method(cel.update_cel_portions)
project.undo_redo.add_undo_method(cel.set_tileset.bind(previous_tileset, false))
project.undo_redo.add_undo_method(cel.update_cel_portions)
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
project.undo_redo.add_do_method(Global.canvas.draw_layers)
project.undo_redo.add_do_method(func(): Global.cel_switched.emit())
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
project.undo_redo.add_undo_method(Global.canvas.draw_layers)
project.undo_redo.add_undo_method(func(): Global.cel_switched.emit())
project.undo_redo.commit_action()

View file

@ -5,11 +5,14 @@
[node name="LayerProperties" type="AcceptDialog"] [node name="LayerProperties" type="AcceptDialog"]
title = "Layer properties" title = "Layer properties"
size = Vector2i(300, 208)
script = ExtResource("1_54q1t") script = ExtResource("1_54q1t")
[node name="GridContainer" type="GridContainer" parent="."] [node name="GridContainer" type="GridContainer" parent="."]
offset_right = 40.0 offset_left = 8.0
offset_bottom = 40.0 offset_top = 8.0
offset_right = 292.0
offset_bottom = 159.0
columns = 2 columns = 2
[node name="NameLabel" type="Label" parent="GridContainer"] [node name="NameLabel" type="Label" parent="GridContainer"]
@ -60,8 +63,19 @@ layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
scroll_fit_content_height = true scroll_fit_content_height = true
[node name="TilesetLabel" type="Label" parent="GridContainer" groups=["TilemapLayers"]]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 0
text = "Tileset:"
[node name="TilesetOptionButton" type="OptionButton" parent="GridContainer" groups=["TilemapLayers"]]
layout_mode = 2
mouse_default_cursor_shape = 2
[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] [connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]
[connection signal="text_changed" from="GridContainer/NameLineEdit" to="." method="_on_name_line_edit_text_changed"] [connection signal="text_changed" from="GridContainer/NameLineEdit" to="." method="_on_name_line_edit_text_changed"]
[connection signal="value_changed" from="GridContainer/OpacitySlider" to="." method="_on_opacity_slider_value_changed"] [connection signal="value_changed" from="GridContainer/OpacitySlider" to="." method="_on_opacity_slider_value_changed"]
[connection signal="item_selected" from="GridContainer/BlendModeOptionButton" to="." method="_on_blend_mode_option_button_item_selected"] [connection signal="item_selected" from="GridContainer/BlendModeOptionButton" to="." method="_on_blend_mode_option_button_item_selected"]
[connection signal="text_changed" from="GridContainer/UserDataTextEdit" to="." method="_on_user_data_text_edit_text_changed"] [connection signal="text_changed" from="GridContainer/UserDataTextEdit" to="." method="_on_user_data_text_edit_text_changed"]
[connection signal="item_selected" from="GridContainer/TilesetOptionButton" to="." method="_on_tileset_option_button_item_selected"]

View file

@ -0,0 +1,46 @@
extends ConfirmationDialog
@onready var animation_timeline := get_parent() as Control
@onready var name_line_edit: LineEdit = $GridContainer/NameLineEdit
@onready var tileset_option_button: OptionButton = $GridContainer/TilesetOptionButton
@onready var tileset_name_line_edit: LineEdit = $GridContainer/TilesetNameLineEdit
@onready var tile_size_slider: ValueSliderV2 = $GridContainer/TileSizeSlider
func _on_confirmed() -> void:
var project := Global.current_project
var layer_name := name_line_edit.text
var tileset_name := tileset_name_line_edit.text
var tile_size := tile_size_slider.value
var tileset: TileSetCustom
if tileset_option_button.selected == 0:
tileset = TileSetCustom.new(tile_size, tileset_name)
else:
tileset = project.tilesets[tileset_option_button.selected - 1]
var layer := LayerTileMap.new(project, tileset, layer_name)
animation_timeline.add_layer(layer, project)
func _on_visibility_changed() -> void:
Global.dialog_open(visible)
func _on_about_to_popup() -> void:
var project := Global.current_project
var default_name := tr("Tilemap") + " %s" % (project.layers.size() + 1)
name_line_edit.text = default_name
tileset_option_button.clear()
tileset_option_button.add_item("New tileset")
for i in project.tilesets.size():
var tileset := project.tilesets[i]
tileset_option_button.add_item(tileset.get_text_info(i))
_on_tileset_option_button_item_selected(tileset_option_button.selected)
func _on_tileset_option_button_item_selected(index: int) -> void:
if index > 0:
var tileset := Global.current_project.tilesets[index - 1]
tileset_name_line_edit.text = tileset.name
tile_size_slider.value = tileset.tile_size
tileset_name_line_edit.editable = index == 0
tile_size_slider.editable = tileset_name_line_edit.editable

View file

@ -0,0 +1,70 @@
[gd_scene load_steps=3 format=3 uid="uid://hbgwxlin4jun"]
[ext_resource type="PackedScene" path="res://src/UI/Nodes/ValueSliderV2.tscn" id="1_uvdem"]
[ext_resource type="Script" path="res://src/UI/Timeline/NewTileMapLayerDialog.gd" id="1_y2r5h"]
[node name="NewTileMapLayerDialog" type="ConfirmationDialog"]
title = "New layer"
position = Vector2i(0, 36)
size = Vector2i(300, 230)
script = ExtResource("1_y2r5h")
[node name="GridContainer" type="GridContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 292.0
offset_bottom = 181.0
columns = 2
[node name="NameLabel" type="Label" parent="GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Name:"
[node name="NameLineEdit" type="LineEdit" parent="GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Tilemap 1"
[node name="TilesetLabel" type="Label" parent="GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Tileset:"
[node name="TilesetOptionButton" type="OptionButton" parent="GridContainer"]
layout_mode = 2
mouse_default_cursor_shape = 2
selected = 0
item_count = 1
popup/item_0/text = "New tileset"
[node name="TilesetNameLabel" type="Label" parent="GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Tileset name:"
[node name="TilesetNameLineEdit" type="LineEdit" parent="GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TileSizeLabel" type="Label" parent="GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Tile size:"
[node name="TileSizeSlider" parent="GridContainer" instance=ExtResource("1_uvdem")]
layout_mode = 2
value = Vector2(16, 16)
min_value = Vector2(1, 1)
max_value = Vector2(128, 128)
allow_greater = true
show_ratio = true
prefix_x = "Width:"
prefix_y = "Height:"
suffix_x = "px"
suffix_y = "px"
[connection signal="about_to_popup" from="." to="." method="_on_about_to_popup"]
[connection signal="confirmed" from="." to="." method="_on_confirmed"]
[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]
[connection signal="item_selected" from="GridContainer/TilesetOptionButton" to="." method="_on_tileset_option_button_item_selected"]

View file

@ -372,9 +372,13 @@ func _setup_panels_submenu(item: String) -> void:
panels_submenu.set_name("panels_submenu") panels_submenu.set_name("panels_submenu")
panels_submenu.hide_on_checkable_item_selection = false panels_submenu.hide_on_checkable_item_selection = false
for element in ui_elements: for element in ui_elements:
panels_submenu.add_check_item(element.name) if element.name == "Tiles":
continue
var id := ui_elements.find(element)
panels_submenu.add_check_item(element.name, id)
var is_hidden: bool = main_ui.is_control_hidden(element) var is_hidden: bool = main_ui.is_control_hidden(element)
panels_submenu.set_item_checked(ui_elements.find(element), !is_hidden) var index := panels_submenu.get_item_index(id)
panels_submenu.set_item_checked(index, !is_hidden)
panels_submenu.id_pressed.connect(_panels_submenu_id_pressed) panels_submenu.id_pressed.connect(_panels_submenu_id_pressed)
window_menu.add_child(panels_submenu) window_menu.add_child(panels_submenu)
@ -718,21 +722,25 @@ func _color_mode_submenu_id_pressed(id: ColorModes) -> void:
var old_color_mode := project.color_mode var old_color_mode := project.color_mode
var redo_data := {} var redo_data := {}
var undo_data := {} var undo_data := {}
var pixel_cels: Array[BaseCel]
# We need to do it this way because Godot
# doesn't like casting typed arrays into other types.
for cel in project.get_all_pixel_cels(): for cel in project.get_all_pixel_cels():
cel.get_image().add_data_to_dictionary(undo_data) pixel_cels.append(cel)
project.serialize_cel_undo_data(pixel_cels, undo_data)
# Change the color mode directly before undo/redo in order to affect the images, # Change the color mode directly before undo/redo in order to affect the images,
# so we can store them as redo data. # so we can store them as redo data.
if id == ColorModes.RGBA: if id == ColorModes.RGBA:
project.color_mode = Image.FORMAT_RGBA8 project.color_mode = Image.FORMAT_RGBA8
else: else:
project.color_mode = Project.INDEXED_MODE project.color_mode = Project.INDEXED_MODE
for cel in project.get_all_pixel_cels(): project.update_tilemaps(undo_data)
cel.get_image().add_data_to_dictionary(redo_data) project.serialize_cel_undo_data(pixel_cels, redo_data)
project.undo_redo.create_action("Change color mode") project.undo_redo.create_action("Change color mode")
project.undos += 1 project.undos += 1
project.undo_redo.add_do_property(project, "color_mode", project.color_mode) project.undo_redo.add_do_property(project, "color_mode", project.color_mode)
project.undo_redo.add_undo_property(project, "color_mode", old_color_mode) project.undo_redo.add_undo_property(project, "color_mode", old_color_mode)
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(_check_color_mode_submenu_item.bind(project)) project.undo_redo.add_do_method(_check_color_mode_submenu_item.bind(project))
project.undo_redo.add_undo_method(_check_color_mode_submenu_item.bind(project)) project.undo_redo.add_undo_method(_check_color_mode_submenu_item.bind(project))
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false)) project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
@ -763,9 +771,10 @@ func _snap_to_submenu_id_pressed(id: int) -> void:
func _panels_submenu_id_pressed(id: int) -> void: func _panels_submenu_id_pressed(id: int) -> void:
if zen_mode: if zen_mode:
return return
var element_visible := panels_submenu.is_item_checked(id) var index := panels_submenu.get_item_index(id)
var element_visible := panels_submenu.is_item_checked(index)
main_ui.set_control_hidden(ui_elements[id], element_visible) main_ui.set_control_hidden(ui_elements[id], element_visible)
panels_submenu.set_item_checked(id, !element_visible) panels_submenu.set_item_checked(index, !element_visible)
func _layouts_submenu_id_pressed(id: int) -> void: func _layouts_submenu_id_pressed(id: int) -> void:
@ -787,8 +796,9 @@ func set_layout(id: int) -> void:
layouts_submenu.set_item_checked(offset, offset == (id + 1)) layouts_submenu.set_item_checked(offset, offset == (id + 1))
for i in ui_elements.size(): for i in ui_elements.size():
var index := panels_submenu.get_item_index(i)
var is_hidden := main_ui.is_control_hidden(ui_elements[i]) var is_hidden := main_ui.is_control_hidden(ui_elements[i])
panels_submenu.set_item_checked(i, !is_hidden) panels_submenu.set_item_checked(index, !is_hidden)
if zen_mode: # Turn zen mode off if zen_mode: # Turn zen mode off
Global.control.find_child("TabsContainer").visible = true Global.control.find_child("TabsContainer").visible = true
@ -866,9 +876,11 @@ func _toggle_show_mouse_guides() -> void:
func _toggle_zen_mode() -> void: func _toggle_zen_mode() -> void:
for i in ui_elements.size(): for i in ui_elements.size():
if ui_elements[i].name == "Main Canvas": var index := panels_submenu.get_item_index(i)
var panel_name := ui_elements[i].name
if panel_name == "Main Canvas" or panel_name == "Tiles":
continue continue
if !panels_submenu.is_item_checked(i): if !panels_submenu.is_item_checked(index):
continue continue
main_ui.set_control_hidden(ui_elements[i], !zen_mode) main_ui.set_control_hidden(ui_elements[i], !zen_mode)
Global.control.find_child("TabsContainer").visible = zen_mode Global.control.find_child("TabsContainer").visible = zen_mode

View file

@ -3,16 +3,25 @@ extends Panel
var shader_disabled := false var shader_disabled := false
var transparency_material: ShaderMaterial var transparency_material: ShaderMaterial
@onready var dockable_container: DockableContainer = $DockableContainer
@onready var main_canvas_container := find_child("Main Canvas") as Container @onready var main_canvas_container := find_child("Main Canvas") as Container
@onready var tiles: TileSetPanel = $DockableContainer/Tiles
func _ready() -> void: func _ready() -> void:
Global.cel_switched.connect(_on_cel_switched)
transparency_material = material transparency_material = material
main_canvas_container.property_list_changed.connect(_re_configure_shader) main_canvas_container.property_list_changed.connect(_re_configure_shader)
update_transparent_shader() update_transparent_shader()
dockable_container.set_control_hidden.call_deferred(tiles, true)
func _re_configure_shader(): func _on_cel_switched() -> void:
var cel := Global.current_project.get_current_cel()
dockable_container.set_control_hidden(tiles, cel is not CelTileMap)
func _re_configure_shader() -> void:
await get_tree().process_frame await get_tree().process_frame
if get_window() != main_canvas_container.get_window(): if get_window() != main_canvas_container.get_window():
material = null material = null

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=54 format=3 uid="uid://c8dsi6ggkqa7a"] [gd_scene load_steps=55 format=3 uid="uid://c8dsi6ggkqa7a"]
[ext_resource type="PackedScene" uid="uid://byu3rtoipuvoc" path="res://src/UI/ToolsPanel/Tools.tscn" id="1"] [ext_resource type="PackedScene" uid="uid://byu3rtoipuvoc" path="res://src/UI/ToolsPanel/Tools.tscn" id="1"]
[ext_resource type="PackedScene" uid="uid://c546tskdu53j1" path="res://src/UI/Canvas/CanvasPreview.tscn" id="2"] [ext_resource type="PackedScene" uid="uid://c546tskdu53j1" path="res://src/UI/Canvas/CanvasPreview.tscn" id="2"]
@ -20,6 +20,7 @@
[ext_resource type="PackedScene" uid="uid://ba24iuv55m4l3" path="res://src/UI/Canvas/Canvas.tscn" id="19"] [ext_resource type="PackedScene" uid="uid://ba24iuv55m4l3" path="res://src/UI/Canvas/Canvas.tscn" id="19"]
[ext_resource type="PackedScene" uid="uid://wplk62pbgih4" path="res://src/Palette/PalettePanel.tscn" id="20"] [ext_resource type="PackedScene" uid="uid://wplk62pbgih4" path="res://src/Palette/PalettePanel.tscn" id="20"]
[ext_resource type="Script" path="res://src/UI/ViewportContainer.gd" id="23"] [ext_resource type="Script" path="res://src/UI/ViewportContainer.gd" id="23"]
[ext_resource type="PackedScene" uid="uid://bfbragmmdwfbl" path="res://src/UI/TilesPanel.tscn" id="23_wyr78"]
[ext_resource type="Script" path="res://addons/dockable_container/layout_split.gd" id="27"] [ext_resource type="Script" path="res://addons/dockable_container/layout_split.gd" id="27"]
[ext_resource type="Script" path="res://addons/dockable_container/dockable_container.gd" id="35"] [ext_resource type="Script" path="res://addons/dockable_container/dockable_container.gd" id="35"]
[ext_resource type="Script" path="res://addons/dockable_container/layout_panel.gd" id="36"] [ext_resource type="Script" path="res://addons/dockable_container/layout_panel.gd" id="36"]
@ -36,7 +37,7 @@ shader_parameter/size = Vector2(100, 100)
[sub_resource type="Resource" id="Resource_xnnnd"] [sub_resource type="Resource" id="Resource_xnnnd"]
resource_name = "Tabs" resource_name = "Tabs"
script = ExtResource("36") script = ExtResource("36")
names = PackedStringArray("Tools", "Reference Images") names = PackedStringArray("Tools", "Reference Images", "Tiles")
current_tab = 0 current_tab = 0
[sub_resource type="Resource" id="Resource_34hle"] [sub_resource type="Resource" id="Resource_34hle"]
@ -401,6 +402,10 @@ size_flags_vertical = 3
[node name="Palettes" parent="DockableContainer" instance=ExtResource("20")] [node name="Palettes" parent="DockableContainer" instance=ExtResource("20")]
layout_mode = 2 layout_mode = 2
[node name="Tiles" parent="DockableContainer" instance=ExtResource("23_wyr78")]
visible = false
layout_mode = 2
[node name="Reference Images" parent="DockableContainer" instance=ExtResource("11")] [node name="Reference Images" parent="DockableContainer" instance=ExtResource("11")]
visible = false visible = false
layout_mode = 2 layout_mode = 2