This commit is contained in:
Adesh Gupta 2025-07-07 10:21:48 +02:00 committed by GitHub
commit 39d729a0de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 167 additions and 49 deletions

View file

@ -208,8 +208,10 @@ pub fn input_mappings() -> Mapping {
// PathToolMessage
entry!(KeyDown(Delete); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDown(Delete); modifiers=[Alt], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDown(Backspace); modifiers=[Alt], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::DeleteAndRefit),
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::DeleteAndRefit),
entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control, molding_in_segment_edit: KeyA }),
entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),

View file

@ -9,7 +9,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node
use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration;
use crate::messages::tool::common_functionality::utility_functions::{is_intersecting, is_visible_point};
use crate::messages::tool::common_functionality::utility_functions::{find_refit_handle_lengths, is_intersecting, is_visible_point};
use crate::messages::tool::tool_messages::path_tool::{PathOverlayMode, PointSelectState};
use bezier_rs::{Bezier, BezierHandles, Subpath, TValue};
use glam::{DAffine2, DVec2};
@ -1268,7 +1268,7 @@ impl ShapeState {
}
}
fn dissolve_anchor(anchor: PointId, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, vector_data: &VectorData) -> Option<[(HandleId, PointId); 2]> {
fn dissolve_anchor(anchor: PointId, responses: &mut VecDeque<Message>, 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 });
@ -1291,11 +1291,15 @@ impl ShapeState {
let [Some(start), Some(end)] = opposites.map(|opposite| opposite.to_manipulator_point().get_anchor(vector_data)) else {
return None;
};
Some([(handles[0], start), (handles[1], end)])
let get_bezier = |segment_id: SegmentId| -> Option<Bezier> { 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<Message>) {
pub fn delete_selected_points(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, refit: bool) {
for (&layer, state) in &mut self.selected_shape_state {
let mut missing_anchors = HashMap::new();
let mut deleted_anchors = HashSet::new();
@ -1334,21 +1338,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)
@ -1359,7 +1388,7 @@ impl ShapeState {
// Avoid reconnecting to points which have adjacent segments selected
// 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
@ -1369,12 +1398,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 });

View file

@ -15,6 +15,8 @@ use graphene_std::text::{FontCache, load_font};
use graphene_std::vector::{HandleExt, HandleId, ManipulatorPointId, PointId, SegmentId, VectorData, VectorModificationType};
use kurbo::{CubicBez, Line, ParamCurveExtrema, PathSeg, Point, QuadBez};
const OPTIMIZATION_SAMPLES: usize = 40;
/// 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(
document: &DocumentMessageHandler,
@ -495,7 +497,7 @@ pub fn transforming_transform_cage(
/// 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 {
pub fn log_optimization(a: f64, b: f64, p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, points1: &[DVec2], optimization_samples: usize) -> f64 {
let start_handle_length = a.exp();
let end_handle_length = b.exp();
@ -506,16 +508,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::<Vec<_>>();
let points = new_curve.compute_lookup_table(Some(optimization_samples), 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
dist / optimization_samples 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) {
pub fn find_two_param_best_approximate(p1: DVec2, p3: DVec2, d1: DVec2, d2: DVec2, min_len1: f64, min_len2: f64, further_segment: Bezier, other_segment: Bezier) -> (DVec2, DVec2) {
let further_segment = if further_segment.start.distance(p1) >= f64::EPSILON {
further_segment.reverse()
} else {
further_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 = further_segment.length(None);
let l2 = other_segment.length(None);
let ratio = l1 / (l1 + l2);
let n_points1 = (OPTIMIZATION_SAMPLES as f64 * ratio).floor() as usize;
let n_points2 = OPTIMIZATION_SAMPLES - n_points1;
let mut points1 = further_segment.compute_lookup_table(Some(2), None).collect::<Vec<_>>();
let points2 = other_segment.compute_lookup_table(Some(n_points2), None).collect::<Vec<_>>();
if points2.len() >= 2 {
points1.extend_from_slice(&points2[1..]);
}
let (a, b) = adam_optimizer(|a: f64, b: f64| -> f64 { log_optimization(a, b, p1, p3, d1, d2, &points1, OPTIMIZATION_SAMPLES) });
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;
@ -535,27 +569,6 @@ 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::<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);
@ -582,9 +595,42 @@ 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<Bezier>, d1: DVec2, d2: DVec2) -> [DVec2; 2] {
let points_per_bezier = OPTIMIZATION_SAMPLES / beziers.len();
let points = if points_per_bezier < 1 {
beziers.iter().map(|bezier| bezier.start()).collect::<Vec<_>>()
} else {
let mut points = Vec::new();
for bezier in &beziers {
let lookup = bezier.compute_lookup_table(Some(points_per_bezier), None).collect::<Vec<_>>();
points.extend_from_slice(&lookup[..lookup.len() - 1]);
}
points
};
let limit = points.len();
let (a, b) = adam_optimizer(|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::<f64>();
dist / limit as f64
});
let len1 = a.exp();
let len2 = b.exp();
[d1 * len1, d2 * len2]
}

View file

@ -20,7 +20,6 @@ use bezier_rs::{Bezier, BezierHandles, TValue};
use graphene_std::renderer::Quad;
use graphene_std::vector::{HandleExt, HandleId, NoHashBuilder, SegmentId, VectorData};
use graphene_std::vector::{ManipulatorPointId, PointId, VectorModificationType};
use std::vec;
#[derive(Default)]
pub struct PathTool {
@ -48,6 +47,7 @@ pub enum PathToolMessage {
DeselectAllPoints,
Delete,
DeleteAndBreakPath,
DeleteAndRefit,
DragStop {
extend_selection: Key,
shrink_selection: Key,
@ -331,6 +331,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
DeselectAllPoints,
BreakPath,
DeleteAndBreakPath,
DeleteAndRefit,
ClosePath,
PointerMove,
),
@ -343,6 +344,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
Delete,
BreakPath,
DeleteAndBreakPath,
DeleteAndRefit,
SwapSelectedHandles,
),
PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant;
@ -353,6 +355,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
Enter,
BreakPath,
DeleteAndBreakPath,
DeleteAndRefit,
Escape,
RightClick,
),
@ -2046,18 +2049,31 @@ 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, false);
shape_editor.delete_selected_segments(document, responses);
shape_editor.delete_selected_points(document, responses);
responses.add(PathToolMessage::SelectionChanged);
PathToolFsmState::Ready
}
(_, PathToolMessage::BreakPath) => {
responses.add(DocumentMessage::AddTransaction);
shape_editor.break_path_at_selected_point(document, responses);
responses.add(PathToolMessage::SelectionChanged);
PathToolFsmState::Ready
}
(_, PathToolMessage::DeleteAndBreakPath) => {
responses.add(DocumentMessage::AddTransaction);
shape_editor.delete_point_and_break_path(document, responses);
responses.add(PathToolMessage::SelectionChanged);
PathToolFsmState::Ready
}
(_, PathToolMessage::DeleteAndRefit) => {
responses.add(DocumentMessage::AddTransaction);
shape_editor.delete_selected_points(document, responses, true);
responses.add(PathToolMessage::SelectionChanged);
PathToolFsmState::Ready
}
(_, PathToolMessage::DoubleClick { extend_selection, shrink_selection }) => {
@ -2468,8 +2484,9 @@ fn update_dynamic_hints(
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());
delete_selected_hints.push(HintInfo::keys([Key::Accel], "With Segments").prepend_plus());
delete_selected_hints.push(HintInfo::keys([Key::Shift], "Re-Fit").prepend_plus());
delete_selected_hints.push(HintInfo::keys([Key::Alt], "Cut Anchor").prepend_plus());
}
if single_colinear_anchor_selected {