1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-02-13 09:13:07 +00:00
Pixelorama/src/Tools/BaseTool.gd
Emmanouil Papadeas 4dc55e538e Fix issues when picking color with the bucket tool
1) The bucket tool now picks colors from the top-most layer, like the rest of the drawing tools.
2) Using the tool while moving the cursor and also holding the color picker shortcut (Alt by default), now picks colors instead of using the bucket tool.
2024-10-17 02:34:27 +03:00

405 lines
14 KiB
GDScript

class_name BaseTool
extends VBoxContainer
var is_moving := false
var is_syncing := false
var kname: String
var tool_slot: Tools.Slot = null
var cursor_text := ""
var _cursor := Vector2i(Vector2.INF)
var _stabilizer_center := Vector2.ZERO
var _draw_cache: Array[Vector2i] = [] ## For storing already drawn pixels
@warning_ignore("unused_private_class_variable")
var _for_frame := 0 ## Cache for which frame
# Only use _spacing_mode and _spacing variables (the others are set automatically)
# The _spacing_mode and _spacing values are to be CHANGED only in the tool scripts (e.g Pencil.gd)
var _spacing_mode := false ## Enables spacing (continuous gaps between two strokes)
var _spacing := Vector2i.ZERO ## Spacing between two strokes
var _stroke_dimensions := Vector2i.ONE ## 2D vector containing _brush_size from Draw.gd
var _spacing_offset := Vector2i.ZERO ## The initial error between position and position.snapped()
@onready var color_rect := $ColorRect as ColorRect
func _ready() -> void:
kname = name.replace(" ", "_").to_lower()
if tool_slot.name == "Left tool":
color_rect.color = Global.left_tool_color
else:
color_rect.color = Global.right_tool_color
$Label.text = Tools.tools[name].display_name
load_config()
func save_config() -> void:
var config := get_config()
Global.config_cache.set_value(tool_slot.kname, kname, config)
if not is_syncing: # If the tool isn't busy syncing with another tool.
Tools.config_changed.emit(tool_slot.button, config)
func load_config() -> void:
var value = Global.config_cache.get_value(tool_slot.kname, kname, {})
set_config(value)
update_config()
func get_config() -> Dictionary:
return {}
func set_config(_config: Dictionary) -> void:
pass
func update_config() -> void:
pass
func draw_start(pos: Vector2i) -> void:
_stabilizer_center = pos
_draw_cache = []
is_moving = true
Global.current_project.can_undo = false
_spacing_offset = _get_spacing_offset(pos)
func draw_move(pos: Vector2i) -> void:
# This can happen if the user switches between tools with a shortcut
# while using another tool
if !is_moving:
draw_start(pos)
func draw_end(_pos: Vector2i) -> void:
is_moving = false
_draw_cache = []
Global.current_project.can_undo = true
func cursor_move(pos: Vector2i) -> void:
_cursor = pos
if _spacing_mode and is_moving:
_cursor = get_spacing_position(pos)
func get_spacing_position(pos: Vector2i) -> Vector2i:
# spacing_factor is the distance the mouse needs to get snapped by in order
# to keep a space "_spacing" between two strokes of dimensions "_stroke_dimensions"
var spacing_factor := _stroke_dimensions + _spacing
var snap_pos := Vector2(pos.snapped(spacing_factor) + _spacing_offset)
# keeping snap_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
var t_l := snap_pos + Vector2(-spacing_factor.x, -spacing_factor.y)
var t_c := snap_pos + Vector2(0, -spacing_factor.y) # t_c is for "top centre" and so on...
var t_r := snap_pos + Vector2(spacing_factor.x, -spacing_factor.y)
var m_l := snap_pos + Vector2(-spacing_factor.x, 0)
var m_c := snap_pos
var m_r := snap_pos + Vector2(spacing_factor.x, 0)
var b_l := snap_pos + Vector2(-spacing_factor.x, spacing_factor.y)
var b_c := snap_pos + Vector2(0, spacing_factor.y)
var b_r := snap_pos + Vector2(spacing_factor.x, spacing_factor.y)
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) < snap_pos.distance_to(pos):
snap_pos = vec
return Vector2i(snap_pos)
func _get_spacing_offset(pos: Vector2i) -> Vector2i:
var spacing_factor := _stroke_dimensions + _spacing # spacing_factor is explained above
# since we just started drawing, the "position" is our intended location so the error
# (_spacing_offset) is measured by subtracting both quantities
return pos - pos.snapped(spacing_factor)
func draw_indicator(left: bool) -> void:
var rect := Rect2(_cursor, Vector2.ONE)
var color := Global.left_tool_color if left else Global.right_tool_color
Global.canvas.indicators.draw_rect(rect, color, false)
func draw_preview() -> void:
pass
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.grid_size)
grid_pos += Vector2(Global.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
var t_l := grid_pos + Vector2(-Global.grid_size.x, -Global.grid_size.y)
var t_c := grid_pos + Vector2(0, -Global.grid_size.y) # t_c is for "top centre" and so on
var t_r := grid_pos + Vector2(Global.grid_size.x, -Global.grid_size.y)
var m_l := grid_pos + Vector2(-Global.grid_size.x, 0)
var m_c := grid_pos
var m_r := grid_pos + Vector2(Global.grid_size.x, 0)
var b_l := grid_pos + Vector2(-Global.grid_size.x, Global.grid_size.y)
var b_c := grid_pos + Vector2(0, Global.grid_size.y)
var b_r := grid_pos + Vector2(Global.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()
if Global.snap_to_rectangular_grid_center:
var grid_center := pos.snapped(Global.grid_size) + Vector2(Global.grid_size / 2)
grid_center += Vector2(Global.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
var t_l := grid_center + Vector2(-Global.grid_size.x, -Global.grid_size.y)
var t_c := grid_center + Vector2(0, -Global.grid_size.y) # t_c is for "top centre" and so on
var t_r := grid_center + Vector2(Global.grid_size.x, -Global.grid_size.y)
var m_l := grid_center + Vector2(-Global.grid_size.x, 0)
var m_c := grid_center
var m_r := grid_center + Vector2(Global.grid_size.x, 0)
var b_l := grid_center + Vector2(-Global.grid_size.x, Global.grid_size.y)
var b_c := grid_center + Vector2(0, Global.grid_size.y)
var b_r := grid_center + Vector2(Global.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()
var snap_to := Vector2.INF
if Global.snap_to_guides:
for guide in Global.current_project.guides:
if guide is SymmetryGuide:
continue
var s1: Vector2 = guide.points[0]
var s2: Vector2 = guide.points[1]
var snap := _snap_to_guide(snap_to, pos, snapping_distance, s1, s2)
if snap == Vector2.INF:
continue
snap_to = snap
if Global.snap_to_perspective_guides:
for point in Global.current_project.vanishing_points:
if not (point.has("pos_x") and point.has("pos_y")): # Sanity check
continue
for i in point.lines.size():
if point.lines[i].has("angle") and point.lines[i].has("length"): # Sanity check
var angle := deg_to_rad(point.lines[i].angle)
var length: float = point.lines[i].length
var start := Vector2(point.pos_x, point.pos_y)
var s1 := start
var s2 := s1 + Vector2(length * cos(angle), length * sin(angle))
var snap := _snap_to_guide(snap_to, pos, snapping_distance, s1, s2)
if snap == Vector2.INF:
continue
snap_to = snap
if snap_to != Vector2.INF:
pos = snap_to.floor()
return pos
func mirror_array(array: Array[Vector2i], h: bool, v: bool) -> Array[Vector2i]:
var new_array: Array[Vector2i] = []
var project := Global.current_project
for point in array:
if h and v:
new_array.append(
Vector2i(project.x_symmetry_point - point.x, project.y_symmetry_point - point.y)
)
elif h:
new_array.append(Vector2i(project.x_symmetry_point - point.x, point.y))
elif v:
new_array.append(Vector2i(point.x, project.y_symmetry_point - point.y))
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_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)
if closest_point == Vector2.INF: # Is not close to a guide
return Vector2.INF
# Snap to the closest guide
if snap_to == Vector2.INF or (snap_to - pos).length() > (closest_point - pos).length():
snap_to = closest_point
return snap_to
func _get_stabilized_position(normal_pos: Vector2) -> Vector2:
if not Tools.stabilizer_enabled:
return normal_pos
var difference := normal_pos - _stabilizer_center
var distance := difference.length() / Tools.stabilizer_value
var angle := difference.angle()
var pos := _stabilizer_center + Vector2(distance, distance) * Vector2.from_angle(angle)
_stabilizer_center = pos
return pos
func _get_draw_rect() -> Rect2i:
if Global.current_project.has_selection:
return Global.current_project.selection_map.get_used_rect()
else:
return Rect2i(Vector2i.ZERO, Global.current_project.size)
func _get_draw_image() -> Image:
return Global.current_project.get_current_cel().get_image()
func _get_selected_draw_images() -> Array[Image]:
var images: Array[Image] = []
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
if project.layers[cel_index[1]].can_layer_get_drawn():
images.append(cel.get_image())
return images
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
var image := Image.new()
image.copy_from(_get_draw_image())
if pos.x > image.get_width() - 1 or pos.y > image.get_height() - 1:
return
var color := Color(0, 0, 0, 0)
var curr_frame: Frame = project.frames[project.current_frame]
for layer in project.layers.size():
var idx := (project.layers.size() - 1) - layer
if project.layers[idx].is_visible_in_hierarchy():
image = curr_frame.cels[idx].get_image()
color = image.get_pixelv(pos)
if not is_zero_approx(color.a):
break
Tools.assign_color(color, tool_slot.button, false)
func _flip_rect(rect: Rect2, rect_size: Vector2, horiz: bool, vert: bool) -> Rect2:
var result := rect
if horiz:
result.position.x = rect_size.x - rect.end.x
result.end.x = rect_size.x - rect.position.x
if vert:
result.position.y = rect_size.y - rect.end.y
result.end.y = rect_size.y - rect.position.y
return result.abs()
func _create_polylines(bitmap: BitMap) -> Array:
var lines := []
var bitmap_size := bitmap.get_size()
for y in bitmap_size.y:
for x in bitmap_size.x:
var p := Vector2i(x, y)
if not bitmap.get_bitv(p):
continue
if x <= 0 or not bitmap.get_bitv(p - Vector2i(1, 0)):
_add_polylines_segment(lines, p, p + Vector2i(0, 1))
if y <= 0 or not bitmap.get_bitv(p - Vector2i(0, 1)):
_add_polylines_segment(lines, p, p + Vector2i(1, 0))
if x + 1 >= bitmap_size.x or not bitmap.get_bitv(p + Vector2i(1, 0)):
_add_polylines_segment(lines, p + Vector2i(1, 0), p + Vector2i(1, 1))
if y + 1 >= bitmap_size.y or not bitmap.get_bitv(p + Vector2i(0, 1)):
_add_polylines_segment(lines, p + Vector2i(0, 1), p + Vector2i(1, 1))
return lines
func _fill_bitmap_with_points(points: Array[Vector2i], bitmap_size: Vector2i) -> BitMap:
var bitmap := BitMap.new()
bitmap.create(bitmap_size)
for point in points:
if point.x < 0 or point.y < 0 or point.x >= bitmap_size.x or point.y >= bitmap_size.y:
continue
bitmap.set_bitv(point, 1)
return bitmap
func _add_polylines_segment(lines: Array, start: Vector2i, end: Vector2i) -> void:
for line in lines:
if line[0] == start:
line.insert(0, end)
return
if line[0] == end:
line.insert(0, start)
return
if line[line.size() - 1] == start:
line.append(end)
return
if line[line.size() - 1] == end:
line.append(start)
return
lines.append([start, end])
func _exit_tree() -> void:
if is_moving:
draw_end(Global.canvas.current_pixel.floor())
Global.canvas.previews_sprite.texture = null