mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
Merge 6c9fe9c071
into 24c6281644
This commit is contained in:
commit
39d729a0de
4 changed files with 167 additions and 49 deletions
|
@ -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),
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue