mirror of
https://github.com/Orama-Interactive/Pixelorama.git
synced 2025-02-13 09:13:07 +00:00
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.
405 lines
14 KiB
GDScript
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
|