From 66a52b092402c27b71b324ba02dc316a8e27b1c7 Mon Sep 17 00:00:00 2001 From: Adesh Gupta Date: Fri, 27 Jun 2025 19:42:15 +0530 Subject: [PATCH] Add feat refit segments on shift + delete --- .../tool/common_functionality/shape_editor.rs | 97 +++++++++------ .../common_functionality/utility_functions.rs | 110 +++++++++++++----- .../messages/tool/tool_messages/path_tool.rs | 5 +- 3 files changed, 146 insertions(+), 66 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index de0a046d27..5f8cc32428 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -8,11 +8,10 @@ use crate::messages::portfolio::document::utility_types::misc::{PathSnapSource, use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration; -use crate::messages::tool::common_functionality::utility_functions::is_visible_point; +use crate::messages::tool::common_functionality::utility_functions::{find_refit_handle_lengths, is_visible_point}; use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState}; use bezier_rs::{Bezier, BezierHandles, Subpath, TValue}; use glam::{DAffine2, DVec2}; -use graph_craft::document; use graphene_std::vector::{HandleId, SegmentId}; use graphene_std::vector::{ManipulatorPointId, PointId, VectorData, VectorModificationType}; @@ -1174,11 +1173,7 @@ impl ShapeState { } } - pub fn get_bezier_from_segment_id(segment: SegmentId, vector_data: &VectorData) -> Option { - vector_data.segment_bezier_iter().find(|(id, _, _, _)| *id == segment).map(|(_, bezier, _, _)| bezier) - } - - fn dissolve_anchor(anchor: PointId, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector_data: &VectorData) -> Option<[(HandleId, PointId); 2]> { + fn dissolve_anchor(anchor: PointId, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector_data: &VectorData) -> Option<[(HandleId, PointId, Bezier); 2]> { // Delete point let modification_type = VectorModificationType::RemovePoint { id: anchor }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); @@ -1201,19 +1196,15 @@ impl ShapeState { let [Some(start), Some(end)] = opposites.map(|opposite| opposite.to_manipulator_point().get_anchor(vector_data)) else { return None; }; - let Some(bezier1) = vector_data.segment_bezier_iter().find(|(id, _, _, _)| *id == segment).map(|(_, bezier, _, _)| bezier) else { - return None; - }; - let Some(bezier2) = vector_data.segment_bezier_iter().find(|(id, _, _, _)| *id == segment).map(|(_, bezier, _, _)| bezier) else { - return None; - }; - // Here we should also return the beziers of the connected handles - Some([(handles[0], start), (handles[1], end)]) + let get_bezier = |segment_id: SegmentId| -> Option { vector_data.segment_bezier_iter().find(|(id, _, _, _)| *id == segment_id).map(|(_, bezier, _, _)| bezier) }; + let beziers = opposites.map(|opposite| get_bezier(opposite.segment)); + + Some([(handles[0], start, beziers[0]?), (handles[1], end, beziers[1]?)]) } /// Dissolve the selected points. - pub fn delete_selected_points(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque) { + pub fn delete_selected_points(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque, refit: bool) { for (&layer, state) in &mut self.selected_shape_state { let mut missing_anchors = HashMap::new(); let mut deleted_anchors = HashSet::new(); @@ -1248,21 +1239,46 @@ impl ShapeState { } let mut visited = Vec::new(); - while let Some((anchor, handles)) = missing_anchors.keys().next().copied().and_then(|id| missing_anchors.remove_entry(&id)) { + while let Some((anchor, connected_info)) = missing_anchors.keys().next().copied().and_then(|id| missing_anchors.remove_entry(&id)) { visited.push(anchor); // If the adjacent point is just this point then skip - let mut handles = handles.map(|handle| (handle.1 != anchor).then_some(handle)); + let mut handles = connected_info.map(|handle| (handle.1 != anchor).then_some(handle)); // If the adjacent points are themselves being deleted, then repeatedly visit the newest agacent points. - for handle in &mut handles { - while let Some((point, connected)) = (*handle).and_then(|(_, point)| missing_anchors.remove_entry(&point)) { - visited.push(point); + let [handle1, handle2] = &mut handles; - *handle = connected.into_iter().find(|(_, point)| !visited.contains(point)); + // Store Beziers for fitting later + let mut beziers_start = Vec::new(); + let mut beziers_end = Vec::new(); + if let Some((_, _, bezier)) = *handle1 { + beziers_start.push(bezier); + } + while let Some((point, connected)) = (*handle1).and_then(|(_, point, _)| missing_anchors.remove_entry(&point)) { + visited.push(point); + + if let Some(new_handle) = connected.into_iter().find(|(_, point, _)| !visited.contains(point)) { + *handle1 = Some(new_handle); + beziers_start.push(new_handle.2); } } + if let Some((_, _, bezier)) = *handle2 { + beziers_end.push(bezier); + } + while let Some((point, connected)) = (*handle2).and_then(|(_, point, _)| missing_anchors.remove_entry(&point)) { + visited.push(point); + + if let Some(new_handle) = connected.into_iter().find(|(_, point, _)| !visited.contains(point)) { + *handle2 = Some(new_handle); + beziers_end.push(new_handle.2); + } + } + + beziers_start.reverse(); + let mut combined = beziers_start.clone(); + combined.extend(beziers_end); + let [Some(start), Some(end)] = handles else { continue }; // Avoid reconnecting to points that are being deleted (this can happen if a whole loop is deleted) @@ -1271,7 +1287,7 @@ impl ShapeState { } // Grab the handles from the opposite side of the segment(s) being deleted and make it relative to the anchor - let [handle_start, handle_end] = [start, end].map(|(handle, _)| { + let [handle_start, handle_end] = [start, end].map(|(handle, _, _)| { let handle = handle.opposite(); let handle_position = handle.to_manipulator_point().get_position(&vector_data); let relative_position = handle @@ -1281,12 +1297,36 @@ impl ShapeState { handle_position.and_then(|handle| relative_position.map(|relative| handle - relative)).unwrap_or_default() }); + let [handle1, handle2] = if refit { + let handle_start_unit = handle_start.try_normalize().unwrap_or_default(); + let handle_end_unit = handle_end.try_normalize().unwrap_or_default(); + + let p1 = start + .0 + .opposite() + .to_manipulator_point() + .get_anchor(&vector_data) + .and_then(|anchor| vector_data.point_domain.position_from_id(anchor)) + .unwrap_or_default(); + + let p3 = end + .0 + .opposite() + .to_manipulator_point() + .get_anchor(&vector_data) + .and_then(|anchor| vector_data.point_domain.position_from_id(anchor)) + .unwrap_or_default(); + find_refit_handle_lengths(p1, p3, combined, handle_start_unit, handle_end_unit) + } else { + [handle_start, handle_end] + }; + let segment = start.0.segment; let modification_type = VectorModificationType::InsertSegment { id: segment, points: [start.1, end.1], - handles: [Some(handle_start), Some(handle_end)], + handles: [Some(handle1), Some(handle2)], }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); @@ -1317,15 +1357,6 @@ impl ShapeState { } } - pub fn delete_point_and_refit(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque) { - // Here define the logic of what happens exactly when sone selected points are deleted - for (&layer, state) in &mut self.selected_shape_state { - let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { - continue; - }; - } - } - pub fn break_path_at_selected_point(&self, document: &DocumentMessageHandler, responses: &mut VecDeque) { for (&layer, state) in &self.selected_shape_state { let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index c6974d5009..3a80f628e4 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -439,16 +439,48 @@ pub fn log_optimization(a: f64, b: f64, p1: DVec2, p3: DVec2, d1: DVec2, d2: DVe 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::>(); - + let points = new_curve.compute_lookup_table(Some(n), None).collect::>(); let dist = points1.iter().zip(points.iter()).map(|(p1, p2)| (p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sum::(); - dist / (2 * n) as f64 + dist / (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 n = 40; + + 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 = ((n) as f64 * ratio).floor() as usize; + let n_points2 = n - n_points1; + let mut points1 = farther_segment.compute_lookup_table(Some(2), None).collect::>(); + let points2 = other_segment.compute_lookup_table(Some(n_points2), None).collect::>(); + if points2.len() >= 2 { + points1.extend_from_slice(&points2[1..]); + } + + let f = |a: f64, b: f64| -> f64 { log_optimization(a, b, p1, p3, d1, d2, &points1, n) }; + + let (a, b) = adam_optimizer(f); + + let len1 = a.exp().max(min_len1); + let len2 = b.exp().max(min_len2); + + (d1 * len1, d2 * len2) +} + +pub fn adam_optimizer(f: impl Fn(f64, f64) -> f64) -> (f64, f64) { let h = 1e-6; let tol = 1e-6; let max_iter = 200; @@ -468,30 +500,9 @@ pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec 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::>(); - let mut points2 = other_segment.compute_lookup_table(Some(n), None).collect::>(); - 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); + let dfa: f64 = (f(a + h, b) - f(a - h, b)) / (2. * h); + let dfb: f64 = (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; @@ -515,9 +526,46 @@ pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec break; } } - - let len1 = a.exp().max(min_len1); - let len2 = b.exp().max(min_len2); - - (d1 * len1, d2 * len2) + (a, b) +} + +pub fn find_refit_handle_lengths(p1: DVec2, p3: DVec2, beziers: Vec, d1: DVec2, d2: DVec2) -> [DVec2; 2] { + let n = 40; + + let points_per_bez = n / beziers.len(); + + let points = if points_per_bez < 1 { + beziers.iter().map(|bezier| bezier.start()).collect::>() + } else { + let mut points = Vec::new(); + for bezier in &beziers { + let lookup = bezier.compute_lookup_table(Some(points_per_bez), None).collect::>(); + points.extend_from_slice(&lookup[..lookup.len() - 1]); + } + points + }; + + let limit = points.len(); + + let f = |a: f64, b: f64| -> f64 { + let start_handle_len = a.exp(); + let end_handle_len = b.exp(); + + let c1 = p1 + d1 * start_handle_len; + let c2 = p3 + d2 * end_handle_len; + + let new_curve = Bezier::from_cubic_coordinates(p1.x, p1.y, c1.x, c1.y, c2.x, c2.y, p3.x, p3.y); + + let new_points = new_curve.compute_lookup_table(Some(limit), None); + let dist = points.iter().zip(new_points).map(|(p1, p2)| (p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sum::(); + + dist / (limit) as f64 + }; + + let (a, b) = adam_optimizer(f); + + let len1 = a.exp(); + let len2 = b.exp(); + + [d1 * len1, d2 * len2] } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 2f6b7edadd..4fc077962d 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -1919,7 +1919,8 @@ impl Fsm for PathToolFsmState { (_, PathToolMessage::Delete) => { // Delete the selected points and clean up overlays responses.add(DocumentMessage::AddTransaction); - shape_editor.delete_selected_points(document, responses); + // shape_editor.delete_selected_points(document, responses); + shape_editor.delete_selected_points(document, responses, false); responses.add(PathToolMessage::SelectionChanged); PathToolFsmState::Ready @@ -1940,7 +1941,7 @@ impl Fsm for PathToolFsmState { } (_, PathToolMessage::DeleteAndRefit) => { responses.add(DocumentMessage::AddTransaction); - shape_editor.delete_point_and_refit(document, responses); + shape_editor.delete_selected_points(document, responses, true); responses.add(PathToolMessage::SelectionChanged); PathToolFsmState::Ready