Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 79 additions & 15 deletions src/Tools/DesignTools/CurveTool.gd
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
extends BaseDrawTool

enum SingleState { START, END, MIDDLE_A, MIDDLE_B, READY }
enum Bezier { CHAINED, SINGLE }

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_inside := false ## When true, the inside area of the curve gets filled.
Expand All @@ -8,6 +11,11 @@ var _editing_bezier := false ## Needed to determine when to show the control po
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
## chained means Krita-like behavior, single means Aseprite-like.
var _bezier_mode: int = Bezier.CHAINED
var _current_state: int = SingleState.START ## Current state of the bezier curve (in SINGLE mode)

@onready var bezier_option_button: OptionButton = $BezierOptions/BezierMode


func _init() -> void:
Expand All @@ -28,6 +36,12 @@ func _on_thickness_value_changed(value: int) -> void:
save_config()


func _on_bezier_mode_item_selected(index: int) -> void:
_bezier_mode = index
update_config()
save_config()


func _on_fill_checkbox_toggled(toggled_on: bool) -> void:
_fill_inside = toggled_on
update_config()
Expand All @@ -46,19 +60,22 @@ func get_config() -> Dictionary:
var config := super.get_config()
config["fill_inside"] = _fill_inside
config["thickness"] = _thickness
config["bezier_mode"] = _bezier_mode
return config


func set_config(config: Dictionary) -> void:
super.set_config(config)
_fill_inside = config.get("fill_inside", _fill_inside)
_thickness = config.get("thickness", _thickness)
_bezier_mode = config.get("bezier_mode", _bezier_mode)


func update_config() -> void:
super.update_config()
$FillCheckbox.button_pressed = _fill_inside
$ThicknessSlider.value = _thickness
bezier_option_button.select(_bezier_mode)


## This tool has no brush, so just return the indicator as it is.
Expand All @@ -72,18 +89,44 @@ func _input(event: InputEvent) -> void:
_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
if _bezier_mode == Bezier.SINGLE and _current_state >= SingleState.MIDDLE_A:
# if bezier's curvature is being changed in SINGLE mode
if _current_state == SingleState.MIDDLE_A:
_curve.set_point_out(
0, Vector2(_last_mouse_position) - _curve.get_point_position(0)
)
_curve.set_point_in(
1,
(
Vector2(_last_mouse_position)
- _curve.get_point_position(_curve.point_count - 1)
)
)
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 _bezier_mode == Bezier.CHAINED:
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)
match _current_state:
SingleState.START: # Point removed in CHAINED mode
_curve.remove_point(_curve.point_count - 1)
SingleState.MIDDLE_A: # End point is to be removed in SINGLE mode
_curve.remove_point(_curve.point_count - 1)
_curve.set_point_out(0, Vector2.ZERO)
_editing_bezier = false
_current_state -= 1
SingleState.MIDDLE_B: # Bezier curvature is to be reset in SINGLE mode
_curve.set_point_out(
0, _last_mouse_position - _curve.get_point_position(0)
)
_current_state -= 1


func draw_start(pos: Vector2i) -> void:
Expand All @@ -95,11 +138,18 @@ func draw_start(pos: Vector2i) -> void:
_picking_color = true
_pick_color(pos)
return
_picking_color = false # fixes _picking_color being true indefinitely after we pick color
Global.canvas.selection.transform_content_confirm()
update_mask()
if !_drawing:
bezier_option_button.disabled = true
_drawing = true
_curve.add_point(pos)
_current_state = SingleState.START
# NOTE: _current_state of CHAINED mode is always SingleState.START so it will always pass this.
if _current_state == SingleState.START or _current_state == SingleState.END:
_curve.add_point(pos)
if _bezier_mode == Bezier.SINGLE:
_current_state += 1
_fill_inside_rect = Rect2i(pos, Vector2i.ZERO)


Expand All @@ -110,7 +160,7 @@ func draw_move(pos: Vector2i) -> void:
if Input.is_action_pressed("shape_displace"):
_pick_color(pos)
return
if _drawing:
if _drawing and _bezier_mode == Bezier.CHAINED:
_editing_bezier = true
var current_position := _curve.get_point_position(_curve.point_count - 1) - Vector2(pos)
if not _editing_out_control_point:
Expand All @@ -119,8 +169,15 @@ func draw_move(pos: Vector2i) -> void:


func draw_end(pos: Vector2i) -> void:
_editing_bezier = false
if _is_hovering_first_position(pos) and _curve.point_count > 1:
# we still need bezier preview when curve is in SINGLE mode.
if _current_state == SingleState.MIDDLE_A or _current_state == SingleState.MIDDLE_B:
_editing_bezier = true
else:
_editing_bezier = false
if (
(_is_hovering_first_position(pos) and _curve.point_count > 1)
or _current_state == SingleState.READY
):
_draw_shape()
super.draw_end(pos)

Expand Down Expand Up @@ -155,11 +212,15 @@ func draw_preview() -> void:
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 current_point = _curve.point_count - 1
if _current_state == SingleState.MIDDLE_A:
# We need this when we are modifying curve of "first" point in SINGLE mode
current_point = 0
var current_position := _curve.get_point_position(current_point)
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 current_point > 0:
start = current_position + _curve.get_point_in(current_point)
var end := current_position + _curve.get_point_out(current_point)
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
Expand All @@ -172,6 +233,7 @@ func draw_preview() -> void:


func _draw_shape() -> void:
bezier_option_button.disabled = false
var points := _bezier()
prepare_undo("Draw Shape")
var images := _get_selected_draw_images()
Expand Down Expand Up @@ -218,9 +280,11 @@ func _bezier() -> Array[Vector2i]:
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)
if _current_state <= SingleState.END: # this is general for both modes
_curve.add_point(last_pixel)
var points := _curve.get_baked_points()
_curve.remove_point(_curve.point_count - 1)
if _current_state <= SingleState.END: # this is general for both modes
_curve.remove_point(_curve.point_count - 1)
var final_points: Array[Vector2i] = []
for i in points.size() - 1:
var point1 := points[i]
Expand Down
28 changes: 25 additions & 3 deletions src/Tools/DesignTools/CurveTool.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,28 @@ suffix = "px"
global_increment_action = "brush_size_increment"
global_decrement_action = "brush_size_decrement"

[node name="FillCheckbox" type="CheckBox" parent="." index="3"]
[node name="BezierOptions" type="HBoxContainer" parent="." index="3"]
layout_mode = 2

[node name="Label" type="Label" parent="BezierOptions" index="0"]
layout_mode = 2
text = "Mode:"

[node name="BezierMode" type="OptionButton" parent="BezierOptions" index="1"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Chained: Makes a chain of multiple bezier curves (Krita inspired).

Single: Makes a single bezier curve (Asiprite inspired)"
alignment = 1
selected = 0
item_count = 2
popup/item_0/text = "Chained"
popup/item_0/id = 0
popup/item_1/text = "Single"
popup/item_1/id = 1

[node name="FillCheckbox" type="CheckBox" parent="." index="4"]
layout_mode = 2
tooltip_text = "Fills the drawn shape with color, instead of drawing a hollow shape"
mouse_default_cursor_shape = 2
Expand All @@ -35,12 +56,13 @@ button_group = SubResource("ButtonGroup_drx24")
[node name="Rotate270" parent="RotationOptions/GridContainer/Rotate" index="2"]
button_group = SubResource("ButtonGroup_drx24")

[node name="Brush" parent="." index="5"]
[node name="Brush" parent="." index="6"]
visible = false

[node name="DoubleClickTimer" type="Timer" parent="." index="7"]
[node name="DoubleClickTimer" type="Timer" parent="." index="8"]
wait_time = 0.2
one_shot = true

[connection signal="value_changed" from="ThicknessSlider" to="." method="_on_thickness_value_changed"]
[connection signal="item_selected" from="BezierOptions/BezierMode" to="." method="_on_bezier_mode_item_selected"]
[connection signal="toggled" from="FillCheckbox" to="." method="_on_fill_checkbox_toggled"]