From d562e50af7cb7894c015ce498fa267791b198f2f Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 16 Nov 2024 03:41:32 +0200 Subject: [PATCH 01/76] Initial work for tilemap layers --- src/Autoload/Global.gd | 2 +- src/Classes/Cels/CelTileMap.gd | 44 ++++++++++++++++++++++++++++++ src/Classes/Layers/LayerTileMap.gd | 19 +++++++++++++ src/Classes/TileSetCustom.gd | 16 +++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/Classes/Cels/CelTileMap.gd create mode 100644 src/Classes/Layers/LayerTileMap.gd create mode 100644 src/Classes/TileSetCustom.gd diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index 8a14d3b56..49a0caa16 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -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. diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd new file mode 100644 index 000000000..26a6b8ccf --- /dev/null +++ b/src/Classes/Cels/CelTileMap.gd @@ -0,0 +1,44 @@ +class_name CelTileMap +extends PixelCel + +enum TileEditingMode { MANUAL, AUTO, STACK } + +var tileset: TileSetCustom +var tile_editing_mode := TileEditingMode.MANUAL +var indices := PackedInt32Array() +var indices_x: int +var indices_y: int + + +func _init(_tileset: TileSetCustom, _image := Image.new(), _opacity := 1.0) -> void: + super._init(_image, _opacity) + tileset = _tileset + indices_x = ceili(float(get_image().get_width()) / tileset.tile_size.x) + indices_y = ceili(float(get_image().get_height()) / tileset.tile_size.y) + indices.resize(indices_x * indices_y) + + +func update_texture() -> void: + super.update_texture() + for i in indices.size(): + var x_coord := float(tileset.tile_size.x) * (i % indices_x) + var y_coord := float(tileset.tile_size.y) * (i / indices_x) + var rect := Rect2i(Vector2i(x_coord, y_coord), tileset.tile_size) + var image_portion := image.get_region(rect) + var index := indices[i] + if tile_editing_mode == TileEditingMode.MANUAL: + if index == 0: + continue + if image_portion.get_data() != tileset.tiles[index].get_data(): + tileset.tiles[index].copy_from(image_portion) + elif tile_editing_mode == TileEditingMode.AUTO: + if index == 0: + continue + else: + if not image_portion.is_invisible(): + tileset.tiles.append(image_portion) + indices[i] = tileset.tiles.size() + + +func get_class_name() -> String: + return "CelTileMap" diff --git a/src/Classes/Layers/LayerTileMap.gd b/src/Classes/Layers/LayerTileMap.gd new file mode 100644 index 000000000..d54b68882 --- /dev/null +++ b/src/Classes/Layers/LayerTileMap.gd @@ -0,0 +1,19 @@ +class_name LayerTileMap +extends PixelLayer + +var tileset: TileSetCustom + + +func _init(_project: Project, _tileset: TileSetCustom, _name := "") -> void: + super._init(_project, _name) + tileset = _tileset + + +# Overridden Methods: +func get_layer_type() -> int: + return Global.LayerTypes.TILEMAP + + +func new_empty_cel() -> BaseCel: + var image := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + return CelTileMap.new(tileset, image) diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd new file mode 100644 index 000000000..2feafe1ed --- /dev/null +++ b/src/Classes/TileSetCustom.gd @@ -0,0 +1,16 @@ +class_name TileSetCustom +extends RefCounted + +var name := "" +var tile_size: Vector2i +var tiles: Array[Image] = [] + + +func _init(_tile_size: Vector2i, _name := "") -> void: + tile_size = _tile_size + name = _name + #var indices_x := ceili(float(_project_size.x) / tile_size.x) + #var indices_y := ceili(float(_project_size.y) / tile_size.y) + #tiles.resize(indices_x * indices_y + 1) + var empty_image := Image.create_empty(tile_size.x, tile_size.y, false, Image.FORMAT_RGBA8) + tiles.append(empty_image) From 85854b3490f3b4778b0399cc7b61162cf3c0efd7 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 16 Nov 2024 15:38:06 +0200 Subject: [PATCH 02/76] Implement all draw modes (untested) --- src/Classes/Cels/CelTileMap.gd | 36 +++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 26a6b8ccf..cb4ba7d62 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -20,22 +20,48 @@ func _init(_tileset: TileSetCustom, _image := Image.new(), _opacity := 1.0) -> v func update_texture() -> void: super.update_texture() + if tile_editing_mode == TileEditingMode.AUTO: + var tiles_to_delete := PackedInt32Array() + for j in range(1, tileset.tiles.size()): + var tile := tileset.tiles[j] + var tile_used := false + for i in indices.size(): + var x_coord := float(tileset.tile_size.x) * (i % indices_x) + var y_coord := float(tileset.tile_size.y) * (i / indices_x) + var rect := Rect2i(Vector2i(x_coord, y_coord), tileset.tile_size) + var image_portion := image.get_region(rect) + if image_portion.is_invisible(): + continue + if image_portion.get_data() == tile.get_data(): + tile_used = true + break + if not tile_used: + tiles_to_delete.append(j) + for j in tiles_to_delete: + tileset.tiles.remove_at(j) for i in indices.size(): var x_coord := float(tileset.tile_size.x) * (i % indices_x) var y_coord := float(tileset.tile_size.y) * (i / indices_x) var rect := Rect2i(Vector2i(x_coord, y_coord), tileset.tile_size) var image_portion := image.get_region(rect) + if image_portion.is_invisible(): + continue var index := indices[i] if tile_editing_mode == TileEditingMode.MANUAL: - if index == 0: + if index == 0 or tileset.tiles.size() <= index: continue if image_portion.get_data() != tileset.tiles[index].get_data(): tileset.tiles[index].copy_from(image_portion) - elif tile_editing_mode == TileEditingMode.AUTO: - if index == 0: - continue + # TODO: Update the rest of the tilemap else: - if not image_portion.is_invisible(): + var found_tile := false + for j in range(1, tileset.tiles.size()): + var tile := tileset.tiles[j] + if image_portion.get_data() == tile.get_data(): + indices[i] = j + found_tile = true + break + if not found_tile: tileset.tiles.append(image_portion) indices[i] = tileset.tiles.size() From dab8c5bed56bb07ea11f650a67c457ddebc0ffbd Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 18 Nov 2024 01:30:27 +0200 Subject: [PATCH 03/76] Add tilemap layers --- src/Autoload/Tools.gd | 24 +++++++++++++++--------- src/Classes/Layers/LayerTileMap.gd | 2 ++ src/Classes/Project.gd | 5 +++++ src/UI/Dialogs/ExportDialog.gd | 2 ++ src/UI/Timeline/AnimationTimeline.gd | 2 ++ src/UI/Timeline/AnimationTimeline.tscn | 4 +++- 6 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/Autoload/Tools.gd b/src/Autoload/Tools.gd index 3a79882e1..17d062e50 100644 --- a/src/Autoload/Tools.gd +++ b/src/Autoload/Tools.gd @@ -88,7 +88,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 +120,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 +130,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 +140,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 +148,7 @@ var tools := { "Shading Tool", "shading", "res://src/Tools/DesignTools/Shading.tscn", - [Global.LayerTypes.PIXEL] + [Global.LayerTypes.PIXEL, Global.LayerTypes.TILEMAP] ), "LineTool": ( @@ -154,7 +158,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 +173,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 +189,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 +204,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""", @@ -232,10 +236,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 diff --git a/src/Classes/Layers/LayerTileMap.gd b/src/Classes/Layers/LayerTileMap.gd index d54b68882..f48ed651e 100644 --- a/src/Classes/Layers/LayerTileMap.gd +++ b/src/Classes/Layers/LayerTileMap.gd @@ -7,6 +7,8 @@ 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: diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index 30baf79a1..bdea6a52b 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -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] @@ -931,3 +932,7 @@ 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) + + +func add_tileset(tileset: TileSetCustom) -> void: + tilesets.append(tileset) diff --git a/src/UI/Dialogs/ExportDialog.gd b/src/UI/Dialogs/ExportDialog.gd index 7f952fee7..c6f27be69 100644 --- a/src/UI/Dialogs/ExportDialog.gd +++ b/src/UI/Dialogs/ExportDialog.gd @@ -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) diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index 5173865a1..dea3d6c52 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -846,6 +846,8 @@ func add_layer(type := 0) -> void: Global.LayerTypes.THREE_D: l = Layer3D.new(project) SteamManager.set_achievement("ACH_3D_LAYER") + Global.LayerTypes.TILEMAP: + l = LayerTileMap.new(project, TileSetCustom.new(Vector2i(16, 16))) var cels := [] for f in project.frames: diff --git a/src/UI/Timeline/AnimationTimeline.tscn b/src/UI/Timeline/AnimationTimeline.tscn index efb04ba70..472f43816 100644 --- a/src/UI/Timeline/AnimationTimeline.tscn +++ b/src/UI/Timeline/AnimationTimeline.tscn @@ -239,12 +239,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 From b74c3149aff356399545b9aa806f4ae0fd8bcb57 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 18 Nov 2024 03:09:46 +0200 Subject: [PATCH 04/76] Add a tileset panel Code is a bit meh, needs to be written better. --- src/Classes/Cels/CelTileMap.gd | 8 ++++---- src/Classes/Project.gd | 2 ++ src/Classes/TileSetCustom.gd | 17 ++++++++++++++++ src/UI/TilesPanel.gd | 36 ++++++++++++++++++++++++++++++++++ src/UI/TilesPanel.tscn | 18 +++++++++++++++++ src/UI/UI.tscn | 9 +++++++-- 6 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 src/UI/TilesPanel.gd create mode 100644 src/UI/TilesPanel.tscn diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index cb4ba7d62..83e3737aa 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -4,7 +4,7 @@ extends PixelCel enum TileEditingMode { MANUAL, AUTO, STACK } var tileset: TileSetCustom -var tile_editing_mode := TileEditingMode.MANUAL +var tile_editing_mode := TileEditingMode.STACK var indices := PackedInt32Array() var indices_x: int var indices_y: int @@ -38,7 +38,7 @@ func update_texture() -> void: if not tile_used: tiles_to_delete.append(j) for j in tiles_to_delete: - tileset.tiles.remove_at(j) + tileset.remove_tile_at_index(j) for i in indices.size(): var x_coord := float(tileset.tile_size.x) * (i % indices_x) var y_coord := float(tileset.tile_size.y) * (i / indices_x) @@ -51,7 +51,7 @@ func update_texture() -> void: if index == 0 or tileset.tiles.size() <= index: continue if image_portion.get_data() != tileset.tiles[index].get_data(): - tileset.tiles[index].copy_from(image_portion) + tileset.replace_tile_at(image_portion, index) # TODO: Update the rest of the tilemap else: var found_tile := false @@ -62,7 +62,7 @@ func update_texture() -> void: found_tile = true break if not found_tile: - tileset.tiles.append(image_portion) + tileset.add_tile(image_portion) indices[i] = tileset.tiles.size() diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index bdea6a52b..12d8e2712 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -8,6 +8,7 @@ signal serialized(dict: Dictionary) signal about_to_deserialize(dict: Dictionary) signal resized signal timeline_updated +signal tilesets_updated const INDEXED_MODE := Image.FORMAT_MAX + 1 @@ -936,3 +937,4 @@ func reorder_reference_image(from: int, to: int) -> void: func add_tileset(tileset: TileSetCustom) -> void: tilesets.append(tileset) + tilesets_updated.emit() diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd index 2feafe1ed..368e44a4b 100644 --- a/src/Classes/TileSetCustom.gd +++ b/src/Classes/TileSetCustom.gd @@ -1,6 +1,8 @@ class_name TileSetCustom extends RefCounted +signal updated + var name := "" var tile_size: Vector2i var tiles: Array[Image] = [] @@ -14,3 +16,18 @@ func _init(_tile_size: Vector2i, _name := "") -> void: #tiles.resize(indices_x * indices_y + 1) var empty_image := Image.create_empty(tile_size.x, tile_size.y, false, Image.FORMAT_RGBA8) tiles.append(empty_image) + + +func add_tile(tile: Image) -> void: + tiles.append(tile) + updated.emit() + + +func remove_tile_at_index(index: int) -> void: + tiles.remove_at(index) + updated.emit() + + +func replace_tile_at(new_tile: Image, index: int) -> void: + tiles[index].copy_from(new_tile) + updated.emit() diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd new file mode 100644 index 000000000..f5c62b09e --- /dev/null +++ b/src/UI/TilesPanel.gd @@ -0,0 +1,36 @@ +extends ScrollContainer + +@onready var h_flow_container: HFlowContainer = $PanelContainer/HFlowContainer + + +func _ready() -> void: + Global.project_switched.connect(_on_project_switched) + Global.project_switched.connect(_update_tilesets) + Global.current_project.tilesets_updated.connect(_update_tilesets) + + +func _on_project_switched() -> void: + if not Global.current_project.tilesets_updated.is_connected(_update_tilesets): + Global.current_project.tilesets_updated.connect(_update_tilesets) + + +# TODO: Handle signal methods better and rename them to avoid confusion. +func _update_tilesets() -> void: + for child in h_flow_container.get_children(): + child.queue_free() + if Global.current_project.tilesets.size() == 0: + return + var tileset := Global.current_project.tilesets[0] + if not tileset.updated.is_connected(_update_tileset): + tileset.updated.connect(_update_tileset) + + +func _update_tileset() -> void: + for child in h_flow_container.get_children(): + child.queue_free() + var tileset := Global.current_project.tilesets[0] + for tile in tileset.tiles: + var texture_rect := TextureButton.new() + texture_rect.custom_minimum_size = Vector2i(32, 32) + texture_rect.texture_normal = ImageTexture.create_from_image(tile) + h_flow_container.add_child(texture_rect) diff --git a/src/UI/TilesPanel.tscn b/src/UI/TilesPanel.tscn new file mode 100644 index 000000000..f7840dcbd --- /dev/null +++ b/src/UI/TilesPanel.tscn @@ -0,0 +1,18 @@ +[gd_scene load_steps=2 format=3 uid="uid://bfbragmmdwfbl"] + +[ext_resource type="Script" path="res://src/UI/TilesPanel.gd" id="1_d2oc5"] + +[node name="Tiles" type="ScrollContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_d2oc5") + +[node name="PanelContainer" type="PanelContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="HFlowContainer" type="HFlowContainer" parent="PanelContainer"] +layout_mode = 2 diff --git a/src/UI/UI.tscn b/src/UI/UI.tscn index 1700b0868..b42265da8 100644 --- a/src/UI/UI.tscn +++ b/src/UI/UI.tscn @@ -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 From 8abf44d65a2ff2eacd41ff0b9f613d933e9e69e5 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:53:21 +0200 Subject: [PATCH 05/76] Support ImageExtended --- src/Autoload/Tools.gd | 2 +- src/Classes/Cels/CelTileMap.gd | 2 +- src/Classes/Layers/LayerTileMap.gd | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Autoload/Tools.gd b/src/Autoload/Tools.gd index 17d062e50..8b5cb0837 100644 --- a/src/Autoload/Tools.gd +++ b/src/Autoload/Tools.gd @@ -217,7 +217,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": diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 83e3737aa..375e962bf 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -10,7 +10,7 @@ var indices_x: int var indices_y: int -func _init(_tileset: TileSetCustom, _image := Image.new(), _opacity := 1.0) -> void: +func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> void: super._init(_image, _opacity) tileset = _tileset indices_x = ceili(float(get_image().get_width()) / tileset.tile_size.x) diff --git a/src/Classes/Layers/LayerTileMap.gd b/src/Classes/Layers/LayerTileMap.gd index f48ed651e..c6a2296cc 100644 --- a/src/Classes/Layers/LayerTileMap.gd +++ b/src/Classes/Layers/LayerTileMap.gd @@ -17,5 +17,9 @@ func get_layer_type() -> int: func new_empty_cel() -> BaseCel: - var image := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) + var format := project.get_image_format() + var is_indexed := project.is_indexed() + var image := ImageExtended.create_custom( + project.size.x, project.size.y, false, format, is_indexed + ) return CelTileMap.new(tileset, image) From df56989d5f9a05f864eeb6c4a1405f6bc6a16ab0 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Thu, 21 Nov 2024 00:29:31 +0200 Subject: [PATCH 06/76] Improve tileset editing logic --- src/Classes/Cels/CelTileMap.gd | 23 +++++++++++++---------- src/Classes/TileSetCustom.gd | 5 +++++ src/Tools/BaseTool.gd | 7 ++++++- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 375e962bf..f4a645c24 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -4,7 +4,7 @@ extends PixelCel enum TileEditingMode { MANUAL, AUTO, STACK } var tileset: TileSetCustom -var tile_editing_mode := TileEditingMode.STACK +var tile_editing_mode := TileEditingMode.AUTO var indices := PackedInt32Array() var indices_x: int var indices_y: int @@ -18,11 +18,10 @@ func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> v indices.resize(indices_x * indices_y) -func update_texture() -> void: - super.update_texture() +func update_tileset() -> void: + var removed_tile_indices: Array[int] = [] if tile_editing_mode == TileEditingMode.AUTO: - var tiles_to_delete := PackedInt32Array() - for j in range(1, tileset.tiles.size()): + for j in range(tileset.tiles.size() - 1, 0, -1): var tile := tileset.tiles[j] var tile_used := false for i in indices.size(): @@ -36,9 +35,8 @@ func update_texture() -> void: tile_used = true break if not tile_used: - tiles_to_delete.append(j) - for j in tiles_to_delete: - tileset.remove_tile_at_index(j) + removed_tile_indices.append(j) + tileset.remove_tile_at_index(j) for i in indices.size(): var x_coord := float(tileset.tile_size.x) * (i % indices_x) var y_coord := float(tileset.tile_size.y) * (i / indices_x) @@ -62,8 +60,13 @@ func update_texture() -> void: found_tile = true break if not found_tile: - tileset.add_tile(image_portion) - indices[i] = tileset.tiles.size() + if removed_tile_indices.is_empty(): + tileset.add_tile(image_portion) + indices[i] = tileset.tiles.size() + else: + var index_position := removed_tile_indices.pop_back() as int + tileset.insert_tile(image_portion, index_position) + indices[i] = index_position func get_class_name() -> String: diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd index 368e44a4b..f420f0548 100644 --- a/src/Classes/TileSetCustom.gd +++ b/src/Classes/TileSetCustom.gd @@ -23,6 +23,11 @@ func add_tile(tile: Image) -> void: updated.emit() +func insert_tile(tile: Image, position: int) -> void: + tiles.insert(position, tile) + updated.emit() + + func remove_tile_at_index(index: int) -> void: tiles.remove_at(index) updated.emit() diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index 73baaac0c..a1994da8b 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -75,7 +75,12 @@ 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 + for cel_index in project.selected_cels: + var cel := project.frames[cel_index[0]].cels[cel_index[1]] + if cel is CelTileMap: + cel.update_tileset() + project.can_undo = true func cursor_move(pos: Vector2i) -> void: From 7893ae2531853e2884b2acfd3a28456e86114fb7 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:19:06 +0200 Subject: [PATCH 07/76] Make manual mode work when the tileset is empty --- src/Classes/Cels/CelTileMap.gd | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index f4a645c24..0812b77d9 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -47,6 +47,9 @@ func update_tileset() -> void: var index := indices[i] if tile_editing_mode == TileEditingMode.MANUAL: if index == 0 or tileset.tiles.size() <= index: + if tileset.tiles.size() <= 1: + tileset.add_tile(image_portion) + indices[i] = tileset.tiles.size() - 1 continue if image_portion.get_data() != tileset.tiles[index].get_data(): tileset.replace_tile_at(image_portion, index) @@ -62,7 +65,7 @@ func update_tileset() -> void: if not found_tile: if removed_tile_indices.is_empty(): tileset.add_tile(image_portion) - indices[i] = tileset.tiles.size() + indices[i] = tileset.tiles.size() - 1 else: var index_position := removed_tile_indices.pop_back() as int tileset.insert_tile(image_portion, index_position) From cadd7c57f1c571f2bd5051aeb979203756afe79e Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 22 Nov 2024 01:30:19 +0200 Subject: [PATCH 08/76] Change tile editing mode from the UI --- src/Classes/Cels/CelTileMap.gd | 7 ++---- src/UI/TilesPanel.gd | 25 ++++++++++++++++++-- src/UI/TilesPanel.tscn | 43 ++++++++++++++++++++++++++++++---- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 0812b77d9..0e4207f1b 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -1,10 +1,7 @@ class_name CelTileMap extends PixelCel -enum TileEditingMode { MANUAL, AUTO, STACK } - var tileset: TileSetCustom -var tile_editing_mode := TileEditingMode.AUTO var indices := PackedInt32Array() var indices_x: int var indices_y: int @@ -20,7 +17,7 @@ func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> v func update_tileset() -> void: var removed_tile_indices: Array[int] = [] - if tile_editing_mode == TileEditingMode.AUTO: + if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.AUTO: for j in range(tileset.tiles.size() - 1, 0, -1): var tile := tileset.tiles[j] var tile_used := false @@ -45,7 +42,7 @@ func update_tileset() -> void: if image_portion.is_invisible(): continue var index := indices[i] - if tile_editing_mode == TileEditingMode.MANUAL: + if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: if index == 0 or tileset.tiles.size() <= index: if tileset.tiles.size() <= 1: tileset.add_tile(image_portion) diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index f5c62b09e..7e4d3fe1c 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -1,6 +1,12 @@ -extends ScrollContainer +class_name TileSetPanel +extends PanelContainer -@onready var h_flow_container: HFlowContainer = $PanelContainer/HFlowContainer +enum TileEditingMode { MANUAL, AUTO, STACK } + +var current_tileset: TileSetCustom +static var tile_editing_mode := TileEditingMode.AUTO + +@onready var h_flow_container: HFlowContainer = $VBoxContainer/ScrollContainer/HFlowContainer func _ready() -> void: @@ -34,3 +40,18 @@ func _update_tileset() -> void: texture_rect.custom_minimum_size = Vector2i(32, 32) texture_rect.texture_normal = ImageTexture.create_from_image(tile) h_flow_container.add_child(texture_rect) + + +func _on_manual_toggled(toggled_on: bool) -> void: + if toggled_on: + tile_editing_mode = TileEditingMode.MANUAL + + +func _on_auto_toggled(toggled_on: bool) -> void: + if toggled_on: + tile_editing_mode = TileEditingMode.AUTO + + +func _on_stack_toggled(toggled_on: bool) -> void: + if toggled_on: + tile_editing_mode = TileEditingMode.STACK diff --git a/src/UI/TilesPanel.tscn b/src/UI/TilesPanel.tscn index f7840dcbd..e764d491d 100644 --- a/src/UI/TilesPanel.tscn +++ b/src/UI/TilesPanel.tscn @@ -1,8 +1,10 @@ -[gd_scene load_steps=2 format=3 uid="uid://bfbragmmdwfbl"] +[gd_scene load_steps=3 format=3 uid="uid://bfbragmmdwfbl"] [ext_resource type="Script" path="res://src/UI/TilesPanel.gd" id="1_d2oc5"] -[node name="Tiles" type="ScrollContainer"] +[sub_resource type="ButtonGroup" id="ButtonGroup_uxnt0"] + +[node name="Tiles" type="PanelContainer"] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -10,9 +12,40 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("1_d2oc5") -[node name="PanelContainer" type="PanelContainer" parent="."] +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="Manual" type="CheckBox" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +mouse_default_cursor_shape = 2 +button_group = SubResource("ButtonGroup_uxnt0") +text = "Manual" + +[node name="Auto" type="CheckBox" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +mouse_default_cursor_shape = 2 +button_pressed = true +button_group = SubResource("ButtonGroup_uxnt0") +text = "Auto" + +[node name="Stack" type="CheckBox" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +mouse_default_cursor_shape = 2 +button_group = SubResource("ButtonGroup_uxnt0") +text = "Stack" + +[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="HFlowContainer" type="HFlowContainer" parent="VBoxContainer/ScrollContainer"] layout_mode = 2 size_flags_horizontal = 3 +size_flags_vertical = 3 -[node name="HFlowContainer" type="HFlowContainer" parent="PanelContainer"] -layout_mode = 2 +[connection signal="toggled" from="VBoxContainer/HBoxContainer/Manual" to="." method="_on_manual_toggled"] +[connection signal="toggled" from="VBoxContainer/HBoxContainer/Auto" to="." method="_on_auto_toggled"] +[connection signal="toggled" from="VBoxContainer/HBoxContainer/Stack" to="." method="_on_stack_toggled"] From a4b33ad83c97949d8b35fd2a68d39dcfa5467683 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 22 Nov 2024 01:43:07 +0200 Subject: [PATCH 09/76] Don't delete tiles that have been added using the stack mode --- src/Classes/Cels/CelTileMap.gd | 19 ++++++++++++------- src/Classes/TileSetCustom.gd | 21 ++++++++++++++++----- src/UI/TilesPanel.gd | 2 +- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 0e4207f1b..976c8d401 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -20,6 +20,9 @@ func update_tileset() -> void: if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.AUTO: for j in range(tileset.tiles.size() - 1, 0, -1): var tile := tileset.tiles[j] + if tile.mode_added == TileSetPanel.TileEditingMode.STACK: + # Don't delete tiles that have been added using the stack mode. + continue var tile_used := false for i in indices.size(): var x_coord := float(tileset.tile_size.x) * (i % indices_x) @@ -28,7 +31,7 @@ func update_tileset() -> void: var image_portion := image.get_region(rect) if image_portion.is_invisible(): continue - if image_portion.get_data() == tile.get_data(): + if image_portion.get_data() == tile.image.get_data(): tile_used = true break if not tile_used: @@ -45,27 +48,29 @@ func update_tileset() -> void: if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: if index == 0 or tileset.tiles.size() <= index: if tileset.tiles.size() <= 1: - tileset.add_tile(image_portion) + tileset.add_tile(image_portion, TileSetPanel.tile_editing_mode) indices[i] = tileset.tiles.size() - 1 continue - if image_portion.get_data() != tileset.tiles[index].get_data(): + if image_portion.get_data() != tileset.tiles[index].image.get_data(): tileset.replace_tile_at(image_portion, index) # TODO: Update the rest of the tilemap - else: + else: # Auto or stack var found_tile := false for j in range(1, tileset.tiles.size()): var tile := tileset.tiles[j] - if image_portion.get_data() == tile.get_data(): + if image_portion.get_data() == tile.image.get_data(): indices[i] = j found_tile = true break if not found_tile: if removed_tile_indices.is_empty(): - tileset.add_tile(image_portion) + tileset.add_tile(image_portion, TileSetPanel.tile_editing_mode) indices[i] = tileset.tiles.size() - 1 else: var index_position := removed_tile_indices.pop_back() as int - tileset.insert_tile(image_portion, index_position) + tileset.insert_tile( + image_portion, index_position, TileSetPanel.tile_editing_mode + ) indices[i] = index_position diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd index f420f0548..bbeb2e539 100644 --- a/src/Classes/TileSetCustom.gd +++ b/src/Classes/TileSetCustom.gd @@ -5,7 +5,16 @@ signal updated var name := "" var tile_size: Vector2i -var tiles: Array[Image] = [] +var tiles: Array[Tile] = [] + + +class Tile: + var image: Image + var mode_added: TileSetPanel.TileEditingMode + + func _init(_image: Image, _mode_added: TileSetPanel.TileEditingMode) -> void: + image = _image + mode_added = _mode_added func _init(_tile_size: Vector2i, _name := "") -> void: @@ -15,15 +24,17 @@ func _init(_tile_size: Vector2i, _name := "") -> void: #var indices_y := ceili(float(_project_size.y) / tile_size.y) #tiles.resize(indices_x * indices_y + 1) var empty_image := Image.create_empty(tile_size.x, tile_size.y, false, Image.FORMAT_RGBA8) - tiles.append(empty_image) + tiles.append(Tile.new(empty_image, TileSetPanel.tile_editing_mode)) -func add_tile(tile: Image) -> void: +func add_tile(image: Image, edit_mode: TileSetPanel.TileEditingMode) -> void: + var tile := Tile.new(image, edit_mode) tiles.append(tile) updated.emit() -func insert_tile(tile: Image, position: int) -> void: +func insert_tile(image: Image, position: int, edit_mode: TileSetPanel.TileEditingMode) -> void: + var tile := Tile.new(image, edit_mode) tiles.insert(position, tile) updated.emit() @@ -34,5 +45,5 @@ func remove_tile_at_index(index: int) -> void: func replace_tile_at(new_tile: Image, index: int) -> void: - tiles[index].copy_from(new_tile) + tiles[index].image.copy_from(new_tile) updated.emit() diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index 7e4d3fe1c..8c672da38 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -38,7 +38,7 @@ func _update_tileset() -> void: for tile in tileset.tiles: var texture_rect := TextureButton.new() texture_rect.custom_minimum_size = Vector2i(32, 32) - texture_rect.texture_normal = ImageTexture.create_from_image(tile) + texture_rect.texture_normal = ImageTexture.create_from_image(tile.image) h_flow_container.add_child(texture_rect) From 488c0238381df50ccfd6a63dc8d83786fe18a4bb Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 22 Nov 2024 02:08:17 +0200 Subject: [PATCH 10/76] Add a way to show the indices of each tile, WIP --- src/UI/Canvas/Canvas.tscn | 4 ++++ src/UI/Canvas/TileModeIndices.gd | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/UI/Canvas/TileModeIndices.gd diff --git a/src/UI/Canvas/Canvas.tscn b/src/UI/Canvas/Canvas.tscn index 339f87727..2a3adbef4 100644 --- a/src/UI/Canvas/Canvas.tscn +++ b/src/UI/Canvas/Canvas.tscn @@ -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,6 @@ script = ExtResource("16_nxilb") [node name="ReferenceImages" type="Node2D" parent="."] script = ExtResource("17_qfjb4") + +[node name="TileModeIndices" type="Node2D" parent="."] +script = ExtResource("19_7a6wb") diff --git a/src/UI/Canvas/TileModeIndices.gd b/src/UI/Canvas/TileModeIndices.gd new file mode 100644 index 000000000..980d9c6a5 --- /dev/null +++ b/src/UI/Canvas/TileModeIndices.gd @@ -0,0 +1,16 @@ +extends Node2D + + +func _input(event: InputEvent) -> void: + queue_redraw() + + +func _draw() -> void: + var current_cel := Global.current_project.get_current_cel() + if current_cel is CelTileMap: + var tilemap_cel := current_cel as CelTileMap + for i in tilemap_cel.indices.size(): + var x := float(tilemap_cel.tileset.tile_size.x) * (i % tilemap_cel.indices_x) + var y := float(tilemap_cel.tileset.tile_size.y) * (i / tilemap_cel.indices_x) + var pos := Vector2i(x, y + tilemap_cel.tileset.tile_size.y) + draw_string(Themes.get_font(), pos, str(tilemap_cel.indices[i]), 0, -1, 12) From d584807c14273603cd4ef0dacbb0cb95646ab976 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 23 Nov 2024 01:51:26 +0200 Subject: [PATCH 11/76] Show tile indices when pressing Control --- src/UI/Canvas/TileModeIndices.gd | 5 +++-- src/UI/TilesPanel.gd | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/UI/Canvas/TileModeIndices.gd b/src/UI/Canvas/TileModeIndices.gd index 980d9c6a5..a577b3bfd 100644 --- a/src/UI/Canvas/TileModeIndices.gd +++ b/src/UI/Canvas/TileModeIndices.gd @@ -2,12 +2,13 @@ extends Node2D func _input(event: InputEvent) -> void: - queue_redraw() + if event.is_action("ctrl"): + queue_redraw() func _draw() -> void: var current_cel := Global.current_project.get_current_cel() - if current_cel is CelTileMap: + if current_cel is CelTileMap and Input.is_action_pressed("ctrl"): var tilemap_cel := current_cel as CelTileMap for i in tilemap_cel.indices.size(): var x := float(tilemap_cel.tileset.tile_size.x) * (i % tilemap_cel.indices_x) diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index 8c672da38..8328993ea 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -3,8 +3,8 @@ extends PanelContainer enum TileEditingMode { MANUAL, AUTO, STACK } -var current_tileset: TileSetCustom static var tile_editing_mode := TileEditingMode.AUTO +var current_tileset: TileSetCustom @onready var h_flow_container: HFlowContainer = $VBoxContainer/ScrollContainer/HFlowContainer From 29262ff7daadbf728d102db598a5568d026cb62a Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 24 Nov 2024 02:19:04 +0200 Subject: [PATCH 12/76] Properly implement the auto tile editing mode Should work well now. --- src/Classes/Cels/CelTileMap.gd | 162 ++++++++++++++++++++++++++------- src/Classes/TileSetCustom.gd | 31 +++++++ 2 files changed, 160 insertions(+), 33 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 976c8d401..da2c4010f 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -16,35 +16,13 @@ func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> v func update_tileset() -> void: - var removed_tile_indices: Array[int] = [] - if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.AUTO: - for j in range(tileset.tiles.size() - 1, 0, -1): - var tile := tileset.tiles[j] - if tile.mode_added == TileSetPanel.TileEditingMode.STACK: - # Don't delete tiles that have been added using the stack mode. - continue - var tile_used := false - for i in indices.size(): - var x_coord := float(tileset.tile_size.x) * (i % indices_x) - var y_coord := float(tileset.tile_size.y) * (i / indices_x) - var rect := Rect2i(Vector2i(x_coord, y_coord), tileset.tile_size) - var image_portion := image.get_region(rect) - if image_portion.is_invisible(): - continue - if image_portion.get_data() == tile.image.get_data(): - tile_used = true - break - if not tile_used: - removed_tile_indices.append(j) - tileset.remove_tile_at_index(j) for i in indices.size(): var x_coord := float(tileset.tile_size.x) * (i % indices_x) var y_coord := float(tileset.tile_size.y) * (i / indices_x) var rect := Rect2i(Vector2i(x_coord, y_coord), tileset.tile_size) var image_portion := image.get_region(rect) - if image_portion.is_invisible(): - continue var index := indices[i] + var current_tile := tileset.tiles[index] if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: if index == 0 or tileset.tiles.size() <= index: if tileset.tiles.size() <= 1: @@ -54,7 +32,9 @@ func update_tileset() -> void: if image_portion.get_data() != tileset.tiles[index].image.get_data(): tileset.replace_tile_at(image_portion, index) # TODO: Update the rest of the tilemap - else: # Auto or stack + elif TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.AUTO: + handle_auto_editing_mode(i, image_portion) + else: # Stack var found_tile := false for j in range(1, tileset.tiles.size()): var tile := tileset.tiles[j] @@ -63,15 +43,131 @@ func update_tileset() -> void: found_tile = true break if not found_tile: - if removed_tile_indices.is_empty(): - tileset.add_tile(image_portion, TileSetPanel.tile_editing_mode) - indices[i] = tileset.tiles.size() - 1 - else: - var index_position := removed_tile_indices.pop_back() as int - tileset.insert_tile( - image_portion, index_position, TileSetPanel.tile_editing_mode - ) - indices[i] = index_position + tileset.add_tile(image_portion, TileSetPanel.tile_editing_mode) + indices[i] = tileset.tiles.size() - 1 + + +## Cases:[br] +## 0) Portion is transparent. Set its index to 0. +## [br] +## 0.5) Portion is transparent and mapped. +## Set its index to 0 and unuse the mapped tile. +## If the mapped tile is removed, educe the index of all portions that have indices greater or equal +## than the existing tile's index. +## [br] +## 1) Portion not mapped, exists in the tileset. +## Map the portion to the existing tile and increase its times_used by one. +## [br] +## 2) Portion not mapped, does not exist in the tileset. +## Add the portion as a tile in the tileset, set its index to be the tileset's tile size - 1. +## [br] +## 3) Portion mapped, tile did not change. Do nothing. +## [br] +## 4) Portion mapped, exists in the tileset. +## The mapped tile still exists in the tileset. +## Map the portion to the existing tile, increase its times_used by one, +## and reduce the previously mapped tile's times_used by 1. +## [br] +## 5) Portion mapped, exists in the tileset. +## The mapped tile does not exist in the tileset anymore. +## Map the portion to the existing tile and increase its times_used by one. +## Remove the previously mapped tile, +## and reduce the index of all portions that have indices greater or equal +## than the existing tile's index. +## [br] +## 6) Portion mapped, does not exist in the tileset. +## The mapped tile still exists in the tileset. +## Add the portion 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) Portion 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) -> void: + var index := indices[i] + var current_tile := tileset.tiles[index] + if image_portion.is_invisible(): + # Case 0: The portion is transparent. + indices[i] = 0 + if index > 0: + # Case 0.5: The portion is transparent and mapped to a tile. + var is_removed := tileset.unuse_tile_at_index(index) + if is_removed: + # Re-index all indices that are after the deleted one. + re_index_tiles_after_index(index) + return + var index_in_tileset := tileset.find_tile(image_portion) + if index == 0: # If the portion is not mapped to a tile. + if index_in_tileset > -1: + # Case 1: The portion is not mapped already, + # and it exists in the tileset as a tile. + tileset.tiles[index_in_tileset].times_used += 1 + indices[i] = index_in_tileset + else: + # Case 2: The portion is not mapped already, + # and it does not exist in the tileset. + tileset.add_tile(image_portion, TileSetPanel.tile_editing_mode) + indices[i] = tileset.tiles.size() - 1 + else: # If the portion is already mapped. + if image_portion.get_data() == current_tile.image.get_data(): + # Case 3: The portion is mapped and it did not change. + # Do nothing and move on to the next portion. + return + var previous_tile_index_in_tileset := tileset.find_tile(current_tile.image) + if index_in_tileset > -1: # If the portion exists in the tileset as a tile. + if current_tile.times_used > 1: + # Case 4: The portion 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 + indices[i] = index_in_tileset + tileset.unuse_tile_at_index(index) + else: + # Case 5: The portion 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 + indices[i] = index_in_tileset + tileset.remove_tile_at_index(index) + # Re-index all indices that are after the deleted one. + re_index_tiles_after_index(index) + else: # If the portion does not exist in the tileset as a tile. + if current_tile.times_used > 1: + # Case 6: The portion 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) + tileset.add_tile(image_portion, TileSetPanel.tile_editing_mode) + indices[i] = tileset.tiles.size() - 1 + else: + # Case 7: The portion 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) + + +## Re-indexes all [member indices] that are larger or equal to [param index], +## by reducing their value by one. +func re_index_tiles_after_index(index: int) -> void: + for i in indices.size(): + var tmp_index := indices[i] + if tmp_index >= index: + indices[i] -= 1 + + +## Unused, should delete. +func re_index_tiles() -> void: + for i in indices.size(): + var x_coord := float(tileset.tile_size.x) * (i % indices_x) + var y_coord := float(tileset.tile_size.y) * (i / indices_x) + var rect := Rect2i(Vector2i(x_coord, y_coord), tileset.tile_size) + var image_portion := image.get_region(rect) + if image_portion.is_invisible(): + indices[i] = 0 + continue + for j in range(1, tileset.tiles.size()): + var tile := tileset.tiles[j] + if image_portion.get_data() == tile.image.get_data(): + indices[i] = j + break func get_class_name() -> String: diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd index bbeb2e539..8d5867a30 100644 --- a/src/Classes/TileSetCustom.gd +++ b/src/Classes/TileSetCustom.gd @@ -11,11 +11,15 @@ var tiles: Array[Tile] = [] class Tile: var image: Image var mode_added: TileSetPanel.TileEditingMode + var times_used := 1 func _init(_image: Image, _mode_added: TileSetPanel.TileEditingMode) -> void: image = _image mode_added = _mode_added + func can_be_removed() -> bool: + return mode_added != TileSetPanel.TileEditingMode.STACK and times_used <= 0 + func _init(_tile_size: Vector2i, _name := "") -> void: tile_size = _tile_size @@ -39,6 +43,14 @@ func insert_tile(image: Image, position: int, edit_mode: TileSetPanel.TileEditin updated.emit() +func unuse_tile_at_index(index: int) -> bool: + tiles[index].times_used -= 1 + if tiles[index].can_be_removed(): + remove_tile_at_index(index) + return true + return false + + func remove_tile_at_index(index: int) -> void: tiles.remove_at(index) updated.emit() @@ -47,3 +59,22 @@ func remove_tile_at_index(index: int) -> void: func replace_tile_at(new_tile: Image, index: int) -> void: tiles[index].image.copy_from(new_tile) updated.emit() + + +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 + + +## Unused, should delete. +func remove_unused_tiles() -> bool: + var tile_removed := false + for i in range(tiles.size() - 1, 0, -1): + var tile := tiles[i] + if tile.can_be_removed(): + tile_removed = true + remove_tile_at_index(i) + return tile_removed From 2ecdf023b2162f91b635a630759d8fa758403231 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 24 Nov 2024 16:04:13 +0200 Subject: [PATCH 13/76] Make the manual mode work, kind of --- src/Classes/Cels/CelTileMap.gd | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index da2c4010f..29b31df5d 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -17,24 +17,28 @@ func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> v func update_tileset() -> void: for i in indices.size(): - var x_coord := float(tileset.tile_size.x) * (i % indices_x) - var y_coord := float(tileset.tile_size.y) * (i / indices_x) - var rect := Rect2i(Vector2i(x_coord, y_coord), tileset.tile_size) + var coords := get_tile_coords(i) + var rect := Rect2i(coords, tileset.tile_size) var image_portion := image.get_region(rect) var index := indices[i] var current_tile := tileset.tiles[index] if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: + if image_portion.is_invisible(): + continue if index == 0 or tileset.tiles.size() <= index: + # If the tileset is empty, only then add a new tile. if tileset.tiles.size() <= 1: tileset.add_tile(image_portion, TileSetPanel.tile_editing_mode) indices[i] = tileset.tiles.size() - 1 continue - if image_portion.get_data() != tileset.tiles[index].image.get_data(): + if image_portion.get_data() != current_tile.image.get_data(): tileset.replace_tile_at(image_portion, index) - # TODO: Update the rest of the tilemap + update_cel_portions() elif TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.AUTO: handle_auto_editing_mode(i, image_portion) 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] @@ -113,7 +117,6 @@ func handle_auto_editing_mode(i: int, image_portion: Image) -> void: # Case 3: The portion is mapped and it did not change. # Do nothing and move on to the next portion. return - var previous_tile_index_in_tileset := tileset.find_tile(current_tile.image) if index_in_tileset > -1: # If the portion exists in the tileset as a tile. if current_tile.times_used > 1: # Case 4: The portion is mapped and it exists in the tileset as a tile, @@ -153,6 +156,24 @@ func re_index_tiles_after_index(index: int) -> void: indices[i] -= 1 +func update_cel_portions() -> void: + for i in indices.size(): + var coords := get_tile_coords(i) + var rect := Rect2i(coords, tileset.tile_size) + var image_portion := image.get_region(rect) + var index := indices[i] + var current_tile := tileset.tiles[index] + if image_portion.get_data() != current_tile.image.get_data(): + var tile_size := current_tile.image.get_size() + image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords) + + +func get_tile_coords(portion_index: int) -> Vector2i: + var x_coord := float(tileset.tile_size.x) * (portion_index % indices_x) + var y_coord := float(tileset.tile_size.y) * (portion_index / indices_x) + return Vector2i(x_coord, y_coord) + + ## Unused, should delete. func re_index_tiles() -> void: for i in indices.size(): From 1bb908638bd226f22c4b77fe312bb8670108fd57 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 24 Nov 2024 16:28:21 +0200 Subject: [PATCH 14/76] Prevent from drawing on empty image portions on manual mode. --- src/Classes/Cels/CelTileMap.gd | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 29b31df5d..75190a543 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -15,6 +15,20 @@ func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> v indices.resize(indices_x * indices_y) +func update_texture() -> void: + super.update_texture() + if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: + for i in indices.size(): + var index := indices[i] + # Prevent from drawing on empty image portions. + if index == 0 and tileset.tiles.size() > 1: + var coords := get_tile_coords(i) + var rect := Rect2i(coords, tileset.tile_size) + var current_tile := tileset.tiles[index] + var tile_size := current_tile.image.get_size() + image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords) + + func update_tileset() -> void: for i in indices.size(): var coords := get_tile_coords(i) @@ -25,7 +39,7 @@ func update_tileset() -> void: if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: if image_portion.is_invisible(): continue - if index == 0 or tileset.tiles.size() <= index: + if index == 0: # If the tileset is empty, only then add a new tile. if tileset.tiles.size() <= 1: tileset.add_tile(image_portion, TileSetPanel.tile_editing_mode) From 7f82be13ab7bea9ff0c5f1da4edcfd0d4a60b9ce Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 24 Nov 2024 17:27:35 +0200 Subject: [PATCH 15/76] Improve tileset panel UI updating logic --- src/Classes/Cels/CelTileMap.gd | 2 +- src/Classes/Project.gd | 2 -- src/UI/TilesPanel.gd | 42 ++++++++++++++++++---------------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 75190a543..4424daa1c 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -16,7 +16,6 @@ func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> v func update_texture() -> void: - super.update_texture() if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: for i in indices.size(): var index := indices[i] @@ -27,6 +26,7 @@ func update_texture() -> void: var current_tile := tileset.tiles[index] var tile_size := current_tile.image.get_size() image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords) + super.update_texture() func update_tileset() -> void: diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index 12d8e2712..bdea6a52b 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -8,7 +8,6 @@ signal serialized(dict: Dictionary) signal about_to_deserialize(dict: Dictionary) signal resized signal timeline_updated -signal tilesets_updated const INDEXED_MODE := Image.FORMAT_MAX + 1 @@ -937,4 +936,3 @@ func reorder_reference_image(from: int, to: int) -> void: func add_tileset(tileset: TileSetCustom) -> void: tilesets.append(tileset) - tilesets_updated.emit() diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index 8328993ea..9f158553f 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -10,31 +10,28 @@ var current_tileset: TileSetCustom func _ready() -> void: - Global.project_switched.connect(_on_project_switched) - Global.project_switched.connect(_update_tilesets) - Global.current_project.tilesets_updated.connect(_update_tilesets) + Global.cel_switched.connect(_on_cel_switched) + Global.project_switched.connect(_on_cel_switched) -func _on_project_switched() -> void: - if not Global.current_project.tilesets_updated.is_connected(_update_tilesets): - Global.current_project.tilesets_updated.connect(_update_tilesets) - - -# TODO: Handle signal methods better and rename them to avoid confusion. -func _update_tilesets() -> void: - for child in h_flow_container.get_children(): - child.queue_free() - if Global.current_project.tilesets.size() == 0: +func _on_cel_switched() -> void: + if Global.current_project.get_current_cel() is not CelTileMap: + _clear_tile_buttons() return - var tileset := Global.current_project.tilesets[0] - if not tileset.updated.is_connected(_update_tileset): - tileset.updated.connect(_update_tileset) + var cel := Global.current_project.get_current_cel() as CelTileMap + if not cel.tileset.updated.is_connected(_update_tileset): + cel.tileset.updated.connect(_update_tileset.bind(cel)) + _update_tileset(cel) -func _update_tileset() -> void: - for child in h_flow_container.get_children(): - child.queue_free() - var tileset := Global.current_project.tilesets[0] +func _update_tileset(cel: BaseCel) -> void: + _clear_tile_buttons() + if cel is not CelTileMap: + return + var tilemap_cel := cel as CelTileMap + if tilemap_cel != Global.current_project.get_current_cel(): + tilemap_cel.tileset.updated.disconnect(_update_tileset) + var tileset := tilemap_cel.tileset for tile in tileset.tiles: var texture_rect := TextureButton.new() texture_rect.custom_minimum_size = Vector2i(32, 32) @@ -42,6 +39,11 @@ func _update_tileset() -> void: h_flow_container.add_child(texture_rect) +func _clear_tile_buttons() -> void: + for child in h_flow_container.get_children(): + child.queue_free() + + func _on_manual_toggled(toggled_on: bool) -> void: if toggled_on: tile_editing_mode = TileEditingMode.MANUAL From 00cd47b94eaa6bbc9fbd2bfec1aa72107f90f27c Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 24 Nov 2024 18:15:13 +0200 Subject: [PATCH 16/76] Better tile buttons --- src/UI/TilesPanel.gd | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index 9f158553f..7329965b4 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -3,6 +3,8 @@ extends PanelContainer enum TileEditingMode { MANUAL, AUTO, STACK } +const TRANSPARENT_CHECKER := preload("res://src/UI/Nodes/TransparentChecker.tscn") + static var tile_editing_mode := TileEditingMode.AUTO var current_tileset: TileSetCustom @@ -32,11 +34,38 @@ func _update_tileset(cel: BaseCel) -> void: if tilemap_cel != Global.current_project.get_current_cel(): tilemap_cel.tileset.updated.disconnect(_update_tileset) var tileset := tilemap_cel.tileset - for tile in tileset.tiles: - var texture_rect := TextureButton.new() - texture_rect.custom_minimum_size = Vector2i(32, 32) - texture_rect.texture_normal = ImageTexture.create_from_image(tile.image) - h_flow_container.add_child(texture_rect) + for i in tileset.tiles.size(): + var tile = tileset.tiles[i] + var button := _create_tile_button(ImageTexture.create_from_image(tile.image), i) + h_flow_container.add_child(button) + + +func _create_tile_button(texture: Texture2D, index: int) -> Button: + var button := Button.new() + button.custom_minimum_size = Vector2i(36, 36) + 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() + 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.pressed.connect(_on_tile_button_pressed.bind(index)) + return button + + +func _on_tile_button_pressed(index: int) -> void: + print(index) func _clear_tile_buttons() -> void: From 3f26e859dc77256d568197bd5433de496a65184e Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 24 Nov 2024 19:25:58 +0200 Subject: [PATCH 17/76] Properly update the tileset when using any tool --- src/Classes/Cels/BaseCel.gd | 4 ++++ src/Classes/Cels/CelTileMap.gd | 4 ++++ src/Tools/BaseShapeDrawer.gd | 3 ++- src/Tools/BaseTool.gd | 11 +++++++---- src/Tools/DesignTools/LineTool.gd | 3 ++- src/Tools/UtilityTools/Move.gd | 3 ++- src/Tools/UtilityTools/Text.gd | 5 +++-- src/UI/Canvas/Selection.gd | 2 ++ 8 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/Classes/Cels/BaseCel.gd b/src/Classes/Cels/BaseCel.gd index 8dedefba4..313544956 100644 --- a/src/Classes/Cels/BaseCel.gd +++ b/src/Classes/Cels/BaseCel.gd @@ -77,6 +77,10 @@ func update_texture() -> void: cel.texture_changed.emit() +func tool_finished_drawing() -> void: + pass + + ## Returns a curated [Dictionary] containing the cel data. func serialize() -> Dictionary: var dict := {"opacity": opacity, "z_index": z_index} diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 4424daa1c..9f1a5cb1b 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -29,6 +29,10 @@ func update_texture() -> void: super.update_texture() +func tool_finished_drawing() -> void: + update_tileset() + + func update_tileset() -> void: for i in indices.size(): var coords := get_tile_coords(i) diff --git a/src/Tools/BaseShapeDrawer.gd b/src/Tools/BaseShapeDrawer.gd index 4af1a156f..8789fccd2 100644 --- a/src/Tools/BaseShapeDrawer.gd +++ b/src/Tools/BaseShapeDrawer.gd @@ -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: diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index a1994da8b..e7bc269a1 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -76,13 +76,16 @@ func draw_end(_pos: Vector2i) -> void: is_moving = false _draw_cache = [] var project := Global.current_project - for cel_index in project.selected_cels: - var cel := project.frames[cel_index[0]].cels[cel_index[1]] - if cel is CelTileMap: - cel.update_tileset() + update_cels(project) project.can_undo = true +func update_cels(project := Global.current_project) -> void: + for cel_index in project.selected_cels: + var cel := project.frames[cel_index[0]].cels[cel_index[1]] as BaseCel + cel.tool_finished_drawing() + + func cursor_move(pos: Vector2i) -> void: _cursor = pos if _spacing_mode and is_moving: diff --git a/src/Tools/DesignTools/LineTool.gd b/src/Tools/DesignTools/LineTool.gd index 3f2f858b2..7c97d0511 100644 --- a/src/Tools/DesignTools/LineTool.gd +++ b/src/Tools/DesignTools/LineTool.gd @@ -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: diff --git a/src/Tools/UtilityTools/Move.gd b/src/Tools/UtilityTools/Move.gd index 247ab85de..476ae025e 100644 --- a/src/Tools/UtilityTools/Move.gd +++ b/src/Tools/UtilityTools/Move.gd @@ -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: diff --git a/src/Tools/UtilityTools/Text.gd b/src/Tools/UtilityTools/Text.gd index b1128de66..f267254bb 100644 --- a/src/Tools/UtilityTools/Text.gd +++ b/src/Tools/UtilityTools/Text.gd @@ -103,8 +103,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: @@ -158,6 +158,7 @@ func text_to_pixels() -> void: if image is ImageExtended: image.convert_rgb_to_indexed() commit_undo("Draw", undo_data) + update_cels(project) func commit_undo(action: String, undo_data: Dictionary) -> void: diff --git a/src/UI/Canvas/Selection.gd b/src/UI/Canvas/Selection.gd index f28fd7cd7..9f12b69b2 100644 --- a/src/UI/Canvas/Selection.gd +++ b/src/UI/Canvas/Selection.gd @@ -517,6 +517,7 @@ func transform_content_confirm() -> void: big_bounding_rectangle.position ) cel_image.convert_rgb_to_indexed() + cel.tool_finished_drawing() project.selection_map.move_bitmap_values(project) commit_undo("Move Selection", undo_data) @@ -552,6 +553,7 @@ func transform_content_cancel() -> void: big_bounding_rectangle.position ) cel.transformed_content = null + cel.tool_finished_drawing() for cel_index in project.selected_cels: canvas.update_texture(cel_index[1]) original_preview_image = Image.new() From 65895374ab1038e0518fbdcfd101121ff2eb0ecc Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 24 Nov 2024 19:41:16 +0200 Subject: [PATCH 18/76] Tileset panel UI improvements --- src/Classes/Cels/CelTileMap.gd | 1 - src/UI/TilesPanel.gd | 29 ++++++++++++++++++++++------- src/UI/TilesPanel.tscn | 6 ++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 9f1a5cb1b..bb3c7edc8 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -22,7 +22,6 @@ func update_texture() -> void: # Prevent from drawing on empty image portions. if index == 0 and tileset.tiles.size() > 1: var coords := get_tile_coords(i) - var rect := Rect2i(coords, tileset.tile_size) var current_tile := tileset.tiles[index] var tile_size := current_tile.image.get_size() image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords) diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index 7329965b4..cc033581d 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -5,7 +5,9 @@ enum TileEditingMode { MANUAL, AUTO, STACK } const TRANSPARENT_CHECKER := preload("res://src/UI/Nodes/TransparentChecker.tscn") +static var placing_tiles := false static var tile_editing_mode := TileEditingMode.AUTO +static var selected_tile_index := 0 var current_tileset: TileSetCustom @onready var h_flow_container: HFlowContainer = $VBoxContainer/ScrollContainer/HFlowContainer @@ -34,14 +36,22 @@ func _update_tileset(cel: BaseCel) -> void: if tilemap_cel != Global.current_project.get_current_cel(): tilemap_cel.tileset.updated.disconnect(_update_tileset) 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 button := _create_tile_button(ImageTexture.create_from_image(tile.image), i) + 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.button_pressed = true h_flow_container.add_child(button) -func _create_tile_button(texture: Texture2D, index: int) -> 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 = Vector2i(36, 36) button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND var texture_rect := TextureRect.new() @@ -54,18 +64,19 @@ func _create_tile_button(texture: Texture2D, index: int) -> Button: 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() + 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.pressed.connect(_on_tile_button_pressed.bind(index)) + button.toggled.connect(_on_tile_button_toggled.bind(index)) return button -func _on_tile_button_pressed(index: int) -> void: - print(index) +func _on_tile_button_toggled(toggled_on: bool, index: int) -> void: + if toggled_on: + selected_tile_index = index func _clear_tile_buttons() -> void: @@ -73,6 +84,10 @@ func _clear_tile_buttons() -> void: child.queue_free() +func _on_place_tiles_toggled(toggled_on: bool) -> void: + placing_tiles = toggled_on + + func _on_manual_toggled(toggled_on: bool) -> void: if toggled_on: tile_editing_mode = TileEditingMode.MANUAL diff --git a/src/UI/TilesPanel.tscn b/src/UI/TilesPanel.tscn index e764d491d..319d499b5 100644 --- a/src/UI/TilesPanel.tscn +++ b/src/UI/TilesPanel.tscn @@ -15,6 +15,11 @@ 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 +text = "Place tiles" + [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] layout_mode = 2 @@ -46,6 +51,7 @@ 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="toggled" from="VBoxContainer/HBoxContainer/Manual" to="." method="_on_manual_toggled"] [connection signal="toggled" from="VBoxContainer/HBoxContainer/Auto" to="." method="_on_auto_toggled"] [connection signal="toggled" from="VBoxContainer/HBoxContainer/Stack" to="." method="_on_stack_toggled"] From 3e2587e4cac6009f7b69b33f92b37fd35fbfda19 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 24 Nov 2024 21:36:27 +0200 Subject: [PATCH 19/76] Implement placing tiles for pencil tool Still needs undo support --- src/Classes/Cels/CelTileMap.gd | 40 ++++++++++++++++++++++++--------- src/Tools/BaseDraw.gd | 8 +++++++ src/Tools/BaseTool.gd | 13 +++++++++++ src/Tools/DesignTools/Pencil.gd | 10 +++++++++ 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index bb3c7edc8..e9610901e 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -15,6 +15,14 @@ func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> v indices.resize(indices_x * indices_y) +func set_index(tile_position: int, index: int) -> void: + index = clampi(index, 0, tileset.tiles.size() - 1) + tileset.tiles[index].times_used += 1 + indices[tile_position] = index + update_cel_portion(tile_position) + Global.canvas.queue_redraw() + + func update_texture() -> void: if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: for i in indices.size(): @@ -173,24 +181,34 @@ func re_index_tiles_after_index(index: int) -> void: indices[i] -= 1 +func update_cel_portion(tile_position: int) -> void: + var coords := get_tile_coords(tile_position) + var rect := Rect2i(coords, tileset.tile_size) + var image_portion := image.get_region(rect) + var index := indices[tile_position] + var current_tile := tileset.tiles[index] + if image_portion.get_data() != current_tile.image.get_data(): + var tile_size := current_tile.image.get_size() + image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords) + + func update_cel_portions() -> void: for i in indices.size(): - var coords := get_tile_coords(i) - var rect := Rect2i(coords, tileset.tile_size) - var image_portion := image.get_region(rect) - var index := indices[i] - var current_tile := tileset.tiles[index] - if image_portion.get_data() != current_tile.image.get_data(): - var tile_size := current_tile.image.get_size() - image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords) + update_cel_portion(i) -func get_tile_coords(portion_index: int) -> Vector2i: - var x_coord := float(tileset.tile_size.x) * (portion_index % indices_x) - var y_coord := float(tileset.tile_size.y) * (portion_index / indices_x) +func get_tile_coords(portion_position: int) -> Vector2i: + var x_coord := float(tileset.tile_size.x) * (portion_position % indices_x) + var y_coord := float(tileset.tile_size.y) * (portion_position / indices_x) return Vector2i(x_coord, y_coord) +func get_tile_position(coords: Vector2i) -> int: + var x := floori(coords.x / tileset.tile_size.x) + var y := floori(coords.y / tileset.tile_size.y) * indices_x + return x + y + + ## Unused, should delete. func re_index_tiles() -> void: for i in indices.size(): diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index 2a1e74f01..0a90e7790 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -303,6 +303,14 @@ func draw_end(pos: Vector2i) -> void: _polylines = _create_polylines(_indicator) +func draw_tile(pos: Vector2i, tile_index: int) -> void: + if Global.current_project.get_current_cel() is not CelTileMap: + return + var tile_position := get_tile_position(pos) + var cel := Global.current_project.get_current_cel() as CelTileMap + cel.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 diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index e7bc269a1..0d60398ff 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -86,6 +86,19 @@ func update_cels(project := Global.current_project) -> void: cel.tool_finished_drawing() +func is_placing_tiles() -> bool: + return Global.current_project.get_current_cel() is CelTileMap and TileSetPanel.placing_tiles + + +func get_tile_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_tile_position(pos) + return tile_pos + + func cursor_move(pos: Vector2i) -> void: _cursor = pos if _spacing_mode and is_moving: diff --git a/src/Tools/DesignTools/Pencil.gd b/src/Tools/DesignTools/Pencil.gd index 520ab865a..f373d7dda 100644 --- a/src/Tools/DesignTools/Pencil.gd +++ b/src/Tools/DesignTools/Pencil.gd @@ -96,6 +96,9 @@ func draw_start(pos: Vector2i) -> void: _old_spacing_mode = _spacing_mode pos = snap_position(pos) super.draw_start(pos) + if is_placing_tiles(): + draw_tile(pos, TileSetPanel.selected_tile_index) + return if Input.is_action_pressed(&"draw_color_picker", true): _picking_color = true _pick_color(pos) @@ -138,6 +141,9 @@ func draw_move(pos_i: Vector2i) -> void: var pos := _get_stabilized_position(pos_i) pos = snap_position(pos) super.draw_move(pos) + if is_placing_tiles(): + draw_tile(pos, TileSetPanel.selected_tile_index) + return if _picking_color: # Still return even if we released Alt if Input.is_action_pressed(&"draw_color_picker", true): _pick_color(pos) @@ -164,6 +170,10 @@ func draw_move(pos_i: Vector2i) -> void: func draw_end(pos: Vector2i) -> void: pos = snap_position(pos) + if is_placing_tiles(): + super.draw_end(pos) + draw_tile(pos, TileSetPanel.selected_tile_index) + return if _picking_color: super.draw_end(pos) return From c1028131a1d73d47bd289cc1d035503939535d1f Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 24 Nov 2024 22:23:05 +0200 Subject: [PATCH 20/76] Undo/redo now removes tiles and re-indexes tilemap tiles --- src/Autoload/Global.gd | 6 +++++- src/Classes/Cels/BaseCel.gd | 2 +- src/Classes/Cels/CelTileMap.gd | 11 +++++++---- src/Classes/TileSetCustom.gd | 28 ++++++++++++++++------------ src/Tools/BaseTool.gd | 7 ------- src/Tools/DesignTools/Pencil.gd | 21 +++++++++++---------- src/Tools/UtilityTools/Text.gd | 1 - src/UI/Canvas/Selection.gd | 2 -- src/UI/Timeline/AnimationTimeline.gd | 2 +- 9 files changed, 41 insertions(+), 39 deletions(-) diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index 49a0caa16..7977d45d4 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -954,7 +954,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( @@ -981,10 +981,14 @@ 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] + cel.on_undo_redo(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] + cel.on_undo_redo(undo) canvas.selection.queue_redraw() if action_name == "Scale": diff --git a/src/Classes/Cels/BaseCel.gd b/src/Classes/Cels/BaseCel.gd index 313544956..f5ccc7b73 100644 --- a/src/Classes/Cels/BaseCel.gd +++ b/src/Classes/Cels/BaseCel.gd @@ -77,7 +77,7 @@ func update_texture() -> void: cel.texture_changed.emit() -func tool_finished_drawing() -> void: +func on_undo_redo(undo: bool) -> void: pass diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index e9610901e..3f0fa576d 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -36,11 +36,11 @@ func update_texture() -> void: super.update_texture() -func tool_finished_drawing() -> void: - update_tileset() +func on_undo_redo(undo: bool) -> void: + update_tileset(undo) -func update_tileset() -> void: +func update_tileset(undo: bool) -> void: for i in indices.size(): var coords := get_tile_coords(i) var rect := Rect2i(coords, tileset.tile_size) @@ -74,6 +74,10 @@ func update_tileset() -> void: if not found_tile: tileset.add_tile(image_portion, TileSetPanel.tile_editing_mode) indices[i] = tileset.tiles.size() - 1 + if undo: + var tile_removed := tileset.remove_unused_tiles() + if tile_removed: + re_index_tiles() ## Cases:[br] @@ -209,7 +213,6 @@ func get_tile_position(coords: Vector2i) -> int: return x + y -## Unused, should delete. func re_index_tiles() -> void: for i in indices.size(): var x_coord := float(tileset.tile_size.x) * (i % indices_x) diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd index 8d5867a30..8fb9b3849 100644 --- a/src/Classes/TileSetCustom.gd +++ b/src/Classes/TileSetCustom.gd @@ -3,6 +3,7 @@ extends RefCounted signal updated +var project: Project var name := "" var tile_size: Vector2i var tiles: Array[Tile] = [] @@ -12,40 +13,44 @@ class Tile: var image: Image var mode_added: TileSetPanel.TileEditingMode var times_used := 1 + var undo_step_added := 0 - func _init(_image: Image, _mode_added: TileSetPanel.TileEditingMode) -> void: + func _init( + _image: Image, _mode_added: TileSetPanel.TileEditingMode, _undo_step_added := 0 + ) -> void: image = _image mode_added = _mode_added + undo_step_added = _undo_step_added - func can_be_removed() -> bool: + func can_be_removed(project: Project) -> bool: + if project.undos < undo_step_added: + return true return mode_added != TileSetPanel.TileEditingMode.STACK and times_used <= 0 -func _init(_tile_size: Vector2i, _name := "") -> void: +func _init(_tile_size: Vector2i, _project: Project, _name := "") -> void: tile_size = _tile_size + project = _project name = _name - #var indices_x := ceili(float(_project_size.x) / tile_size.x) - #var indices_y := ceili(float(_project_size.y) / tile_size.y) - #tiles.resize(indices_x * indices_y + 1) var empty_image := Image.create_empty(tile_size.x, tile_size.y, false, Image.FORMAT_RGBA8) tiles.append(Tile.new(empty_image, TileSetPanel.tile_editing_mode)) func add_tile(image: Image, edit_mode: TileSetPanel.TileEditingMode) -> void: - var tile := Tile.new(image, edit_mode) + var tile := Tile.new(image, edit_mode, project.undos) tiles.append(tile) updated.emit() func insert_tile(image: Image, position: int, edit_mode: TileSetPanel.TileEditingMode) -> void: - var tile := Tile.new(image, edit_mode) + var tile := Tile.new(image, edit_mode, project.undos) tiles.insert(position, tile) updated.emit() func unuse_tile_at_index(index: int) -> bool: tiles[index].times_used -= 1 - if tiles[index].can_be_removed(): + if tiles[index].can_be_removed(project): remove_tile_at_index(index) return true return false @@ -69,12 +74,11 @@ func find_tile(image: Image) -> int: return -1 -## Unused, should delete. func remove_unused_tiles() -> bool: var tile_removed := false for i in range(tiles.size() - 1, 0, -1): var tile := tiles[i] - if tile.can_be_removed(): - tile_removed = true + if tile.can_be_removed(project): remove_tile_at_index(i) + tile_removed = true return tile_removed diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index 0d60398ff..4e173e7fc 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -76,16 +76,9 @@ func draw_end(_pos: Vector2i) -> void: is_moving = false _draw_cache = [] var project := Global.current_project - update_cels(project) project.can_undo = true -func update_cels(project := Global.current_project) -> void: - for cel_index in project.selected_cels: - var cel := project.frames[cel_index[0]].cels[cel_index[1]] as BaseCel - cel.tool_finished_drawing() - - func is_placing_tiles() -> bool: return Global.current_project.get_current_cel() is CelTileMap and TileSetPanel.placing_tiles diff --git a/src/Tools/DesignTools/Pencil.gd b/src/Tools/DesignTools/Pencil.gd index f373d7dda..3a47716a1 100644 --- a/src/Tools/DesignTools/Pencil.gd +++ b/src/Tools/DesignTools/Pencil.gd @@ -96,9 +96,6 @@ func draw_start(pos: Vector2i) -> void: _old_spacing_mode = _spacing_mode pos = snap_position(pos) super.draw_start(pos) - if is_placing_tiles(): - draw_tile(pos, TileSetPanel.selected_tile_index) - return if Input.is_action_pressed(&"draw_color_picker", true): _picking_color = true _pick_color(pos) @@ -106,6 +103,10 @@ func draw_start(pos: Vector2i) -> void: _picking_color = false Global.canvas.selection.transform_content_confirm() + prepare_undo("Draw") + if is_placing_tiles(): + draw_tile(pos, TileSetPanel.selected_tile_index) + return var can_skip_mask := true if tool_slot.color.a < 1 and !_overwrite: can_skip_mask = false @@ -115,7 +116,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") @@ -141,13 +141,13 @@ func draw_move(pos_i: Vector2i) -> void: var pos := _get_stabilized_position(pos_i) pos = snap_position(pos) super.draw_move(pos) - if is_placing_tiles(): - draw_tile(pos, TileSetPanel.selected_tile_index) - return if _picking_color: # Still return even if we released Alt if Input.is_action_pressed(&"draw_color_picker", true): _pick_color(pos) return + if is_placing_tiles(): + draw_tile(pos, TileSetPanel.selected_tile_index) + return if _draw_line: _spacing_mode = false # spacing mode is disabled during line mode @@ -170,12 +170,13 @@ func draw_move(pos_i: Vector2i) -> void: func draw_end(pos: Vector2i) -> void: pos = snap_position(pos) + if _picking_color: + super.draw_end(pos) + return if is_placing_tiles(): super.draw_end(pos) draw_tile(pos, TileSetPanel.selected_tile_index) - return - if _picking_color: - super.draw_end(pos) + commit_undo() return if _draw_line: diff --git a/src/Tools/UtilityTools/Text.gd b/src/Tools/UtilityTools/Text.gd index f267254bb..b3ce7a3e2 100644 --- a/src/Tools/UtilityTools/Text.gd +++ b/src/Tools/UtilityTools/Text.gd @@ -158,7 +158,6 @@ func text_to_pixels() -> void: if image is ImageExtended: image.convert_rgb_to_indexed() commit_undo("Draw", undo_data) - update_cels(project) func commit_undo(action: String, undo_data: Dictionary) -> void: diff --git a/src/UI/Canvas/Selection.gd b/src/UI/Canvas/Selection.gd index 9f12b69b2..f28fd7cd7 100644 --- a/src/UI/Canvas/Selection.gd +++ b/src/UI/Canvas/Selection.gd @@ -517,7 +517,6 @@ func transform_content_confirm() -> void: big_bounding_rectangle.position ) cel_image.convert_rgb_to_indexed() - cel.tool_finished_drawing() project.selection_map.move_bitmap_values(project) commit_undo("Move Selection", undo_data) @@ -553,7 +552,6 @@ func transform_content_cancel() -> void: big_bounding_rectangle.position ) cel.transformed_content = null - cel.tool_finished_drawing() for cel_index in project.selected_cels: canvas.update_texture(cel_index[1]) original_preview_image = Image.new() diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index dea3d6c52..a6255ffa6 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -847,7 +847,7 @@ func add_layer(type := 0) -> void: l = Layer3D.new(project) SteamManager.set_achievement("ACH_3D_LAYER") Global.LayerTypes.TILEMAP: - l = LayerTileMap.new(project, TileSetCustom.new(Vector2i(16, 16))) + l = LayerTileMap.new(project, TileSetCustom.new(Vector2i(16, 16), project)) var cels := [] for f in project.frames: From 6b95908ef31c2a5da2915280669198cc7475ab8e Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 25 Nov 2024 00:15:06 +0200 Subject: [PATCH 21/76] Place tiles mode works with eraser and color picker tools --- src/Autoload/Tools.gd | 2 ++ src/Classes/Cels/CelTileMap.gd | 4 ++++ src/Tools/BaseTool.gd | 6 +++++- src/Tools/DesignTools/Eraser.gd | 15 ++++++++++++--- src/Tools/UtilityTools/ColorPicker.gd | 7 +++++-- src/UI/TilesPanel.gd | 11 ++++++++--- src/UI/TilesPanel.tscn | 3 ++- 7 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/Autoload/Tools.gd b/src/Autoload/Tools.gd index 8b5cb0837..1bde704bc 100644 --- a/src/Autoload/Tools.gd +++ b/src/Autoload/Tools.gd @@ -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) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 3f0fa576d..f17a493b3 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -46,6 +46,10 @@ func update_tileset(undo: bool) -> void: var rect := Rect2i(coords, tileset.tile_size) var image_portion := image.get_region(rect) var index := indices[i] + if index >= tileset.tiles.size(): + print(i, " is out of bounds") + index = 0 + indices[i] = 0 var current_tile := tileset.tiles[index] if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: if image_portion.is_invisible(): diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index 4e173e7fc..b93e5832f 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -354,7 +354,11 @@ func _pick_color(pos: Vector2i) -> void: if pos.x < 0 or pos.y < 0: return - + if is_placing_tiles(): + var tile_position := get_tile_position(pos) + var cel := Global.current_project.get_current_cel() as CelTileMap + Tools.selected_tile_index_changed.emit(cel.indices[tile_position]) + 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: diff --git a/src/Tools/DesignTools/Eraser.gd b/src/Tools/DesignTools/Eraser.gd index 8f9d15b9f..556993b6a 100644 --- a/src/Tools/DesignTools/Eraser.gd +++ b/src/Tools/DesignTools/Eraser.gd @@ -42,13 +42,14 @@ func draw_start(pos: Vector2i) -> void: _pick_color(pos) return _picking_color = false - Global.canvas.selection.transform_content_confirm() + prepare_undo("Draw") + if is_placing_tiles(): + draw_tile(pos, 0) + return 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") @@ -74,6 +75,9 @@ func draw_move(pos_i: Vector2i) -> void: if Input.is_action_pressed(&"draw_color_picker", true): _pick_color(pos) return + if is_placing_tiles(): + draw_tile(pos, 0) + return if _draw_line: if Global.mirror_view: @@ -95,6 +99,11 @@ func draw_end(pos: Vector2i) -> void: if _picking_color: super.draw_end(pos) return + if is_placing_tiles(): + super.draw_end(pos) + draw_tile(pos, 0) + commit_undo() + return if _draw_line: if Global.mirror_view: diff --git a/src/Tools/UtilityTools/ColorPicker.gd b/src/Tools/UtilityTools/ColorPicker.gd index d04f70502..d449fc0b1 100644 --- a/src/Tools/UtilityTools/ColorPicker.gd +++ b/src/Tools/UtilityTools/ColorPicker.gd @@ -63,10 +63,13 @@ 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 is_placing_tiles(): + var tile_position := get_tile_position(pos) + var cel := Global.current_project.get_current_cel() as CelTileMap + Tools.selected_tile_index_changed.emit(cel.indices[tile_position]) + 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: diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index cc033581d..1a2a63187 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -10,10 +10,11 @@ static var tile_editing_mode := TileEditingMode.AUTO static var selected_tile_index := 0 var current_tileset: TileSetCustom -@onready var h_flow_container: HFlowContainer = $VBoxContainer/ScrollContainer/HFlowContainer +@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) Global.project_switched.connect(_on_cel_switched) @@ -45,7 +46,7 @@ func _update_tileset(cel: BaseCel) -> void: var button := _create_tile_button(texture, i, button_group) if i == selected_tile_index: button.button_pressed = true - h_flow_container.add_child(button) + tile_button_container.add_child(button) func _create_tile_button(texture: Texture2D, index: int, button_group: ButtonGroup) -> Button: @@ -74,13 +75,17 @@ func _create_tile_button(texture: Texture2D, index: int, button_group: ButtonGro return button +func select_tile(tile_index: int) -> void: + tile_button_container.get_child(tile_index).button_pressed = true + + func _on_tile_button_toggled(toggled_on: bool, index: int) -> void: if toggled_on: selected_tile_index = index func _clear_tile_buttons() -> void: - for child in h_flow_container.get_children(): + for child in tile_button_container.get_children(): child.queue_free() diff --git a/src/UI/TilesPanel.tscn b/src/UI/TilesPanel.tscn index 319d499b5..e4abb0cda 100644 --- a/src/UI/TilesPanel.tscn +++ b/src/UI/TilesPanel.tscn @@ -46,7 +46,8 @@ text = "Stack" layout_mode = 2 size_flags_vertical = 3 -[node name="HFlowContainer" type="HFlowContainer" parent="VBoxContainer/ScrollContainer"] +[node name="TileButtonContainer" type="HFlowContainer" parent="VBoxContainer/ScrollContainer"] +unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 From 64809c64d9c29d2113e04a5a1a32ffd6b5cd25cc Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 25 Nov 2024 00:37:33 +0200 Subject: [PATCH 22/76] Prevent from setting tile indices out of bounds of the canvas --- src/Classes/Cels/BaseCel.gd | 2 +- src/Classes/Cels/CelTileMap.gd | 19 ++++++++++++------- src/Tools/BaseDraw.gd | 1 + 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Classes/Cels/BaseCel.gd b/src/Classes/Cels/BaseCel.gd index f5ccc7b73..e9455d338 100644 --- a/src/Classes/Cels/BaseCel.gd +++ b/src/Classes/Cels/BaseCel.gd @@ -77,7 +77,7 @@ func update_texture() -> void: cel.texture_changed.emit() -func on_undo_redo(undo: bool) -> void: +func on_undo_redo(_undo: bool) -> void: pass diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index f17a493b3..476bba033 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -81,7 +81,7 @@ func update_tileset(undo: bool) -> void: if undo: var tile_removed := tileset.remove_unused_tiles() if tile_removed: - re_index_tiles() + re_index_all_tiles() ## Cases:[br] @@ -207,21 +207,26 @@ func update_cel_portions() -> void: func get_tile_coords(portion_position: int) -> Vector2i: var x_coord := float(tileset.tile_size.x) * (portion_position % indices_x) + @warning_ignore("integer_division") var y_coord := float(tileset.tile_size.y) * (portion_position / indices_x) return Vector2i(x_coord, y_coord) func get_tile_position(coords: Vector2i) -> int: - var x := floori(coords.x / tileset.tile_size.x) - var y := floori(coords.y / tileset.tile_size.y) * indices_x + @warning_ignore("integer_division") + var x := coords.x / tileset.tile_size.x + x = clampi(x, 0, indices_x - 1) + @warning_ignore("integer_division") + var y := coords.y / tileset.tile_size.y + y = clampi(y, 0, indices_y - 1) + y *= indices_x return x + y -func re_index_tiles() -> void: +func re_index_all_tiles() -> void: for i in indices.size(): - var x_coord := float(tileset.tile_size.x) * (i % indices_x) - var y_coord := float(tileset.tile_size.y) * (i / indices_x) - var rect := Rect2i(Vector2i(x_coord, y_coord), tileset.tile_size) + var coords := get_tile_coords(i) + var rect := Rect2i(coords, tileset.tile_size) var image_portion := image.get_region(rect) if image_portion.is_invisible(): indices[i] = 0 diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index 0a90e7790..bb9da1a22 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -306,6 +306,7 @@ func draw_end(pos: Vector2i) -> void: func draw_tile(pos: Vector2i, tile_index: int) -> void: if Global.current_project.get_current_cel() is not CelTileMap: return + pos = Global.current_project.tiles.get_canon_position(pos) var tile_position := get_tile_position(pos) var cel := Global.current_project.get_current_cel() as CelTileMap cel.set_index(tile_position, tile_index) From 9bd2eb7f0f5c7b1a2fbac9dc133518dc9468c5fb Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 25 Nov 2024 01:44:37 +0200 Subject: [PATCH 23/76] Save and load to/from pxo files --- src/Autoload/OpenSave.gd | 20 +++++ src/Classes/Cels/CelTileMap.gd | 57 ++++++++----- src/Classes/Layers/LayerTileMap.gd | 19 +++++ src/Classes/Project.gd | 132 ++++++++++++++++------------- src/Classes/TileSetCustom.gd | 9 ++ 5 files changed, 159 insertions(+), 78 deletions(-) diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index dc9f8f3c2..a710f8dd5 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -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, 2) 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: diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 476bba033..6d77f5f8b 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -1,7 +1,13 @@ class_name CelTileMap extends PixelCel -var tileset: TileSetCustom +var tileset: TileSetCustom: + set(value): + tileset = value + if is_instance_valid(tileset): + indices_x = ceili(float(get_image().get_width()) / tileset.tile_size.x) + indices_y = ceili(float(get_image().get_height()) / tileset.tile_size.y) + indices.resize(indices_x * indices_y) var indices := PackedInt32Array() var indices_x: int var indices_y: int @@ -10,9 +16,6 @@ var indices_y: int func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> void: super._init(_image, _opacity) tileset = _tileset - indices_x = ceili(float(get_image().get_width()) / tileset.tile_size.x) - indices_y = ceili(float(get_image().get_height()) / tileset.tile_size.y) - indices.resize(indices_x * indices_y) func set_index(tile_position: int, index: int) -> void: @@ -23,23 +26,6 @@ func set_index(tile_position: int, index: int) -> void: Global.canvas.queue_redraw() -func update_texture() -> void: - if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: - for i in indices.size(): - var index := indices[i] - # Prevent from drawing on empty image portions. - if index == 0 and tileset.tiles.size() > 1: - var coords := get_tile_coords(i) - var current_tile := tileset.tiles[index] - var tile_size := current_tile.image.get_size() - image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords) - super.update_texture() - - -func on_undo_redo(undo: bool) -> void: - update_tileset(undo) - - func update_tileset(undo: bool) -> void: for i in indices.size(): var coords := get_tile_coords(i) @@ -238,5 +224,34 @@ func re_index_all_tiles() -> void: break +# Overridden Methods: +func update_texture() -> void: + if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: + for i in indices.size(): + var index := indices[i] + # Prevent from drawing on empty image portions. + if index == 0 and tileset.tiles.size() > 1: + var coords := get_tile_coords(i) + var current_tile := tileset.tiles[index] + var tile_size := current_tile.image.get_size() + image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords) + super.update_texture() + + +func on_undo_redo(undo: bool) -> void: + update_tileset(undo) + + +func serialize() -> Dictionary: + var dict := super.serialize() + dict["tile_indices"] = indices + return dict + + +func deserialize(dict: Dictionary) -> void: + super.deserialize(dict) + indices = dict.get("tile_indices") + + func get_class_name() -> String: return "CelTileMap" diff --git a/src/Classes/Layers/LayerTileMap.gd b/src/Classes/Layers/LayerTileMap.gd index c6a2296cc..1f73737ee 100644 --- a/src/Classes/Layers/LayerTileMap.gd +++ b/src/Classes/Layers/LayerTileMap.gd @@ -12,6 +12,19 @@ func _init(_project: Project, _tileset: TileSetCustom, _name := "") -> void: # 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 @@ -23,3 +36,9 @@ func new_empty_cel() -> BaseCel: 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) diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index bdea6a52b..512bf1310 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -296,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) @@ -316,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, @@ -345,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, self) + 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)): @@ -354,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: @@ -426,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) @@ -484,6 +476,32 @@ func _deserialize_metadata(object: Object, dict: Dictionary) -> void: object.set_meta(meta, metadata[meta]) +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 diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd index 8fb9b3849..84531817c 100644 --- a/src/Classes/TileSetCustom.gd +++ b/src/Classes/TileSetCustom.gd @@ -82,3 +82,12 @@ func remove_unused_tiles() -> bool: remove_tile_at_index(i) tile_removed = true return tile_removed + + +func serialize() -> Dictionary: + return {"name": name, "tile_size": tile_size, "tile_amount": tiles.size()} + + +func deserialize(dict: Dictionary) -> void: + name = dict.get("name", name) + tile_size = str_to_var("Vector2i" + dict.get("tile_size")) From fc8f21a436f079a8fb81abe22e9a8bbb01819bce Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 25 Nov 2024 02:05:49 +0200 Subject: [PATCH 24/76] Resize indices on project resize --- src/Autoload/Global.gd | 12 +++++++----- src/Classes/Cels/BaseCel.gd | 4 ++++ src/Classes/Cels/CelTileMap.gd | 7 +++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index 7977d45d4..4b1d1690d 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -980,14 +980,18 @@ 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) cel.on_undo_redo(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) cel.on_undo_redo(undo) canvas.selection.queue_redraw() @@ -995,9 +999,7 @@ func undo_or_redo( 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() diff --git a/src/Classes/Cels/BaseCel.gd b/src/Classes/Cels/BaseCel.gd index e9455d338..2ac64dcab 100644 --- a/src/Classes/Cels/BaseCel.gd +++ b/src/Classes/Cels/BaseCel.gd @@ -96,6 +96,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 diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 6d77f5f8b..ef65f0109 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -238,6 +238,13 @@ func update_texture() -> void: super.update_texture() +func size_changed(new_size: Vector2i) -> void: + indices_x = ceili(float(new_size.x) / tileset.tile_size.x) + indices_y = ceili(float(new_size.y) / tileset.tile_size.y) + indices.resize(indices_x * indices_y) + re_index_all_tiles() + + func on_undo_redo(undo: bool) -> void: update_tileset(undo) From 46ebb0930c9905b4d6810a1618c3b5d11eb3ff98 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 25 Nov 2024 02:44:32 +0200 Subject: [PATCH 25/76] Add a dialog when creating a tilemap layer to allow users to set the tileset's settings --- src/Classes/Layers/LayerTileMap.gd | 4 ++ src/UI/Timeline/AnimationTimeline.gd | 51 +++++++++------- src/UI/Timeline/AnimationTimeline.tscn | 7 ++- src/UI/Timeline/NewTileMapLayerDialog.gd | 39 ++++++++++++ src/UI/Timeline/NewTileMapLayerDialog.tscn | 69 ++++++++++++++++++++++ 5 files changed, 147 insertions(+), 23 deletions(-) create mode 100644 src/UI/Timeline/NewTileMapLayerDialog.gd create mode 100644 src/UI/Timeline/NewTileMapLayerDialog.tscn diff --git a/src/Classes/Layers/LayerTileMap.gd b/src/Classes/Layers/LayerTileMap.gd index 1f73737ee..d60bc8b17 100644 --- a/src/Classes/Layers/LayerTileMap.gd +++ b/src/Classes/Layers/LayerTileMap.gd @@ -42,3 +42,7 @@ 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 diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index a6255ffa6..7ef569173 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -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 @@ -832,26 +833,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") - Global.LayerTypes.TILEMAP: - l = LayerTileMap.new(project, TileSetCustom.new(Vector2i(16, 16), project)) + 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: @@ -864,14 +873,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)) diff --git a/src/UI/Timeline/AnimationTimeline.tscn b/src/UI/Timeline/AnimationTimeline.tscn index 472f43816..622135afa 100644 --- a/src/UI/Timeline/AnimationTimeline.tscn +++ b/src/UI/Timeline/AnimationTimeline.tscn @@ -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"] @@ -1116,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 @@ -1125,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]] diff --git a/src/UI/Timeline/NewTileMapLayerDialog.gd b/src/UI/Timeline/NewTileMapLayerDialog.gd new file mode 100644 index 000000000..cdb1f71fc --- /dev/null +++ b/src/UI/Timeline/NewTileMapLayerDialog.gd @@ -0,0 +1,39 @@ +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, project, 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] + var item_string := " %s (%s×%s)" % [i, tileset.tile_size.x, tileset.tile_size.y] + if not tileset.name.is_empty(): + item_string += ": " + tileset.name + tileset_option_button.add_item(tr("Tileset" + item_string)) diff --git a/src/UI/Timeline/NewTileMapLayerDialog.tscn b/src/UI/Timeline/NewTileMapLayerDialog.tscn new file mode 100644 index 000000000..46fb9dde5 --- /dev/null +++ b/src/UI/Timeline/NewTileMapLayerDialog.tscn @@ -0,0 +1,69 @@ +[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"] From 24af6573e677a7f7fcfa01090714b12ae52c3dcf Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:17:01 +0200 Subject: [PATCH 26/76] Fix out of bounds issues when undoing/redoing when the place tiles mode is enabled --- src/Classes/Cels/CelTileMap.gd | 48 +++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index ef65f0109..339945671 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -11,6 +11,9 @@ var tileset: TileSetCustom: var indices := PackedInt32Array() var indices_x: int var indices_y: int +## Dictionary of [int] and an [Array] of [bool] ([member TileSetPanel.placing_tiles]) +## and [enum TileSetPanel.TileEditingMode]. +var undo_redo_modes := {} func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> void: @@ -27,6 +30,12 @@ func set_index(tile_position: int, index: int) -> void: func update_tileset(undo: bool) -> void: + var undos := tileset.project.undos + if not undo and not _is_redo(): + undo_redo_modes[undos] = [TileSetPanel.placing_tiles, TileSetPanel.tile_editing_mode] + if undo: + undos += 1 + var tile_editing_mode := _get_tile_editing_mode(undos) for i in indices.size(): var coords := get_tile_coords(i) var rect := Rect2i(coords, tileset.tile_size) @@ -37,19 +46,19 @@ func update_tileset(undo: bool) -> void: index = 0 indices[i] = 0 var current_tile := tileset.tiles[index] - if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: + 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, TileSetPanel.tile_editing_mode) + tileset.add_tile(image_portion, tile_editing_mode) indices[i] = tileset.tiles.size() - 1 continue if image_portion.get_data() != current_tile.image.get_data(): tileset.replace_tile_at(image_portion, index) update_cel_portions() - elif TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.AUTO: + elif tile_editing_mode == TileSetPanel.TileEditingMode.AUTO: handle_auto_editing_mode(i, image_portion) else: # Stack if image_portion.is_invisible(): @@ -62,7 +71,7 @@ func update_tileset(undo: bool) -> void: found_tile = true break if not found_tile: - tileset.add_tile(image_portion, TileSetPanel.tile_editing_mode) + tileset.add_tile(image_portion, tile_editing_mode) indices[i] = tileset.tiles.size() - 1 if undo: var tile_removed := tileset.remove_unused_tiles() @@ -129,7 +138,7 @@ func handle_auto_editing_mode(i: int, image_portion: Image) -> void: else: # Case 2: The portion is not mapped already, # and it does not exist in the tileset. - tileset.add_tile(image_portion, TileSetPanel.tile_editing_mode) + tileset.add_tile(image_portion, TileSetPanel.TileEditingMode.AUTO) indices[i] = tileset.tiles.size() - 1 else: # If the portion is already mapped. if image_portion.get_data() == current_tile.image.get_data(): @@ -157,7 +166,7 @@ func handle_auto_editing_mode(i: int, image_portion: Image) -> void: # exist in the tileset as a tile, # and the currently mapped tile still exists in the tileset. tileset.unuse_tile_at_index(index) - tileset.add_tile(image_portion, TileSetPanel.tile_editing_mode) + tileset.add_tile(image_portion, TileSetPanel.TileEditingMode.AUTO) indices[i] = tileset.tiles.size() - 1 else: # Case 7: The portion is mapped and it does not @@ -224,9 +233,26 @@ func re_index_all_tiles() -> void: break +func _is_redo() -> bool: + return Global.control.redone + + +func _get_tile_editing_mode(undos: int) -> TileSetPanel.TileEditingMode: + var tile_editing_mode: TileSetPanel.TileEditingMode + if undo_redo_modes.has(undos): + tile_editing_mode = undo_redo_modes[undos][1] + else: + tile_editing_mode = TileSetPanel.tile_editing_mode + return tile_editing_mode + + # Overridden Methods: func update_texture() -> void: - if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: + var undos := tileset.project.undos + #if undo: + #undos += 1 + var tile_editing_mode := _get_tile_editing_mode(undos) + if tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: for i in indices.size(): var index := indices[i] # Prevent from drawing on empty image portions. @@ -246,6 +272,14 @@ func size_changed(new_size: Vector2i) -> void: func on_undo_redo(undo: bool) -> void: + var undos := tileset.project.undos + if undo: + undos += 1 + if (undo or _is_redo()) and undo_redo_modes.has(undos): + var placing_tiles: bool = undo_redo_modes[undos][0] + if placing_tiles: + re_index_all_tiles() + return update_tileset(undo) From 6f3e3c8566586359a52d23bb32ed0d6829f6b7e7 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:24:38 +0200 Subject: [PATCH 27/76] When a tilemap cel is selected, force the first grid to have the same size as the tile size --- src/UI/Canvas/Grid.gd | 21 +++++++++++++-------- src/UI/Canvas/TileModeIndices.gd | 8 ++++---- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/UI/Canvas/Grid.gd b/src/UI/Canvas/Grid.gd index 7090bbd60..b38bde401 100644 --- a/src/UI/Canvas/Grid.gd +++ b/src/UI/Canvas/Grid.gd @@ -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 diff --git a/src/UI/Canvas/TileModeIndices.gd b/src/UI/Canvas/TileModeIndices.gd index a577b3bfd..40c632071 100644 --- a/src/UI/Canvas/TileModeIndices.gd +++ b/src/UI/Canvas/TileModeIndices.gd @@ -11,7 +11,7 @@ func _draw() -> void: if current_cel is CelTileMap and Input.is_action_pressed("ctrl"): var tilemap_cel := current_cel as CelTileMap for i in tilemap_cel.indices.size(): - var x := float(tilemap_cel.tileset.tile_size.x) * (i % tilemap_cel.indices_x) - var y := float(tilemap_cel.tileset.tile_size.y) * (i / tilemap_cel.indices_x) - var pos := Vector2i(x, y + tilemap_cel.tileset.tile_size.y) - draw_string(Themes.get_font(), pos, str(tilemap_cel.indices[i]), 0, -1, 12) + var pos := tilemap_cel.get_tile_coords(i) + pos.y += tilemap_cel.tileset.tile_size.y + var text := str(tilemap_cel.indices[i]) + draw_string(Themes.get_font(), pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10) From 5c90501293d3644bfb06baff1b019674be0c9708 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:22:38 +0200 Subject: [PATCH 28/76] Enable tile drawing mode when clicking on a tile button --- src/UI/TilesPanel.gd | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index 1a2a63187..df80188c8 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -10,6 +10,7 @@ static var tile_editing_mode := TileEditingMode.AUTO static var selected_tile_index := 0 var current_tileset: TileSetCustom +@onready var place_tiles: CheckBox = $VBoxContainer/PlaceTiles @onready var tile_button_container: HFlowContainer = %TileButtonContainer @@ -45,7 +46,7 @@ func _update_tileset(cel: BaseCel) -> void: var texture := ImageTexture.create_from_image(tile.image) var button := _create_tile_button(texture, i, button_group) if i == selected_tile_index: - button.button_pressed = true + button.set_pressed_no_signal(true) tile_button_container.add_child(button) @@ -82,6 +83,7 @@ func select_tile(tile_index: int) -> void: 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: From e84e9d46f3d462bb3ebc8656114c4d92f08de73b Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:38:20 +0200 Subject: [PATCH 29/76] Resize tileset buttons --- src/UI/TilesPanel.gd | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index df80188c8..164025bab 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -4,11 +4,23 @@ 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 static var placing_tiles := false static var tile_editing_mode := TileEditingMode.AUTO static var selected_tile_index := 0 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 tile_button_container: HFlowContainer = %TileButtonContainer @@ -20,6 +32,14 @@ func _ready() -> void: Global.project_switched.connect(_on_cel_switched) +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 _on_cel_switched() -> void: if Global.current_project.get_current_cel() is not CelTileMap: _clear_tile_buttons() @@ -54,7 +74,7 @@ func _create_tile_button(texture: Texture2D, index: int, button_group: ButtonGro var button := Button.new() button.button_group = button_group button.toggle_mode = true - button.custom_minimum_size = Vector2i(36, 36) + 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 From a617039967601c067ac11c0f001283fc0baa6c2c Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:45:04 +0200 Subject: [PATCH 30/76] Fix tileset panel updating when undoing and the wrong tilemap cel is selected --- src/UI/TilesPanel.gd | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index 164025bab..ed3df48c5 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -40,13 +40,26 @@ func _gui_input(event: InputEvent) -> void: get_viewport().set_input_as_handled() +func set_tileset(tileset: TileSetCustom, cel: BaseCel) -> 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.bind(cel)) + + func _on_cel_switched() -> void: if Global.current_project.get_current_cel() is not CelTileMap: + set_tileset(null, null) _clear_tile_buttons() return var cel := Global.current_project.get_current_cel() as CelTileMap - if not cel.tileset.updated.is_connected(_update_tileset): - cel.tileset.updated.connect(_update_tileset.bind(cel)) + set_tileset(cel.tileset, cel) _update_tileset(cel) @@ -55,8 +68,6 @@ func _update_tileset(cel: BaseCel) -> void: if cel is not CelTileMap: return var tilemap_cel := cel as CelTileMap - if tilemap_cel != Global.current_project.get_current_cel(): - tilemap_cel.tileset.updated.disconnect(_update_tileset) var tileset := tilemap_cel.tileset var button_group := ButtonGroup.new() if selected_tile_index >= tileset.tiles.size(): From 6ac98e1bc6e8545eb0e32e2e1afbd8e5d56b5b7d Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:02:05 +0200 Subject: [PATCH 31/76] Make manual tile editing mode automatically update all other image portions that have the same tile index --- src/Autoload/Global.gd | 4 +-- src/Classes/Cels/BaseCel.gd | 2 +- src/Classes/Cels/CelTileMap.gd | 53 ++++++++++++++++++++++++---------- src/Classes/Cels/PixelCel.gd | 4 +-- src/UI/Canvas/Canvas.gd | 6 ++-- 5 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index 4b1d1690d..4b373734f 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -983,7 +983,7 @@ func undo_or_redo( 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) + canvas.update_texture(layer_index, frame_index, project, undo) cel.on_undo_redo(undo) else: for i in project.frames.size(): @@ -991,7 +991,7 @@ func undo_or_redo( var cel := project.frames[i].cels[j] if action_name == "Scale": cel.size_changed(project.size) - canvas.update_texture(j, i, project) + canvas.update_texture(j, i, project, undo) cel.on_undo_redo(undo) canvas.selection.queue_redraw() diff --git a/src/Classes/Cels/BaseCel.gd b/src/Classes/Cels/BaseCel.gd index 2ac64dcab..ec9b9089c 100644 --- a/src/Classes/Cels/BaseCel.gd +++ b/src/Classes/Cels/BaseCel.gd @@ -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 diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 339945671..fef8ff6b2 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -14,6 +14,12 @@ var indices_y: int ## Dictionary of [int] and an [Array] of [bool] ([member TileSetPanel.placing_tiles]) ## and [enum TileSetPanel.TileEditingMode]. var undo_redo_modes := {} +## 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_tileset]. +var editing_images := {} func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> void: @@ -30,6 +36,7 @@ func set_index(tile_position: int, index: int) -> void: func update_tileset(undo: bool) -> void: + editing_images.clear() var undos := tileset.project.undos if not undo and not _is_redo(): undo_redo_modes[undos] = [TileSetPanel.placing_tiles, TileSetPanel.tile_editing_mode] @@ -42,7 +49,7 @@ func update_tileset(undo: bool) -> void: var image_portion := image.get_region(rect) var index := indices[i] if index >= tileset.tiles.size(): - print(i, " is out of bounds") + printerr("Tile at position ", i, ", mapped to ", index, " is out of bounds!") index = 0 indices[i] = 0 var current_tile := tileset.tiles[index] @@ -57,7 +64,6 @@ func update_tileset(undo: bool) -> void: continue if image_portion.get_data() != current_tile.image.get_data(): tileset.replace_tile_at(image_portion, index) - update_cel_portions() elif tile_editing_mode == TileSetPanel.TileEditingMode.AUTO: handle_auto_editing_mode(i, image_portion) else: # Stack @@ -195,6 +201,7 @@ func update_cel_portion(tile_position: int) -> void: image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords) +## Unused, should delete. func update_cel_portions() -> void: for i in indices.size(): update_cel_portion(i) @@ -247,21 +254,35 @@ func _get_tile_editing_mode(undos: int) -> TileSetPanel.TileEditingMode: # Overridden Methods: -func update_texture() -> void: - var undos := tileset.project.undos - #if undo: - #undos += 1 - var tile_editing_mode := _get_tile_editing_mode(undos) - if tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: - for i in indices.size(): - var index := indices[i] +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) + return + + for i in indices.size(): + var index := indices[i] + var coords := get_tile_coords(i) + var rect := Rect2i(coords, tileset.tile_size) + var image_portion := image.get_region(rect) + var current_tile := tileset.tiles[index] + if index == 0 and tileset.tiles.size() > 1: # Prevent from drawing on empty image portions. - if index == 0 and tileset.tiles.size() > 1: - var coords := get_tile_coords(i) - var current_tile := tileset.tiles[index] - var tile_size := current_tile.image.get_size() - image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords) - super.update_texture() + 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: + editing_images[index] = [i, image_portion] + var editing_image := editing_images[index][1] as Image + if editing_image.get_data() != image_portion.get_data(): + var tile_size := image_portion.get_size() + image.blit_rect(editing_image, Rect2i(Vector2i.ZERO, tile_size), coords) + else: + if image_portion.get_data() != current_tile.image.get_data(): + editing_images[index] = [i, image_portion] + super.update_texture(undo) func size_changed(new_size: Vector2i) -> void: diff --git a/src/Classes/Cels/PixelCel.gd b/src/Classes/Cels/PixelCel.gd index dc2bb3aa7..82c09bf61 100644 --- a/src/Classes/Cels/PixelCel.gd +++ b/src/Classes/Cels/PixelCel.gd @@ -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: diff --git a/src/UI/Canvas/Canvas.gd b/src/UI/Canvas/Canvas.gd index 62518ffd5..bf0aadc99 100644 --- a/src/UI/Canvas/Canvas.gd +++ b/src/UI/Canvas/Canvas.gd @@ -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: From f410cf89176fac9840ae4c9bd914dd3383a3cbe6 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:23:37 +0200 Subject: [PATCH 32/76] Manual mode should update other cels that have the same tileset --- src/Autoload/OpenSave.gd | 2 +- src/Classes/Cels/CelTileMap.gd | 37 +++++++++++++++++++++++----------- src/Classes/TileSetCustom.gd | 28 +++++++++++++------------ src/UI/TilesPanel.gd | 8 ++++---- 4 files changed, 45 insertions(+), 30 deletions(-) diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index a710f8dd5..80c00df04 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -269,7 +269,7 @@ func open_pxo_file(path: String, is_backup := false, replace_empty := true) -> v var image := Image.create_from_data( tile_size.x, tile_size.y, false, new_project.get_image_format(), image_data ) - tileset.add_tile(image, 2) + tileset.add_tile(image, null, 2) zip_reader.close() new_project.export_directory_path = path.get_base_dir() diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index fef8ff6b2..57a295167 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -3,11 +3,16 @@ extends PixelCel var tileset: TileSetCustom: set(value): + if is_instance_valid(tileset): + if tileset.updated.is_connected(_on_tileset_updated): + tileset.updated.disconnect(_on_tileset_updated) tileset = value if is_instance_valid(tileset): indices_x = ceili(float(get_image().get_width()) / tileset.tile_size.x) indices_y = ceili(float(get_image().get_height()) / tileset.tile_size.y) indices.resize(indices_x * indices_y) + if not tileset.updated.is_connected(_on_tileset_updated): + tileset.updated.connect(_on_tileset_updated) var indices := PackedInt32Array() var indices_x: int var indices_y: int @@ -59,11 +64,11 @@ func update_tileset(undo: bool) -> void: if index == 0: # If the tileset is empty, only then add a new tile. if tileset.tiles.size() <= 1: - tileset.add_tile(image_portion, tile_editing_mode) + tileset.add_tile(image_portion, self, tile_editing_mode) indices[i] = tileset.tiles.size() - 1 continue if image_portion.get_data() != current_tile.image.get_data(): - tileset.replace_tile_at(image_portion, index) + tileset.replace_tile_at(image_portion, index, self) elif tile_editing_mode == TileSetPanel.TileEditingMode.AUTO: handle_auto_editing_mode(i, image_portion) else: # Stack @@ -77,10 +82,10 @@ func update_tileset(undo: bool) -> void: found_tile = true break if not found_tile: - tileset.add_tile(image_portion, tile_editing_mode) + tileset.add_tile(image_portion, self, tile_editing_mode) indices[i] = tileset.tiles.size() - 1 if undo: - var tile_removed := tileset.remove_unused_tiles() + var tile_removed := tileset.remove_unused_tiles(self) if tile_removed: re_index_all_tiles() @@ -129,7 +134,7 @@ func handle_auto_editing_mode(i: int, image_portion: Image) -> void: indices[i] = 0 if index > 0: # Case 0.5: The portion is transparent and mapped to a tile. - var is_removed := tileset.unuse_tile_at_index(index) + 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_tiles_after_index(index) @@ -144,7 +149,7 @@ func handle_auto_editing_mode(i: int, image_portion: Image) -> void: else: # Case 2: The portion is not mapped already, # and it does not exist in the tileset. - tileset.add_tile(image_portion, TileSetPanel.TileEditingMode.AUTO) + tileset.add_tile(image_portion, self, TileSetPanel.TileEditingMode.AUTO) indices[i] = tileset.tiles.size() - 1 else: # If the portion is already mapped. if image_portion.get_data() == current_tile.image.get_data(): @@ -157,13 +162,13 @@ func handle_auto_editing_mode(i: int, image_portion: Image) -> void: # and the currently mapped tile still exists in the tileset. tileset.tiles[index_in_tileset].times_used += 1 indices[i] = index_in_tileset - tileset.unuse_tile_at_index(index) + tileset.unuse_tile_at_index(index, self) else: # Case 5: The portion 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 indices[i] = index_in_tileset - tileset.remove_tile_at_index(index) + tileset.remove_tile_at_index(index, self) # Re-index all indices that are after the deleted one. re_index_tiles_after_index(index) else: # If the portion does not exist in the tileset as a tile. @@ -171,14 +176,14 @@ func handle_auto_editing_mode(i: int, image_portion: Image) -> void: # Case 6: The portion 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) - tileset.add_tile(image_portion, TileSetPanel.TileEditingMode.AUTO) + tileset.unuse_tile_at_index(index, self) + tileset.add_tile(image_portion, self, TileSetPanel.TileEditingMode.AUTO) indices[i] = tileset.tiles.size() - 1 else: # Case 7: The portion 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) + tileset.replace_tile_at(image_portion, index, self) ## Re-indexes all [member indices] that are larger or equal to [param index], @@ -201,7 +206,6 @@ func update_cel_portion(tile_position: int) -> void: image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords) -## Unused, should delete. func update_cel_portions() -> void: for i in indices.size(): update_cel_portion(i) @@ -253,6 +257,15 @@ func _get_tile_editing_mode(undos: int) -> TileSetPanel.TileEditingMode: return tile_editing_mode +## If the tileset has been modified by another tile, make sure to also update it here. +func _on_tileset_updated(cel: CelTileMap) -> void: + if cel == self or not is_instance_valid(cel): + return + update_cel_portions() + Global.canvas.update_all_layers = true + Global.canvas.queue_redraw() + + # Overridden Methods: func update_texture(undo := false) -> void: var tile_editing_mode := TileSetPanel.tile_editing_mode diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd index 84531817c..0f3c664c3 100644 --- a/src/Classes/TileSetCustom.gd +++ b/src/Classes/TileSetCustom.gd @@ -1,7 +1,7 @@ class_name TileSetCustom extends RefCounted -signal updated +signal updated(cel: CelTileMap) var project: Project var name := "" @@ -36,34 +36,36 @@ func _init(_tile_size: Vector2i, _project: Project, _name := "") -> void: tiles.append(Tile.new(empty_image, TileSetPanel.tile_editing_mode)) -func add_tile(image: Image, edit_mode: TileSetPanel.TileEditingMode) -> void: +func add_tile(image: Image, cel: CelTileMap, edit_mode: TileSetPanel.TileEditingMode) -> void: var tile := Tile.new(image, edit_mode, project.undos) tiles.append(tile) - updated.emit() + updated.emit(cel) -func insert_tile(image: Image, position: int, edit_mode: TileSetPanel.TileEditingMode) -> void: +func insert_tile( + image: Image, position: int, cel: CelTileMap, edit_mode: TileSetPanel.TileEditingMode +) -> void: var tile := Tile.new(image, edit_mode, project.undos) tiles.insert(position, tile) - updated.emit() + updated.emit(cel) -func unuse_tile_at_index(index: int) -> bool: +func unuse_tile_at_index(index: int, cel: CelTileMap) -> bool: tiles[index].times_used -= 1 if tiles[index].can_be_removed(project): - remove_tile_at_index(index) + remove_tile_at_index(index, cel) return true return false -func remove_tile_at_index(index: int) -> void: +func remove_tile_at_index(index: int, cel: CelTileMap) -> void: tiles.remove_at(index) - updated.emit() + updated.emit(cel) -func replace_tile_at(new_tile: Image, index: int) -> void: +func replace_tile_at(new_tile: Image, index: int, cel: CelTileMap) -> void: tiles[index].image.copy_from(new_tile) - updated.emit() + updated.emit(cel) func find_tile(image: Image) -> int: @@ -74,12 +76,12 @@ func find_tile(image: Image) -> int: return -1 -func remove_unused_tiles() -> bool: +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(project): - remove_tile_at_index(i) + remove_tile_at_index(i, cel) tile_removed = true return tile_removed diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index ed3df48c5..e25ab7375 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -40,7 +40,7 @@ func _gui_input(event: InputEvent) -> void: get_viewport().set_input_as_handled() -func set_tileset(tileset: TileSetCustom, cel: BaseCel) -> void: +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): @@ -50,16 +50,16 @@ func set_tileset(tileset: TileSetCustom, cel: BaseCel) -> void: is_instance_valid(current_tileset) and not current_tileset.updated.is_connected(_update_tileset) ): - current_tileset.updated.connect(_update_tileset.bind(cel)) + current_tileset.updated.connect(_update_tileset) func _on_cel_switched() -> void: if Global.current_project.get_current_cel() is not CelTileMap: - set_tileset(null, null) + set_tileset(null) _clear_tile_buttons() return var cel := Global.current_project.get_current_cel() as CelTileMap - set_tileset(cel.tileset, cel) + set_tileset(cel.tileset) _update_tileset(cel) From 020be2056684fe77813b004e666e889ff462934b Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 26 Nov 2024 00:17:07 +0200 Subject: [PATCH 33/76] Preview tiles when using tools and draw tiles mode is enabled. --- src/Tools/BaseDraw.gd | 77 +++++++++++++++++------------- src/Tools/BaseShapeDrawer.gd | 2 +- src/Tools/BaseTool.gd | 52 +++++++++++--------- src/Tools/DesignTools/CurveTool.gd | 2 +- src/Tools/DesignTools/Eraser.gd | 2 +- src/Tools/DesignTools/LineTool.gd | 2 +- src/Tools/DesignTools/Pencil.gd | 2 +- src/Tools/DesignTools/Shading.gd | 2 +- src/UI/TilesPanel.gd | 18 +++++-- 9 files changed, 96 insertions(+), 63 deletions(-) diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index bb9da1a22..90c0ae9e1 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -1,3 +1,4 @@ +class_name BaseDrawTool extends BaseTool const IMAGE_BRUSHES := [Brushes.FILE, Brushes.RANDOM_FILE, Brushes.CUSTOM] @@ -42,6 +43,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 +162,40 @@ 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 is_placing_tiles(): + var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset + var tile_index := clampi(TileSetPanel.selected_tile_index, 0, tileset.tiles.size() - 1) + _brush_image.copy_from(tileset.tiles[tile_index].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) @@ -491,7 +499,12 @@ 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 is_placing_tiles(): + var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset + var grid_size := tileset.tile_size + snapped_position = _snap_to_grid_center(snapped_position, grid_size, -1) + draw_indicator_at(snapped_position, Vector2i.ZERO, color) if ( Global.current_project.has_selection and Global.current_project.tiles.mode == Tiles.MODE.NONE @@ -500,7 +513,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): @@ -508,12 +521,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 is_placing_tiles(): pos -= _brush_image.get_size() / 2 pos -= offset canvas.draw_texture(_brush_texture, pos) diff --git a/src/Tools/BaseShapeDrawer.gd b/src/Tools/BaseShapeDrawer.gd index 8789fccd2..0e7550092 100644 --- a/src/Tools/BaseShapeDrawer.gd +++ b/src/Tools/BaseShapeDrawer.gd @@ -1,4 +1,4 @@ -extends "res://src/Tools/BaseDraw.gd" +extends BaseDrawTool var _start := Vector2i.ZERO var _offset := Vector2i.ZERO diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index b93e5832f..2ab088871 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -80,6 +80,8 @@ func draw_end(_pos: Vector2i) -> void: 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 @@ -167,28 +169,7 @@ func snap_position(pos: Vector2) -> Vector2: pos = grid_point.floor() if Global.snap_to_rectangular_grid_center: - var grid_center := ( - pos.snapped(Global.grids[0].grid_size) + Vector2(Global.grids[0].grid_size / 2) - ) - 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() + pos = _snap_to_grid_center(pos, Global.grids[0].grid_size, snapping_distance) var snap_to := Vector2.INF if Global.snap_to_guides: @@ -301,6 +282,33 @@ func _get_closest_point_to_segment( return closest_point +func _snap_to_grid_center(pos: Vector2, grid_size: Vector2i, snapping_distance: float) -> Vector2: + var grid_center := pos.snapped(grid_size) + Vector2(grid_size / 2) + 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(-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: diff --git a/src/Tools/DesignTools/CurveTool.gd b/src/Tools/DesignTools/CurveTool.gd index c326d5b43..fc690b584 100644 --- a/src/Tools/DesignTools/CurveTool.gd +++ b/src/Tools/DesignTools/CurveTool.gd @@ -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. diff --git a/src/Tools/DesignTools/Eraser.gd b/src/Tools/DesignTools/Eraser.gd index 556993b6a..a2ea9b501 100644 --- a/src/Tools/DesignTools/Eraser.gd +++ b/src/Tools/DesignTools/Eraser.gd @@ -1,4 +1,4 @@ -extends "res://src/Tools/BaseDraw.gd" +extends BaseDrawTool var _last_position := Vector2.INF var _clear_image: Image diff --git a/src/Tools/DesignTools/LineTool.gd b/src/Tools/DesignTools/LineTool.gd index 7c97d0511..f856cd6ac 100644 --- a/src/Tools/DesignTools/LineTool.gd +++ b/src/Tools/DesignTools/LineTool.gd @@ -1,4 +1,4 @@ -extends "res://src/Tools/BaseDraw.gd" +extends BaseDrawTool var _original_pos := Vector2i.ZERO var _start := Vector2i.ZERO diff --git a/src/Tools/DesignTools/Pencil.gd b/src/Tools/DesignTools/Pencil.gd index 3a47716a1..825645b51 100644 --- a/src/Tools/DesignTools/Pencil.gd +++ b/src/Tools/DesignTools/Pencil.gd @@ -1,4 +1,4 @@ -extends "res://src/Tools/BaseDraw.gd" +extends BaseDrawTool var _prev_mode := false var _last_position := Vector2i(Vector2.INF) diff --git a/src/Tools/DesignTools/Shading.gd b/src/Tools/DesignTools/Shading.gd index 74faea35a..2413158ed 100644 --- a/src/Tools/DesignTools/Shading.gd +++ b/src/Tools/DesignTools/Shading.gd @@ -1,4 +1,4 @@ -extends "res://src/Tools/BaseDraw.gd" +extends BaseDrawTool enum ShadingMode { SIMPLE, HUE_SHIFTING, COLOR_REPLACE } enum LightenDarken { LIGHTEN, DARKEN } diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index e25ab7375..bbf909eb4 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -7,9 +7,15 @@ const TRANSPARENT_CHECKER := preload("res://src/UI/Nodes/TransparentChecker.tscn const MIN_BUTTON_SIZE := 36 const MAX_BUTTON_SIZE := 144 -static var placing_tiles := false +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 +static var selected_tile_index := 0: + set(value): + selected_tile_index = value + _call_update_brushes() var current_tileset: TileSetCustom var button_size := 36: set(value): @@ -29,7 +35,7 @@ var button_size := 36: func _ready() -> void: Tools.selected_tile_index_changed.connect(select_tile) Global.cel_switched.connect(_on_cel_switched) - Global.project_switched.connect(_on_cel_switched) + #Global.project_switched.connect(_on_cel_switched) func _gui_input(event: InputEvent) -> void: @@ -111,6 +117,12 @@ 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 From 281c2052900f5ac40d47d7cdf5dde0327f1544c2 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 26 Nov 2024 00:46:58 +0200 Subject: [PATCH 34/76] Automatically hide and show the tiles panel when the current cel is a tilemap cel --- src/UI/TilesPanel.tscn | 14 ++++++------- src/UI/TopMenuContainer/TopMenuContainer.gd | 22 ++++++++++++++------- src/UI/UI.gd | 11 ++++++++++- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/UI/TilesPanel.tscn b/src/UI/TilesPanel.tscn index e4abb0cda..6ffd270a1 100644 --- a/src/UI/TilesPanel.tscn +++ b/src/UI/TilesPanel.tscn @@ -20,23 +20,23 @@ layout_mode = 2 mouse_default_cursor_shape = 2 text = "Place tiles" -[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] +[node name="ModeButtonsContainer" type="HFlowContainer" parent="VBoxContainer"] layout_mode = 2 -[node name="Manual" type="CheckBox" parent="VBoxContainer/HBoxContainer"] +[node name="Manual" type="CheckBox" parent="VBoxContainer/ModeButtonsContainer"] layout_mode = 2 mouse_default_cursor_shape = 2 button_group = SubResource("ButtonGroup_uxnt0") text = "Manual" -[node name="Auto" type="CheckBox" parent="VBoxContainer/HBoxContainer"] +[node name="Auto" type="CheckBox" parent="VBoxContainer/ModeButtonsContainer"] layout_mode = 2 mouse_default_cursor_shape = 2 button_pressed = true button_group = SubResource("ButtonGroup_uxnt0") text = "Auto" -[node name="Stack" type="CheckBox" parent="VBoxContainer/HBoxContainer"] +[node name="Stack" type="CheckBox" parent="VBoxContainer/ModeButtonsContainer"] layout_mode = 2 mouse_default_cursor_shape = 2 button_group = SubResource("ButtonGroup_uxnt0") @@ -53,6 +53,6 @@ size_flags_horizontal = 3 size_flags_vertical = 3 [connection signal="toggled" from="VBoxContainer/PlaceTiles" to="." method="_on_place_tiles_toggled"] -[connection signal="toggled" from="VBoxContainer/HBoxContainer/Manual" to="." method="_on_manual_toggled"] -[connection signal="toggled" from="VBoxContainer/HBoxContainer/Auto" to="." method="_on_auto_toggled"] -[connection signal="toggled" from="VBoxContainer/HBoxContainer/Stack" to="." method="_on_stack_toggled"] +[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"] diff --git a/src/UI/TopMenuContainer/TopMenuContainer.gd b/src/UI/TopMenuContainer/TopMenuContainer.gd index de29ebf89..1579ba342 100644 --- a/src/UI/TopMenuContainer/TopMenuContainer.gd +++ b/src/UI/TopMenuContainer/TopMenuContainer.gd @@ -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) @@ -763,9 +767,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 +792,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 +872,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 name := ui_elements[i].name + if name == "Main Canvas" or 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 diff --git a/src/UI/UI.gd b/src/UI/UI.gd index c95234f6d..7ba4a9c9e 100644 --- a/src/UI/UI.gd +++ b/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 From 5e5789752d5b7ef01714c4c8461773783efc43f2 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 26 Nov 2024 00:59:16 +0200 Subject: [PATCH 35/76] Prevent users from editing tileset name and size if they choose to not create a new tileset --- src/UI/Timeline/NewTileMapLayerDialog.gd | 6 ++++++ src/UI/Timeline/NewTileMapLayerDialog.tscn | 1 + 2 files changed, 7 insertions(+) diff --git a/src/UI/Timeline/NewTileMapLayerDialog.gd b/src/UI/Timeline/NewTileMapLayerDialog.gd index cdb1f71fc..c6b790e7c 100644 --- a/src/UI/Timeline/NewTileMapLayerDialog.gd +++ b/src/UI/Timeline/NewTileMapLayerDialog.gd @@ -37,3 +37,9 @@ func _on_about_to_popup() -> void: if not tileset.name.is_empty(): item_string += ": " + tileset.name tileset_option_button.add_item(tr("Tileset" + item_string)) + _on_tileset_option_button_item_selected(tileset_option_button.selected) + + +func _on_tileset_option_button_item_selected(index: int) -> void: + tileset_name_line_edit.editable = index == 0 + tile_size_slider.editable = tileset_name_line_edit.editable diff --git a/src/UI/Timeline/NewTileMapLayerDialog.tscn b/src/UI/Timeline/NewTileMapLayerDialog.tscn index 46fb9dde5..2d20c8d36 100644 --- a/src/UI/Timeline/NewTileMapLayerDialog.tscn +++ b/src/UI/Timeline/NewTileMapLayerDialog.tscn @@ -67,3 +67,4 @@ 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"] From 62882cb8b112f83be936798c732ef9f71259e2b1 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 26 Nov 2024 01:17:41 +0200 Subject: [PATCH 36/76] Load images as tilesets --- src/Autoload/OpenSave.gd | 19 +++++++++++++++++++ src/UI/Dialogs/ImportPreviewDialog.gd | 17 ++++++++++++++--- src/UI/Dialogs/ImportPreviewDialog.tscn | 3 +-- src/UI/Timeline/NewTileMapLayerDialog.gd | 4 ++++ 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index 80c00df04..edd169ccc 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -835,6 +835,25 @@ 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: + 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, project, path.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) + ) + cropped_image.convert(project.get_image_format()) + tileset.add_tile(cropped_image, null, 2) + 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 diff --git a/src/UI/Dialogs/ImportPreviewDialog.gd b/src/UI/Dialogs/ImportPreviewDialog.gd index eeb60f354..e11750c1f 100644 --- a/src/UI/Dialogs/ImportPreviewDialog.gd +++ b/src/UI/Dialogs/ImportPreviewDialog.gd @@ -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,10 @@ 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: + OpenSave.open_image_as_tileset( + path, image, spritesheet_horizontal, spritesheet_vertical + ) else: if current_import_option in custom_importers.keys(): @@ -250,7 +256,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 +308,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 +515,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(): diff --git a/src/UI/Dialogs/ImportPreviewDialog.tscn b/src/UI/Dialogs/ImportPreviewDialog.tscn index 9ac4d01bb..327f60a7c 100644 --- a/src/UI/Dialogs/ImportPreviewDialog.tscn +++ b/src/UI/Dialogs/ImportPreviewDialog.tscn @@ -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" diff --git a/src/UI/Timeline/NewTileMapLayerDialog.gd b/src/UI/Timeline/NewTileMapLayerDialog.gd index c6b790e7c..497840d19 100644 --- a/src/UI/Timeline/NewTileMapLayerDialog.gd +++ b/src/UI/Timeline/NewTileMapLayerDialog.gd @@ -41,5 +41,9 @@ func _on_about_to_popup() -> void: 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 From 74a40be7f74dde1efc9a9b116f4dd7044b710309 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 26 Nov 2024 01:46:24 +0200 Subject: [PATCH 37/76] Add smart tileset importing --- src/Autoload/OpenSave.gd | 28 +++++++++++++++++++++++++-- src/UI/Dialogs/ImportPreviewDialog.gd | 13 ++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index edd169ccc..bb4a17cad 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -838,22 +838,46 @@ func import_reference_image_from_image(image: Image) -> void: 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, project, path.get_file()) + var tileset := TileSetCustom.new(tile_size, project, 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) ) - cropped_image.convert(project.get_image_format()) + @warning_ignore("int_as_enum_without_cast") tileset.add_tile(cropped_image, null, 2) 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, project, 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, 2) + 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 diff --git a/src/UI/Dialogs/ImportPreviewDialog.gd b/src/UI/Dialogs/ImportPreviewDialog.gd index e11750c1f..5f4155900 100644 --- a/src/UI/Dialogs/ImportPreviewDialog.gd +++ b/src/UI/Dialogs/ImportPreviewDialog.gd @@ -210,9 +210,16 @@ func _on_ImportPreviewDialog_confirmed() -> void: var dir := DirAccess.open(path.get_base_dir()) dir.copy(path, Global.home_data_directory.path_join(location)) elif current_import_option == ImageImportOptions.TILESET: - OpenSave.open_image_as_tileset( - path, image, spritesheet_horizontal, spritesheet_vertical - ) + 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(): From 35f78cf02ceb34ba1106df977326c0ae6803955f Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 26 Nov 2024 03:36:53 +0200 Subject: [PATCH 38/76] Refactor CelTileMap to eventually support alternative tiles Such as rotated and flipped tiles --- src/Classes/Cels/CelTileMap.gd | 82 ++++++++++++++++++--------- src/Tools/BaseTool.gd | 2 +- src/Tools/UtilityTools/ColorPicker.gd | 2 +- src/UI/Canvas/TileModeIndices.gd | 2 +- 4 files changed, 57 insertions(+), 31 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 57a295167..60981468a 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -8,12 +8,10 @@ var tileset: TileSetCustom: tileset.updated.disconnect(_on_tileset_updated) tileset = value if is_instance_valid(tileset): - indices_x = ceili(float(get_image().get_width()) / tileset.tile_size.x) - indices_y = ceili(float(get_image().get_height()) / tileset.tile_size.y) - indices.resize(indices_x * indices_y) + _resize_indices(get_image().get_size()) if not tileset.updated.is_connected(_on_tileset_updated): tileset.updated.connect(_on_tileset_updated) -var indices := PackedInt32Array() +var indices: Array[Tile] var indices_x: int var indices_y: int ## Dictionary of [int] and an [Array] of [bool] ([member TileSetPanel.placing_tiles]) @@ -27,6 +25,22 @@ var undo_redo_modes := {} var editing_images := {} +class Tile: + var index := 0 + var flip_h := false + var flip_v := false + var 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, _opacity := 1.0) -> void: super._init(_image, _opacity) tileset = _tileset @@ -35,7 +49,7 @@ func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> v func set_index(tile_position: int, index: int) -> void: index = clampi(index, 0, tileset.tiles.size() - 1) tileset.tiles[index].times_used += 1 - indices[tile_position] = index + indices[tile_position].index = index update_cel_portion(tile_position) Global.canvas.queue_redraw() @@ -52,11 +66,11 @@ func update_tileset(undo: bool) -> void: var coords := get_tile_coords(i) var rect := Rect2i(coords, tileset.tile_size) var image_portion := image.get_region(rect) - var index := indices[i] + var index := indices[i].index if index >= tileset.tiles.size(): printerr("Tile at position ", i, ", mapped to ", index, " is out of bounds!") index = 0 - indices[i] = 0 + indices[i].index = 0 var current_tile := tileset.tiles[index] if tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: if image_portion.is_invisible(): @@ -65,7 +79,7 @@ func update_tileset(undo: bool) -> void: # If the tileset is empty, only then add a new tile. if tileset.tiles.size() <= 1: tileset.add_tile(image_portion, self, tile_editing_mode) - indices[i] = tileset.tiles.size() - 1 + indices[i].index = tileset.tiles.size() - 1 continue if image_portion.get_data() != current_tile.image.get_data(): tileset.replace_tile_at(image_portion, index, self) @@ -78,12 +92,12 @@ func update_tileset(undo: bool) -> void: for j in range(1, tileset.tiles.size()): var tile := tileset.tiles[j] if image_portion.get_data() == tile.image.get_data(): - indices[i] = j + indices[i].index = j found_tile = true break if not found_tile: tileset.add_tile(image_portion, self, tile_editing_mode) - indices[i] = tileset.tiles.size() - 1 + indices[i].index = tileset.tiles.size() - 1 if undo: var tile_removed := tileset.remove_unused_tiles(self) if tile_removed: @@ -127,11 +141,11 @@ func update_tileset(undo: bool) -> void: ## 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) -> void: - var index := indices[i] + var index := indices[i].index var current_tile := tileset.tiles[index] if image_portion.is_invisible(): # Case 0: The portion is transparent. - indices[i] = 0 + indices[i].index = 0 if index > 0: # Case 0.5: The portion is transparent and mapped to a tile. var is_removed := tileset.unuse_tile_at_index(index, self) @@ -145,12 +159,12 @@ func handle_auto_editing_mode(i: int, image_portion: Image) -> void: # Case 1: The portion is not mapped already, # and it exists in the tileset as a tile. tileset.tiles[index_in_tileset].times_used += 1 - indices[i] = index_in_tileset + indices[i].index = index_in_tileset else: # Case 2: The portion is not mapped already, # and it does not exist in the tileset. tileset.add_tile(image_portion, self, TileSetPanel.TileEditingMode.AUTO) - indices[i] = tileset.tiles.size() - 1 + indices[i].index = tileset.tiles.size() - 1 else: # If the portion is already mapped. if image_portion.get_data() == current_tile.image.get_data(): # Case 3: The portion is mapped and it did not change. @@ -161,13 +175,13 @@ func handle_auto_editing_mode(i: int, image_portion: Image) -> void: # Case 4: The portion 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 - indices[i] = index_in_tileset + indices[i].index = index_in_tileset tileset.unuse_tile_at_index(index, self) else: # Case 5: The portion 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 - indices[i] = index_in_tileset + indices[i].index = index_in_tileset tileset.remove_tile_at_index(index, self) # Re-index all indices that are after the deleted one. re_index_tiles_after_index(index) @@ -178,7 +192,7 @@ func handle_auto_editing_mode(i: int, image_portion: Image) -> void: # and the currently mapped tile still exists in the tileset. tileset.unuse_tile_at_index(index, self) tileset.add_tile(image_portion, self, TileSetPanel.TileEditingMode.AUTO) - indices[i] = tileset.tiles.size() - 1 + indices[i].index = tileset.tiles.size() - 1 else: # Case 7: The portion is mapped and it does not # exist in the tileset as a tile, @@ -190,16 +204,16 @@ func handle_auto_editing_mode(i: int, image_portion: Image) -> void: ## by reducing their value by one. func re_index_tiles_after_index(index: int) -> void: for i in indices.size(): - var tmp_index := indices[i] + var tmp_index := indices[i].index if tmp_index >= index: - indices[i] -= 1 + indices[i].index -= 1 func update_cel_portion(tile_position: int) -> void: var coords := get_tile_coords(tile_position) var rect := Rect2i(coords, tileset.tile_size) var image_portion := image.get_region(rect) - var index := indices[tile_position] + var index := indices[tile_position].index var current_tile := tileset.tiles[index] if image_portion.get_data() != current_tile.image.get_data(): var tile_size := current_tile.image.get_size() @@ -235,15 +249,23 @@ func re_index_all_tiles() -> void: var rect := Rect2i(coords, tileset.tile_size) var image_portion := image.get_region(rect) if image_portion.is_invisible(): - indices[i] = 0 + indices[i].index = 0 continue for j in range(1, tileset.tiles.size()): var tile := tileset.tiles[j] if image_portion.get_data() == tile.image.get_data(): - indices[i] = j + indices[i].index = j break +func _resize_indices(new_size: Vector2i) -> void: + indices_x = ceili(float(new_size.x) / tileset.tile_size.x) + indices_y = ceili(float(new_size.y) / tileset.tile_size.y) + indices.resize(indices_x * indices_y) + for i in indices.size(): + indices[i] = Tile.new() + + func _is_redo() -> bool: return Global.control.redone @@ -274,7 +296,7 @@ func update_texture(undo := false) -> void: return for i in indices.size(): - var index := indices[i] + var index := indices[i].index var coords := get_tile_coords(i) var rect := Rect2i(coords, tileset.tile_size) var image_portion := image.get_region(rect) @@ -299,9 +321,7 @@ func update_texture(undo := false) -> void: func size_changed(new_size: Vector2i) -> void: - indices_x = ceili(float(new_size.x) / tileset.tile_size.x) - indices_y = ceili(float(new_size.y) / tileset.tile_size.y) - indices.resize(indices_x * indices_y) + _resize_indices(new_size) re_index_all_tiles() @@ -319,13 +339,19 @@ func on_undo_redo(undo: bool) -> void: func serialize() -> Dictionary: var dict := super.serialize() - dict["tile_indices"] = indices + var tile_indices := [] + tile_indices.resize(indices.size()) + for i in tile_indices.size(): + tile_indices[i] = indices[i].serialize() + dict["tile_indices"] = tile_indices return dict func deserialize(dict: Dictionary) -> void: super.deserialize(dict) - indices = dict.get("tile_indices") + var tile_indices = dict.get("tile_indices") + for i in tile_indices.size(): + indices[i].deserialize(tile_indices[i]) func get_class_name() -> String: diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index 2ab088871..f54b73c7f 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -365,7 +365,7 @@ func _pick_color(pos: Vector2i) -> void: if is_placing_tiles(): var tile_position := get_tile_position(pos) var cel := Global.current_project.get_current_cel() as CelTileMap - Tools.selected_tile_index_changed.emit(cel.indices[tile_position]) + Tools.selected_tile_index_changed.emit(cel.indices[tile_position].index) return var image := Image.new() image.copy_from(_get_draw_image()) diff --git a/src/Tools/UtilityTools/ColorPicker.gd b/src/Tools/UtilityTools/ColorPicker.gd index d449fc0b1..11966bd5a 100644 --- a/src/Tools/UtilityTools/ColorPicker.gd +++ b/src/Tools/UtilityTools/ColorPicker.gd @@ -68,7 +68,7 @@ func _pick_color(pos: Vector2i) -> void: if is_placing_tiles(): var tile_position := get_tile_position(pos) var cel := Global.current_project.get_current_cel() as CelTileMap - Tools.selected_tile_index_changed.emit(cel.indices[tile_position]) + Tools.selected_tile_index_changed.emit(cel.indices[tile_position].index) return var image := Image.new() image.copy_from(_get_draw_image()) diff --git a/src/UI/Canvas/TileModeIndices.gd b/src/UI/Canvas/TileModeIndices.gd index 40c632071..2a130bd08 100644 --- a/src/UI/Canvas/TileModeIndices.gd +++ b/src/UI/Canvas/TileModeIndices.gd @@ -13,5 +13,5 @@ func _draw() -> void: for i in tilemap_cel.indices.size(): var pos := tilemap_cel.get_tile_coords(i) pos.y += tilemap_cel.tileset.tile_size.y - var text := str(tilemap_cel.indices[i]) + var text := str(tilemap_cel.indices[i].index) draw_string(Themes.get_font(), pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10) From 8be3a1a54f47b71b41b3ae583b504b2bf7fa682e Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 26 Nov 2024 03:58:47 +0200 Subject: [PATCH 39/76] Add logic for checking if two tiles are equal with transformations applied to them There is currently no exposed way to apply transformations to tiles. --- src/Classes/Cels/CelTileMap.gd | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 60981468a..562e8f12e 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -29,6 +29,8 @@ class Tile: var index := 0 var flip_h := false var flip_v := false + ## If [code]true[/code], the tile is rotated 90 degrees counter-clockwise, + ## and then flipped vertically. var transpose := false func serialize() -> Dictionary: @@ -81,7 +83,7 @@ func update_tileset(undo: bool) -> void: tileset.add_tile(image_portion, self, tile_editing_mode) indices[i].index = tileset.tiles.size() - 1 continue - if image_portion.get_data() != current_tile.image.get_data(): + 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) @@ -91,7 +93,7 @@ func update_tileset(undo: bool) -> void: var found_tile := false for j in range(1, tileset.tiles.size()): var tile := tileset.tiles[j] - if image_portion.get_data() == tile.image.get_data(): + if tiles_equal(i, image_portion, tile.image): indices[i].index = j found_tile = true break @@ -166,7 +168,7 @@ func handle_auto_editing_mode(i: int, image_portion: Image) -> void: tileset.add_tile(image_portion, self, TileSetPanel.TileEditingMode.AUTO) indices[i].index = tileset.tiles.size() - 1 else: # If the portion is already mapped. - if image_portion.get_data() == current_tile.image.get_data(): + if tiles_equal(i, image_portion, current_tile.image): # Case 3: The portion is mapped and it did not change. # Do nothing and move on to the next portion. return @@ -215,7 +217,7 @@ func update_cel_portion(tile_position: int) -> void: var image_portion := image.get_region(rect) var index := indices[tile_position].index var current_tile := tileset.tiles[index] - if image_portion.get_data() != current_tile.image.get_data(): + if not tiles_equal(tile_position, image_portion, current_tile.image): var tile_size := current_tile.image.get_size() image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords) @@ -243,6 +245,24 @@ func get_tile_position(coords: Vector2i) -> int: return x + y +func tiles_equal(portion_index: int, image_portion: Image, tile_image: Image) -> bool: + var tile_data := indices[portion_index] + var final_image_portion := Image.new() + final_image_portion.copy_from(image_portion) + if tile_data.flip_h: + final_image_portion.flip_x() + if tile_data.flip_v: + final_image_portion.flip_y() + if tile_data.transpose: + var tmp_image := Image.new() + tmp_image.copy_from(final_image_portion) + tmp_image.rotate_90(COUNTERCLOCKWISE) + final_image_portion.blit_rect( + tmp_image, Rect2i(Vector2i.ZERO, tmp_image.get_size()), Vector2i.ZERO + ) + return final_image_portion.get_data() == tile_image.get_data() + + func re_index_all_tiles() -> void: for i in indices.size(): var coords := get_tile_coords(i) @@ -253,7 +273,7 @@ func re_index_all_tiles() -> void: continue for j in range(1, tileset.tiles.size()): var tile := tileset.tiles[j] - if image_portion.get_data() == tile.image.get_data(): + if tiles_equal(i, image_portion, tile.image): indices[i].index = j break @@ -311,11 +331,11 @@ func update_texture(undo := false) -> void: if i == editing_portion: editing_images[index] = [i, image_portion] var editing_image := editing_images[index][1] as Image - if editing_image.get_data() != image_portion.get_data(): + if not tiles_equal(i, image_portion, editing_image): var tile_size := image_portion.get_size() image.blit_rect(editing_image, Rect2i(Vector2i.ZERO, tile_size), coords) else: - if image_portion.get_data() != current_tile.image.get_data(): + if not tiles_equal(i, image_portion, current_tile.image): editing_images[index] = [i, image_portion] super.update_texture(undo) From 2ccb9dd6f7748aed2de15de40dd74bfd6b704988 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:51:26 +0200 Subject: [PATCH 40/76] Support tile transformation, no undo/redo yet --- src/Classes/Cels/CelTileMap.gd | 82 ++++++++++++++++++++++++-------- src/Tools/BaseDraw.gd | 12 ++++- src/UI/Canvas/TileModeIndices.gd | 5 +- src/UI/TilesPanel.gd | 25 +++++++++- src/UI/TilesPanel.tscn | 24 ++++++++++ 5 files changed, 122 insertions(+), 26 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 562e8f12e..ac776c12b 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -33,6 +33,16 @@ class Tile: ## and then flipped vertically. 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 serialize() -> Dictionary: return {"index": index, "flip_h": flip_h, "flip_v": flip_v, "transpose": transpose} @@ -52,6 +62,9 @@ func set_index(tile_position: int, index: int) -> void: index = clampi(index, 0, tileset.tiles.size() - 1) tileset.tiles[index].times_used += 1 indices[tile_position].index = index + indices[tile_position].flip_h = TileSetPanel.is_flipped_h + indices[tile_position].flip_v = TileSetPanel.is_flipped_v + indices[tile_position].transpose = TileSetPanel.is_transposed update_cel_portion(tile_position) Global.canvas.queue_redraw() @@ -215,11 +228,15 @@ func update_cel_portion(tile_position: int) -> void: var coords := get_tile_coords(tile_position) var rect := Rect2i(coords, tileset.tile_size) var image_portion := image.get_region(rect) - var index := indices[tile_position].index - var current_tile := tileset.tiles[index] - if not tiles_equal(tile_position, image_portion, current_tile.image): - var tile_size := current_tile.image.get_size() - image.blit_rect(current_tile.image, Rect2i(Vector2i.ZERO, tile_size), coords) + var tile_data := indices[tile_position] + var index := tile_data.index + var current_tile := tileset.tiles[index].image + var transformed_tile := transform_tile( + current_tile, tile_data.flip_h, tile_data.flip_v, tile_data.transpose + ) + if not tiles_equal(tile_position, image_portion, transformed_tile): + var tile_size := transformed_tile.get_size() + image.blit_rect(transformed_tile, Rect2i(Vector2i.ZERO, tile_size), coords) func update_cel_portions() -> void: @@ -247,20 +264,33 @@ func get_tile_position(coords: Vector2i) -> int: func tiles_equal(portion_index: int, image_portion: Image, tile_image: Image) -> bool: var tile_data := indices[portion_index] - var final_image_portion := Image.new() - final_image_portion.copy_from(image_portion) - if tile_data.flip_h: - final_image_portion.flip_x() - if tile_data.flip_v: - final_image_portion.flip_y() - if tile_data.transpose: + var final_image_portion := transform_tile( + tile_image, tile_data.flip_h, tile_data.flip_v, tile_data.transpose + ) + return image_portion.get_data() == final_image_portion.get_data() + + +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 flip_h: + transformed_tile.flip_x() + if flip_v: + transformed_tile.flip_y() + if transpose: var tmp_image := Image.new() - tmp_image.copy_from(final_image_portion) - tmp_image.rotate_90(COUNTERCLOCKWISE) - final_image_portion.blit_rect( + 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, tmp_image.get_size()), Vector2i.ZERO ) - return final_image_portion.get_data() == tile_image.get_data() + transformed_tile.flip_y() + return transformed_tile func re_index_all_tiles() -> void: @@ -316,7 +346,8 @@ func update_texture(undo := false) -> void: return for i in indices.size(): - var index := indices[i].index + var tile_data := indices[i] + var index := tile_data.index var coords := get_tile_coords(i) var rect := Rect2i(coords, tileset.tile_size) var image_portion := image.get_region(rect) @@ -329,14 +360,23 @@ func update_texture(undo := false) -> void: if editing_images.has(index): var editing_portion := editing_images[index][0] as int if i == editing_portion: - editing_images[index] = [i, image_portion] + var transformed_image := transform_tile( + image_portion, tile_data.flip_h, tile_data.flip_v, tile_data.transpose, true + ) + editing_images[index] = [i, transformed_image] var editing_image := editing_images[index][1] as Image - if not tiles_equal(i, image_portion, editing_image): + var transformed_editing_image := transform_tile( + editing_image, tile_data.flip_h, tile_data.flip_v, tile_data.transpose + ) + if not image_portion.get_data() == transformed_editing_image.get_data(): var tile_size := image_portion.get_size() - image.blit_rect(editing_image, Rect2i(Vector2i.ZERO, tile_size), coords) + image.blit_rect(transformed_editing_image, Rect2i(Vector2i.ZERO, tile_size), coords) else: if not tiles_equal(i, image_portion, current_tile.image): - editing_images[index] = [i, image_portion] + var transformed_image := transform_tile( + image_portion, tile_data.flip_h, tile_data.flip_v, tile_data.transpose, true + ) + editing_images[index] = [i, transformed_image] super.update_texture(undo) diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index 90c0ae9e1..64d3f973b 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -163,9 +163,17 @@ func update_config() -> void: func update_brush() -> void: $Brush/BrushSize.suffix = "px" # Assume we are using default brushes if is_placing_tiles(): - var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset + 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) - _brush_image.copy_from(tileset.tiles[tile_index].image) + 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: diff --git a/src/UI/Canvas/TileModeIndices.gd b/src/UI/Canvas/TileModeIndices.gd index 2a130bd08..6f36d9b2e 100644 --- a/src/UI/Canvas/TileModeIndices.gd +++ b/src/UI/Canvas/TileModeIndices.gd @@ -13,5 +13,6 @@ func _draw() -> void: for i in tilemap_cel.indices.size(): var pos := tilemap_cel.get_tile_coords(i) pos.y += tilemap_cel.tileset.tile_size.y - var text := str(tilemap_cel.indices[i].index) - draw_string(Themes.get_font(), pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10) + var tile_data := tilemap_cel.indices[i] + var text := tile_data.to_string() + draw_multiline_string(Themes.get_font(), pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10) diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index bbf909eb4..fabe67658 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -16,6 +16,18 @@ 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): @@ -35,7 +47,6 @@ var button_size := 36: func _ready() -> void: Tools.selected_tile_index_changed.connect(select_tile) Global.cel_switched.connect(_on_cel_switched) - #Global.project_switched.connect(_on_cel_switched) func _gui_input(event: InputEvent) -> void: @@ -151,3 +162,15 @@ func _on_auto_toggled(toggled_on: bool) -> void: func _on_stack_toggled(toggled_on: bool) -> void: if toggled_on: tile_editing_mode = TileEditingMode.STACK + + +func _on_flip_horizontal_button_toggled(toggled_on: bool) -> void: + is_flipped_h = toggled_on + + +func _on_flip_vertical_button_toggled(toggled_on: bool) -> void: + is_flipped_v = toggled_on + + +func _on_transpose_button_toggled(toggled_on: bool) -> void: + is_transposed = toggled_on diff --git a/src/UI/TilesPanel.tscn b/src/UI/TilesPanel.tscn index 6ffd270a1..5e1c4dcf9 100644 --- a/src/UI/TilesPanel.tscn +++ b/src/UI/TilesPanel.tscn @@ -20,6 +20,27 @@ layout_mode = 2 mouse_default_cursor_shape = 2 text = "Place tiles" +[node name="TransformButtonsContainer" type="HFlowContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="FlipHorizontalButton" type="Button" parent="VBoxContainer/TransformButtonsContainer"] +layout_mode = 2 +mouse_default_cursor_shape = 2 +toggle_mode = true +text = "H" + +[node name="FlipVerticalButton" type="Button" parent="VBoxContainer/TransformButtonsContainer"] +layout_mode = 2 +mouse_default_cursor_shape = 2 +toggle_mode = true +text = "V" + +[node name="TransposeButton" type="Button" parent="VBoxContainer/TransformButtonsContainer"] +layout_mode = 2 +mouse_default_cursor_shape = 2 +toggle_mode = true +text = "T" + [node name="ModeButtonsContainer" type="HFlowContainer" parent="VBoxContainer"] layout_mode = 2 @@ -53,6 +74,9 @@ size_flags_horizontal = 3 size_flags_vertical = 3 [connection signal="toggled" from="VBoxContainer/PlaceTiles" to="." method="_on_place_tiles_toggled"] +[connection signal="toggled" from="VBoxContainer/TransformButtonsContainer/FlipHorizontalButton" to="." method="_on_flip_horizontal_button_toggled"] +[connection signal="toggled" from="VBoxContainer/TransformButtonsContainer/FlipVerticalButton" to="." method="_on_flip_vertical_button_toggled"] +[connection signal="toggled" from="VBoxContainer/TransformButtonsContainer/TransposeButton" to="." method="_on_transpose_button_toggled"] [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"] From ad919a2a10e046861035d75ec68f7b63ed1dc36c Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:11:56 +0200 Subject: [PATCH 41/76] Fix manual mode when the tilemap is empty. --- src/Classes/Cels/CelTileMap.gd | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index ac776c12b..8e7638420 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -352,10 +352,11 @@ func update_texture(undo := false) -> void: var rect := Rect2i(coords, tileset.tile_size) var image_portion := image.get_region(rect) var current_tile := tileset.tiles[index] - if index == 0 and 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) + 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 From a10a0a9bdadedc54d6674a27a7713cb1bb84100b Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:41:24 +0200 Subject: [PATCH 42/76] Experimental undo redo for tileset tiles --- src/Classes/Cels/CelTileMap.gd | 20 ++++++++++++++++++++ src/Tools/BaseDraw.gd | 8 ++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 8e7638420..73af2e2ba 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -398,6 +398,26 @@ func on_undo_redo(undo: bool) -> void: update_tileset(undo) +func serialize_undo_data() -> Dictionary: + var dict := {} + var indices_serialized := [] + indices_serialized.resize(indices.size()) + for i in indices.size(): + indices_serialized[i] = indices[i].serialize() + dict["tiles_data"] = indices_serialized + return dict + + +func deserialize_undo_data(dict: Dictionary, undo_redo: UndoRedo, undo: bool) -> void: + var tiles_data = dict["tiles_data"] + for i in tiles_data.size(): + var tile_data: Dictionary = tiles_data[i] + if undo: + undo_redo.add_undo_method(indices[i].deserialize.bind(tile_data)) + else: + undo_redo.add_do_method(indices[i].deserialize.bind(tile_data)) + + func serialize() -> Dictionary: var dict := super.serialize() var tile_indices := [] diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index 64d3f973b..f333de2cc 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -282,6 +282,12 @@ func commit_undo() -> void: project.undos += 1 Global.undo_redo_compress_images(redo_data, _undo_data, project) + for cel in redo_data: + if cel is CelTileMap: + (cel as CelTileMap).deserialize_undo_data(redo_data[cel], project.undo_redo, false) + for cel in _undo_data: + if cel is CelTileMap: + (cel as CelTileMap).deserialize_undo_data(_undo_data[cel], project.undo_redo, true) 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() @@ -762,6 +768,8 @@ func _get_undo_data() -> Dictionary: 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() return data From 6be273d098644d52bd0a6cd850d9f44f55bdc9b0 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:10:52 +0200 Subject: [PATCH 43/76] Add documentation for LayerTileMap and TileSetCustom, along with a class description for CelTileMap --- src/Classes/Cels/CelTileMap.gd | 13 +++++++-- src/Classes/Layers/LayerTileMap.gd | 8 ++++++ src/Classes/TileSetCustom.gd | 46 ++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 73af2e2ba..44c85fc47 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -1,6 +1,15 @@ 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. + var tileset: TileSetCustom: set(value): if is_instance_valid(tileset): @@ -124,8 +133,8 @@ func update_tileset(undo: bool) -> void: ## [br] ## 0.5) Portion is transparent and mapped. ## Set its index to 0 and unuse the mapped tile. -## If the mapped tile is removed, educe the index of all portions that have indices greater or equal -## than the existing tile's index. +## If the mapped tile is removed, reduce the index of all portions that have +## indices greater or equal than the existing tile's index. ## [br] ## 1) Portion not mapped, exists in the tileset. ## Map the portion to the existing tile and increase its times_used by one. diff --git a/src/Classes/Layers/LayerTileMap.gd b/src/Classes/Layers/LayerTileMap.gd index d60bc8b17..eace980cd 100644 --- a/src/Classes/Layers/LayerTileMap.gd +++ b/src/Classes/Layers/LayerTileMap.gd @@ -1,6 +1,14 @@ 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. + +## 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 diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd index 0f3c664c3..9a051838c 100644 --- a/src/Classes/TileSetCustom.gd +++ b/src/Classes/TileSetCustom.gd @@ -1,18 +1,34 @@ class_name TileSetCustom extends RefCounted +## A Tileset is a collection of tiles, used by [LayerTileMap]s and [CelTileMap]s. +## The tileset contains the [Project] that it is being used by, 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) +## The [Project] the tileset is being used by. var project: Project +## 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] = [] +## 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 mode that was used when this tile was added to the tileset. var mode_added: TileSetPanel.TileEditingMode + ## The amount of tiles this tile is being used in tilemaps. var times_used := 1 + ## The step number of undo/redo when this tile was added to the tileset. var undo_step_added := 0 func _init( @@ -22,6 +38,12 @@ class Tile: mode_added = _mode_added undo_step_added = _undo_step_added + ## A method that checks if the tile should be removed from the tileset. + ## Returns [code]true[/code] if the current undo step is less than [member undo_step_added], + ## which essentially means that the tile always gets removed if the user undos to the point + ## the tile was added to the tileset. + ## Otherwise, it returns [code]true[/code] if [member mode_added] is not equal to + ## [enum TileSetPanel.TileEditingMode.STACK] and the amount of [member times_used] is 0. func can_be_removed(project: Project) -> bool: if project.undos < undo_step_added: return true @@ -36,12 +58,18 @@ func _init(_tile_size: Vector2i, _project: Project, _name := "") -> void: tiles.append(Tile.new(empty_image, TileSetPanel.tile_editing_mode)) +## 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, edit_mode: TileSetPanel.TileEditingMode) -> void: var tile := Tile.new(image, edit_mode, project.undos) tiles.append(tile) updated.emit(cel) +## 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, edit_mode: TileSetPanel.TileEditingMode ) -> void: @@ -50,6 +78,12 @@ func insert_tile( updated.emit(cel) +## 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(project): @@ -58,16 +92,21 @@ func unuse_tile_at_index(index: int, cel: CelTileMap) -> bool: 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) +## 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) +## 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] @@ -76,6 +115,9 @@ func find_tile(image: Image) -> int: 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): @@ -86,10 +128,14 @@ func remove_unused_tiles(cel: CelTileMap) -> bool: return tile_removed +## 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")) From e870679869023aeabd2c4f3f4ac111d740057398 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:08:53 +0200 Subject: [PATCH 44/76] Rename some variables and methods in CelTileMap --- src/Classes/Cels/CelTileMap.gd | 473 +++++++++++++------------- src/Classes/Layers/LayerTileMap.gd | 1 + src/Tools/BaseDraw.gd | 2 +- src/Tools/BaseTool.gd | 8 +- src/Tools/UtilityTools/ColorPicker.gd | 4 +- src/UI/Canvas/TileModeIndices.gd | 6 +- 6 files changed, 248 insertions(+), 246 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 44c85fc47..f04095370 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -10,6 +10,7 @@ extends PixelCel ## 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: set(value): if is_instance_valid(tileset): @@ -17,12 +18,12 @@ var tileset: TileSetCustom: tileset.updated.disconnect(_on_tileset_updated) tileset = value if is_instance_valid(tileset): - _resize_indices(get_image().get_size()) + _resize_cells(get_image().get_size()) if not tileset.updated.is_connected(_on_tileset_updated): tileset.updated.connect(_on_tileset_updated) -var indices: Array[Tile] -var indices_x: int -var indices_y: int +var cells: Array[Cell] +var horizontal_cells: int +var vertical_cells: int ## Dictionary of [int] and an [Array] of [bool] ([member TileSetPanel.placing_tiles]) ## and [enum TileSetPanel.TileEditingMode]. var undo_redo_modes := {} @@ -34,7 +35,7 @@ var undo_redo_modes := {} var editing_images := {} -class Tile: +class Cell: var index := 0 var flip_h := false var flip_v := false @@ -67,214 +68,39 @@ func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> v tileset = _tileset -func set_index(tile_position: int, index: int) -> void: +func set_index(cell_position: int, index: int) -> void: index = clampi(index, 0, tileset.tiles.size() - 1) tileset.tiles[index].times_used += 1 - indices[tile_position].index = index - indices[tile_position].flip_h = TileSetPanel.is_flipped_h - indices[tile_position].flip_v = TileSetPanel.is_flipped_v - indices[tile_position].transpose = TileSetPanel.is_transposed - update_cel_portion(tile_position) + 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() -func update_tileset(undo: bool) -> void: - editing_images.clear() - var undos := tileset.project.undos - if not undo and not _is_redo(): - undo_redo_modes[undos] = [TileSetPanel.placing_tiles, TileSetPanel.tile_editing_mode] - if undo: - undos += 1 - var tile_editing_mode := _get_tile_editing_mode(undos) - for i in indices.size(): - var coords := get_tile_coords(i) - var rect := Rect2i(coords, tileset.tile_size) - var image_portion := image.get_region(rect) - var index := indices[i].index - if index >= tileset.tiles.size(): - printerr("Tile at position ", i, ", mapped to ", index, " is out of bounds!") - index = 0 - indices[i].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, tile_editing_mode) - indices[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) - 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): - indices[i].index = j - found_tile = true - break - if not found_tile: - tileset.add_tile(image_portion, self, tile_editing_mode) - indices[i].index = tileset.tiles.size() - 1 - if undo: - var tile_removed := tileset.remove_unused_tiles(self) - if tile_removed: - re_index_all_tiles() - - -## Cases:[br] -## 0) Portion is transparent. Set its index to 0. -## [br] -## 0.5) Portion 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 portions that have -## indices greater or equal than the existing tile's index. -## [br] -## 1) Portion not mapped, exists in the tileset. -## Map the portion to the existing tile and increase its times_used by one. -## [br] -## 2) Portion not mapped, does not exist in the tileset. -## Add the portion as a tile in the tileset, set its index to be the tileset's tile size - 1. -## [br] -## 3) Portion mapped, tile did not change. Do nothing. -## [br] -## 4) Portion mapped, exists in the tileset. -## The mapped tile still exists in the tileset. -## Map the portion to the existing tile, increase its times_used by one, -## and reduce the previously mapped tile's times_used by 1. -## [br] -## 5) Portion mapped, exists in the tileset. -## The mapped tile does not exist in the tileset anymore. -## Map the portion to the existing tile and increase its times_used by one. -## Remove the previously mapped tile, -## and reduce the index of all portions that have indices greater or equal -## than the existing tile's index. -## [br] -## 6) Portion mapped, does not exist in the tileset. -## The mapped tile still exists in the tileset. -## Add the portion 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) Portion 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) -> void: - var index := indices[i].index - var current_tile := tileset.tiles[index] - if image_portion.is_invisible(): - # Case 0: The portion is transparent. - indices[i].index = 0 - if index > 0: - # Case 0.5: The portion 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_tiles_after_index(index) - return - var index_in_tileset := tileset.find_tile(image_portion) - if index == 0: # If the portion is not mapped to a tile. - if index_in_tileset > -1: - # Case 1: The portion is not mapped already, - # and it exists in the tileset as a tile. - tileset.tiles[index_in_tileset].times_used += 1 - indices[i].index = index_in_tileset - else: - # Case 2: The portion is not mapped already, - # and it does not exist in the tileset. - tileset.add_tile(image_portion, self, TileSetPanel.TileEditingMode.AUTO) - indices[i].index = tileset.tiles.size() - 1 - else: # If the portion is already mapped. - if tiles_equal(i, image_portion, current_tile.image): - # Case 3: The portion is mapped and it did not change. - # Do nothing and move on to the next portion. - return - if index_in_tileset > -1: # If the portion exists in the tileset as a tile. - if current_tile.times_used > 1: - # Case 4: The portion 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 - indices[i].index = index_in_tileset - tileset.unuse_tile_at_index(index, self) - else: - # Case 5: The portion 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 - indices[i].index = index_in_tileset - tileset.remove_tile_at_index(index, self) - # Re-index all indices that are after the deleted one. - re_index_tiles_after_index(index) - else: # If the portion does not exist in the tileset as a tile. - if current_tile.times_used > 1: - # Case 6: The portion 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, TileSetPanel.TileEditingMode.AUTO) - indices[i].index = tileset.tiles.size() - 1 - else: - # Case 7: The portion 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) - - -## Re-indexes all [member indices] that are larger or equal to [param index], -## by reducing their value by one. -func re_index_tiles_after_index(index: int) -> void: - for i in indices.size(): - var tmp_index := indices[i].index - if tmp_index >= index: - indices[i].index -= 1 - - -func update_cel_portion(tile_position: int) -> void: - var coords := get_tile_coords(tile_position) - var rect := Rect2i(coords, tileset.tile_size) - var image_portion := image.get_region(rect) - var tile_data := indices[tile_position] - var index := tile_data.index - var current_tile := tileset.tiles[index].image - var transformed_tile := transform_tile( - current_tile, tile_data.flip_h, tile_data.flip_v, tile_data.transpose - ) - if not tiles_equal(tile_position, image_portion, transformed_tile): - var tile_size := transformed_tile.get_size() - image.blit_rect(transformed_tile, Rect2i(Vector2i.ZERO, tile_size), coords) - - -func update_cel_portions() -> void: - for i in indices.size(): - update_cel_portion(i) - - -func get_tile_coords(portion_position: int) -> Vector2i: - var x_coord := float(tileset.tile_size.x) * (portion_position % indices_x) +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) * (portion_position / indices_x) + var y_coord := float(tileset.tile_size.y) * (cell_position / horizontal_cells) return Vector2i(x_coord, y_coord) -func get_tile_position(coords: Vector2i) -> int: +func get_cell_position(coords: Vector2i) -> int: @warning_ignore("integer_division") var x := coords.x / tileset.tile_size.x - x = clampi(x, 0, indices_x - 1) + x = clampi(x, 0, horizontal_cells - 1) @warning_ignore("integer_division") var y := coords.y / tileset.tile_size.y - y = clampi(y, 0, indices_y - 1) - y *= indices_x + y = clampi(y, 0, vertical_cells - 1) + y *= horizontal_cells return x + y -func tiles_equal(portion_index: int, image_portion: Image, tile_image: Image) -> bool: - var tile_data := indices[portion_index] +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, tile_data.flip_h, tile_data.flip_v, tile_data.transpose + tile_image, cell_data.flip_h, cell_data.flip_v, cell_data.transpose ) return image_portion.get_data() == final_image_portion.get_data() @@ -302,27 +128,202 @@ func transform_tile( return transformed_tile -func re_index_all_tiles() -> void: - for i in indices.size(): - var coords := get_tile_coords(i) +func update_tileset(undo: bool) -> void: + editing_images.clear() + var undos := tileset.project.undos + if not undo and not _is_redo(): + undo_redo_modes[undos] = [TileSetPanel.placing_tiles, TileSetPanel.tile_editing_mode] + if undo: + undos += 1 + var tile_editing_mode := _get_tile_editing_mode(undos) + 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) + var index := cells[i].index + if index >= tileset.tiles.size(): + printerr("Cell at position ", i + 1, ", mapped to ", index, " is out of bounds!") + index = 0 + cells[i].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, tile_editing_mode) + 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) + 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): + cells[i].index = j + found_tile = true + break + if not found_tile: + tileset.add_tile(image_portion, self, tile_editing_mode) + cells[i].index = tileset.tiles.size() - 1 + if undo: + var tile_removed := tileset.remove_unused_tiles(self) + if tile_removed: + _re_index_all_cells() + + +## 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) -> void: + var index := cells[i].index + var current_tile := tileset.tiles[index] + if image_portion.is_invisible(): + # Case 0: The cell is transparent. + cells[i].index = 0 + 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, TileSetPanel.TileEditingMode.AUTO) + 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, TileSetPanel.TileEditingMode.AUTO) + 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) + + +## 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 + + +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 + 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 not tiles_equal(cell_position, image_portion, transformed_tile): + var tile_size := transformed_tile.get_size() + image.blit_rect(transformed_tile, Rect2i(Vector2i.ZERO, tile_size), coords) + + +func _update_cel_portions() -> void: + for i in cells.size(): + _update_cell(i) + + +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(): - indices[i].index = 0 + cells[i].index = 0 continue for j in range(1, tileset.tiles.size()): var tile := tileset.tiles[j] if tiles_equal(i, image_portion, tile.image): - indices[i].index = j + cells[i].index = j break -func _resize_indices(new_size: Vector2i) -> void: - indices_x = ceili(float(new_size.x) / tileset.tile_size.x) - indices_y = ceili(float(new_size.y) / tileset.tile_size.y) - indices.resize(indices_x * indices_y) - for i in indices.size(): - indices[i] = Tile.new() +func _resize_cells(new_size: Vector2i) -> 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(): + cells[i] = Cell.new() func _is_redo() -> bool: @@ -342,7 +343,7 @@ func _get_tile_editing_mode(undos: int) -> TileSetPanel.TileEditingMode: func _on_tileset_updated(cel: CelTileMap) -> void: if cel == self or not is_instance_valid(cel): return - update_cel_portions() + _update_cel_portions() Global.canvas.update_all_layers = true Global.canvas.queue_redraw() @@ -354,10 +355,10 @@ func update_texture(undo := false) -> void: super.update_texture(undo) return - for i in indices.size(): - var tile_data := indices[i] - var index := tile_data.index - var coords := get_tile_coords(i) + for i in cells.size(): + var cell_data := cells[i] + var index := cell_data.index + 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] @@ -371,12 +372,12 @@ func update_texture(undo := false) -> void: var editing_portion := editing_images[index][0] as int if i == editing_portion: var transformed_image := transform_tile( - image_portion, tile_data.flip_h, tile_data.flip_v, tile_data.transpose, true + 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, tile_data.flip_h, tile_data.flip_v, tile_data.transpose + 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() @@ -384,15 +385,15 @@ func update_texture(undo := false) -> void: else: if not tiles_equal(i, image_portion, current_tile.image): var transformed_image := transform_tile( - image_portion, tile_data.flip_h, tile_data.flip_v, tile_data.transpose, true + 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 size_changed(new_size: Vector2i) -> void: - _resize_indices(new_size) - re_index_all_tiles() + _resize_cells(new_size) + _re_index_all_cells() func on_undo_redo(undo: bool) -> void: @@ -402,46 +403,46 @@ func on_undo_redo(undo: bool) -> void: if (undo or _is_redo()) and undo_redo_modes.has(undos): var placing_tiles: bool = undo_redo_modes[undos][0] if placing_tiles: - re_index_all_tiles() + _re_index_all_cells() return update_tileset(undo) func serialize_undo_data() -> Dictionary: var dict := {} - var indices_serialized := [] - indices_serialized.resize(indices.size()) - for i in indices.size(): - indices_serialized[i] = indices[i].serialize() - dict["tiles_data"] = indices_serialized + var cells_serialized := [] + cells_serialized.resize(cells.size()) + for i in cells.size(): + cells_serialized[i] = cells[i].serialize() + dict["cells_data"] = cells_serialized return dict func deserialize_undo_data(dict: Dictionary, undo_redo: UndoRedo, undo: bool) -> void: - var tiles_data = dict["tiles_data"] - for i in tiles_data.size(): - var tile_data: Dictionary = tiles_data[i] + var cells_data = dict["cells_data"] + for i in cells_data.size(): + var cell_data: Dictionary = cells_data[i] if undo: - undo_redo.add_undo_method(indices[i].deserialize.bind(tile_data)) + undo_redo.add_undo_method(cells[i].deserialize.bind(cell_data)) else: - undo_redo.add_do_method(indices[i].deserialize.bind(tile_data)) + undo_redo.add_do_method(cells[i].deserialize.bind(cell_data)) func serialize() -> Dictionary: var dict := super.serialize() - var tile_indices := [] - tile_indices.resize(indices.size()) - for i in tile_indices.size(): - tile_indices[i] = indices[i].serialize() - dict["tile_indices"] = tile_indices + 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 tile_indices = dict.get("tile_indices") - for i in tile_indices.size(): - indices[i].deserialize(tile_indices[i]) + var cell_indices = dict.get("cell_indices") + for i in cell_indices.size(): + cells[i].deserialize(cell_indices[i]) func get_class_name() -> String: diff --git a/src/Classes/Layers/LayerTileMap.gd b/src/Classes/Layers/LayerTileMap.gd index eace980cd..c70ba91af 100644 --- a/src/Classes/Layers/LayerTileMap.gd +++ b/src/Classes/Layers/LayerTileMap.gd @@ -5,6 +5,7 @@ extends PixelLayer ## 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. diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index f333de2cc..22f6b592c 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -329,7 +329,7 @@ func draw_tile(pos: Vector2i, tile_index: int) -> void: if Global.current_project.get_current_cel() is not CelTileMap: return pos = Global.current_project.tiles.get_canon_position(pos) - var tile_position := get_tile_position(pos) + var tile_position := get_cell_position(pos) var cel := Global.current_project.get_current_cel() as CelTileMap cel.set_index(tile_position, tile_index) diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index f54b73c7f..33975e8e6 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -85,12 +85,12 @@ func is_placing_tiles() -> bool: return Global.current_project.get_current_cel() is CelTileMap and TileSetPanel.placing_tiles -func get_tile_position(pos: Vector2i) -> int: +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_tile_position(pos) + tile_pos = cel.get_cell_position(pos) return tile_pos @@ -363,9 +363,9 @@ func _pick_color(pos: Vector2i) -> void: if pos.x < 0 or pos.y < 0: return if is_placing_tiles(): - var tile_position := get_tile_position(pos) + var tile_position := get_cell_position(pos) var cel := Global.current_project.get_current_cel() as CelTileMap - Tools.selected_tile_index_changed.emit(cel.indices[tile_position].index) + Tools.selected_tile_index_changed.emit(cel.cells[tile_position].index) return var image := Image.new() image.copy_from(_get_draw_image()) diff --git a/src/Tools/UtilityTools/ColorPicker.gd b/src/Tools/UtilityTools/ColorPicker.gd index 11966bd5a..7117b3ecf 100644 --- a/src/Tools/UtilityTools/ColorPicker.gd +++ b/src/Tools/UtilityTools/ColorPicker.gd @@ -66,9 +66,9 @@ func _pick_color(pos: Vector2i) -> void: if pos.x < 0 or pos.y < 0: return if is_placing_tiles(): - var tile_position := get_tile_position(pos) + var tile_position := get_cell_position(pos) var cel := Global.current_project.get_current_cel() as CelTileMap - Tools.selected_tile_index_changed.emit(cel.indices[tile_position].index) + Tools.selected_tile_index_changed.emit(cel.cells[tile_position].index) return var image := Image.new() image.copy_from(_get_draw_image()) diff --git a/src/UI/Canvas/TileModeIndices.gd b/src/UI/Canvas/TileModeIndices.gd index 6f36d9b2e..600308c06 100644 --- a/src/UI/Canvas/TileModeIndices.gd +++ b/src/UI/Canvas/TileModeIndices.gd @@ -10,9 +10,9 @@ func _draw() -> void: var current_cel := Global.current_project.get_current_cel() if current_cel is CelTileMap and Input.is_action_pressed("ctrl"): var tilemap_cel := current_cel as CelTileMap - for i in tilemap_cel.indices.size(): - var pos := tilemap_cel.get_tile_coords(i) + for i in tilemap_cel.cells.size(): + var pos := tilemap_cel.get_cell_coords_in_image(i) pos.y += tilemap_cel.tileset.tile_size.y - var tile_data := tilemap_cel.indices[i] + var tile_data := tilemap_cel.cells[i] var text := tile_data.to_string() draw_multiline_string(Themes.get_font(), pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10) From 8d1652dc09c196ce69794fe770993b47e618951f Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:37:19 +0200 Subject: [PATCH 45/76] Write some documentation in CelTIleMap WIP --- src/Classes/Cels/CelTileMap.gd | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index f04095370..135c8755a 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -21,8 +21,13 @@ var tileset: TileSetCustom: _resize_cells(get_image().get_size()) if not tileset.updated.is_connected(_on_tileset_updated): tileset.updated.connect(_on_tileset_updated) + +## 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 an [Array] of [bool] ([member TileSetPanel.placing_tiles]) ## and [enum TileSetPanel.TileEditingMode]. @@ -35,12 +40,16 @@ var undo_redo_modes := {} 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. + ## and then flipped vertically in this cell. var transpose := false func _to_string() -> String: @@ -68,6 +77,8 @@ func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> v tileset = _tileset +## 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) tileset.tiles[index].times_used += 1 @@ -79,6 +90,9 @@ func set_index(cell_position: int, index: int) -> void: 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") @@ -86,6 +100,9 @@ func get_cell_coords_in_image(cell_position: int) -> Vector2i: 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 @@ -97,6 +114,8 @@ func get_cell_position(coords: Vector2i) -> int: return x + y +## 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( From 903ea5134a9de1d13ec41e26f04efabb4a3266f4 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:27:30 +0200 Subject: [PATCH 46/76] Make undo/redo store tilemap cell indices and tileset tiles Fixes issues with cases 0.5 and 5 of auto mode, and should be a better system overall. Only works with BaseDraw tools, needs to be applied everywhere as well. --- src/Autoload/Global.gd | 2 - src/Autoload/OpenSave.gd | 10 ++-- src/Classes/Cels/BaseCel.gd | 4 -- src/Classes/Cels/CelTileMap.gd | 70 +++++++----------------- src/Classes/Project.gd | 2 +- src/Classes/TileSetCustom.gd | 65 +++++++++++----------- src/Tools/BaseDraw.gd | 3 + src/UI/Timeline/NewTileMapLayerDialog.gd | 2 +- 8 files changed, 65 insertions(+), 93 deletions(-) diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index 4b373734f..e55f4fef1 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -984,7 +984,6 @@ func undo_or_redo( if action_name == "Scale": cel.size_changed(project.size) canvas.update_texture(layer_index, frame_index, project, undo) - cel.on_undo_redo(undo) else: for i in project.frames.size(): for j in project.layers.size(): @@ -992,7 +991,6 @@ func undo_or_redo( if action_name == "Scale": cel.size_changed(project.size) canvas.update_texture(j, i, project, undo) - cel.on_undo_redo(undo) canvas.selection.queue_redraw() if action_name == "Scale": diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index bb4a17cad..28ff6d169 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -269,7 +269,7 @@ func open_pxo_file(path: String, is_backup := false, replace_empty := true) -> v 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, 2) + tileset.add_tile(image, null) zip_reader.close() new_project.export_directory_path = path.get_base_dir() @@ -844,14 +844,14 @@ func open_image_as_tileset( 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, project, path.get_basename().get_file()) + 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, 2) + tileset.add_tile(cropped_image, null) project.tilesets.append(tileset) @@ -866,7 +866,7 @@ func open_image_as_tileset_smart( 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, project, path.get_basename().get_file()) + 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( @@ -874,7 +874,7 @@ func open_image_as_tileset_smart( ) cropped_image.blit_rect(image, rect, offset) @warning_ignore("int_as_enum_without_cast") - tileset.add_tile(cropped_image, null, 2) + tileset.add_tile(cropped_image, null) project.tilesets.append(tileset) diff --git a/src/Classes/Cels/BaseCel.gd b/src/Classes/Cels/BaseCel.gd index ec9b9089c..4b01ec7d8 100644 --- a/src/Classes/Cels/BaseCel.gd +++ b/src/Classes/Cels/BaseCel.gd @@ -77,10 +77,6 @@ func update_texture(_undo := false) -> void: cel.texture_changed.emit() -func on_undo_redo(_undo: bool) -> void: - pass - - ## Returns a curated [Dictionary] containing the cel data. func serialize() -> Dictionary: var dict := {"opacity": opacity, "z_index": z_index} diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 135c8755a..8682c3a78 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -29,9 +29,6 @@ var cells: Array[Cell] var horizontal_cells: int ## The amount of vertical cells. var vertical_cells: int -## Dictionary of [int] and an [Array] of [bool] ([member TileSetPanel.placing_tiles]) -## and [enum TileSetPanel.TileEditingMode]. -var undo_redo_modes := {} ## 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 @@ -147,14 +144,9 @@ func transform_tile( return transformed_tile -func update_tileset(undo: bool) -> void: +func update_tileset() -> void: editing_images.clear() - var undos := tileset.project.undos - if not undo and not _is_redo(): - undo_redo_modes[undos] = [TileSetPanel.placing_tiles, TileSetPanel.tile_editing_mode] - if undo: - undos += 1 - var tile_editing_mode := _get_tile_editing_mode(undos) + var tile_editing_mode := TileSetPanel.tile_editing_mode for i in cells.size(): var coords := get_cell_coords_in_image(i) var rect := Rect2i(coords, tileset.tile_size) @@ -171,7 +163,7 @@ func update_tileset(undo: bool) -> void: 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, tile_editing_mode) + tileset.add_tile(image_portion, self) cells[i].index = tileset.tiles.size() - 1 continue if not tiles_equal(i, image_portion, current_tile.image): @@ -189,12 +181,8 @@ func update_tileset(undo: bool) -> void: found_tile = true break if not found_tile: - tileset.add_tile(image_portion, self, tile_editing_mode) + tileset.add_tile(image_portion, self) cells[i].index = tileset.tiles.size() - 1 - if undo: - var tile_removed := tileset.remove_unused_tiles(self) - if tile_removed: - _re_index_all_cells() ## Cases:[br] @@ -256,7 +244,7 @@ func _handle_auto_editing_mode(i: int, image_portion: Image) -> void: else: # Case 2: The cell is not mapped already, # and it does not exist in the tileset. - tileset.add_tile(image_portion, self, TileSetPanel.TileEditingMode.AUTO) + 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): @@ -284,7 +272,7 @@ func _handle_auto_editing_mode(i: int, image_portion: Image) -> void: # 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, TileSetPanel.TileEditingMode.AUTO) + tileset.add_tile(image_portion, self) cells[i].index = tileset.tiles.size() - 1 else: # Case 7: The cell is mapped and it does not @@ -349,15 +337,6 @@ func _is_redo() -> bool: return Global.control.redone -func _get_tile_editing_mode(undos: int) -> TileSetPanel.TileEditingMode: - var tile_editing_mode: TileSetPanel.TileEditingMode - if undo_redo_modes.has(undos): - tile_editing_mode = undo_redo_modes[undos][1] - else: - tile_editing_mode = TileSetPanel.tile_editing_mode - return tile_editing_mode - - ## If the tileset has been modified by another tile, make sure to also update it here. func _on_tileset_updated(cel: CelTileMap) -> void: if cel == self or not is_instance_valid(cel): @@ -415,36 +394,29 @@ func size_changed(new_size: Vector2i) -> void: _re_index_all_cells() -func on_undo_redo(undo: bool) -> void: - var undos := tileset.project.undos - if undo: - undos += 1 - if (undo or _is_redo()) and undo_redo_modes.has(undos): - var placing_tiles: bool = undo_redo_modes[undos][0] - if placing_tiles: - _re_index_all_cells() - return - update_tileset(undo) - - func serialize_undo_data() -> Dictionary: var dict := {} - var cells_serialized := [] - cells_serialized.resize(cells.size()) - for i in cells.size(): - cells_serialized[i] = cells[i].serialize() - dict["cells_data"] = cells_serialized + 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() return dict func deserialize_undo_data(dict: Dictionary, undo_redo: UndoRedo, undo: bool) -> void: - var cells_data = dict["cells_data"] - for i in cells_data.size(): - var cell_data: Dictionary = cells_data[i] - if undo: + var cell_indices = dict["cell_indices"] + if undo: + for i in cell_indices.size(): + var cell_data: Dictionary = cell_indices[i] undo_redo.add_undo_method(cells[i].deserialize.bind(cell_data)) - else: + undo_redo.add_undo_method(tileset.deserialize_undo_data.bind(dict.get("tileset"), self)) + else: + for i in cell_indices.size(): + var cell_data: Dictionary = cell_indices[i] undo_redo.add_do_method(cells[i].deserialize.bind(cell_data)) + undo_redo.add_do_method(tileset.deserialize_undo_data.bind(dict.get("tileset"), self)) func serialize() -> Dictionary: diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index 512bf1310..317369523 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -352,7 +352,7 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces 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, self) + var tileset := TileSetCustom.new(tile_size) tileset.deserialize(saved_tileset) tilesets.append(tileset) if dict.has("frames") and dict.has("layers"): diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd index 9a051838c..49c89b714 100644 --- a/src/Classes/TileSetCustom.gd +++ b/src/Classes/TileSetCustom.gd @@ -10,8 +10,6 @@ extends RefCounted ## The [CelTileMap] that the changes are coming from is referenced in the [param cel] parameter. signal updated(cel: CelTileMap) -## The [Project] the tileset is being used by. -var project: Project ## The tileset's name. var name := "" ## The size of each individual tile. @@ -24,45 +22,30 @@ var tiles: Array[Tile] = [] class Tile: ## The [Image] tile itself. var image: Image - ## The mode that was used when this tile was added to the tileset. - var mode_added: TileSetPanel.TileEditingMode ## The amount of tiles this tile is being used in tilemaps. var times_used := 1 - ## The step number of undo/redo when this tile was added to the tileset. - var undo_step_added := 0 - func _init( - _image: Image, _mode_added: TileSetPanel.TileEditingMode, _undo_step_added := 0 - ) -> void: + func _init(_image: Image) -> void: image = _image - mode_added = _mode_added - undo_step_added = _undo_step_added ## A method that checks if the tile should be removed from the tileset. - ## Returns [code]true[/code] if the current undo step is less than [member undo_step_added], - ## which essentially means that the tile always gets removed if the user undos to the point - ## the tile was added to the tileset. - ## Otherwise, it returns [code]true[/code] if [member mode_added] is not equal to - ## [enum TileSetPanel.TileEditingMode.STACK] and the amount of [member times_used] is 0. - func can_be_removed(project: Project) -> bool: - if project.undos < undo_step_added: - return true - return mode_added != TileSetPanel.TileEditingMode.STACK and times_used <= 0 + ## 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, _project: Project, _name := "") -> void: +func _init(_tile_size: Vector2i, _name := "") -> void: tile_size = _tile_size - project = _project name = _name var empty_image := Image.create_empty(tile_size.x, tile_size.y, false, Image.FORMAT_RGBA8) - tiles.append(Tile.new(empty_image, TileSetPanel.tile_editing_mode)) + 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, edit_mode: TileSetPanel.TileEditingMode) -> void: - var tile := Tile.new(image, edit_mode, project.undos) +func add_tile(image: Image, cel: CelTileMap) -> void: + var tile := Tile.new(image) tiles.append(tile) updated.emit(cel) @@ -70,10 +53,8 @@ func add_tile(image: Image, cel: CelTileMap, edit_mode: TileSetPanel.TileEditing ## 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, edit_mode: TileSetPanel.TileEditingMode -) -> void: - var tile := Tile.new(image, edit_mode, project.undos) +func insert_tile(image: Image, position: int, cel: CelTileMap) -> void: + var tile := Tile.new(image) tiles.insert(position, tile) updated.emit(cel) @@ -86,7 +67,7 @@ func insert_tile( ## 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(project): + if tiles[index].can_be_removed(): remove_tile_at_index(index, cel) return true return false @@ -122,7 +103,7 @@ 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(project): + if tile.can_be_removed(): remove_tile_at_index(i, cel) tile_removed = true return tile_removed @@ -139,3 +120,25 @@ func serialize() -> Dictionary: func deserialize(dict: Dictionary) -> void: name = dict.get("name", name) tile_size = str_to_var("Vector2i" + dict.get("tile_size")) + + +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 + + +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) diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index 22f6b592c..5fa176d47 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -272,6 +272,9 @@ func prepare_undo(action: String) -> void: func commit_undo() -> void: + for cel in _undo_data: + if cel is CelTileMap: + (cel as CelTileMap).update_tileset() var redo_data := _get_undo_data() var project := Global.current_project var frame := -1 diff --git a/src/UI/Timeline/NewTileMapLayerDialog.gd b/src/UI/Timeline/NewTileMapLayerDialog.gd index 497840d19..0c4020d03 100644 --- a/src/UI/Timeline/NewTileMapLayerDialog.gd +++ b/src/UI/Timeline/NewTileMapLayerDialog.gd @@ -14,7 +14,7 @@ func _on_confirmed() -> void: var tile_size := tile_size_slider.value var tileset: TileSetCustom if tileset_option_button.selected == 0: - tileset = TileSetCustom.new(tile_size, project, tileset_name) + tileset = TileSetCustom.new(tile_size, tileset_name) else: tileset = project.tilesets[tileset_option_button.selected - 1] var layer := LayerTileMap.new(project, tileset, layer_name) From 13070b6244f9f8b3e698d9d99fa3010adc81a0c0 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:38:57 +0200 Subject: [PATCH 47/76] Remove transformations from cells when using auto or stack mode --- src/Classes/Cels/CelTileMap.gd | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 8682c3a78..b21733765 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -59,6 +59,11 @@ class Cell: 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} @@ -177,12 +182,15 @@ func update_tileset() -> void: 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 + 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() ## Cases:[br] @@ -227,6 +235,7 @@ func _handle_auto_editing_mode(i: int, image_portion: Image) -> void: if image_portion.is_invisible(): # Case 0: The cell is transparent. 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) @@ -279,6 +288,7 @@ func _handle_auto_editing_mode(i: int, image_portion: Image) -> void: # 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], From 8ab71490cf4ff1d7e0b37ac88e5b19040cace374 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:39:09 +0200 Subject: [PATCH 48/76] Fix variable name shadowing in TopMenuContainer --- src/UI/TopMenuContainer/TopMenuContainer.gd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UI/TopMenuContainer/TopMenuContainer.gd b/src/UI/TopMenuContainer/TopMenuContainer.gd index 1579ba342..622921701 100644 --- a/src/UI/TopMenuContainer/TopMenuContainer.gd +++ b/src/UI/TopMenuContainer/TopMenuContainer.gd @@ -873,8 +873,8 @@ func _toggle_show_mouse_guides() -> void: func _toggle_zen_mode() -> void: for i in ui_elements.size(): var index := panels_submenu.get_item_index(i) - var name := ui_elements[i].name - if name == "Main Canvas" or name == "Tiles": + var panel_name := ui_elements[i].name + if panel_name == "Main Canvas" or panel_name == "Tiles": continue if !panels_submenu.is_item_checked(index): continue From 5425275e9cf927128a1d48aa070912e7eb755d00 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:55:59 +0200 Subject: [PATCH 49/76] Some improvements to TileModeIndices --- src/UI/Canvas/TileModeIndices.gd | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/UI/Canvas/TileModeIndices.gd b/src/UI/Canvas/TileModeIndices.gd index 600308c06..c8b782b3e 100644 --- a/src/UI/Canvas/TileModeIndices.gd +++ b/src/UI/Canvas/TileModeIndices.gd @@ -1,5 +1,7 @@ extends Node2D +const FONT_SIZE := 16 + func _input(event: InputEvent) -> void: if event.is_action("ctrl"): @@ -8,11 +10,15 @@ func _input(event: InputEvent) -> void: 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 tile_data := tilemap_cel.cells[i] var text := tile_data.to_string() - draw_multiline_string(Themes.get_font(), pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10) + draw_multiline_string(Themes.get_font(), pos * 2, text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE) + draw_set_transform(position, rotation, scale) From 50b4a8428f01cf07bc4a4f93529ff44cbd5cf270 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Thu, 28 Nov 2024 17:32:32 +0200 Subject: [PATCH 50/76] Fix issues with transposed tiles --- src/Classes/Cels/CelTileMap.gd | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index b21733765..feee70742 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -131,10 +131,6 @@ func transform_tile( ) -> Image: var transformed_tile := Image.new() transformed_tile.copy_from(tile_image) - if flip_h: - transformed_tile.flip_x() - if flip_v: - transformed_tile.flip_y() if transpose: var tmp_image := Image.new() tmp_image.copy_from(transformed_tile) @@ -143,8 +139,15 @@ func transform_tile( else: tmp_image.rotate_90(COUNTERCLOCKWISE) transformed_tile.blit_rect( - tmp_image, Rect2i(Vector2i.ZERO, tmp_image.get_size()), Vector2i.ZERO + 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 @@ -361,6 +364,7 @@ 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(): From 29b281f7baf1f8c2c064cd39d2c5e264a7fbcd02 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Thu, 28 Nov 2024 17:32:39 +0200 Subject: [PATCH 51/76] Format TileModeIndices --- src/UI/Canvas/TileModeIndices.gd | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/UI/Canvas/TileModeIndices.gd b/src/UI/Canvas/TileModeIndices.gd index c8b782b3e..46048187d 100644 --- a/src/UI/Canvas/TileModeIndices.gd +++ b/src/UI/Canvas/TileModeIndices.gd @@ -20,5 +20,7 @@ func _draw() -> void: 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_multiline_string( + Themes.get_font(), pos * 2, text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + ) draw_set_transform(position, rotation, scale) From d1bcab4bf95242587e83874e244972359771491c Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Thu, 28 Nov 2024 17:55:55 +0200 Subject: [PATCH 52/76] Fix placing tiles not working when switching to indexed mode --- src/Classes/Cels/CelTileMap.gd | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index feee70742..acc98a89c 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -316,6 +316,7 @@ func _update_cell(cell_position: int) -> void: if not tiles_equal(cell_position, image_portion, transformed_tile): var tile_size := transformed_tile.get_size() image.blit_rect(transformed_tile, Rect2i(Vector2i.ZERO, tile_size), coords) + image.convert_rgb_to_indexed() func _update_cel_portions() -> void: From b3a429466dd8b7909c383be9bb06eb8db350ebf3 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Thu, 28 Nov 2024 18:07:30 +0200 Subject: [PATCH 53/76] Fixed bugs when placing a transformed tile over a non-transformed tile --- src/Classes/Cels/CelTileMap.gd | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index acc98a89c..a1aa4f231 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -83,8 +83,12 @@ func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> v ## 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) - tileset.tiles[index].times_used += 1 - cells[cell_position].index = index + var previous_index := cells[cell_position].index + if previous_index != index: + if previous_index > 0: + 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 @@ -313,7 +317,7 @@ func _update_cell(cell_position: int) -> void: var transformed_tile := transform_tile( current_tile, cell_data.flip_h, cell_data.flip_v, cell_data.transpose ) - if not tiles_equal(cell_position, image_portion, transformed_tile): + 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() From 9e86492bfe51bb6dc2e5987647750bcb09d6a27d Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:28:49 +0200 Subject: [PATCH 54/76] Continue with the undo/redo rewrite Works everywhere except image resizing, replacing cels and merging layers --- src/Classes/ImageEffect.gd | 15 +++---- src/Classes/Project.gd | 26 +++++++++++ src/Tools/BaseDraw.gd | 22 ++-------- src/Tools/BaseTool.gd | 11 +++++ src/Tools/DesignTools/Bucket.gd | 14 +++--- src/Tools/UtilityTools/Move.gd | 11 ++--- src/Tools/UtilityTools/Text.gd | 9 ++-- src/UI/Canvas/Selection.gd | 17 ++++---- .../Dialogs/ImageEffects/FlipImageDialog.gd | 10 ++--- .../LayerEffects/LayerEffectsSettings.gd | 43 ++++++++++--------- src/UI/TopMenuContainer/TopMenuContainer.gd | 12 ++++-- 11 files changed, 105 insertions(+), 85 deletions(-) diff --git a/src/Classes/ImageEffect.gd b/src/Classes/ImageEffect.gd index f50aef337..d1cf3c12e 100644 --- a/src/Classes/ImageEffect.gd +++ b/src/Classes/ImageEffect.gd @@ -157,10 +157,11 @@ func display_animate_dialog() -> void: func _commit_undo(action: String, undo_data: Dictionary, project: Project) -> void: + project.update_tilesets(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 diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index 317369523..35143700c 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -651,6 +651,26 @@ func get_all_pixel_cels() -> Array[PixelCel]: return cels +func serialize_cel_undo_data(cels: Array[BaseCel], data: Dictionary) -> void: + for cel in cels: + 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() + + +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) + + ## 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: @@ -954,3 +974,9 @@ func reorder_reference_image(from: int, to: int) -> void: func add_tileset(tileset: TileSetCustom) -> void: tilesets.append(tileset) + + +func update_tilesets(cel_dictionary: Dictionary) -> void: + for cel in cel_dictionary: + if cel is CelTileMap: + (cel as CelTileMap).update_tileset() diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index 5fa176d47..fb14fa909 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -272,11 +272,9 @@ func prepare_undo(action: String) -> void: func commit_undo() -> void: - for cel in _undo_data: - if cel is CelTileMap: - (cel as CelTileMap).update_tileset() - var redo_data := _get_undo_data() var project := Global.current_project + project.update_tilesets(_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: @@ -284,13 +282,7 @@ func commit_undo() -> void: layer = project.current_layer project.undos += 1 - Global.undo_redo_compress_images(redo_data, _undo_data, project) - for cel in redo_data: - if cel is CelTileMap: - (cel as CelTileMap).deserialize_undo_data(redo_data[cel], project.undo_redo, false) - for cel in _undo_data: - if cel is CelTileMap: - (cel as CelTileMap).deserialize_undo_data(_undo_data[cel], project.undo_redo, true) + 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() @@ -766,13 +758,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) - if cel is CelTileMap: - data[cel] = (cel as CelTileMap).serialize_undo_data() + project.serialize_cel_undo_data(cels, data) return data diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index 33975e8e6..6a2e5f33e 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -344,6 +344,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 diff --git a/src/Tools/DesignTools/Bucket.gd b/src/Tools/DesignTools/Bucket.gd index 801b4e634..6712e6be3 100644 --- a/src/Tools/DesignTools/Bucket.gd +++ b/src/Tools/DesignTools/Bucket.gd @@ -494,8 +494,9 @@ func _set_pixel_pattern(image: ImageExtended, x: int, y: int, pattern_size: Vect func commit_undo() -> void: - var redo_data := _get_undo_data() var project := Global.current_project + project.update_tilesets(_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 +505,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 +515,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 diff --git a/src/Tools/UtilityTools/Move.gd b/src/Tools/UtilityTools/Move.gd index 476ae025e..dc0635ddd 100644 --- a/src/Tools/UtilityTools/Move.gd +++ b/src/Tools/UtilityTools/Move.gd @@ -130,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_tilesets(_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: @@ -140,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() @@ -158,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 diff --git a/src/Tools/UtilityTools/Text.gd b/src/Tools/UtilityTools/Text.gd index b3ce7a3e2..f7bfc1173 100644 --- a/src/Tools/UtilityTools/Text.gd +++ b/src/Tools/UtilityTools/Text.gd @@ -161,8 +161,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_tilesets(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: @@ -171,7 +172,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() @@ -179,9 +180,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 diff --git a/src/UI/Canvas/Selection.gd b/src/UI/Canvas/Selection.gd index f28fd7cd7..ba3fa0986 100644 --- a/src/UI/Canvas/Selection.gd +++ b/src/UI/Canvas/Selection.gd @@ -568,12 +568,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_tilesets(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 +604,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]] diff --git a/src/UI/Dialogs/ImageEffects/FlipImageDialog.gd b/src/UI/Dialogs/ImageEffects/FlipImageDialog.gd index aa4f81248..b5633d258 100644 --- a/src/UI/Dialogs/ImageEffects/FlipImageDialog.gd +++ b/src/UI/Dialogs/ImageEffects/FlipImageDialog.gd @@ -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_tilesets(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 diff --git a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd index 09c9ff5b4..9560a8ffa 100644 --- a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd +++ b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd @@ -149,34 +149,37 @@ 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) + if cel is CelTileMap: + (cel as CelTileMap).update_tileset() + 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() diff --git a/src/UI/TopMenuContainer/TopMenuContainer.gd b/src/UI/TopMenuContainer/TopMenuContainer.gd index 622921701..3e4ea7882 100644 --- a/src/UI/TopMenuContainer/TopMenuContainer.gd +++ b/src/UI/TopMenuContainer/TopMenuContainer.gd @@ -722,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_tilesets(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)) From 815388f2fa2f692e40d5f0b6c6827acc525e14b4 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:51:32 +0200 Subject: [PATCH 55/76] Don't execute update_tileset is we are on draw tiles mode --- src/Classes/Cels/CelTileMap.gd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index a1aa4f231..112f82b9f 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -158,6 +158,8 @@ func transform_tile( func update_tileset() -> void: editing_images.clear() + if TileSetPanel.placing_tiles: + return var tile_editing_mode := TileSetPanel.tile_editing_mode for i in cells.size(): var coords := get_cell_coords_in_image(i) From d2cfe72c16df42d671439e0403df1b4015cf93f3 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:08:46 +0200 Subject: [PATCH 56/76] Draw tiles on all selected draw cels Not working properly yet --- src/Tools/BaseDraw.gd | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index fb14fa909..21cd49057 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -321,12 +321,12 @@ func draw_end(pos: Vector2i) -> void: func draw_tile(pos: Vector2i, tile_index: int) -> void: - if Global.current_project.get_current_cel() is not CelTileMap: - return - pos = Global.current_project.tiles.get_canon_position(pos) - var tile_position := get_cell_position(pos) - var cel := Global.current_project.get_current_cel() as CelTileMap - cel.set_index(tile_position, tile_index) + for cel in _get_selected_draw_cels(): + if cel is not CelTileMap: + return + pos = Global.current_project.tiles.get_canon_position(pos) + var tile_position := get_cell_position(pos) + (cel as CelTileMap).set_index(tile_position, tile_index) func _prepare_tool() -> void: From 0997fa8536b9f0820e36255a9c5476f293918162 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:09:12 +0200 Subject: [PATCH 57/76] Linked tilemap cels should now work --- src/Classes/Cels/CelTileMap.gd | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 112f82b9f..3db8fd070 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -359,7 +359,7 @@ func _is_redo() -> bool: ## If the tileset has been modified by another tile, make sure to also update it here. func _on_tileset_updated(cel: CelTileMap) -> void: - if cel == self or not is_instance_valid(cel): + if cel == self or not is_instance_valid(cel) or cel in link_set["cels"]: return _update_cel_portions() Global.canvas.update_all_layers = true @@ -432,12 +432,14 @@ func deserialize_undo_data(dict: Dictionary, undo_redo: UndoRedo, undo: bool) -> for i in cell_indices.size(): var cell_data: Dictionary = cell_indices[i] undo_redo.add_undo_method(cells[i].deserialize.bind(cell_data)) - undo_redo.add_undo_method(tileset.deserialize_undo_data.bind(dict.get("tileset"), self)) + if dict.has("tileset"): + undo_redo.add_undo_method(tileset.deserialize_undo_data.bind(dict.tileset, self)) else: for i in cell_indices.size(): var cell_data: Dictionary = cell_indices[i] undo_redo.add_do_method(cells[i].deserialize.bind(cell_data)) - undo_redo.add_do_method(tileset.deserialize_undo_data.bind(dict.get("tileset"), self)) + if dict.has("tileset"): + undo_redo.add_do_method(tileset.deserialize_undo_data.bind(dict.tileset, self)) func serialize() -> Dictionary: From de6784202eaddc87c80b6d2dba3c36d17b8803ff Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:30:46 +0200 Subject: [PATCH 58/76] Layer/frame cloning works. --- src/Classes/Cels/CelTileMap.gd | 12 ++++++++++-- src/UI/Timeline/AnimationTimeline.gd | 10 +++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 3db8fd070..1a020477a 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -74,7 +74,7 @@ class Cell: transpose = dict.get("transpose", transpose) -func _init(_tileset: TileSetCustom, _image: ImageExtended, _opacity := 1.0) -> void: +func _init(_tileset: TileSetCustom, _image := ImageExtended.new(), _opacity := 1.0) -> void: super._init(_image, _opacity) tileset = _tileset @@ -359,7 +359,9 @@ func _is_redo() -> bool: ## If the tileset has been modified by another tile, make sure to also update it here. func _on_tileset_updated(cel: CelTileMap) -> void: - if cel == self or not is_instance_valid(cel) or cel in link_set["cels"]: + if cel == self or not is_instance_valid(cel): + return + if link_set != null and cel in link_set["cels"]: return _update_cel_portions() Global.canvas.update_all_layers = true @@ -367,6 +369,12 @@ func _on_tileset_updated(cel: CelTileMap) -> void: # 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: diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index 7ef569173..16dc18a9d 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -476,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() @@ -897,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() @@ -915,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() From db73d40550ca9ae8c8d1c8da1e70aec5c440b949 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 02:18:47 +0200 Subject: [PATCH 59/76] Merge layers into tilemap layers --- src/UI/Timeline/AnimationTimeline.gd | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index 16dc18a9d..77611b8d7 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -1104,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: + undo_data[bottom_cel] = (bottom_cel as CelTileMap).serialize_undo_data() + (bottom_cel as CelTileMap).update_tileset(new_bottom_image) + redo_data[bottom_cel] = (bottom_cel as CelTileMap).serialize_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( From 7b6f70e999a55cde041581dc1f4f7601108567a3 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 03:11:52 +0200 Subject: [PATCH 60/76] Almost make cel replacing work Needs to fix image resizing in order for this to work properly with undo/redo. --- src/Autoload/OpenSave.gd | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index 28ff6d169..a4c58ba26 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -719,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_tileset() + 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)) From 8110442ca17d45be0848c29f03e78076448ed857 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 03:29:27 +0200 Subject: [PATCH 61/76] Revert 3f39dbf3 --- src/Classes/Cels/CelTileMap.gd | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 1a020477a..6889ecc50 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -158,8 +158,6 @@ func transform_tile( func update_tileset() -> void: editing_images.clear() - if TileSetPanel.placing_tiles: - return var tile_editing_mode := TileSetPanel.tile_editing_mode for i in cells.size(): var coords := get_cell_coords_in_image(i) From 99e8cfa602a4d2480727f9a14798f8af51888474 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:57:21 +0200 Subject: [PATCH 62/76] Resizing should now work Also fixes cel replacing --- src/Autoload/DrawingAlgos.gd | 53 ++++++++++++++++------------ src/Classes/Cels/CelTileMap.gd | 41 +++++++++++++-------- src/Classes/TileSetCustom.gd | 22 ++++++++++-- src/UI/Timeline/AnimationTimeline.gd | 6 ++-- 4 files changed, 80 insertions(+), 42 deletions(-) diff --git a/src/Autoload/DrawingAlgos.gd b/src/Autoload/DrawingAlgos.gd index 6a668fea8..97f2c0f83 100644 --- a/src/Autoload/DrawingAlgos.gd +++ b/src/Autoload/DrawingAlgos.gd @@ -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) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 6889ecc50..02cd13035 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -156,13 +156,14 @@ func transform_tile( return transformed_tile -func update_tileset() -> void: +func update_tileset( + tile_editing_mode := TileSetPanel.tile_editing_mode, source_image := image +) -> void: editing_images.clear() - var tile_editing_mode := TileSetPanel.tile_editing_mode 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) + var image_portion := source_image.get_region(rect) var index := cells[i].index if index >= tileset.tiles.size(): printerr("Cell at position ", i + 1, ", mapped to ", index, " is out of bounds!") @@ -416,11 +417,6 @@ func update_texture(undo := false) -> void: super.update_texture(undo) -func size_changed(new_size: Vector2i) -> void: - _resize_cells(new_size) - _re_index_all_cells() - - func serialize_undo_data() -> Dictionary: var dict := {} var cell_indices := [] @@ -432,22 +428,39 @@ func serialize_undo_data() -> Dictionary: return dict +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(): + _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_tileset(tile_editing_mode, source_image) + redo_data[self] = serialize_undo_data() + + func deserialize_undo_data(dict: Dictionary, undo_redo: UndoRedo, undo: bool) -> void: var cell_indices = dict["cell_indices"] if undo: - for i in cell_indices.size(): - var cell_data: Dictionary = cell_indices[i] - undo_redo.add_undo_method(cells[i].deserialize.bind(cell_data)) + undo_redo.add_undo_method(deserialize_cell_data.bind(cell_indices)) if dict.has("tileset"): undo_redo.add_undo_method(tileset.deserialize_undo_data.bind(dict.tileset, self)) else: - for i in cell_indices.size(): - var cell_data: Dictionary = cell_indices[i] - undo_redo.add_do_method(cells[i].deserialize.bind(cell_data)) + undo_redo.add_do_method(deserialize_cell_data.bind(cell_indices)) if dict.has("tileset"): undo_redo.add_do_method(tileset.deserialize_undo_data.bind(dict.tileset, self)) +func deserialize_cell_data(cell_indices: Array) -> void: + _resize_cells(image.get_size()) + for i in cell_indices.size(): + var cell_data: Dictionary = cell_indices[i] + cells[i].deserialize(cell_data) + + func serialize() -> Dictionary: var dict := super.serialize() var cell_indices := [] diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd index 49c89b714..830e9988e 100644 --- a/src/Classes/TileSetCustom.gd +++ b/src/Classes/TileSetCustom.gd @@ -2,8 +2,8 @@ class_name TileSetCustom extends RefCounted ## A Tileset is a collection of tiles, used by [LayerTileMap]s and [CelTileMap]s. -## The tileset contains the [Project] that it is being used by, its [member name]. -## the size of each individual tile, and the collection of [TileSetCustom.Tile]s itself. +## 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. @@ -16,6 +16,11 @@ var name := "" 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. @@ -109,6 +114,19 @@ func remove_unused_tiles(cel: CelTileMap) -> bool: 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) + _tileset_has_been_cleared = true + set_deferred("_tileset_has_been_cleared", false) + + ## 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: diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index 77611b8d7..2cf58ce6c 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -1107,9 +1107,9 @@ func _on_MergeDownLayer_pressed() -> void: var undo_data := {} var redo_data := {} if bottom_cel is CelTileMap: - undo_data[bottom_cel] = (bottom_cel as CelTileMap).serialize_undo_data() - (bottom_cel as CelTileMap).update_tileset(new_bottom_image) - redo_data[bottom_cel] = (bottom_cel as CelTileMap).serialize_undo_data() + (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) project.deserialize_cel_undo_data(redo_data, undo_data) From 6243d1dc3d6223830437b57c8759a3675d3a6871 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:26:19 +0200 Subject: [PATCH 63/76] Only resize cells on undo/redo when needed --- src/Classes/Cels/CelTileMap.gd | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 02cd13035..534e2436e 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -425,6 +425,7 @@ func serialize_undo_data() -> Dictionary: cell_indices[i] = cells[i].serialize() dict["cell_indices"] = cell_indices dict["tileset"] = tileset.serialize_undo_data() + dict["resize"] = false return dict @@ -433,6 +434,7 @@ func serialize_undo_data_source_image( ) -> 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 @@ -440,22 +442,24 @@ func serialize_undo_data_source_image( tile_editing_mode = TileSetPanel.TileEditingMode.AUTO update_tileset(tile_editing_mode, source_image) redo_data[self] = serialize_undo_data() + redo_data[self]["resize"] = undo_data[self]["resize"] func deserialize_undo_data(dict: Dictionary, undo_redo: UndoRedo, undo: bool) -> void: - var cell_indices = dict["cell_indices"] + var cell_indices = dict.cell_indices if undo: - undo_redo.add_undo_method(deserialize_cell_data.bind(cell_indices)) + 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)) + 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)) -func deserialize_cell_data(cell_indices: Array) -> void: - _resize_cells(image.get_size()) +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) From d35b78f0134a0544b3f586e9f056020b01be1482 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:51:03 +0200 Subject: [PATCH 64/76] Fix layer effect applying not updating the tilesets properly --- src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd index 9560a8ffa..757ad5b12 100644 --- a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd +++ b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd @@ -164,8 +164,12 @@ func _apply_effect(layer: BaseLayer, effect: LayerEffect) -> void: 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_tilesets(undo_data) + for frame in project.frames: + var cel := frame.cels[layer.index] + var cel_image := cel.get_image() if cel is CelTileMap: - (cel as CelTileMap).update_tileset() 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 From 08de3cec451c9cf9c79c9f5d1dd75f369f251e67 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:36:05 +0200 Subject: [PATCH 65/76] Make drawing on multiple selected tilemap cels that share the same tileset and applied a layer effect to all cels of the layer work This now creates an issue when manual mode is used and we undo. Other cels don't get updated, even though they were changed by manual mode. --- src/Classes/Cels/CelTileMap.gd | 12 ++++++++++-- src/Classes/TileSetCustom.gd | 14 +++++++------- src/UI/TilesPanel.gd | 4 ++-- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 534e2436e..00d6bcf20 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -314,6 +314,11 @@ func _update_cell(cell_position: int) -> void: var image_portion := image.get_region(rect) var cell_data := cells[cell_position] var index := cell_data.index + if index >= tileset.tiles.size(): + printerr( + "Cell at position ", cell_position + 1, ", mapped to ", index, " is out of bounds!" + ) + return 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 @@ -357,12 +362,15 @@ func _is_redo() -> bool: ## If the tileset has been modified by another tile, make sure to also update it here. -func _on_tileset_updated(cel: CelTileMap) -> void: +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 - _update_cel_portions() + if replace_index > -1: # Manual mode + _update_cel_portions() + else: + _re_index_all_cells() Global.canvas.update_all_layers = true Global.canvas.queue_redraw() diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd index 830e9988e..c56ad4321 100644 --- a/src/Classes/TileSetCustom.gd +++ b/src/Classes/TileSetCustom.gd @@ -8,7 +8,7 @@ extends RefCounted ## 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) +signal updated(cel: CelTileMap, replace_index: int) ## The tileset's name. var name := "" @@ -52,7 +52,7 @@ func _init(_tile_size: Vector2i, _name := "") -> void: func add_tile(image: Image, cel: CelTileMap) -> void: var tile := Tile.new(image) tiles.append(tile) - updated.emit(cel) + updated.emit(cel, -1) ## Adds a new [param image] as a tile in a given [param position] in the tileset. @@ -61,7 +61,7 @@ func add_tile(image: Image, cel: CelTileMap) -> void: func insert_tile(image: Image, position: int, cel: CelTileMap) -> void: var tile := Tile.new(image) tiles.insert(position, tile) - updated.emit(cel) + updated.emit(cel, -1) ## Reduces a tile's [member TileSetCustom.Tile.times_used] by one, @@ -82,14 +82,14 @@ func unuse_tile_at_index(index: int, cel: CelTileMap) -> bool: ## 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) + 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) + updated.emit(cel, index) ## Finds and returns the position of a tile [param image] inside the tileset. @@ -122,7 +122,7 @@ func clear_tileset(cel: CelTileMap) -> void: 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) + updated.emit(cel, -1) _tileset_has_been_cleared = true set_deferred("_tileset_has_been_cleared", false) @@ -159,4 +159,4 @@ func deserialize_undo_data(dict: Dictionary, cel: CelTileMap) -> void: tiles[i] = Tile.new(image) tiles[i].times_used = tile_data[2] i += 1 - updated.emit(cel) + updated.emit(cel, -1) diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index fabe67658..89477fee6 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -77,10 +77,10 @@ func _on_cel_switched() -> void: return var cel := Global.current_project.get_current_cel() as CelTileMap set_tileset(cel.tileset) - _update_tileset(cel) + _update_tileset(cel, -1) -func _update_tileset(cel: BaseCel) -> void: +func _update_tileset(cel: BaseCel, _replace_index: int) -> void: _clear_tile_buttons() if cel is not CelTileMap: return From 2fb4af09d5cb01bb96dc369c94e222e23458bfad Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:58:46 +0200 Subject: [PATCH 66/76] Include all cels that share the same tileset in undo/redo if manual mode is enabled. --- src/Classes/Project.gd | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index 35143700c..118ab87eb 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -652,7 +652,10 @@ func get_all_pixel_cels() -> Array[PixelCel]: func serialize_cel_undo_data(cels: Array[BaseCel], data: Dictionary) -> void: - for cel in cels: + var cels_to_serialize := cels + if TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: + 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() @@ -671,6 +674,23 @@ func deserialize_cel_undo_data(redo_data: Dictionary, undo_data: Dictionary) -> (cel as CelTileMap).deserialize_undo_data(undo_data[cel], undo_redo, true) +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: From aa4ca7a42274c7bcf22054a635666fdcd5e8de01 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 18:24:39 +0200 Subject: [PATCH 67/76] Add documentation for Project and TileSetCustom --- src/Classes/Project.gd | 24 +++++++++++++++++++++++- src/Classes/TileSetCustom.gd | 3 +++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index 118ab87eb..4aba6f9a6 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -476,6 +476,11 @@ 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: @@ -651,9 +656,16 @@ 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 TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: + if ( + TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL + and not TileSetPanel.placing_tiles + ): cels_to_serialize = find_same_tileset_tilemap_cels(cels) for cel in cels_to_serialize: if not cel is PixelCel: @@ -664,6 +676,10 @@ func serialize_cel_undo_data(cels: Array[BaseCel], data: Dictionary) -> void: 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: @@ -674,6 +690,9 @@ func deserialize_cel_undo_data(redo_data: Dictionary, undo_data: Dictionary) -> (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] @@ -992,10 +1011,13 @@ func reorder_reference_image(from: int, to: int) -> void: 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_tileset]. func update_tilesets(cel_dictionary: Dictionary) -> void: for cel in cel_dictionary: if cel is CelTileMap: diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd index c56ad4321..96f25ef2a 100644 --- a/src/Classes/TileSetCustom.gd +++ b/src/Classes/TileSetCustom.gd @@ -140,6 +140,8 @@ func deserialize(dict: Dictionary) -> void: 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: @@ -148,6 +150,7 @@ func serialize_undo_data() -> Dictionary: 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 From fdd3e613cbc08654e8cac7783db05ca6fb08a0cc Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 19:08:55 +0200 Subject: [PATCH 68/76] Add documentation for CelTileMap and rename update_tileset to update_tilemap --- src/Autoload/OpenSave.gd | 2 +- src/Classes/Cels/CelTileMap.gd | 131 +++++++++++------- src/Classes/ImageEffect.gd | 2 +- src/Classes/Project.gd | 6 +- src/Tools/BaseDraw.gd | 2 +- src/Tools/DesignTools/Bucket.gd | 2 +- src/Tools/UtilityTools/Move.gd | 2 +- src/Tools/UtilityTools/Text.gd | 2 +- src/UI/Canvas/Selection.gd | 2 +- .../Dialogs/ImageEffects/FlipImageDialog.gd | 2 +- .../LayerEffects/LayerEffectsSettings.gd | 2 +- src/UI/TopMenuContainer/TopMenuContainer.gd | 2 +- 12 files changed, 93 insertions(+), 64 deletions(-) diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index a4c58ba26..0a8eb3237 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -727,7 +727,7 @@ func open_image_at_cel(image: Image, layer_index := 0, frame_index := 0) -> void cel_image.convert_rgb_to_indexed() var redo_data := {} if cel is CelTileMap: - (cel as CelTileMap).update_tileset() + (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) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 00d6bcf20..fd266828b 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -33,7 +33,7 @@ var vertical_cells: int ## 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_tileset]. +## Gets reset on [method update_tilemap]. var editing_images := {} @@ -130,6 +130,9 @@ func tiles_equal(cell_position: int, image_portion: Image, tile_image: Image) -> 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: @@ -156,7 +159,59 @@ func transform_tile( return transformed_tile -func update_tileset( +## 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() @@ -201,6 +256,8 @@ func update_tileset( cells[i].remove_transformations() +## 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] @@ -308,6 +365,8 @@ func _re_index_cells_after_index(index: int) -> void: 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) @@ -329,11 +388,14 @@ func _update_cell(cell_position: int) -> void: 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) @@ -349,6 +411,7 @@ func _re_index_all_cells() -> void: break +## Resizes the [member cells] array based on [param new_size]. func _resize_cells(new_size: Vector2i) -> void: horizontal_cells = ceili(float(new_size.x) / tileset.tile_size.x) vertical_cells = ceili(float(new_size.y) / tileset.tile_size.y) @@ -357,11 +420,17 @@ func _resize_cells(new_size: Vector2i) -> void: 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 tile, make sure to also update it here. +## 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 @@ -375,6 +444,14 @@ func _on_tileset_updated(cel: CelTileMap, replace_index: int) -> void: 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) @@ -425,54 +502,6 @@ func update_texture(undo := false) -> void: super.update_texture(undo) -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 - - -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_tileset(tile_editing_mode, source_image) - redo_data[self] = serialize_undo_data() - redo_data[self]["resize"] = undo_data[self]["resize"] - - -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)) - - -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) - - func serialize() -> Dictionary: var dict := super.serialize() var cell_indices := [] diff --git a/src/Classes/ImageEffect.gd b/src/Classes/ImageEffect.gd index d1cf3c12e..5efec7fe0 100644 --- a/src/Classes/ImageEffect.gd +++ b/src/Classes/ImageEffect.gd @@ -157,7 +157,7 @@ func display_animate_dialog() -> void: func _commit_undo(action: String, undo_data: Dictionary, project: Project) -> void: - project.update_tilesets(undo_data) + project.update_tilemaps(undo_data) var redo_data := _get_undo_data(project) project.undos += 1 project.undo_redo.create_action(action) diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index 4aba6f9a6..7b9fd9ef4 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -1017,8 +1017,8 @@ func add_tileset(tileset: TileSetCustom) -> void: ## Loops through all cels in [param cel_dictionary], and for [CelTileMap]s, -## it calls [method CelTileMap.update_tileset]. -func update_tilesets(cel_dictionary: Dictionary) -> void: +## 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_tileset() + (cel as CelTileMap).update_tilemap() diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index 21cd49057..f8cd6e201 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -273,7 +273,7 @@ func prepare_undo(action: String) -> void: func commit_undo() -> void: var project := Global.current_project - project.update_tilesets(_undo_data) + project.update_tilemaps(_undo_data) var redo_data := _get_undo_data() var frame := -1 var layer := -1 diff --git a/src/Tools/DesignTools/Bucket.gd b/src/Tools/DesignTools/Bucket.gd index 6712e6be3..02db74905 100644 --- a/src/Tools/DesignTools/Bucket.gd +++ b/src/Tools/DesignTools/Bucket.gd @@ -495,7 +495,7 @@ func _set_pixel_pattern(image: ImageExtended, x: int, y: int, pattern_size: Vect func commit_undo() -> void: var project := Global.current_project - project.update_tilesets(_undo_data) + project.update_tilemaps(_undo_data) var redo_data := _get_undo_data() var frame := -1 var layer := -1 diff --git a/src/Tools/UtilityTools/Move.gd b/src/Tools/UtilityTools/Move.gd index dc0635ddd..437c2484a 100644 --- a/src/Tools/UtilityTools/Move.gd +++ b/src/Tools/UtilityTools/Move.gd @@ -131,7 +131,7 @@ func _snap_position(pos: Vector2) -> Vector2: func _commit_undo(action: String) -> void: var project := Global.current_project - project.update_tilesets(_undo_data) + project.update_tilemaps(_undo_data) var redo_data := _get_undo_data() var frame := -1 var layer := -1 diff --git a/src/Tools/UtilityTools/Text.gd b/src/Tools/UtilityTools/Text.gd index f7bfc1173..a805b8b62 100644 --- a/src/Tools/UtilityTools/Text.gd +++ b/src/Tools/UtilityTools/Text.gd @@ -162,7 +162,7 @@ func text_to_pixels() -> void: func commit_undo(action: String, undo_data: Dictionary) -> void: var project := Global.current_project - project.update_tilesets(undo_data) + project.update_tilemaps(undo_data) var redo_data := _get_undo_data() var frame := -1 var layer := -1 diff --git a/src/UI/Canvas/Selection.gd b/src/UI/Canvas/Selection.gd index ba3fa0986..df0ea3c7d 100644 --- a/src/UI/Canvas/Selection.gd +++ b/src/UI/Canvas/Selection.gd @@ -569,7 +569,7 @@ func commit_undo(action: String, undo_data_tmp: Dictionary) -> void: print("No undo data found!") return var project := Global.current_project - project.update_tilesets(undo_data_tmp) + 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) diff --git a/src/UI/Dialogs/ImageEffects/FlipImageDialog.gd b/src/UI/Dialogs/ImageEffects/FlipImageDialog.gd index b5633d258..51aee6e2e 100644 --- a/src/UI/Dialogs/ImageEffects/FlipImageDialog.gd +++ b/src/UI/Dialogs/ImageEffects/FlipImageDialog.gd @@ -47,7 +47,7 @@ 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_tilesets(undo_data) + project.update_tilemaps(undo_data) var redo_data := _get_undo_data(project) project.undos += 1 project.undo_redo.create_action(action) diff --git a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd index 757ad5b12..802509522 100644 --- a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd +++ b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd @@ -165,7 +165,7 @@ func _apply_effect(layer: BaseLayer, effect: LayerEffect) -> void: var shader_image_effect := ShaderImageEffect.new() shader_image_effect.generate_image(cel_image, effect.shader, effect.params, image_size) - project.update_tilesets(undo_data) + project.update_tilemaps(undo_data) for frame in project.frames: var cel := frame.cels[layer.index] var cel_image := cel.get_image() diff --git a/src/UI/TopMenuContainer/TopMenuContainer.gd b/src/UI/TopMenuContainer/TopMenuContainer.gd index 3e4ea7882..7f78eba23 100644 --- a/src/UI/TopMenuContainer/TopMenuContainer.gd +++ b/src/UI/TopMenuContainer/TopMenuContainer.gd @@ -734,7 +734,7 @@ func _color_mode_submenu_id_pressed(id: ColorModes) -> void: project.color_mode = Image.FORMAT_RGBA8 else: project.color_mode = Project.INDEXED_MODE - project.update_tilesets(undo_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 From c4a5b3b380f67641f6a1815a33cc892ae90d6661 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 20:25:25 +0200 Subject: [PATCH 69/76] Update Translations.pot --- Translations/Translations.pot | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Translations/Translations.pot b/Translations/Translations.pot index e88a43a1f..4111a5777 100644 --- a/Translations/Translations.pot +++ b/Translations/Translations.pot @@ -2247,33 +2247,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 +2974,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 +3379,25 @@ 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/UI/Dialogs/ImportPreviewDialog.gd +msgid "Tileset" +msgstr "" + +#. A tileset is a collection of tiles. +#: src/UI/Timeline/NewTileMapLayerDialog.tscn +msgid "Tileset:" +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 "" From f5428952b24eecdec7452a87f639cfaf4023f255 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 29 Nov 2024 23:45:04 +0200 Subject: [PATCH 70/76] Add rotate left and right buttons in the tiles panel instead of transpose --- Translations/Translations.pot | 20 ++++ assets/graphics/misc/mirror_x.svg | 1 + assets/graphics/misc/mirror_x.svg.import | 37 ++++++++ assets/graphics/misc/mirror_y.svg | 1 + assets/graphics/misc/mirror_y.svg.import | 37 ++++++++ project.godot | 22 ++++- src/Autoload/Global.gd | 9 +- src/UI/TilesPanel.gd | 40 ++++++-- src/UI/TilesPanel.tscn | 111 +++++++++++++++++++---- 9 files changed, 253 insertions(+), 25 deletions(-) create mode 100644 assets/graphics/misc/mirror_x.svg create mode 100644 assets/graphics/misc/mirror_x.svg.import create mode 100644 assets/graphics/misc/mirror_y.svg create mode 100644 assets/graphics/misc/mirror_y.svg.import diff --git a/Translations/Translations.pot b/Translations/Translations.pot index 4111a5777..c59b716d3 100644 --- a/Translations/Translations.pot +++ b/Translations/Translations.pot @@ -3401,3 +3401,23 @@ 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 "" diff --git a/assets/graphics/misc/mirror_x.svg b/assets/graphics/misc/mirror_x.svg new file mode 100644 index 000000000..c73996a39 --- /dev/null +++ b/assets/graphics/misc/mirror_x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/graphics/misc/mirror_x.svg.import b/assets/graphics/misc/mirror_x.svg.import new file mode 100644 index 000000000..96239f95f --- /dev/null +++ b/assets/graphics/misc/mirror_x.svg.import @@ -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 diff --git a/assets/graphics/misc/mirror_y.svg b/assets/graphics/misc/mirror_y.svg new file mode 100644 index 000000000..7f8787231 --- /dev/null +++ b/assets/graphics/misc/mirror_y.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/graphics/misc/mirror_y.svg.import b/assets/graphics/misc/mirror_y.svg.import new file mode 100644 index 000000000..eef8847d9 --- /dev/null +++ b/assets/graphics/misc/mirror_y.svg.import @@ -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 diff --git a/project.godot b/project.godot index 6c4397d44..a899ba5a1 100644 --- a/project.godot +++ b/project.godot @@ -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,26 @@ show_pixel_indices={ "deadzone": 0.5, "events": [] } +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] diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index e55f4fef1..295ec1457 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -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"] diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index 89477fee6..5fbb37b44 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -6,6 +6,13 @@ 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): @@ -41,12 +48,15 @@ var button_size := 36: 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: @@ -147,6 +157,8 @@ func _clear_tile_buttons() -> void: 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: @@ -164,13 +176,29 @@ func _on_stack_toggled(toggled_on: bool) -> void: tile_editing_mode = TileEditingMode.STACK -func _on_flip_horizontal_button_toggled(toggled_on: bool) -> void: - is_flipped_h = toggled_on +func _on_flip_horizontal_button_pressed() -> void: + is_flipped_h = not is_flipped_h -func _on_flip_vertical_button_toggled(toggled_on: bool) -> void: - is_flipped_v = toggled_on +func _on_flip_vertical_button_pressed() -> void: + is_flipped_v = not is_flipped_v -func _on_transpose_button_toggled(toggled_on: bool) -> void: - is_transposed = toggled_on +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 diff --git a/src/UI/TilesPanel.tscn b/src/UI/TilesPanel.tscn index 5e1c4dcf9..d92e5f031 100644 --- a/src/UI/TilesPanel.tscn +++ b/src/UI/TilesPanel.tscn @@ -1,6 +1,33 @@ -[gd_scene load_steps=3 format=3 uid="uid://bfbragmmdwfbl"] +[gd_scene load_steps=14 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_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"] @@ -18,28 +45,79 @@ layout_mode = 2 [node name="PlaceTiles" type="CheckBox" parent="VBoxContainer"] layout_mode = 2 mouse_default_cursor_shape = 2 -text = "Place tiles" +text = "Draw tiles" [node name="TransformButtonsContainer" type="HFlowContainer" parent="VBoxContainer"] layout_mode = 2 -[node name="FlipHorizontalButton" type="Button" parent="VBoxContainer/TransformButtonsContainer"] +[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 -toggle_mode = true -text = "H" +shortcut = SubResource("Shortcut_yas23") -[node name="FlipVerticalButton" type="Button" parent="VBoxContainer/TransformButtonsContainer"] -layout_mode = 2 -mouse_default_cursor_shape = 2 -toggle_mode = true -text = "V" +[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="TransposeButton" type="Button" parent="VBoxContainer/TransformButtonsContainer"] +[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 -toggle_mode = true -text = "T" +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 @@ -74,9 +152,10 @@ size_flags_horizontal = 3 size_flags_vertical = 3 [connection signal="toggled" from="VBoxContainer/PlaceTiles" to="." method="_on_place_tiles_toggled"] -[connection signal="toggled" from="VBoxContainer/TransformButtonsContainer/FlipHorizontalButton" to="." method="_on_flip_horizontal_button_toggled"] -[connection signal="toggled" from="VBoxContainer/TransformButtonsContainer/FlipVerticalButton" to="." method="_on_flip_vertical_button_toggled"] -[connection signal="toggled" from="VBoxContainer/TransformButtonsContainer/TransposeButton" to="." method="_on_transpose_button_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"] From 1336819760023db9d3627256fe6534ee60314866 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 30 Nov 2024 00:20:41 +0200 Subject: [PATCH 71/76] Disable draw tiles mode when pressing one of the tile edit mode buttons --- src/UI/TilesPanel.gd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/UI/TilesPanel.gd b/src/UI/TilesPanel.gd index 5fbb37b44..330a8c628 100644 --- a/src/UI/TilesPanel.gd +++ b/src/UI/TilesPanel.gd @@ -162,16 +162,19 @@ func _on_place_tiles_toggled(toggled_on: bool) -> void: 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 From 6b6606005d7ee54d2c9e0e5625a3837fad3b8a3b Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 30 Nov 2024 12:48:54 +0200 Subject: [PATCH 72/76] Add get_text_info() in TileSetCustom --- Translations/Translations.pot | 5 +++++ src/Classes/TileSetCustom.gd | 9 +++++++++ src/UI/Timeline/NewTileMapLayerDialog.gd | 5 +---- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Translations/Translations.pot b/Translations/Translations.pot index c59b716d3..e0a6358ff 100644 --- a/Translations/Translations.pot +++ b/Translations/Translations.pot @@ -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 "" @@ -3381,6 +3385,7 @@ 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 "" diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd index 96f25ef2a..362ef67ea 100644 --- a/src/Classes/TileSetCustom.gd +++ b/src/Classes/TileSetCustom.gd @@ -127,6 +127,15 @@ func clear_tileset(cel: CelTileMap) -> void: 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 + + ## 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: diff --git a/src/UI/Timeline/NewTileMapLayerDialog.gd b/src/UI/Timeline/NewTileMapLayerDialog.gd index 0c4020d03..6fe79e281 100644 --- a/src/UI/Timeline/NewTileMapLayerDialog.gd +++ b/src/UI/Timeline/NewTileMapLayerDialog.gd @@ -33,10 +33,7 @@ func _on_about_to_popup() -> void: tileset_option_button.add_item("New tileset") for i in project.tilesets.size(): var tileset := project.tilesets[i] - var item_string := " %s (%s×%s)" % [i, tileset.tile_size.x, tileset.tile_size.y] - if not tileset.name.is_empty(): - item_string += ": " + tileset.name - tileset_option_button.add_item(tr("Tileset" + item_string)) + tileset_option_button.add_item(tileset.get_text_info(i)) _on_tileset_option_button_item_selected(tileset_option_button.selected) From d881e9f6058b0b85f79ba7fcff913c825cd2415b Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 30 Nov 2024 14:38:18 +0200 Subject: [PATCH 73/76] Add tilesets in the project properties and a button to delete them Can only delete tilesets that are not currently used by a tilemap layer --- Translations/Translations.pot | 5 +++ src/Classes/TileSetCustom.gd | 11 +++++ src/UI/Dialogs/ProjectProperties.gd | 52 +++++++++++++++++++--- src/UI/Dialogs/ProjectProperties.tscn | 64 +++++++++++++++++++-------- 4 files changed, 108 insertions(+), 24 deletions(-) diff --git a/Translations/Translations.pot b/Translations/Translations.pot index e0a6358ff..948144197 100644 --- a/Translations/Translations.pot +++ b/Translations/Translations.pot @@ -3395,6 +3395,11 @@ msgstr "" 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 "" diff --git a/src/Classes/TileSetCustom.gd b/src/Classes/TileSetCustom.gd index 362ef67ea..8870629f6 100644 --- a/src/Classes/TileSetCustom.gd +++ b/src/Classes/TileSetCustom.gd @@ -136,6 +136,17 @@ func get_text_info(tile_index: int) -> String: 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: diff --git a/src/UI/Dialogs/ProjectProperties.gd b/src/UI/Dialogs/ProjectProperties.gd index 9380e6229..3edfc43c7 100644 --- a/src/UI/Dialogs/ProjectProperties.gd +++ b/src/UI/Dialogs/ProjectProperties.gd @@ -1,11 +1,15 @@ 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 CLOSE_TEXTURE := preload("res://assets/graphics/misc/close.svg") + +@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 +25,25 @@ 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(): + 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, CLOSE_TEXTURE, -1, using_layers.size() > 0) func _on_name_line_edit_text_changed(new_text: String) -> void: @@ -29,3 +52,20 @@ 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: # 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() diff --git a/src/UI/Dialogs/ProjectProperties.tscn b/src/UI/Dialogs/ProjectProperties.tscn index 72c90b7c6..48e87a479 100644 --- a/src/UI/Dialogs/ProjectProperties.tscn +++ b/src/UI/Dialogs/ProjectProperties.tscn @@ -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"] From 1cd2159e288c35933ff9c85c7b7a9e8f00699329 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 30 Nov 2024 15:10:07 +0200 Subject: [PATCH 74/76] Duplicate tilesets from the project properties --- src/UI/Dialogs/ProjectProperties.gd | 55 ++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/src/UI/Dialogs/ProjectProperties.gd b/src/UI/Dialogs/ProjectProperties.gd index 3edfc43c7..c2c4bc29c 100644 --- a/src/UI/Dialogs/ProjectProperties.gd +++ b/src/UI/Dialogs/ProjectProperties.gd @@ -1,6 +1,7 @@ extends AcceptDialog -const CLOSE_TEXTURE := preload("res://assets/graphics/misc/close.svg") +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 @@ -29,21 +30,26 @@ func _on_visibility_changed() -> void: tilesets_list.clear() var root_item := tilesets_list.create_item() for i in Global.current_project.tilesets.size(): - 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, CLOSE_TEXTURE, -1, using_layers.size() > 0) + _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: @@ -58,7 +64,22 @@ func _on_tilesets_list_button_clicked(item: TreeItem, column: int, id: int, _mbi var tileset_index: int = item.get_metadata(column) var project := Global.current_project var tileset := project.tilesets[tileset_index] - if id == 0: # Delete + 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 From 40f56c3b0852fffe4f7e26cc72e03de97d723bfe Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 30 Nov 2024 16:22:54 +0200 Subject: [PATCH 75/76] Update the default layouts to include the Tiles panel --- assets/layouts/Default.tres | 81 ++++++++++++++++++++-------------- assets/layouts/Tallscreen.tres | 67 +++++++++++++++++----------- 2 files changed, 89 insertions(+), 59 deletions(-) diff --git a/assets/layouts/Default.tres b/assets/layouts/Default.tres index 6f753dda1..119610248 100644 --- a/assets/layouts/Default.tres +++ b/assets/layouts/Default.tres @@ -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 diff --git a/assets/layouts/Tallscreen.tres b/assets/layouts/Tallscreen.tres index cf1252892..b1d96489b 100644 --- a/assets/layouts/Tallscreen.tres +++ b/assets/layouts/Tallscreen.tres @@ -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 From f9bde9dcc3218d348e08a268320f58c89f86f764 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 30 Nov 2024 19:02:13 +0200 Subject: [PATCH 76/76] Change tileset in a layer from the project properties --- src/Classes/Cels/CelTileMap.gd | 86 +++++++++++++++++++--------- src/Classes/Project.gd | 5 +- src/UI/Timeline/LayerProperties.gd | 42 +++++++++++++- src/UI/Timeline/LayerProperties.tscn | 18 +++++- 4 files changed, 118 insertions(+), 33 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index fd266828b..9fbb369bb 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -11,16 +11,7 @@ extends PixelCel ## 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: - set(value): - if is_instance_valid(tileset): - if tileset.updated.is_connected(_on_tileset_updated): - tileset.updated.disconnect(_on_tileset_updated) - tileset = value - if is_instance_valid(tileset): - _resize_cells(get_image().get_size()) - if not tileset.updated.is_connected(_on_tileset_updated): - tileset.updated.connect(_on_tileset_updated) +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]. @@ -76,7 +67,20 @@ class Cell: func _init(_tileset: TileSetCustom, _image := ImageExtended.new(), _opacity := 1.0) -> void: super._init(_image, _opacity) - tileset = _tileset + 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 @@ -84,8 +88,9 @@ func _init(_tileset: TileSetCustom, _image := ImageExtended.new(), _opacity := 1 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: + 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 @@ -215,15 +220,14 @@ 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(): - printerr("Cell at position ", i + 1, ", mapped to ", index, " is out of bounds!") index = 0 - cells[i].index = 0 var current_tile := tileset.tiles[index] if tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL: if image_portion.is_invisible(): @@ -237,7 +241,7 @@ func update_tilemap( 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) + _handle_auto_editing_mode(i, image_portion, tileset_size_before_update) else: # Stack if image_portion.is_invisible(): continue @@ -254,6 +258,23 @@ func update_tilemap( 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 @@ -294,11 +315,17 @@ func update_tilemap( ## 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) -> void: +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: @@ -374,10 +401,7 @@ func _update_cell(cell_position: int) -> void: var cell_data := cells[cell_position] var index := cell_data.index if index >= tileset.tiles.size(): - printerr( - "Cell at position ", cell_position + 1, ", mapped to ", index, " is out of bounds!" - ) - return + 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 @@ -389,7 +413,7 @@ func _update_cell(cell_position: int) -> void: ## Calls [method _update_cell] for all [member cells]. -func _update_cel_portions() -> void: +func update_cel_portions() -> void: for i in cells.size(): _update_cell(i) @@ -402,7 +426,11 @@ func _re_index_all_cells() -> void: var rect := Rect2i(coords, tileset.tile_size) var image_portion := image.get_region(rect) if image_portion.is_invisible(): - cells[i].index = 0 + 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] @@ -412,12 +440,16 @@ func _re_index_all_cells() -> void: ## Resizes the [member cells] array based on [param new_size]. -func _resize_cells(new_size: Vector2i) -> void: +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(): - cells[i] = Cell.new() + 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. @@ -429,7 +461,7 @@ func _is_redo() -> bool: ## 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. +## 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): @@ -437,7 +469,7 @@ func _on_tileset_updated(cel: CelTileMap, replace_index: int) -> void: if link_set != null and cel in link_set["cels"]: return if replace_index > -1: # Manual mode - _update_cel_portions() + update_cel_portions() else: _re_index_all_cells() Global.canvas.update_all_layers = true @@ -469,6 +501,8 @@ func update_texture(undo := false) -> void: 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) diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index 7b9fd9ef4..387883113 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -662,10 +662,7 @@ func get_all_pixel_cels() -> Array[PixelCel]: ## 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 ( - TileSetPanel.tile_editing_mode == TileSetPanel.TileEditingMode.MANUAL - and not TileSetPanel.placing_tiles - ): + 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: diff --git a/src/UI/Timeline/LayerProperties.gd b/src/UI/Timeline/LayerProperties.gd index f8753ac85..fd4dfd229 100644 --- a/src/UI/Timeline/LayerProperties.gd +++ b/src/UI/Timeline/LayerProperties.gd @@ -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() diff --git a/src/UI/Timeline/LayerProperties.tscn b/src/UI/Timeline/LayerProperties.tscn index 7979e7eb4..74ac0b682 100644 --- a/src/UI/Timeline/LayerProperties.tscn +++ b/src/UI/Timeline/LayerProperties.tscn @@ -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"]