Skip to content

Commit 4df815c

Browse files
authored
Fixes to path aligning with Ctrl (#1800)
1 parent 2ab2263 commit 4df815c

3 files changed

Lines changed: 72 additions & 27 deletions

File tree

src/data_classes/AttributePathdata.gd

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ func get_subpath(index: int) -> Vector2i:
5353
return Vector2i(subpath_start_indices[i], (subpath_start_indices[i + 1] if i < subpath_start_indices.size() - 1 else get_command_count()) - 1)
5454
return Vector2i(-1, -1)
5555

56+
# Check if a command is zero-length.
57+
func is_command_zero_length(index: int) -> bool:
58+
var cmd := get_command(index)
59+
if cmd is PathCommand.MoveCommand:
60+
return true
61+
elif cmd is PathCommand.CloseCommand:
62+
var start_cmd := get_command(get_subpath(index).x)
63+
return is_equal_approx(cmd.start_x, start_cmd.x) and is_equal_approx(cmd.start_y, start_cmd.y)
64+
elif cmd is PathCommand.HorizontalLineCommand:
65+
return is_equal_approx(cmd.start_x, cmd.x)
66+
elif cmd is PathCommand.VerticalLineCommand:
67+
return is_equal_approx(cmd.start_y, cmd.y)
68+
else:
69+
return is_equal_approx(cmd.start_x, cmd.x) and is_equal_approx(cmd.start_y, cmd.y)
70+
5671
# Gets the implied shorthand cubic bezier curve control. Not dependent on the current path command (even if it's not a curve).
5772
func get_implied_S_control(index: int) -> PackedFloat64Array:
5873
var cmd := get_command(index)

src/ui_widgets/PathHandle.gd

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,37 +22,61 @@ func set_position(new_position: PackedFloat64Array) -> void:
2222
var pathdata: AttributePathdata = element.get_attribute(pathdata_name)
2323

2424
# Constrain to match the angle of the neighboring segment if Ctrl is pressed.
25+
# Quadratic beziers can have two neighboring segments, align to the closest.
2526
if Input.is_key_pressed(KEY_CTRL):
2627
var cmd := pathdata.get_command(command_index)
28+
var cmd_char := cmd.command_char.to_lower()
29+
var subpath := pathdata.get_subpath(command_index)
2730

28-
var opposite: PackedFloat64Array
29-
var offset: PackedFloat64Array
30-
if command_index != 0 and pathdata.get_command(command_index - 1).command_char in "CcSsQqLlHhVv" and\
31-
cmd.command_char in "CcQqLl" and not (cmd.command_char.to_lower() in "CcQq" and (x_param != "x1" or y_param != "y1")):
32-
# Using previous command.
33-
var other := pathdata.get_command(command_index - 1)
34-
offset = [cmd.start_x, cmd.start_y]
35-
36-
match other.command_char:
37-
"C", "c", "S", "s": opposite = [other.x2, other.y2]
38-
"Q", "q": opposite = [other.x1, other.y1]
39-
"L", "l", "H", "h", "V", "v": opposite = [other.start_x, other.start_y]
40-
elif command_index < pathdata.get_command_count() - 1 and pathdata.get_command(command_index + 1).command_char in "CcQqLlHhVv" and\
41-
cmd.command_char in "CcSsQq" and not (cmd.command_char in "CcSs" and\
42-
(x_param != "x2" or y_param != "y2") or cmd.command_char in "Qq" and (x_param != "x1" or y_param != "y1")):
43-
# Using next command.
44-
var other := pathdata.get_command(command_index + 1)
45-
offset = [other.start_x, other.start_y]
46-
47-
match other.command_char:
48-
"C", "c", "Q", "q": opposite = [other.x1, other.y1]
49-
"L", "l": opposite = [other.x, other.y]
50-
"H", "h": opposite = [other.x, other.start_y]
51-
"V", "v": opposite = [other.start_x, other.y]
31+
# Batches of 4 coordinates representing lines: [x1, y1, x2, y2].
32+
var alignment_lines: Array[PackedFloat64Array] = []
5233

53-
opposite = [opposite[0] - offset[0], opposite[1] - offset[1]]
54-
new_position = Utils64Bit.vector_project([new_position[0] - offset[0], new_position[1] - offset[1]], opposite)
55-
new_position = [new_position[0] + offset[0], new_position[1] + offset[1]]
34+
if cmd_char in "l" or (cmd_char in "cq" and x_param == "x1" and y_param == "y1"):
35+
var prev_cmd_index := command_index
36+
while true:
37+
prev_cmd_index -= 1
38+
if prev_cmd_index < subpath.x and pathdata.get_command(subpath.y) is PathCommand.CloseCommand:
39+
prev_cmd_index = subpath.y
40+
if not pathdata.is_command_zero_length(prev_cmd_index) or prev_cmd_index == command_index:
41+
break
42+
if prev_cmd_index != command_index:
43+
var prev_cmd := pathdata.get_command(prev_cmd_index)
44+
var new_line := PackedFloat64Array([cmd.start_x, cmd.start_y])
45+
match prev_cmd.command_char.to_lower():
46+
"c", "s": alignment_lines.append(new_line + PackedFloat64Array([prev_cmd.x2, prev_cmd.y2]))
47+
"q": alignment_lines.append(new_line + PackedFloat64Array([prev_cmd.x1, prev_cmd.y1]))
48+
"l", "h", "v": alignment_lines.append(new_line + PackedFloat64Array([prev_cmd.start_x, prev_cmd.start_y]))
49+
50+
if (cmd_char in "cs" and x_param == "x2" and y_param == "y2") or (cmd_char in "q" and x_param == "x1" and y_param == "y1"):
51+
var next_cmd_index := command_index
52+
while true:
53+
next_cmd_index += 1
54+
if next_cmd_index > subpath.y and pathdata.get_command(subpath.y) is PathCommand.CloseCommand:
55+
next_cmd_index = subpath.x
56+
if not pathdata.is_command_zero_length(next_cmd_index) or next_cmd_index == command_index:
57+
break
58+
if next_cmd_index != command_index:
59+
var next_cmd := pathdata.get_command(next_cmd_index)
60+
var new_line := PackedFloat64Array([cmd.x, cmd.y])
61+
match next_cmd.command_char.to_lower():
62+
"c", "q": alignment_lines.append(new_line + PackedFloat64Array([next_cmd.x1, next_cmd.y1]))
63+
"l": alignment_lines.append(new_line + PackedFloat64Array([next_cmd.x, next_cmd.y]))
64+
"h": alignment_lines.append(new_line + PackedFloat64Array([next_cmd.x, next_cmd.start_y]))
65+
"v": alignment_lines.append(new_line + PackedFloat64Array([next_cmd.start_x, next_cmd.y]))
66+
"z":
67+
var start_command := pathdata.get_command(subpath.x)
68+
alignment_lines.append(new_line + PackedFloat64Array([start_command.x, start_command.y]))
69+
70+
var closest_distance := INF
71+
for line in alignment_lines:
72+
var direction := [line[0] - line[2], line[1] - line[3]]
73+
var relative_position := [new_position[0] - line[2], new_position[1] - line[3]]
74+
var projected := Utils64Bit.vector_project(relative_position, direction)
75+
var projected_position := [projected[0] + line[2], projected[1] + line[3]]
76+
var distance := Utils64Bit.distance_squared_to(projected_position, new_position)
77+
if distance < closest_distance:
78+
closest_distance = distance
79+
new_position = projected_position
5680

5781
pathdata.set_command_property(command_index, x_param, new_position[0])
5882
pathdata.set_command_property(command_index, y_param, new_position[1])

src/utils/Utils64Bit.gd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,9 @@ static func vector_project(a: PackedFloat64Array, b: PackedFloat64Array) -> Pack
8484
## 64-bit version of Vector2.dot(Vector2) = float
8585
static func dot(a: PackedFloat64Array, b: PackedFloat64Array) -> float:
8686
return a[0] * b[0] + a[1] * b[1]
87+
88+
## 64-bit version of Vector2.distance_squared_to(Vector2) = float
89+
static func distance_squared_to(a: PackedFloat64Array, b: PackedFloat64Array) -> float:
90+
var x := a[0] - b[0]
91+
var y := a[1] - b[1]
92+
return x * x + y * y

0 commit comments

Comments
 (0)