1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 09:09:47 +00:00

Implement tilemap layers (#1146)

This commit is contained in:
Emmanouil Papadeas 2024-12-05 03:57:44 +02:00 committed by GitHub
parent b48bb4a094
commit f91bb18fb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 3257 additions and 622 deletions

View file

@ -2239,6 +2239,10 @@ msgstr ""
msgid "Group"
msgstr ""
#. A tilemap is a type of layer, which is divided by grid cells, the size of which is determined by the tileset it uses. Each grid cell is mapped to a tile in the tileset. Tilemaps can be used to create game levels and layouts.
msgid "Tilemap"
msgstr ""
msgid "Layers"
msgstr ""
@ -2247,33 +2251,47 @@ msgid "Clipping mask"
msgstr ""
#. Hint tooltip of the create new layer button, found on the left side of the timeline.
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Create a new layer"
msgstr ""
#. One of the options of the create new layer button.
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Add Pixel Layer"
msgstr ""
#. One of the options of the create new layer button.
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Add Group Layer"
msgstr ""
#. One of the options of the create new layer button.
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Add 3D Layer"
msgstr ""
#. One of the options of the create new layer button.
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Add Tilemap Layer"
msgstr ""
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Remove current layer"
msgstr ""
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Move up the current layer"
msgstr ""
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Move down the current layer"
msgstr ""
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Clone current layer"
msgstr ""
#: src/UI/Timeline/AnimationTimeline.tscn
msgid "Merge current layer with the one below"
msgstr ""
@ -2960,6 +2978,10 @@ msgstr ""
msgid "Recorder"
msgstr ""
#. Tiles are images of a specific shape, usually rectangular, that are laid out in a grid. They are used in tile-based video games. https://en.wikipedia.org/wiki/Tile-based_video_game
msgid "Tiles"
msgstr ""
msgid "Crop"
msgstr ""
@ -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.
msgid "Do you want to download the image from %s?"
msgstr ""
#. A tileset is a collection of tiles.
#: src/Classes/TileSetCustom.gd
#: src/UI/Dialogs/ImportPreviewDialog.gd
msgid "Tileset"
msgstr ""
#. A tileset is a collection of tiles.
#: src/UI/Timeline/NewTileMapLayerDialog.tscn
msgid "Tileset:"
msgstr ""
#. A tileset is a collection of tiles.
#: src/UI/Dialogs/ProjectProperties.tscn
msgid "Tilesets"
msgstr ""
#: src/UI/Timeline/NewTileMapLayerDialog.tscn
msgid "New tileset"
msgstr ""
#: src/UI/Timeline/NewTileMapLayerDialog.tscn
msgid "Tileset name:"
msgstr ""
#: src/UI/Timeline/NewTileMapLayerDialog.tscn
msgid "Tile size:"
msgstr ""
#: src/UI/TilesPanel.tscn
msgid "Draw tiles"
msgstr ""
#: src/UI/TilesPanel.tscn
msgid "Rotate tile left (counterclockwise)"
msgstr ""
#: src/UI/TilesPanel.tscn
msgid "Rotate tile right (clockwise)"
msgstr ""
#: src/UI/TilesPanel.tscn
msgid "Flip tile horizontally"
msgstr ""
#: src/UI/TilesPanel.tscn
msgid "Flip tile vertically"
msgstr ""

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

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

View file

@ -908,7 +908,7 @@ previous_project={
}
center_canvas={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":67,"physical_keycode":0,"key_label":0,"unicode":67,"location":0,"echo":false,"script":null)
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":66,"physical_keycode":0,"key_label":0,"unicode":66,"location":0,"echo":false,"script":null)
]
}
left_text_tool={
@ -925,6 +925,46 @@ show_pixel_indices={
"deadzone": 0.5,
"events": []
}
toggle_draw_tiles_mode={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
tile_edit_mode_manual={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":49,"key_label":0,"unicode":33,"location":0,"echo":false,"script":null)
]
}
tile_edit_mode_auto={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":50,"key_label":0,"unicode":64,"location":0,"echo":false,"script":null)
]
}
tile_edit_mode_stack={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":51,"key_label":0,"unicode":35,"location":0,"echo":false,"script":null)
]
}
tile_rotate_left={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":90,"key_label":0,"unicode":90,"location":0,"echo":false,"script":null)
]
}
tile_rotate_right={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":88,"key_label":0,"unicode":88,"location":0,"echo":false,"script":null)
]
}
tile_flip_horizontal={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":67,"location":0,"echo":false,"script":null)
]
}
tile_flip_vertical={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":86,"key_label":0,"unicode":86,"location":0,"echo":false,"script":null)
]
}
[input_devices]

View file

@ -535,9 +535,11 @@ func center(indices: Array) -> void:
tmp_centered.blend_rect(cel.image, used_rect, offset)
var centered := ImageExtended.new()
centered.copy_from_custom(tmp_centered, cel_image.is_indexed)
if cel is CelTileMap:
(cel as CelTileMap).serialize_undo_data_source_image(centered, redo_data, undo_data)
centered.add_data_to_dictionary(redo_data, cel_image)
cel_image.add_data_to_dictionary(undo_data)
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_do_method(Global.undo_or_redo.bind(false))
project.undo_redo.commit_action()
@ -546,13 +548,13 @@ func center(indices: Array) -> void:
func scale_project(width: int, height: int, interpolation: int) -> void:
var redo_data := {}
var undo_data := {}
for f in Global.current_project.frames:
for i in range(f.cels.size() - 1, -1, -1):
var cel := f.cels[i]
for cel in Global.current_project.get_all_pixel_cels():
if not cel is PixelCel:
continue
var cel_image := (cel as PixelCel).get_image()
var sprite := _resize_image(cel_image, width, height, interpolation) as ImageExtended
if cel is CelTileMap:
(cel as CelTileMap).serialize_undo_data_source_image(sprite, redo_data, undo_data)
sprite.add_data_to_dictionary(redo_data, cel_image)
cel_image.add_data_to_dictionary(undo_data)
@ -596,9 +598,9 @@ func _resize_image(
func crop_to_selection() -> void:
if not Global.current_project.has_selection:
return
Global.canvas.selection.transform_content_confirm()
var redo_data := {}
var undo_data := {}
Global.canvas.selection.transform_content_confirm()
var rect: Rect2i = Global.canvas.selection.big_bounding_rectangle
# Loop through all the cels to crop them
for 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 cropped := ImageExtended.new()
cropped.copy_from_custom(tmp_cropped, cel_image.is_indexed)
if cel is CelTileMap:
(cel as CelTileMap).serialize_undo_data_source_image(cropped, redo_data, undo_data)
cropped.add_data_to_dictionary(redo_data, cel_image)
cel_image.add_data_to_dictionary(undo_data)
@ -617,8 +621,7 @@ func crop_to_selection() -> void:
func crop_to_content() -> void:
Global.canvas.selection.transform_content_confirm()
var used_rect := Rect2i()
for f in Global.current_project.frames:
for cel in f.cels:
for cel in Global.current_project.get_all_pixel_cels():
if not cel is PixelCel:
continue
var cel_used_rect := cel.get_image().get_used_rect()
@ -644,6 +647,8 @@ func crop_to_content() -> void:
var tmp_cropped := cel_image.get_region(used_rect)
var cropped := ImageExtended.new()
cropped.copy_from_custom(tmp_cropped, cel_image.is_indexed)
if cel is CelTileMap:
(cel as CelTileMap).serialize_undo_data_source_image(cropped, redo_data, undo_data)
cropped.add_data_to_dictionary(redo_data, cel_image)
cel_image.add_data_to_dictionary(undo_data)
@ -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)
)
resized.convert_rgb_to_indexed()
if cel is CelTileMap:
(cel as CelTileMap).serialize_undo_data_source_image(resized, redo_data, undo_data)
resized.add_data_to_dictionary(redo_data, cel_image)
cel_image.add_data_to_dictionary(undo_data)
@ -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.x_symmetry_axis, "points", new_x_symmetry_axis_points)
project.undo_redo.add_do_property(project.y_symmetry_axis, "points", new_y_symmetry_axis_points)
Global.undo_redo_compress_images(redo_data, undo_data)
project.deserialize_cel_undo_data(redo_data, undo_data)
project.undo_redo.add_undo_property(project, "size", project.size)
project.undo_redo.add_undo_property(project, "x_symmetry_point", project.x_symmetry_point)
project.undo_redo.add_undo_property(project, "y_symmetry_point", project.y_symmetry_point)

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 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 }
## ## Used to tell whether a color is being taken from the current theme,
## or if it is a custom color.
@ -897,12 +897,17 @@ func _initialize_keychain() -> void:
&"reference_rotate": Keychain.InputAction.new("", "Reference images", false),
&"reference_scale": Keychain.InputAction.new("", "Reference images", false),
&"reference_quick_menu": Keychain.InputAction.new("", "Reference images", false),
&"cancel_reference_transform": Keychain.InputAction.new("", "Reference images", false)
&"cancel_reference_transform": Keychain.InputAction.new("", "Reference images", false),
&"tile_rotate_left": Keychain.InputAction.new("", "Tileset panel", false),
&"tile_rotate_right": Keychain.InputAction.new("", "Tileset panel", false),
&"tile_flip_horizontal": Keychain.InputAction.new("", "Tileset panel", false),
&"tile_flip_vertical": Keychain.InputAction.new("", "Tileset panel", false)
}
Keychain.groups = {
"Canvas": Keychain.InputGroup.new("", false),
"Cursor movement": Keychain.InputGroup.new("Canvas"),
"Reference images": Keychain.InputGroup.new("Canvas"),
"Buttons": Keychain.InputGroup.new(),
"Tools": Keychain.InputGroup.new(),
"Left": Keychain.InputGroup.new("Tools"),
@ -921,7 +926,7 @@ func _initialize_keychain() -> void:
"Shape tools": Keychain.InputGroup.new("Tool modifiers"),
"Selection tools": Keychain.InputGroup.new("Tool modifiers"),
"Transformation tools": Keychain.InputGroup.new("Tool modifiers"),
"Reference images": Keychain.InputGroup.new("Canvas")
"Tileset panel": Keychain.InputGroup.new()
}
Keychain.ignore_actions = ["left_mouse", "right_mouse", "middle_mouse", "shift", "ctrl"]
@ -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
## [member general_redo] a step further. Does further work if the current action requires it
## like refreshing textures, redraw UI elements etc...[br]
## [param frame_index] and [param layer_index] are there for optimizzation. if the undo or redo
## [param frame_index] and [param layer_index] are there for optimization. if the undo or redo
## happens only in one cel then the cel's frame and layer should be passed to [param frame_index]
## and [param layer_index] respectively, otherwise the entire timeline will be refreshed.
func undo_or_redo(
@ -980,20 +985,24 @@ func undo_or_redo(
]
):
if layer_index > -1 and frame_index > -1:
canvas.update_texture(layer_index, frame_index, project)
var cel := project.frames[frame_index].cels[layer_index]
if action_name == "Scale":
cel.size_changed(project.size)
canvas.update_texture(layer_index, frame_index, project, undo)
else:
for i in project.frames.size():
for j in project.layers.size():
canvas.update_texture(j, i, project)
var cel := project.frames[i].cels[j]
if action_name == "Scale":
cel.size_changed(project.size)
canvas.update_texture(j, i, project, undo)
canvas.selection.queue_redraw()
if action_name == "Scale":
for i in project.frames.size():
for j in project.layers.size():
var current_cel := project.frames[i].cels[j]
if current_cel is Cel3D:
current_cel.size_changed(project.size)
else:
if current_cel is not Cel3D:
current_cel.image_texture.set_image(current_cel.get_image())
canvas.camera_zoom()
canvas.grid.queue_redraw()

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
else:
new_project.tiles.reset_mask()
if result.has("tilesets"):
for i in result.tilesets.size():
var tileset_dict: Dictionary = result.tilesets[i]
var tileset := new_project.tilesets[i]
var tile_size := tileset.tile_size
var tile_amount: int = tileset_dict.tile_amount
for j in tile_amount:
var image_data := zip_reader.read_file("tilesets/%s/%s" % [i, j])
var image := Image.create_from_data(
tile_size.x, tile_size.y, false, new_project.get_image_format(), image_data
)
tileset.add_tile(image, null)
zip_reader.close()
new_project.export_directory_path = path.get_base_dir()
@ -418,6 +430,14 @@ func save_pxo_file(
zip_packer.start_file("image_data/tile_map")
zip_packer.write_file(project.tiles.tile_mask.get_data())
zip_packer.close_file()
for i in project.tilesets.size():
var tileset := project.tilesets[i]
var tileset_path := "tilesets/%s" % i
for j in tileset.tiles.size():
var tile := tileset.tiles[j]
zip_packer.start_file(tileset_path.path_join(str(j)))
zip_packer.write_file(tile.image.get_data())
zip_packer.close_file()
zip_packer.close()
if temp_path != path:
@ -699,17 +719,18 @@ func open_image_at_cel(image: Image, layer_index := 0, frame_index := 0) -> void
return
image.convert(project.get_image_format())
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 := {}
if cel is CelTileMap:
undo_data[cel] = (cel as CelTileMap).serialize_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_method(project.change_cel.bind(frame_index, layer_index))
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()
func open_image_as_tileset(
path: String, image: Image, horiz: int, vert: int, project := Global.current_project
) -> void:
image.convert(project.get_image_format())
horiz = mini(horiz, image.get_size().x)
vert = mini(vert, image.get_size().y)
var frame_width := image.get_size().x / horiz
var frame_height := image.get_size().y / vert
var tile_size := Vector2i(frame_width, frame_height)
var tileset := TileSetCustom.new(tile_size, path.get_basename().get_file())
for yy in range(vert):
for xx in range(horiz):
var cropped_image := image.get_region(
Rect2i(frame_width * xx, frame_height * yy, frame_width, frame_height)
)
@warning_ignore("int_as_enum_without_cast")
tileset.add_tile(cropped_image, null)
project.tilesets.append(tileset)
func open_image_as_tileset_smart(
path: String,
image: Image,
sliced_rects: Array[Rect2i],
tile_size: Vector2i,
project := Global.current_project
) -> void:
image.convert(project.get_image_format())
if sliced_rects.size() == 0: # Image is empty sprite (manually set data to be consistent)
tile_size = image.get_size()
sliced_rects.append(Rect2i(Vector2i.ZERO, tile_size))
var tileset := TileSetCustom.new(tile_size, path.get_basename().get_file())
for rect in sliced_rects:
var offset: Vector2 = (0.5 * (tile_size - rect.size)).floor()
var cropped_image := Image.create(
tile_size.x, tile_size.y, false, project.get_image_format()
)
cropped_image.blit_rect(image, rect, offset)
@warning_ignore("int_as_enum_without_cast")
tileset.add_tile(cropped_image, null)
project.tilesets.append(tileset)
func set_new_imported_tab(project: Project, path: String) -> void:
var prev_project_empty := Global.current_project.is_empty()
var prev_project_pos := Global.current_project_index

View file

@ -2,6 +2,8 @@
extends Node
signal color_changed(color_info: Dictionary, button: int)
@warning_ignore("unused_signal")
signal selected_tile_index_changed(tile_index: int)
signal config_changed(slot_idx: int, config: Dictionary)
@warning_ignore("unused_signal")
signal flip_rotated(flip_x, flip_y, rotate_90, rotate_180, rotate_270)
@ -88,7 +90,11 @@ var tools := {
),
"Move":
Tool.new(
"Move", "Move", "move", "res://src/Tools/UtilityTools/Move.tscn", [Global.LayerTypes.PIXEL]
"Move",
"Move",
"move",
"res://src/Tools/UtilityTools/Move.tscn",
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP]
),
"Zoom": Tool.new("Zoom", "Zoom", "zoom", "res://src/Tools/UtilityTools/Zoom.tscn"),
"Pan": Tool.new("Pan", "Pan", "pan", "res://src/Tools/UtilityTools/Pan.tscn"),
@ -116,7 +122,7 @@ var tools := {
"Pencil",
"pencil",
"res://src/Tools/DesignTools/Pencil.tscn",
[Global.LayerTypes.PIXEL],
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
"Hold %s to make a line",
["draw_create_line"]
),
@ -126,7 +132,7 @@ var tools := {
"Eraser",
"eraser",
"res://src/Tools/DesignTools/Eraser.tscn",
[Global.LayerTypes.PIXEL],
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
"Hold %s to make a line",
["draw_create_line"]
),
@ -136,7 +142,7 @@ var tools := {
"Bucket",
"fill",
"res://src/Tools/DesignTools/Bucket.tscn",
[Global.LayerTypes.PIXEL]
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP]
),
"Shading":
Tool.new(
@ -144,7 +150,7 @@ var tools := {
"Shading Tool",
"shading",
"res://src/Tools/DesignTools/Shading.tscn",
[Global.LayerTypes.PIXEL]
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP]
),
"LineTool":
(
@ -154,7 +160,7 @@ var tools := {
"Line Tool",
"linetool",
"res://src/Tools/DesignTools/LineTool.tscn",
[Global.LayerTypes.PIXEL],
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
"""Hold %s to snap the angle of the line
Hold %s to center the shape on the click origin
Hold %s to displace the shape's origin""",
@ -169,7 +175,7 @@ Hold %s to displace the shape's origin""",
"Curve Tool",
"curvetool",
"res://src/Tools/DesignTools/CurveTool.tscn",
[Global.LayerTypes.PIXEL],
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
"""Draws bezier curves
Press %s/%s to add new points
Press and drag to control the curvature
@ -185,7 +191,7 @@ Press %s to remove the last added point""",
"Rectangle Tool",
"rectangletool",
"res://src/Tools/DesignTools/RectangleTool.tscn",
[Global.LayerTypes.PIXEL],
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
"""Hold %s to create a 1:1 shape
Hold %s to center the shape on the click origin
Hold %s to displace the shape's origin""",
@ -200,7 +206,7 @@ Hold %s to displace the shape's origin""",
"Ellipse Tool",
"ellipsetool",
"res://src/Tools/DesignTools/EllipseTool.tscn",
[Global.LayerTypes.PIXEL],
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
"""Hold %s to create a 1:1 shape
Hold %s to center the shape on the click origin
Hold %s to displace the shape's origin""",
@ -213,7 +219,7 @@ Hold %s to displace the shape's origin""",
"Text",
"text",
"res://src/Tools/UtilityTools/Text.tscn",
[Global.LayerTypes.PIXEL],
[Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP],
""
),
"3DShapeEdit":
@ -232,10 +238,12 @@ var _panels := {}
var _curr_layer_type := Global.LayerTypes.PIXEL
var _left_tools_per_layer_type := {
Global.LayerTypes.PIXEL: "Pencil",
Global.LayerTypes.TILEMAP: "Pencil",
Global.LayerTypes.THREE_D: "3DShapeEdit",
}
var _right_tools_per_layer_type := {
Global.LayerTypes.PIXEL: "Eraser",
Global.LayerTypes.TILEMAP: "Eraser",
Global.LayerTypes.THREE_D: "Pan",
}
var _tool_buttons: Node
@ -575,6 +583,87 @@ func calculate_mirror_x_minus_y(pos: Vector2i, project: Project) -> Vector2i:
)
func is_placing_tiles() -> bool:
if Global.current_project.frames.size() == 0 or Global.current_project.layers.size() == 0:
return false
return Global.current_project.get_current_cel() is CelTileMap and TileSetPanel.placing_tiles
func _get_closest_point_to_grid(pos: Vector2, distance: float, grid_pos: Vector2) -> Vector2:
# If the cursor is close to the start/origin of a grid cell, snap to that
var snap_distance := distance * Vector2.ONE
var closest_point := Vector2.INF
var rect := Rect2()
rect.position = pos - (snap_distance / 4.0)
rect.end = pos + (snap_distance / 4.0)
if rect.has_point(grid_pos):
closest_point = grid_pos
return closest_point
# If the cursor is far from the grid cell origin but still close to a grid line
# Look for a point close to a horizontal grid line
var grid_start_hor := Vector2(0, grid_pos.y)
var grid_end_hor := Vector2(Global.current_project.size.x, grid_pos.y)
var closest_point_hor := get_closest_point_to_segment(
pos, distance, grid_start_hor, grid_end_hor
)
# Look for a point close to a vertical grid line
var grid_start_ver := Vector2(grid_pos.x, 0)
var grid_end_ver := Vector2(grid_pos.x, Global.current_project.size.y)
var closest_point_ver := get_closest_point_to_segment(
pos, distance, grid_start_ver, grid_end_ver
)
# Snap to the closest point to the closest grid line
var horizontal_distance := (closest_point_hor - pos).length()
var vertical_distance := (closest_point_ver - pos).length()
if horizontal_distance < vertical_distance:
closest_point = closest_point_hor
elif horizontal_distance > vertical_distance:
closest_point = closest_point_ver
elif horizontal_distance == vertical_distance and closest_point_hor != Vector2.INF:
closest_point = grid_pos
return closest_point
func get_closest_point_to_segment(
pos: Vector2, distance: float, s1: Vector2, s2: Vector2
) -> Vector2:
var test_line := (s2 - s1).rotated(deg_to_rad(90)).normalized()
var from_a := pos - test_line * distance
var from_b := pos + test_line * distance
var closest_point := Vector2.INF
if Geometry2D.segment_intersects_segment(from_a, from_b, s1, s2):
closest_point = Geometry2D.get_closest_point_to_segment(pos, s1, s2)
return closest_point
func snap_to_rectangular_grid_boundary(
pos: Vector2, grid_size: Vector2i, grid_offset := Vector2i.ZERO, snapping_distance := 9999.0
) -> Vector2:
var grid_pos := pos.snapped(grid_size)
grid_pos += Vector2(grid_offset)
# keeping grid_pos as is would have been fine but this adds extra accuracy as to
# which snap point (from the list below) is closest to mouse and occupy THAT point
# t_l is for "top left" and so on
var t_l := grid_pos + Vector2(-grid_size.x, -grid_size.y)
var t_c := grid_pos + Vector2(0, -grid_size.y)
var t_r := grid_pos + Vector2(grid_size.x, -grid_size.y)
var m_l := grid_pos + Vector2(-grid_size.x, 0)
var m_c := grid_pos
var m_r := grid_pos + Vector2(grid_size.x, 0)
var b_l := grid_pos + Vector2(-grid_size.x, grid_size.y)
var b_c := grid_pos + Vector2(0, grid_size.y)
var b_r := grid_pos + Vector2(grid_size)
var vec_arr: PackedVector2Array = [t_l, t_c, t_r, m_l, m_c, m_r, b_l, b_c, b_r]
for vec in vec_arr:
if vec.distance_to(pos) < grid_pos.distance_to(pos):
grid_pos = vec
var grid_point := _get_closest_point_to_grid(pos, snapping_distance, grid_pos)
if grid_point != Vector2.INF:
pos = grid_point.floor()
return pos
func set_button_size(button_size: int) -> void:
var size := Vector2(24, 24) if button_size == Global.ButtonSize.SMALL else Vector2(32, 32)
if not is_instance_valid(_tool_buttons):

View file

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

View file

@ -0,0 +1,700 @@
# gdlint: ignore=max-public-methods
class_name CelTileMap
extends PixelCel
## A cel type for 2D tile-based maps.
## A Tilemap cel uses a [TileSetCustom], which it inherits from its [LayerTileMap].
## Extending from [PixelCel], it contains an internal [Image], which is divided in
## grid cells, the size of which comes from [member TileSetCustom.tile_size].
## Each cell contains an index, which is an integer used to map that portion of the
## internal [member PixelCel.image] to a tile in [member tileset], as well as
## information that specifies if that cell has a transformation applied to it,
## such as horizontal flipping, vertical flipping, or if it's transposed.
## The [TileSetCustom] that this cel uses, passed down from the cel's [LayerTileMap].
var tileset: TileSetCustom
## The [Array] of type [CelTileMap.Cell] that contains data for each cell of the tilemap.
## The array's size is equal to [member horizontal_cells] * [member vertical_cells].
var cells: Array[Cell]
## The amount of horizontal cells.
var horizontal_cells: int
## The amount of vertical cells.
var vertical_cells: int
## Dictionary of [int] and [Array].
## The key is the index of the tile in the tileset,
## and the value is the index of the tilemap tile that changed first, along with
## its image that is being changed when manual mode is enabled.
## Gets reset on [method update_tilemap].
var editing_images := {}
## An internal class of [CelTIleMap], which contains data used by individual cells of the tilemap.
class Cell:
## The index of the [TileSetCustom] tile that the cell is mapped to.
var index := 0
## If [code]true[/code], the tile is flipped horizontally in this cell.
var flip_h := false
## If [code]true[/code], the tile is flipped vertically in this cell.
var flip_v := false
## If [code]true[/code], the tile is rotated 90 degrees counter-clockwise,
## and then flipped vertically in this cell.
var transpose := false
func _to_string() -> String:
var text := str(index)
if flip_h:
text += "H"
if flip_v:
text += "V"
if transpose:
text += "T"
return text
func remove_transformations() -> void:
flip_h = false
flip_v = false
transpose = false
func serialize() -> Dictionary:
return {"index": index, "flip_h": flip_h, "flip_v": flip_v, "transpose": transpose}
func deserialize(dict: Dictionary) -> void:
index = dict.get("index", index)
flip_h = dict.get("flip_h", flip_h)
flip_v = dict.get("flip_v", flip_v)
transpose = dict.get("transpose", transpose)
func _init(_tileset: TileSetCustom, _image := ImageExtended.new(), _opacity := 1.0) -> void:
super._init(_image, _opacity)
set_tileset(_tileset)
func set_tileset(new_tileset: TileSetCustom, reset_indices := true) -> void:
if tileset == new_tileset:
return
if is_instance_valid(tileset):
if tileset.updated.is_connected(_on_tileset_updated):
tileset.updated.disconnect(_on_tileset_updated)
tileset = new_tileset
if is_instance_valid(tileset):
_resize_cells(get_image().get_size(), reset_indices)
if not tileset.updated.is_connected(_on_tileset_updated):
tileset.updated.connect(_on_tileset_updated)
## Maps the cell at position [param cell_position] to
## the [member tileset]'s tile of index [param index].
func set_index(cell_position: int, index: int) -> void:
index = clampi(index, 0, tileset.tiles.size() - 1)
var previous_index := cells[cell_position].index
if previous_index != index:
if previous_index > 0 and previous_index < tileset.tiles.size():
tileset.tiles[previous_index].times_used -= 1
tileset.tiles[index].times_used += 1
cells[cell_position].index = index
cells[cell_position].flip_h = TileSetPanel.is_flipped_h
cells[cell_position].flip_v = TileSetPanel.is_flipped_v
cells[cell_position].transpose = TileSetPanel.is_transposed
_update_cell(cell_position)
Global.canvas.queue_redraw()
## Returns the pixel coordinates of the tilemap's cell
## at position [cell_position] in the cel's image.
## The reverse of [method get_cell_position].
func get_cell_coords_in_image(cell_position: int) -> Vector2i:
var x_coord := float(tileset.tile_size.x) * (cell_position % horizontal_cells)
@warning_ignore("integer_division")
var y_coord := float(tileset.tile_size.y) * (cell_position / horizontal_cells)
return Vector2i(x_coord, y_coord)
## Returns the position of a cell in the tilemap
## at pixel coordinates [param coords] in the cel's image.
## The reverse of [method get_cell_coords_in_image].
func get_cell_position(coords: Vector2i) -> int:
@warning_ignore("integer_division")
var x := coords.x / tileset.tile_size.x
x = clampi(x, 0, horizontal_cells - 1)
@warning_ignore("integer_division")
var y := coords.y / tileset.tile_size.y
y = clampi(y, 0, vertical_cells - 1)
y *= horizontal_cells
return x + y
## Returns the position of a cell in the tilemap
## at tilemap coordinates [param coords] in the cel's image.
func get_cell_position_in_tilemap_space(coords: Vector2i) -> int:
var x := coords.x
x = clampi(x, 0, horizontal_cells - 1)
var y := coords.y
y = clampi(y, 0, vertical_cells - 1)
y *= horizontal_cells
return x + y
## Returns the index of a cell in the tilemap
## at pixel coordinates [param coords] in the cel's image.
func get_cell_index_at_coords(coords: Vector2i) -> int:
return cells[get_cell_position(coords)].index
## Returns the index of a cell in the tilemap
## at tilemap coordinates [param coords] in the cel's image.
func get_cell_index_at_coords_in_tilemap_space(coords: Vector2i) -> int:
return cells[get_cell_position_in_tilemap_space(coords)].index
## Returns [code]true[/code] if the tile at cell position [param cell_position]
## with image [param image_portion] is equal to [param tile_image].
func _tiles_equal(cell_position: int, image_portion: Image, tile_image: Image) -> bool:
var cell_data := cells[cell_position]
var final_image_portion := transform_tile(
tile_image, cell_data.flip_h, cell_data.flip_v, cell_data.transpose
)
return image_portion.get_data() == final_image_portion.get_data()
## Applies transformations to [param tile_image] based on [param flip_h],
## [param flip_v] and [param transpose], and returns the transformed image.
## If [param reverse] is [code]true[/code], the transposition is applied the reverse way.
func transform_tile(
tile_image: Image, flip_h: bool, flip_v: bool, transpose: bool, reverse := false
) -> Image:
var transformed_tile := Image.new()
transformed_tile.copy_from(tile_image)
if transpose:
var tmp_image := Image.new()
tmp_image.copy_from(transformed_tile)
if reverse:
tmp_image.rotate_90(CLOCKWISE)
else:
tmp_image.rotate_90(COUNTERCLOCKWISE)
transformed_tile.blit_rect(
tmp_image, Rect2i(Vector2i.ZERO, transformed_tile.get_size()), Vector2i.ZERO
)
if reverse and not (flip_h != flip_v):
transformed_tile.flip_x()
else:
transformed_tile.flip_y()
if flip_h:
transformed_tile.flip_x()
if flip_v:
transformed_tile.flip_y()
return transformed_tile
## Given a [param selection_map] and a [param selection_rect],
## the method finds the cells that are currently selected and returns them
## in the form of a 2D array that contains the serialiazed data
##of the selected cells in the form of [Dictionary].
func get_selected_cells(selection_map: SelectionMap, selection_rect: Rect2i) -> Array[Array]:
var selected_cells: Array[Array] = []
for x in range(0, selection_rect.size.x, tileset.tile_size.x):
selected_cells.append([])
for y in range(0, selection_rect.size.y, tileset.tile_size.y):
var pos := Vector2i(x, y) + selection_rect.position
var x_index := x / tileset.tile_size.x
if selection_map.is_pixel_selected(pos):
var cell_pos := get_cell_position(pos)
selected_cells[x_index].append(cells[cell_pos].serialize())
else:
# If it's not selected, append the transparent tile 0.
selected_cells[x_index].append(
{"index": 0, "flip_h": false, "flip_v": false, "transpose": false}
)
return selected_cells
## Resizes [param selected_indices], which is an array of arrays of [Dictionary],
## to [param horizontal_size] and [param vertical_size].
## This method is used when resizing a selection and draw tiles mode is enabled.
func resize_selection(
selected_cells: Array[Array], horizontal_size: int, vertical_size: int
) -> Array[Array]:
var resized_cells: Array[Array] = []
var current_columns := selected_cells.size()
if current_columns == 0:
return resized_cells
var current_rows := selected_cells[0].size()
if current_rows == 0:
return resized_cells
resized_cells.resize(horizontal_size)
for x in horizontal_size:
resized_cells[x] = []
resized_cells[x].resize(vertical_size)
var column_middles := current_columns - 2
if current_columns == 1:
for x in horizontal_size:
_resize_rows(selected_cells[0], resized_cells[x], current_rows, vertical_size)
else:
for x in horizontal_size:
if x == 0:
_resize_rows(selected_cells[0], resized_cells[x], current_rows, vertical_size)
elif x == horizontal_size - 1:
_resize_rows(selected_cells[-1], resized_cells[x], current_rows, vertical_size)
else:
if x < current_columns - 1:
_resize_rows(selected_cells[x], resized_cells[x], current_rows, vertical_size)
else:
if column_middles == 0:
_resize_rows(
selected_cells[-1], resized_cells[x], current_rows, vertical_size
)
else:
var x_index := x - (column_middles * ((x - 1) / column_middles))
_resize_rows(
selected_cells[x_index], resized_cells[x], current_rows, vertical_size
)
return resized_cells
## Helper method of [method resize_selection].
func _resize_rows(
selected_cells: Array, resized_cells: Array, current_rows: int, vertical_size: int
) -> void:
var row_middles := current_rows - 2
if current_rows == 1:
for y in vertical_size:
resized_cells[y] = selected_cells[0]
else:
for y in vertical_size:
if y == 0:
resized_cells[y] = selected_cells[0]
elif y == vertical_size - 1:
resized_cells[y] = selected_cells[-1]
else:
if y < current_rows - 1:
resized_cells[y] = selected_cells[y]
else:
if row_middles == 0:
resized_cells[y] = selected_cells[-1]
else:
var y_index := y - (row_middles * ((y - 1) / row_middles))
resized_cells[y] = selected_cells[y_index]
## Applies the [param selected_cells] data to [param target_image] data,
## offset by [param selection_rect]. The target image needs to be resized first.
## This method is used when resizing a selection and draw tiles mode is enabled.
func apply_resizing_to_image(
target_image: Image, selected_cells: Array[Array], selection_rect: Rect2i
) -> void:
for x in selected_cells.size():
for y in selected_cells[x].size():
var pos := Vector2i(x, y) * tileset.tile_size + selection_rect.position
var cell_pos := get_cell_position(pos)
var coords := get_cell_coords_in_image(cell_pos) - selection_rect.position
var rect := Rect2i(coords, tileset.tile_size)
var image_portion := target_image.get_region(rect)
var cell_data := Cell.new()
cell_data.deserialize(selected_cells[x][y])
var index := cell_data.index
if index >= tileset.tiles.size():
index = 0
var current_tile := tileset.tiles[index].image
var transformed_tile := transform_tile(
current_tile, cell_data.flip_h, cell_data.flip_v, cell_data.transpose
)
if image_portion.get_data() != transformed_tile.get_data():
var tile_size := transformed_tile.get_size()
target_image.blit_rect(transformed_tile, Rect2i(Vector2i.ZERO, tile_size), coords)
if target_image is ImageExtended:
target_image.convert_rgb_to_indexed()
## Appends data to a [Dictionary] to be used for undo/redo.
func serialize_undo_data() -> Dictionary:
var dict := {}
var cell_indices := []
cell_indices.resize(cells.size())
for i in cell_indices.size():
cell_indices[i] = cells[i].serialize()
dict["cell_indices"] = cell_indices
dict["tileset"] = tileset.serialize_undo_data()
dict["resize"] = false
return dict
## Same purpose as [method serialize_undo_data], but for when the image resource
## ([param source_image]) we want to store to the undo/redo stack
## is not the same as [member image]. This method also handles the resizing logic for undo/redo.
func serialize_undo_data_source_image(
source_image: ImageExtended, redo_data: Dictionary, undo_data: Dictionary
) -> void:
undo_data[self] = serialize_undo_data()
if source_image.get_size() != image.get_size():
undo_data[self]["resize"] = true
_resize_cells(source_image.get_size())
tileset.clear_tileset(self)
var tile_editing_mode := TileSetPanel.tile_editing_mode
if tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL:
tile_editing_mode = TileSetPanel.TileEditingMode.AUTO
update_tilemap(tile_editing_mode, source_image)
redo_data[self] = serialize_undo_data()
redo_data[self]["resize"] = undo_data[self]["resize"]
## Reads data from a [param dict] [Dictionary], and uses them to add methods to [param undo_redo].
func deserialize_undo_data(dict: Dictionary, undo_redo: UndoRedo, undo: bool) -> void:
var cell_indices = dict.cell_indices
if undo:
undo_redo.add_undo_method(_deserialize_cell_data.bind(cell_indices, dict.resize))
if dict.has("tileset"):
undo_redo.add_undo_method(tileset.deserialize_undo_data.bind(dict.tileset, self))
else:
undo_redo.add_do_method(_deserialize_cell_data.bind(cell_indices, dict.resize))
if dict.has("tileset"):
undo_redo.add_do_method(tileset.deserialize_undo_data.bind(dict.tileset, self))
## Gets called every time a change is being applied to the [param image],
## such as when finishing drawing with a draw tool, or when applying an image effect.
## This method responsible for updating the indices of the [member cells], as well as
## updating the [member tileset] with the incoming changes.
## The updating behavior depends on the current tile editing mode
## by [member TileSetPanel.tile_editing_mode].
## If a [param source_image] is provided, that image is being used instead of [member image].
func update_tilemap(
tile_editing_mode := TileSetPanel.tile_editing_mode, source_image := image
) -> void:
editing_images.clear()
var tileset_size_before_update := tileset.tiles.size()
for i in cells.size():
var coords := get_cell_coords_in_image(i)
var rect := Rect2i(coords, tileset.tile_size)
var image_portion := source_image.get_region(rect)
var index := cells[i].index
if index >= tileset.tiles.size():
index = 0
var current_tile := tileset.tiles[index]
if tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL:
if image_portion.is_invisible():
continue
if index == 0:
# If the tileset is empty, only then add a new tile.
if tileset.tiles.size() <= 1:
tileset.add_tile(image_portion, self)
cells[i].index = tileset.tiles.size() - 1
continue
if not _tiles_equal(i, image_portion, current_tile.image):
tileset.replace_tile_at(image_portion, index, self)
elif tile_editing_mode == TileSetPanel.TileEditingMode.AUTO:
_handle_auto_editing_mode(i, image_portion, tileset_size_before_update)
else: # Stack
if image_portion.is_invisible():
continue
var found_tile := false
for j in range(1, tileset.tiles.size()):
var tile := tileset.tiles[j]
if _tiles_equal(i, image_portion, tile.image):
if cells[i].index != j:
cells[i].index = j
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
func update_texture() -> void:
func update_texture(undo := false) -> void:
image_texture.set_image(image)
super.update_texture()
super.update_texture(undo)
func get_class_name() -> String:

View file

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

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
Global.canvas.selection.marching_ants_outline.offset = selection_offset
var has_selection := false
var tilesets: Array[TileSetCustom]
## For every camera (currently there are 3)
var cameras_rotation: PackedFloat32Array = [0.0, 0.0, 0.0]
@ -295,6 +296,9 @@ func serialize() -> Dictionary:
var reference_image_data := []
for reference_image in reference_images:
reference_image_data.append(reference_image.serialize())
var tileset_data := []
for tileset in tilesets:
tileset_data.append(tileset.serialize())
var metadata := _serialize_metadata(self)
@ -315,6 +319,7 @@ func serialize() -> Dictionary:
"frames": frame_data,
"brushes": brush_data,
"reference_images": reference_image_data,
"tilesets": tileset_data,
"vanishing_points": vanishing_points,
"export_file_name": file_name,
"export_file_format": file_format,
@ -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"):
tiles.y_basis.x = dict.tile_mode_y_basis_x
tiles.y_basis.y = dict.tile_mode_y_basis_y
if dict.has("tilesets"):
for saved_tileset in dict["tilesets"]:
var tile_size = str_to_var("Vector2i" + saved_tileset.get("tile_size"))
var tileset := TileSetCustom.new(tile_size)
tileset.deserialize(saved_tileset)
tilesets.append(tileset)
if dict.has("frames") and dict.has("layers"):
for saved_layer in dict.layers:
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))
Global.LayerTypes.THREE_D:
layers.append(Layer3D.new(self))
var frame_i := 0
for frame in dict.frames:
var cels: Array[BaseCel] = []
var cel_i := 0
for cel in frame.cels:
match int(dict.layers[cel_i].get("type", Global.LayerTypes.PIXEL)):
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
Global.LayerTypes.TILEMAP:
layers.append(LayerTileMap.new(self, null))
# Parent references to other layers are created when deserializing
# 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
layers[layer_i].deserialize(layer_dict)
_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"):
for tag in dict.tags:
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])
## Called by [method deserialize], this method loads an image at
## a given [param frame_i] frame index and a [param cel_i] cel index from a pxo file,
## and returns it as an [ImageExtended].
## If the pxo file is saved with Pixelorama version 1.0 and on,
## the [param zip_reader] is used to load the image. Otherwise, [param file] is used.
func _load_image_from_pxo(
frame_i: int, cel_i: int, zip_reader: ZIPReader, file: FileAccess
) -> ImageExtended:
var image: Image
var indices_data := PackedByteArray()
if is_instance_valid(zip_reader): # For pxo files saved in 1.0+
var path := "image_data/frames/%s/layer_%s" % [frame_i + 1, cel_i + 1]
var image_data := zip_reader.read_file(path)
image = Image.create_from_data(size.x, size.y, false, get_image_format(), image_data)
var indices_path := "image_data/frames/%s/indices_layer_%s" % [frame_i + 1, cel_i + 1]
if zip_reader.file_exists(indices_path):
indices_data = zip_reader.read_file(indices_path)
elif is_instance_valid(file): # For pxo files saved in 0.x
var buffer := file.get_buffer(size.x * size.y * 4)
image = Image.create_from_data(size.x, size.y, false, get_image_format(), buffer)
var pixelorama_image := ImageExtended.new()
pixelorama_image.is_indexed = is_indexed()
if not indices_data.is_empty() and is_indexed():
pixelorama_image.indices_image = Image.create_from_data(
size.x, size.y, false, Image.FORMAT_R8, indices_data
)
pixelorama_image.copy_from(image)
pixelorama_image.select_palette("", true)
return pixelorama_image
func _size_changed(value: Vector2i) -> void:
if not is_instance_valid(tiles):
size = value
@ -632,6 +656,57 @@ func get_all_pixel_cels() -> Array[PixelCel]:
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,
## then the order of drawing is the same as the order of the layers itself.
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)
reference_images.insert(to, ri)
Global.canvas.reference_image_container.move_child(ri, to)
## Adds a new [param tileset] to [member tilesets].
func add_tileset(tileset: TileSetCustom) -> void:
tilesets.append(tileset)
## Loops through all cels in [param cel_dictionary], and for [CelTileMap]s,
## it calls [method CelTileMap.update_tilemap].
func update_tilemaps(cel_dictionary: Dictionary) -> void:
for cel in cel_dictionary:
if cel is CelTileMap:
(cel as CelTileMap).update_tilemap()

View file

@ -77,6 +77,13 @@ func select_pixel(pixel: Vector2i, select := true) -> void:
set_pixelv(pixel, Color(0))
func select_rect(rect: Rect2i, select := true) -> void:
if select:
fill_rect(rect, Color(1, 1, 1, 1))
else:
fill_rect(rect, Color(0))
func select_all() -> void:
fill(Color(1, 1, 1, 1))

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
const IMAGE_BRUSHES := [Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM]
@ -17,6 +18,7 @@ var _brush_image := Image.new()
var _orignal_brush_image := Image.new() ## Contains the original _brush_image, without resizing
var _brush_texture := ImageTexture.new()
var _strength := 1.0
var _is_eraser := false
@warning_ignore("unused_private_class_variable")
var _picking_color := false
@ -42,6 +44,7 @@ var _circle_tool_shortcut: Array[Vector2i]
func _ready() -> void:
super._ready()
Global.cel_switched.connect(update_brush)
Global.global_tool_options.dynamics_panel.dynamics_changed.connect(_reset_dynamics)
Tools.color_changed.connect(_on_Color_changed)
Global.brushes_popup.brush_removed.connect(_on_Brush_removed)
@ -160,6 +163,20 @@ func update_config() -> void:
func update_brush() -> void:
$Brush/BrushSize.suffix = "px" # Assume we are using default brushes
if Tools.is_placing_tiles():
var tilemap_cel := Global.current_project.get_current_cel() as CelTileMap
var tileset := tilemap_cel.tileset
var tile_index := clampi(TileSetPanel.selected_tile_index, 0, tileset.tiles.size() - 1)
var tile_image := tileset.tiles[tile_index].image
tile_image = tilemap_cel.transform_tile(
tile_image,
TileSetPanel.is_flipped_h,
TileSetPanel.is_flipped_v,
TileSetPanel.is_transposed
)
_brush_image.copy_from(tile_image)
_brush_texture = ImageTexture.create_from_image(_brush_image)
else:
match _brush.type:
Brushes.PIXEL:
_brush_texture = ImageTexture.create_from_image(
@ -256,8 +273,9 @@ func prepare_undo(action: String) -> void:
func commit_undo() -> void:
var redo_data := _get_undo_data()
var project := Global.current_project
project.update_tilemaps(_undo_data)
var redo_data := _get_undo_data()
var frame := -1
var layer := -1
if Global.animation_timeline.animation_timer.is_stopped() and project.selected_cels.size() == 1:
@ -265,7 +283,7 @@ func commit_undo() -> void:
layer = project.current_layer
project.undos += 1
Global.undo_redo_compress_images(redo_data, _undo_data, project)
project.deserialize_cel_undo_data(redo_data, _undo_data)
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false, frame, layer))
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true, frame, layer))
project.undo_redo.commit_action()
@ -303,6 +321,22 @@ func draw_end(pos: Vector2i) -> void:
_polylines = _create_polylines(_indicator)
func draw_tile(pos: Vector2i) -> void:
var tile_index := 0 if _is_eraser else TileSetPanel.selected_tile_index
var mirrored_positions := Tools.get_mirrored_positions(pos, Global.current_project)
var tile_positions := PackedInt32Array()
tile_positions.resize(mirrored_positions.size() + 1)
tile_positions[0] = get_cell_position(pos)
for i in mirrored_positions.size():
var mirrored_position := mirrored_positions[i]
tile_positions[i + 1] = get_cell_position(mirrored_position)
for cel in _get_selected_draw_cels():
if cel is not CelTileMap:
return
for tile_position in tile_positions:
(cel as CelTileMap).set_index(tile_position, tile_index)
func _prepare_tool() -> void:
if !Global.current_project.layers[Global.current_project.current_layer].can_layer_get_drawn():
return
@ -482,7 +516,14 @@ func remove_unselected_parts_of_brush(brush: Image, dst: Vector2i) -> Image:
func draw_indicator(left: bool) -> void:
var color := Global.left_tool_color if left else Global.right_tool_color
draw_indicator_at(snap_position(_cursor), Vector2i.ZERO, color)
var snapped_position := snap_position(_cursor)
if Tools.is_placing_tiles():
var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset
var grid_size := tileset.tile_size
snapped_position = _snap_to_rectangular_grid_center(
snapped_position, grid_size, Vector2i.ZERO, -1
)
draw_indicator_at(snapped_position, Vector2i.ZERO, color)
if (
Global.current_project.has_selection
and Global.current_project.tiles.mode == Tiles.MODE.NONE
@ -491,7 +532,7 @@ func draw_indicator(left: bool) -> void:
var nearest_pos := Global.current_project.selection_map.get_nearest_position(pos)
if nearest_pos != Vector2i.ZERO:
var offset := nearest_pos
draw_indicator_at(snap_position(_cursor), offset, Color.GREEN)
draw_indicator_at(snapped_position, offset, Color.GREEN)
return
if Global.current_project.tiles.mode and Global.current_project.tiles.has_point(_cursor):
@ -499,12 +540,12 @@ func draw_indicator(left: bool) -> void:
var nearest_tile := Global.current_project.tiles.get_nearest_tile(pos)
if nearest_tile.position != Vector2i.ZERO:
var offset := nearest_tile.position
draw_indicator_at(snap_position(_cursor), offset, Color.GREEN)
draw_indicator_at(snapped_position, offset, Color.GREEN)
func draw_indicator_at(pos: Vector2i, offset: Vector2i, color: Color) -> void:
var canvas: Node2D = Global.canvas.indicators
if _brush.type in IMAGE_BRUSHES and not _draw_line:
if _brush.type in IMAGE_BRUSHES and not _draw_line or Tools.is_placing_tiles():
pos -= _brush_image.get_size() / 2
pos -= offset
canvas.draw_texture(_brush_texture, pos)
@ -539,6 +580,9 @@ func _set_pixel_no_cache(pos: Vector2i, ignore_mirroring := false) -> void:
pos = _stroke_project.tiles.get_canon_position(pos)
if Global.current_project.has_selection:
pos = Global.current_project.selection_map.get_canon_position(pos)
if Tools.is_placing_tiles():
draw_tile(pos)
return
if !_stroke_project.can_pixel_get_drawn(pos):
return
@ -727,11 +771,7 @@ func _get_undo_data() -> Dictionary:
if not cel is PixelCel:
continue
cels.append(cel)
for cel in cels:
if not cel is PixelCel:
continue
var image := (cel as PixelCel).get_image()
image.add_data_to_dictionary(data)
project.serialize_cel_undo_data(cels, data)
return data

View file

@ -152,6 +152,10 @@ func draw_move(pos: Vector2i) -> void:
if not _move:
return
if Tools.is_placing_tiles():
var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset
var grid_size := tileset.tile_size
pos = Tools.snap_to_rectangular_grid_boundary(pos, grid_size)
if Input.is_action_pressed("transform_snap_axis"): # Snap to axis
var angle := Vector2(pos).angle_to_point(_start_pos)
if absf(angle) <= PI / 4 or absf(angle) >= 3 * PI / 4:
@ -211,6 +215,13 @@ func apply_selection(_position: Vector2i) -> void:
_intersect = true
func select_tilemap_cell(
cel: CelTileMap, cell_position: int, selection: SelectionMap, select: bool
) -> void:
var rect := Rect2i(cel.get_cell_coords_in_image(cell_position), cel.tileset.tile_size)
selection.select_rect(rect, select)
func _on_confirm_button_pressed() -> void:
if selection_node.is_moving_content:
selection_node.transform_content_confirm()

View file

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

View file

@ -75,7 +75,17 @@ func draw_move(pos: Vector2i) -> void:
func draw_end(_pos: Vector2i) -> void:
is_moving = false
_draw_cache = []
Global.current_project.can_undo = true
var project := Global.current_project
project.can_undo = true
func get_cell_position(pos: Vector2i) -> int:
var tile_pos := 0
if Global.current_project.get_current_cel() is not CelTileMap:
return tile_pos
var cel := Global.current_project.get_current_cel() as CelTileMap
tile_pos = cel.get_cell_position(pos)
return tile_pos
func cursor_move(pos: Vector2i) -> void:
@ -129,52 +139,14 @@ func draw_preview() -> void:
func snap_position(pos: Vector2) -> Vector2:
var snapping_distance := Global.snapping_distance / Global.camera.zoom.x
if Global.snap_to_rectangular_grid_boundary:
var grid_pos := pos.snapped(Global.grids[0].grid_size)
grid_pos += Vector2(Global.grids[0].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(-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()
pos = Tools.snap_to_rectangular_grid_boundary(
pos, Global.grids[0].grid_size, Global.grids[0].grid_offset, snapping_distance
)
if Global.snap_to_rectangular_grid_center:
var grid_center := (
pos.snapped(Global.grids[0].grid_size) + Vector2(Global.grids[0].grid_size / 2)
pos = _snap_to_rectangular_grid_center(
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
if Global.snap_to_guides:
@ -240,57 +212,39 @@ func mirror_array(array: Array[Vector2i], callable := func(_array): pass) -> Arr
return new_array
func _get_closest_point_to_grid(pos: Vector2, distance: float, grid_pos: Vector2) -> Vector2:
# If the cursor is close to the start/origin of a grid cell, snap to that
var snap_distance := distance * Vector2.ONE
var closest_point := Vector2.INF
var rect := Rect2()
rect.position = pos - (snap_distance / 4.0)
rect.end = pos + (snap_distance / 4.0)
if rect.has_point(grid_pos):
closest_point = grid_pos
return closest_point
# If the cursor is far from the grid cell origin but still close to a grid line
# Look for a point close to a horizontal grid line
var grid_start_hor := Vector2(0, grid_pos.y)
var grid_end_hor := Vector2(Global.current_project.size.x, grid_pos.y)
var closest_point_hor := _get_closest_point_to_segment(
pos, distance, grid_start_hor, grid_end_hor
)
# Look for a point close to a vertical grid line
var grid_start_ver := Vector2(grid_pos.x, 0)
var grid_end_ver := Vector2(grid_pos.x, Global.current_project.size.y)
var closest_point_ver := _get_closest_point_to_segment(
pos, distance, grid_start_ver, grid_end_ver
)
# Snap to the closest point to the closest grid line
var horizontal_distance := (closest_point_hor - pos).length()
var vertical_distance := (closest_point_ver - pos).length()
if horizontal_distance < vertical_distance:
closest_point = closest_point_hor
elif horizontal_distance > vertical_distance:
closest_point = closest_point_ver
elif horizontal_distance == vertical_distance and closest_point_hor != Vector2.INF:
closest_point = grid_pos
return closest_point
func _get_closest_point_to_segment(
pos: Vector2, distance: float, s1: Vector2, s2: Vector2
func _snap_to_rectangular_grid_center(
pos: Vector2, grid_size: Vector2i, grid_offset: Vector2i, snapping_distance: float
) -> Vector2:
var test_line := (s2 - s1).rotated(deg_to_rad(90)).normalized()
var from_a := pos - test_line * distance
var from_b := pos + test_line * distance
var closest_point := Vector2.INF
if Geometry2D.segment_intersects_segment(from_a, from_b, s1, s2):
closest_point = Geometry2D.get_closest_point_to_segment(pos, s1, s2)
return closest_point
var grid_center := pos.snapped(grid_size) + Vector2(grid_size / 2)
grid_center += Vector2(grid_offset)
# keeping grid_center as is would have been fine but this adds extra accuracy as to
# which snap point (from the list below) is closest to mouse and occupy THAT point
# t_l is for "top left" and so on
var t_l := grid_center + Vector2(-grid_size.x, -grid_size.y)
var t_c := grid_center + Vector2(0, -grid_size.y)
var t_r := grid_center + Vector2(grid_size.x, -grid_size.y)
var m_l := grid_center + Vector2(-grid_size.x, 0)
var m_c := grid_center
var m_r := grid_center + Vector2(grid_size.x, 0)
var b_l := grid_center + Vector2(-grid_size.x, grid_size.y)
var b_c := grid_center + Vector2(0, grid_size.y)
var b_r := grid_center + Vector2(grid_size)
var vec_arr := [t_l, t_c, t_r, m_l, m_c, m_r, b_l, b_c, b_r]
for vec in vec_arr:
if vec.distance_to(pos) < grid_center.distance_to(pos):
grid_center = vec
if snapping_distance < 0:
pos = grid_center.floor()
else:
if grid_center.distance_to(pos) <= snapping_distance:
pos = grid_center.floor()
return pos
func _snap_to_guide(
snap_to: Vector2, pos: Vector2, distance: float, s1: Vector2, s2: Vector2
) -> Vector2:
var closest_point := _get_closest_point_to_segment(pos, distance, s1, s2)
var closest_point := Tools.get_closest_point_to_segment(pos, distance, s1, s2)
if closest_point == Vector2.INF: # Is not close to a guide
return Vector2.INF
# Snap to the closest guide
@ -322,6 +276,17 @@ func _get_draw_image() -> ImageExtended:
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]:
var images: Array[ImageExtended] = []
var project := Global.current_project
@ -340,7 +305,10 @@ func _pick_color(pos: Vector2i) -> void:
if pos.x < 0 or pos.y < 0:
return
if Tools.is_placing_tiles():
var cel := Global.current_project.get_current_cel() as CelTileMap
Tools.selected_tile_index_changed.emit(cel.get_cell_index_at_coords(pos))
return
var image := Image.new()
image.copy_from(_get_draw_image())
if pos.x > image.get_width() - 1 or pos.y > image.get_height() - 1:

View file

@ -186,6 +186,11 @@ func draw_end(pos: Vector2i) -> void:
commit_undo()
func draw_tile(pos: Vector2i, cel: CelTileMap) -> void:
var tile_position := get_cell_position(pos)
cel.set_index(tile_position, TileSetPanel.selected_tile_index)
func fill(pos: Vector2i) -> void:
match _fill_area:
FillArea.AREA:
@ -199,6 +204,17 @@ func fill(pos: Vector2i) -> void:
func fill_in_color(pos: Vector2i) -> void:
var project := Global.current_project
if Tools.is_placing_tiles():
for cel in _get_selected_draw_cels():
if cel is not CelTileMap:
continue
var tilemap_cel := cel as CelTileMap
var tile_index := tilemap_cel.get_cell_index_at_coords(pos)
for i in tilemap_cel.cells.size():
var cell := tilemap_cel.cells[i]
if cell.index == tile_index:
tilemap_cel.set_index(i, TileSetPanel.selected_tile_index)
return
var color := project.get_current_cel().get_image().get_pixelv(pos)
var images := _get_selected_draw_images()
for image in images:
@ -311,6 +327,74 @@ func fill_in_selection() -> void:
gen.generate_image(image, PATTERN_FILL_SHADER, params, project.size)
func _flood_fill(pos: Vector2i) -> void:
# implements the floodfill routine by Shawn Hargreaves
# from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c
var project := Global.current_project
if Tools.is_placing_tiles():
for cel in _get_selected_draw_cels():
if cel is not CelTileMap:
continue
var tile_index := (cel as CelTileMap).get_cell_index_at_coords(pos)
# init flood data structures
_allegro_flood_segments = []
_allegro_image_segments = []
_compute_segments_for_tilemap(pos, cel, tile_index)
_color_segments_tilemap(cel)
return
var images := _get_selected_draw_images()
for image in images:
if Tools.check_alpha_lock(image, pos):
continue
var color: Color = image.get_pixelv(pos)
if _fill_with == FillWith.COLOR or _pattern == null:
# end early if we are filling with the same color
if tool_slot.color.is_equal_approx(color):
continue
else:
# end early if we are filling with an empty pattern
var pattern_size := _pattern.image.get_size()
if pattern_size.x == 0 or pattern_size.y == 0:
return
# init flood data structures
_allegro_flood_segments = []
_allegro_image_segments = []
_compute_segments_for_image(pos, project, image, color)
# now actually color the image: since we have already checked a few things for the points
# we'll process here, we're going to skip a bunch of safety checks to speed things up.
_color_segments(image)
func _compute_segments_for_image(
pos: Vector2i, project: Project, image: Image, src_color: Color
) -> void:
# initially allocate at least 1 segment per line of image
for j in image.get_height():
_add_new_segment(j)
# start flood algorithm
_flood_line_around_point(pos, project, image, src_color)
# test all segments while also discovering more
var done := false
while not done:
done = true
var max_index := _allegro_flood_segments.size()
for c in max_index:
var p := _allegro_flood_segments[c]
if p.todo_below: # check below the segment?
p.todo_below = false
if _check_flooded_segment(
p.y + 1, p.left_position, p.right_position, project, image, src_color
):
done = false
if p.todo_above: # check above the segment?
p.todo_above = false
if _check_flooded_segment(
p.y - 1, p.left_position, p.right_position, project, image, src_color
):
done = false
## Add a new segment to the array
func _add_new_segment(y := 0) -> void:
_allegro_flood_segments.append(Segment.new(y))
@ -407,62 +491,6 @@ func _check_flooded_segment(
return ret
func _flood_fill(pos: Vector2i) -> void:
# implements the floodfill routine by Shawn Hargreaves
# from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c
var project := Global.current_project
var images := _get_selected_draw_images()
for image in images:
if Tools.check_alpha_lock(image, pos):
continue
var color: Color = image.get_pixelv(pos)
if _fill_with == FillWith.COLOR or _pattern == null:
# end early if we are filling with the same color
if tool_slot.color.is_equal_approx(color):
continue
else:
# end early if we are filling with an empty pattern
var pattern_size := _pattern.image.get_size()
if pattern_size.x == 0 or pattern_size.y == 0:
return
# init flood data structures
_allegro_flood_segments = []
_allegro_image_segments = []
_compute_segments_for_image(pos, project, image, color)
# now actually color the image: since we have already checked a few things for the points
# we'll process here, we're going to skip a bunch of safety checks to speed things up.
_color_segments(image)
func _compute_segments_for_image(
pos: Vector2i, project: Project, image: Image, src_color: Color
) -> void:
# initially allocate at least 1 segment per line of image
for j in image.get_height():
_add_new_segment(j)
# start flood algorithm
_flood_line_around_point(pos, project, image, src_color)
# test all segments while also discovering more
var done := false
while not done:
done = true
var max_index := _allegro_flood_segments.size()
for c in max_index:
var p := _allegro_flood_segments[c]
if p.todo_below: # check below the segment?
p.todo_below = false
if _check_flooded_segment(
p.y + 1, p.left_position, p.right_position, project, image, src_color
):
done = false
if p.todo_above: # check above the segment?
p.todo_above = false
if _check_flooded_segment(
p.y - 1, p.left_position, p.right_position, project, image, src_color
):
done = false
func _color_segments(image: ImageExtended) -> void:
if _fill_with == FillWith.COLOR or _pattern == null:
# This is needed to ensure that the color used to fill is not wrong, due to float
@ -493,9 +521,119 @@ func _set_pixel_pattern(image: ImageExtended, x: int, y: int, pattern_size: Vect
image.set_pixel_custom(x, y, pc)
func _compute_segments_for_tilemap(pos: Vector2i, cel: CelTileMap, src_index: int) -> void:
# initially allocate at least 1 segment per line of the tilemap
for j in cel.vertical_cells:
_add_new_segment(j)
pos /= cel.tileset.tile_size
# start flood algorithm
_flood_line_around_point_tilemap(pos, cel, src_index)
# test all segments while also discovering more
var done := false
while not done:
done = true
var max_index := _allegro_flood_segments.size()
for c in max_index:
var p := _allegro_flood_segments[c]
if p.todo_below: # check below the segment?
p.todo_below = false
if _check_flooded_segment_tilemap(
p.y + 1, p.left_position, p.right_position, cel, src_index
):
done = false
if p.todo_above: # check above the segment?
p.todo_above = false
if _check_flooded_segment_tilemap(
p.y - 1, p.left_position, p.right_position, cel, src_index
):
done = false
## Fill an horizontal segment around the specified position, and adds it to the
## list of segments filled. Returns the first x coordinate after the part of the
## line that has been filled.
## Τhis method is called by [method _flood_fill] after the required data structures
## have been initialized.
func _flood_line_around_point_tilemap(pos: Vector2i, cel: CelTileMap, src_index: int) -> int:
if cel.get_cell_index_at_coords_in_tilemap_space(pos) != src_index:
return pos.x + 1
var west := pos
var east := pos
while west.x >= 0 && cel.get_cell_index_at_coords_in_tilemap_space(west) == src_index:
west += Vector2i.LEFT
while (
east.x < cel.horizontal_cells
&& cel.get_cell_index_at_coords_in_tilemap_space(east) == src_index
):
east += Vector2i.RIGHT
# Make a note of the stuff we processed
var c := pos.y
var segment := _allegro_flood_segments[c]
# we may have already processed some segments on this y coordinate
if segment.flooding:
while segment.next > 0:
c = segment.next # index of next segment in this line of image
segment = _allegro_flood_segments[c]
# found last current segment on this line
c = _allegro_flood_segments.size()
segment.next = c
_add_new_segment(pos.y)
segment = _allegro_flood_segments[c]
# set the values for the current segment
segment.flooding = true
segment.left_position = west.x + 1
segment.right_position = east.x - 1
segment.y = pos.y
segment.next = 0
# Should we process segments above or below this one?
# when there is a selected area, the pixels above and below the one we started creating this
# segment from may be outside it. It's easier to assume we should be checking for segments
# above and below this one than to specifically check every single pixel in it, because that
# test will be performed later anyway.
# On the other hand, this test we described is the same `project.can_pixel_get_drawn` does if
# there is no selection, so we don't need branching here.
segment.todo_above = pos.y > 0
segment.todo_below = pos.y < cel.vertical_cells - 1
# this is an actual segment we should be coloring, so we add it to the results for the
# current image
if segment.right_position >= segment.left_position:
_allegro_image_segments.append(segment)
# we know the point just east of the segment is not part of a segment that should be
# processed, else it would be part of this segment
return east.x + 1
func _check_flooded_segment_tilemap(
y: int, left: int, right: int, cel: CelTileMap, src_index: int
) -> bool:
var ret := false
var c := 0
while left <= right:
c = y
while true:
var segment := _allegro_flood_segments[c]
if left >= segment.left_position and left <= segment.right_position:
left = segment.right_position + 2
break
c = segment.next
if c == 0: # couldn't find a valid segment, so we draw a new one
left = _flood_line_around_point_tilemap(Vector2i(left, y), cel, src_index)
ret = true
break
return ret
func _color_segments_tilemap(cel: CelTileMap) -> void:
for c in _allegro_image_segments.size():
var p := _allegro_image_segments[c]
for px in range(p.left_position, p.right_position + 1):
draw_tile(Vector2i(px, p.y) * cel.tileset.tile_size, cel)
func commit_undo() -> void:
var redo_data := _get_undo_data()
var project := Global.current_project
project.update_tilemaps(_undo_data)
var redo_data := _get_undo_data()
var frame := -1
var layer := -1
if Global.animation_timeline.animation_timer.is_stopped() and project.selected_cels.size() == 1:
@ -504,7 +642,7 @@ func commit_undo() -> void:
project.undos += 1
project.undo_redo.create_action("Draw")
Global.undo_redo_compress_images(redo_data, _undo_data, project)
project.deserialize_cel_undo_data(redo_data, _undo_data)
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false, frame, layer))
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true, frame, layer))
project.undo_redo.commit_action()
@ -514,14 +652,13 @@ func commit_undo() -> void:
func _get_undo_data() -> Dictionary:
var data := {}
if Global.animation_timeline.animation_timer.is_stopped():
var images := _get_selected_draw_images()
for image in images:
image.add_data_to_dictionary(data)
Global.current_project.serialize_cel_undo_data(_get_selected_draw_cels(), data)
else:
var cels: Array[BaseCel]
for frame in Global.current_project.frames:
var cel := frame.cels[Global.current_project.current_layer]
if not cel is PixelCel:
continue
var image := (cel as PixelCel).get_image()
image.add_data_to_dictionary(data)
cels.append(cel)
Global.current_project.serialize_cel_undo_data(cels, 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 _drawing := false ## Set to true when a curve is being drawn.
@ -195,6 +195,9 @@ func _draw_shape() -> void:
func _draw_pixel(point: Vector2i, images: Array[ImageExtended]) -> void:
if Tools.is_placing_tiles():
draw_tile(point)
else:
if Global.current_project.can_pixel_get_drawn(point):
for image in images:
_drawer.set_pixel(image, point, tool_slot.color)

View file

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

View file

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

View file

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

View file

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

View file

@ -32,15 +32,38 @@ func apply_selection(pos: Vector2i) -> void:
if pos.x > project.size.x - 1 or pos.y > project.size.y - 1:
return
var cel_image := Image.new()
cel_image.copy_from(_get_draw_image())
var color := cel_image.get_pixelv(pos)
var operation := 0
if _subtract:
operation = 1
elif _intersect:
operation = 2
if Tools.is_placing_tiles():
var prev_selection_map := SelectionMap.new() # Used for intersect
prev_selection_map.copy_from(project.selection_map)
if !_add and !_subtract and !_intersect:
Global.canvas.selection.clear_selection()
if _intersect:
project.selection_map.clear()
for cel in _get_selected_draw_cels():
if cel is not CelTileMap:
continue
var tilemap_cel := cel as CelTileMap
var tile_index := tilemap_cel.get_cell_index_at_coords(pos)
for i in tilemap_cel.cells.size():
var cell := tilemap_cel.cells[i]
if cell.index == tile_index:
if _intersect:
var p := (cel as CelTileMap).get_cell_coords_in_image(i)
select_tilemap_cell(
cel, i, project.selection_map, prev_selection_map.is_pixel_selected(p)
)
else:
select_tilemap_cell(cel, i, project.selection_map, !_subtract)
else:
var cel_image := Image.new()
cel_image.copy_from(_get_draw_image())
var color := cel_image.get_pixelv(pos)
var params := {"color": color, "tolerance": _tolerance, "operation": operation}
if _add or _subtract or _intersect:
var selection_tex := ImageTexture.create_from_image(project.selection_map)

View file

@ -81,6 +81,24 @@ func apply_selection(_position: Vector2i) -> void:
Global.canvas.selection.commit_undo("Select", undo_data)
if _rect.size == Vector2i.ZERO:
return
if Tools.is_placing_tiles():
var operation := 0
if _subtract:
operation = 1
elif _intersect:
operation = 2
Global.canvas.selection.select_rect(_rect, operation)
# Handle mirroring
var mirror_positions := Tools.get_mirrored_positions(_rect.position, project, 1)
var mirror_ends := Tools.get_mirrored_positions(_rect.end, project, 1)
for i in mirror_positions.size():
var mirror_rect := Rect2i()
mirror_rect.position = mirror_positions[i]
mirror_rect.end = mirror_ends[i]
Global.canvas.selection.select_rect(mirror_rect.abs(), operation)
Global.canvas.selection.commit_undo("Select", undo_data)
else:
set_ellipse(project.selection_map, _rect.position)
# Handle mirroring
var mirror_positions := Tools.get_mirrored_positions(_rect.position, project, 1)
@ -116,8 +134,12 @@ func set_ellipse(selection_map: SelectionMap, pos: Vector2i) -> void:
# Given an origin point and destination point, returns a rect representing
# where the shape will be drawn and what is its size
func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i:
if Tools.is_placing_tiles():
var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset
var grid_size := tileset.tile_size
origin = Tools.snap_to_rectangular_grid_boundary(origin, grid_size)
dest = Tools.snap_to_rectangular_grid_boundary(dest, grid_size)
var rect := Rect2i()
# Center the rect on the mouse
if _expand_from_center:
var new_size := dest - origin
@ -140,6 +162,7 @@ func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i:
rect.position = Vector2i(mini(origin.x, dest.x), mini(origin.y, dest.y))
rect.size = (origin - dest).abs()
if not Tools.is_placing_tiles():
rect.size += Vector2i.ONE
return rect

View file

@ -70,9 +70,9 @@ func apply_selection(_position) -> void:
if _draw_points.size() > 3:
if _intersect:
project.selection_map.clear()
lasso_selection(_draw_points, project.selection_map, previous_selection_map)
lasso_selection(_draw_points, project, previous_selection_map)
# Handle mirroring
var callable := lasso_selection.bind(project.selection_map, previous_selection_map)
var callable := lasso_selection.bind(project, previous_selection_map)
mirror_array(_draw_points, callable)
Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect()
else:
@ -85,8 +85,9 @@ func apply_selection(_position) -> void:
func lasso_selection(
points: Array[Vector2i], selection_map: SelectionMap, previous_selection_map: SelectionMap
points: Array[Vector2i], project: Project, previous_selection_map: SelectionMap
) -> void:
var selection_map := project.selection_map
var selection_size := selection_map.get_size()
var bounding_rect := Rect2i(points[0], Vector2i.ZERO)
for point in points:
@ -95,9 +96,9 @@ func lasso_selection(
bounding_rect = bounding_rect.expand(point)
if _intersect:
if previous_selection_map.is_pixel_selected(point):
selection_map.select_pixel(point, true)
select_pixel(point, project, true)
else:
selection_map.select_pixel(point, !_subtract)
select_pixel(point, project, !_subtract)
var v := Vector2i()
for x in bounding_rect.size.x:
@ -107,9 +108,17 @@ func lasso_selection(
if Geometry2D.is_point_in_polygon(v, points):
if _intersect:
if previous_selection_map.is_pixel_selected(v):
selection_map.select_pixel(v, true)
select_pixel(v, project, true)
else:
selection_map.select_pixel(v, !_subtract)
select_pixel(v, project, !_subtract)
func select_pixel(point: Vector2i, project: Project, select: bool) -> void:
if Tools.is_placing_tiles():
var tilemap := project.get_current_cel() as CelTileMap
var cell_position := tilemap.get_cell_position(point)
select_tilemap_cell(tilemap, cell_position, project.selection_map, select)
project.selection_map.select_pixel(point, select)
# Bresenham's Algorithm

View file

@ -34,10 +34,10 @@ func apply_selection(pos: Vector2i) -> void:
var cel_image := Image.new()
cel_image.copy_from(_get_draw_image())
_flood_fill(pos, cel_image, project.selection_map, previous_selection_map)
_flood_fill(pos, cel_image, project, previous_selection_map)
# Handle mirroring
for mirror_pos in Tools.get_mirrored_positions(pos):
_flood_fill(mirror_pos, cel_image, project.selection_map, previous_selection_map)
_flood_fill(mirror_pos, cel_image, project, previous_selection_map)
Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect()
Global.canvas.selection.commit_undo("Select", undo_data)
@ -59,6 +59,39 @@ func update_config() -> void:
$ToleranceSlider.value = _tolerance * 255.0
func _on_tolerance_slider_value_changed(value: float) -> void:
_tolerance = value / 255.0
update_config()
save_config()
func _flood_fill(
pos: Vector2i, image: Image, project: Project, previous_selection_map: SelectionMap
) -> void:
# implements the floodfill routine by Shawn Hargreaves
# from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c
var selection_map := project.selection_map
if Tools.is_placing_tiles():
for cel in _get_selected_draw_cels():
if cel is not CelTileMap:
continue
var tile_index := (cel as CelTileMap).get_cell_index_at_coords(pos)
# init flood data structures
_allegro_flood_segments = []
_allegro_image_segments = []
_compute_segments_for_tilemap(pos, cel, tile_index)
_select_segments_tilemap(project, previous_selection_map)
return
var color := image.get_pixelv(pos)
# init flood data structures
_allegro_flood_segments = []
_allegro_image_segments = []
_compute_segments_for_image(pos, project, image, color)
# now actually color the image: since we have already checked a few things for the points
# we'll process here, we're going to skip a bunch of safety checks to speed things up.
_select_segments(selection_map, previous_selection_map)
# Add a new segment to the array
func _add_new_segment(y := 0) -> void:
_allegro_flood_segments.append(Segment.new(y))
@ -140,22 +173,6 @@ func _check_flooded_segment(
return ret
func _flood_fill(
pos: Vector2i, image: Image, selection_map: SelectionMap, previous_selection_map: SelectionMap
) -> void:
# implements the floodfill routine by Shawn Hargreaves
# from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c
var project := Global.current_project
var color := image.get_pixelv(pos)
# init flood data structures
_allegro_flood_segments = []
_allegro_image_segments = []
_compute_segments_for_image(pos, project, image, color)
# now actually color the image: since we have already checked a few things for the points
# we'll process here, we're going to skip a bunch of safety checks to speed things up.
_select_segments(selection_map, previous_selection_map)
func _compute_segments_for_image(
pos: Vector2i, project: Project, image: Image, src_color: Color
) -> void:
@ -201,7 +218,128 @@ func _set_bit(p: Vector2i, selection_map: SelectionMap, prev_selection_map: Sele
selection_map.select_pixel(p, !_subtract)
func _on_tolerance_slider_value_changed(value: float) -> void:
_tolerance = value / 255.0
update_config()
save_config()
func _compute_segments_for_tilemap(pos: Vector2i, cel: CelTileMap, src_index: int) -> void:
# initially allocate at least 1 segment per line of the tilemap
for j in cel.vertical_cells:
_add_new_segment(j)
pos /= cel.tileset.tile_size
# start flood algorithm
_flood_line_around_point_tilemap(pos, cel, src_index)
# test all segments while also discovering more
var done := false
while not done:
done = true
var max_index := _allegro_flood_segments.size()
for c in max_index:
var p := _allegro_flood_segments[c]
if p.todo_below: # check below the segment?
p.todo_below = false
if _check_flooded_segment_tilemap(
p.y + 1, p.left_position, p.right_position, cel, src_index
):
done = false
if p.todo_above: # check above the segment?
p.todo_above = false
if _check_flooded_segment_tilemap(
p.y - 1, p.left_position, p.right_position, cel, src_index
):
done = false
## Fill an horizontal segment around the specified position, and adds it to the
## list of segments filled. Returns the first x coordinate after the part of the
## line that has been filled.
## Τhis method is called by [method _flood_fill] after the required data structures
## have been initialized.
func _flood_line_around_point_tilemap(pos: Vector2i, cel: CelTileMap, src_index: int) -> int:
if cel.get_cell_index_at_coords_in_tilemap_space(pos) != src_index:
return pos.x + 1
var west := pos
var east := pos
while west.x >= 0 && cel.get_cell_index_at_coords_in_tilemap_space(west) == src_index:
west += Vector2i.LEFT
while (
east.x < cel.horizontal_cells
&& cel.get_cell_index_at_coords_in_tilemap_space(east) == src_index
):
east += Vector2i.RIGHT
# Make a note of the stuff we processed
var c := pos.y
var segment := _allegro_flood_segments[c]
# we may have already processed some segments on this y coordinate
if segment.flooding:
while segment.next > 0:
c = segment.next # index of next segment in this line of image
segment = _allegro_flood_segments[c]
# found last current segment on this line
c = _allegro_flood_segments.size()
segment.next = c
_add_new_segment(pos.y)
segment = _allegro_flood_segments[c]
# set the values for the current segment
segment.flooding = true
segment.left_position = west.x + 1
segment.right_position = east.x - 1
segment.y = pos.y
segment.next = 0
# Should we process segments above or below this one?
# when there is a selected area, the pixels above and below the one we started creating this
# segment from may be outside it. It's easier to assume we should be checking for segments
# above and below this one than to specifically check every single pixel in it, because that
# test will be performed later anyway.
# On the other hand, this test we described is the same `project.can_pixel_get_drawn` does if
# there is no selection, so we don't need branching here.
segment.todo_above = pos.y > 0
segment.todo_below = pos.y < cel.vertical_cells - 1
# this is an actual segment we should be coloring, so we add it to the results for the
# current image
if segment.right_position >= segment.left_position:
_allegro_image_segments.append(segment)
# we know the point just east of the segment is not part of a segment that should be
# processed, else it would be part of this segment
return east.x + 1
func _check_flooded_segment_tilemap(
y: int, left: int, right: int, cel: CelTileMap, src_index: int
) -> bool:
var ret := false
var c := 0
while left <= right:
c = y
while true:
var segment := _allegro_flood_segments[c]
if left >= segment.left_position and left <= segment.right_position:
left = segment.right_position + 2
break
c = segment.next
if c == 0: # couldn't find a valid segment, so we draw a new one
left = _flood_line_around_point_tilemap(Vector2i(left, y), cel, src_index)
ret = true
break
return ret
func _select_segments_tilemap(project: Project, previous_selection_map: SelectionMap) -> void:
# short circuit for flat colors
for c in _allegro_image_segments.size():
var p := _allegro_image_segments[c]
for px in range(p.left_position, p.right_position + 1):
# We don't have to check again whether the point being processed is within the bounds
_set_bit_rect(Vector2i(px, p.y), project, previous_selection_map)
func _set_bit_rect(p: Vector2i, project: Project, prev_selection_map: SelectionMap) -> void:
var selection_map := project.selection_map
var tilemap := project.get_current_cel() as CelTileMap
var cell_position := tilemap.get_cell_position_in_tilemap_space(p)
if _intersect:
var image_coords := tilemap.get_cell_coords_in_image(cell_position)
select_tilemap_cell(
tilemap,
cell_position,
project.selection_map,
prev_selection_map.is_pixel_selected(image_coords)
)
else:
select_tilemap_cell(tilemap, cell_position, project.selection_map, !_subtract)

View file

@ -99,10 +99,10 @@ func apply_selection(pos: Vector2i) -> void:
if _draw_points.size() >= 1:
if _intersect:
project.selection_map.clear()
paint_selection(project.selection_map, previous_selection_map, _draw_points)
paint_selection(project, previous_selection_map, _draw_points)
# Handle mirroring
var mirror := mirror_array(_draw_points)
paint_selection(project.selection_map, previous_selection_map, mirror)
paint_selection(project, previous_selection_map, mirror)
Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect()
else:
if !cleared:
@ -114,17 +114,26 @@ func apply_selection(pos: Vector2i) -> void:
func paint_selection(
selection_map: SelectionMap, previous_selection_map: SelectionMap, points: Array[Vector2i]
project: Project, previous_selection_map: SelectionMap, points: Array[Vector2i]
) -> void:
var selection_map := project.selection_map
var selection_size := selection_map.get_size()
for point in points:
if point.x < 0 or point.y < 0 or point.x >= selection_size.x or point.y >= selection_size.y:
continue
if _intersect:
if previous_selection_map.is_pixel_selected(point):
selection_map.select_pixel(point, true)
select_pixel(point, project, true)
else:
selection_map.select_pixel(point, !_subtract)
select_pixel(point, project, !_subtract)
func select_pixel(point: Vector2i, project: Project, select: bool) -> void:
if Tools.is_placing_tiles():
var tilemap := project.get_current_cel() as CelTileMap
var cell_position := tilemap.get_cell_position(point)
select_tilemap_cell(tilemap, cell_position, project.selection_map, select)
project.selection_map.select_pixel(point, select)
# Bresenham's Algorithm

View file

@ -107,9 +107,9 @@ func apply_selection(pos: Vector2i) -> void:
if _draw_points.size() > 3:
if _intersect:
project.selection_map.clear()
lasso_selection(_draw_points, project.selection_map, previous_selection_map)
lasso_selection(_draw_points, project, previous_selection_map)
# Handle mirroring
var callable := lasso_selection.bind(project.selection_map, previous_selection_map)
var callable := lasso_selection.bind(project, previous_selection_map)
mirror_array(_draw_points, callable)
Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect()
else:
@ -128,8 +128,9 @@ func _clear() -> void:
func lasso_selection(
points: Array[Vector2i], selection_map: SelectionMap, previous_selection_map: SelectionMap
points: Array[Vector2i], project: Project, previous_selection_map: SelectionMap
) -> void:
var selection_map := project.selection_map
var selection_size := selection_map.get_size()
var bounding_rect := Rect2i(points[0], Vector2i.ZERO)
for point in points:
@ -138,9 +139,9 @@ func lasso_selection(
bounding_rect = bounding_rect.expand(point)
if _intersect:
if previous_selection_map.is_pixel_selected(point):
selection_map.select_pixel(point, true)
select_pixel(point, project, true)
else:
selection_map.select_pixel(point, !_subtract)
select_pixel(point, project, !_subtract)
var v := Vector2i()
for x in bounding_rect.size.x:
@ -150,9 +151,17 @@ func lasso_selection(
if Geometry2D.is_point_in_polygon(v, points):
if _intersect:
if previous_selection_map.is_pixel_selected(v):
selection_map.select_pixel(v, true)
select_pixel(v, project, true)
else:
selection_map.select_pixel(v, !_subtract)
select_pixel(v, project, !_subtract)
func select_pixel(point: Vector2i, project: Project, select: bool) -> void:
if Tools.is_placing_tiles():
var tilemap := project.get_current_cel() as CelTileMap
var cell_position := tilemap.get_cell_position(point)
select_tilemap_cell(tilemap, cell_position, project.selection_map, select)
project.selection_map.select_pixel(point, select)
# Bresenham's Algorithm

View file

@ -101,6 +101,11 @@ func apply_selection(pos: Vector2i) -> void:
## Given an origin point and destination point, returns a rect representing
## where the shape will be drawn and what is its size
func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i:
if Tools.is_placing_tiles():
var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset
var grid_size := tileset.tile_size
origin = Tools.snap_to_rectangular_grid_boundary(origin, grid_size)
dest = Tools.snap_to_rectangular_grid_boundary(dest, grid_size)
var rect := Rect2i()
# Center the rect on the mouse
@ -125,6 +130,7 @@ func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i:
rect.position = Vector2i(mini(origin.x, dest.x), mini(origin.y, dest.y))
rect.size = (origin - dest).abs()
if not Tools.is_placing_tiles():
rect.size += Vector2i.ONE
return rect

View file

@ -63,10 +63,12 @@ func draw_end(pos: Vector2i) -> void:
func _pick_color(pos: Vector2i) -> void:
var project := Global.current_project
pos = project.tiles.get_canon_position(pos)
if pos.x < 0 or pos.y < 0:
return
if Tools.is_placing_tiles():
var cel := Global.current_project.get_current_cel() as CelTileMap
Tools.selected_tile_index_changed.emit(cel.get_cell_index_at_coords(pos))
return
var image := Image.new()
image.copy_from(_get_draw_image())
if pos.x > image.get_width() - 1 or pos.y > image.get_height() - 1:

View file

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

View file

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

View file

@ -110,13 +110,15 @@ func camera_zoom() -> void:
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:
frame_i = project.current_frame
if frame_i < project.frames.size() and layer_i < project.layers.size():
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
# e.g. when undoing/redoing, when applying image effects to the entire frame, etc
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="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="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/TileModeIndices.gd" id="19_7a6wb"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_6b0ox"]
shader = ExtResource("1_253dh")
@ -113,3 +114,7 @@ script = ExtResource("16_nxilb")
[node name="ReferenceImages" type="Node2D" parent="."]
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:
Global.project_switched.connect(queue_redraw)
Global.cel_switched.connect(queue_redraw)
func _draw() -> void:
@ -32,28 +33,32 @@ func _draw() -> 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 x: float = (
target_rect.position.x
+ fposmod(grid.grid_offset.x - target_rect.position.x, grid.grid_size.x)
target_rect.position.x + fposmod(grid_offset.x - target_rect.position.x, grid_size.x)
)
while x <= target_rect.end.x:
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.end.y))
x += grid.grid_size.x
x += grid_size.x
var y: float = (
target_rect.position.y
+ fposmod(grid.grid_offset.y - target_rect.position.y, grid.grid_size.y)
target_rect.position.y + fposmod(grid_offset.y - target_rect.position.y, grid_size.y)
)
while y <= target_rect.end.y:
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.end.x, y))
y += grid.grid_size.y
y += grid_size.y
unique_rect_lines.append_array(grid_multiline_points)
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:
var grid = Global.grids[grid_index]
var grid := Global.grids[grid_index]
var grid_multiline_points := PackedVector2Array()
var cell_size: Vector2 = grid.isometric_grid_size

View file

@ -28,7 +28,7 @@ var big_bounding_rectangle := Rect2i():
if slot.tool_node is BaseSelectionTool:
slot.tool_node.set_spinbox_values()
_update_gizmos()
var image_current_pixel := Vector2.ZERO ## The ACTUAL pixel coordinate of image
var image_current_pixel := Vector2.ZERO ## The pixel coordinates of the cursor
var temp_rect := Rect2()
var rect_aspect_ratio := 0.0
@ -38,6 +38,7 @@ var original_big_bounding_rectangle := Rect2i()
var original_preview_image := Image.new()
var original_bitmap := SelectionMap.new()
var original_offset := Vector2.ZERO
var original_selected_tilemap_cells: Array[Array]
var preview_image := Image.new()
var preview_image_texture := ImageTexture.new()
@ -224,6 +225,10 @@ func _move_with_arrow_keys(event: InputEvent) -> void:
if is_zero_approx(absf(move.y)):
move.y = 0
var final_direction := (move * step).round()
if Tools.is_placing_tiles():
var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset
var grid_size := tileset.tile_size
final_direction *= Vector2(grid_size)
move_content(final_direction)
@ -314,17 +319,21 @@ func _update_on_zoom() -> void:
func _gizmo_resize() -> void:
var dir := dragged_gizmo.direction
var mouse_pos := image_current_pixel
if Tools.is_placing_tiles():
var tilemap := Global.current_project.get_current_cel() as CelTileMap
mouse_pos = mouse_pos.snapped(tilemap.tileset.tile_size)
if Input.is_action_pressed("shape_center"):
# Code inspired from https://github.com/GDQuest/godot-open-rpg
if dir.x != 0 and dir.y != 0: # Border gizmos
temp_rect.size = ((image_current_pixel - temp_rect_pivot) * 2.0 * Vector2(dir))
temp_rect.size = ((mouse_pos - temp_rect_pivot) * 2.0 * Vector2(dir))
elif dir.y == 0: # Center left and right gizmos
temp_rect.size.x = (image_current_pixel.x - temp_rect_pivot.x) * 2.0 * dir.x
temp_rect.size.x = (mouse_pos.x - temp_rect_pivot.x) * 2.0 * dir.x
elif dir.x == 0: # Center top and bottom gizmos
temp_rect.size.y = (image_current_pixel.y - temp_rect_pivot.y) * 2.0 * dir.y
temp_rect.size.y = (mouse_pos.y - temp_rect_pivot.y) * 2.0 * dir.y
temp_rect = Rect2(-1.0 * temp_rect.size / 2 + temp_rect_pivot, temp_rect.size)
else:
_resize_rect(image_current_pixel, dir)
_resize_rect(mouse_pos, dir)
if Input.is_action_pressed("shape_perfect") or resize_keep_ratio: # Maintain aspect ratio
var end_y := temp_rect.end.y
@ -379,8 +388,23 @@ func resize_selection() -> void:
else:
Global.current_project.selection_map.copy_from(original_bitmap)
if is_moving_content:
content_pivot = original_big_bounding_rectangle.size / 2.0
preview_image.copy_from(original_preview_image)
if Tools.is_placing_tiles():
for cel in _get_selected_draw_cels():
if cel is not CelTileMap:
continue
var tilemap := cel as CelTileMap
var horizontal_size := size.x / tilemap.tileset.tile_size.x
var vertical_size := size.y / tilemap.tileset.tile_size.y
var selected_cells := tilemap.resize_selection(
original_selected_tilemap_cells, horizontal_size, vertical_size
)
preview_image.crop(size.x, size.y)
tilemap.apply_resizing_to_image(
preview_image, selected_cells, big_bounding_rectangle
)
else:
content_pivot = original_big_bounding_rectangle.size / 2.0
DrawingAlgos.nn_rotate(preview_image, angle, content_pivot)
preview_image.resize(size.x, size.y, Image.INTERPOLATE_NEAREST)
if temp_rect.size.x < 0:
@ -456,6 +480,15 @@ func move_borders(move: Vector2i) -> void:
return
marching_ants_outline.offset += Vector2(move)
big_bounding_rectangle.position += move
if Tools.is_placing_tiles():
var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset
var grid_size := tileset.tile_size
marching_ants_outline.offset = Tools.snap_to_rectangular_grid_boundary(
marching_ants_outline.offset, grid_size
)
big_bounding_rectangle.position = Vector2i(
Tools.snap_to_rectangular_grid_boundary(big_bounding_rectangle.position, grid_size)
)
queue_redraw()
@ -479,9 +512,15 @@ func transform_content_start() -> void:
undo_data = get_undo_data(false)
return
is_moving_content = true
original_bitmap.copy_from(Global.current_project.selection_map)
var project := Global.current_project
original_bitmap.copy_from(project.selection_map)
original_big_bounding_rectangle = big_bounding_rectangle
original_offset = Global.current_project.selection_offset
original_offset = project.selection_offset
var current_cel := project.get_current_cel()
if current_cel is CelTileMap:
original_selected_tilemap_cells = (current_cel as CelTileMap).get_selected_cells(
project.selection_map, big_bounding_rectangle
)
queue_redraw()
canvas.queue_redraw()
@ -501,6 +540,18 @@ func transform_content_confirm() -> void:
if not is_pasting:
src.copy_from(cel.transformed_content)
cel.transformed_content = null
if Tools.is_placing_tiles():
if cel is not CelTileMap:
continue
var tilemap := cel as CelTileMap
var horizontal_size := preview_image.get_width() / tilemap.tileset.tile_size.x
var vertical_size := preview_image.get_height() / tilemap.tileset.tile_size.y
var selected_cells := tilemap.resize_selection(
original_selected_tilemap_cells, horizontal_size, vertical_size
)
src.crop(preview_image.get_width(), preview_image.get_height())
tilemap.apply_resizing_to_image(src, selected_cells, big_bounding_rectangle)
else:
DrawingAlgos.nn_rotate(src, angle, content_pivot)
src.resize(
preview_image.get_width(), preview_image.get_height(), Image.INTERPOLATE_NEAREST
@ -510,6 +561,13 @@ func transform_content_confirm() -> void:
if temp_rect.size.y < 0:
src.flip_y()
if Tools.is_placing_tiles():
cel_image.blit_rect(
src,
Rect2i(Vector2i.ZERO, project.selection_map.get_size()),
big_bounding_rectangle.position
)
else:
cel_image.blit_rect_mask(
src,
src,
@ -523,6 +581,7 @@ func transform_content_confirm() -> void:
original_preview_image = Image.new()
preview_image = Image.new()
original_bitmap = SelectionMap.new()
original_selected_tilemap_cells.clear()
is_moving_content = false
is_pasting = false
angle = 0.0
@ -557,6 +616,7 @@ func transform_content_cancel() -> void:
original_preview_image = Image.new()
preview_image = Image.new()
original_bitmap = SelectionMap.new()
original_selected_tilemap_cells.clear()
is_pasting = false
angle = 0.0
content_pivot = Vector2.ZERO
@ -568,12 +628,12 @@ func commit_undo(action: String, undo_data_tmp: Dictionary) -> void:
if !undo_data_tmp:
print("No undo data found!")
return
var redo_data := get_undo_data(undo_data_tmp["undo_image"])
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.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(
self, "big_bounding_rectangle", redo_data["big_bounding_rectangle"]
)
@ -604,15 +664,14 @@ func get_undo_data(undo_image: bool) -> Dictionary:
data["undo_image"] = undo_image
if undo_image:
var images := _get_selected_draw_images()
for image in images:
image.add_data_to_dictionary(data)
Global.current_project.serialize_cel_undo_data(_get_selected_draw_cels(), data)
return data
func _get_selected_draw_cels() -> Array[PixelCel]:
var cels: Array[PixelCel] = []
# TODO: Change BaseCel to PixelCel if Godot ever fixes issues
# with typed arrays being cast into other types.
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]]

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:")
elif layer is Layer3D:
layer_name = tr("3D layer:")
elif layer is LayerTileMap:
layer_name = tr("Tilemap layer:")
layer_name += " %s" % layer.get_layer_path()
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:
_flip_selection(project)
project.update_tilemaps(undo_data)
var redo_data := _get_undo_data(project)
project.undos += 1
project.undo_redo.create_action(action)
Global.undo_redo_compress_images(redo_data, undo_data, project)
project.deserialize_cel_undo_data(redo_data, undo_data)
if redo_data.has("outline_offset"):
project.undo_redo.add_do_property(project, "selection_offset", redo_data["outline_offset"])
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:
var affect_selection := selection_checkbox.button_pressed and project.has_selection
var data := {}
var data := super._get_undo_data(project)
if affect_selection:
data[project.selection_map] = project.selection_map.data
data["outline_offset"] = project.selection_offset
var images := _get_selected_draw_images(project)
for image in images:
data[image] = image.data
return data

View file

@ -11,7 +11,8 @@ enum ImageImportOptions {
NEW_REFERENCE_IMAGE,
PALETTE,
BRUSH,
PATTERN
PATTERN,
TILESET
}
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 brush")
import_option_button.add_item("New pattern")
import_option_button.add_item("Tileset")
# adding custom importers
for id in custom_importers.keys():
@ -207,6 +209,17 @@ func _on_ImportPreviewDialog_confirmed() -> void:
var location := "Patterns".path_join(file_name_ext)
var dir := DirAccess.open(path.get_base_dir())
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:
if current_import_option in custom_importers.keys():
@ -250,7 +263,11 @@ func synchronize() -> void:
dialog.at_layer_option.get_node("AtLayerOption") as OptionButton
)
# 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 v_frames := spritesheet_options.find_child("VerticalFrames") 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()
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
spritesheet_options.visible = true
texture_rect.get_child(0).visible = true
@ -505,6 +522,7 @@ func _call_queue_redraw() -> void:
if (
current_import_option == ImageImportOptions.SPRITESHEET_TAB
or current_import_option == ImageImportOptions.SPRITESHEET_LAYER
or current_import_option == ImageImportOptions.TILESET
):
if smart_slice:
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"]
layout_mode = 2
mouse_default_cursor_shape = 2
item_count = 3
selected = 0
item_count = 3
popup/item_0/text = "File brush"
popup/item_0/id = 0
popup/item_1/text = "Project brush"
popup/item_1/id = 1
popup/item_2/text = "Random brush"

View file

@ -1,11 +1,16 @@
extends AcceptDialog
@onready var size_value_label := $GridContainer/SizeValueLabel as Label
@onready var color_mode_value_label := $GridContainer/ColorModeValueLabel as Label
@onready var frames_value_label := $GridContainer/FramesValueLabel as Label
@onready var layers_value_label := $GridContainer/LayersValueLabel as Label
@onready var name_line_edit := $GridContainer/NameLineEdit as LineEdit
@onready var user_data_text_edit := $GridContainer/UserDataTextEdit as TextEdit
const DUPLICATE_TEXTURE := preload("res://assets/graphics/timeline/copy_frame.png")
const REMOVE_TEXTURE := preload("res://assets/graphics/misc/close.png")
@onready var size_value_label := $VBoxContainer/GridContainer/SizeValueLabel as Label
@onready var color_mode_value_label := $VBoxContainer/GridContainer/ColorModeValueLabel as Label
@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:
@ -21,6 +26,30 @@ func _on_visibility_changed() -> void:
layers_value_label.text = str(Global.current_project.layers.size())
name_line_edit.text = Global.current_project.name
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:
@ -29,3 +58,35 @@ func _on_name_line_edit_text_changed(new_text: String) -> void:
func _on_user_data_text_edit_text_changed() -> void:
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"]
title = "Project Properties"
size = Vector2i(197, 235)
position = Vector2i(0, 36)
size = Vector2i(300, 288)
script = ExtResource("1_0n4uc")
[node name="GridContainer" type="GridContainer" parent="."]
[node name="VBoxContainer" type="VBoxContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 189.0
offset_bottom = 186.0
offset_right = 292.0
offset_bottom = 239.0
[node name="GridContainer" type="GridContainer" parent="VBoxContainer"]
layout_mode = 2
columns = 2
[node name="SizeLabel" type="Label" parent="GridContainer"]
[node name="SizeLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Size:"
[node name="SizeValueLabel" type="Label" parent="GridContainer"]
[node name="SizeValueLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "64x64"
[node name="ColorModeLabel" type="Label" parent="GridContainer"]
[node name="ColorModeLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Color mode:"
[node name="ColorModeValueLabel" type="Label" parent="GridContainer"]
[node name="ColorModeValueLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "RGBA8"
[node name="FramesLabel" type="Label" parent="GridContainer"]
[node name="FramesLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Frames:"
[node name="FramesValueLabel" type="Label" parent="GridContainer"]
[node name="FramesValueLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "1"
[node name="LayersLabel" type="Label" parent="GridContainer"]
[node name="LayersLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Layers:"
[node name="LayersValueLabel" type="Label" parent="GridContainer"]
[node name="LayersValueLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "1"
[node name="NameLabel" type="Label" parent="GridContainer"]
[node name="NameLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Name:"
[node name="NameLineEdit" type="LineEdit" parent="GridContainer"]
[node name="NameLineEdit" type="LineEdit" parent="VBoxContainer/GridContainer"]
layout_mode = 2
[node name="UserDataLabel" type="Label" parent="GridContainer"]
[node name="UserDataLabel" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 0
text = "User data:"
[node name="UserDataTextEdit" type="TextEdit" parent="GridContainer"]
[node name="UserDataTextEdit" type="TextEdit" parent="VBoxContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
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="text_changed" from="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/NameLineEdit" to="." method="_on_name_line_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 fps_spinbox := %FPSValue as ValueSlider
@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 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
@ -70,7 +71,7 @@ func _ready() -> void:
cel_size_slider.min_value = min_cel_size
cel_size_slider.max_value = max_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)
animation_timer.wait_time = 1 / Global.current_project.fps
fps_spinbox.value = Global.current_project.fps
@ -475,6 +476,8 @@ func copy_frames(
)
if src_cel.selected != null:
selected_id = src_cel.selected.id
elif src_cel is CelTileMap:
new_cel = CelTileMap.new(src_cel.tileset)
else:
new_cel = src_cel.get_script().new()
@ -832,24 +835,34 @@ func _on_FuturePlacement_item_selected(index: int) -> void:
# Layer buttons
func add_layer(type := 0) -> void:
func _on_add_layer_pressed() -> void:
var project := Global.current_project
var current_layer := project.layers[project.current_layer]
var l: BaseLayer
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")
var layer := PixelLayer.new(project)
add_layer(layer, project)
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 := []
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
if current_layer is GroupLayer:
@ -862,14 +875,14 @@ func add_layer(type := 0) -> void:
layer_button.visible = expanded
Global.cel_vbox.get_child(layer_button.get_index()).visible = expanded
# make layer child of group
l.parent = Global.current_project.layers[project.current_layer]
layer.parent = Global.current_project.layers[project.current_layer]
else:
# 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.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_do_method(project.change_cel.bind(-1, new_layer_idx))
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 cels := [] # 2D Array of Cels
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.index = src_layer.index
var src_layer_data: Dictionary = src_layer.serialize()
@ -904,6 +921,8 @@ func _on_CloneLayer_pressed() -> void:
new_cel = Cel3D.new(
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:
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_undo_property(bottom_cel, "image", bottom_cel.image)
else:
var redo_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)
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_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="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://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="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://fbwld5ofmocm" path="res://assets/graphics/timeline/loop.png" id="31"]
@ -239,12 +240,14 @@ offset_left = -22.0
offset_top = -10.0
offset_bottom = 10.0
mouse_default_cursor_shape = 2
item_count = 3
item_count = 4
popup/item_0/text = "Add Pixel Layer"
popup/item_1/text = "Add Group Layer"
popup/item_1/id = 1
popup/item_2/text = "Add 3D Layer"
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"]
layout_mode = 0
@ -1114,6 +1117,8 @@ size_flags_horizontal = 0
mouse_default_cursor_shape = 2
text = "Color mode"
[node name="NewTileMapLayerDialog" parent="." instance=ExtResource("29_t0mtf")]
[node name="DragHighlight" type="ColorRect" parent="."]
visible = false
z_index = 2
@ -1123,7 +1128,7 @@ offset_bottom = 40.0
mouse_filter = 2
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/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]]

View file

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

View file

@ -8,13 +8,15 @@ var layer_indices: PackedInt32Array
@onready var opacity_slider := $GridContainer/OpacitySlider as ValueSlider
@onready var blend_modes_button := $GridContainer/BlendModeOptionButton as OptionButton
@onready var user_data_text_edit := $GridContainer/UserDataTextEdit as TextEdit
@onready var tileset_option_button := $GridContainer/TilesetOptionButton as OptionButton
func _on_visibility_changed() -> void:
if layer_indices.size() == 0:
return
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:
_fill_blend_modes_option_button()
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)
blend_modes_button.selected = blend_mode_index
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:
layer_indices = []
@ -86,6 +96,7 @@ func _on_blend_mode_option_button_item_selected(index: BaseLayer.BlendModes) ->
Global.canvas.update_all_layers = true
var project := Global.current_project
var current_mode := blend_modes_button.get_item_id(index)
project.undos += 1
project.undo_redo.create_action("Set Blend Mode")
for layer_index in layer_indices:
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:
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"]
title = "Layer properties"
size = Vector2i(300, 208)
script = ExtResource("1_54q1t")
[node name="GridContainer" type="GridContainer" parent="."]
offset_right = 40.0
offset_bottom = 40.0
offset_left = 8.0
offset_top = 8.0
offset_right = 292.0
offset_bottom = 159.0
columns = 2
[node name="NameLabel" type="Label" parent="GridContainer"]
@ -60,8 +63,19 @@ layout_mode = 2
size_flags_horizontal = 3
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="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="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="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.hide_on_checkable_item_selection = false
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)
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)
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 redo_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():
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,
# so we can store them as redo data.
if id == ColorModes.RGBA:
project.color_mode = Image.FORMAT_RGBA8
else:
project.color_mode = Project.INDEXED_MODE
for cel in project.get_all_pixel_cels():
cel.get_image().add_data_to_dictionary(redo_data)
project.update_tilemaps(undo_data)
project.serialize_cel_undo_data(pixel_cels, redo_data)
project.undo_redo.create_action("Change color mode")
project.undos += 1
project.undo_redo.add_do_property(project, "color_mode", project.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_undo_method(_check_color_mode_submenu_item.bind(project))
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:
if zen_mode:
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)
panels_submenu.set_item_checked(id, !element_visible)
panels_submenu.set_item_checked(index, !element_visible)
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))
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])
panels_submenu.set_item_checked(i, !is_hidden)
panels_submenu.set_item_checked(index, !is_hidden)
if zen_mode: # Turn zen mode off
Global.control.find_child("TabsContainer").visible = true
@ -866,9 +876,11 @@ func _toggle_show_mouse_guides() -> void:
func _toggle_zen_mode() -> void:
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
if !panels_submenu.is_item_checked(i):
if !panels_submenu.is_item_checked(index):
continue
main_ui.set_control_hidden(ui_elements[i], !zen_mode)
Global.control.find_child("TabsContainer").visible = zen_mode

View file

@ -3,16 +3,25 @@ extends Panel
var shader_disabled := false
var transparency_material: ShaderMaterial
@onready var dockable_container: DockableContainer = $DockableContainer
@onready var main_canvas_container := find_child("Main Canvas") as Container
@onready var tiles: TileSetPanel = $DockableContainer/Tiles
func _ready() -> void:
Global.cel_switched.connect(_on_cel_switched)
transparency_material = material
main_canvas_container.property_list_changed.connect(_re_configure_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
if get_window() != main_canvas_container.get_window():
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://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://wplk62pbgih4" path="res://src/Palette/PalettePanel.tscn" id="20"]
[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/dockable_container.gd" id="35"]
[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"]
resource_name = "Tabs"
script = ExtResource("36")
names = PackedStringArray("Tools", "Reference Images")
names = PackedStringArray("Tools", "Reference Images", "Tiles")
current_tab = 0
[sub_resource type="Resource" id="Resource_34hle"]
@ -401,6 +402,10 @@ size_flags_vertical = 3
[node name="Palettes" parent="DockableContainer" instance=ExtResource("20")]
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")]
visible = false
layout_mode = 2