Skip to content

Add molding segments to the Path tool #2660

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
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 @@ -103,6 +103,7 @@ pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.;
pub const SEGMENT_INSERTION_DISTANCE: f64 = 7.5;
pub const SEGMENT_OVERLAY_SIZE: f64 = 10.;
pub const HANDLE_LENGTH_FACTOR: f64 = 0.5;
pub const MOLDING_FALLOFF: f64 = 70.;

// PEN TOOL
pub const CREATE_CURVE_THRESHOLD: f64 = 5.;
Expand Down
2 changes: 1 addition & 1 deletion editor/src/messages/input_mapper/input_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),
entry!(KeyDown(KeyR); action_dispatch=PathToolMessage::GRS { key: KeyR }),
entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }),
entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt }),
entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt, temporary_toggle_colinear_molding: Alt, permanent_toggle_colinear_molding: KeyC }),
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::DeselectAllPoints),
Expand Down
75 changes: 74 additions & 1 deletion editor/src/messages/tool/common_functionality/shape_editor.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::graph_modification_utils::{self, merge_layers};
use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
use super::utility_functions::calculate_segment_angle;
use super::utility_functions::{adjust_handle_colinearity, calculate_segment_angle, disable_g1_continuity, molded_control_points, restore_g1_continuity, restore_previous_handle_position};
use crate::consts::HANDLE_LENGTH_FACTOR;
use crate::messages::portfolio::document::overlays::utility_functions::selected_segments;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
Expand Down Expand Up @@ -276,6 +276,79 @@ impl ClosestSegment {
.unwrap_or(DVec2::ZERO);
tangent.perp()
}

/// Molding the bezier curve
/// Returns adjacent handles' HandleId if colinearity is broken temporarily
pub fn mold_handle_positions(
&self,
document: &DocumentMessageHandler,
responses: &mut VecDeque<Message>,
c1: DVec2,
c2: DVec2,
new_b: DVec2,
falloff: f64,
permanent_toggle_colinear: bool,
temporary_toggle_colinear: bool,
temporary_adjacent_handles: Option<[Option<HandleId>; 2]>,
) -> Option<[Option<HandleId>; 2]> {
let t = self.t;

let transform = document.metadata().transform_to_viewport(self.layer);
let new_b = transform.inverse().transform_point2(new_b);

let start = self.bezier.start;
let end = self.bezier.end;
let (nc1, nc2) = molded_control_points(start, end, t, falloff, new_b, c1, c2);

let handle1 = HandleId::primary(self.segment);
let handle2 = HandleId::end(self.segment);
let layer = self.layer;

let modification_type = handle1.set_relative_position(nc1 - start);
responses.add(GraphOperationMessage::Vector { layer, modification_type });

let modification_type = handle2.set_relative_position(nc2 - end);
responses.add(GraphOperationMessage::Vector { layer, modification_type });

// If adjacent segments have colinear handles, their direction is changed but their handle lengths is preserved
// TODO: Find something which is more appropriate
let Some(vector_data) = document.network_interface.compute_modified_vector(self.layer()) else {
return None;
};

if permanent_toggle_colinear {
// Disable G1 continuity
disable_g1_continuity(handle1, &vector_data, layer, responses);
disable_g1_continuity(handle2, &vector_data, layer, responses);
} else if temporary_toggle_colinear {
// Disable G1 continuity
let mut other_handles = [None, None];
other_handles[0] = restore_previous_handle_position(handle1, c1, start, &vector_data, layer, responses);
other_handles[1] = restore_previous_handle_position(handle2, c2, end, &vector_data, layer, responses);

// Store other HandleId in tool data to regain colinearity later
if temporary_adjacent_handles.is_some() {
return temporary_adjacent_handles;
} else {
return Some(other_handles);
}
} else {
// Move the colinear handles so that colinearity is maintained
adjust_handle_colinearity(handle1, start, nc1, &vector_data, layer, responses);
adjust_handle_colinearity(handle2, end, nc2, &vector_data, layer, responses);

if let Some(adj_handles) = temporary_adjacent_handles {
if let Some(other_handle1) = adj_handles[0] {
restore_g1_continuity(handle1, other_handle1, nc1, start, &vector_data, layer, responses);
}

if let Some(other_handle2) = adj_handles[1] {
restore_g1_continuity(handle2, other_handle2, nc2, end, &vector_data, layer, responses);
}
}
}
None
}
}

// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
Expand Down
190 changes: 189 additions & 1 deletion editor/src/messages/tool/common_functionality/utility_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::messages::tool::tool_messages::path_tool::PathOverlayMode;
use glam::DVec2;
use graphene_core::renderer::Quad;
use graphene_core::text::{FontCache, load_face};
use graphene_std::vector::{ManipulatorPointId, PointId, SegmentId, VectorData};
use graphene_std::vector::{HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType};

/// 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(
Expand Down Expand Up @@ -95,6 +95,194 @@ pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector_data:
required_handle.map(|handle| -(handle - anchor_position).angle_to(DVec2::X))
}

pub fn molded_control_points(start: DVec2, end: DVec2, t: f64, falloff: f64, new_b: DVec2, c1: DVec2, c2: DVec2) -> (DVec2, DVec2) {
let v1 = (1. - t) * start + t * c1;
let a = (1. - t) * c1 + t * c2;
let v2 = (1. - t) * c2 + t * end;
let e1 = (1. - t) * v1 + t * a;
let e2 = (1. - t) * a + t * v2;
let b = (1. - t) * e1 + t * e2;

let d1 = e1 - b;
let d2 = e2 - b;
let ne1 = new_b + d1;
let ne2 = new_b + d2;

// Calculating new points A and C (C stays the same)
let point_c_ratio = (1. - t).powi(3) / (t.powi(3) + (1. - t).powi(3));
let ab_bc_ratio = ((t.powi(3) + (1. - t).powi(3) - 1.) / (t.powi(3) + (1. - t).powi(3))).abs();
let c = point_c_ratio * start + (1. - point_c_ratio) * end;
let new_a = new_b + (new_b - c) / ab_bc_ratio;

// Derive the new control points c1, c2
let (nc1, nc2) = derive_control_points(t, new_a, ne1, ne2, start, end);

// Calculating idealized curve
if let Some((ideal_c1, ideal_c2)) = get_idealised_cubic_curve(start, new_b, end) {
let d = (b - new_b).length();
let interpolation_ratio = d.min(falloff) / falloff;
let ic1 = (1. - interpolation_ratio) * nc1 + interpolation_ratio * ideal_c1;
let ic2 = (1. - interpolation_ratio) * nc2 + interpolation_ratio * ideal_c2;
(ic1, ic2)
} else {
(nc1, nc2)
}
}

pub fn get_idealised_cubic_curve(p1: DVec2, p2: DVec2, p3: DVec2) -> Option<(DVec2, DVec2)> {
use std::f64::consts::{PI, TAU};

let center = calculate_center(p1, p2, p3)?;

let d1 = (p1 - p2).length();
let d2 = (p2 - p3).length();
let t = d1 / (d1 + d2);

let start = p1;
let end = p3;

let [a, b, _c] = compute_abc_for_cubic_through_points(p1, p2, p3, t);

let angle = ((end.y - start.y).atan2(end.x - start.x) - (b.y - start.y).atan2(b.x - start.x) + TAU) % TAU;
let factor = if !(0.0..=PI).contains(&angle) { -1. } else { 1. };
let bc = factor * (start - end).length() / 3.;
let de1 = t * bc;
let de2 = (1. - t) * bc;
let tangent = [
DVec2::new(b.x - 10. * (b.y - center.y), b.y + 10. * (b.x - center.x)),
DVec2::new(b.x + 10. * (b.y - center.y), b.y - 10. * (b.x - center.x)),
];

let normalized_tangent = (tangent[1] - tangent[0]).try_normalize()?;

let e1 = DVec2::new(b.x + de1 * normalized_tangent.x, b.y + de1 * normalized_tangent.y);
let e2 = DVec2::new(b.x - de2 * normalized_tangent.x, b.y - de2 * normalized_tangent.y);

// Deriving control points
Some(derive_control_points(t, a, e1, e2, start, end))
}

fn derive_control_points(t: f64, a: DVec2, e1: DVec2, e2: DVec2, start: DVec2, end: DVec2) -> (DVec2, DVec2) {
let v1 = (e1 - t * a) / (1. - t);
let v2 = (e2 - (1. - t) * a) / t;
let c1 = (v1 - (1. - t) * start) / t;
let c2 = (v2 - t * end) / (1. - t);
(c1, c2)
}

fn calculate_center(p1: DVec2, p2: DVec2, p3: DVec2) -> Option<DVec2> {
// Calculate midpoints of two sides
let mid1 = (p1 + p2) / 2.;
let mid2 = (p2 + p3) / 2.;

// Calculate perpendicular bisectors
let dir1 = p2 - p1;
let dir2 = p3 - p2;
let perp_dir1 = DVec2::new(-dir1.y, dir1.x);
let perp_dir2 = DVec2::new(-dir2.y, dir2.x);

// Create points along the perpendicular directions
let mid1_plus = mid1 + perp_dir1;
let mid2_plus = mid2 + perp_dir2;

// Find intersection of the two perpendicular bisectors
line_line_intersection(mid1, mid1_plus, mid2, mid2_plus)
}

fn line_line_intersection(a1: DVec2, a2: DVec2, b1: DVec2, b2: DVec2) -> Option<DVec2> {
let (x1, y1) = (a1.x, a1.y);
let (x2, y2) = (a2.x, a2.y);
let (x3, y3) = (b1.x, b1.y);
let (x4, y4) = (b2.x, b2.y);

// Calculate numerator components
let nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4);
let ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4);

// Calculate denominator
let d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);

// Check for parallel lines (colinear points)
if d.abs() < f64::EPSILON { None } else { Some(DVec2::new(nx / d, ny / d)) }
}

pub fn compute_abc_for_cubic_through_points(start_point: DVec2, point_on_curve: DVec2, end_point: DVec2, t: f64) -> [DVec2; 3] {
let point_c_ratio = (1. - t).powi(3) / (t.powi(3) + (1. - t).powi(3));
let c = point_c_ratio * start_point + (1. - point_c_ratio) * end_point;

let ab_bc_ratio = ((t.powi(3) + (1. - t).powi(3) - 1.) / (t.powi(3) + (1. - t).powi(3))).abs();
let a = point_on_curve + (point_on_curve - c) / ab_bc_ratio;
[a, point_on_curve, c]
}

pub fn adjust_handle_colinearity(handle: HandleId, anchor_position: DVec2, target_control_point: DVec2, vector_data: &VectorData, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
if let Some(other_handle) = vector_data.other_colinear_handle(handle) {
if let Some(handle_position) = other_handle.to_manipulator_point().get_position(vector_data) {
if let Some(direction) = (anchor_position - target_control_point).try_normalize() {
let new_relative_position = (handle_position - anchor_position).length() * direction;
let modification_type = other_handle.set_relative_position(new_relative_position);
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
}
}
}

pub fn disable_g1_continuity(handle: HandleId, vector_data: &VectorData, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
if let Some(other_handle) = vector_data.other_colinear_handle(handle) {
let handles = [handle, other_handle];
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
}

pub fn restore_previous_handle_position(
handle: HandleId,
original_c: DVec2,
anchor_position: DVec2,
vector_data: &VectorData,
layer: LayerNodeIdentifier,
responses: &mut VecDeque<Message>,
) -> Option<HandleId> {
if let Some(other_handle) = vector_data.other_colinear_handle(handle) {
if let Some(handle_position) = other_handle.to_manipulator_point().get_position(vector_data) {
if let Some(direction) = (anchor_position - original_c).try_normalize() {
let old_relative_position = (handle_position - anchor_position).length() * direction;
let modification_type = other_handle.set_relative_position(old_relative_position);
responses.add(GraphOperationMessage::Vector { layer, modification_type });

let handles = [handle, other_handle];
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
responses.add(GraphOperationMessage::Vector { layer, modification_type });

return Some(other_handle);
}
}
}
None
}

pub fn restore_g1_continuity(
handle: HandleId,
other_handle: HandleId,
control_point: DVec2,
anchor_position: DVec2,
vector_data: &VectorData,
layer: LayerNodeIdentifier,
responses: &mut VecDeque<Message>,
) {
if let Some(handle_position) = other_handle.to_manipulator_point().get_position(vector_data) {
if let Some(direction) = (anchor_position - control_point).try_normalize() {
let new_relative_position = (handle_position - anchor_position).length() * direction;
let modification_type = other_handle.set_relative_position(new_relative_position);
responses.add(GraphOperationMessage::Vector { layer, modification_type });

let handles = [handle, other_handle];
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: true };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
}
}

/// Check whether a point is visible in the current overlay mode.
pub fn is_visible_point(
manipulator_point_id: ManipulatorPointId,
Expand Down
Loading
Loading