Skip to content
Open
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
1 change: 1 addition & 0 deletions editor/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ pub const SEGMENT_INSERTION_DISTANCE: f64 = 5.;
pub const SEGMENT_OVERLAY_SIZE: f64 = 10.;
pub const SEGMENT_SELECTED_THICKNESS: f64 = 3.;
pub const HANDLE_LENGTH_FACTOR: f64 = 0.5;
pub const SNAP_DAMPENING_START_MULTIPLIER: f64 = 0.3;

// GRADIENT TOOL
pub const GRADIENT_MIDPOINT_DIAMOND_RADIUS: f64 = 4.;
Expand Down
272 changes: 270 additions & 2 deletions editor/src/messages/tool/tool_messages/path_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use super::tool_prelude::*;
use crate::consts::{
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_05, COLOR_OVERLAY_GRAY, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_GREEN_25, COLOR_OVERLAY_RED, COLOR_OVERLAY_RED_25, DEFAULT_STROKE_WIDTH,
DOUBLE_CLICK_MILLISECONDS, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, DRILL_THROUGH_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE,
SELECTION_THRESHOLD, SELECTION_TOLERANCE,
SELECTION_THRESHOLD, SELECTION_TOLERANCE, SNAP_DAMPENING_START_MULTIPLIER,
};
use crate::messages::clipboard::utility_types::ClipboardContent;
use crate::messages::input_mapper::utility_types::macros::action_shortcut_manual;
Expand All @@ -13,6 +13,7 @@ use crate::messages::portfolio::document::overlays::utility_functions::{path_ove
use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext};
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::misc::{PathSnapTarget, SnapTarget};
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::portfolio::document::utility_types::transformation::Axis;
use crate::messages::preferences::SelectionMode;
Expand All @@ -22,7 +23,7 @@ use crate::messages::tool::common_functionality::pivot::{PivotGizmo, PivotGizmoT
use crate::messages::tool::common_functionality::shape_editor::{
ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedLayerState, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState,
};
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnappedPoint};
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate, make_path_editable_is_allowed};
use graphene_std::Color;
use graphene_std::renderer::Quad;
Expand Down Expand Up @@ -1062,6 +1063,9 @@ impl PathToolData {
snap_angle: bool,
tangent_to_neighboring_tangents: bool,
) -> f64 {
if handle_vector.length_squared() < f64::EPSILON {
return self.angle;
}
let current_angle = -handle_vector.angle_to(DVec2::X);

if let Some((vector, layer)) = shape_editor
Expand Down Expand Up @@ -1150,11 +1154,47 @@ impl PathToolData {
false => self.snap_manager.free_snap(&snap_data, &snap_point, Default::default()),
};

let snap_result = self.reduce_snap_weight(snap_result, new_handle_position, anchor_position);

self.snap_manager.update_indicator(snap_result.clone());

document.metadata().document_to_viewport.transform_vector2(snap_result.snapped_point_document - handle_position)
}

fn reduce_snap_weight(&self, mut snap_result: SnappedPoint, new_handle_position: DVec2, anchor_position: DVec2) -> SnappedPoint {
//If the snapping result is a non-finite position
if snap_result.distance == f64::INFINITY {
return SnappedPoint {
snapped_point_document: new_handle_position,
..Default::default()
};
}

if !matches!(snap_result.target, SnapTarget::Path(PathSnapTarget::AlongPath)) {
return snap_result;
}

let selection_status = &self.selection_status;
if selection_status.angle() != Some(ManipulatorAngle::Colinear) {
return snap_result;
}

let anchor_distance = snap_result.snapped_point_document.distance(anchor_position);
let dampening_start = snap_result.tolerance * SNAP_DAMPENING_START_MULTIPLIER;
if anchor_distance >= dampening_start {
return snap_result;
}

// 1 -> full snap; 0 -> no snap
let t: f64 = (anchor_distance / dampening_start).clamp(0.0, 1.0);
// smoothstep function: 3 * t ^ 2 - 2 * t ^ 3
let weight = t * t * (3.0 - 2.0 * t);
//linear interpolation
snap_result.snapped_point_document = new_handle_position.lerp(snap_result.snapped_point_document, weight);

snap_result
}

fn start_snap_along_axis(&mut self, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
// Find the negative delta to take the point to the drag start position
let current_mouse = input.mouse.position;
Expand Down Expand Up @@ -3662,3 +3702,231 @@ fn update_dynamic_hints(
hint_data.send_layout(responses);
responses.add(ToolMessage::UpdateHints);
}

#[cfg(test)]
mod test_path {
use crate::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
use crate::messages::input_mapper::utility_types::input_mouse::MouseKeys;
use crate::messages::portfolio::document::utility_types::misc::{SNAP_FUNCTIONS_FOR_BOUNDING_BOXES, SNAP_FUNCTIONS_FOR_PATHS};
pub use crate::test_utils::test_prelude::*;
use glam::DAffine2;
use graphene_std::subpath::BezierHandles;
use graphene_std::vector::Vector;

async fn prepare_document_for_path_snap_weight(anchor_positions: &[DVec2]) -> EditorTestUtils {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.set_viewport_size(DVec2::splat(-1000.), DVec2::splat(1000.)).await; // Necessary for doing snapping since snaps outside of the viewport are discarded

editor.drag_tool(ToolType::Artboard, 0., 0., 1000., 600., ModifierKeys::empty()).await; // Necessary for doing path snapping without it that path snapping does not work
editor.select_tool(ToolType::Select).await;

// Disable all bounding box snapping
for (_, closure, _) in SNAP_FUNCTIONS_FOR_BOUNDING_BOXES {
editor
.handle_message(DocumentMessage::SetSnapping {
closure: Some(closure),
snapping_state: false,
})
.await;
}

// Disable all path snapping EXCEPT along_path
for (name, closure, _) in SNAP_FUNCTIONS_FOR_PATHS {
let enabled = name == "Along Paths";

editor
.handle_message(DocumentMessage::SetSnapping {
closure: Some(closure),
snapping_state: enabled,
})
.await;
}

//Create Bezier path for testing
editor.drag_tool(ToolType::Pen, anchor_positions[0].x, anchor_positions[0].y, 650., 30., ModifierKeys::empty()).await;
editor.drag_tool(ToolType::Pen, anchor_positions[1].x, anchor_positions[1].y, 370., 420., ModifierKeys::empty()).await;
editor.drag_tool(ToolType::Pen, anchor_positions[2].x, anchor_positions[2].y, 20., 330., ModifierKeys::empty()).await;
editor.press(Key::Enter, ModifierKeys::empty()).await;

assert_eq!(
editor.active_document().metadata().all_layers().count(),
2,
"The document should contain one artboard and one path layers"
);

let (modified_path, layer_to_viewport) = get_path_data(&editor);
assert_anchor_positions(&modified_path, layer_to_viewport, anchor_positions, 1e-10);

let expected_handles: Vec<BezierHandles> = vec![
BezierHandles::Cubic {
handle_start: DVec2::new(700.0, 60.0),
handle_end: DVec2::new(430.0, 180.0),
},
BezierHandles::Cubic {
handle_start: DVec2::new(370.0, 420.0),
handle_end: DVec2::new(80.0, 70.0),
},
];
assert_handle_positions(&modified_path, &expected_handles, layer_to_viewport, 1e-10);

editor
}

fn get_path_data(editor: &EditorTestUtils) -> (Vector, DAffine2) {
let document = editor.active_document();

let path_layer = document.metadata().all_layers().nth(1).expect("Expected path layer");

let modified_path = document.network_interface.compute_modified_vector(path_layer).expect("Vector not found in the path layer");

let layer_to_document = document.metadata().transform_to_document(path_layer);

(modified_path, layer_to_document)
}

fn assert_anchor_positions(vector: &Vector, transform: DAffine2, expected_anchors: &[DVec2], epsilon: f64) {
let anchors_in_viewport: Vec<DVec2> = vector
.point_domain
.ids()
.iter()
.filter_map(|&point_id| {
let pos = vector.point_domain.position_from_id(point_id)?;
Some(transform.transform_point2(pos))
})
.collect();

assert_eq!(anchors_in_viewport.len(), expected_anchors.len(), "Anchor count mismatch");

for (i, expected) in expected_anchors.iter().enumerate() {
let actual = anchors_in_viewport[i];
let distance = (actual - *expected).length();

assert!(distance < epsilon, "Anchor {i} mismatch: expected {expected:?}, got {actual:?}, distance {distance}");
}
}

fn assert_handle_positions(vector: &Vector, expected_handles: &[BezierHandles], transform: DAffine2, epsilon: f64) {
let segment_ids = vector.segment_domain.ids();

assert_eq!(segment_ids.len(), expected_handles.len(), "Segment count mismatch for handles");

for (i, segment_id) in segment_ids.iter().enumerate() {
let segment = vector.segment_from_id(*segment_id).expect("Segment not found");
let expected = &expected_handles[i];

match (&segment.handles, expected) {
(BezierHandles::Linear, BezierHandles::Linear) => {
// OK
}

(BezierHandles::Quadratic { handle: actual }, BezierHandles::Quadratic { handle: expected }) => {
let actual_viewport = transform.transform_point2(*actual);

let dist = (actual_viewport - expected).length();

assert!(
dist < epsilon,
"Segment {i} quadratic handle mismatch: expected {:?}, got {:?}, dist {}",
expected,
actual_viewport,
dist
);
}

(
BezierHandles::Cubic {
handle_start: actual_start,
handle_end: actual_end,
},
BezierHandles::Cubic {
handle_start: expected_start,
handle_end: expected_end,
},
) => {
let actual_start_viewport = transform.transform_point2(*actual_start);
let actual_end_viewport = transform.transform_point2(*actual_end);

let dist_start = (actual_start_viewport - expected_start).length();
let dist_end = (actual_end_viewport - expected_end).length();

assert!(
dist_start < epsilon,
"Segment {i} cubic start handle mismatch: expected {:?}, got {:?}, dist {}",
expected_start,
actual_start_viewport,
dist_start
);

assert!(
dist_end < epsilon,
"Segment {i} cubic end handle mismatch: expected {:?}, got {:?}, dist {}",
expected_end,
actual_end_viewport,
dist_end
);
}
// Mismatch case
(actual, expected) => {
panic!("Segment {i} handle type mismatch: actual = {:?}, expected = {:?}", actual, expected);
}
}
}
}

#[tokio::test]
async fn path_move_handle_close_to_anchor_with_along_path_snapping() {
let anchor_positions = [DVec2::new(50., 30.), DVec2::new(400., 300.), DVec2::new(50., 200.)];
let delta_x = -2.;
let delta_y = 1.;
let mut editor = prepare_document_for_path_snap_weight(&anchor_positions).await;

editor.click_tool(ToolType::Path, MouseKeys::LEFT, anchor_positions[1], ModifierKeys::empty()).await;
editor
.drag_tool(ToolType::Path, 370., 420., anchor_positions[1].x + delta_x, anchor_positions[1].y + delta_y, ModifierKeys::empty())
.await;
editor.press(Key::Enter, ModifierKeys::empty()).await;

let (modified_path, layer_to_document) = get_path_data(&editor);

assert_anchor_positions(&modified_path, layer_to_document, &anchor_positions, 1e-10);

let expected_handles: Vec<BezierHandles> = vec![
BezierHandles::Cubic {
handle_start: DVec2::new(700.0, 60.0),
handle_end: DVec2::new(470.9153, 198.6539),
},
BezierHandles::Cubic {
handle_start: DVec2::new(399.0945, 301.2940),
handle_end: DVec2::new(80.0, 70.0),
},
];
assert_handle_positions(&modified_path, &expected_handles, layer_to_document, 1e-4);
}

#[tokio::test]
async fn path_move_handle_to_anchor() {
let anchor_positions = [DVec2::new(50., 30.), DVec2::new(400., 300.), DVec2::new(50., 200.)];
let mut editor = prepare_document_for_path_snap_weight(&anchor_positions).await;

editor.click_tool(ToolType::Path, MouseKeys::LEFT, anchor_positions[1], ModifierKeys::empty()).await;
editor.drag_tool(ToolType::Path, 370., 420., anchor_positions[1].x, anchor_positions[1].y, ModifierKeys::empty()).await;
editor.press(Key::Enter, ModifierKeys::empty()).await;

let (modified_path, layer_to_document) = get_path_data(&editor);

assert_anchor_positions(&modified_path, layer_to_document, &anchor_positions, 1e-10);

let expected_handles: Vec<BezierHandles> = vec![
BezierHandles::Cubic {
handle_start: DVec2::new(700.0, 60.0),
handle_end: DVec2::new(430.0, 180.0),
},
BezierHandles::Cubic {
handle_start: DVec2::new(400.0, 300.0),
handle_end: DVec2::new(80.0, 70.0),
},
];
assert_handle_positions(&modified_path, &expected_handles, layer_to_document, 1e-10);
}
}