mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
Add anchor sliding along adjacent segments in the Path tool (#2682)
* Improved comments * Add point sliding with approximate t value * Add similarity calculation * Numerical approach to fit the curve * Reliable point sliding for cubic segments * Fix formatting and clean comments * Fix cubic with one handle logic * Cancel on right click and escape * Two parameter optimization * Esc/ Right click cancellation * Code review * Fix dynamic hints * Revert selected_points_counts and fix comments * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
eda858b6e2
commit
781271782c
3 changed files with 349 additions and 36 deletions
|
@ -107,11 +107,7 @@ impl SelectedLayerState {
|
|||
}
|
||||
|
||||
pub fn selected_points_count(&self) -> usize {
|
||||
let count = self.selected_points.iter().fold(0, |acc, point| {
|
||||
let is_ignored = (point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors);
|
||||
acc + if is_ignored { 0 } else { 1 }
|
||||
});
|
||||
count
|
||||
self.selected_points.len()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
|||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_text;
|
||||
use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
|
||||
use bezier_rs::Bezier;
|
||||
use glam::DVec2;
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::text::{FontCache, load_face};
|
||||
|
@ -196,3 +197,99 @@ pub fn is_visible_point(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates similarity metric between new bezier curve and two old beziers by using sampled points.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn log_optimization(a: f64, b: f64, p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, points1: &[DVec2], n: usize) -> f64 {
|
||||
let start_handle_length = a.exp();
|
||||
let end_handle_length = b.exp();
|
||||
|
||||
// Compute the handle positions of new bezier curve
|
||||
let c1 = p1 + d1 * start_handle_length;
|
||||
let c2 = p3 + d2 * end_handle_length;
|
||||
|
||||
let new_curve = Bezier::from_cubic_coordinates(p1.x, p1.y, c1.x, c1.y, c2.x, c2.y, p3.x, p3.y);
|
||||
|
||||
// Sample 2*n points from new curve and get the L2 metric between all of points
|
||||
let points = new_curve.compute_lookup_table(Some(2 * n), None).collect::<Vec<_>>();
|
||||
|
||||
let dist = points1.iter().zip(points.iter()).map(|(p1, p2)| (p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sum::<f64>();
|
||||
|
||||
dist / (2 * n) as f64
|
||||
}
|
||||
|
||||
/// Calculates optimal handle lengths with adam optimization.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, min_len1: f64, min_len2: f64, farther_segment: Bezier, other_segment: Bezier) -> (DVec2, DVec2) {
|
||||
let h = 1e-6;
|
||||
let tol = 1e-6;
|
||||
let max_iter = 200;
|
||||
|
||||
let mut a = (5_f64).ln();
|
||||
let mut b = (5_f64).ln();
|
||||
|
||||
let mut m_a = 0.;
|
||||
let mut v_a = 0.;
|
||||
let mut m_b = 0.;
|
||||
let mut v_b = 0.;
|
||||
|
||||
let initial_alpha = 0.05;
|
||||
let decay_rate: f64 = 0.99;
|
||||
|
||||
let beta1 = 0.9;
|
||||
let beta2 = 0.999;
|
||||
let epsilon = 1e-8;
|
||||
|
||||
let n = 20;
|
||||
|
||||
let farther_segment = if farther_segment.start.distance(p1) >= f64::EPSILON {
|
||||
farther_segment.reverse()
|
||||
} else {
|
||||
farther_segment
|
||||
};
|
||||
|
||||
let other_segment = if other_segment.end.distance(p3) >= f64::EPSILON { other_segment.reverse() } else { other_segment };
|
||||
|
||||
// Now we sample points proportional to the lengths of the beziers
|
||||
let l1 = farther_segment.length(None);
|
||||
let l2 = other_segment.length(None);
|
||||
let ratio = l1 / (l1 + l2);
|
||||
let n_points1 = ((2 * n) as f64 * ratio).floor() as usize;
|
||||
let mut points1 = farther_segment.compute_lookup_table(Some(n_points1), None).collect::<Vec<_>>();
|
||||
let mut points2 = other_segment.compute_lookup_table(Some(n), None).collect::<Vec<_>>();
|
||||
points1.append(&mut points2);
|
||||
|
||||
let f = |a: f64, b: f64| -> f64 { log_optimization(a, b, p1, p3, d1, d2, &points1, n) };
|
||||
|
||||
for t in 1..=max_iter {
|
||||
let dfa = (f(a + h, b) - f(a - h, b)) / (2. * h);
|
||||
let dfb = (f(a, b + h) - f(a, b - h)) / (2. * h);
|
||||
|
||||
m_a = beta1 * m_a + (1. - beta1) * dfa;
|
||||
m_b = beta1 * m_b + (1. - beta1) * dfb;
|
||||
|
||||
v_a = beta2 * v_a + (1. - beta2) * dfa * dfa;
|
||||
v_b = beta2 * v_b + (1. - beta2) * dfb * dfb;
|
||||
|
||||
let m_a_hat = m_a / (1. - beta1.powi(t));
|
||||
let v_a_hat = v_a / (1. - beta2.powi(t));
|
||||
let m_b_hat = m_b / (1. - beta1.powi(t));
|
||||
let v_b_hat = v_b / (1. - beta2.powi(t));
|
||||
|
||||
let alpha_t = initial_alpha * decay_rate.powi(t);
|
||||
|
||||
// Update log-lengths
|
||||
a -= alpha_t * m_a_hat / (v_a_hat.sqrt() + epsilon);
|
||||
b -= alpha_t * m_b_hat / (v_b_hat.sqrt() + epsilon);
|
||||
|
||||
// Convergence check
|
||||
if dfa.abs() < tol && dfb.abs() < tol {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let len1 = a.exp().max(min_len1);
|
||||
let len2 = b.exp().max(min_len2);
|
||||
|
||||
(d1 * len1, d2 * len2)
|
||||
}
|
||||
|
|
|
@ -15,7 +15,8 @@ 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 crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate};
|
||||
use bezier_rs::{Bezier, TValue};
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::vector::{ManipulatorPointId, PointId, VectorModificationType};
|
||||
use graphene_std::vector::{HandleId, NoHashBuilder, SegmentId, VectorData};
|
||||
|
@ -308,6 +309,12 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||
Escape,
|
||||
RightClick,
|
||||
),
|
||||
PathToolFsmState::SlidingPoint => actions!(PathToolMessageDiscriminant;
|
||||
PointerMove,
|
||||
DragStop,
|
||||
Escape,
|
||||
RightClick
|
||||
),
|
||||
PathToolFsmState::MoldingSegment => actions!(PathToolMessageDiscriminant;
|
||||
PointerMove,
|
||||
DragStop,
|
||||
|
@ -342,6 +349,20 @@ pub enum PointSelectState {
|
|||
Anchor,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SlidingSegmentData {
|
||||
segment_id: SegmentId,
|
||||
bezier: Bezier,
|
||||
start: PointId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SlidingPointInfo {
|
||||
anchor: PointId,
|
||||
layer: LayerNodeIdentifier,
|
||||
connected_segments: [SlidingSegmentData; 2],
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
enum PathToolFsmState {
|
||||
#[default]
|
||||
|
@ -350,6 +371,7 @@ enum PathToolFsmState {
|
|||
Drawing {
|
||||
selection_shape: SelectionShapeType,
|
||||
},
|
||||
SlidingPoint,
|
||||
MoldingSegment,
|
||||
}
|
||||
|
||||
|
@ -393,6 +415,7 @@ struct PathToolData {
|
|||
temporary_adjacent_handles_while_molding: Option<[Option<HandleId>; 2]>,
|
||||
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
|
||||
adjacent_anchor_offset: Option<DVec2>,
|
||||
sliding_point_info: Option<SlidingPointInfo>,
|
||||
}
|
||||
|
||||
impl PathToolData {
|
||||
|
@ -907,12 +930,156 @@ impl PathToolData {
|
|||
tangent_vector.try_normalize()
|
||||
}
|
||||
|
||||
fn start_sliding_point(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler) -> bool {
|
||||
let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_)));
|
||||
|
||||
if single_anchor_selected {
|
||||
let Some(anchor) = shape_editor.selected_points().next() else { return false };
|
||||
let Some(layer) = document.network_interface.selected_nodes().selected_layers(document.metadata()).next() else {
|
||||
return false;
|
||||
};
|
||||
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Check that the handles of anchor point are also colinear
|
||||
if !vector_data.colinear(*anchor) {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(point_id) = anchor.as_anchor() else { return false };
|
||||
|
||||
let mut connected_segments = [None, None];
|
||||
for (segment, bezier, start, end) in vector_data.segment_bezier_iter() {
|
||||
if start == point_id || end == point_id {
|
||||
match (connected_segments[0], connected_segments[1]) {
|
||||
(None, None) => connected_segments[0] = Some(SlidingSegmentData { segment_id: segment, bezier, start }),
|
||||
(Some(_), None) => connected_segments[1] = Some(SlidingSegmentData { segment_id: segment, bezier, start }),
|
||||
_ => {
|
||||
warn!("more than two segments connected to the anchor point");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let connected_segments = if let [Some(seg1), Some(seg2)] = connected_segments {
|
||||
[seg1, seg2]
|
||||
} else {
|
||||
warn!("expected exactly two connected segments");
|
||||
return false;
|
||||
};
|
||||
|
||||
self.sliding_point_info = Some(SlidingPointInfo {
|
||||
anchor: point_id,
|
||||
layer,
|
||||
connected_segments,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn slide_point(&mut self, target_position: DVec2, responses: &mut VecDeque<Message>, network_interface: &NodeNetworkInterface, shape_editor: &ShapeState) {
|
||||
let Some(sliding_point_info) = self.sliding_point_info else { return };
|
||||
let anchor = sliding_point_info.anchor;
|
||||
let layer = sliding_point_info.layer;
|
||||
|
||||
let Some(vector_data) = network_interface.compute_modified_vector(layer) else { return };
|
||||
let transform = network_interface.document_metadata().transform_to_viewport(layer);
|
||||
let layer_pos = transform.inverse().transform_point2(target_position);
|
||||
|
||||
let segments = sliding_point_info.connected_segments;
|
||||
|
||||
let t1 = segments[0].bezier.project(layer_pos);
|
||||
let position1 = segments[0].bezier.evaluate(TValue::Parametric(t1));
|
||||
|
||||
let t2 = segments[1].bezier.project(layer_pos);
|
||||
let position2 = segments[1].bezier.evaluate(TValue::Parametric(t2));
|
||||
|
||||
let (closer_segment, farther_segment, t_value, new_position) = if position2.distance(layer_pos) < position1.distance(layer_pos) {
|
||||
(segments[1], segments[0], t2, position2)
|
||||
} else {
|
||||
(segments[0], segments[1], t1, position1)
|
||||
};
|
||||
|
||||
// Move the anchor to the new position
|
||||
let Some(current_position) = ManipulatorPointId::Anchor(anchor).get_position(&vector_data) else {
|
||||
return;
|
||||
};
|
||||
let delta = new_position - current_position;
|
||||
|
||||
shape_editor.move_anchor(anchor, &vector_data, delta, layer, None, responses);
|
||||
|
||||
// Make a split at the t_value
|
||||
let [first, second] = closer_segment.bezier.split(TValue::Parametric(t_value));
|
||||
let closer_segment_other_point = if anchor == closer_segment.start { closer_segment.bezier.end } else { closer_segment.bezier.start };
|
||||
|
||||
let (split_segment, other_segment) = if first.start == closer_segment_other_point { (first, second) } else { (second, first) };
|
||||
|
||||
// Primary handle maps to primary handle and secondary maps to secondary
|
||||
let closer_primary_handle = HandleId::primary(closer_segment.segment_id);
|
||||
let Some(handle_position) = split_segment.handle_start() else { return };
|
||||
let relative_position1 = handle_position - split_segment.start;
|
||||
let modification_type = closer_primary_handle.set_relative_position(relative_position1);
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
|
||||
let closer_secondary_handle = HandleId::end(closer_segment.segment_id);
|
||||
let Some(handle_position) = split_segment.handle_end() else { return };
|
||||
let relative_position2 = handle_position - split_segment.end;
|
||||
let modification_type = closer_secondary_handle.set_relative_position(relative_position2);
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
|
||||
let end_handle_direction = if anchor == closer_segment.start { -relative_position1 } else { -relative_position2 };
|
||||
|
||||
let (farther_other_point, start_handle, end_handle, start_handle_pos) = if anchor == farther_segment.start {
|
||||
(
|
||||
farther_segment.bezier.end,
|
||||
HandleId::end(farther_segment.segment_id),
|
||||
HandleId::primary(farther_segment.segment_id),
|
||||
farther_segment.bezier.handle_end(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
farther_segment.bezier.start,
|
||||
HandleId::primary(farther_segment.segment_id),
|
||||
HandleId::end(farther_segment.segment_id),
|
||||
farther_segment.bezier.handle_start(),
|
||||
)
|
||||
};
|
||||
let Some(start_handle_position) = start_handle_pos else { return };
|
||||
let start_handle_direction = start_handle_position - farther_other_point;
|
||||
|
||||
// Get normalized direction vectors, if cubic handle is zero then we consider corresponding tangent
|
||||
let d1 = start_handle_direction.try_normalize().unwrap_or({
|
||||
if anchor == farther_segment.start {
|
||||
-farther_segment.bezier.tangent(TValue::Parametric(0.99))
|
||||
} else {
|
||||
farther_segment.bezier.tangent(TValue::Parametric(0.01))
|
||||
}
|
||||
});
|
||||
|
||||
let d2 = end_handle_direction.try_normalize().unwrap_or_default();
|
||||
|
||||
let min_len1 = start_handle_direction.length() * 0.4;
|
||||
let min_len2 = end_handle_direction.length() * 0.4;
|
||||
|
||||
let (relative_pos1, relative_pos2) = find_two_param_best_approximate(farther_other_point, new_position, d1, d2, min_len1, min_len2, farther_segment.bezier, other_segment);
|
||||
|
||||
// Now set those handles to these handle lengths keeping the directions d1, d2
|
||||
let modification_type = start_handle.set_relative_position(relative_pos1);
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
|
||||
let modification_type = end_handle.set_relative_position(relative_pos2);
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn drag(
|
||||
&mut self,
|
||||
equidistant: bool,
|
||||
lock_angle: bool,
|
||||
snap_angle: bool,
|
||||
snap_axis: bool,
|
||||
shape_editor: &mut ShapeState,
|
||||
document: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
|
@ -925,9 +1092,10 @@ impl PathToolData {
|
|||
.selected_points()
|
||||
.any(|point| matches!(point, ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_)));
|
||||
|
||||
if snap_angle && self.snapping_axis.is_none() && !single_handle_selected {
|
||||
// This is where it starts snapping along axis
|
||||
if snap_axis && self.snapping_axis.is_none() && !single_handle_selected {
|
||||
self.start_snap_along_axis(shape_editor, document, input, responses);
|
||||
} else if !snap_angle && self.snapping_axis.is_some() {
|
||||
} else if !snap_axis && self.snapping_axis.is_some() {
|
||||
self.stop_snap_along_axis(shape_editor, document, input, responses);
|
||||
}
|
||||
|
||||
|
@ -1037,7 +1205,8 @@ impl PathToolData {
|
|||
self.previous_mouse_position += document_to_viewport.inverse().transform_vector2(unsnapped_delta);
|
||||
}
|
||||
|
||||
if snap_angle && self.snapping_axis.is_some() {
|
||||
// Constantly checking and changing the snapping axis based on current mouse position
|
||||
if snap_axis && self.snapping_axis.is_some() {
|
||||
let Some(current_axis) = self.snapping_axis else { return };
|
||||
let total_delta = self.drag_start_pos - input.mouse.position;
|
||||
|
||||
|
@ -1234,6 +1403,7 @@ impl Fsm for PathToolFsmState {
|
|||
}
|
||||
}
|
||||
}
|
||||
Self::SlidingPoint => {}
|
||||
Self::MoldingSegment => {}
|
||||
}
|
||||
|
||||
|
@ -1380,10 +1550,15 @@ impl Fsm for PathToolFsmState {
|
|||
}
|
||||
|
||||
if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, tool_action_data.shape_editor, tool_action_data.document, responses) {
|
||||
if snap_angle_state && lock_angle_state && tool_data.start_sliding_point(tool_action_data.shape_editor, tool_action_data.document) {
|
||||
return PathToolFsmState::SlidingPoint;
|
||||
}
|
||||
|
||||
tool_data.drag(
|
||||
equidistant_state,
|
||||
lock_angle_state,
|
||||
snap_angle_state,
|
||||
snap_angle_state,
|
||||
tool_action_data.shape_editor,
|
||||
tool_action_data.document,
|
||||
input,
|
||||
|
@ -1418,6 +1593,10 @@ impl Fsm for PathToolFsmState {
|
|||
|
||||
PathToolFsmState::Dragging(tool_data.dragging_state)
|
||||
}
|
||||
(PathToolFsmState::SlidingPoint, PathToolMessage::PointerMove { .. }) => {
|
||||
tool_data.slide_point(input.mouse.position, responses, &document.network_interface, shape_editor);
|
||||
PathToolFsmState::SlidingPoint
|
||||
}
|
||||
(PathToolFsmState::MoldingSegment, PathToolMessage::PointerMove { break_colinear_molding, .. }) => {
|
||||
if tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD {
|
||||
tool_data.molding_segment = true;
|
||||
|
@ -1570,6 +1749,14 @@ impl Fsm for PathToolFsmState {
|
|||
tool_data.snap_manager.cleanup(responses);
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(PathToolFsmState::SlidingPoint, PathToolMessage::Escape | PathToolMessage::RightClick) => {
|
||||
tool_data.sliding_point_info = None;
|
||||
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(PathToolFsmState::MoldingSegment, PathToolMessage::Escape | PathToolMessage::RightClick) => {
|
||||
// Undo the molding and go back to the state before
|
||||
tool_data.molding_info = None;
|
||||
|
@ -1627,7 +1814,7 @@ impl Fsm for PathToolFsmState {
|
|||
(_, PathToolMessage::DragStop { extend_selection, .. }) => {
|
||||
let extend_selection = input.keyboard.get(extend_selection as usize);
|
||||
let drag_occurred = tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD;
|
||||
// TODO: Here we want only visible points to be considered
|
||||
|
||||
let nearest_point = shape_editor.find_nearest_visible_point_indices(
|
||||
&document.network_interface,
|
||||
input.mouse.position,
|
||||
|
@ -1713,9 +1900,8 @@ impl Fsm for PathToolFsmState {
|
|||
shape_editor.deselect_all_points();
|
||||
}
|
||||
|
||||
if tool_data.snapping_axis.is_some() {
|
||||
tool_data.snapping_axis = None;
|
||||
}
|
||||
tool_data.snapping_axis = None;
|
||||
tool_data.sliding_point_info = None;
|
||||
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
|
@ -2065,32 +2251,65 @@ fn calculate_adjacent_anchor_tangent(
|
|||
}
|
||||
}
|
||||
|
||||
fn update_dynamic_hints(state: PathToolFsmState, responses: &mut VecDeque<Message>, _shape_editor: &mut ShapeState, document: &DocumentMessageHandler, tool_data: &PathToolData) {
|
||||
fn update_dynamic_hints(state: PathToolFsmState, responses: &mut VecDeque<Message>, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, tool_data: &PathToolData) {
|
||||
// Condinting based on currently selected segment if it has any one g1 continuous handle
|
||||
|
||||
let hint_data = match state {
|
||||
PathToolFsmState::Ready => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]),
|
||||
HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]),
|
||||
// TODO: Only show if at least one anchor is selected, and dynamically show either "Smooth" or "Sharp" based on the current state
|
||||
HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDouble, "Convert Anchor Point"),
|
||||
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "To Sharp"),
|
||||
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::LmbDrag, "To Smooth"),
|
||||
]),
|
||||
// TODO: Only show the following hints if at least one point is selected
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]),
|
||||
HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]),
|
||||
HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]),
|
||||
HintGroup(vec![
|
||||
HintInfo::keys([Key::Delete], "Delete Selected"),
|
||||
// TODO: Only show the following hints if at least one anchor is selected
|
||||
HintInfo::keys([Key::Accel], "No Dissolve").prepend_plus(),
|
||||
HintInfo::keys([Key::Shift], "Cut Anchor").prepend_plus(),
|
||||
]),
|
||||
]),
|
||||
PathToolFsmState::Ready => {
|
||||
// Show point sliding hints only when there is an anchor with colinear handles selected
|
||||
let single_anchor_selected = shape_editor.selected_points().count() == 1 && shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_)));
|
||||
let at_least_one_anchor_selected = shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_)));
|
||||
let at_least_one_point_selected = shape_editor.selected_points().count() >= 1;
|
||||
|
||||
let single_colinear_anchor_selected = if single_anchor_selected {
|
||||
let anchor = shape_editor.selected_points().next().unwrap();
|
||||
let layer = document.network_interface.selected_nodes().selected_layers(document.metadata()).next().unwrap();
|
||||
let vector_data = document.network_interface.compute_modified_vector(layer).unwrap();
|
||||
vector_data.colinear(*anchor)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let mut drag_selected_hints = vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")];
|
||||
let mut delete_selected_hints = vec![HintInfo::keys([Key::Delete], "Delete Selected")];
|
||||
|
||||
if at_least_one_anchor_selected {
|
||||
delete_selected_hints.push(HintInfo::keys([Key::Accel], "No Dissolve").prepend_plus());
|
||||
delete_selected_hints.push(HintInfo::keys([Key::Shift], "Cut Anchor").prepend_plus());
|
||||
}
|
||||
|
||||
if single_colinear_anchor_selected {
|
||||
drag_selected_hints.push(HintInfo::multi_keys([[Key::Control], [Key::Shift]], "Slide").prepend_plus());
|
||||
}
|
||||
|
||||
let mut hint_data = vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]),
|
||||
HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]),
|
||||
];
|
||||
|
||||
if at_least_one_anchor_selected {
|
||||
// TODO: Dynamically show either "Smooth" or "Sharp" based on the current state
|
||||
hint_data.push(HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDouble, "Convert Anchor Point"),
|
||||
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "To Sharp"),
|
||||
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::LmbDrag, "To Smooth"),
|
||||
]));
|
||||
}
|
||||
|
||||
if at_least_one_point_selected {
|
||||
let mut groups = vec![
|
||||
HintGroup(drag_selected_hints),
|
||||
HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]),
|
||||
HintGroup(vec![HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus()]),
|
||||
HintGroup(delete_selected_hints),
|
||||
];
|
||||
hint_data.append(&mut groups);
|
||||
}
|
||||
|
||||
HintData(hint_data)
|
||||
}
|
||||
PathToolFsmState::Dragging(dragging_state) => {
|
||||
let colinear = dragging_state.colinear;
|
||||
let mut dragging_hint_data = HintData(Vec::new());
|
||||
|
@ -2188,6 +2407,7 @@ fn update_dynamic_hints(state: PathToolFsmState, responses: &mut VecDeque<Messag
|
|||
|
||||
HintData(molding_hints)
|
||||
}
|
||||
PathToolFsmState::SlidingPoint => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
|
||||
};
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue