mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-08 00:05:00 +00:00
Add Path tool feature for angle locking upon pressing Ctrl while dragging handle over anchor (#2612)
* almost_fixed * fix need to refactor * fixed issed need to refactor * refactor-done fixed issue * move function to common_functionality --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
3d37ef79ac
commit
d39308c048
5 changed files with 150 additions and 38 deletions
|
@ -729,7 +729,9 @@ impl ShapeState {
|
|||
let length = transform.transform_vector2(unselected_position - anchor).length();
|
||||
let position = transform.inverse().transform_vector2(direction * length);
|
||||
let modification_type = unselected_handle.set_relative_position(position);
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
if (anchor - selected_position).length() > 1e-6 {
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
}
|
||||
}
|
||||
// If both handles are selected, average the angles of the handles
|
||||
else {
|
||||
|
@ -775,6 +777,7 @@ impl ShapeState {
|
|||
in_viewport_space: bool,
|
||||
was_alt_dragging: bool,
|
||||
opposite_handle_position: Option<DVec2>,
|
||||
skip_opposite_handle: bool,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) {
|
||||
for (&layer, state) in &self.selected_shape_state {
|
||||
|
@ -816,6 +819,11 @@ impl ShapeState {
|
|||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
|
||||
let Some(other) = vector_data.other_colinear_handle(handle) else { continue };
|
||||
|
||||
if skip_opposite_handle {
|
||||
continue;
|
||||
}
|
||||
|
||||
if state.is_selected(other.to_manipulator_point()) {
|
||||
// If two colinear handles are being dragged at the same time but not the anchor, it is necessary to break the colinear state.
|
||||
let handles = [handle, other];
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils::get_t
|
|||
use glam::DVec2;
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::text::{FontCache, load_face};
|
||||
use graphene_std::vector::PointId;
|
||||
use graphene_std::vector::{ManipulatorPointId, PointId, SegmentId, VectorData};
|
||||
|
||||
/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
|
||||
pub fn should_extend(
|
||||
|
@ -66,3 +66,30 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH
|
|||
|
||||
Quad::from_box([DVec2::ZERO, far])
|
||||
}
|
||||
|
||||
pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data: &VectorData, pen_tool: bool) -> Option<f64> {
|
||||
let is_start = |point: PointId, segment: SegmentId| vector_data.segment_start_from_id(segment) == Some(point);
|
||||
let anchor_position = vector_data.point_domain.position_from_id(anchor)?;
|
||||
let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector_data);
|
||||
let start_handle = ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data);
|
||||
|
||||
let start_point = if is_start(anchor, segment) {
|
||||
vector_data.segment_end_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
|
||||
} else {
|
||||
vector_data.segment_start_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
|
||||
};
|
||||
|
||||
let required_handle = if is_start(anchor, segment) {
|
||||
start_handle
|
||||
.filter(|&handle| pen_tool && handle != anchor_position)
|
||||
.or(end_handle.filter(|&handle| Some(handle) != start_point))
|
||||
.or(start_point)
|
||||
} else {
|
||||
end_handle
|
||||
.filter(|&handle| pen_tool && handle != anchor_position)
|
||||
.or(start_handle.filter(|&handle| Some(handle) != start_point))
|
||||
.or(start_point)
|
||||
};
|
||||
|
||||
required_handle.map(|handle| -(handle - anchor_position).angle_to(DVec2::X))
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ use crate::messages::tool::common_functionality::shape_editor::{
|
|||
ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState,
|
||||
};
|
||||
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
|
||||
use crate::messages::tool::common_functionality::utility_functions::calculate_segment_angle;
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::vector::{ManipulatorPointId, PointId, VectorModificationType};
|
||||
use graphene_std::vector::{HandleId, NoHashBuilder, SegmentId, VectorData};
|
||||
|
@ -380,6 +381,8 @@ struct PathToolData {
|
|||
snapping_axis: Option<Axis>,
|
||||
alt_clicked_on_anchor: bool,
|
||||
alt_dragging_from_anchor: bool,
|
||||
angle_locked: bool,
|
||||
temporary_colinear_handles: bool,
|
||||
}
|
||||
|
||||
impl PathToolData {
|
||||
|
@ -727,11 +730,38 @@ impl PathToolData {
|
|||
Some((handle_position_document, anchor_position_document, handle_id))
|
||||
}
|
||||
|
||||
fn calculate_handle_angle(&mut self, handle_vector: DVec2, handle_id: ManipulatorPointId, lock_angle: bool, snap_angle: bool) -> f64 {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn calculate_handle_angle(
|
||||
&mut self,
|
||||
shape_editor: &mut ShapeState,
|
||||
document: &DocumentMessageHandler,
|
||||
responses: &mut VecDeque<Message>,
|
||||
relative_vector: DVec2,
|
||||
handle_vector: DVec2,
|
||||
handle_id: ManipulatorPointId,
|
||||
lock_angle: bool,
|
||||
snap_angle: bool,
|
||||
) -> f64 {
|
||||
let current_angle = -handle_vector.angle_to(DVec2::X);
|
||||
|
||||
if let Some(vector_data) = shape_editor
|
||||
.selected_shape_state
|
||||
.iter()
|
||||
.next()
|
||||
.and_then(|(layer, _)| document.network_interface.compute_modified_vector(*layer))
|
||||
{
|
||||
if relative_vector.length() < 25. && lock_angle && !self.angle_locked {
|
||||
if let Some(angle) = calculate_lock_angle(self, shape_editor, responses, document, &vector_data, handle_id) {
|
||||
self.angle = angle;
|
||||
return angle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When the angle is locked we use the old angle
|
||||
|
||||
if self.current_selected_handle_id == Some(handle_id) && lock_angle {
|
||||
self.angle_locked = true;
|
||||
return self.angle;
|
||||
}
|
||||
|
||||
|
@ -785,7 +815,7 @@ impl PathToolData {
|
|||
let drag_start = self.drag_start_pos;
|
||||
let opposite_delta = drag_start - current_mouse;
|
||||
|
||||
shape_editor.move_selected_points(None, document, opposite_delta, false, true, false, None, responses);
|
||||
shape_editor.move_selected_points(None, document, opposite_delta, false, true, false, None, false, responses);
|
||||
|
||||
// Calculate the projected delta and shift the points along that delta
|
||||
let delta = current_mouse - drag_start;
|
||||
|
@ -797,7 +827,7 @@ impl PathToolData {
|
|||
_ => DVec2::new(delta.x, 0.),
|
||||
};
|
||||
|
||||
shape_editor.move_selected_points(None, document, projected_delta, false, true, false, None, responses);
|
||||
shape_editor.move_selected_points(None, document, projected_delta, false, true, false, None, false, responses);
|
||||
}
|
||||
|
||||
fn stop_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
|
@ -813,12 +843,12 @@ impl PathToolData {
|
|||
_ => DVec2::new(opposite_delta.x, 0.),
|
||||
};
|
||||
|
||||
shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, false, None, responses);
|
||||
shape_editor.move_selected_points(None, document, opposite_projected_delta, false, true, false, None, false, responses);
|
||||
|
||||
// Calculate what actually would have been the original delta for the point, and apply that
|
||||
let delta = current_mouse - drag_start;
|
||||
|
||||
shape_editor.move_selected_points(None, document, delta, false, true, false, None, responses);
|
||||
shape_editor.move_selected_points(None, document, delta, false, true, false, None, false, responses);
|
||||
|
||||
self.snapping_axis = None;
|
||||
}
|
||||
|
@ -872,7 +902,7 @@ impl PathToolData {
|
|||
let snapped_delta = if let Some((handle_pos, anchor_pos, handle_id)) = self.try_get_selected_handle_and_anchor(shape_editor, document) {
|
||||
let cursor_pos = handle_pos + raw_delta;
|
||||
|
||||
let handle_angle = self.calculate_handle_angle(cursor_pos - anchor_pos, handle_id, lock_angle, snap_angle);
|
||||
let handle_angle = self.calculate_handle_angle(shape_editor, document, responses, handle_pos - anchor_pos, cursor_pos - anchor_pos, handle_id, lock_angle, snap_angle);
|
||||
|
||||
let constrained_direction = DVec2::new(handle_angle.cos(), handle_angle.sin());
|
||||
let projected_length = (cursor_pos - anchor_pos).dot(constrained_direction);
|
||||
|
@ -931,7 +961,14 @@ impl PathToolData {
|
|||
self.alt_dragging_from_anchor = false;
|
||||
self.alt_clicked_on_anchor = false;
|
||||
}
|
||||
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, responses);
|
||||
|
||||
let mut skip_opposite = false;
|
||||
if self.temporary_colinear_handles && !lock_angle {
|
||||
shape_editor.disable_colinear_handles_state_on_selected(&document.network_interface, responses);
|
||||
self.temporary_colinear_handles = false;
|
||||
skip_opposite = true;
|
||||
}
|
||||
shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, true, was_alt_dragging, opposite, skip_opposite, responses);
|
||||
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(snapped_delta);
|
||||
} else {
|
||||
let Some(axis) = self.snapping_axis else { return };
|
||||
|
@ -940,7 +977,7 @@ impl PathToolData {
|
|||
Axis::Y => DVec2::new(0., unsnapped_delta.y),
|
||||
_ => DVec2::new(unsnapped_delta.x, 0.),
|
||||
};
|
||||
shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, false, opposite, responses);
|
||||
shape_editor.move_selected_points(handle_lengths, document, projected_delta, equidistant, true, false, opposite, false, responses);
|
||||
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta);
|
||||
}
|
||||
|
||||
|
@ -1238,6 +1275,10 @@ impl Fsm for PathToolFsmState {
|
|||
let lock_angle_state = input.keyboard.get(lock_angle as usize);
|
||||
let snap_angle_state = input.keyboard.get(snap_angle as usize);
|
||||
|
||||
if !lock_angle_state {
|
||||
tool_data.angle_locked = false;
|
||||
}
|
||||
|
||||
if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) {
|
||||
tool_data.drag(
|
||||
equidistant_state,
|
||||
|
@ -1412,6 +1453,10 @@ impl Fsm for PathToolFsmState {
|
|||
}
|
||||
}
|
||||
|
||||
if tool_data.temporary_colinear_handles {
|
||||
tool_data.temporary_colinear_handles = false;
|
||||
}
|
||||
|
||||
if tool_data.handle_drag_toggle && drag_occurred {
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag);
|
||||
|
@ -1511,6 +1556,7 @@ impl Fsm for PathToolFsmState {
|
|||
false,
|
||||
false,
|
||||
tool_data.opposite_handle_position,
|
||||
false,
|
||||
responses,
|
||||
);
|
||||
|
||||
|
@ -1749,3 +1795,52 @@ fn get_selection_status(network_interface: &NodeNetworkInterface, shape_state: &
|
|||
|
||||
SelectionStatus::None
|
||||
}
|
||||
|
||||
fn calculate_lock_angle(
|
||||
tool_data: &mut PathToolData,
|
||||
shape_state: &mut ShapeState,
|
||||
responses: &mut VecDeque<Message>,
|
||||
document: &DocumentMessageHandler,
|
||||
vector_data: &VectorData,
|
||||
handle_id: ManipulatorPointId,
|
||||
) -> Option<f64> {
|
||||
let anchor = handle_id.get_anchor(vector_data)?;
|
||||
let anchor_position = vector_data.point_domain.position_from_id(anchor);
|
||||
let current_segment = handle_id.get_segment();
|
||||
let points_connected = vector_data.connected_count(anchor);
|
||||
|
||||
let (anchor_position, segment) = anchor_position.zip(current_segment)?;
|
||||
if points_connected == 1 {
|
||||
calculate_segment_angle(anchor, segment, vector_data, false)
|
||||
} else {
|
||||
let opposite_handle = handle_id
|
||||
.get_handle_pair(vector_data)
|
||||
.iter()
|
||||
.flatten()
|
||||
.find(|&h| h.to_manipulator_point() != handle_id)
|
||||
.copied()
|
||||
.map(|h| h.to_manipulator_point());
|
||||
let opposite_handle_position = opposite_handle.and_then(|h| h.get_position(vector_data)).filter(|pos| (pos - anchor_position).length() > 1e-6);
|
||||
|
||||
if let Some(opposite_pos) = opposite_handle_position {
|
||||
if !vector_data.colinear_manipulators.iter().flatten().map(|h| h.to_manipulator_point()).any(|h| h == handle_id) {
|
||||
shape_state.convert_selected_manipulators_to_colinear_handles(responses, document);
|
||||
tool_data.temporary_colinear_handles = true;
|
||||
}
|
||||
Some(-(opposite_pos - anchor_position).angle_to(DVec2::X))
|
||||
} else {
|
||||
let angle_1 = vector_data
|
||||
.adjacent_segment(&handle_id)
|
||||
.and_then(|(_, adjacent_segment)| calculate_segment_angle(anchor, adjacent_segment, vector_data, false));
|
||||
|
||||
let angle_2 = calculate_segment_angle(anchor, segment, vector_data, false);
|
||||
|
||||
match (angle_1, angle_2) {
|
||||
(Some(angle_1), Some(angle_2)) => Some((angle_1 + angle_2) / 2.0),
|
||||
(Some(angle_1), None) => Some(angle_1),
|
||||
(None, Some(angle_2)) => Some(angle_2),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::messages::tool::common_functionality::color_selector::{ToolColorOptio
|
|||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, merge_layers};
|
||||
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
|
||||
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnapTypeConfiguration};
|
||||
use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend};
|
||||
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, closest_point, should_extend};
|
||||
use bezier_rs::{Bezier, BezierHandles};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::Color;
|
||||
|
@ -1295,31 +1295,8 @@ impl PenToolData {
|
|||
|
||||
match (self.handle_type, self.path_closed) {
|
||||
(TargetHandle::FuturePreviewOutHandle, _) | (TargetHandle::PreviewInHandle, true) => {
|
||||
let is_start = |point: PointId, segment: SegmentId| vector_data.segment_start_from_id(segment) == Some(point);
|
||||
|
||||
let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector_data);
|
||||
let start_handle = ManipulatorPointId::PrimaryHandle(segment).get_position(vector_data);
|
||||
|
||||
let start_point = if is_start(anchor, segment) {
|
||||
vector_data.segment_end_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
|
||||
} else {
|
||||
vector_data.segment_start_from_id(segment).and_then(|id| vector_data.point_domain.position_from_id(id))
|
||||
};
|
||||
|
||||
let required_handle = if is_start(anchor, segment) {
|
||||
start_handle
|
||||
.filter(|&handle| handle != anchor_position)
|
||||
.or(end_handle.filter(|&handle| Some(handle) != start_point))
|
||||
.or(start_point)
|
||||
} else {
|
||||
end_handle
|
||||
.filter(|&handle| handle != anchor_position)
|
||||
.or(start_handle.filter(|&handle| Some(handle) != start_point))
|
||||
.or(start_point)
|
||||
};
|
||||
|
||||
if let Some(required_handle) = required_handle {
|
||||
self.angle = -(required_handle - anchor_position).angle_to(DVec2::X);
|
||||
if let Some(required_handle) = calculate_segment_angle(anchor, segment, vector_data, true) {
|
||||
self.angle = required_handle;
|
||||
self.handle_mode = HandleMode::ColinearEquidistant;
|
||||
}
|
||||
}
|
||||
|
@ -1332,8 +1309,6 @@ impl PenToolData {
|
|||
self.handle_mode = HandleMode::ColinearEquidistant;
|
||||
}
|
||||
}
|
||||
|
||||
// Closure to check if a point is the start or end of a segment
|
||||
}
|
||||
|
||||
fn add_point_layer_position(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, viewport: DVec2) {
|
||||
|
|
|
@ -561,6 +561,13 @@ impl ManipulatorPointId {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_segment(self) -> Option<SegmentId> {
|
||||
match self {
|
||||
ManipulatorPointId::PrimaryHandle(segment) | ManipulatorPointId::EndHandle(segment) => Some(segment),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of handle found on a bézier curve.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue