From c0a82021450595d5021f6e5d47c4026c9852fc14 Mon Sep 17 00:00:00 2001
From: Emmanouil Papadeas <manoschool@yahoo.gr>
Date: Wed, 6 Dec 2023 03:22:33 +0200
Subject: [PATCH] Add cel properties and z-index to individual cels

---
 Translations/Translations.pot    |  5 ++-
 src/Autoload/DrawingAlgos.gd     | 11 ++++---
 src/Classes/Cels/BaseCel.gd      |  9 ++++--
 src/Classes/Project.gd           | 32 ++++++++++++++++---
 src/UI/Canvas/Canvas.gd          | 50 ++++++++++++++++++-----------
 src/UI/Canvas/CanvasPreview.gd   |  2 +-
 src/UI/Timeline/CelButton.gd     | 16 +++++++++-
 src/UI/Timeline/CelButton.tscn   | 54 +++++++++++++++++++++++++++-----
 src/UI/Timeline/FrameButton.gd   |  2 +-
 src/UI/Timeline/FrameButton.tscn | 14 ++++-----
 10 files changed, 146 insertions(+), 49 deletions(-)

diff --git a/Translations/Translations.pot b/Translations/Translations.pot
index ccdeaa68f..d8a6bd01f 100644
--- a/Translations/Translations.pot
+++ b/Translations/Translations.pot
@@ -1815,12 +1815,15 @@ msgstr ""
 msgid "Unlink Cels"
 msgstr ""
 
-msgid "Frame Properties"
+msgid "Properties"
 msgstr ""
 
 msgid "Frame properties"
 msgstr ""
 
+msgid "Cel properties"
+msgstr ""
+
 #. Found on the popup menu that appears when a user right-clicks on a frame button. When clicked, the order of the selected frames is being reversed.
 msgid "Reverse Frames"
 msgstr ""
diff --git a/src/Autoload/DrawingAlgos.gd b/src/Autoload/DrawingAlgos.gd
index 8c550e67c..e8ec9475f 100644
--- a/src/Autoload/DrawingAlgos.gd
+++ b/src/Autoload/DrawingAlgos.gd
@@ -21,22 +21,23 @@ func blend_layers(
 	# the second are the opacities and the third are the origins
 	var metadata_image := Image.create(project.layers.size(), 3, false, Image.FORMAT_R8)
 	for i in project.layers.size():
-		var layer := project.layers[i]
+		var ordered_index := project.ordered_layers[i]
+		var layer := project.layers[ordered_index]
 		var include := true if layer.is_visible_in_hierarchy() else false
 		if only_selected and include:
 			var test_array := [project.frames.find(frame), i]
 			if not test_array in project.selected_cels:
 				include = false
-		var cel := frame.cels[i]
+		var cel := frame.cels[ordered_index]
 		var cel_image := layer.display_effects(cel)
 		textures.append(cel_image)
 		# Store the blend mode
-		metadata_image.set_pixel(i, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0))
+		metadata_image.set_pixel(ordered_index, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0))
 		# Store the opacity
 		if include:
-			metadata_image.set_pixel(i, 1, Color(cel.opacity, 0.0, 0.0, 0.0))
+			metadata_image.set_pixel(ordered_index, 1, Color(cel.opacity, 0.0, 0.0, 0.0))
 		else:
-			metadata_image.set_pixel(i, 1, Color())
+			metadata_image.set_pixel(ordered_index, 1, Color())
 	var texture_array := Texture2DArray.new()
 	texture_array.create_from_images(textures)
 	var params := {
diff --git a/src/Classes/Cels/BaseCel.gd b/src/Classes/Cels/BaseCel.gd
index 8e0d08a8b..12d31739b 100644
--- a/src/Classes/Cels/BaseCel.gd
+++ b/src/Classes/Cels/BaseCel.gd
@@ -1,9 +1,9 @@
 class_name BaseCel
 extends RefCounted
 ## Base class for cel properties.
-## The term "cel" comes from "celluloid" (https://en.wikipedia.org/wiki/Cel).
+## "Cel" is short for the term "celluloid" [url]https://en.wikipedia.org/wiki/Cel[/url].
 
-signal texture_changed  ## Emitted whenever cel's tecture is changed
+signal texture_changed  ## Emitted whenever the cel's texture is changed
 
 var opacity := 1.0  ## Opacity/Transparency of the cel.
 ## The image stored in the cel.
@@ -14,6 +14,7 @@ var image_texture: Texture2D:
 ## [br] If the cel is not linked then it is [code]null[/code].
 var link_set = null  # { "cels": Array, "hue": float } or null
 var transformed_content: Image  ## Used in transformations (moving, scaling etc with selections).
+var z_index := 0
 
 # Methods to Override:
 
@@ -70,12 +71,14 @@ func update_texture() -> void:
 
 ## Returns a curated [Dictionary] containing the cel data.
 func serialize() -> Dictionary:
-	return {"opacity": opacity}
+	return {"opacity": opacity, "z_index": z_index}
 
 
 ## Sets the cel data according to a curated [Dictionary] obtained from [method serialize].
 func deserialize(dict: Dictionary) -> void:
 	opacity = dict["opacity"]
+	if dict.has("z_index"):
+		z_index = dict["z_index"]
 
 
 ## Used to perform cleanup after a cel is removed.
diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd
index 2d5d10065..f649b5f79 100644
--- a/src/Classes/Project.gd
+++ b/src/Classes/Project.gd
@@ -33,14 +33,15 @@ var frames: Array[Frame] = []
 var layers: Array[BaseLayer] = []
 var current_frame := 0
 var current_layer := 0
-var selected_cels := [[0, 0]]  # Array of Arrays of 2 integers (frame & layer)
+var selected_cels := [[0, 0]]  ## Array of Arrays of 2 integers (frame & layer)
+var ordered_layers: Array[int] = [0]
 
 var animation_tags: Array[AnimationTag] = []:
 	set = _animation_tags_changed
 var guides: Array[Guide] = []
 var brushes: Array[Image] = []
 var reference_images: Array[ReferenceImage] = []
-var vanishing_points := []  # Array of Vanishing Points
+var vanishing_points := []  ## Array of Vanishing Points
 var fps := 6.0
 
 var x_symmetry_point: float
@@ -49,15 +50,15 @@ var x_symmetry_axis := SymmetryGuide.new()
 var y_symmetry_axis := SymmetryGuide.new()
 
 var selection_map := SelectionMap.new()
-# This is useful for when the selection is outside of the canvas boundaries,
-# on the left and/or above (negative coords)
+## This is useful for when the selection is outside of the canvas boundaries,
+## on the left and/or above (negative coords)
 var selection_offset := Vector2i.ZERO:
 	set(value):
 		selection_offset = value
 		Global.canvas.selection.marching_ants_outline.offset = selection_offset
 var has_selection := false
 
-# For every camera (currently there are 3)
+## For every camera (currently there are 3)
 var cameras_rotation: PackedFloat32Array = [0.0, 0.0, 0.0]
 var cameras_zoom: PackedVector2Array = [
 	Vector2(0.15, 0.15), Vector2(0.15, 0.15), Vector2(0.15, 0.15)
@@ -436,6 +437,7 @@ func deserialize(dict: Dictionary) -> void:
 	if dict.has("fps"):
 		fps = dict.fps
 	_deserialize_metadata(self, dict)
+	order_layers()
 
 
 func _serialize_metadata(object: Object) -> Dictionary:
@@ -613,6 +615,25 @@ func find_first_drawable_cel(frame := frames[current_frame]) -> BaseCel:
 	return result
 
 
+func order_layers(frame_index := current_frame) -> void:
+	ordered_layers = []
+	for i in layers.size():
+		ordered_layers.append(i)
+	ordered_layers.sort_custom(_z_index_sort.bind(frame_index))
+
+
+func _z_index_sort(a: int, b: int, frame_index: int) -> bool:
+	var z_index_a := frames[frame_index].cels[a].z_index
+	var z_index_b := frames[frame_index].cels[b].z_index
+	var layer_index_a := layers[a].index + z_index_a
+	var layer_index_b := layers[b].index + z_index_b
+	if layer_index_a < layer_index_b:
+		return true
+	if layer_index_a == layer_index_b and z_index_a < z_index_b:
+		return true
+	return false
+
+
 # Timeline modifications
 # Modifying layers or frames Arrays on the current project should generally only be done
 # through these methods.
@@ -832,6 +853,7 @@ func _update_frame_ui() -> void:
 
 ## Update the layer indices and layer/cel buttons
 func _update_layer_ui() -> void:
+	order_layers()
 	for l in layers.size():
 		layers[l].index = l
 		Global.layer_vbox.get_child(layers.size() - 1 - l).layer_index = l
diff --git a/src/UI/Canvas/Canvas.gd b/src/UI/Canvas/Canvas.gd
index 89f560190..f38b8eaac 100644
--- a/src/UI/Canvas/Canvas.gd
+++ b/src/UI/Canvas/Canvas.gd
@@ -29,6 +29,8 @@ var layer_metadata_texture := ImageTexture.new()
 
 
 func _ready() -> void:
+	material.set_shader_parameter("layers", layer_texture_array)
+	material.set_shader_parameter("metadata", layer_metadata_texture)
 	Global.project_changed.connect(queue_redraw)
 	onion_past.type = onion_past.PAST
 	onion_past.blue_red_color = Global.onion_skinning_past_color
@@ -124,7 +126,7 @@ func update_texture(layer_i: int, frame_i := -1, project := Global.current_proje
 			cel_image.get_size()
 			== Vector2i(layer_texture_array.get_width(), layer_texture_array.get_height())
 		):
-			layer_texture_array.update_layer(cel_image, layer_i)
+			layer_texture_array.update_layer(cel_image, project.ordered_layers[layer_i])
 
 
 func update_selected_cels_textures(project := Global.current_project) -> void:
@@ -146,60 +148,72 @@ func draw_layers() -> void:
 	)
 	if recreate_texture_array:
 		var textures: Array[Image] = []
+		textures.resize(project.layers.size())
 		# Nx3 texture, where N is the number of layers and the first row are the blend modes,
 		# the second are the opacities and the third are the origins
 		layer_metadata_image = Image.create(project.layers.size(), 3, false, Image.FORMAT_RG8)
 		# Draw current frame layers
 		for i in project.layers.size():
+			var ordered_index := project.ordered_layers[i]
 			var layer := project.layers[i]
+			var cel := current_cels[i]
 			var cel_image: Image
 			if Global.display_layer_effects:
-				cel_image = layer.display_effects(current_cels[i])
+				cel_image = layer.display_effects(cel)
 			else:
-				cel_image = current_cels[i].get_image()
-			textures.append(cel_image)
+				cel_image = cel.get_image()
+			textures[ordered_index] = cel_image
 			# Store the blend mode
-			layer_metadata_image.set_pixel(i, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0))
+			layer_metadata_image.set_pixel(
+				ordered_index, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0)
+			)
 			# Store the opacity
 			if layer.is_visible_in_hierarchy():
-				layer_metadata_image.set_pixel(i, 1, Color(current_cels[i].opacity, 0.0, 0.0, 0.0))
+				layer_metadata_image.set_pixel(ordered_index, 1, Color(cel.opacity, 0.0, 0.0, 0.0))
 			else:
-				layer_metadata_image.set_pixel(i, 1, Color())
+				layer_metadata_image.set_pixel(ordered_index, 1, Color())
 			# Store the origin
 			if [project.current_frame, i] in project.selected_cels:
 				var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size())
-				layer_metadata_image.set_pixel(i, 2, Color(origin.x, origin.y, 0.0, 0.0))
+				layer_metadata_image.set_pixel(
+					ordered_index, 2, Color(origin.x, origin.y, 0.0, 0.0)
+				)
 			else:
-				layer_metadata_image.set_pixel(i, 2, Color())
+				layer_metadata_image.set_pixel(ordered_index, 2, Color())
 
 		layer_texture_array.create_from_images(textures)
 		layer_metadata_texture.set_image(layer_metadata_image)
 	else:  # Update the TextureArray
 		if layer_texture_array.get_layers() > 0:
 			for i in project.layers.size():
-				var layer := project.layers[i]
-				var test_array := [project.current_frame, i]
 				if not update_all_layers:
+					var test_array := [project.current_frame, i]
 					if not test_array in project.selected_cels:
 						continue
+				var ordered_index := project.ordered_layers[i]
+				var layer := project.layers[i]
 				var cel := current_cels[i]
 				var cel_image: Image
 				if Global.display_layer_effects:
 					cel_image = layer.display_effects(cel)
 				else:
 					cel_image = cel.get_image()
-				layer_texture_array.update_layer(cel_image, i)
-				layer_metadata_image.set_pixel(i, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0))
+				layer_texture_array.update_layer(cel_image, ordered_index)
+				layer_metadata_image.set_pixel(
+					ordered_index, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0)
+				)
 				if layer.is_visible_in_hierarchy():
-					layer_metadata_image.set_pixel(i, 1, Color(cel.opacity, 0.0, 0.0, 0.0))
+					layer_metadata_image.set_pixel(
+						ordered_index, 1, Color(cel.opacity, 0.0, 0.0, 0.0)
+					)
 				else:
-					layer_metadata_image.set_pixel(i, 1, Color())
+					layer_metadata_image.set_pixel(ordered_index, 1, Color())
 				var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size())
-				layer_metadata_image.set_pixel(i, 2, Color(origin.x, origin.y, 0.0, 0.0))
+				layer_metadata_image.set_pixel(
+					ordered_index, 2, Color(origin.x, origin.y, 0.0, 0.0)
+				)
 			layer_metadata_texture.update(layer_metadata_image)
 
-	material.set_shader_parameter("layers", layer_texture_array)
-	material.set_shader_parameter("metadata", layer_metadata_texture)
 	material.set_shader_parameter("origin_x_positive", move_preview_location.x > 0)
 	material.set_shader_parameter("origin_y_positive", move_preview_location.y > 0)
 	update_all_layers = false
diff --git a/src/UI/Canvas/CanvasPreview.gd b/src/UI/Canvas/CanvasPreview.gd
index 63d7ca7c4..888b7aff7 100644
--- a/src/UI/Canvas/CanvasPreview.gd
+++ b/src/UI/Canvas/CanvasPreview.gd
@@ -80,7 +80,7 @@ func _draw_layers() -> void:
 	# the second are the opacities and the third are the origins
 	var metadata_image := Image.create(project.layers.size(), 3, false, Image.FORMAT_R8)
 	# Draw current frame layers
-	for i in project.layers.size():
+	for i in project.ordered_layers:
 		if current_cels[i] is GroupCel:
 			continue
 		var layer := project.layers[i]
diff --git a/src/UI/Timeline/CelButton.gd b/src/UI/Timeline/CelButton.gd
index cd3a224eb..e0fc0861f 100644
--- a/src/UI/Timeline/CelButton.gd
+++ b/src/UI/Timeline/CelButton.gd
@@ -1,6 +1,6 @@
 extends Button
 
-enum MenuOptions { DELETE, LINK, UNLINK, PROPERTIES }
+enum MenuOptions { PROPERTIES, DELETE, LINK, UNLINK }
 
 var frame := 0
 var layer := 0
@@ -10,6 +10,7 @@ var cel: BaseCel
 @onready var linked_indicator: Polygon2D = get_node_or_null("LinkedIndicator")
 @onready var cel_texture: TextureRect = $CelTexture
 @onready var transparent_checker: ColorRect = $CelTexture/TransparentChecker
+@onready var properties: AcceptDialog = $Properties
 
 
 func _ready() -> void:
@@ -98,6 +99,8 @@ func _on_CelButton_pressed() -> void:
 
 func _on_PopupMenu_id_pressed(id: int) -> void:
 	match id:
+		MenuOptions.PROPERTIES:
+			properties.popup_centered()
 		MenuOptions.DELETE:
 			_delete_cel_content()
 
@@ -294,3 +297,14 @@ func _get_region_rect(x_begin: float, x_end: float) -> Rect2:
 	rect.position.x += rect.size.x * x_begin
 	rect.size.x *= x_end - x_begin
 	return rect
+
+
+func _on_z_index_slider_value_changed(value: float) -> void:
+	cel.z_index = value
+	Global.current_project.order_layers()
+	Global.canvas.update_all_layers = true
+	Global.canvas.queue_redraw()
+
+
+func _on_properties_visibility_changed() -> void:
+	Global.dialog_open(properties.visible)
diff --git a/src/UI/Timeline/CelButton.tscn b/src/UI/Timeline/CelButton.tscn
index e068d78b1..66df574f7 100644
--- a/src/UI/Timeline/CelButton.tscn
+++ b/src/UI/Timeline/CelButton.tscn
@@ -1,8 +1,9 @@
-[gd_scene load_steps=5 format=3 uid="uid://dw7ci3uixjuev"]
+[gd_scene load_steps=6 format=3 uid="uid://dw7ci3uixjuev"]
 
 [ext_resource type="Script" path="res://src/UI/Timeline/CelButton.gd" id="1_iewgo"]
 [ext_resource type="PackedScene" uid="uid://3pmb60gpst7b" path="res://src/UI/Nodes/TransparentChecker.tscn" id="2_mi8wp"]
 [ext_resource type="Shader" path="res://src/Shaders/TransparentChecker.gdshader" id="3_qv21g"]
+[ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="4_wcpcc"]
 
 [sub_resource type="ShaderMaterial" id="1"]
 shader = ExtResource("3_qv21g")
@@ -53,13 +54,15 @@ grow_horizontal = 2
 grow_vertical = 2
 
 [node name="PopupMenu" type="PopupMenu" parent="."]
-item_count = 3
-item_0/text = "Delete"
-item_0/id = -1
-item_1/text = "Link Cels to"
-item_1/id = -1
-item_2/text = "Unlink Cels"
+item_count = 4
+item_0/text = "Properties"
+item_0/id = 0
+item_1/text = "Delete"
+item_1/id = 1
+item_2/text = "Link Cels to"
 item_2/id = 2
+item_3/text = "Unlink Cels"
+item_3/id = 3
 
 [node name="LinkedIndicator" type="Polygon2D" parent="."]
 color = Color(0, 1, 0, 1)
@@ -67,6 +70,43 @@ invert_enabled = true
 invert_border = 1.0
 polygon = PackedVector2Array(0, 0, 36, 0, 36, 36, 0, 36)
 
+[node name="Properties" type="AcceptDialog" parent="."]
+title = "Cel properties"
+size = Vector2i(300, 100)
+exclusive = false
+popup_window = true
+
+[node name="GridContainer" type="GridContainer" parent="Properties"]
+offset_left = 8.0
+offset_top = 8.0
+offset_right = 292.0
+offset_bottom = 55.0
+columns = 2
+
+[node name="Label" type="Label" parent="Properties/GridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Z-Index:"
+
+[node name="ZIndexSlider" type="TextureProgressBar" parent="Properties/GridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+focus_mode = 2
+mouse_default_cursor_shape = 2
+theme_type_variation = &"ValueSlider"
+min_value = -64.0
+max_value = 64.0
+allow_greater = true
+allow_lesser = true
+nine_patch_stretch = true
+stretch_margin_left = 3
+stretch_margin_top = 3
+stretch_margin_right = 3
+stretch_margin_bottom = 3
+script = ExtResource("4_wcpcc")
+
 [connection signal="pressed" from="." to="." method="_on_CelButton_pressed"]
 [connection signal="resized" from="." to="." method="_on_CelButton_resized"]
 [connection signal="id_pressed" from="PopupMenu" to="." method="_on_PopupMenu_id_pressed"]
+[connection signal="visibility_changed" from="Properties" to="." method="_on_properties_visibility_changed"]
+[connection signal="value_changed" from="Properties/GridContainer/ZIndexSlider" to="." method="_on_z_index_slider_value_changed"]
diff --git a/src/UI/Timeline/FrameButton.gd b/src/UI/Timeline/FrameButton.gd
index fe3f68074..f50074fa8 100644
--- a/src/UI/Timeline/FrameButton.gd
+++ b/src/UI/Timeline/FrameButton.gd
@@ -1,6 +1,6 @@
 extends Button
 
-enum { REMOVE, CLONE, MOVE_LEFT, MOVE_RIGHT, PROPERTIES, REVERSE, CENTER }
+enum { PROPERTIES, REMOVE, CLONE, MOVE_LEFT, MOVE_RIGHT, REVERSE, CENTER }
 
 var frame := 0
 
diff --git a/src/UI/Timeline/FrameButton.tscn b/src/UI/Timeline/FrameButton.tscn
index 1a822cbf1..39459e6c8 100644
--- a/src/UI/Timeline/FrameButton.tscn
+++ b/src/UI/Timeline/FrameButton.tscn
@@ -14,19 +14,19 @@ script = ExtResource("1")
 
 [node name="PopupMenu" type="PopupMenu" parent="."]
 item_count = 7
-item_0/text = "Remove Frame"
+item_0/text = "Properties"
 item_0/id = -1
-item_0/disabled = true
-item_1/text = "Clone Frame"
+item_1/text = "Remove Frame"
 item_1/id = -1
-item_2/text = "Move Left"
+item_1/disabled = true
+item_2/text = "Clone Frame"
 item_2/id = -1
-item_2/disabled = true
-item_3/text = "Move Right"
+item_3/text = "Move Left"
 item_3/id = -1
 item_3/disabled = true
-item_4/text = "Frame Properties"
+item_4/text = "Move Right"
 item_4/id = -1
+item_4/disabled = true
 item_5/text = "Reverse Frames"
 item_5/id = 5
 item_5/disabled = true