Skip to content

Commit 2306e98

Browse files
indierustyKeavon
andauthored
Refactor the 'Offset Path' node to use Kurbo entirely (#2946)
* impl function for segment intersections * fix and improve segment intersections * copy and refactor related segment intersection methods * copy and refactor tests for segment intersection from bezier-rs * impl intersection with bezpaths * copy and refactor tests * rename few variables in the tests module * rename position_on_bezpath to evaluate_bezpath * copy and refactor function to clip two intersecting simple bezpaths * refactor comments * copy and refactor functions for milter join * copy and refactor milter and round join functions from bezier-rs * it worked! refactor offset path node impl * fix few bugs * improve vars names and add comments * Code review * fmt --------- Co-authored-by: Keavon Chambers <[email protected]>
1 parent f15023e commit 2306e98

File tree

7 files changed

+649
-139
lines changed

7 files changed

+649
-139
lines changed

node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs

Lines changed: 134 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1+
use super::intersection::bezpath_intersections;
12
use super::poisson_disk::poisson_disk_sample;
3+
use super::util::segment_tangent;
24
use crate::vector::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
3-
use crate::vector::misc::{PointSpacingType, dvec2_to_point};
4-
use glam::DVec2;
5+
use crate::vector::misc::{PointSpacingType, dvec2_to_point, point_to_dvec2};
6+
use glam::{DMat2, DVec2};
57
use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape};
8+
use std::f64::consts::{FRAC_PI_2, PI};
69

7-
/// Splits the [`BezPath`] at `t` value which lie in the range of [0, 1].
10+
/// Splits the [`BezPath`] at segment index at `t` value which lie in the range of [0, 1].
811
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
9-
pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> {
12+
pub fn split_bezpath_at_segment(bezpath: &BezPath, segment_index: usize, t: f64) -> Option<(BezPath, BezPath)> {
1013
if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 {
1114
return None;
1215
}
1316

1417
// Get the segment which lies at the split.
15-
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None);
1618
let segment = bezpath.get_seg(segment_index + 1).unwrap();
1719

1820
// Divide the segment.
@@ -53,7 +55,19 @@ pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezP
5355
Some((first_bezpath, second_bezpath))
5456
}
5557

56-
pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
58+
/// Splits the [`BezPath`] at a `t` value which lies in the range of [0, 1].
59+
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
60+
pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> {
61+
if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 {
62+
return None;
63+
}
64+
65+
// Get the segment which lies at the split.
66+
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None);
67+
split_bezpath_at_segment(bezpath, segment_index, t)
68+
}
69+
70+
pub fn evaluate_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
5771
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
5872
bezpath.get_seg(segment_index + 1).unwrap().eval(t)
5973
}
@@ -328,3 +342,117 @@ pub fn is_linear(segment: &PathSeg) -> bool {
328342
PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => is_colinear(p0, p1, p3) && is_colinear(p0, p2, p3),
329343
}
330344
}
345+
346+
// TODO: If a segment curls back on itself tightly enough it could intersect again at the portion that should be trimmed. This could cause the Subpaths to be clipped
347+
// TODO: at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks.
348+
/// Helper function to clip overlap of two intersecting open BezPaths. Returns an Option because intersections may not exist for certain arrangements and distances.
349+
/// Assumes that the BezPaths represents simple Bezier segments, and clips the BezPaths at the last intersection of the first BezPath, and first intersection of the last BezPath.
350+
pub fn clip_simple_bezpaths(bezpath1: &BezPath, bezpath2: &BezPath) -> Option<(BezPath, BezPath)> {
351+
// Split the first subpath at its last intersection
352+
let subpath_1_intersections = bezpath_intersections(bezpath1, bezpath2, None, None);
353+
if subpath_1_intersections.is_empty() {
354+
return None;
355+
}
356+
let (segment_index, t) = *subpath_1_intersections.last()?;
357+
let (clipped_subpath1, _) = split_bezpath_at_segment(bezpath1, segment_index, t)?;
358+
359+
// Split the second subpath at its first intersection
360+
let subpath_2_intersections = bezpath_intersections(bezpath2, bezpath1, None, None);
361+
if subpath_2_intersections.is_empty() {
362+
return None;
363+
}
364+
let (segment_index, t) = subpath_2_intersections[0];
365+
let (_, clipped_subpath2) = split_bezpath_at_segment(bezpath2, segment_index, t)?;
366+
367+
Some((clipped_subpath1, clipped_subpath2))
368+
}
369+
370+
/// Returns the [`PathEl`] that is needed for a miter join if it is possible.
371+
///
372+
/// `miter_limit` defines a limit for the ratio between the miter length and the stroke width.
373+
/// Alternatively, this can be interpreted as limiting the angle that the miter can form.
374+
/// When the limit is exceeded, no [`PathEl`] will be returned.
375+
/// This value should be greater than 0. If not, the default of 4 will be used.
376+
pub fn miter_line_join(bezpath1: &BezPath, bezpath2: &BezPath, miter_limit: Option<f64>) -> Option<[PathEl; 2]> {
377+
let miter_limit = match miter_limit {
378+
Some(miter_limit) if miter_limit > f64::EPSILON => miter_limit,
379+
_ => 4.,
380+
};
381+
// TODO: Besides returning None using the `?` operator, is there a more appropriate way to handle a `None` result from `get_segment`?
382+
let in_segment = bezpath1.segments().last()?;
383+
let out_segment = bezpath2.segments().next()?;
384+
385+
let in_tangent = segment_tangent(in_segment, 1.);
386+
let out_tangent = segment_tangent(out_segment, 0.);
387+
388+
if in_tangent == DVec2::ZERO || out_tangent == DVec2::ZERO {
389+
// Avoid panic from normalizing zero vectors
390+
// TODO: Besides returning None, is there a more appropriate way to handle this?
391+
return None;
392+
}
393+
394+
let angle = (in_tangent * -1.).angle_to(out_tangent).abs();
395+
396+
if angle.to_degrees() < miter_limit {
397+
return None;
398+
}
399+
400+
let p1 = in_segment.end();
401+
let p2 = point_to_dvec2(p1) + in_tangent.normalize();
402+
let line1 = Line::new(p1, dvec2_to_point(p2));
403+
404+
let p1 = out_segment.start();
405+
let p2 = point_to_dvec2(p1) + out_tangent.normalize();
406+
let line2 = Line::new(p1, dvec2_to_point(p2));
407+
408+
// If we don't find the intersection point to draw the miter join, we instead default to a bevel join.
409+
// Otherwise, we return the element to create the join.
410+
let intersection = line1.crossing_point(line2)?;
411+
412+
Some([PathEl::LineTo(intersection), PathEl::LineTo(out_segment.start())])
413+
}
414+
415+
/// Computes the [`PathEl`] to form a circular join from `left` to `right`, along a circle around `center`.
416+
/// By default, the angle is assumed to be 180 degrees.
417+
pub fn compute_circular_subpath_details(left: DVec2, arc_point: DVec2, right: DVec2, center: DVec2, angle: Option<f64>) -> [PathEl; 2] {
418+
let center_to_arc_point = arc_point - center;
419+
420+
// Based on https://pomax.github.io/bezierinfo/#circles_cubic
421+
let handle_offset_factor = if let Some(angle) = angle { 4. / 3. * (angle / 4.).tan() } else { 0.551784777779014 };
422+
423+
let p1 = dvec2_to_point(left - (left - center).perp() * handle_offset_factor);
424+
let p2 = dvec2_to_point(arc_point + center_to_arc_point.perp() * handle_offset_factor);
425+
let p3 = dvec2_to_point(arc_point);
426+
427+
let first_half = PathEl::CurveTo(p1, p2, p3);
428+
429+
let p1 = dvec2_to_point(arc_point - center_to_arc_point.perp() * handle_offset_factor);
430+
let p2 = dvec2_to_point(right + (right - center).perp() * handle_offset_factor);
431+
let p3 = dvec2_to_point(right);
432+
433+
let second_half = PathEl::CurveTo(p1, p2, p3);
434+
435+
[first_half, second_half]
436+
}
437+
438+
/// Returns two [`PathEl`] to create a round join with the provided center.
439+
pub fn round_line_join(bezpath1: &BezPath, bezpath2: &BezPath, center: DVec2) -> [PathEl; 2] {
440+
let left = point_to_dvec2(bezpath1.segments().last().unwrap().end());
441+
let right = point_to_dvec2(bezpath2.segments().next().unwrap().start());
442+
443+
let center_to_right = right - center;
444+
let center_to_left = left - center;
445+
446+
let in_segment = bezpath1.segments().last();
447+
let in_tangent = in_segment.map(|in_segment| segment_tangent(in_segment, 1.));
448+
449+
let mut angle = center_to_right.angle_to(center_to_left) / 2.;
450+
let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
451+
452+
if in_tangent.map(|in_tangent| (arc_point - left).angle_to(in_tangent).abs()).unwrap_or_default() > FRAC_PI_2 {
453+
angle = angle - PI * (if angle < 0. { -1. } else { 1. });
454+
arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
455+
}
456+
457+
compute_circular_subpath_details(left, arc_point, right, center, Some(angle))
458+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// Minimum allowable separation between adjacent `t` values when calculating curve intersections
2+
pub const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3;
3+
4+
/// Constant used to determine if `f64`s are equivalent.
5+
#[cfg(test)]
6+
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;

0 commit comments

Comments
 (0)