1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-03-15 15:55:18 +00:00
Pixelorama/src/UI/Canvas/ReferenceImages.gd
2024-03-23 04:36:26 +02:00

335 lines
10 KiB
GDScript

extends Node2D
## This node contains [ReferenceImage] nodes
signal reference_image_changed(index: int)
enum Mode { SELECT, MOVE, ROTATE, SCALE }
var mode: Mode = Mode.SELECT
var index: int:
get:
return Global.current_project.reference_index
var drag_start_pos: Vector2
var dragging := false
var lmb_held := false ## Holds whether the LBB is being held (use dragging for actual checks)
# Original Transform
var og_pos: Vector2
var og_scale: Vector2
var og_rotation: float
var undo_data: Dictionary
var reference_menu := PopupMenu.new()
func _ready() -> void:
Global.camera.zoom_changed.connect(_update_on_zoom)
Global.control.get_node("Dialogs").add_child(reference_menu)
# Makes sure that the dark overlay disappears when the popup is hidden
reference_menu.visibility_changed.connect(func(): Global.dialog_open(reference_menu.visible))
# Emitted when a item is selected from the menu
reference_menu.id_pressed.connect(_reference_menu_id_pressed)
## Updates the index and configures the "gizmo"
func update_index(new_index: int) -> void:
index = new_index
reference_image_changed.emit(new_index)
queue_redraw()
func _input(event: InputEvent) -> void:
var local_mouse_pos := get_local_mouse_position()
# Check if that event was for the quick menu (opened by the shortcut)
if event.is_action_pressed("reference_quick_menu"):
var list: Array[ReferenceImage] = Global.current_project.reference_images
populate_reference_menu(list, true)
reference_menu.position = Global.control.get_global_mouse_position()
reference_menu.popup()
var ri: ReferenceImage = Global.current_project.get_current_reference_image()
if !ri:
return
# Check if want to cancelthe reference transform
if event.is_action_pressed("cancel_reference_transform") and dragging:
ri.position = og_pos
ri.scale = og_scale
ri.rotation = og_rotation
dragging = false
Global.can_draw = true
commit_undo("Cancel Transform Content", undo_data)
return
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
if event.is_pressed():
dragging = false
lmb_held = true
undo_data = get_undo_data()
Global.can_draw = false
drag_start_pos = get_local_mouse_position()
# Set the original positions
og_pos = ri.position
og_scale = ri.scale
og_rotation = ri.rotation
if !event.is_pressed():
Global.can_draw = true
if dragging:
commit_undo("Transform Content", undo_data)
else:
# Overlapping reference images
var overlapping: Array[ReferenceImage] = []
for idx: int in Global.current_project.reference_images.size():
var r := Global.current_project.reference_images[idx]
# The bounding polygon
var p := get_reference_polygon(idx)
if Geometry2D.is_point_in_polygon(local_mouse_pos, p):
overlapping.append(r)
# Some special cases
# 1. There is only one Reference Image
if overlapping.size() == 1:
var idx := overlapping[0].get_index()
Global.current_project.set_reference_image_index(idx)
# 2. There are more than 1 Reference Images
elif overlapping.size() > 1:
populate_reference_menu(overlapping, true)
reference_menu.position = Global.control.get_global_mouse_position()
reference_menu.popup()
# 3. There are no Reference Images
else:
Global.current_project.set_reference_image_index(-1)
undo_data.clear()
dragging = false
lmb_held = false
if event is InputEventMouseMotion:
# We check if the LMB is pressed and if we're not dragging then we force the
# draggin state.
# We dont use timers because it makes more sense to wait for the users mouse to move
# and that's what defines dragging. It would be smart to add a "deadzone" to determine
# if the mouse had moved enough.
if lmb_held and !dragging:
dragging = true
if dragging:
var text := ""
if mode == Mode.SELECT:
# Scale
if Input.is_action_pressed("reference_scale"):
scale_reference_image(local_mouse_pos, ri)
text = str(
"Moving: ", (og_scale * 100).floor(), " -> ", (ri.scale * 100).floor()
)
# Rotate
elif Input.is_action_pressed("reference_rotate"):
rotate_reference_image(local_mouse_pos, ri)
text = str(
"Rotating: ",
floorf(rad_to_deg(og_rotation)),
"° -> ",
floorf(rad_to_deg(ri.rotation)),
"°"
)
else:
move_reference_image(local_mouse_pos, ri)
text = str("Moving to: ", og_pos.floor(), " -> ", ri.position.floor())
elif mode == Mode.MOVE:
move_reference_image(local_mouse_pos, ri)
text = str("Moving to: ", og_pos.floor(), " -> ", ri.position.floor())
elif mode == Mode.ROTATE:
rotate_reference_image(local_mouse_pos, ri)
text = str(
"Rotating: ",
floorf(rad_to_deg(og_rotation)),
"° -> ",
floorf(rad_to_deg(ri.rotation)),
"°"
)
elif mode == Mode.SCALE:
scale_reference_image(local_mouse_pos, ri)
text = str("Moving: ", (og_scale * 100).floor(), " -> ", (ri.scale * 100).floor())
Global.cursor_position_label.text = text
queue_redraw()
## Uniformly scales the [ReferenceImage] using this nodes "local_mouse_position".
func scale_reference_image(mouse_pos: Vector2, img: ReferenceImage) -> void:
var s := (
Vector2.ONE
* minf(
float(mouse_pos.x - drag_start_pos.x),
float(mouse_pos.y - drag_start_pos.y),
)
)
img.scale = (og_scale + (s / 100.0))
## Rotate the [ReferenceImage] using this nodes "local_mouse_position".
func rotate_reference_image(mouse_pos: Vector2, img: ReferenceImage) -> void:
var starting_angle := og_rotation - og_pos.angle_to_point(drag_start_pos)
var new_angle := img.position.angle_to_point(mouse_pos)
var angle := starting_angle + new_angle
angle = deg_to_rad(floorf(rad_to_deg(wrapf(angle, -PI, PI))))
img.rotation = angle
## Move the [ReferenceImage] using this nodes "local_mouse_position".
func move_reference_image(mouse_pos: Vector2, img: ReferenceImage) -> void:
img.position = (mouse_pos - (drag_start_pos - og_pos)).floor()
## Makes a polygon that matches the transformed [ReferenceImage]
func get_reference_polygon(i: int) -> PackedVector2Array:
if i < 0:
return []
var ri: ReferenceImage = Global.current_project.reference_images[i]
var rect := ri.get_rect()
var poly := get_transformed_rect_polygon(rect, ri.transform)
return poly
## Returns a [PackedVector2Array] based on the corners of the [Rect2].
## This function also transforms the polygon.
func get_transformed_rect_polygon(rect: Rect2, t: Transform2D) -> PackedVector2Array:
# First we scale the Rect2
rect.position *= t.get_scale()
rect.size *= t.get_scale()
# We create a polygon based on the Rect2
var p: PackedVector2Array = [
rect.position,
Vector2(rect.end.x, rect.position.y),
rect.end,
Vector2(rect.position.x, rect.end.y)
]
# Finally rotate and move the polygon
var final: PackedVector2Array = []
for v: Vector2 in p:
var vert := v.rotated(t.get_rotation()) + t.get_origin()
final.append(vert)
return final
func populate_reference_menu(items: Array[ReferenceImage], default := false) -> void:
reference_menu.clear()
# Default / Reset
if default:
reference_menu.add_item("None", 0)
reference_menu.add_separator()
for ri: ReferenceImage in items:
# NOTE: using image_path.get_file() instead of full image_path because usually paths are
# long and if we are limiting to 22 characters as well, then every entry will end up
# looking the same
var idx: int = ri.get_index() + 1
var label: String = "(%o) %s" % [idx, ri.image_path.get_file()]
# We trim the length of the title
label = label.left(22) + "..."
reference_menu.add_item(label, idx)
# When a id is pressed in the reference menu
func _reference_menu_id_pressed(id: int) -> void:
Global.can_draw = true
Global.current_project.set_reference_image_index(id - 1)
reference_menu.hide()
func remove_reference_image(idx: int) -> void:
var ri: ReferenceImage = Global.current_project.get_reference_image(idx)
Global.current_project.reference_images.remove_at(idx)
ri.queue_free()
Global.current_project.set_reference_image_index(-1)
Global.current_project.change_project()
func _update_on_zoom() -> void:
queue_redraw()
func get_undo_data() -> Dictionary:
var ri: ReferenceImage = Global.current_project.get_current_reference_image()
if !ri:
return {}
var data := {}
data["position"] = ri.position
data["scale"] = ri.scale
data["rotation"] = ri.rotation
data["overlay_color"] = ri.overlay_color
data["filter"] = ri.filter
data["monochrome"] = ri.monochrome
data["color_clamping"] = ri.color_clamping
return data
func commit_undo(action: String, undo_data_tmp: Dictionary) -> void:
if !undo_data_tmp:
print("No undo data found for ReferenceImages.gd!")
return
var ri: ReferenceImage = Global.current_project.get_current_reference_image()
if !ri:
print("No Reference Image ReferenceImages.gd!")
return
var redo_data: Dictionary = get_undo_data()
var project := Global.current_project
project.undos += 1
project.undo_redo.create_action(action)
for key in undo_data_tmp.keys():
if redo_data.has(key):
project.undo_redo.add_do_property(ri, key, redo_data.get(key))
project.undo_redo.add_undo_property(ri, key, undo_data_tmp.get(key))
project.undo_redo.add_do_method(Global.general_redo.bind(project))
project.undo_redo.add_do_method(ri.change_properties)
project.undo_redo.add_undo_method(Global.general_undo.bind(project))
project.undo_redo.add_undo_method(ri.change_properties)
project.undo_redo.commit_action()
undo_data.clear()
func _draw() -> void:
if index < 0:
return
var line_width := 2.0 / Global.camera.zoom.x
# If we are dragging show where the Reference was coming from
if dragging:
var i: ReferenceImage = Global.current_project.get_current_reference_image()
var prev_transform := Transform2D(og_rotation, og_scale, 0.0, og_pos)
var prev_poly := get_transformed_rect_polygon(i.get_rect(), prev_transform)
prev_poly.append(prev_poly[0])
draw_polyline(prev_poly, Color(1, 0.29, 0.29), line_width)
# First we highlight the Reference Images under the mouse with yellow
for ri: ReferenceImage in Global.current_project.reference_images:
var p := get_transformed_rect_polygon(ri.get_rect(), ri.transform)
p.append(p[0])
if ri.get_index() == index:
draw_polyline(p, Color(0.50, 0.99, 0.29), line_width)
elif Geometry2D.is_point_in_polygon(get_local_mouse_position(), p) and !dragging:
draw_polyline(p, Color(0.98, 0.80, 0.29), line_width)