From a8c41312f83eab40d13b87451b933e8398e7d091 Mon Sep 17 00:00:00 2001
From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com>
Date: Tue, 21 Jan 2025 03:40:43 +0200
Subject: [PATCH] [Experimental] Expose selection rotation gizmo

It's not working perfectly yet so it's possible it may get unexposed in the stable version if many issues are found, especially if it causes regressions to selection moving & resizing. Needs testing.
---
 src/Autoload/DrawingAlgos.gd     |  9 ++++
 src/Classes/ShaderImageEffect.gd |  3 ++
 src/UI/Canvas/Selection.gd       | 80 ++++++++++++++++++++------------
 3 files changed, 62 insertions(+), 30 deletions(-)

diff --git a/src/Autoload/DrawingAlgos.gd b/src/Autoload/DrawingAlgos.gd
index 4b274604f..df0b1cc9e 100644
--- a/src/Autoload/DrawingAlgos.gd
+++ b/src/Autoload/DrawingAlgos.gd
@@ -316,6 +316,15 @@ func type_is_shader(algorithm: RotationAlgorithm) -> bool:
 	return algorithm <= RotationAlgorithm.NNS
 
 
+func transform_rectangle(rect: Rect2, matrix: Transform2D, pivot := rect.size / 2) -> Rect2:
+	var offset_rect := rect
+	var offset_pos := -pivot
+	offset_rect.position = offset_pos
+	offset_rect = offset_rect * matrix
+	offset_rect.position = rect.position + offset_rect.position - offset_pos
+	return offset_rect
+
+
 func rotxel(sprite: Image, angle: float, pivot: Vector2) -> void:
 	if is_zero_approx(angle) or is_equal_approx(angle, TAU):
 		return
diff --git a/src/Classes/ShaderImageEffect.gd b/src/Classes/ShaderImageEffect.gd
index 4ec43313f..51a487e45 100644
--- a/src/Classes/ShaderImageEffect.gd
+++ b/src/Classes/ShaderImageEffect.gd
@@ -56,6 +56,9 @@ func generate_image(
 	RenderingServer.free_rid(ci_rid)
 	RenderingServer.free_rid(mat_rid)
 	RenderingServer.free_rid(texture)
+	if not is_instance_valid(viewport_texture):  # Very rare bug
+		done.emit()
+		return
 	viewport_texture.convert(img.get_format())
 	img.copy_from(viewport_texture)
 	if resized_width:
diff --git a/src/UI/Canvas/Selection.gd b/src/UI/Canvas/Selection.gd
index 538208961..f116f0e2d 100644
--- a/src/UI/Canvas/Selection.gd
+++ b/src/UI/Canvas/Selection.gd
@@ -16,6 +16,7 @@ var is_moving_content := false:
 		is_moving_content_changed.emit()
 var arrow_key_move := false
 var is_pasting := false
+## The bounding rectangle of the selection. Always has a non-negative size.
 var big_bounding_rectangle := Rect2i():
 	set(value):
 		big_bounding_rectangle = value
@@ -30,9 +31,15 @@ var big_bounding_rectangle := Rect2i():
 		_update_gizmos()
 var image_current_pixel := Vector2.ZERO  ## The pixel coordinates of the cursor
 
+## Same as [member big_bounding_rectangle], but allows for negative sizes during transformations,
+## to check if the selected content should be flipped.
 var temp_rect := Rect2()
 var rect_aspect_ratio := 0.0
 var temp_rect_pivot := Vector2.ZERO
+## A [Rect2i] that is used during resizing.
+## Together with a transformation matrix constructed by [member angle], it determines the final
+## size of [member big_bounding_rectangle] at the end of the transformation.
+var resized_rect := Rect2i()
 
 var original_big_bounding_rectangle := Rect2i()
 var original_preview_image := Image.new()
@@ -46,7 +53,7 @@ var undo_data: Dictionary
 var gizmos: Array[Gizmo] = []
 var dragged_gizmo: Gizmo = null
 var angle := 0.0
-var rotation_algorithm := DrawingAlgos.RotationAlgorithm.NN
+var rotation_algorithm := DrawingAlgos.RotationAlgorithm.NNS
 var content_pivot := Vector2.ZERO
 var mouse_pos_on_gizmo_drag := Vector2.ZERO
 var resize_keep_ratio := false
@@ -100,7 +107,7 @@ func _ready() -> void:
 	gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2i(0, 1)))  # Center bottom
 	gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2i(-1, 1)))  # Bottom left
 	gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2i(-1, 0)))  # Center left
-	#gizmos.append(Gizmo.new(Gizmo.Type.ROTATE)) # Rotation gizmo (temp)
+	gizmos.append(Gizmo.new(Gizmo.Type.ROTATE))  # Rotation gizmo (temp)
 
 
 func _input(event: InputEvent) -> void:
@@ -149,7 +156,8 @@ func _input(event: InputEvent) -> void:
 					var vertical_flip := signi(temp_rect.size.y)
 					dragged_gizmo.direction.x *= horizontal_flip
 					dragged_gizmo.direction.y *= vertical_flip
-					temp_rect = big_bounding_rectangle
+					resized_rect.position = big_bounding_rectangle.position
+					temp_rect = resized_rect
 					# If temp_rect had negative size, switch the position and end points
 					if horizontal_flip < 0:
 						var pos := temp_rect.position.x
@@ -298,9 +306,9 @@ func _update_gizmos() -> void:
 	)
 
 	# Rotation gizmo (temp)
-	#gizmos[8].rect = Rect2(
-	#Vector2((rect_end.x + rect_pos.x - size.x) / 2, rect_pos.y - size.y - (size.y * 2)), size
-	#)
+	gizmos[8].rect = Rect2(
+		Vector2((rect_end.x + rect_pos.x - size.x) / 2, rect_pos.y - size.y - (size.y * 2)), size
+	)
 	queue_redraw()
 
 
@@ -356,13 +364,12 @@ func _gizmo_resize() -> void:
 		if dir == Vector2i(-1, -1):  # Top left corner
 			temp_rect.position.y = end_y - temp_rect.size.y
 
-	big_bounding_rectangle = temp_rect.abs()
-	if big_bounding_rectangle.size.x == 0:
-		big_bounding_rectangle.size.x = 1
-	if big_bounding_rectangle.size.y == 0:
-		big_bounding_rectangle.size.y = 1
+	resized_rect = temp_rect.abs()
+	if resized_rect.size.x == 0:
+		resized_rect.size.x = 1
+	if resized_rect.size.y == 0:
+		resized_rect.size.y = 1
 
-	big_bounding_rectangle = big_bounding_rectangle  # Call the setter method
 	resize_selection()
 
 
@@ -383,11 +390,15 @@ func _resize_rect(pos: Vector2, dir: Vector2) -> void:
 
 
 func resize_selection() -> void:
-	var size := big_bounding_rectangle.size.abs()
+	var project := Global.current_project
+	var transformation_matrix := Transform2D(angle, Vector2.ZERO)
+	big_bounding_rectangle = DrawingAlgos.transform_rectangle(resized_rect, transformation_matrix)
+	var size := resized_rect.size.abs()
+	content_pivot = size / 2.0
 	if original_bitmap.is_empty():
 		print("original_bitmap is empty, this shouldn't happen.")
 	else:
-		Global.current_project.selection_map.copy_from(original_bitmap)
+		project.selection_map.copy_from(original_bitmap)
 	if is_moving_content:
 		preview_image.copy_from(original_preview_image)
 		if Tools.is_placing_tiles():
@@ -405,32 +416,41 @@ func resize_selection() -> void:
 					preview_image, selected_cells, big_bounding_rectangle
 				)
 		else:
-			content_pivot = original_big_bounding_rectangle.size / 2.0
-			var transformation_matrix := Transform2D(angle, Vector2.ZERO)
 			var params := {"transformation_matrix": transformation_matrix, "pivot": content_pivot}
-			DrawingAlgos.transform(preview_image, params, rotation_algorithm)
 			preview_image.resize(size.x, size.y, Image.INTERPOLATE_NEAREST)
+			DrawingAlgos.transform(preview_image, params, rotation_algorithm, true)
 			if temp_rect.size.x < 0:
 				preview_image.flip_x()
 			if temp_rect.size.y < 0:
 				preview_image.flip_y()
 		preview_image_texture = ImageTexture.create_from_image(preview_image)
 
-	Global.current_project.selection_map.copy_from(original_bitmap)
+	project.selection_map.copy_from(original_bitmap)
 
-	var bitmap_pivot := original_big_bounding_rectangle.get_center()
-	var bitmap_matrix := Transform2D(angle, Vector2.ZERO)
-	var bitmap_params := {"transformation_matrix": bitmap_matrix, "pivot": bitmap_pivot}
-	DrawingAlgos.transform(Global.current_project.selection_map, bitmap_params, rotation_algorithm)
-	Global.current_project.selection_map.resize_bitmap_values(
-		Global.current_project, size, temp_rect.size.x < 0, temp_rect.size.y < 0
+	var bitmap_params := {"transformation_matrix": transformation_matrix, "pivot": content_pivot}
+	project.selection_map.resize_bitmap_values(
+		project, size, temp_rect.size.x < 0, temp_rect.size.y < 0
 	)
-	Global.current_project.selection_map_changed()
+	var transformed_map := Image.new()
+	transformed_map = project.selection_map.get_region(project.selection_map.get_used_rect())
+	project.selection_map.clear()
+	DrawingAlgos.transform(transformed_map, bitmap_params, rotation_algorithm, true)
+	var dst := big_bounding_rectangle.position
+	if dst.x < 0:
+		dst.x = 0
+	if dst.y < 0:
+		dst.y = 0
+	project.selection_map.blit_rect(
+		transformed_map, Rect2i(Vector2i.ZERO, project.selection_map.get_size()), dst
+	)
+	project.selection_map_changed()
 	queue_redraw()
 	canvas.queue_redraw()
 
 
 func _gizmo_rotate() -> void:
+	if Tools.is_placing_tiles():
+		return
 	var pivot_in_world_coords := content_pivot + Vector2(big_bounding_rectangle.position)
 	angle = image_current_pixel.angle_to_point(pivot_in_world_coords) - PI / 2
 	angle = snappedf(angle, PI / 64)
@@ -515,6 +535,7 @@ func transform_content_start() -> void:
 		return
 	undo_data = get_undo_data(true)
 	temp_rect = big_bounding_rectangle
+	resized_rect = big_bounding_rectangle
 	_get_preview_image()
 	if original_preview_image.is_empty():
 		undo_data = get_undo_data(false)
@@ -564,10 +585,9 @@ func transform_content_confirm() -> void:
 				var params := {
 					"transformation_matrix": transformation_matrix, "pivot": content_pivot
 				}
-				DrawingAlgos.transform(src, params, rotation_algorithm)
-				src.resize(
-					preview_image.get_width(), preview_image.get_height(), Image.INTERPOLATE_NEAREST
-				)
+				var size := resized_rect.size.abs()
+				src.resize(size.x, size.y, Image.INTERPOLATE_NEAREST)
+				DrawingAlgos.transform(src, params, rotation_algorithm, true)
 				if temp_rect.size.x < 0:
 					src.flip_x()
 				if temp_rect.size.y < 0:
@@ -587,7 +607,6 @@ func transform_content_confirm() -> void:
 				big_bounding_rectangle.position
 			)
 		cel_image.convert_rgb_to_indexed()
-	project.selection_map.move_bitmap_values(project)
 	commit_undo("Move Selection", undo_data)
 
 	original_preview_image = Image.new()
@@ -852,6 +871,7 @@ func paste(in_place := false) -> void:
 			)
 	big_bounding_rectangle = big_bounding_rectangle
 	temp_rect = big_bounding_rectangle
+	resized_rect = big_bounding_rectangle
 	is_moving_content = true
 	is_pasting = true
 	original_preview_image = clipboard.image