Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 134 additions & 6 deletions node-graph/gcore/src/vector/algorithms/bezpath_algorithms.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
use super::intersection::bezpath_intersections;
use super::poisson_disk::poisson_disk_sample;
use super::util::segment_tangent;
use crate::vector::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
use crate::vector::misc::{PointSpacingType, dvec2_to_point};
use glam::DVec2;
use crate::vector::misc::{PointSpacingType, dvec2_to_point, point_to_dvec2};
use glam::{DMat2, DVec2};
use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape};
use std::f64::consts::{FRAC_PI_2, PI};

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

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

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

pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
/// Splits the [`BezPath`] at a `t` value which lies in the range of [0, 1].
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> {
if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 {
return None;
}

// Get the segment which lies at the split.
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None);
split_bezpath_at_segment(bezpath, segment_index, t)
}

pub fn evaluate_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point {
let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
bezpath.get_seg(segment_index + 1).unwrap().eval(t)
}
Expand Down Expand Up @@ -328,3 +342,117 @@ pub fn is_linear(segment: &PathSeg) -> bool {
PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => is_colinear(p0, p1, p3) && is_colinear(p0, p2, p3),
}
}

// 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
// TODO: at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks.
/// Helper function to clip overlap of two intersecting open BezPaths. Returns an Option because intersections may not exist for certain arrangements and distances.
/// 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.
pub fn clip_simple_bezpaths(bezpath1: &BezPath, bezpath2: &BezPath) -> Option<(BezPath, BezPath)> {
// Split the first subpath at its last intersection
let subpath_1_intersections = bezpath_intersections(bezpath1, bezpath2, None, None);
if subpath_1_intersections.is_empty() {
return None;
}
let (segment_index, t) = *subpath_1_intersections.last()?;
let (clipped_subpath1, _) = split_bezpath_at_segment(bezpath1, segment_index, t)?;

// Split the second subpath at its first intersection
let subpath_2_intersections = bezpath_intersections(bezpath2, bezpath1, None, None);
if subpath_2_intersections.is_empty() {
return None;
}
let (segment_index, t) = subpath_2_intersections[0];
let (_, clipped_subpath2) = split_bezpath_at_segment(bezpath2, segment_index, t)?;

Some((clipped_subpath1, clipped_subpath2))
}

/// Returns the [`PathEl`] that is needed for a miter join if it is possible.
///
/// `miter_limit` defines a limit for the ratio between the miter length and the stroke width.
/// Alternatively, this can be interpreted as limiting the angle that the miter can form.
/// When the limit is exceeded, no [`PathEl`] will be returned.
/// This value should be greater than 0. If not, the default of 4 will be used.
pub fn miter_line_join(bezpath1: &BezPath, bezpath2: &BezPath, miter_limit: Option<f64>) -> Option<[PathEl; 2]> {
let miter_limit = match miter_limit {
Some(miter_limit) if miter_limit > f64::EPSILON => miter_limit,
_ => 4.,
};
// TODO: Besides returning None using the `?` operator, is there a more appropriate way to handle a `None` result from `get_segment`?
let in_segment = bezpath1.segments().last()?;
let out_segment = bezpath2.segments().next()?;

let in_tangent = segment_tangent(in_segment, 1.);
let out_tangent = segment_tangent(out_segment, 0.);

if in_tangent == DVec2::ZERO || out_tangent == DVec2::ZERO {
// Avoid panic from normalizing zero vectors
// TODO: Besides returning None, is there a more appropriate way to handle this?
return None;
}

let angle = (in_tangent * -1.).angle_to(out_tangent).abs();

if angle.to_degrees() < miter_limit {
return None;
}

let p1 = in_segment.end();
let p2 = point_to_dvec2(p1) + in_tangent.normalize();
let line1 = Line::new(p1, dvec2_to_point(p2));

let p1 = out_segment.start();
let p2 = point_to_dvec2(p1) + out_tangent.normalize();
let line2 = Line::new(p1, dvec2_to_point(p2));

// If we don't find the intersection point to draw the miter join, we instead default to a bevel join.
// Otherwise, we return the element to create the join.
let intersection = line1.crossing_point(line2)?;

Some([PathEl::LineTo(intersection), PathEl::LineTo(out_segment.start())])
}

/// Computes the [`PathEl`] to form a circular join from `left` to `right`, along a circle around `center`.
/// By default, the angle is assumed to be 180 degrees.
pub fn compute_circular_subpath_details(left: DVec2, arc_point: DVec2, right: DVec2, center: DVec2, angle: Option<f64>) -> [PathEl; 2] {
let center_to_arc_point = arc_point - center;

// Based on https://pomax.github.io/bezierinfo/#circles_cubic
let handle_offset_factor = if let Some(angle) = angle { 4. / 3. * (angle / 4.).tan() } else { 0.551784777779014 };

let p1 = dvec2_to_point(left - (left - center).perp() * handle_offset_factor);
let p2 = dvec2_to_point(arc_point + center_to_arc_point.perp() * handle_offset_factor);
let p3 = dvec2_to_point(arc_point);

let first_half = PathEl::CurveTo(p1, p2, p3);

let p1 = dvec2_to_point(arc_point - center_to_arc_point.perp() * handle_offset_factor);
let p2 = dvec2_to_point(right + (right - center).perp() * handle_offset_factor);
let p3 = dvec2_to_point(right);

let second_half = PathEl::CurveTo(p1, p2, p3);

[first_half, second_half]
}

/// Returns two [`PathEl`] to create a round join with the provided center.
pub fn round_line_join(bezpath1: &BezPath, bezpath2: &BezPath, center: DVec2) -> [PathEl; 2] {
let left = point_to_dvec2(bezpath1.segments().last().unwrap().end());
let right = point_to_dvec2(bezpath2.segments().next().unwrap().start());

let center_to_right = right - center;
let center_to_left = left - center;

let in_segment = bezpath1.segments().last();
let in_tangent = in_segment.map(|in_segment| segment_tangent(in_segment, 1.));

let mut angle = center_to_right.angle_to(center_to_left) / 2.;
let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);

if in_tangent.map(|in_tangent| (arc_point - left).angle_to(in_tangent).abs()).unwrap_or_default() > FRAC_PI_2 {
angle = angle - PI * (if angle < 0. { -1. } else { 1. });
arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
}

compute_circular_subpath_details(left, arc_point, right, center, Some(angle))
}
6 changes: 6 additions & 0 deletions node-graph/gcore/src/vector/algorithms/contants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// Minimum allowable separation between adjacent `t` values when calculating curve intersections
pub const MIN_SEPARATION_VALUE: f64 = 5. * 1e-3;

/// Constant used to determine if `f64`s are equivalent.
#[cfg(test)]
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
Loading
Loading