1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-02-01 16:09:49 +00:00
Pixelorama/src/UI/Canvas/Selection.gd
Manolis Papadeas 1d5b44fad2 Fix Copy & Delete to work on the currently selected parts of the image
This also fixed a crash when deleting while content is being moved/resized
2021-04-19 19:05:05 +03:00

517 lines
19 KiB
GDScript

extends Node2D
class Clipboard:
var image := Image.new()
var selection_bitmap := BitMap.new()
var big_bounding_rectangle := Rect2()
var selection_offset := Vector2.ZERO
class Gizmo:
enum Type {SCALE, ROTATE}
var rect : Rect2
var direction := Vector2.ZERO
var type : int
func _init(_type : int = Type.SCALE, _direction := Vector2.ZERO) -> void:
type = _type
direction = _direction
func get_cursor() -> int:
var cursor := Input.CURSOR_MOVE
if direction == Vector2.ZERO:
return Input.CURSOR_POINTING_HAND
elif direction == Vector2(-1, -1) or direction == Vector2(1, 1): # Top left or bottom right
cursor = Input.CURSOR_FDIAGSIZE
elif direction == Vector2(1, -1) or direction == Vector2(-1, 1): # Top right or bottom left
cursor = Input.CURSOR_BDIAGSIZE
elif direction == Vector2(0, -1) or direction == Vector2(0, 1): # Center top or center bottom
cursor = Input.CURSOR_VSIZE
elif direction == Vector2(-1, 0) or direction == Vector2(1, 0): # Center left or center right
cursor = Input.CURSOR_HSIZE
return cursor
var clipboard := Clipboard.new()
var is_moving_content := false
var is_pasting := false
var big_bounding_rectangle := Rect2() setget _big_bounding_rectangle_changed
var temp_rect := Rect2()
var original_big_bounding_rectangle := Rect2()
var original_preview_image := Image.new()
var original_bitmap := BitMap.new()
var original_offset := Vector2.ZERO
var preview_image := Image.new()
var preview_image_texture := ImageTexture.new()
var undo_data : Dictionary
var drawn_rect := Rect2(0, 0, 0, 0)
var gizmos := [] # Array of Gizmos
var dragged_gizmo : Gizmo = null
var prev_angle := 0
var mouse_pos_on_gizmo_drag := Vector2.ZERO
onready var marching_ants_outline : Sprite = $MarchingAntsOutline
func _ready() -> void:
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2(-1, -1))) # Top left
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2(0, -1))) # Center top
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2(1, -1))) # Top right
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2(1, 0))) # Center right
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2(1, 1))) # Bottom right
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2(0, 1))) # Center bottom
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2(-1, 1))) # Bottom left
gizmos.append(Gizmo.new(Gizmo.Type.SCALE, Vector2(-1, 0))) # Center left
# gizmos.append(Gizmo.new(Gizmo.Type.ROTATE)) # Rotation gizmo (temp)
func _input(event : InputEvent) -> void:
if event is InputEventKey:
if is_moving_content: # Temporary code
if event.scancode == 16777221: # Enter
move_content_confirm()
elif event.scancode == 16777217: # Escape
move_content_cancel()
elif event is InputEventMouse:
var gizmo : Gizmo
for g in gizmos:
if g.rect.has_point(Global.canvas.current_pixel):
gizmo = g
break
if gizmo:
Global.main_viewport.mouse_default_cursor_shape = gizmo.get_cursor()
elif !dragged_gizmo:
Global.main_viewport.mouse_default_cursor_shape = Input.CURSOR_CROSS
if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
if event.pressed:
if gizmo:
Global.has_focus = false
mouse_pos_on_gizmo_drag = Global.canvas.current_pixel
dragged_gizmo = gizmo
temp_rect = big_bounding_rectangle
move_content_start()
Global.current_project.selection_offset = Vector2.ZERO
if gizmo.type == Gizmo.Type.ROTATE:
var img_size := max(original_preview_image.get_width(), original_preview_image.get_height())
original_preview_image.crop(img_size, img_size)
elif dragged_gizmo:
Global.has_focus = true
dragged_gizmo = null
if dragged_gizmo:
if dragged_gizmo.type == Gizmo.Type.SCALE:
gizmo_resize()
else:
gizmo_rotate()
func _draw() -> void:
var _position := position
var _scale := scale
if Global.mirror_view:
_position.x = _position.x + Global.current_project.size.x
_scale.x = -1
draw_set_transform(_position, rotation, _scale)
draw_rect(drawn_rect, Color.black, false)
if big_bounding_rectangle.size != Vector2.ZERO:
for gizmo in gizmos: # Draw gizmos
draw_rect(gizmo.rect, Color.black)
var filled_rect : Rect2 = gizmo.rect
var filled_size : Vector2 = gizmo.rect.size * Vector2(0.2, 0.2)
filled_rect.position += filled_size
filled_rect.size -= filled_size * 2
draw_rect(filled_rect, Color.white) # Filled white square
if is_moving_content and !preview_image.is_empty():
draw_texture(preview_image_texture, big_bounding_rectangle.position, Color(1, 1, 1, 0.5))
draw_set_transform(position, rotation, scale)
func _big_bounding_rectangle_changed(value : Rect2) -> void:
big_bounding_rectangle = value
update_gizmos()
func update_gizmos() -> void:
var rect_pos : Vector2 = big_bounding_rectangle.position
var rect_end : Vector2 = big_bounding_rectangle.end
var size := Vector2.ONE * Global.camera.zoom * 10
# Clockwise, starting from top-left corner
gizmos[0].rect = Rect2(rect_pos - size, size)
gizmos[1].rect = Rect2(Vector2((rect_end.x + rect_pos.x - size.x) / 2, rect_pos.y - size.y), size)
gizmos[2].rect = Rect2(Vector2(rect_end.x, rect_pos.y - size.y), size)
gizmos[3].rect = Rect2(Vector2(rect_end.x, (rect_end.y + rect_pos.y - size.y) / 2), size)
gizmos[4].rect = Rect2(rect_end, size)
gizmos[5].rect = Rect2(Vector2((rect_end.x + rect_pos.x - size.x) / 2, rect_end.y), size)
gizmos[6].rect = Rect2(Vector2(rect_pos.x - size.x, rect_end.y), size)
gizmos[7].rect = Rect2(Vector2(rect_pos.x - size.x, (rect_end.y + rect_pos.y - size.y) / 2), size)
# 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)
update()
func update_on_zoom(zoom : float) -> void:
var size := max(Global.current_project.selection_bitmap.get_size().x, Global.current_project.selection_bitmap.get_size().y)
marching_ants_outline.material.set_shader_param("width", zoom)
marching_ants_outline.material.set_shader_param("frequency", (1.0 / zoom) * 10 * size / 64)
for gizmo in gizmos:
if gizmo.rect.size == Vector2.ZERO:
return
update_gizmos()
func gizmo_resize() -> void:
var diff : Vector2 = (Global.canvas.current_pixel - mouse_pos_on_gizmo_drag) * dragged_gizmo.direction
var dir := dragged_gizmo.direction
if diff != Vector2.ZERO:
mouse_pos_on_gizmo_drag = Global.canvas.current_pixel
var left := 0.0 if dir.x >= 0 else diff.x
var top := 0.0 if dir.y >= 0 else diff.y
var right := diff.x if dir.x >= 0 else 0.0
var bottom := diff.y if dir.y >= 0 else 0.0
temp_rect = temp_rect.grow_individual(left, top, right, bottom)
big_bounding_rectangle = temp_rect.abs()
big_bounding_rectangle.position = big_bounding_rectangle.position.ceil()
self.big_bounding_rectangle.size = big_bounding_rectangle.size.ceil()
var size = big_bounding_rectangle.size.abs()
preview_image.copy_from(original_preview_image)
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:
preview_image.flip_y()
preview_image_texture.create_from_image(preview_image, 0)
Global.current_project.selection_bitmap = Global.current_project.resize_bitmap_values(original_bitmap, size, temp_rect.size.x < 0, temp_rect.size.y < 0)
Global.current_project.selection_bitmap_changed()
update()
func gizmo_rotate() -> void: # Does not work properly yet
var angle := Global.canvas.current_pixel.angle_to_point(mouse_pos_on_gizmo_drag)
angle = deg2rad(floor(rad2deg(angle)))
if angle == prev_angle:
return
prev_angle = angle
# var img_size := max(original_preview_image.get_width(), original_preview_image.get_height())
# warning-ignore:integer_division
# warning-ignore:integer_division
# var pivot = Vector2(original_preview_image.get_width() / 2, original_preview_image.get_height() / 2)
var pivot = Vector2(big_bounding_rectangle.size.x / 2, big_bounding_rectangle.size.y / 2)
preview_image.copy_from(original_preview_image)
if original_big_bounding_rectangle.position != big_bounding_rectangle.position:
preview_image.fill(Color(0, 0, 0, 0))
var pos_diff := (original_big_bounding_rectangle.position - big_bounding_rectangle.position).abs()
# pos_diff.y = 0
preview_image.blit_rect(original_preview_image, Rect2(Vector2.ZERO, preview_image.get_size()), pos_diff)
DrawingAlgos.nn_rotate(preview_image, angle, pivot)
preview_image_texture.create_from_image(preview_image, 0)
var bitmap_image = Global.current_project.bitmap_to_image(original_bitmap)
var bitmap_pivot = original_big_bounding_rectangle.position + ((original_big_bounding_rectangle.end - original_big_bounding_rectangle.position) / 2)
DrawingAlgos.nn_rotate(bitmap_image, angle, bitmap_pivot)
Global.current_project.selection_bitmap.create_from_image_alpha(bitmap_image)
Global.current_project.selection_bitmap_changed()
self.big_bounding_rectangle = bitmap_image.get_used_rect()
update()
func select_rect(rect : Rect2, select := true) -> void:
var project : Project = Global.current_project
var selection_bitmap_copy : BitMap = project.selection_bitmap.duplicate()
var offset_position := Vector2.ZERO # Used only if the selection is outside of the canvas boundaries, on the left and/or above (negative coords)
if big_bounding_rectangle.position.x < 0:
rect.position.x -= big_bounding_rectangle.position.x
offset_position.x = big_bounding_rectangle.position.x
if big_bounding_rectangle.position.y < 0:
rect.position.y -= big_bounding_rectangle.position.y
offset_position.y = big_bounding_rectangle.position.y
if offset_position != Vector2.ZERO:
big_bounding_rectangle.position -= offset_position
project.move_bitmap_values(selection_bitmap_copy)
selection_bitmap_copy.set_bit_rect(rect, select)
big_bounding_rectangle = project.get_selection_rectangle(selection_bitmap_copy)
if offset_position != Vector2.ZERO:
big_bounding_rectangle.position += offset_position
project.move_bitmap_values(selection_bitmap_copy)
project.selection_bitmap = selection_bitmap_copy
self.big_bounding_rectangle = big_bounding_rectangle # call getter method
func move_borders_start() -> void:
undo_data = _get_undo_data(false)
func move_borders(move : Vector2) -> void:
marching_ants_outline.offset += move
self.big_bounding_rectangle.position += move
update()
func move_borders_end() -> void:
var selected_bitmap_copy = Global.current_project.selection_bitmap.duplicate()
Global.current_project.move_bitmap_values(selected_bitmap_copy)
Global.current_project.selection_bitmap = selected_bitmap_copy
commit_undo("Rectangle Select", undo_data)
update()
func move_content_start() -> void:
if !is_moving_content:
is_moving_content = true
undo_data = _get_undo_data(true)
get_preview_image()
original_bitmap = Global.current_project.selection_bitmap.duplicate()
original_big_bounding_rectangle = big_bounding_rectangle
original_offset = Global.current_project.selection_offset
update()
func move_content(move : Vector2) -> void:
move_borders(move)
func move_content_confirm() -> void:
if !is_moving_content:
return
var project : Project = Global.current_project
var cel_image : Image = project.frames[project.current_frame].cels[project.current_layer].image
cel_image.blit_rect_mask(preview_image, preview_image, Rect2(Vector2.ZERO, Global.current_project.selection_bitmap.get_size()), big_bounding_rectangle.position)
var selected_bitmap_copy = Global.current_project.selection_bitmap.duplicate()
Global.current_project.move_bitmap_values(selected_bitmap_copy)
Global.current_project.selection_bitmap = selected_bitmap_copy
original_preview_image = Image.new()
preview_image = Image.new()
original_bitmap = BitMap.new()
is_moving_content = false
is_pasting = false
commit_undo("Move Selection", undo_data)
update()
func move_content_cancel() -> void:
if preview_image.is_empty():
return
var project : Project = Global.current_project
project.selection_offset = original_offset
is_moving_content = false
self.big_bounding_rectangle = original_big_bounding_rectangle
project.selection_bitmap = original_bitmap
project.selection_bitmap_changed()
preview_image = original_preview_image
if !is_pasting:
var cel_image : Image = project.frames[project.current_frame].cels[project.current_layer].image
cel_image.blit_rect_mask(preview_image, preview_image, Rect2(Vector2.ZERO, Global.current_project.selection_bitmap.get_size()), big_bounding_rectangle.position)
Global.canvas.update_texture(project.current_layer)
original_preview_image = Image.new()
preview_image = Image.new()
original_bitmap = BitMap.new()
is_pasting = false
update()
func commit_undo(action : String, _undo_data : Dictionary) -> void:
var redo_data = _get_undo_data("image_data" in _undo_data)
var project := Global.current_project
project.undos += 1
project.undo_redo.create_action(action)
project.undo_redo.add_do_property(project, "selection_bitmap", redo_data["selection_bitmap"])
project.undo_redo.add_do_property(self, "big_bounding_rectangle", redo_data["big_bounding_rectangle"])
project.undo_redo.add_do_property(project, "selection_offset", redo_data["outline_offset"])
project.undo_redo.add_undo_property(project, "selection_bitmap", _undo_data["selection_bitmap"])
project.undo_redo.add_undo_property(self, "big_bounding_rectangle", _undo_data["big_bounding_rectangle"])
project.undo_redo.add_undo_property(project, "selection_offset", _undo_data["outline_offset"])
if "image_data" in _undo_data:
var image : Image = project.frames[project.current_frame].cels[project.current_layer].image
project.undo_redo.add_do_property(image, "data", redo_data["image_data"])
project.undo_redo.add_undo_property(image, "data", _undo_data["image_data"])
project.undo_redo.add_do_method(Global, "redo", project.current_frame, project.current_layer)
project.undo_redo.add_do_method(project, "selection_bitmap_changed")
project.undo_redo.add_undo_method(Global, "undo", project.current_frame, project.current_layer)
project.undo_redo.add_undo_method(project, "selection_bitmap_changed")
project.undo_redo.commit_action()
undo_data.clear()
func _get_undo_data(undo_image : bool) -> Dictionary:
var data := {}
var project := Global.current_project
data["selection_bitmap"] = project.selection_bitmap
data["big_bounding_rectangle"] = big_bounding_rectangle
data["outline_offset"] = Global.current_project.selection_offset
if undo_image:
var image : Image = project.frames[project.current_frame].cels[project.current_layer].image
image.unlock()
data["image_data"] = image.data
image.lock()
return data
func cut() -> void:
copy()
delete()
func copy() -> void:
var project := Global.current_project
if !project.has_selection:
return
var image : Image = project.frames[project.current_frame].cels[project.current_layer].image
var to_copy := Image.new()
if is_moving_content:
to_copy.copy_from(preview_image)
else:
to_copy = image.get_rect(big_bounding_rectangle)
to_copy.lock()
# Remove unincluded pixels if the selection is not a single rectangle
for x in to_copy.get_size().x:
for y in to_copy.get_size().y:
var pos := Vector2(x, y)
var offset_pos = big_bounding_rectangle.position
if offset_pos.x < 0:
offset_pos.x = 0
if offset_pos.y < 0:
offset_pos.y = 0
if not project.selection_bitmap.get_bit(pos + offset_pos):
to_copy.set_pixelv(pos, Color(0))
to_copy.unlock()
clipboard.image = to_copy
clipboard.selection_bitmap = project.selection_bitmap.duplicate()
clipboard.big_bounding_rectangle = big_bounding_rectangle
clipboard.selection_offset = project.selection_offset
func paste() -> void:
if !clipboard.image:
return
undo_data = _get_undo_data(true)
var project := Global.current_project
original_bitmap = Global.current_project.selection_bitmap.duplicate()
original_big_bounding_rectangle = big_bounding_rectangle
original_offset = Global.current_project.selection_offset
clear_selection()
project.selection_bitmap = clipboard.selection_bitmap.duplicate()
self.big_bounding_rectangle = clipboard.big_bounding_rectangle
project.selection_offset = clipboard.selection_offset
is_moving_content = true
is_pasting = true
original_preview_image = clipboard.image
preview_image.copy_from(original_preview_image)
preview_image_texture.create_from_image(preview_image, 0)
project.selection_bitmap_changed()
func delete() -> void:
var project := Global.current_project
if !project.has_selection:
return
if is_moving_content:
is_moving_content = false
original_preview_image = Image.new()
preview_image = Image.new()
original_bitmap = BitMap.new()
is_pasting = false
update()
commit_undo("Draw", undo_data)
return
var _undo_data = _get_undo_data(true)
var image : Image = project.frames[project.current_frame].cels[project.current_layer].image
for x in big_bounding_rectangle.size.x:
for y in big_bounding_rectangle.size.y:
var pos := Vector2(x, y) + big_bounding_rectangle.position
if project.can_pixel_get_drawn(pos):
image.set_pixelv(pos, Color(0))
commit_undo("Draw", _undo_data)
func select_all() -> void:
var project := Global.current_project
var _undo_data = _get_undo_data(false)
clear_selection()
var full_rect = Rect2(Vector2.ZERO, project.size)
select_rect(full_rect)
commit_undo("Rectangle Select", _undo_data)
func invert() -> void:
move_content_confirm()
var project := Global.current_project
var _undo_data = _get_undo_data(false)
var selection_bitmap_copy : BitMap = project.selection_bitmap.duplicate()
selection_bitmap_copy = project.resize_bitmap(selection_bitmap_copy, project.size)
project.invert_bitmap(selection_bitmap_copy)
project.selection_bitmap = selection_bitmap_copy
Global.current_project.selection_bitmap_changed()
self.big_bounding_rectangle = project.get_selection_rectangle(selection_bitmap_copy)
project.selection_offset = Vector2.ZERO
commit_undo("Rectangle Select", _undo_data)
func clear_selection(use_undo := false) -> void:
var project := Global.current_project
if !project.has_selection:
return
move_content_confirm()
var _undo_data = _get_undo_data(false)
var selection_bitmap_copy : BitMap = project.selection_bitmap.duplicate()
selection_bitmap_copy = project.resize_bitmap(selection_bitmap_copy, project.size)
var full_rect = Rect2(Vector2.ZERO, selection_bitmap_copy.get_size())
selection_bitmap_copy.set_bit_rect(full_rect, false)
project.selection_bitmap = selection_bitmap_copy
self.big_bounding_rectangle = Rect2()
project.selection_offset = Vector2.ZERO
update()
if use_undo:
commit_undo("Clear Selection", _undo_data)
func get_preview_image() -> void:
var project : Project = Global.current_project
var cel_image : Image = project.frames[project.current_frame].cels[project.current_layer].image
if original_preview_image.is_empty():
# original_preview_image.copy_from(cel_image)
original_preview_image = cel_image.get_rect(big_bounding_rectangle)
original_preview_image.lock()
# For non-rectangular selections
for x in range(0, big_bounding_rectangle.size.x):
for y in range(0, big_bounding_rectangle.size.y):
var pos := Vector2(x, y)
if !project.can_pixel_get_drawn(pos + big_bounding_rectangle.position):
original_preview_image.set_pixelv(pos, Color(0, 0, 0, 0))
original_preview_image.unlock()
preview_image.copy_from(original_preview_image)
preview_image_texture.create_from_image(preview_image, 0)
var clear_image := Image.new()
clear_image.create(original_preview_image.get_width(), original_preview_image.get_height(), false, Image.FORMAT_RGBA8)
cel_image.blit_rect_mask(clear_image, original_preview_image, Rect2(Vector2.ZERO, Global.current_project.selection_bitmap.get_size()), big_bounding_rectangle.position)
Global.canvas.update_texture(project.current_layer)