mirror of
https://github.com/Orama-Interactive/Pixelorama.git
synced 2025-02-22 05:23:14 +00:00
305 lines
9.9 KiB
GDScript
305 lines
9.9 KiB
GDScript
extends "res://src/Tools/BaseDraw.gd"
|
|
|
|
var _curve := Curve2D.new() ## The [Curve2D] responsible for the shape of the curve being drawn.
|
|
var _drawing := false ## Set to true when a curve is being drawn.
|
|
var _fill := false ## When true, the inside area of the curve gets filled.
|
|
var _fill_inside_rect := Rect2i() ## The bounding box that surrounds the area that gets filled.
|
|
var _editing_bezier := false ## Needed to determine when to show the control points preview line.
|
|
var _editing_out_control_point := false ## True when controlling the out control point only.
|
|
var _thickness := 1 ## The thickness of the curve.
|
|
var _last_mouse_position := Vector2.INF ## The last position of the mouse
|
|
|
|
|
|
func _init() -> void:
|
|
# To prevent tool from remaining active when switching projects
|
|
Global.project_about_to_switch.connect(_clear)
|
|
_drawer.color_op = Drawer.ColorOp.new()
|
|
update_indicator()
|
|
|
|
|
|
func update_brush() -> void:
|
|
pass
|
|
|
|
|
|
func _on_thickness_value_changed(value: int) -> void:
|
|
_thickness = value
|
|
update_indicator()
|
|
update_config()
|
|
save_config()
|
|
|
|
|
|
func _on_fill_checkbox_toggled(toggled_on: bool) -> void:
|
|
_fill = toggled_on
|
|
update_config()
|
|
save_config()
|
|
|
|
|
|
func update_indicator() -> void:
|
|
var bitmap := BitMap.new()
|
|
bitmap.create(Vector2i.ONE * _thickness)
|
|
bitmap.set_bit_rect(Rect2i(Vector2i.ZERO, Vector2i.ONE * _thickness), true)
|
|
_indicator = bitmap
|
|
_polylines = _create_polylines(_indicator)
|
|
|
|
|
|
func get_config() -> Dictionary:
|
|
var config := super.get_config()
|
|
config["thickness"] = _thickness
|
|
return config
|
|
|
|
|
|
func set_config(config: Dictionary) -> void:
|
|
super.set_config(config)
|
|
_thickness = config.get("thickness", _thickness)
|
|
|
|
|
|
func update_config() -> void:
|
|
super.update_config()
|
|
$ThicknessSlider.value = _thickness
|
|
|
|
|
|
## This tool has no brush, so just return the indicator as it is.
|
|
func _create_brush_indicator() -> BitMap:
|
|
return _indicator
|
|
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
if _drawing:
|
|
if event is InputEventMouseMotion:
|
|
_last_mouse_position = Global.canvas.current_pixel.floor()
|
|
if Global.mirror_view:
|
|
_last_mouse_position.x = Global.current_project.size.x - 1 - _last_mouse_position.x
|
|
elif event is InputEventMouseButton:
|
|
if event.double_click and event.button_index == tool_slot.button:
|
|
$DoubleClickTimer.start()
|
|
_draw_shape()
|
|
else:
|
|
if event.is_action_pressed("shape_perfect"):
|
|
_editing_out_control_point = true
|
|
elif event.is_action_released("shape_perfect"):
|
|
_editing_out_control_point = false
|
|
if event.is_action_pressed("change_tool_mode"): # Control removes the last added point
|
|
if _curve.point_count > 1:
|
|
_curve.remove_point(_curve.point_count - 1)
|
|
|
|
|
|
func draw_start(pos: Vector2i) -> void:
|
|
if !$DoubleClickTimer.is_stopped():
|
|
return
|
|
pos = snap_position(pos)
|
|
super.draw_start(pos)
|
|
if Input.is_action_pressed("shape_displace"):
|
|
_picking_color = true
|
|
_pick_color(pos)
|
|
return
|
|
Global.canvas.selection.transform_content_confirm()
|
|
update_mask()
|
|
if !_drawing:
|
|
_drawing = true
|
|
_curve.add_point(pos)
|
|
_fill_inside_rect = Rect2i(pos, Vector2i.ZERO)
|
|
|
|
|
|
func draw_move(pos: Vector2i) -> void:
|
|
pos = snap_position(pos)
|
|
super.draw_move(pos)
|
|
if _picking_color: # Still return even if we released Alt
|
|
if Input.is_action_pressed("shape_displace"):
|
|
_pick_color(pos)
|
|
return
|
|
if _drawing:
|
|
_editing_bezier = true
|
|
var current_position := _curve.get_point_position(_curve.point_count - 1) - Vector2(pos)
|
|
if not _editing_out_control_point:
|
|
_curve.set_point_in(_curve.point_count - 1, current_position)
|
|
_curve.set_point_out(_curve.point_count - 1, -current_position)
|
|
|
|
|
|
func draw_end(pos: Vector2i) -> void:
|
|
_editing_bezier = false
|
|
if _is_hovering_first_position(pos) and _curve.point_count > 1:
|
|
_draw_shape()
|
|
super.draw_end(pos)
|
|
|
|
|
|
func draw_preview() -> void:
|
|
var previews := Global.canvas.previews_sprite
|
|
if not _drawing:
|
|
previews.texture = null
|
|
return
|
|
var points := _bezier()
|
|
var image := Image.create(
|
|
Global.current_project.size.x, Global.current_project.size.y, false, Image.FORMAT_LA8
|
|
)
|
|
for i in points.size():
|
|
if Global.mirror_view: # This fixes previewing in mirror mode
|
|
points[i].x = image.get_width() - points[i].x - 1
|
|
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(points[i]):
|
|
image.set_pixelv(points[i], Color.WHITE)
|
|
|
|
# Handle mirroring
|
|
if Tools.horizontal_mirror:
|
|
for point in mirror_array(points, true, false):
|
|
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
|
image.set_pixelv(point, Color.WHITE)
|
|
if Tools.vertical_mirror:
|
|
for point in mirror_array(points, true, true):
|
|
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
|
image.set_pixelv(point, Color.WHITE)
|
|
if Tools.vertical_mirror:
|
|
for point in mirror_array(points, false, true):
|
|
if Rect2i(Vector2i.ZERO, image.get_size()).has_point(point):
|
|
image.set_pixelv(point, Color.WHITE)
|
|
var texture := ImageTexture.create_from_image(image)
|
|
previews.texture = texture
|
|
|
|
var canvas := Global.canvas.previews
|
|
var circle_radius := Vector2.ONE * (5.0 / Global.camera.zoom.x)
|
|
if _is_hovering_first_position(_last_mouse_position) and _curve.point_count > 1:
|
|
var circle_center := _curve.get_point_position(0)
|
|
if Global.mirror_view: # This fixes previewing in mirror mode
|
|
circle_center.x = Global.current_project.size.x - circle_center.x - 1
|
|
circle_center += Vector2.ONE * 0.5
|
|
draw_empty_circle(canvas, circle_center, circle_radius * 2.0, Color.BLACK)
|
|
if _editing_bezier:
|
|
var current_position := _curve.get_point_position(_curve.point_count - 1)
|
|
var start := current_position
|
|
if _curve.point_count > 1:
|
|
start = current_position + _curve.get_point_in(_curve.point_count - 1)
|
|
var end := current_position + _curve.get_point_out(_curve.point_count - 1)
|
|
if Global.mirror_view: # This fixes previewing in mirror mode
|
|
current_position.x = Global.current_project.size.x - current_position.x - 1
|
|
start.x = Global.current_project.size.x - start.x - 1
|
|
end.x = Global.current_project.size.x - end.x - 1
|
|
|
|
canvas.draw_line(start, current_position, Color.BLACK)
|
|
canvas.draw_line(current_position, end, Color.BLACK)
|
|
draw_empty_circle(canvas, start, circle_radius, Color.BLACK)
|
|
draw_empty_circle(canvas, end, circle_radius, Color.BLACK)
|
|
|
|
|
|
func _draw_shape() -> void:
|
|
var points := _bezier()
|
|
prepare_undo("Draw Shape")
|
|
var images := _get_selected_draw_images()
|
|
for point in points:
|
|
# Reset drawer every time because pixel perfect sometimes breaks the tool
|
|
_drawer.reset()
|
|
_fill_inside_rect = _fill_inside_rect.expand(point)
|
|
# Draw each point offsetted based on the shape's thickness
|
|
_draw_pixel(point, images)
|
|
if _fill:
|
|
var v := Vector2i()
|
|
for x in _fill_inside_rect.size.x:
|
|
v.x = x + _fill_inside_rect.position.x
|
|
for y in _fill_inside_rect.size.y:
|
|
v.y = y + _fill_inside_rect.position.y
|
|
if Geometry2D.is_point_in_polygon(v, points):
|
|
_draw_pixel(v, images)
|
|
_clear()
|
|
commit_undo()
|
|
|
|
|
|
func _draw_pixel(point: Vector2i, images: Array[Image]) -> void:
|
|
if Global.current_project.can_pixel_get_drawn(point):
|
|
for image in images:
|
|
_drawer.set_pixel(image, point, tool_slot.color)
|
|
|
|
|
|
func _clear() -> void:
|
|
_curve.clear_points()
|
|
_fill_inside_rect = Rect2i()
|
|
_drawing = false
|
|
_editing_out_control_point = false
|
|
Global.canvas.previews.queue_redraw()
|
|
|
|
|
|
## Get the [member _curve]'s baked points, and draw lines between them using [method _fill_gap].
|
|
func _bezier() -> Array[Vector2i]:
|
|
var last_pixel := Global.canvas.current_pixel
|
|
if Global.mirror_view:
|
|
# Mirror the last point of the curve
|
|
last_pixel.x = (Global.current_project.size.x - 1) - last_pixel.x
|
|
_curve.add_point(last_pixel)
|
|
var points := _curve.get_baked_points()
|
|
_curve.remove_point(_curve.point_count - 1)
|
|
var final_points: Array[Vector2i] = []
|
|
for i in points.size() - 1:
|
|
var point1 := points[i]
|
|
var point2 := points[i + 1]
|
|
final_points.append_array(_fill_gap(point1, point2))
|
|
return final_points
|
|
|
|
|
|
## Fills the gap between [param point_a] and [param point_b] using Bresenham's line algorithm.
|
|
## Takes the [member _thickness] into account.
|
|
func _fill_gap(point_a: Vector2i, point_b: Vector2i) -> Array[Vector2i]:
|
|
var array: Array[Vector2i] = []
|
|
var dx := absi(point_b.x - point_a.x)
|
|
var dy := -absi(point_b.y - point_a.y)
|
|
var err := dx + dy
|
|
var e2 := err << 1
|
|
var sx := 1 if point_a.x < point_b.x else -1
|
|
var sy := 1 if point_a.y < point_b.y else -1
|
|
var x := point_a.x
|
|
var y := point_a.y
|
|
|
|
var start := point_a - Vector2i.ONE * (_thickness >> 1)
|
|
var end := start + Vector2i.ONE * _thickness
|
|
for yy in range(start.y, end.y):
|
|
for xx in range(start.x, end.x):
|
|
array.append(Vector2i(xx, yy))
|
|
|
|
while !(x == point_b.x && y == point_b.y):
|
|
e2 = err << 1
|
|
if e2 >= dy:
|
|
err += dy
|
|
x += sx
|
|
if e2 <= dx:
|
|
err += dx
|
|
y += sy
|
|
|
|
var pos := Vector2i(x, y)
|
|
start = pos - Vector2i.ONE * (_thickness >> 1)
|
|
end = start + Vector2i.ONE * _thickness
|
|
for yy in range(start.y, end.y):
|
|
for xx in range(start.x, end.x):
|
|
array.append(Vector2i(xx, yy))
|
|
|
|
return array
|
|
|
|
|
|
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 _is_hovering_first_position(pos: Vector2) -> bool:
|
|
return _curve.point_count > 0 and _curve.get_point_position(0) == pos
|
|
|
|
|
|
# Thanks to
|
|
# https://www.reddit.com/r/godot/comments/3ktq39/drawing_empty_circles_and_curves/cv0f4eo/
|
|
func draw_empty_circle(
|
|
canvas: CanvasItem, circle_center: Vector2, circle_radius: Vector2, color: Color
|
|
) -> void:
|
|
var draw_counter := 1
|
|
var line_origin := Vector2()
|
|
var line_end := Vector2()
|
|
line_origin = circle_radius + circle_center
|
|
|
|
while draw_counter <= 360:
|
|
line_end = circle_radius.rotated(deg_to_rad(draw_counter)) + circle_center
|
|
canvas.draw_line(line_origin, line_end, color)
|
|
draw_counter += 1
|
|
line_origin = line_end
|
|
|
|
line_end = circle_radius.rotated(TAU) + circle_center
|
|
canvas.draw_line(line_origin, line_end, color)
|