From 174f7d4b9f1b5e02e956c2116c8bd25daed9eeb8 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/88] 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 062889b4bb60ee06f07bc4bb515dec42cdf8ef31 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/88] 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 e119a91f5b86d2b2566476a491e478fbffddb187 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/88] 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 4f1ee0e8285e29558327a7a6ccb960e70b82d7dd 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/88] 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 a8755bd92f0a65d28fb2751a18ebb805b516bfe4 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/88] 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 f42454ef03ed3c9465bee475e1f18f9502121959 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/88] 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 27c0787f264d259b2c68a8dc34828f262103a9dc 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/88] 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 dd4f6b7b6cd6d5ecb7d578cc0985f744613ff39a 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/88] 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 9187d8a9be298b6d2b4bd69c13f39253a22d9900 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/88] 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 f69e4bdc9ef2fcf9683c26db22b972dfe375df69 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/88] 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 bd90f28de8ddd8c424e7fbe3b8827a8a408631f9 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/88] 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 d95c3f7555decd8a120e2bb69c42ca9460aa195f 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/88] 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 cd6212d8921be874673e0aade416bd7b06b1ca35 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/88] 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 6c79136f09ac8b0c54f610a55ea56e45ce6013b4 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/88] 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 6b77e30e08ffd5b36904f0a34b55d893529b3f67 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/88] 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 b87bfdf7e8f8dc8d95830eb88a99d99502fbc2ad 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/88] 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 12eda321769c54d9c337af30927f159da95c8cf6 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/88] 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 b882d9ee8..94fcffe5e 100644 --- a/src/Tools/UtilityTools/Text.gd +++ b/src/Tools/UtilityTools/Text.gd @@ -104,8 +104,8 @@ func draw_move(pos: Vector2i) -> void: _offset = pos -func draw_end(_position: Vector2i) -> void: - pass +func draw_end(pos: Vector2i) -> void: + super.draw_end(pos) func text_to_pixels() -> void: @@ -159,6 +159,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 0678fd8719766afc3d6a6c5c0214e6fedac1a600 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/88] 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 664197c9f32c2a5a6e9ef3485b4f2d8d23f8c172 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/88] 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 5b50270ee2bb1071d55d4c82b201870c24bf3b4e 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/88] 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 94fcffe5e..7c84f77bb 100644 --- a/src/Tools/UtilityTools/Text.gd +++ b/src/Tools/UtilityTools/Text.gd @@ -159,7 +159,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 d8c27a7966a9cc80f66d7190d9b2adb638953640 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/88] 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 c24a2240fe0cbddb575601c9352f1ac3f3fc7df8 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/88] 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 f077b147e9ca6fa2c4612b4800812e345174817f 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/88] 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 419154bafea04f5d2661db9831df2ceb1cdecdbc 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/88] 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 9c5b0f0f76a2b0b4cc9c445f230e06096b030cdc 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/88] 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 04ab9faa876fcea4a892b1af52904b1471e8cbd2 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/88] 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 6c200d3afef1ca2a2d0c58d5d89a59acf2cbaeeb 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/88] 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 14d0c76310c5954ecc3f50e35297ad0d2f3fda59 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/88] 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 20c7a9fdfc252f675d789d48b149547964f1d5bf 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/88] 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 c1fd209588e453c417b772c66ae1f04033e7a42c 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/88] 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 2301ba9fcc6f89d1f97328fd07d4b0bdb3b37552 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/88] 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 ad1252c142fd5e205ffc9cf1e36a68e55f8cf278 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/88] 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 81d4812b924f18773435cd672e47a3f3922e0004 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/88] 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 afe51262c96c37f2041d6f44bcf91b5a4dc4c8dd 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/88] 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 177428cc1b66c0bbc2d0d4ff992c3d2f8d0df551 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/88] 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 9f3564fe715048b4bb9e3276646c7a2d6381ea34 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/88] 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 610d2deb27af2d5cb7531a62c02d90889591c07c 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/88] 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 a696db3fc01c4461ebfd8af08d04f0213dc6116f 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/88] 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 4e62d00296c499d5937c7b14668864985603a7d8 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/88] 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 3bf556345d837e1d2b77124b2e99ead82243ea96 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/88] 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 5f10a913d4c62a475218b4062b34fd0269b313df 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/88] 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 ac75c8197c7168d97d49d1b91a9ca9f89d107216 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/88] 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 2c6dcdcf789eb9333076bace5958055b93783d72 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/88] 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 5e4eebe139b838d1f3129f8bc064d4557b705686 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/88] 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 b059ae4c8be1df98c017e0507d25315ccda7711f 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/88] 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 442285d15f55ecd60a436b5759bfaa02b62d7fcc 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/88] 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 ad83823f5860f46064eeb8a2f768de24e0941e06 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/88] 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 50da6c0ce957c3d1126ac8cb413fb1519e668d88 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/88] 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 5e3cc0a9a1b0ec006890b50347f4b444e42c43b1 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/88] 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 ae52151021386d100f428b31ae1b968eac0a37ad 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/88] 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 45ae8b7dee73a37d911c9951a54cfd160f16d808 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/88] 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 58d4e7efabde388a7ab73c7c4b9716eab72d3ab6 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/88] 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 533b28452b7f6996f2b2ef5d4322545fbc88324b 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/88] 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 f9b2ff2d6a6e10e93e13891fe73e3813441e7cbf 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/88] 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 7c84f77bb..5487add12 100644 --- a/src/Tools/UtilityTools/Text.gd +++ b/src/Tools/UtilityTools/Text.gd @@ -162,8 +162,9 @@ func text_to_pixels() -> void: func commit_undo(action: String, undo_data: Dictionary) -> void: - var redo_data := _get_undo_data() var project := Global.current_project + project.update_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: @@ -172,7 +173,7 @@ func commit_undo(action: String, undo_data: Dictionary) -> void: project.undos += 1 project.undo_redo.create_action(action) - Global.undo_redo_compress_images(redo_data, undo_data, project) + project.deserialize_cel_undo_data(redo_data, undo_data) project.undo_redo.add_do_method(Global.undo_or_redo.bind(false, frame, layer)) project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true, frame, layer)) project.undo_redo.commit_action() @@ -180,9 +181,7 @@ func commit_undo(action: String, undo_data: Dictionary) -> void: func _get_undo_data() -> Dictionary: var data := {} - var images := _get_selected_draw_images() - for image in images: - image.add_data_to_dictionary(data) + Global.current_project.serialize_cel_undo_data(_get_selected_draw_cels(), data) return data 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 d341d73fcf9101dcd55d226d5309e1bc38f7c160 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/88] 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 0b2fa7ab6e2e9649c6c3dcc0386baa7645a54bc9 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/88] 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 f197c6c55b639725dd9feb77945eabd144a65bf1 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/88] 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 c14ac5d57966d25077af4fc2b0d8a8409fe56bfa 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/88] 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 3cd4bd92ec991282779646537e0ca5c6e590013e 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/88] 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 d5dac1c527917d9a493cb2c1483fd2473dc1f995 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/88] 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 a3324591be1c1acd4798b999df679bfdce3be859 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/88] 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 5e34faf79387a2d90d502f03eb85f17b1e9bdf37 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/88] 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 381eed84d5e12cb56077ec2e2fa0695849d1bf75 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/88] 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 59a2ec4db1b1c8ba29d2c30dc0effe665b57c3a7 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/88] 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 a1f785f67e3ad79ff18ff52825bca89eca9ec559 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/88] 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 ca2e67612f7d36e464ccd7f0d45d3b4b22e8ffb9 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/88] 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 31c5bf7cac607015176abbdd1e7930fbc4ea869d 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/88] 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 423c77235dc55da45176fb5ba6fe4ee310bc304e 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/88] 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 5487add12..20fa26783 100644 --- a/src/Tools/UtilityTools/Text.gd +++ b/src/Tools/UtilityTools/Text.gd @@ -163,7 +163,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 800c8a6c19076b04f617b559b02cb85a5c53340e 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/88] 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 cfa067ebb084e8976d0c0415cba7e86b84516bb5 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/88] 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 8346b465b65ae65949d3b0e0d422f22b305d0fff 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/88] 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 a9946099ef1e50e84b357bac909229f853bd5ce3 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/88] 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 137ac25c00e53b68a3be104b7e156d00850f6664 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/88] 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 ef1b77662c77a50d182335d0468d943a689fc370 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/88] 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 e20585fde320b08c0a964eb5dac2194650891494 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/88] 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 228bc65a3817f32aadaf192010388f81978070d9 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/88] 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"] From 889e93e548e3db5fa47365b0b1260fce59f05404 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 2 Dec 2024 23:42:08 +0200 Subject: [PATCH 77/88] Add support for the draw tile mode for the rest of the draw tools, except bucket Also fixes issues with the draw tile mode with the pencil and eraser tools, such as leaving gaps if the mouse is moving fast, and support for spacing and fill inside tool options. --- src/Tools/BaseDraw.gd | 10 +++++++--- src/Tools/BaseShapeDrawer.gd | 9 ++++++--- src/Tools/DesignTools/CurveTool.gd | 9 ++++++--- src/Tools/DesignTools/Eraser.gd | 12 +----------- src/Tools/DesignTools/LineTool.gd | 11 +++++++---- src/Tools/DesignTools/Pencil.gd | 11 ----------- 6 files changed, 27 insertions(+), 35 deletions(-) diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index f8cd6e201..a54b05c35 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -18,6 +18,7 @@ var _brush_image := Image.new() var _orignal_brush_image := Image.new() ## Contains the original _brush_image, without resizing var _brush_texture := ImageTexture.new() var _strength := 1.0 +var _is_eraser := false @warning_ignore("unused_private_class_variable") var _picking_color := false @@ -320,12 +321,12 @@ func draw_end(pos: Vector2i) -> void: _polylines = _create_polylines(_indicator) -func draw_tile(pos: Vector2i, tile_index: int) -> void: +func draw_tile(pos: Vector2i) -> void: + var tile_position := get_cell_position(pos) + var tile_index := 0 if _is_eraser else TileSetPanel.selected_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) @@ -570,6 +571,9 @@ func _set_pixel_no_cache(pos: Vector2i, ignore_mirroring := false) -> void: pos = _stroke_project.tiles.get_canon_position(pos) if Global.current_project.has_selection: pos = Global.current_project.selection_map.get_canon_position(pos) + if is_placing_tiles(): + draw_tile(pos) + return if !_stroke_project.can_pixel_get_drawn(pos): return diff --git a/src/Tools/BaseShapeDrawer.gd b/src/Tools/BaseShapeDrawer.gd index 0e7550092..3050d7be0 100644 --- a/src/Tools/BaseShapeDrawer.gd +++ b/src/Tools/BaseShapeDrawer.gd @@ -189,9 +189,12 @@ func _draw_shape(origin: Vector2i, dest: Vector2i) -> void: _drawer.reset() # Draw each point offsetted based on the shape's thickness var draw_pos := point + thickness_vector - if Global.current_project.can_pixel_get_drawn(draw_pos): - for image in images: - _drawer.set_pixel(image, draw_pos, tool_slot.color) + if is_placing_tiles(): + draw_tile(draw_pos) + else: + if Global.current_project.can_pixel_get_drawn(draw_pos): + for image in images: + _drawer.set_pixel(image, draw_pos, tool_slot.color) commit_undo() diff --git a/src/Tools/DesignTools/CurveTool.gd b/src/Tools/DesignTools/CurveTool.gd index fc690b584..2ebcc875e 100644 --- a/src/Tools/DesignTools/CurveTool.gd +++ b/src/Tools/DesignTools/CurveTool.gd @@ -195,9 +195,12 @@ func _draw_shape() -> void: func _draw_pixel(point: Vector2i, images: Array[ImageExtended]) -> void: - if Global.current_project.can_pixel_get_drawn(point): - for image in images: - _drawer.set_pixel(image, point, tool_slot.color) + if is_placing_tiles(): + draw_tile(point) + else: + if Global.current_project.can_pixel_get_drawn(point): + for image in images: + _drawer.set_pixel(image, point, tool_slot.color) func _clear() -> void: diff --git a/src/Tools/DesignTools/Eraser.gd b/src/Tools/DesignTools/Eraser.gd index a2ea9b501..e28868d87 100644 --- a/src/Tools/DesignTools/Eraser.gd +++ b/src/Tools/DesignTools/Eraser.gd @@ -19,6 +19,7 @@ class EraseOp: func _init() -> void: _drawer.color_op = EraseOp.new() + _is_eraser = true _clear_image = Image.create(1, 1, false, Image.FORMAT_RGBA8) _clear_image.fill(Color(0, 0, 0, 0)) @@ -44,9 +45,6 @@ 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, 0) - return update_mask(_strength == 1) _changed = false _drawer.color_op.changed = false @@ -75,9 +73,6 @@ 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: @@ -99,11 +94,6 @@ 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/DesignTools/LineTool.gd b/src/Tools/DesignTools/LineTool.gd index f856cd6ac..668bfcc21 100644 --- a/src/Tools/DesignTools/LineTool.gd +++ b/src/Tools/DesignTools/LineTool.gd @@ -174,10 +174,13 @@ func _draw_shape() -> void: for point in points: # Reset drawer every time because pixel perfect sometimes breaks the tool _drawer.reset() - # Draw each point offsetted based on the shape's thickness - if Global.current_project.can_pixel_get_drawn(point): - for image in images: - _drawer.set_pixel(image, point, tool_slot.color) + if is_placing_tiles(): + draw_tile(point) + else: + # Draw each point offsetted based on the shape's thickness + if Global.current_project.can_pixel_get_drawn(point): + for image in images: + _drawer.set_pixel(image, point, tool_slot.color) commit_undo() diff --git a/src/Tools/DesignTools/Pencil.gd b/src/Tools/DesignTools/Pencil.gd index 825645b51..1817d8db1 100644 --- a/src/Tools/DesignTools/Pencil.gd +++ b/src/Tools/DesignTools/Pencil.gd @@ -104,9 +104,6 @@ func draw_start(pos: Vector2i) -> void: 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 @@ -145,9 +142,6 @@ 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, TileSetPanel.selected_tile_index) - return if _draw_line: _spacing_mode = false # spacing mode is disabled during line mode @@ -173,11 +167,6 @@ 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, TileSetPanel.selected_tile_index) - commit_undo() - return if _draw_line: _spacing_mode = false # spacing mode is disabled during line mode From 23304079b6f4c9e825800ab6941df6ef3946f41f Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:31:34 +0200 Subject: [PATCH 78/88] Support mirroring when using draw tiles mode --- src/Tools/BaseDraw.gd | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index a54b05c35..730448963 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -322,12 +322,19 @@ func draw_end(pos: Vector2i) -> void: func draw_tile(pos: Vector2i) -> void: - var tile_position := get_cell_position(pos) var tile_index := 0 if _is_eraser else TileSetPanel.selected_tile_index + var mirrored_positions := Tools.get_mirrored_positions(pos, Global.current_project) + var tile_positions := PackedInt32Array() + tile_positions.resize(mirrored_positions.size() + 1) + tile_positions[0] = get_cell_position(pos) + for i in mirrored_positions.size(): + var mirrored_position := mirrored_positions[i] + tile_positions[i + 1] = get_cell_position(mirrored_position) for cel in _get_selected_draw_cels(): if cel is not CelTileMap: return - (cel as CelTileMap).set_index(tile_position, tile_index) + for tile_position in tile_positions: + (cel as CelTileMap).set_index(tile_position, tile_index) func _prepare_tool() -> void: From 61a3488ead91dc03fb8f689967e6e0b689a40c03 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:33:18 +0200 Subject: [PATCH 79/88] Use the AutoInvertColors shader for when showing the tile mode indices --- src/UI/Canvas/Canvas.tscn | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/UI/Canvas/Canvas.tscn b/src/UI/Canvas/Canvas.tscn index 2a3adbef4..ac4c51920 100644 --- a/src/UI/Canvas/Canvas.tscn +++ b/src/UI/Canvas/Canvas.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=24 format=3 uid="uid://ba24iuv55m4l3"] +[gd_scene load_steps=25 format=3 uid="uid://ba24iuv55m4l3"] [ext_resource type="Script" path="res://src/UI/Canvas/Canvas.gd" id="1"] [ext_resource type="Shader" path="res://src/Shaders/BlendLayers.gdshader" id="1_253dh"] @@ -116,4 +116,5 @@ script = ExtResource("16_nxilb") script = ExtResource("17_qfjb4") [node name="TileModeIndices" type="Node2D" parent="."] +material = SubResource("ShaderMaterial_ascg6") script = ExtResource("19_7a6wb") From 8efbf0bf833ba5431b198ea5acf31ec790ecaa58 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 3 Dec 2024 00:40:02 +0200 Subject: [PATCH 80/88] Add more keyboard shortcuts --- project.godot | 20 ++++++++++++++++++++ src/UI/TilesPanel.tscn | 30 +++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/project.godot b/project.godot index a899ba5a1..aa3a195ef 100644 --- a/project.godot +++ b/project.godot @@ -925,6 +925,26 @@ show_pixel_indices={ "deadzone": 0.5, "events": [] } +toggle_draw_tiles_mode={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +tile_edit_mode_manual={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":49,"key_label":0,"unicode":33,"location":0,"echo":false,"script":null) +] +} +tile_edit_mode_auto={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":50,"key_label":0,"unicode":64,"location":0,"echo":false,"script":null) +] +} +tile_edit_mode_stack={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":51,"key_label":0,"unicode":35,"location":0,"echo":false,"script":null) +] +} tile_rotate_left={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":90,"key_label":0,"unicode":90,"location":0,"echo":false,"script":null) diff --git a/src/UI/TilesPanel.tscn b/src/UI/TilesPanel.tscn index d92e5f031..a247c87f6 100644 --- a/src/UI/TilesPanel.tscn +++ b/src/UI/TilesPanel.tscn @@ -1,10 +1,16 @@ -[gd_scene load_steps=14 format=3 uid="uid://bfbragmmdwfbl"] +[gd_scene load_steps=22 format=3 uid="uid://bfbragmmdwfbl"] [ext_resource type="Script" path="res://src/UI/TilesPanel.gd" id="1_d2oc5"] [ext_resource type="Texture2D" uid="uid://bv7ldl8obhawm" path="res://assets/graphics/misc/icon_reload.png" id="2_r1kie"] [ext_resource type="Texture2D" uid="uid://bpsfilx47bw3r" path="res://assets/graphics/misc/mirror_x.svg" id="3_5o62r"] [ext_resource type="Texture2D" uid="uid://bk6iaxiyl74ih" path="res://assets/graphics/misc/mirror_y.svg" id="4_2xhnr"] +[sub_resource type="InputEventAction" id="InputEventAction_klv67"] +action = &"toggle_draw_tiles_mode" + +[sub_resource type="Shortcut" id="Shortcut_6ebuw"] +events = [SubResource("InputEventAction_klv67")] + [sub_resource type="InputEventAction" id="InputEventAction_yr0lx"] action = &"tile_rotate_left" @@ -31,6 +37,24 @@ events = [SubResource("InputEventAction_18g3a")] [sub_resource type="ButtonGroup" id="ButtonGroup_uxnt0"] +[sub_resource type="InputEventAction" id="InputEventAction_mhgo3"] +action = &"tile_edit_mode_manual" + +[sub_resource type="Shortcut" id="Shortcut_pgg48"] +events = [SubResource("InputEventAction_mhgo3")] + +[sub_resource type="InputEventAction" id="InputEventAction_h1wos"] +action = &"tile_edit_mode_auto" + +[sub_resource type="Shortcut" id="Shortcut_a0fx5"] +events = [SubResource("InputEventAction_h1wos")] + +[sub_resource type="InputEventAction" id="InputEventAction_i4ufh"] +action = &"tile_edit_mode_stack" + +[sub_resource type="Shortcut" id="Shortcut_ysxej"] +events = [SubResource("InputEventAction_i4ufh")] + [node name="Tiles" type="PanelContainer"] anchors_preset = 15 anchor_right = 1.0 @@ -45,6 +69,7 @@ layout_mode = 2 [node name="PlaceTiles" type="CheckBox" parent="VBoxContainer"] layout_mode = 2 mouse_default_cursor_shape = 2 +shortcut = SubResource("Shortcut_6ebuw") text = "Draw tiles" [node name="TransformButtonsContainer" type="HFlowContainer" parent="VBoxContainer"] @@ -126,6 +151,7 @@ layout_mode = 2 layout_mode = 2 mouse_default_cursor_shape = 2 button_group = SubResource("ButtonGroup_uxnt0") +shortcut = SubResource("Shortcut_pgg48") text = "Manual" [node name="Auto" type="CheckBox" parent="VBoxContainer/ModeButtonsContainer"] @@ -133,12 +159,14 @@ layout_mode = 2 mouse_default_cursor_shape = 2 button_pressed = true button_group = SubResource("ButtonGroup_uxnt0") +shortcut = SubResource("Shortcut_a0fx5") text = "Auto" [node name="Stack" type="CheckBox" parent="VBoxContainer/ModeButtonsContainer"] layout_mode = 2 mouse_default_cursor_shape = 2 button_group = SubResource("ButtonGroup_uxnt0") +shortcut = SubResource("Shortcut_ysxej") text = "Stack" [node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"] From 4d5eebc670f043a32311a755cf7b61d26f6ab56e Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 3 Dec 2024 02:59:01 +0200 Subject: [PATCH 81/88] The bucket tool now works with draw tiles mode --- src/Classes/Cels/CelTileMap.gd | 23 +++ src/Tools/BaseTool.gd | 3 +- src/Tools/DesignTools/Bucket.gd | 249 ++++++++++++++++++++------ src/Tools/UtilityTools/ColorPicker.gd | 3 +- 4 files changed, 218 insertions(+), 60 deletions(-) diff --git a/src/Classes/Cels/CelTileMap.gd b/src/Classes/Cels/CelTileMap.gd index 9fbb369bb..5ab399658 100644 --- a/src/Classes/Cels/CelTileMap.gd +++ b/src/Classes/Cels/CelTileMap.gd @@ -125,6 +125,29 @@ func get_cell_position(coords: Vector2i) -> int: return x + y +## Returns the position of a cell in the tilemap +## at tilemap coordinates [param coords] in the cel's image. +func get_cell_position_in_tilemap_space(coords: Vector2i) -> int: + var x := coords.x + x = clampi(x, 0, horizontal_cells - 1) + var y := coords.y + y = clampi(y, 0, vertical_cells - 1) + y *= horizontal_cells + return x + y + + +## Returns the index of a cell in the tilemap +## at pixel coordinates [param coords] in the cel's image. +func get_cell_index_at_coords(coords: Vector2i) -> int: + return cells[get_cell_position(coords)].index + + +## Returns the index of a cell in the tilemap +## at tilemap coordinates [param coords] in the cel's image. +func get_cell_index_at_coords_in_tilemap_space(coords: Vector2i) -> int: + return cells[get_cell_position_in_tilemap_space(coords)].index + + ## Returns [code]true[/code] if the tile at cell position [param cell_position] ## with image [param image_portion] is equal to [param tile_image]. func tiles_equal(cell_position: int, image_portion: Image, tile_image: Image) -> bool: diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index 6a2e5f33e..e3de4759d 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -374,9 +374,8 @@ func _pick_color(pos: Vector2i) -> void: if pos.x < 0 or pos.y < 0: return if is_placing_tiles(): - var tile_position := get_cell_position(pos) var cel := Global.current_project.get_current_cel() as CelTileMap - Tools.selected_tile_index_changed.emit(cel.cells[tile_position].index) + Tools.selected_tile_index_changed.emit(cel.get_cell_index_at_coords(pos)) return var image := Image.new() image.copy_from(_get_draw_image()) diff --git a/src/Tools/DesignTools/Bucket.gd b/src/Tools/DesignTools/Bucket.gd index 02db74905..c54a91d35 100644 --- a/src/Tools/DesignTools/Bucket.gd +++ b/src/Tools/DesignTools/Bucket.gd @@ -186,6 +186,11 @@ func draw_end(pos: Vector2i) -> void: commit_undo() +func draw_tile(pos: Vector2i, cel: CelTileMap) -> void: + var tile_position := get_cell_position(pos) + cel.set_index(tile_position, TileSetPanel.selected_tile_index) + + func fill(pos: Vector2i) -> void: match _fill_area: FillArea.AREA: @@ -199,6 +204,17 @@ func fill(pos: Vector2i) -> void: func fill_in_color(pos: Vector2i) -> void: var project := Global.current_project + if is_placing_tiles(): + for cel in _get_selected_draw_cels(): + if cel is not CelTileMap: + continue + var tilemap_cel := cel as CelTileMap + var tile_index := tilemap_cel.get_cell_index_at_coords(pos) + for i in tilemap_cel.cells.size(): + var cell := tilemap_cel.cells[i] + if cell.index == tile_index: + tilemap_cel.set_index(i, TileSetPanel.selected_tile_index) + return var color := project.get_current_cel().get_image().get_pixelv(pos) var images := _get_selected_draw_images() for image in images: @@ -311,6 +327,74 @@ func fill_in_selection() -> void: gen.generate_image(image, PATTERN_FILL_SHADER, params, project.size) +func _flood_fill(pos: Vector2i) -> void: + # implements the floodfill routine by Shawn Hargreaves + # from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c + var project := Global.current_project + if is_placing_tiles(): + for cel in _get_selected_draw_cels(): + if cel is not CelTileMap: + continue + var tile_index := (cel as CelTileMap).get_cell_index_at_coords(pos) + # init flood data structures + _allegro_flood_segments = [] + _allegro_image_segments = [] + _compute_segments_for_tilemap(pos, cel, tile_index) + _color_segments_tilemap(cel) + return + + var images := _get_selected_draw_images() + for image in images: + if Tools.check_alpha_lock(image, pos): + continue + var color: Color = image.get_pixelv(pos) + if _fill_with == FillWith.COLOR or _pattern == null: + # end early if we are filling with the same color + if tool_slot.color.is_equal_approx(color): + continue + else: + # end early if we are filling with an empty pattern + var pattern_size := _pattern.image.get_size() + if pattern_size.x == 0 or pattern_size.y == 0: + return + # init flood data structures + _allegro_flood_segments = [] + _allegro_image_segments = [] + _compute_segments_for_image(pos, project, image, color) + # now actually color the image: since we have already checked a few things for the points + # we'll process here, we're going to skip a bunch of safety checks to speed things up. + _color_segments(image) + + +func _compute_segments_for_image( + pos: Vector2i, project: Project, image: Image, src_color: Color +) -> void: + # initially allocate at least 1 segment per line of image + for j in image.get_height(): + _add_new_segment(j) + # start flood algorithm + _flood_line_around_point(pos, project, image, src_color) + # test all segments while also discovering more + var done := false + while not done: + done = true + var max_index := _allegro_flood_segments.size() + for c in max_index: + var p := _allegro_flood_segments[c] + if p.todo_below: # check below the segment? + p.todo_below = false + if _check_flooded_segment( + p.y + 1, p.left_position, p.right_position, project, image, src_color + ): + done = false + if p.todo_above: # check above the segment? + p.todo_above = false + if _check_flooded_segment( + p.y - 1, p.left_position, p.right_position, project, image, src_color + ): + done = false + + ## Add a new segment to the array func _add_new_segment(y := 0) -> void: _allegro_flood_segments.append(Segment.new(y)) @@ -407,62 +491,6 @@ func _check_flooded_segment( return ret -func _flood_fill(pos: Vector2i) -> void: - # implements the floodfill routine by Shawn Hargreaves - # from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c - var project := Global.current_project - var images := _get_selected_draw_images() - for image in images: - if Tools.check_alpha_lock(image, pos): - continue - var color: Color = image.get_pixelv(pos) - if _fill_with == FillWith.COLOR or _pattern == null: - # end early if we are filling with the same color - if tool_slot.color.is_equal_approx(color): - continue - else: - # end early if we are filling with an empty pattern - var pattern_size := _pattern.image.get_size() - if pattern_size.x == 0 or pattern_size.y == 0: - return - # init flood data structures - _allegro_flood_segments = [] - _allegro_image_segments = [] - _compute_segments_for_image(pos, project, image, color) - # now actually color the image: since we have already checked a few things for the points - # we'll process here, we're going to skip a bunch of safety checks to speed things up. - _color_segments(image) - - -func _compute_segments_for_image( - pos: Vector2i, project: Project, image: Image, src_color: Color -) -> void: - # initially allocate at least 1 segment per line of image - for j in image.get_height(): - _add_new_segment(j) - # start flood algorithm - _flood_line_around_point(pos, project, image, src_color) - # test all segments while also discovering more - var done := false - while not done: - done = true - var max_index := _allegro_flood_segments.size() - for c in max_index: - var p := _allegro_flood_segments[c] - if p.todo_below: # check below the segment? - p.todo_below = false - if _check_flooded_segment( - p.y + 1, p.left_position, p.right_position, project, image, src_color - ): - done = false - if p.todo_above: # check above the segment? - p.todo_above = false - if _check_flooded_segment( - p.y - 1, p.left_position, p.right_position, project, image, src_color - ): - done = false - - func _color_segments(image: ImageExtended) -> void: if _fill_with == FillWith.COLOR or _pattern == null: # This is needed to ensure that the color used to fill is not wrong, due to float @@ -493,6 +521,115 @@ func _set_pixel_pattern(image: ImageExtended, x: int, y: int, pattern_size: Vect image.set_pixel_custom(x, y, pc) +func _compute_segments_for_tilemap(pos: Vector2i, cel: CelTileMap, src_index: int) -> void: + # initially allocate at least 1 segment per line of the tilemap + for j in cel.vertical_cells: + _add_new_segment(j) + pos /= cel.tileset.tile_size + # start flood algorithm + _flood_line_around_point_tilemap(pos, cel, src_index) + # test all segments while also discovering more + var done := false + while not done: + done = true + var max_index := _allegro_flood_segments.size() + for c in max_index: + var p := _allegro_flood_segments[c] + if p.todo_below: # check below the segment? + p.todo_below = false + if _check_flooded_segment_tilemap( + p.y + 1, p.left_position, p.right_position, cel, src_index + ): + done = false + if p.todo_above: # check above the segment? + p.todo_above = false + if _check_flooded_segment_tilemap( + p.y - 1, p.left_position, p.right_position, cel, src_index + ): + done = false + + +## Fill an horizontal segment around the specified position, and adds it to the +## list of segments filled. Returns the first x coordinate after the part of the +## line that has been filled. +## Τhis method is called by [method _flood_fill] after the required data structures +## have been initialized. +func _flood_line_around_point_tilemap(pos: Vector2i, cel: CelTileMap, src_index: int) -> int: + if cel.get_cell_index_at_coords_in_tilemap_space(pos) != src_index: + return pos.x + 1 + var west := pos + var east := pos + while west.x >= 0 && cel.get_cell_index_at_coords_in_tilemap_space(west) == src_index: + west += Vector2i.LEFT + while ( + east.x < cel.horizontal_cells + && cel.get_cell_index_at_coords_in_tilemap_space(east) == src_index + ): + east += Vector2i.RIGHT + # Make a note of the stuff we processed + var c := pos.y + var segment := _allegro_flood_segments[c] + # we may have already processed some segments on this y coordinate + if segment.flooding: + while segment.next > 0: + c = segment.next # index of next segment in this line of image + segment = _allegro_flood_segments[c] + # found last current segment on this line + c = _allegro_flood_segments.size() + segment.next = c + _add_new_segment(pos.y) + segment = _allegro_flood_segments[c] + # set the values for the current segment + segment.flooding = true + segment.left_position = west.x + 1 + segment.right_position = east.x - 1 + segment.y = pos.y + segment.next = 0 + # Should we process segments above or below this one? + # when there is a selected area, the pixels above and below the one we started creating this + # segment from may be outside it. It's easier to assume we should be checking for segments + # above and below this one than to specifically check every single pixel in it, because that + # test will be performed later anyway. + # On the other hand, this test we described is the same `project.can_pixel_get_drawn` does if + # there is no selection, so we don't need branching here. + segment.todo_above = pos.y > 0 + segment.todo_below = pos.y < cel.vertical_cells - 1 + # this is an actual segment we should be coloring, so we add it to the results for the + # current image + if segment.right_position >= segment.left_position: + _allegro_image_segments.append(segment) + # we know the point just east of the segment is not part of a segment that should be + # processed, else it would be part of this segment + return east.x + 1 + + +func _check_flooded_segment_tilemap( + y: int, left: int, right: int, cel: CelTileMap, src_index: int +) -> bool: + var ret := false + var c := 0 + while left <= right: + c = y + while true: + var segment := _allegro_flood_segments[c] + if left >= segment.left_position and left <= segment.right_position: + left = segment.right_position + 2 + break + c = segment.next + if c == 0: # couldn't find a valid segment, so we draw a new one + left = _flood_line_around_point_tilemap(Vector2i(left, y), cel, src_index) + ret = true + break + return ret + + +func _color_segments_tilemap(cel: CelTileMap) -> void: + for c in _allegro_image_segments.size(): + var p := _allegro_image_segments[c] + for px in range(p.left_position, p.right_position + 1): + draw_tile(Vector2i(px, p.y) * cel.tileset.tile_size, cel) + + func commit_undo() -> void: var project := Global.current_project project.update_tilemaps(_undo_data) diff --git a/src/Tools/UtilityTools/ColorPicker.gd b/src/Tools/UtilityTools/ColorPicker.gd index 7117b3ecf..a5bb3df99 100644 --- a/src/Tools/UtilityTools/ColorPicker.gd +++ b/src/Tools/UtilityTools/ColorPicker.gd @@ -66,9 +66,8 @@ func _pick_color(pos: Vector2i) -> void: if pos.x < 0 or pos.y < 0: return if is_placing_tiles(): - var tile_position := get_cell_position(pos) var cel := Global.current_project.get_current_cel() as CelTileMap - Tools.selected_tile_index_changed.emit(cel.cells[tile_position].index) + Tools.selected_tile_index_changed.emit(cel.get_cell_index_at_coords(pos)) return var image := Image.new() image.copy_from(_get_draw_image()) From 7fb65c313667a0b4dfbee40c58194145b53c3041 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:42:23 +0200 Subject: [PATCH 82/88] Add a _snap_to_rectangular_grid_boundary() method to BaseTool And rename _snap_to_grid_center() to _snap_to_rectangular_grid_center() --- src/Tools/BaseDraw.gd | 2 +- src/Tools/BaseTool.gd | 57 +++++++++++++++++++++++++------------------ 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index 730448963..3f7c09d6e 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -520,7 +520,7 @@ func draw_indicator(left: bool) -> void: 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) + snapped_position = _snap_to_rectangular_grid_center(snapped_position, grid_size, -1) draw_indicator_at(snapped_position, Vector2i.ZERO, color) if ( Global.current_project.has_selection diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index e3de4759d..37764a59e 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -145,31 +145,10 @@ func draw_preview() -> void: func snap_position(pos: Vector2) -> Vector2: var snapping_distance := Global.snapping_distance / Global.camera.zoom.x if Global.snap_to_rectangular_grid_boundary: - var grid_pos := pos.snapped(Global.grids[0].grid_size) - grid_pos += Vector2(Global.grids[0].grid_offset) - # keeping grid_pos as is would have been fine but this adds extra accuracy as to - # which snap point (from the list below) is closest to mouse and occupy THAT point - # t_l is for "top left" and so on - var t_l := grid_pos + Vector2(-Global.grids[0].grid_size.x, -Global.grids[0].grid_size.y) - var t_c := grid_pos + Vector2(0, -Global.grids[0].grid_size.y) - var t_r := grid_pos + Vector2(Global.grids[0].grid_size.x, -Global.grids[0].grid_size.y) - var m_l := grid_pos + Vector2(-Global.grids[0].grid_size.x, 0) - var m_c := grid_pos - var m_r := grid_pos + Vector2(Global.grids[0].grid_size.x, 0) - var b_l := grid_pos + Vector2(-Global.grids[0].grid_size.x, Global.grids[0].grid_size.y) - var b_c := grid_pos + Vector2(0, Global.grids[0].grid_size.y) - var b_r := grid_pos + Vector2(Global.grids[0].grid_size) - var vec_arr: PackedVector2Array = [t_l, t_c, t_r, m_l, m_c, m_r, b_l, b_c, b_r] - for vec in vec_arr: - if vec.distance_to(pos) < grid_pos.distance_to(pos): - grid_pos = vec - - var grid_point := _get_closest_point_to_grid(pos, snapping_distance, grid_pos) - if grid_point != Vector2.INF: - pos = grid_point.floor() + pos = _snap_to_rectangular_grid_boundary(pos, Global.grids[0].grid_size, snapping_distance) if Global.snap_to_rectangular_grid_center: - pos = _snap_to_grid_center(pos, Global.grids[0].grid_size, snapping_distance) + pos = _snap_to_rectangular_grid_center(pos, Global.grids[0].grid_size, snapping_distance) var snap_to := Vector2.INF if Global.snap_to_guides: @@ -282,7 +261,37 @@ func _get_closest_point_to_segment( return closest_point -func _snap_to_grid_center(pos: Vector2, grid_size: Vector2i, snapping_distance: float) -> Vector2: +func _snap_to_rectangular_grid_boundary( + pos: Vector2, grid_size: Vector2i, snapping_distance: float +) -> Vector2: + var grid_pos := pos.snapped(Global.grids[0].grid_size) + grid_pos += Vector2(Global.grids[0].grid_offset) + # keeping grid_pos as is would have been fine but this adds extra accuracy as to + # which snap point (from the list below) is closest to mouse and occupy THAT point + # t_l is for "top left" and so on + var t_l := grid_pos + Vector2(-Global.grids[0].grid_size.x, -Global.grids[0].grid_size.y) + var t_c := grid_pos + Vector2(0, -Global.grids[0].grid_size.y) + var t_r := grid_pos + Vector2(Global.grids[0].grid_size.x, -Global.grids[0].grid_size.y) + var m_l := grid_pos + Vector2(-Global.grids[0].grid_size.x, 0) + var m_c := grid_pos + var m_r := grid_pos + Vector2(Global.grids[0].grid_size.x, 0) + var b_l := grid_pos + Vector2(-Global.grids[0].grid_size.x, Global.grids[0].grid_size.y) + var b_c := grid_pos + Vector2(0, Global.grids[0].grid_size.y) + var b_r := grid_pos + Vector2(Global.grids[0].grid_size) + var vec_arr: PackedVector2Array = [t_l, t_c, t_r, m_l, m_c, m_r, b_l, b_c, b_r] + for vec in vec_arr: + if vec.distance_to(pos) < grid_pos.distance_to(pos): + grid_pos = vec + + var grid_point := _get_closest_point_to_grid(pos, snapping_distance, grid_pos) + if grid_point != Vector2.INF: + pos = grid_point.floor() + return pos + + +func _snap_to_rectangular_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 From ad2fcf48912062af74f2b8b7925d3cf0d98e5f87 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:55:36 +0200 Subject: [PATCH 83/88] Add a grid_offset parameter for the rectangular grid snap methods --- src/Tools/BaseDraw.gd | 4 +++- src/Tools/BaseTool.gd | 34 +++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index 3f7c09d6e..9b2ddf0f2 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -520,7 +520,9 @@ func draw_indicator(left: bool) -> void: if is_placing_tiles(): var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset var grid_size := tileset.tile_size - snapped_position = _snap_to_rectangular_grid_center(snapped_position, grid_size, -1) + snapped_position = _snap_to_rectangular_grid_center( + snapped_position, grid_size, Vector2i.ZERO, -1 + ) draw_indicator_at(snapped_position, Vector2i.ZERO, color) if ( Global.current_project.has_selection diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index 37764a59e..51fa3221d 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -145,10 +145,14 @@ func draw_preview() -> void: func snap_position(pos: Vector2) -> Vector2: var snapping_distance := Global.snapping_distance / Global.camera.zoom.x if Global.snap_to_rectangular_grid_boundary: - pos = _snap_to_rectangular_grid_boundary(pos, Global.grids[0].grid_size, snapping_distance) + pos = _snap_to_rectangular_grid_boundary( + pos, Global.grids[0].grid_size, Global.grids[0].grid_offset, snapping_distance + ) if Global.snap_to_rectangular_grid_center: - pos = _snap_to_rectangular_grid_center(pos, Global.grids[0].grid_size, snapping_distance) + pos = _snap_to_rectangular_grid_center( + pos, Global.grids[0].grid_size, Global.grids[0].grid_offset, snapping_distance + ) var snap_to := Vector2.INF if Global.snap_to_guides: @@ -262,22 +266,22 @@ func _get_closest_point_to_segment( func _snap_to_rectangular_grid_boundary( - pos: Vector2, grid_size: Vector2i, snapping_distance: float + pos: Vector2, grid_size: Vector2i, grid_offset: Vector2i, snapping_distance: float ) -> Vector2: - var grid_pos := pos.snapped(Global.grids[0].grid_size) - grid_pos += Vector2(Global.grids[0].grid_offset) + var grid_pos := pos.snapped(grid_size) + grid_pos += Vector2(grid_offset) # keeping grid_pos as is would have been fine but this adds extra accuracy as to # which snap point (from the list below) is closest to mouse and occupy THAT point # t_l is for "top left" and so on - var t_l := grid_pos + Vector2(-Global.grids[0].grid_size.x, -Global.grids[0].grid_size.y) - var t_c := grid_pos + Vector2(0, -Global.grids[0].grid_size.y) - var t_r := grid_pos + Vector2(Global.grids[0].grid_size.x, -Global.grids[0].grid_size.y) - var m_l := grid_pos + Vector2(-Global.grids[0].grid_size.x, 0) + var t_l := grid_pos + Vector2(-grid_size.x, -grid_size.y) + var t_c := grid_pos + Vector2(0, -grid_size.y) + var t_r := grid_pos + Vector2(grid_size.x, -grid_size.y) + var m_l := grid_pos + Vector2(-grid_size.x, 0) var m_c := grid_pos - var m_r := grid_pos + Vector2(Global.grids[0].grid_size.x, 0) - var b_l := grid_pos + Vector2(-Global.grids[0].grid_size.x, Global.grids[0].grid_size.y) - var b_c := grid_pos + Vector2(0, Global.grids[0].grid_size.y) - var b_r := grid_pos + Vector2(Global.grids[0].grid_size) + var m_r := grid_pos + Vector2(grid_size.x, 0) + var b_l := grid_pos + Vector2(-grid_size.x, grid_size.y) + var b_c := grid_pos + Vector2(0, grid_size.y) + var b_r := grid_pos + Vector2(grid_size) var vec_arr: PackedVector2Array = [t_l, t_c, t_r, m_l, m_c, m_r, b_l, b_c, b_r] for vec in vec_arr: if vec.distance_to(pos) < grid_pos.distance_to(pos): @@ -290,10 +294,10 @@ func _snap_to_rectangular_grid_boundary( func _snap_to_rectangular_grid_center( - pos: Vector2, grid_size: Vector2i, snapping_distance: float + pos: Vector2, grid_size: Vector2i, grid_offset: Vector2i, snapping_distance: float ) -> Vector2: var grid_center := pos.snapped(grid_size) + Vector2(grid_size / 2) - grid_center += Vector2(Global.grids[0].grid_offset) + grid_center += Vector2(grid_offset) # keeping grid_center as is would have been fine but this adds extra accuracy as to # which snap point (from the list below) is closest to mouse and occupy THAT point # t_l is for "top left" and so on From d579baf830c4432450e5785eebc7a2ba505f600f Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 4 Dec 2024 01:38:56 +0200 Subject: [PATCH 84/88] Some refactoring and initial support for draw tile mode for selection tools Only rectangle selection for now, and resizing doesn't yet work --- src/Autoload/Tools.gd | 81 ++++++++++++++++++++++++ src/Tools/BaseDraw.gd | 8 +-- src/Tools/BaseSelectionTool.gd | 4 ++ src/Tools/BaseShapeDrawer.gd | 2 +- src/Tools/BaseTool.gd | 87 +------------------------- src/Tools/DesignTools/Bucket.gd | 4 +- src/Tools/DesignTools/CurveTool.gd | 2 +- src/Tools/DesignTools/LineTool.gd | 2 +- src/Tools/SelectionTools/RectSelect.gd | 8 ++- src/Tools/UtilityTools/ColorPicker.gd | 2 +- src/UI/Canvas/Selection.gd | 22 ++++++- 11 files changed, 124 insertions(+), 98 deletions(-) diff --git a/src/Autoload/Tools.gd b/src/Autoload/Tools.gd index 1bde704bc..2b7b13455 100644 --- a/src/Autoload/Tools.gd +++ b/src/Autoload/Tools.gd @@ -583,6 +583,87 @@ func calculate_mirror_x_minus_y(pos: Vector2i, project: Project) -> Vector2i: ) +func is_placing_tiles() -> bool: + if Global.current_project.frames.size() == 0 or Global.current_project.layers.size() == 0: + return false + return Global.current_project.get_current_cel() is CelTileMap and TileSetPanel.placing_tiles + + +func _get_closest_point_to_grid(pos: Vector2, distance: float, grid_pos: Vector2) -> Vector2: + # If the cursor is close to the start/origin of a grid cell, snap to that + var snap_distance := distance * Vector2.ONE + var closest_point := Vector2.INF + var rect := Rect2() + rect.position = pos - (snap_distance / 4.0) + rect.end = pos + (snap_distance / 4.0) + if rect.has_point(grid_pos): + closest_point = grid_pos + return closest_point + # If the cursor is far from the grid cell origin but still close to a grid line + # Look for a point close to a horizontal grid line + var grid_start_hor := Vector2(0, grid_pos.y) + var grid_end_hor := Vector2(Global.current_project.size.x, grid_pos.y) + var closest_point_hor := get_closest_point_to_segment( + pos, distance, grid_start_hor, grid_end_hor + ) + # Look for a point close to a vertical grid line + var grid_start_ver := Vector2(grid_pos.x, 0) + var grid_end_ver := Vector2(grid_pos.x, Global.current_project.size.y) + var closest_point_ver := get_closest_point_to_segment( + pos, distance, grid_start_ver, grid_end_ver + ) + # Snap to the closest point to the closest grid line + var horizontal_distance := (closest_point_hor - pos).length() + var vertical_distance := (closest_point_ver - pos).length() + if horizontal_distance < vertical_distance: + closest_point = closest_point_hor + elif horizontal_distance > vertical_distance: + closest_point = closest_point_ver + elif horizontal_distance == vertical_distance and closest_point_hor != Vector2.INF: + closest_point = grid_pos + return closest_point + + +func get_closest_point_to_segment( + pos: Vector2, distance: float, s1: Vector2, s2: Vector2 +) -> Vector2: + var test_line := (s2 - s1).rotated(deg_to_rad(90)).normalized() + var from_a := pos - test_line * distance + var from_b := pos + test_line * distance + var closest_point := Vector2.INF + if Geometry2D.segment_intersects_segment(from_a, from_b, s1, s2): + closest_point = Geometry2D.get_closest_point_to_segment(pos, s1, s2) + return closest_point + + +func snap_to_rectangular_grid_boundary( + pos: Vector2, grid_size: Vector2i, grid_offset := Vector2i.ZERO, snapping_distance := 9999.0 +) -> Vector2: + var grid_pos := pos.snapped(grid_size) + grid_pos += Vector2(grid_offset) + # keeping grid_pos as is would have been fine but this adds extra accuracy as to + # which snap point (from the list below) is closest to mouse and occupy THAT point + # t_l is for "top left" and so on + var t_l := grid_pos + Vector2(-grid_size.x, -grid_size.y) + var t_c := grid_pos + Vector2(0, -grid_size.y) + var t_r := grid_pos + Vector2(grid_size.x, -grid_size.y) + var m_l := grid_pos + Vector2(-grid_size.x, 0) + var m_c := grid_pos + var m_r := grid_pos + Vector2(grid_size.x, 0) + var b_l := grid_pos + Vector2(-grid_size.x, grid_size.y) + var b_c := grid_pos + Vector2(0, grid_size.y) + var b_r := grid_pos + Vector2(grid_size) + var vec_arr: PackedVector2Array = [t_l, t_c, t_r, m_l, m_c, m_r, b_l, b_c, b_r] + for vec in vec_arr: + if vec.distance_to(pos) < grid_pos.distance_to(pos): + grid_pos = vec + + var grid_point := _get_closest_point_to_grid(pos, snapping_distance, grid_pos) + if grid_point != Vector2.INF: + pos = grid_point.floor() + return pos + + func set_button_size(button_size: int) -> void: var size := Vector2(24, 24) if button_size == Global.ButtonSize.SMALL else Vector2(32, 32) if not is_instance_valid(_tool_buttons): diff --git a/src/Tools/BaseDraw.gd b/src/Tools/BaseDraw.gd index 9b2ddf0f2..e01088a0a 100644 --- a/src/Tools/BaseDraw.gd +++ b/src/Tools/BaseDraw.gd @@ -163,7 +163,7 @@ func update_config() -> void: func update_brush() -> void: $Brush/BrushSize.suffix = "px" # Assume we are using default brushes - if is_placing_tiles(): + if Tools.is_placing_tiles(): var tilemap_cel := Global.current_project.get_current_cel() as CelTileMap var tileset := tilemap_cel.tileset var tile_index := clampi(TileSetPanel.selected_tile_index, 0, tileset.tiles.size() - 1) @@ -517,7 +517,7 @@ 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 var snapped_position := snap_position(_cursor) - if is_placing_tiles(): + if Tools.is_placing_tiles(): var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset var grid_size := tileset.tile_size snapped_position = _snap_to_rectangular_grid_center( @@ -545,7 +545,7 @@ func draw_indicator(left: bool) -> void: 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 or is_placing_tiles(): + if _brush.type in IMAGE_BRUSHES and not _draw_line or Tools.is_placing_tiles(): pos -= _brush_image.get_size() / 2 pos -= offset canvas.draw_texture(_brush_texture, pos) @@ -580,7 +580,7 @@ func _set_pixel_no_cache(pos: Vector2i, ignore_mirroring := false) -> void: pos = _stroke_project.tiles.get_canon_position(pos) if Global.current_project.has_selection: pos = Global.current_project.selection_map.get_canon_position(pos) - if is_placing_tiles(): + if Tools.is_placing_tiles(): draw_tile(pos) return if !_stroke_project.can_pixel_get_drawn(pos): diff --git a/src/Tools/BaseSelectionTool.gd b/src/Tools/BaseSelectionTool.gd index 8ed365050..ce30f0b2f 100644 --- a/src/Tools/BaseSelectionTool.gd +++ b/src/Tools/BaseSelectionTool.gd @@ -152,6 +152,10 @@ func draw_move(pos: Vector2i) -> void: if not _move: return + if Tools.is_placing_tiles(): + var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset + var grid_size := tileset.tile_size + pos = Tools.snap_to_rectangular_grid_boundary(pos, grid_size) if Input.is_action_pressed("transform_snap_axis"): # Snap to axis var angle := Vector2(pos).angle_to_point(_start_pos) if absf(angle) <= PI / 4 or absf(angle) >= 3 * PI / 4: diff --git a/src/Tools/BaseShapeDrawer.gd b/src/Tools/BaseShapeDrawer.gd index 3050d7be0..5111b626f 100644 --- a/src/Tools/BaseShapeDrawer.gd +++ b/src/Tools/BaseShapeDrawer.gd @@ -189,7 +189,7 @@ func _draw_shape(origin: Vector2i, dest: Vector2i) -> void: _drawer.reset() # Draw each point offsetted based on the shape's thickness var draw_pos := point + thickness_vector - if is_placing_tiles(): + if Tools.is_placing_tiles(): draw_tile(draw_pos) else: if Global.current_project.can_pixel_get_drawn(draw_pos): diff --git a/src/Tools/BaseTool.gd b/src/Tools/BaseTool.gd index 51fa3221d..cdc14345d 100644 --- a/src/Tools/BaseTool.gd +++ b/src/Tools/BaseTool.gd @@ -79,12 +79,6 @@ func draw_end(_pos: Vector2i) -> void: project.can_undo = true -func is_placing_tiles() -> bool: - if Global.current_project.frames.size() == 0 or Global.current_project.layers.size() == 0: - return false - return Global.current_project.get_current_cel() is CelTileMap and TileSetPanel.placing_tiles - - func get_cell_position(pos: Vector2i) -> int: var tile_pos := 0 if Global.current_project.get_current_cel() is not CelTileMap: @@ -145,7 +139,7 @@ func draw_preview() -> void: func snap_position(pos: Vector2) -> Vector2: var snapping_distance := Global.snapping_distance / Global.camera.zoom.x if Global.snap_to_rectangular_grid_boundary: - pos = _snap_to_rectangular_grid_boundary( + pos = Tools.snap_to_rectangular_grid_boundary( pos, Global.grids[0].grid_size, Global.grids[0].grid_offset, snapping_distance ) @@ -218,81 +212,6 @@ func mirror_array(array: Array[Vector2i], callable := func(_array): pass) -> Arr return new_array -func _get_closest_point_to_grid(pos: Vector2, distance: float, grid_pos: Vector2) -> Vector2: - # If the cursor is close to the start/origin of a grid cell, snap to that - var snap_distance := distance * Vector2.ONE - var closest_point := Vector2.INF - var rect := Rect2() - rect.position = pos - (snap_distance / 4.0) - rect.end = pos + (snap_distance / 4.0) - if rect.has_point(grid_pos): - closest_point = grid_pos - return closest_point - # If the cursor is far from the grid cell origin but still close to a grid line - # Look for a point close to a horizontal grid line - var grid_start_hor := Vector2(0, grid_pos.y) - var grid_end_hor := Vector2(Global.current_project.size.x, grid_pos.y) - var closest_point_hor := _get_closest_point_to_segment( - pos, distance, grid_start_hor, grid_end_hor - ) - # Look for a point close to a vertical grid line - var grid_start_ver := Vector2(grid_pos.x, 0) - var grid_end_ver := Vector2(grid_pos.x, Global.current_project.size.y) - var closest_point_ver := _get_closest_point_to_segment( - pos, distance, grid_start_ver, grid_end_ver - ) - # Snap to the closest point to the closest grid line - var horizontal_distance := (closest_point_hor - pos).length() - var vertical_distance := (closest_point_ver - pos).length() - if horizontal_distance < vertical_distance: - closest_point = closest_point_hor - elif horizontal_distance > vertical_distance: - closest_point = closest_point_ver - elif horizontal_distance == vertical_distance and closest_point_hor != Vector2.INF: - closest_point = grid_pos - return closest_point - - -func _get_closest_point_to_segment( - pos: Vector2, distance: float, s1: Vector2, s2: Vector2 -) -> Vector2: - var test_line := (s2 - s1).rotated(deg_to_rad(90)).normalized() - var from_a := pos - test_line * distance - var from_b := pos + test_line * distance - var closest_point := Vector2.INF - if Geometry2D.segment_intersects_segment(from_a, from_b, s1, s2): - closest_point = Geometry2D.get_closest_point_to_segment(pos, s1, s2) - return closest_point - - -func _snap_to_rectangular_grid_boundary( - pos: Vector2, grid_size: Vector2i, grid_offset: Vector2i, snapping_distance: float -) -> Vector2: - var grid_pos := pos.snapped(grid_size) - grid_pos += Vector2(grid_offset) - # keeping grid_pos as is would have been fine but this adds extra accuracy as to - # which snap point (from the list below) is closest to mouse and occupy THAT point - # t_l is for "top left" and so on - var t_l := grid_pos + Vector2(-grid_size.x, -grid_size.y) - var t_c := grid_pos + Vector2(0, -grid_size.y) - var t_r := grid_pos + Vector2(grid_size.x, -grid_size.y) - var m_l := grid_pos + Vector2(-grid_size.x, 0) - var m_c := grid_pos - var m_r := grid_pos + Vector2(grid_size.x, 0) - var b_l := grid_pos + Vector2(-grid_size.x, grid_size.y) - var b_c := grid_pos + Vector2(0, grid_size.y) - var b_r := grid_pos + Vector2(grid_size) - var vec_arr: PackedVector2Array = [t_l, t_c, t_r, m_l, m_c, m_r, b_l, b_c, b_r] - for vec in vec_arr: - if vec.distance_to(pos) < grid_pos.distance_to(pos): - grid_pos = vec - - var grid_point := _get_closest_point_to_grid(pos, snapping_distance, grid_pos) - if grid_point != Vector2.INF: - pos = grid_point.floor() - return pos - - func _snap_to_rectangular_grid_center( pos: Vector2, grid_size: Vector2i, grid_offset: Vector2i, snapping_distance: float ) -> Vector2: @@ -325,7 +244,7 @@ func _snap_to_rectangular_grid_center( func _snap_to_guide( snap_to: Vector2, pos: Vector2, distance: float, s1: Vector2, s2: Vector2 ) -> Vector2: - var closest_point := _get_closest_point_to_segment(pos, distance, s1, s2) + var closest_point := Tools.get_closest_point_to_segment(pos, distance, s1, s2) if closest_point == Vector2.INF: # Is not close to a guide return Vector2.INF # Snap to the closest guide @@ -386,7 +305,7 @@ func _pick_color(pos: Vector2i) -> void: if pos.x < 0 or pos.y < 0: return - if is_placing_tiles(): + if Tools.is_placing_tiles(): var cel := Global.current_project.get_current_cel() as CelTileMap Tools.selected_tile_index_changed.emit(cel.get_cell_index_at_coords(pos)) return diff --git a/src/Tools/DesignTools/Bucket.gd b/src/Tools/DesignTools/Bucket.gd index c54a91d35..e46149ae3 100644 --- a/src/Tools/DesignTools/Bucket.gd +++ b/src/Tools/DesignTools/Bucket.gd @@ -204,7 +204,7 @@ func fill(pos: Vector2i) -> void: func fill_in_color(pos: Vector2i) -> void: var project := Global.current_project - if is_placing_tiles(): + if Tools.is_placing_tiles(): for cel in _get_selected_draw_cels(): if cel is not CelTileMap: continue @@ -331,7 +331,7 @@ func _flood_fill(pos: Vector2i) -> void: # implements the floodfill routine by Shawn Hargreaves # from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c var project := Global.current_project - if is_placing_tiles(): + if Tools.is_placing_tiles(): for cel in _get_selected_draw_cels(): if cel is not CelTileMap: continue diff --git a/src/Tools/DesignTools/CurveTool.gd b/src/Tools/DesignTools/CurveTool.gd index 2ebcc875e..c14684534 100644 --- a/src/Tools/DesignTools/CurveTool.gd +++ b/src/Tools/DesignTools/CurveTool.gd @@ -195,7 +195,7 @@ func _draw_shape() -> void: func _draw_pixel(point: Vector2i, images: Array[ImageExtended]) -> void: - if is_placing_tiles(): + if Tools.is_placing_tiles(): draw_tile(point) else: if Global.current_project.can_pixel_get_drawn(point): diff --git a/src/Tools/DesignTools/LineTool.gd b/src/Tools/DesignTools/LineTool.gd index 668bfcc21..5e8917f0d 100644 --- a/src/Tools/DesignTools/LineTool.gd +++ b/src/Tools/DesignTools/LineTool.gd @@ -174,7 +174,7 @@ func _draw_shape() -> void: for point in points: # Reset drawer every time because pixel perfect sometimes breaks the tool _drawer.reset() - if is_placing_tiles(): + if Tools.is_placing_tiles(): draw_tile(point) else: # Draw each point offsetted based on the shape's thickness diff --git a/src/Tools/SelectionTools/RectSelect.gd b/src/Tools/SelectionTools/RectSelect.gd index 1e3cb7cf1..690379cb7 100644 --- a/src/Tools/SelectionTools/RectSelect.gd +++ b/src/Tools/SelectionTools/RectSelect.gd @@ -101,6 +101,11 @@ func apply_selection(pos: Vector2i) -> void: ## Given an origin point and destination point, returns a rect representing ## where the shape will be drawn and what is its size func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i: + if Tools.is_placing_tiles(): + var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset + var grid_size := tileset.tile_size + origin = Tools.snap_to_rectangular_grid_boundary(origin, grid_size) + dest = Tools.snap_to_rectangular_grid_boundary(dest, grid_size) var rect := Rect2i() # Center the rect on the mouse @@ -125,6 +130,7 @@ func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i: rect.position = Vector2i(mini(origin.x, dest.x), mini(origin.y, dest.y)) rect.size = (origin - dest).abs() - rect.size += Vector2i.ONE + if not Tools.is_placing_tiles(): + rect.size += Vector2i.ONE return rect diff --git a/src/Tools/UtilityTools/ColorPicker.gd b/src/Tools/UtilityTools/ColorPicker.gd index a5bb3df99..8e207edc7 100644 --- a/src/Tools/UtilityTools/ColorPicker.gd +++ b/src/Tools/UtilityTools/ColorPicker.gd @@ -65,7 +65,7 @@ func _pick_color(pos: Vector2i) -> void: pos = project.tiles.get_canon_position(pos) if pos.x < 0 or pos.y < 0: return - if is_placing_tiles(): + if Tools.is_placing_tiles(): var cel := Global.current_project.get_current_cel() as CelTileMap Tools.selected_tile_index_changed.emit(cel.get_cell_index_at_coords(pos)) return diff --git a/src/UI/Canvas/Selection.gd b/src/UI/Canvas/Selection.gd index df0ea3c7d..fb4967081 100644 --- a/src/UI/Canvas/Selection.gd +++ b/src/UI/Canvas/Selection.gd @@ -224,6 +224,10 @@ func _move_with_arrow_keys(event: InputEvent) -> void: if is_zero_approx(absf(move.y)): move.y = 0 var final_direction := (move * step).round() + if Tools.is_placing_tiles(): + var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset + var grid_size := tileset.tile_size + final_direction *= Vector2(grid_size) move_content(final_direction) @@ -313,6 +317,8 @@ func _update_on_zoom() -> void: func _gizmo_resize() -> void: + if Tools.is_placing_tiles(): + return var dir := dragged_gizmo.direction if Input.is_action_pressed("shape_center"): # Code inspired from https://github.com/GDQuest/godot-open-rpg @@ -379,10 +385,11 @@ func resize_selection() -> void: else: Global.current_project.selection_map.copy_from(original_bitmap) if is_moving_content: - content_pivot = original_big_bounding_rectangle.size / 2.0 preview_image.copy_from(original_preview_image) - DrawingAlgos.nn_rotate(preview_image, angle, content_pivot) - preview_image.resize(size.x, size.y, Image.INTERPOLATE_NEAREST) + if not Tools.is_placing_tiles(): + content_pivot = original_big_bounding_rectangle.size / 2.0 + DrawingAlgos.nn_rotate(preview_image, angle, content_pivot) + preview_image.resize(size.x, size.y, Image.INTERPOLATE_NEAREST) if temp_rect.size.x < 0: preview_image.flip_x() if temp_rect.size.y < 0: @@ -456,6 +463,15 @@ func move_borders(move: Vector2i) -> void: return marching_ants_outline.offset += Vector2(move) big_bounding_rectangle.position += move + if Tools.is_placing_tiles(): + var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset + var grid_size := tileset.tile_size + marching_ants_outline.offset = Tools.snap_to_rectangular_grid_boundary( + marching_ants_outline.offset, grid_size + ) + big_bounding_rectangle.position = Vector2i( + Tools.snap_to_rectangular_grid_boundary(big_bounding_rectangle.position, grid_size) + ) queue_redraw() From 4365ed8a3a8f568116f6cd59f246668476bb4981 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 4 Dec 2024 03:07:35 +0200 Subject: [PATCH 85/88] Support draw tiles mode in elliptical and color select tools --- src/Classes/SelectionMap.gd | 7 ++++ src/Tools/BaseSelectionTool.gd | 7 ++++ src/Tools/SelectionTools/ColorSelect.gd | 45 ++++++++++++++++----- src/Tools/SelectionTools/EllipseSelect.gd | 49 +++++++++++++++++------ 4 files changed, 84 insertions(+), 24 deletions(-) diff --git a/src/Classes/SelectionMap.gd b/src/Classes/SelectionMap.gd index abdc3329c..ee9932d55 100644 --- a/src/Classes/SelectionMap.gd +++ b/src/Classes/SelectionMap.gd @@ -77,6 +77,13 @@ func select_pixel(pixel: Vector2i, select := true) -> void: set_pixelv(pixel, Color(0)) +func select_rect(rect: Rect2i, select := true) -> void: + if select: + fill_rect(rect, Color(1, 1, 1, 1)) + else: + fill_rect(rect, Color(0)) + + func select_all() -> void: fill(Color(1, 1, 1, 1)) diff --git a/src/Tools/BaseSelectionTool.gd b/src/Tools/BaseSelectionTool.gd index ce30f0b2f..26cb48d55 100644 --- a/src/Tools/BaseSelectionTool.gd +++ b/src/Tools/BaseSelectionTool.gd @@ -215,6 +215,13 @@ func apply_selection(_position: Vector2i) -> void: _intersect = true +func select_tilemap_cell( + cel: CelTileMap, cell_position: int, selection: SelectionMap, select: bool +) -> void: + var rect := Rect2i(cel.get_cell_coords_in_image(cell_position), cel.tileset.tile_size) + selection.select_rect(rect, select) + + func _on_confirm_button_pressed() -> void: if selection_node.is_moving_content: selection_node.transform_content_confirm() diff --git a/src/Tools/SelectionTools/ColorSelect.gd b/src/Tools/SelectionTools/ColorSelect.gd index 1e172929f..c1606db06 100644 --- a/src/Tools/SelectionTools/ColorSelect.gd +++ b/src/Tools/SelectionTools/ColorSelect.gd @@ -32,23 +32,46 @@ func apply_selection(pos: Vector2i) -> void: if pos.x > project.size.x - 1 or pos.y > project.size.y - 1: return - var cel_image := Image.new() - cel_image.copy_from(_get_draw_image()) - var color := cel_image.get_pixelv(pos) var operation := 0 if _subtract: operation = 1 elif _intersect: operation = 2 - var params := {"color": color, "tolerance": _tolerance, "operation": operation} - if _add or _subtract or _intersect: - var selection_tex := ImageTexture.create_from_image(project.selection_map) - params["selection"] = selection_tex - var gen := ShaderImageEffect.new() - gen.generate_image(cel_image, shader, params, project.size) - cel_image.convert(Image.FORMAT_LA8) + if Tools.is_placing_tiles(): + var prev_selection_map := SelectionMap.new() # Used for intersect + prev_selection_map.copy_from(project.selection_map) + if !_add and !_subtract and !_intersect: + Global.canvas.selection.clear_selection() + if _intersect: + project.selection_map.clear() + for cel in _get_selected_draw_cels(): + if cel is not CelTileMap: + continue + var tilemap_cel := cel as CelTileMap + var tile_index := tilemap_cel.get_cell_index_at_coords(pos) + for i in tilemap_cel.cells.size(): + var cell := tilemap_cel.cells[i] + if cell.index == tile_index: + if _intersect: + var p := (cel as CelTileMap).get_cell_coords_in_image(i) + select_tilemap_cell( + cel, i, project.selection_map, prev_selection_map.is_pixel_selected(p) + ) + else: + select_tilemap_cell(cel, i, project.selection_map, !_subtract) + else: + var cel_image := Image.new() + cel_image.copy_from(_get_draw_image()) + var color := cel_image.get_pixelv(pos) + var params := {"color": color, "tolerance": _tolerance, "operation": operation} + if _add or _subtract or _intersect: + var selection_tex := ImageTexture.create_from_image(project.selection_map) + params["selection"] = selection_tex + var gen := ShaderImageEffect.new() + gen.generate_image(cel_image, shader, params, project.size) + cel_image.convert(Image.FORMAT_LA8) - project.selection_map.copy_from(cel_image) + project.selection_map.copy_from(cel_image) Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect() Global.canvas.selection.commit_undo("Select", undo_data) diff --git a/src/Tools/SelectionTools/EllipseSelect.gd b/src/Tools/SelectionTools/EllipseSelect.gd index 80539a797..1d4234d4d 100644 --- a/src/Tools/SelectionTools/EllipseSelect.gd +++ b/src/Tools/SelectionTools/EllipseSelect.gd @@ -81,18 +81,36 @@ func apply_selection(_position: Vector2i) -> void: Global.canvas.selection.commit_undo("Select", undo_data) if _rect.size == Vector2i.ZERO: return - set_ellipse(project.selection_map, _rect.position) - # Handle mirroring - var mirror_positions := Tools.get_mirrored_positions(_rect.position, project, 1) - var mirror_ends := Tools.get_mirrored_positions(_rect.end, project, 1) - for i in mirror_positions.size(): - var mirror_rect := Rect2i() - mirror_rect.position = mirror_positions[i] - mirror_rect.end = mirror_ends[i] - set_ellipse(project.selection_map, mirror_rect.abs().position) + if Tools.is_placing_tiles(): + var operation := 0 + if _subtract: + operation = 1 + elif _intersect: + operation = 2 + Global.canvas.selection.select_rect(_rect, operation) + # Handle mirroring + var mirror_positions := Tools.get_mirrored_positions(_rect.position, project, 1) + var mirror_ends := Tools.get_mirrored_positions(_rect.end, project, 1) + for i in mirror_positions.size(): + var mirror_rect := Rect2i() + mirror_rect.position = mirror_positions[i] + mirror_rect.end = mirror_ends[i] + Global.canvas.selection.select_rect(mirror_rect.abs(), operation) - Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect() - Global.canvas.selection.commit_undo("Select", undo_data) + Global.canvas.selection.commit_undo("Select", undo_data) + else: + set_ellipse(project.selection_map, _rect.position) + # Handle mirroring + var mirror_positions := Tools.get_mirrored_positions(_rect.position, project, 1) + var mirror_ends := Tools.get_mirrored_positions(_rect.end, project, 1) + for i in mirror_positions.size(): + var mirror_rect := Rect2i() + mirror_rect.position = mirror_positions[i] + mirror_rect.end = mirror_ends[i] + set_ellipse(project.selection_map, mirror_rect.abs().position) + + Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect() + Global.canvas.selection.commit_undo("Select", undo_data) func set_ellipse(selection_map: SelectionMap, pos: Vector2i) -> void: @@ -116,8 +134,12 @@ func set_ellipse(selection_map: SelectionMap, pos: Vector2i) -> void: # Given an origin point and destination point, returns a rect representing # where the shape will be drawn and what is its size func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i: + if Tools.is_placing_tiles(): + var tileset := (Global.current_project.get_current_cel() as CelTileMap).tileset + var grid_size := tileset.tile_size + origin = Tools.snap_to_rectangular_grid_boundary(origin, grid_size) + dest = Tools.snap_to_rectangular_grid_boundary(dest, grid_size) var rect := Rect2i() - # Center the rect on the mouse if _expand_from_center: var new_size := dest - origin @@ -140,6 +162,7 @@ func _get_result_rect(origin: Vector2i, dest: Vector2i) -> Rect2i: rect.position = Vector2i(mini(origin.x, dest.x), mini(origin.y, dest.y)) rect.size = (origin - dest).abs() - rect.size += Vector2i.ONE + if not Tools.is_placing_tiles(): + rect.size += Vector2i.ONE return rect From 58ab5b7083d0993b03b3e7263443c2c5d2583185 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 4 Dec 2024 04:26:09 +0200 Subject: [PATCH 86/88] Support draw tiles mode in lasso and polygon select tools --- src/Tools/SelectionTools/Lasso.gd | 23 ++++++++++++++++------- src/Tools/SelectionTools/PolygonSelect.gd | 23 ++++++++++++++++------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/Tools/SelectionTools/Lasso.gd b/src/Tools/SelectionTools/Lasso.gd index 1f73eb9b1..0797eea6e 100644 --- a/src/Tools/SelectionTools/Lasso.gd +++ b/src/Tools/SelectionTools/Lasso.gd @@ -70,9 +70,9 @@ func apply_selection(_position) -> void: if _draw_points.size() > 3: if _intersect: project.selection_map.clear() - lasso_selection(_draw_points, project.selection_map, previous_selection_map) + lasso_selection(_draw_points, project, previous_selection_map) # Handle mirroring - var callable := lasso_selection.bind(project.selection_map, previous_selection_map) + var callable := lasso_selection.bind(project, previous_selection_map) mirror_array(_draw_points, callable) Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect() else: @@ -85,8 +85,9 @@ func apply_selection(_position) -> void: func lasso_selection( - points: Array[Vector2i], selection_map: SelectionMap, previous_selection_map: SelectionMap + points: Array[Vector2i], project: Project, previous_selection_map: SelectionMap ) -> void: + var selection_map := project.selection_map var selection_size := selection_map.get_size() var bounding_rect := Rect2i(points[0], Vector2i.ZERO) for point in points: @@ -95,9 +96,9 @@ func lasso_selection( bounding_rect = bounding_rect.expand(point) if _intersect: if previous_selection_map.is_pixel_selected(point): - selection_map.select_pixel(point, true) + select_pixel(point, project, true) else: - selection_map.select_pixel(point, !_subtract) + select_pixel(point, project, !_subtract) var v := Vector2i() for x in bounding_rect.size.x: @@ -107,9 +108,17 @@ func lasso_selection( if Geometry2D.is_point_in_polygon(v, points): if _intersect: if previous_selection_map.is_pixel_selected(v): - selection_map.select_pixel(v, true) + select_pixel(v, project, true) else: - selection_map.select_pixel(v, !_subtract) + select_pixel(v, project, !_subtract) + + +func select_pixel(point: Vector2i, project: Project, select: bool) -> void: + if Tools.is_placing_tiles(): + var tilemap := project.get_current_cel() as CelTileMap + var cell_position := tilemap.get_cell_position(point) + select_tilemap_cell(tilemap, cell_position, project.selection_map, select) + project.selection_map.select_pixel(point, select) # Bresenham's Algorithm diff --git a/src/Tools/SelectionTools/PolygonSelect.gd b/src/Tools/SelectionTools/PolygonSelect.gd index 3c8776a75..0103fb449 100644 --- a/src/Tools/SelectionTools/PolygonSelect.gd +++ b/src/Tools/SelectionTools/PolygonSelect.gd @@ -107,9 +107,9 @@ func apply_selection(pos: Vector2i) -> void: if _draw_points.size() > 3: if _intersect: project.selection_map.clear() - lasso_selection(_draw_points, project.selection_map, previous_selection_map) + lasso_selection(_draw_points, project, previous_selection_map) # Handle mirroring - var callable := lasso_selection.bind(project.selection_map, previous_selection_map) + var callable := lasso_selection.bind(project, previous_selection_map) mirror_array(_draw_points, callable) Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect() else: @@ -128,8 +128,9 @@ func _clear() -> void: func lasso_selection( - points: Array[Vector2i], selection_map: SelectionMap, previous_selection_map: SelectionMap + points: Array[Vector2i], project: Project, previous_selection_map: SelectionMap ) -> void: + var selection_map := project.selection_map var selection_size := selection_map.get_size() var bounding_rect := Rect2i(points[0], Vector2i.ZERO) for point in points: @@ -138,9 +139,9 @@ func lasso_selection( bounding_rect = bounding_rect.expand(point) if _intersect: if previous_selection_map.is_pixel_selected(point): - selection_map.select_pixel(point, true) + select_pixel(point, project, true) else: - selection_map.select_pixel(point, !_subtract) + select_pixel(point, project, !_subtract) var v := Vector2i() for x in bounding_rect.size.x: @@ -150,9 +151,17 @@ func lasso_selection( if Geometry2D.is_point_in_polygon(v, points): if _intersect: if previous_selection_map.is_pixel_selected(v): - selection_map.select_pixel(v, true) + select_pixel(v, project, true) else: - selection_map.select_pixel(v, !_subtract) + select_pixel(v, project, !_subtract) + + +func select_pixel(point: Vector2i, project: Project, select: bool) -> void: + if Tools.is_placing_tiles(): + var tilemap := project.get_current_cel() as CelTileMap + var cell_position := tilemap.get_cell_position(point) + select_tilemap_cell(tilemap, cell_position, project.selection_map, select) + project.selection_map.select_pixel(point, select) # Bresenham's Algorithm From 0c3facf3762848a6d64f1b81bd060c7fc6c46009 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 4 Dec 2024 04:29:24 +0200 Subject: [PATCH 87/88] Support draw tiles mode in the paint select tool --- src/Tools/SelectionTools/PaintSelect.gd | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Tools/SelectionTools/PaintSelect.gd b/src/Tools/SelectionTools/PaintSelect.gd index ddee4e5a7..d692964b9 100644 --- a/src/Tools/SelectionTools/PaintSelect.gd +++ b/src/Tools/SelectionTools/PaintSelect.gd @@ -99,10 +99,10 @@ func apply_selection(pos: Vector2i) -> void: if _draw_points.size() >= 1: if _intersect: project.selection_map.clear() - paint_selection(project.selection_map, previous_selection_map, _draw_points) + paint_selection(project, previous_selection_map, _draw_points) # Handle mirroring var mirror := mirror_array(_draw_points) - paint_selection(project.selection_map, previous_selection_map, mirror) + paint_selection(project, previous_selection_map, mirror) Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect() else: if !cleared: @@ -114,17 +114,26 @@ func apply_selection(pos: Vector2i) -> void: func paint_selection( - selection_map: SelectionMap, previous_selection_map: SelectionMap, points: Array[Vector2i] + project: Project, previous_selection_map: SelectionMap, points: Array[Vector2i] ) -> void: + var selection_map := project.selection_map var selection_size := selection_map.get_size() for point in points: if point.x < 0 or point.y < 0 or point.x >= selection_size.x or point.y >= selection_size.y: continue if _intersect: if previous_selection_map.is_pixel_selected(point): - selection_map.select_pixel(point, true) + select_pixel(point, project, true) else: - selection_map.select_pixel(point, !_subtract) + select_pixel(point, project, !_subtract) + + +func select_pixel(point: Vector2i, project: Project, select: bool) -> void: + if Tools.is_placing_tiles(): + var tilemap := project.get_current_cel() as CelTileMap + var cell_position := tilemap.get_cell_position(point) + select_tilemap_cell(tilemap, cell_position, project.selection_map, select) + project.selection_map.select_pixel(point, select) # Bresenham's Algorithm From e0e328f967faf2cc339d9199ca0c9678c0cf5d47 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 4 Dec 2024 04:40:54 +0200 Subject: [PATCH 88/88] Support draw tiles mode in the magic wand tool Now all selection tools are supported. They can all move, but not yet resize. --- src/Tools/SelectionTools/MagicWand.gd | 182 ++++++++++++++++++++++---- 1 file changed, 160 insertions(+), 22 deletions(-) diff --git a/src/Tools/SelectionTools/MagicWand.gd b/src/Tools/SelectionTools/MagicWand.gd index 9e9d6d230..2547c438d 100644 --- a/src/Tools/SelectionTools/MagicWand.gd +++ b/src/Tools/SelectionTools/MagicWand.gd @@ -34,10 +34,10 @@ func apply_selection(pos: Vector2i) -> void: var cel_image := Image.new() cel_image.copy_from(_get_draw_image()) - _flood_fill(pos, cel_image, project.selection_map, previous_selection_map) + _flood_fill(pos, cel_image, project, previous_selection_map) # Handle mirroring for mirror_pos in Tools.get_mirrored_positions(pos): - _flood_fill(mirror_pos, cel_image, project.selection_map, previous_selection_map) + _flood_fill(mirror_pos, cel_image, project, previous_selection_map) Global.canvas.selection.big_bounding_rectangle = project.selection_map.get_used_rect() Global.canvas.selection.commit_undo("Select", undo_data) @@ -59,6 +59,39 @@ func update_config() -> void: $ToleranceSlider.value = _tolerance * 255.0 +func _on_tolerance_slider_value_changed(value: float) -> void: + _tolerance = value / 255.0 + update_config() + save_config() + + +func _flood_fill( + pos: Vector2i, image: Image, project: Project, previous_selection_map: SelectionMap +) -> void: + # implements the floodfill routine by Shawn Hargreaves + # from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c + var selection_map := project.selection_map + if Tools.is_placing_tiles(): + for cel in _get_selected_draw_cels(): + if cel is not CelTileMap: + continue + var tile_index := (cel as CelTileMap).get_cell_index_at_coords(pos) + # init flood data structures + _allegro_flood_segments = [] + _allegro_image_segments = [] + _compute_segments_for_tilemap(pos, cel, tile_index) + _select_segments_tilemap(project, previous_selection_map) + return + var color := image.get_pixelv(pos) + # init flood data structures + _allegro_flood_segments = [] + _allegro_image_segments = [] + _compute_segments_for_image(pos, project, image, color) + # now actually color the image: since we have already checked a few things for the points + # we'll process here, we're going to skip a bunch of safety checks to speed things up. + _select_segments(selection_map, previous_selection_map) + + # Add a new segment to the array func _add_new_segment(y := 0) -> void: _allegro_flood_segments.append(Segment.new(y)) @@ -140,22 +173,6 @@ func _check_flooded_segment( return ret -func _flood_fill( - pos: Vector2i, image: Image, selection_map: SelectionMap, previous_selection_map: SelectionMap -) -> void: - # implements the floodfill routine by Shawn Hargreaves - # from https://www1.udel.edu/CIS/software/dist/allegro-4.2.1/src/flood.c - var project := Global.current_project - var color := image.get_pixelv(pos) - # init flood data structures - _allegro_flood_segments = [] - _allegro_image_segments = [] - _compute_segments_for_image(pos, project, image, color) - # now actually color the image: since we have already checked a few things for the points - # we'll process here, we're going to skip a bunch of safety checks to speed things up. - _select_segments(selection_map, previous_selection_map) - - func _compute_segments_for_image( pos: Vector2i, project: Project, image: Image, src_color: Color ) -> void: @@ -201,7 +218,128 @@ func _set_bit(p: Vector2i, selection_map: SelectionMap, prev_selection_map: Sele selection_map.select_pixel(p, !_subtract) -func _on_tolerance_slider_value_changed(value: float) -> void: - _tolerance = value / 255.0 - update_config() - save_config() +func _compute_segments_for_tilemap(pos: Vector2i, cel: CelTileMap, src_index: int) -> void: + # initially allocate at least 1 segment per line of the tilemap + for j in cel.vertical_cells: + _add_new_segment(j) + pos /= cel.tileset.tile_size + # start flood algorithm + _flood_line_around_point_tilemap(pos, cel, src_index) + # test all segments while also discovering more + var done := false + while not done: + done = true + var max_index := _allegro_flood_segments.size() + for c in max_index: + var p := _allegro_flood_segments[c] + if p.todo_below: # check below the segment? + p.todo_below = false + if _check_flooded_segment_tilemap( + p.y + 1, p.left_position, p.right_position, cel, src_index + ): + done = false + if p.todo_above: # check above the segment? + p.todo_above = false + if _check_flooded_segment_tilemap( + p.y - 1, p.left_position, p.right_position, cel, src_index + ): + done = false + + +## Fill an horizontal segment around the specified position, and adds it to the +## list of segments filled. Returns the first x coordinate after the part of the +## line that has been filled. +## Τhis method is called by [method _flood_fill] after the required data structures +## have been initialized. +func _flood_line_around_point_tilemap(pos: Vector2i, cel: CelTileMap, src_index: int) -> int: + if cel.get_cell_index_at_coords_in_tilemap_space(pos) != src_index: + return pos.x + 1 + var west := pos + var east := pos + while west.x >= 0 && cel.get_cell_index_at_coords_in_tilemap_space(west) == src_index: + west += Vector2i.LEFT + while ( + east.x < cel.horizontal_cells + && cel.get_cell_index_at_coords_in_tilemap_space(east) == src_index + ): + east += Vector2i.RIGHT + # Make a note of the stuff we processed + var c := pos.y + var segment := _allegro_flood_segments[c] + # we may have already processed some segments on this y coordinate + if segment.flooding: + while segment.next > 0: + c = segment.next # index of next segment in this line of image + segment = _allegro_flood_segments[c] + # found last current segment on this line + c = _allegro_flood_segments.size() + segment.next = c + _add_new_segment(pos.y) + segment = _allegro_flood_segments[c] + # set the values for the current segment + segment.flooding = true + segment.left_position = west.x + 1 + segment.right_position = east.x - 1 + segment.y = pos.y + segment.next = 0 + # Should we process segments above or below this one? + # when there is a selected area, the pixels above and below the one we started creating this + # segment from may be outside it. It's easier to assume we should be checking for segments + # above and below this one than to specifically check every single pixel in it, because that + # test will be performed later anyway. + # On the other hand, this test we described is the same `project.can_pixel_get_drawn` does if + # there is no selection, so we don't need branching here. + segment.todo_above = pos.y > 0 + segment.todo_below = pos.y < cel.vertical_cells - 1 + # this is an actual segment we should be coloring, so we add it to the results for the + # current image + if segment.right_position >= segment.left_position: + _allegro_image_segments.append(segment) + # we know the point just east of the segment is not part of a segment that should be + # processed, else it would be part of this segment + return east.x + 1 + + +func _check_flooded_segment_tilemap( + y: int, left: int, right: int, cel: CelTileMap, src_index: int +) -> bool: + var ret := false + var c := 0 + while left <= right: + c = y + while true: + var segment := _allegro_flood_segments[c] + if left >= segment.left_position and left <= segment.right_position: + left = segment.right_position + 2 + break + c = segment.next + if c == 0: # couldn't find a valid segment, so we draw a new one + left = _flood_line_around_point_tilemap(Vector2i(left, y), cel, src_index) + ret = true + break + return ret + + +func _select_segments_tilemap(project: Project, previous_selection_map: SelectionMap) -> void: + # short circuit for flat colors + for c in _allegro_image_segments.size(): + var p := _allegro_image_segments[c] + for px in range(p.left_position, p.right_position + 1): + # We don't have to check again whether the point being processed is within the bounds + _set_bit_rect(Vector2i(px, p.y), project, previous_selection_map) + + +func _set_bit_rect(p: Vector2i, project: Project, prev_selection_map: SelectionMap) -> void: + var selection_map := project.selection_map + var tilemap := project.get_current_cel() as CelTileMap + var cell_position := tilemap.get_cell_position_in_tilemap_space(p) + if _intersect: + var image_coords := tilemap.get_cell_coords_in_image(cell_position) + select_tilemap_cell( + tilemap, + cell_position, + project.selection_map, + prev_selection_map.is_pixel_selected(image_coords) + ) + else: + select_tilemap_cell(tilemap, cell_position, project.selection_map, !_subtract)