1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-30 23:19:49 +00:00

Curve tool implementation (#1019)

* curve tool

* formatting

* formatting

* saving my progress

* update to kirita mode

* Formatting

* fixes for mirror mode

* added way to remove point, added tool shortcut

* Add translation strings

* Use Curve2D instead of a control_points array

---------

Co-authored-by: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com>
This commit is contained in:
Variable 2024-05-20 05:46:40 +05:00 committed by GitHub
parent fe4fc8b0a2
commit 601c25f2dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 380 additions and 0 deletions

View file

@ -1275,6 +1275,16 @@ msgid "Line Tool\n\n"
"Hold %s to displace the shape's origin" "Hold %s to displace the shape's origin"
msgstr "" msgstr ""
msgid "Curve Tool\n\n"
"%s for left mouse button\n"
"%s for right mouse button\n\n"
"Draws bezier curves\n"
"Press and drag to control the curvature\n"
"Press and drag to curve them\n"
"Press %s to remove the last added point"
msgstr ""
msgid "Rectangle Tool\n\n" msgid "Rectangle Tool\n\n"
"%s for left mouse button\n" "%s for left mouse button\n"
"%s for right mouse button\n\n" "%s for right mouse button\n\n"
@ -2342,6 +2352,9 @@ msgstr ""
msgid "Line Tool" msgid "Line Tool"
msgstr "" msgstr ""
msgid "Curve Tool"
msgstr ""
msgid "Rectangle Tool" msgid "Rectangle Tool"
msgstr "" msgstr ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://deuy67ughoib3"
path="res://.godot/imported/curvetool.png-035b507342a996edf5b660cc7e5f8671.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/graphics/tools/cursors/curvetool.png"
dest_files=["res://.godot/imported/curvetool.png-035b507342a996edf5b660cc7e5f8671.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://br0lilqrvdvdc"
path="res://.godot/imported/curvetool.png-15311b84332d233b1a0c1817d9a6bd5d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/graphics/tools/curvetool.png"
dest_files=["res://.godot/imported/curvetool.png-15311b84332d233b1a0c1817d9a6bd5d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -865,6 +865,16 @@ go_to_next_layer={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"command_or_control_autoremap":true,"alt_pressed":false,"shift_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"echo":false,"script":null) "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"command_or_control_autoremap":true,"alt_pressed":false,"shift_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"echo":false,"script":null)
] ]
} }
left_curvetool_tool={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":74,"physical_keycode":0,"key_label":0,"unicode":106,"echo":false,"script":null)
]
}
right_curvetool_tool={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":74,"physical_keycode":0,"key_label":0,"unicode":106,"echo":false,"script":null)
]
}
[input_devices] [input_devices]

View file

@ -151,6 +151,22 @@ Hold %s to displace the shape's origin""",
["shape_perfect", "shape_center", "shape_displace"] ["shape_perfect", "shape_center", "shape_displace"]
) )
), ),
"CurveTool":
(
Tool
. new(
"CurveTool",
"Curve Tool",
"curvetool",
"res://src/Tools/DesignTools/CurveTool.tscn",
[Global.LayerTypes.PIXEL],
"""Draws bezier curves
Press %s/%s to add new points
Press and drag to control the curvature
Press %s to remove the last added point""",
["activate_left_tool", "activate_right_tool", "change_tool_mode"]
)
),
"RectangleTool": "RectangleTool":
( (
Tool Tool

View file

@ -0,0 +1,234 @@
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 _editing_bezier := false ## Needed to determine when to show the control points preview line.
var _thickness := 1 ## The thickness of the curve.
func _init() -> void:
Global.project_about_to_switch.connect(_draw_shape) # To prevent tool from remaining active
_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 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
func _input(event: InputEvent) -> void:
if _drawing:
if event is InputEventMouseButton:
if event.double_click and event.button_index == tool_slot.button:
$DoubleClickTimer.start()
_draw_shape()
else:
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)
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)
_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
super.draw_end(pos)
func draw_preview() -> void:
if not _drawing:
return
var canvas: Node2D = Global.canvas.previews
var pos := canvas.position
var canvas_scale := canvas.scale
if Global.mirror_view: # This fixes previewing in mirror mode
pos.x = pos.x + Global.current_project.size.x
canvas_scale.x = -1
var points := _bezier()
canvas.draw_set_transform(pos, canvas.rotation, canvas_scale)
var indicator := _fill_bitmap_with_points(points, Global.current_project.size)
for line in _create_polylines(indicator):
canvas.draw_polyline(PackedVector2Array(line), Color.BLACK)
canvas.draw_set_transform(canvas.position, canvas.rotation, canvas.scale)
if _editing_bezier:
var start := _curve.get_point_position(0)
if _curve.point_count > 1:
start = (
_curve.get_point_position(_curve.point_count - 1)
+ _curve.get_point_in(_curve.point_count - 1)
)
var end := (
_curve.get_point_position(_curve.point_count - 1)
+ _curve.get_point_out(_curve.point_count - 1)
)
if Global.mirror_view: # This fixes previewing in mirror mode
start.x = Global.current_project.size.x - start.x - 1
end.x = Global.current_project.size.x - end.x - 1
canvas.draw_line(start, end, Color.BLACK)
var circle_radius := Vector2.ONE * (5.0 / Global.camera.zoom.x)
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")
for point in points:
# Reset drawer every time because pixel perfect sometimes breaks the tool
_drawer.reset()
# Draw each point offsetted based on the shape's thickness
draw_tool(point)
_curve.clear_points()
_drawing = false
commit_undo()
## 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
# 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)

View file

@ -0,0 +1,39 @@
[gd_scene load_steps=5 format=3 uid="uid://ckfvd8gf3oy4r"]
[ext_resource type="PackedScene" uid="uid://ubyatap3sylf" path="res://src/Tools/BaseDraw.tscn" id="1_rvuea"]
[ext_resource type="Script" path="res://src/Tools/DesignTools/CurveTool.gd" id="2_tjnp6"]
[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/ValueSlider.tscn" id="3_g0nav"]
[sub_resource type="ButtonGroup" id="ButtonGroup_ko8wf"]
resource_name = "rotate"
allow_unpress = true
[node name="ToolOptions" instance=ExtResource("1_rvuea")]
script = ExtResource("2_tjnp6")
[node name="ThicknessSlider" parent="." index="2" instance=ExtResource("3_g0nav")]
layout_mode = 2
min_value = 1.0
value = 1.0
prefix = "Size:"
suffix = "px"
global_increment_action = "brush_size_increment"
global_decrement_action = "brush_size_decrement"
[node name="Rotate90" parent="RotationOptions/Rotate" index="0"]
button_group = SubResource("ButtonGroup_ko8wf")
[node name="Rotate180" parent="RotationOptions/Rotate" index="1"]
button_group = SubResource("ButtonGroup_ko8wf")
[node name="Rotate270" parent="RotationOptions/Rotate" index="2"]
button_group = SubResource("ButtonGroup_ko8wf")
[node name="Brush" parent="." index="4"]
visible = false
[node name="DoubleClickTimer" type="Timer" parent="." index="6"]
wait_time = 0.2
one_shot = true
[connection signal="value_changed" from="ThicknessSlider" to="." method="_on_Thickness_value_changed"]