mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-22 14:04:05 +00:00
Improve snapping performance (#3067)
* Use lyon_geom for intersection calculation of bezier segments * Implement approximate nearest point calculation and loosen bounding boxes * Add algorithm explanation * Update editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs Co-authored-by: Keavon Chambers <keavon@keavon.com> --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
b44a4fba1e
commit
7c30f6168b
7 changed files with 186 additions and 8 deletions
|
@ -35,6 +35,7 @@ tinyvec = { workspace = true }
|
|||
parley = { workspace = true }
|
||||
skrifa = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
lyon_geom = { workspace = true }
|
||||
log = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
poly-cool = { workspace = true }
|
||||
|
|
|
@ -1,5 +1,24 @@
|
|||
use super::contants::MIN_SEPARATION_VALUE;
|
||||
use kurbo::{BezPath, DEFAULT_ACCURACY, ParamCurve, PathSeg, Shape};
|
||||
use lyon_geom::{CubicBezierSegment, Point};
|
||||
|
||||
/// Converts a kurbo cubic bezier to a lyon_geom CubicBezierSegment
|
||||
fn kurbo_cubic_to_lyon(cubic: kurbo::CubicBez) -> CubicBezierSegment<f64> {
|
||||
CubicBezierSegment {
|
||||
from: Point::new(cubic.p0.x, cubic.p0.y),
|
||||
ctrl1: Point::new(cubic.p1.x, cubic.p1.y),
|
||||
ctrl2: Point::new(cubic.p2.x, cubic.p2.y),
|
||||
to: Point::new(cubic.p3.x, cubic.p3.y),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fast cubic-cubic intersection using lyon_geom's analytical approach
|
||||
fn cubic_cubic_intersections_lyon(cubic1: kurbo::CubicBez, cubic2: kurbo::CubicBez) -> Vec<(f64, f64)> {
|
||||
let lyon_cubic1 = kurbo_cubic_to_lyon(cubic1);
|
||||
let lyon_cubic2 = kurbo_cubic_to_lyon(cubic2);
|
||||
|
||||
lyon_cubic1.cubic_intersections_t(&lyon_cubic2).to_vec()
|
||||
}
|
||||
|
||||
/// Calculates the intersection points the bezpath has with a given segment and returns a list of `(usize, f64)` tuples,
|
||||
/// where the `usize` represents the index of the segment in the bezpath, and the `f64` represents the `t`-value local to
|
||||
|
@ -37,6 +56,8 @@ pub fn segment_intersections(segment1: PathSeg, segment2: PathSeg, accuracy: Opt
|
|||
match (segment1, segment2) {
|
||||
(PathSeg::Line(line), segment2) => segment2.intersect_line(line).iter().map(|i| (i.line_t, i.segment_t)).collect(),
|
||||
(segment1, PathSeg::Line(line)) => segment1.intersect_line(line).iter().map(|i| (i.segment_t, i.line_t)).collect(),
|
||||
// Fast path for cubic-cubic intersections using lyon_geom
|
||||
(PathSeg::Cubic(cubic1), PathSeg::Cubic(cubic2)) => cubic_cubic_intersections_lyon(cubic1, cubic2),
|
||||
(segment1, segment2) => {
|
||||
let mut intersections = Vec::new();
|
||||
segment_intersections_inner(segment1, 0., 1., segment2, 0., 1., accuracy, &mut intersections);
|
||||
|
@ -51,6 +72,21 @@ pub fn subsegment_intersections(segment1: PathSeg, min_t1: f64, max_t1: f64, seg
|
|||
match (segment1, segment2) {
|
||||
(PathSeg::Line(line), segment2) => segment2.intersect_line(line).iter().map(|i| (i.line_t, i.segment_t)).collect(),
|
||||
(segment1, PathSeg::Line(line)) => segment1.intersect_line(line).iter().map(|i| (i.segment_t, i.line_t)).collect(),
|
||||
// Fast path for cubic-cubic intersections using lyon_geom with subsegment parameters
|
||||
(PathSeg::Cubic(cubic1), PathSeg::Cubic(cubic2)) => {
|
||||
let sub_cubic1 = cubic1.subsegment(min_t1..max_t1);
|
||||
let sub_cubic2 = cubic2.subsegment(min_t2..max_t2);
|
||||
|
||||
cubic_cubic_intersections_lyon(sub_cubic1, sub_cubic2)
|
||||
.into_iter()
|
||||
// Convert subsegment t-values back to original segment t-values
|
||||
.map(|(t1, t2)| {
|
||||
let original_t1 = min_t1 + t1 * (max_t1 - min_t1);
|
||||
let original_t2 = min_t2 + t2 * (max_t2 - min_t2);
|
||||
(original_t1, original_t2)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
(segment1, segment2) => {
|
||||
let mut intersections = Vec::new();
|
||||
segment_intersections_inner(segment1, min_t1, max_t1, segment2, min_t2, max_t2, accuracy, &mut intersections);
|
||||
|
@ -59,12 +95,33 @@ pub fn subsegment_intersections(segment1: PathSeg, min_t1: f64, max_t1: f64, seg
|
|||
}
|
||||
}
|
||||
|
||||
fn approx_bounding_box(path_seg: PathSeg) -> kurbo::Rect {
|
||||
use kurbo::Rect;
|
||||
match path_seg {
|
||||
PathSeg::Line(line) => kurbo::Rect::from_points(line.p0, line.p1),
|
||||
PathSeg::Quad(quad_bez) => {
|
||||
let r1 = Rect::from_points(quad_bez.p0, quad_bez.p1);
|
||||
let r2 = Rect::from_points(quad_bez.p1, quad_bez.p2);
|
||||
r1.union(r2)
|
||||
}
|
||||
PathSeg::Cubic(cubic_bez) => {
|
||||
let r1 = Rect::from_points(cubic_bez.p0, cubic_bez.p1);
|
||||
let r2 = Rect::from_points(cubic_bez.p2, cubic_bez.p3);
|
||||
r1.union(r2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements [https://pomax.github.io/bezierinfo/#curveintersection] to find intersection between two Bezier segments
|
||||
/// by splitting the segment recursively until the size of the subsegment's bounding box is smaller than the accuracy.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn segment_intersections_inner(segment1: PathSeg, min_t1: f64, max_t1: f64, segment2: PathSeg, min_t2: f64, max_t2: f64, accuracy: f64, intersections: &mut Vec<(f64, f64)>) {
|
||||
let bbox1 = segment1.subsegment(min_t1..max_t1).bounding_box();
|
||||
let bbox2 = segment2.subsegment(min_t2..max_t2).bounding_box();
|
||||
let bbox1 = approx_bounding_box(segment1.subsegment(min_t1..max_t1));
|
||||
let bbox2 = approx_bounding_box(segment2.subsegment(min_t2..max_t2));
|
||||
|
||||
if intersections.len() > 50 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mid_t1 = (min_t1 + max_t1) / 2.;
|
||||
let mid_t2 = (min_t2 + max_t2) / 2.;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue