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:
parent
b48bb4a094
commit
f91bb18fb2
|
@ -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 ""
|
||||
|
|
1
assets/graphics/misc/mirror_x.svg
Normal file
1
assets/graphics/misc/mirror_x.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="none" stroke="#e0e0e0" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6 2 8l2 2M2 8h11m-1-2 2 2-2 2"/></svg>
|
After Width: | Height: | Size: 206 B |
37
assets/graphics/misc/mirror_x.svg.import
Normal file
37
assets/graphics/misc/mirror_x.svg.import
Normal file
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bpsfilx47bw3r"
|
||||
path="res://.godot/imported/mirror_x.svg-16a0646fb607af92a2ccf231dd0f1d98.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/graphics/misc/mirror_x.svg"
|
||||
dest_files=["res://.godot/imported/mirror_x.svg-16a0646fb607af92a2ccf231dd0f1d98.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
1
assets/graphics/misc/mirror_y.svg
Normal file
1
assets/graphics/misc/mirror_y.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="none" stroke="#e0e0e0" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 4 8 2 6 4m2-2v11m2-1-2 2-2-2"/></svg>
|
After Width: | Height: | Size: 206 B |
37
assets/graphics/misc/mirror_y.svg.import
Normal file
37
assets/graphics/misc/mirror_y.svg.import
Normal file
|
@ -0,0 +1,37 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bk6iaxiyl74ih"
|
||||
path="res://.godot/imported/mirror_y.svg-47cb90f0f94e4ed7c37f151a9ddbaab0.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/graphics/misc/mirror_y.svg"
|
||||
dest_files=["res://.godot/imported/mirror_y.svg-47cb90f0f94e4ed7c37f151a9ddbaab0.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
|
@ -1,30 +1,44 @@
|
|||
[gd_resource type="Resource" script_class="DockableLayout" load_steps=27 format=3 uid="uid://4xtpiowddm7p"]
|
||||
[gd_resource type="Resource" script_class="DockableLayout" load_steps=29 format=3 uid="uid://4xtpiowddm7p"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_panel.gd" id="1"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_split.gd" id="2"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout.gd" id="3"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_panel.gd" id="1_jxh43"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_split.gd" id="2_lw52w"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout.gd" id="3_4h5wj"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_atmme"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Tools")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_4b0py"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Tiles")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_epagr"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 1
|
||||
percent = 0.5
|
||||
first = SubResource("Resource_atmme")
|
||||
second = SubResource("Resource_4b0py")
|
||||
|
||||
[sub_resource type="Resource" id="Resource_ouvfk"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Main Canvas")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_an0ef"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Perspective Editor")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_xgnjk"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 0
|
||||
percent = 0.5
|
||||
first = SubResource("Resource_ouvfk")
|
||||
|
@ -32,13 +46,13 @@ second = SubResource("Resource_an0ef")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_o7cqb"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Second Canvas")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_ataha"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 0
|
||||
percent = 0.980952
|
||||
first = SubResource("Resource_xgnjk")
|
||||
|
@ -46,13 +60,13 @@ second = SubResource("Resource_o7cqb")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_8y4au"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Animation Timeline")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_q2jwk"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 1
|
||||
percent = 0.75578
|
||||
first = SubResource("Resource_ataha")
|
||||
|
@ -60,19 +74,19 @@ second = SubResource("Resource_8y4au")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_5r0ap"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Canvas Preview")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_6pqxe"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Recorder")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_ln20x"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 1
|
||||
percent = 0.911765
|
||||
first = SubResource("Resource_5r0ap")
|
||||
|
@ -80,39 +94,39 @@ second = SubResource("Resource_6pqxe")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_dksrd"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Global Tool Options")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_kmey0"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Color Picker", "Reference Images")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_1tm61"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 1
|
||||
percent = 0.134307
|
||||
percent = 0.0499712
|
||||
first = SubResource("Resource_dksrd")
|
||||
second = SubResource("Resource_kmey0")
|
||||
|
||||
[sub_resource type="Resource" id="Resource_btl4b"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Left Tool Options")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_eu0mc"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Right Tool Options")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_8ff4m"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 0
|
||||
percent = 0.5
|
||||
first = SubResource("Resource_btl4b")
|
||||
|
@ -120,21 +134,21 @@ second = SubResource("Resource_eu0mc")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_e72nu"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 1
|
||||
percent = 0.660142
|
||||
percent = 0.643859
|
||||
first = SubResource("Resource_1tm61")
|
||||
second = SubResource("Resource_8ff4m")
|
||||
|
||||
[sub_resource type="Resource" id="Resource_sg54a"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1")
|
||||
script = ExtResource("1_jxh43")
|
||||
names = PackedStringArray("Palettes")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_gdwmg"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 1
|
||||
percent = 0.82948
|
||||
first = SubResource("Resource_e72nu")
|
||||
|
@ -142,7 +156,7 @@ second = SubResource("Resource_sg54a")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_acda3"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 1
|
||||
percent = 0.0549133
|
||||
first = SubResource("Resource_ln20x")
|
||||
|
@ -150,30 +164,31 @@ second = SubResource("Resource_gdwmg")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_2qk0j"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 0
|
||||
percent = 0.731967
|
||||
percent = 0.704098
|
||||
first = SubResource("Resource_q2jwk")
|
||||
second = SubResource("Resource_acda3")
|
||||
|
||||
[sub_resource type="Resource" id="Resource_msuil"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2")
|
||||
script = ExtResource("2_lw52w")
|
||||
direction = 0
|
||||
percent = 0.0
|
||||
first = SubResource("Resource_atmme")
|
||||
first = SubResource("Resource_epagr")
|
||||
second = SubResource("Resource_2qk0j")
|
||||
|
||||
[resource]
|
||||
resource_name = "Default"
|
||||
script = ExtResource("3")
|
||||
script = ExtResource("3_4h5wj")
|
||||
root = SubResource("Resource_msuil")
|
||||
hidden_tabs = {
|
||||
"Canvas Preview": true,
|
||||
"Color Picker Sliders": true,
|
||||
"Perspective Editor": true,
|
||||
"Recorder": true,
|
||||
"Second Canvas": true
|
||||
"Second Canvas": true,
|
||||
"Tiles": true
|
||||
}
|
||||
windows = {}
|
||||
save_on_change = false
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
[gd_resource type="Resource" script_class="DockableLayout" load_steps=23 format=3 uid="uid://brcnmadkdaqok"]
|
||||
[gd_resource type="Resource" script_class="DockableLayout" load_steps=25 format=3 uid="uid://brcnmadkdaqok"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_panel.gd" id="1_nokpu"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_split.gd" id="2_q5vl6"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout.gd" id="3_ox7l5"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_panel.gd" id="1_t44r1"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout_split.gd" id="2_rngtv"]
|
||||
[ext_resource type="Script" path="res://addons/dockable_container/layout.gd" id="3_v86xb"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_kn4x4"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Main Canvas")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_btw27"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Second Canvas")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_bp28t"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 0
|
||||
percent = 0.829091
|
||||
first = SubResource("Resource_kn4x4")
|
||||
|
@ -26,13 +26,13 @@ second = SubResource("Resource_btw27")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_10g0s"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Perspective Editor")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_otntk"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 0
|
||||
percent = 0.8625
|
||||
first = SubResource("Resource_bp28t")
|
||||
|
@ -40,25 +40,25 @@ second = SubResource("Resource_10g0s")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_12axs"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Tools")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_1omiw"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Left Tool Options", "Right Tool Options")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_p32ds"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Color Picker")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_n6xyc"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 0
|
||||
percent = 0.5
|
||||
first = SubResource("Resource_1omiw")
|
||||
|
@ -66,19 +66,19 @@ second = SubResource("Resource_p32ds")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_1dcep"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Canvas Preview", "Reference Images", "Recorder")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_hc3ve"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Global Tool Options")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_nppps"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 1
|
||||
percent = 0.729839
|
||||
first = SubResource("Resource_1dcep")
|
||||
|
@ -86,13 +86,13 @@ second = SubResource("Resource_hc3ve")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_d54jb"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Palettes")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_f6rik"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 0
|
||||
percent = 0.5
|
||||
first = SubResource("Resource_nppps")
|
||||
|
@ -100,7 +100,7 @@ second = SubResource("Resource_d54jb")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_26vov"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 0
|
||||
percent = 0.501251
|
||||
first = SubResource("Resource_n6xyc")
|
||||
|
@ -108,21 +108,35 @@ second = SubResource("Resource_f6rik")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_m3axb"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_nokpu")
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Animation Timeline")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_8dhxy"]
|
||||
resource_name = "Tabs"
|
||||
script = ExtResource("1_t44r1")
|
||||
names = PackedStringArray("Tiles")
|
||||
current_tab = 0
|
||||
|
||||
[sub_resource type="Resource" id="Resource_j3q3h"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 0
|
||||
percent = 0.5
|
||||
first = SubResource("Resource_m3axb")
|
||||
second = SubResource("Resource_8dhxy")
|
||||
|
||||
[sub_resource type="Resource" id="Resource_af0bk"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 1
|
||||
percent = 0.5
|
||||
first = SubResource("Resource_26vov")
|
||||
second = SubResource("Resource_m3axb")
|
||||
second = SubResource("Resource_j3q3h")
|
||||
|
||||
[sub_resource type="Resource" id="Resource_1xpva"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 0
|
||||
percent = 0.03125
|
||||
first = SubResource("Resource_12axs")
|
||||
|
@ -130,7 +144,7 @@ second = SubResource("Resource_af0bk")
|
|||
|
||||
[sub_resource type="Resource" id="Resource_6dytr"]
|
||||
resource_name = "Split"
|
||||
script = ExtResource("2_q5vl6")
|
||||
script = ExtResource("2_rngtv")
|
||||
direction = 1
|
||||
percent = 0.459538
|
||||
first = SubResource("Resource_otntk")
|
||||
|
@ -138,12 +152,13 @@ second = SubResource("Resource_1xpva")
|
|||
|
||||
[resource]
|
||||
resource_name = "Tallscreen"
|
||||
script = ExtResource("3_ox7l5")
|
||||
script = ExtResource("3_v86xb")
|
||||
root = SubResource("Resource_6dytr")
|
||||
hidden_tabs = {
|
||||
"Perspective Editor": true,
|
||||
"Recorder": true,
|
||||
"Second Canvas": true
|
||||
"Second Canvas": true,
|
||||
"Tiles": true
|
||||
}
|
||||
windows = {}
|
||||
save_on_change = false
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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,15 +548,15 @@ 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]
|
||||
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
|
||||
sprite.add_data_to_dictionary(redo_data, cel_image)
|
||||
cel_image.add_data_to_dictionary(undo_data)
|
||||
for cel in Global.current_project.get_all_pixel_cels():
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
var cel_image := (cel as PixelCel).get_image()
|
||||
var sprite := _resize_image(cel_image, width, height, interpolation) as ImageExtended
|
||||
if cel is CelTileMap:
|
||||
(cel as CelTileMap).serialize_undo_data_source_image(sprite, redo_data, undo_data)
|
||||
sprite.add_data_to_dictionary(redo_data, cel_image)
|
||||
cel_image.add_data_to_dictionary(undo_data)
|
||||
|
||||
general_do_and_undo_scale(width, height, redo_data, undo_data)
|
||||
|
||||
|
@ -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,18 +621,17 @@ func crop_to_selection() -> void:
|
|||
func crop_to_content() -> void:
|
||||
Global.canvas.selection.transform_content_confirm()
|
||||
var used_rect := Rect2i()
|
||||
for f in Global.current_project.frames:
|
||||
for cel in f.cels:
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
var cel_used_rect := cel.get_image().get_used_rect()
|
||||
if cel_used_rect == Rect2i(0, 0, 0, 0): # If the cel has no content
|
||||
continue
|
||||
for cel in Global.current_project.get_all_pixel_cels():
|
||||
if not cel is PixelCel:
|
||||
continue
|
||||
var cel_used_rect := cel.get_image().get_used_rect()
|
||||
if cel_used_rect == Rect2i(0, 0, 0, 0): # If the cel has no content
|
||||
continue
|
||||
|
||||
if used_rect == Rect2i(0, 0, 0, 0): # If we still haven't found the first cel with content
|
||||
used_rect = cel_used_rect
|
||||
else:
|
||||
used_rect = used_rect.merge(cel_used_rect)
|
||||
if used_rect == Rect2i(0, 0, 0, 0): # If we still haven't found the first cel with content
|
||||
used_rect = cel_used_rect
|
||||
else:
|
||||
used_rect = used_rect.merge(cel_used_rect)
|
||||
|
||||
# If no layer has any content, just return
|
||||
if used_rect == Rect2i(0, 0, 0, 0):
|
||||
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
700
src/Classes/Cels/CelTileMap.gd
Normal file
700
src/Classes/Cels/CelTileMap.gd
Normal 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"
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
57
src/Classes/Layers/LayerTileMap.gd
Normal file
57
src/Classes/Layers/LayerTileMap.gd
Normal file
|
@ -0,0 +1,57 @@
|
|||
class_name LayerTileMap
|
||||
extends PixelLayer
|
||||
|
||||
## A layer type for 2D tile-based maps.
|
||||
## A LayerTileMap uses a [TileSetCustom], which is then by all of its [CelTileMap]s.
|
||||
## This class doesn't hold any actual tilemap data, as they are different in each cel.
|
||||
## For this reason, that data is being handled by the [CelTileMap] class.
|
||||
## Not to be confused with [TileMapLayer], which is a Godot node.
|
||||
|
||||
## The [TileSetCustom] that this layer uses.
|
||||
## Internally, this class doesn't make much use of this.
|
||||
## It's mostly only used to be passed down to the layer's [CelTileMap]s.
|
||||
var tileset: TileSetCustom
|
||||
|
||||
|
||||
func _init(_project: Project, _tileset: TileSetCustom, _name := "") -> void:
|
||||
super._init(_project, _name)
|
||||
tileset = _tileset
|
||||
if not project.tilesets.has(tileset):
|
||||
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
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
185
src/Classes/TileSetCustom.gd
Normal file
185
src/Classes/TileSetCustom.gd
Normal 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)
|
|
@ -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,34 +163,48 @@ func update_config() -> void:
|
|||
|
||||
func update_brush() -> void:
|
||||
$Brush/BrushSize.suffix = "px" # Assume we are using default brushes
|
||||
match _brush.type:
|
||||
Brushes.PIXEL:
|
||||
_brush_texture = ImageTexture.create_from_image(
|
||||
load("res://assets/graphics/pixel_image.png")
|
||||
)
|
||||
_stroke_dimensions = Vector2.ONE * _brush_size
|
||||
Brushes.CIRCLE:
|
||||
_brush_texture = ImageTexture.create_from_image(
|
||||
load("res://assets/graphics/circle_9x9.png")
|
||||
)
|
||||
_stroke_dimensions = Vector2.ONE * _brush_size
|
||||
Brushes.FILLED_CIRCLE:
|
||||
_brush_texture = ImageTexture.create_from_image(
|
||||
load("res://assets/graphics/circle_filled_9x9.png")
|
||||
)
|
||||
_stroke_dimensions = Vector2.ONE * _brush_size
|
||||
Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM:
|
||||
$Brush/BrushSize.suffix = "00 %" # Use a different size convention on images
|
||||
if _brush.random.size() <= 1:
|
||||
_orignal_brush_image = _brush.image
|
||||
else:
|
||||
var random := randi() % _brush.random.size()
|
||||
_orignal_brush_image = _brush.random[random]
|
||||
_brush_image = _create_blended_brush_image(_orignal_brush_image)
|
||||
update_brush_image_flip_and_rotate()
|
||||
_brush_texture = ImageTexture.create_from_image(_brush_image)
|
||||
update_mirror_brush()
|
||||
_stroke_dimensions = _brush_image.get_size()
|
||||
if Tools.is_placing_tiles():
|
||||
var tilemap_cel := Global.current_project.get_current_cel() as CelTileMap
|
||||
var tileset := tilemap_cel.tileset
|
||||
var tile_index := clampi(TileSetPanel.selected_tile_index, 0, tileset.tiles.size() - 1)
|
||||
var tile_image := tileset.tiles[tile_index].image
|
||||
tile_image = tilemap_cel.transform_tile(
|
||||
tile_image,
|
||||
TileSetPanel.is_flipped_h,
|
||||
TileSetPanel.is_flipped_v,
|
||||
TileSetPanel.is_transposed
|
||||
)
|
||||
_brush_image.copy_from(tile_image)
|
||||
_brush_texture = ImageTexture.create_from_image(_brush_image)
|
||||
else:
|
||||
match _brush.type:
|
||||
Brushes.PIXEL:
|
||||
_brush_texture = ImageTexture.create_from_image(
|
||||
load("res://assets/graphics/pixel_image.png")
|
||||
)
|
||||
_stroke_dimensions = Vector2.ONE * _brush_size
|
||||
Brushes.CIRCLE:
|
||||
_brush_texture = ImageTexture.create_from_image(
|
||||
load("res://assets/graphics/circle_9x9.png")
|
||||
)
|
||||
_stroke_dimensions = Vector2.ONE * _brush_size
|
||||
Brushes.FILLED_CIRCLE:
|
||||
_brush_texture = ImageTexture.create_from_image(
|
||||
load("res://assets/graphics/circle_filled_9x9.png")
|
||||
)
|
||||
_stroke_dimensions = Vector2.ONE * _brush_size
|
||||
Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM:
|
||||
$Brush/BrushSize.suffix = "00 %" # Use a different size convention on images
|
||||
if _brush.random.size() <= 1:
|
||||
_orignal_brush_image = _brush.image
|
||||
else:
|
||||
var random := randi() % _brush.random.size()
|
||||
_orignal_brush_image = _brush.random[random]
|
||||
_brush_image = _create_blended_brush_image(_orignal_brush_image)
|
||||
update_brush_image_flip_and_rotate()
|
||||
_brush_texture = ImageTexture.create_from_image(_brush_image)
|
||||
update_mirror_brush()
|
||||
_stroke_dimensions = _brush_image.get_size()
|
||||
_circle_tool_shortcut = []
|
||||
_indicator = _create_brush_indicator()
|
||||
_polylines = _create_polylines(_indicator)
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,9 +189,12 @@ func _draw_shape(origin: Vector2i, dest: Vector2i) -> void:
|
|||
_drawer.reset()
|
||||
# Draw each point offsetted based on the shape's thickness
|
||||
var draw_pos := point + thickness_vector
|
||||
if Global.current_project.can_pixel_get_drawn(draw_pos):
|
||||
for image in images:
|
||||
_drawer.set_pixel(image, draw_pos, tool_slot.color)
|
||||
if Tools.is_placing_tiles():
|
||||
draw_tile(draw_pos)
|
||||
else:
|
||||
if Global.current_project.can_pixel_get_drawn(draw_pos):
|
||||
for image in images:
|
||||
_drawer.set_pixel(image, draw_pos, tool_slot.color)
|
||||
|
||||
commit_undo()
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,9 +195,12 @@ func _draw_shape() -> void:
|
|||
|
||||
|
||||
func _draw_pixel(point: Vector2i, images: Array[ImageExtended]) -> void:
|
||||
if Global.current_project.can_pixel_get_drawn(point):
|
||||
for image in images:
|
||||
_drawer.set_pixel(image, point, tool_slot.color)
|
||||
if Tools.is_placing_tiles():
|
||||
draw_tile(point)
|
||||
else:
|
||||
if Global.current_project.can_pixel_get_drawn(point):
|
||||
for image in images:
|
||||
_drawer.set_pixel(image, point, tool_slot.color)
|
||||
|
||||
|
||||
func _clear() -> void:
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,10 +174,13 @@ func _draw_shape() -> void:
|
|||
for point in points:
|
||||
# Reset drawer every time because pixel perfect sometimes breaks the tool
|
||||
_drawer.reset()
|
||||
# Draw each point offsetted based on the shape's thickness
|
||||
if Global.current_project.can_pixel_get_drawn(point):
|
||||
for image in images:
|
||||
_drawer.set_pixel(image, point, tool_slot.color)
|
||||
if Tools.is_placing_tiles():
|
||||
draw_tile(point)
|
||||
else:
|
||||
# Draw each point offsetted based on the shape's thickness
|
||||
if Global.current_project.can_pixel_get_drawn(point):
|
||||
for image in images:
|
||||
_drawer.set_pixel(image, point, tool_slot.color)
|
||||
|
||||
commit_undo()
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
extends "res://src/Tools/BaseDraw.gd"
|
||||
extends BaseDrawTool
|
||||
|
||||
enum ShadingMode { SIMPLE, HUE_SHIFTING, COLOR_REPLACE }
|
||||
enum LightenDarken { LIGHTEN, DARKEN }
|
||||
|
|
|
@ -32,23 +32,46 @@ func apply_selection(pos: Vector2i) -> void:
|
|||
if pos.x > project.size.x - 1 or pos.y > project.size.y - 1:
|
||||
return
|
||||
|
||||
var cel_image := Image.new()
|
||||
cel_image.copy_from(_get_draw_image())
|
||||
var color := cel_image.get_pixelv(pos)
|
||||
var operation := 0
|
||||
if _subtract:
|
||||
operation = 1
|
||||
elif _intersect:
|
||||
operation = 2
|
||||
|
||||
var params := {"color": color, "tolerance": _tolerance, "operation": operation}
|
||||
if _add or _subtract or _intersect:
|
||||
var selection_tex := ImageTexture.create_from_image(project.selection_map)
|
||||
params["selection"] = selection_tex
|
||||
var gen := ShaderImageEffect.new()
|
||||
gen.generate_image(cel_image, shader, params, project.size)
|
||||
cel_image.convert(Image.FORMAT_LA8)
|
||||
if Tools.is_placing_tiles():
|
||||
var prev_selection_map := SelectionMap.new() # Used for intersect
|
||||
prev_selection_map.copy_from(project.selection_map)
|
||||
if !_add and !_subtract and !_intersect:
|
||||
Global.canvas.selection.clear_selection()
|
||||
if _intersect:
|
||||
project.selection_map.clear()
|
||||
for cel in _get_selected_draw_cels():
|
||||
if cel is not CelTileMap:
|
||||
continue
|
||||
var tilemap_cel := cel as CelTileMap
|
||||
var tile_index := tilemap_cel.get_cell_index_at_coords(pos)
|
||||
for i in tilemap_cel.cells.size():
|
||||
var cell := tilemap_cel.cells[i]
|
||||
if cell.index == tile_index:
|
||||
if _intersect:
|
||||
var p := (cel as CelTileMap).get_cell_coords_in_image(i)
|
||||
select_tilemap_cell(
|
||||
cel, i, project.selection_map, prev_selection_map.is_pixel_selected(p)
|
||||
)
|
||||
else:
|
||||
select_tilemap_cell(cel, i, project.selection_map, !_subtract)
|
||||
else:
|
||||
var cel_image := Image.new()
|
||||
cel_image.copy_from(_get_draw_image())
|
||||
var color := cel_image.get_pixelv(pos)
|
||||
var params := {"color": color, "tolerance": _tolerance, "operation": operation}
|
||||
if _add or _subtract or _intersect:
|
||||
var selection_tex := ImageTexture.create_from_image(project.selection_map)
|
||||
params["selection"] = selection_tex
|
||||
var gen := ShaderImageEffect.new()
|
||||
gen.generate_image(cel_image, shader, params, project.size)
|
||||
cel_image.convert(Image.FORMAT_LA8)
|
||||
|
||||
project.selection_map.copy_from(cel_image)
|
||||
project.selection_map.copy_from(cel_image)
|
||||
Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect()
|
||||
Global.canvas.selection.commit_undo("Select", undo_data)
|
||||
|
|
|
@ -81,18 +81,36 @@ func apply_selection(_position: Vector2i) -> void:
|
|||
Global.canvas.selection.commit_undo("Select", undo_data)
|
||||
if _rect.size == Vector2i.ZERO:
|
||||
return
|
||||
set_ellipse(project.selection_map, _rect.position)
|
||||
# Handle mirroring
|
||||
var mirror_positions := Tools.get_mirrored_positions(_rect.position, project, 1)
|
||||
var mirror_ends := Tools.get_mirrored_positions(_rect.end, project, 1)
|
||||
for i in mirror_positions.size():
|
||||
var mirror_rect := Rect2i()
|
||||
mirror_rect.position = mirror_positions[i]
|
||||
mirror_rect.end = mirror_ends[i]
|
||||
set_ellipse(project.selection_map, mirror_rect.abs().position)
|
||||
if Tools.is_placing_tiles():
|
||||
var operation := 0
|
||||
if _subtract:
|
||||
operation = 1
|
||||
elif _intersect:
|
||||
operation = 2
|
||||
Global.canvas.selection.select_rect(_rect, operation)
|
||||
# Handle mirroring
|
||||
var mirror_positions := Tools.get_mirrored_positions(_rect.position, project, 1)
|
||||
var mirror_ends := Tools.get_mirrored_positions(_rect.end, project, 1)
|
||||
for i in mirror_positions.size():
|
||||
var mirror_rect := Rect2i()
|
||||
mirror_rect.position = mirror_positions[i]
|
||||
mirror_rect.end = mirror_ends[i]
|
||||
Global.canvas.selection.select_rect(mirror_rect.abs(), operation)
|
||||
|
||||
Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect()
|
||||
Global.canvas.selection.commit_undo("Select", undo_data)
|
||||
Global.canvas.selection.commit_undo("Select", undo_data)
|
||||
else:
|
||||
set_ellipse(project.selection_map, _rect.position)
|
||||
# Handle mirroring
|
||||
var mirror_positions := Tools.get_mirrored_positions(_rect.position, project, 1)
|
||||
var mirror_ends := Tools.get_mirrored_positions(_rect.end, project, 1)
|
||||
for i in mirror_positions.size():
|
||||
var mirror_rect := Rect2i()
|
||||
mirror_rect.position = mirror_positions[i]
|
||||
mirror_rect.end = mirror_ends[i]
|
||||
set_ellipse(project.selection_map, mirror_rect.abs().position)
|
||||
|
||||
Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect()
|
||||
Global.canvas.selection.commit_undo("Select", undo_data)
|
||||
|
||||
|
||||
func set_ellipse(selection_map: SelectionMap, pos: Vector2i) -> void:
|
||||
|
@ -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()
|
||||
|
||||
rect.size += Vector2i.ONE
|
||||
if not Tools.is_placing_tiles():
|
||||
rect.size += Vector2i.ONE
|
||||
|
||||
return rect
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -101,6 +101,11 @@ func apply_selection(pos: Vector2i) -> void:
|
|||
## Given an origin point and destination point, returns a rect representing
|
||||
## where the shape will be drawn and what is its size
|
||||
func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i:
|
||||
if Tools.is_placing_tiles():
|
||||
var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset
|
||||
var grid_size := tileset.tile_size
|
||||
origin = Tools.snap_to_rectangular_grid_boundary(origin, grid_size)
|
||||
dest = Tools.snap_to_rectangular_grid_boundary(dest, grid_size)
|
||||
var rect := Rect2i()
|
||||
|
||||
# Center the rect on the mouse
|
||||
|
@ -125,6 +130,7 @@ func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i:
|
|||
rect.position = Vector2i(mini(origin.x, dest.x), mini(origin.y, dest.y))
|
||||
rect.size = (origin - dest).abs()
|
||||
|
||||
rect.size += Vector2i.ONE
|
||||
if not Tools.is_placing_tiles():
|
||||
rect.size += Vector2i.ONE
|
||||
|
||||
return rect
|
||||
|
|
|
@ -63,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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,14 +388,29 @@ 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)
|
||||
DrawingAlgos.nn_rotate(preview_image, angle, content_pivot)
|
||||
preview_image.resize(size.x, size.y, Image.INTERPOLATE_NEAREST)
|
||||
if temp_rect.size.x < 0:
|
||||
preview_image.flip_x()
|
||||
if temp_rect.size.y < 0:
|
||||
preview_image.flip_y()
|
||||
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:
|
||||
preview_image.flip_x()
|
||||
if temp_rect.size.y < 0:
|
||||
preview_image.flip_y()
|
||||
preview_image_texture = ImageTexture.create_from_image(preview_image)
|
||||
|
||||
Global.current_project.selection_map.copy_from(original_bitmap)
|
||||
|
@ -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,21 +540,40 @@ func transform_content_confirm() -> void:
|
|||
if not is_pasting:
|
||||
src.copy_from(cel.transformed_content)
|
||||
cel.transformed_content = null
|
||||
DrawingAlgos.nn_rotate(src, angle, content_pivot)
|
||||
src.resize(
|
||||
preview_image.get_width(), preview_image.get_height(), Image.INTERPOLATE_NEAREST
|
||||
)
|
||||
if temp_rect.size.x < 0:
|
||||
src.flip_x()
|
||||
if temp_rect.size.y < 0:
|
||||
src.flip_y()
|
||||
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
|
||||
)
|
||||
if temp_rect.size.x < 0:
|
||||
src.flip_x()
|
||||
if temp_rect.size.y < 0:
|
||||
src.flip_y()
|
||||
|
||||
cel_image.blit_rect_mask(
|
||||
src,
|
||||
src,
|
||||
Rect2i(Vector2i.ZERO, project.selection_map.get_size()),
|
||||
big_bounding_rectangle.position
|
||||
)
|
||||
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,
|
||||
Rect2i(Vector2i.ZERO, project.selection_map.get_size()),
|
||||
big_bounding_rectangle.position
|
||||
)
|
||||
cel_image.convert_rgb_to_indexed()
|
||||
project.selection_map.move_bitmap_values(project)
|
||||
commit_undo("Move Selection", undo_data)
|
||||
|
@ -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]]
|
||||
|
|
26
src/UI/Canvas/TileModeIndices.gd
Normal file
26
src/UI/Canvas/TileModeIndices.gd
Normal 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)
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
207
src/UI/TilesPanel.gd
Normal 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
189
src/UI/TilesPanel.tscn
Normal 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"]
|
|
@ -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(
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"]
|
||||
|
|
46
src/UI/Timeline/NewTileMapLayerDialog.gd
Normal file
46
src/UI/Timeline/NewTileMapLayerDialog.gd
Normal 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
|
70
src/UI/Timeline/NewTileMapLayerDialog.tscn
Normal file
70
src/UI/Timeline/NewTileMapLayerDialog.tscn
Normal 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"]
|
|
@ -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
|
||||
|
|
11
src/UI/UI.gd
11
src/UI/UI.gd
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue