mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 13:58:12 +08:00
Bezier-rs: Add joins and caps to offsets and outlines (#1083)
* Intial work * Improve miter and add round join * Get arcs to go opposite direction * Add cap and other refactors * Rename joint to join, fix some bugs * Fix single point issue * Clean up * Fix iframe sizes and update UI * Address comments and handle single point outline * Rename variables, fix branches in outline * Address comments
This commit is contained in:
committed by
Keavon Chambers
parent
7e124c8035
commit
c0576ab4e0
@@ -88,7 +88,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
/// Returns the number of segments contained within the `Subpath`.
|
||||
pub fn len_segments(&self) -> usize {
|
||||
let mut number_of_curves = self.len();
|
||||
if !self.closed {
|
||||
if !self.closed && number_of_curves > 0 {
|
||||
number_of_curves -= 1
|
||||
}
|
||||
number_of_curves
|
||||
@@ -112,6 +112,17 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
&self.manipulator_groups
|
||||
}
|
||||
|
||||
/// Returns if the Subpath is equivalent to a single point.
|
||||
pub fn is_point(&self) -> bool {
|
||||
if self.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let point = self.manipulator_groups[0].anchor;
|
||||
self.manipulator_groups
|
||||
.iter()
|
||||
.all(|manipulator_group| manipulator_group.anchor.abs_diff_eq(point, MAX_ABSOLUTE_DIFFERENCE))
|
||||
}
|
||||
|
||||
/// Appends to the `svg` mutable string with an SVG shape representation of the curve.
|
||||
pub fn curve_to_svg(&self, svg: &mut String, attributes: String) {
|
||||
let curve_start_argument = format!("{SVG_ARG_MOVE}{} {}", self[0].anchor.x, self[0].anchor.y);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::*;
|
||||
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use crate::utils::SubpathTValue;
|
||||
use crate::utils::{compute_circular_subpath_details, line_intersection, SubpathTValue};
|
||||
use crate::TValue;
|
||||
|
||||
use glam::DVec2;
|
||||
use glam::{DMat2, DVec2};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
/// Calculate the point on the subpath based on the parametric `t`-value provided.
|
||||
@@ -22,7 +23,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
/// - `error`: an optional f64 value to provide an error bound
|
||||
/// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two.
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
|
||||
pub fn intersections(&self, other: &Bezier, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
self.iter()
|
||||
.enumerate()
|
||||
@@ -34,27 +35,20 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
/// This function expects the following:
|
||||
/// - other: a [Bezier] curve to check intersections against
|
||||
/// - error: an optional f64 value to provide an error bound
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/intersect-cubic/solo" title="Intersection Demo"></iframe>
|
||||
pub fn subpath_intersections(&self, other: &Subpath<ManipulatorGroupId>, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
let mut intersection_t_values: Vec<(usize, f64)> = other.iter().flat_map(|bezier| self.intersections(&bezier, error, minimum_separation)).collect();
|
||||
intersection_t_values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
intersection_t_values
|
||||
}
|
||||
|
||||
/// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/tangent/solo" title="Tangent Demo"></iframe>
|
||||
pub fn tangent(&self, t: SubpathTValue) -> DVec2 {
|
||||
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||
self.get_segment(segment_index).unwrap().tangent(TValue::Parametric(t))
|
||||
}
|
||||
|
||||
/// Returns a list of `t` values that correspond to the self intersection points of the subpath. For each intersection point, the returned `t` value is the smaller of the two that correspond to the point.
|
||||
/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point.
|
||||
/// - `minimum_separation`: the minimum difference two adjacent `t`-values must have when comparing adjacent `t`-values in sorted order.
|
||||
/// If the comparison condition is not satisfied, the function takes the larger `t`-value of the two
|
||||
///
|
||||
/// **NOTE**: if an intersection were to occur within an `error` distance away from an anchor point, the algorithm will filter that intersection out.
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/self-intersect/solo" title="Self-Intersection Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/bezier-rs-demos#subpath/self-intersect/solo" title="Self-Intersection Demo"></iframe>
|
||||
pub fn self_intersections(&self, error: Option<f64>, minimum_separation: Option<f64>) -> Vec<(usize, f64)> {
|
||||
let mut intersections_vec = Vec::new();
|
||||
let err = error.unwrap_or(MAX_ABSOLUTE_DIFFERENCE);
|
||||
@@ -74,6 +68,13 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
intersections_vec
|
||||
}
|
||||
|
||||
/// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/tangent/solo" title="Tangent Demo"></iframe>
|
||||
pub fn tangent(&self, t: SubpathTValue) -> DVec2 {
|
||||
let (segment_index, t) = self.t_value_to_parametric(t);
|
||||
self.get_segment(segment_index).unwrap().tangent(TValue::Parametric(t))
|
||||
}
|
||||
|
||||
/// Returns a normalized unit vector representing the direction of the normal on the subpath based on the parametric `t`-value provided.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/normal/solo" title="Normal Demo"></iframe>
|
||||
pub fn normal(&self, t: SubpathTValue) -> DVec2 {
|
||||
@@ -83,7 +84,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
|
||||
/// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric subpaths respectively.
|
||||
/// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/local-extrema/solo" title="Local Extrema Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/local-extrema/solo" title="Local Extrema Demo"></iframe>
|
||||
pub fn local_extrema(&self) -> [Vec<f64>; 2] {
|
||||
let number_of_curves = self.len_segments() as f64;
|
||||
|
||||
@@ -98,7 +99,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
}
|
||||
|
||||
/// Return the min and max corners that represent the bounding box of the subpath.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/bounding-box/solo" title="Bounding Box Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="325px" src="https://graphite.rs/bezier-rs-demos#subpath/bounding-box/solo" title="Bounding Box Demo"></iframe>
|
||||
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
self.iter().map(|bezier| bezier.bounding_box()).reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
|
||||
}
|
||||
@@ -112,7 +113,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
|
||||
/// Returns list of `t`-values representing the inflection points of the subpath.
|
||||
/// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/inflections/solo" title="Inflections Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/bezier-rs-demos#subpath/inflections/solo" title="Inflections Demo"></iframe>
|
||||
pub fn inflections(&self) -> Vec<f64> {
|
||||
let number_of_curves = self.len_segments() as f64;
|
||||
let inflection_t_values: Vec<f64> = self
|
||||
@@ -135,6 +136,92 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
pub fn contains_point(&self, target_point: DVec2) -> bool {
|
||||
self.iter().map(|bezier| bezier.winding(target_point)).sum::<i32>() != 0
|
||||
}
|
||||
|
||||
/// Returns the manipulator point that is needed for a miter join if it is possible.
|
||||
pub(crate) fn miter_line_join(&self, other: &Subpath<ManipulatorGroupId>) -> Option<ManipulatorGroup<ManipulatorGroupId>> {
|
||||
let in_segment = self.get_segment(self.len_segments() - 1).unwrap();
|
||||
let out_segment = other.get_segment(0).unwrap();
|
||||
let in_tangent = in_segment.tangent(TValue::Parametric(1.));
|
||||
let out_tangent = out_segment.tangent(TValue::Parametric(0.));
|
||||
|
||||
let normalized_in_tangent = in_tangent.normalize();
|
||||
let normalized_out_tangent = out_tangent.normalize();
|
||||
|
||||
// The tangents must not be parallel for the miter join
|
||||
if !normalized_in_tangent.abs_diff_eq(normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) && !normalized_in_tangent.abs_diff_eq(-normalized_out_tangent, MAX_ABSOLUTE_DIFFERENCE) {
|
||||
let intersection = line_intersection(in_segment.end(), in_tangent, out_segment.start(), out_tangent);
|
||||
|
||||
// Draw the miter join if the intersection occurs in the correct direction with respect to the path
|
||||
if (intersection - in_segment.end()).normalize().abs_diff_eq(in_tangent, MAX_ABSOLUTE_DIFFERENCE)
|
||||
&& (out_segment.start() - intersection).normalize().abs_diff_eq(out_tangent, MAX_ABSOLUTE_DIFFERENCE)
|
||||
{
|
||||
return Some(ManipulatorGroup {
|
||||
anchor: intersection,
|
||||
in_handle: None,
|
||||
out_handle: None,
|
||||
id: ManipulatorGroupId::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
// If we can't draw the miter join, default to a bevel join
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the necessary information to create a round join with the provided center.
|
||||
/// The returned items correspond to:
|
||||
/// - The `out_handle` for the last manipulator group of `self`
|
||||
/// - The new manipulator group to be added
|
||||
/// - The `in_handle` for the first manipulator group of `other`
|
||||
pub(crate) fn round_line_join(&self, other: &Subpath<ManipulatorGroupId>, center: DVec2) -> (DVec2, ManipulatorGroup<ManipulatorGroupId>, DVec2) {
|
||||
let left = self.manipulator_groups[self.len() - 1].anchor;
|
||||
let right = other.manipulator_groups[0].anchor;
|
||||
|
||||
let center_to_right = right - center;
|
||||
let center_to_left = left - center;
|
||||
|
||||
let in_segment = self.get_segment(self.len_segments() - 1).unwrap();
|
||||
let in_tangent = in_segment.tangent(TValue::Parametric(1.));
|
||||
|
||||
let mut angle = center_to_right.angle_between(center_to_left) / 2.;
|
||||
let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right);
|
||||
|
||||
if (arc_point - left).angle_between(in_tangent).abs() > 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))
|
||||
}
|
||||
|
||||
/// Returns the necessary information to create a round cap between the end of `self` and the beginning of `other`.
|
||||
/// The returned items correspond to:
|
||||
/// - The `out_handle` for the last manipulator group of `self`
|
||||
/// - The new manipulator group to be added
|
||||
/// - The `in_handle` for the first manipulator group of `other`
|
||||
pub(crate) fn round_cap(&self, other: &Subpath<ManipulatorGroupId>) -> (DVec2, ManipulatorGroup<ManipulatorGroupId>, DVec2) {
|
||||
let left = self.manipulator_groups[self.len() - 1].anchor;
|
||||
let right = other.manipulator_groups[0].anchor;
|
||||
|
||||
let center = (right + left) / 2.;
|
||||
let center_to_right = right - center;
|
||||
|
||||
let arc_point = center + center_to_right.perp();
|
||||
|
||||
compute_circular_subpath_details(left, arc_point, right, center, None)
|
||||
}
|
||||
|
||||
/// Returns the two manipulator groups that create a sqaure cap between the end of `self` and the beginning of `other`.
|
||||
pub(crate) fn square_cap(&self, other: &Subpath<ManipulatorGroupId>) -> [ManipulatorGroup<ManipulatorGroupId>; 2] {
|
||||
let left = self.manipulator_groups[self.len() - 1].anchor;
|
||||
let right = other.manipulator_groups[0].anchor;
|
||||
|
||||
let center = (right + left) / 2.;
|
||||
let center_to_right = right - center;
|
||||
|
||||
let translation = center_to_right.perp();
|
||||
|
||||
[ManipulatorGroup::new_anchor(left + translation), ManipulatorGroup::new_anchor(right + translation)]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -517,4 +604,86 @@ mod tests {
|
||||
}
|
||||
|
||||
// TODO: add more intersection tests
|
||||
|
||||
#[test]
|
||||
fn round_join_counter_clockwise_rotation() {
|
||||
// Test case where the round join is drawn in the counter clockwise direction between two consecutive offsets
|
||||
let subpath = Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: DVec2::new(20., 20.),
|
||||
out_handle: Some(DVec2::new(10., 90.)),
|
||||
in_handle: None,
|
||||
id: EmptyId,
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: DVec2::new(114., 159.),
|
||||
out_handle: None,
|
||||
in_handle: Some(DVec2::new(60., 40.)),
|
||||
id: EmptyId,
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: DVec2::new(148., 155.),
|
||||
out_handle: None,
|
||||
in_handle: None,
|
||||
id: EmptyId,
|
||||
},
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
let offset = subpath.offset(10., utils::Join::Round);
|
||||
let offset_len = offset.len();
|
||||
|
||||
let manipulator_groups = offset.manipulator_groups();
|
||||
let round_start = manipulator_groups[offset_len - 4].anchor;
|
||||
let round_point = manipulator_groups[offset_len - 3].anchor;
|
||||
let round_end = manipulator_groups[offset_len - 2].anchor;
|
||||
|
||||
let middle = (round_start + round_end) / 2.;
|
||||
|
||||
assert!((round_point - middle).angle_between(round_start - middle) > 0.);
|
||||
assert!((round_end - middle).angle_between(round_point - middle) > 0.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_join_clockwise_rotation() {
|
||||
// Test case where the round join is drawn in the clockwise direction between two consecutive offsets
|
||||
let subpath = Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: DVec2::new(20., 20.),
|
||||
out_handle: Some(DVec2::new(10., 90.)),
|
||||
in_handle: None,
|
||||
id: EmptyId,
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: DVec2::new(150., 40.),
|
||||
out_handle: None,
|
||||
in_handle: Some(DVec2::new(60., 40.)),
|
||||
id: EmptyId,
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: DVec2::new(78., 36.),
|
||||
out_handle: None,
|
||||
in_handle: None,
|
||||
id: EmptyId,
|
||||
},
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
let offset = subpath.offset(-15., utils::Join::Round);
|
||||
let offset_len = offset.len();
|
||||
|
||||
let manipulator_groups = offset.manipulator_groups();
|
||||
let round_start = manipulator_groups[offset_len - 4].anchor;
|
||||
let round_point = manipulator_groups[offset_len - 3].anchor;
|
||||
let round_end = manipulator_groups[offset_len - 2].anchor;
|
||||
|
||||
let middle = (round_start + round_end) / 2.;
|
||||
|
||||
assert!((round_point - middle).angle_between(round_start - middle) < 0.);
|
||||
assert!((round_end - middle).angle_between(round_point - middle) < 0.);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ use std::vec;
|
||||
|
||||
use super::*;
|
||||
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use crate::utils::{Joint, SubpathTValue, TValue};
|
||||
use crate::utils::{Cap, Join, SubpathTValue, TValue};
|
||||
|
||||
use glam::DAffine2;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
/// Helper function to ensure the index and t value pair is mapped within a maximum index value.
|
||||
/// Allows for the point to be fetched without needing to handle an additional edge case.
|
||||
@@ -109,9 +109,14 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
}
|
||||
|
||||
/// Returns a [Subpath] with a reversed winding order.
|
||||
/// Note that a reversed closed subpath will start on the same manipulator group and simply wind the other direction
|
||||
pub fn reverse(&self) -> Subpath<ManipulatorGroupId> {
|
||||
let mut reversed = Subpath::reverse_manipulator_groups(self.manipulator_groups());
|
||||
if self.closed {
|
||||
reversed.rotate_right(1);
|
||||
};
|
||||
Subpath {
|
||||
manipulator_groups: Subpath::reverse_manipulator_groups(&self.manipulator_groups),
|
||||
manipulator_groups: reversed,
|
||||
closed: self.closed,
|
||||
}
|
||||
}
|
||||
@@ -121,7 +126,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
/// The resulting Subpath will wind from the given `t1` to `t2`.
|
||||
/// That means, if the value of `t1` > `t2`, it will cross the break between endpoints from `t1` to `t = 1 = 0` to `t2`.
|
||||
/// If a path winding in the reverse direction is desired, call `trim` on the `Subpath` returned from `Subpath::reverse`.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/trim/solo" title="Trim Demo"></iframe>
|
||||
/// <iframe frameBorder="0" width="100%" height="450px" src="https://graphite.rs/bezier-rs-demos#subpath/trim/solo" title="Trim Demo"></iframe>
|
||||
pub fn trim(&self, t1: SubpathTValue, t2: SubpathTValue) -> Subpath<ManipulatorGroupId> {
|
||||
// Return a clone of the Subpath if it is not long enough to be a valid Bezier
|
||||
if self.manipulator_groups.is_empty() {
|
||||
@@ -278,6 +283,9 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
/// Smooths a Subpath up to the first derivative, using a weighted averaged based on segment length.
|
||||
/// The Subpath must be open, and contain no quadratic segments.
|
||||
pub(crate) fn smooth_open_subpath(&mut self) {
|
||||
if self.len() < 2 {
|
||||
return;
|
||||
}
|
||||
for i in 1..self.len() - 1 {
|
||||
let first_bezier = self.manipulator_groups[i - 1].to_bezier(&self.manipulator_groups[i]);
|
||||
let second_bezier = self.manipulator_groups[i].to_bezier(&self.manipulator_groups[i + 1]);
|
||||
@@ -326,17 +334,22 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
}
|
||||
|
||||
/// Reduces the segments of the subpath into simple subcurves, then scales each subcurve a set `distance` away.
|
||||
/// The intersections of segments of the subpath are joined using the method specified by the `joint` argument.
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#subpath/offset/solo" title="Offset Demo"></iframe>
|
||||
pub fn offset(&self, distance: f64, joint: Joint) -> Subpath<ManipulatorGroupId> {
|
||||
/// The intersections of segments of the subpath are joined using the method specified by the `join` argument.
|
||||
/// <iframe frameBorder="0" width="100%" height="400px" src="https://graphite.rs/bezier-rs-demos#subpath/offset/solo" title="Offset Demo"></iframe>
|
||||
pub fn offset(&self, distance: f64, join: Join) -> Subpath<ManipulatorGroupId> {
|
||||
assert!(self.len_segments() > 1, "Cannot offset an empty Subpath.");
|
||||
|
||||
// An offset at a distance 0 from the curve is simply the same curve
|
||||
if distance == 0. {
|
||||
// An offset of a single point is not defined
|
||||
if distance == 0. || self.len() == 1 {
|
||||
return self.clone();
|
||||
}
|
||||
|
||||
let mut subpaths = self.iter().map(|bezier| bezier.offset(distance)).collect::<Vec<Subpath<ManipulatorGroupId>>>();
|
||||
let mut subpaths = self
|
||||
.iter()
|
||||
.filter(|bezier| !bezier.is_point())
|
||||
.map(|bezier| bezier.offset(distance))
|
||||
.collect::<Vec<Subpath<ManipulatorGroupId>>>();
|
||||
let mut drop_common_point = vec![true; self.len()];
|
||||
|
||||
// Clip or join consecutive Subpaths
|
||||
@@ -359,7 +372,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
let angle = out_tangent.angle_between(in_tangent);
|
||||
|
||||
// The angle is concave. The Subpath overlap and must be clipped
|
||||
let mut apply_joint = true;
|
||||
let mut apply_join = true;
|
||||
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
|
||||
// If the distance is large enough, there may still be no intersections. Also, if the angle is close enough to zero,
|
||||
// subpath intersections may find no intersections. In this case, the points are likely close enough that we can approximate
|
||||
@@ -367,16 +380,27 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(subpath1, subpath2) {
|
||||
subpaths[i] = clipped_subpath1;
|
||||
subpaths[j] = clipped_subpath2;
|
||||
apply_joint = false;
|
||||
apply_join = false;
|
||||
}
|
||||
}
|
||||
// The angle is convex. The Subpath must be joined using the specified Joint type
|
||||
if apply_joint {
|
||||
match joint {
|
||||
Joint::Bevel => {
|
||||
drop_common_point[j] = false;
|
||||
// The angle is convex. The Subpath must be joined using the specified join type
|
||||
if apply_join {
|
||||
drop_common_point[j] = false;
|
||||
match join {
|
||||
Join::Bevel => {}
|
||||
Join::Miter => {
|
||||
let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j]);
|
||||
if let Some(miter_manipulator_group) = miter_manipulator_group {
|
||||
subpaths[i].manipulator_groups.push(miter_manipulator_group);
|
||||
}
|
||||
}
|
||||
Join::Round => {
|
||||
let (out_handle, round_point, in_handle) = subpaths[i].round_line_join(&subpaths[j], self.manipulator_groups[j].anchor);
|
||||
let last_index = subpaths[i].manipulator_groups.len() - 1;
|
||||
subpaths[i].manipulator_groups[last_index].out_handle = Some(out_handle);
|
||||
subpaths[i].manipulator_groups.push(round_point.clone());
|
||||
subpaths[j].manipulator_groups[0].in_handle = Some(in_handle);
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -387,22 +411,35 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
let in_tangent = self.get_segment(0).unwrap().tangent(TValue::Parametric(0.));
|
||||
let angle = out_tangent.angle_between(in_tangent);
|
||||
|
||||
let mut apply_joint = true;
|
||||
let mut apply_join = true;
|
||||
if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) {
|
||||
if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(&subpaths[subpaths.len() - 1], &subpaths[0]) {
|
||||
// Merge the clipped subpaths
|
||||
let last_index = subpaths.len() - 1;
|
||||
subpaths[last_index] = clipped_subpath1;
|
||||
subpaths[0] = clipped_subpath2;
|
||||
apply_joint = false;
|
||||
apply_join = false;
|
||||
}
|
||||
}
|
||||
if apply_joint {
|
||||
match joint {
|
||||
Joint::Bevel => {
|
||||
drop_common_point[0] = false;
|
||||
if apply_join {
|
||||
drop_common_point[0] = false;
|
||||
match join {
|
||||
Join::Bevel => {}
|
||||
Join::Miter => {
|
||||
let last_subpath_index = subpaths.len() - 1;
|
||||
let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0]);
|
||||
if let Some(miter_manipulator_group) = miter_manipulator_group {
|
||||
subpaths[last_subpath_index].manipulator_groups.push(miter_manipulator_group);
|
||||
}
|
||||
}
|
||||
Join::Round => {
|
||||
let last_subpath_index = subpaths.len() - 1;
|
||||
let (out_handle, round_point, in_handle) = subpaths[last_subpath_index].round_line_join(&subpaths[0], self.manipulator_groups[0].anchor);
|
||||
let last_index = subpaths[last_subpath_index].manipulator_groups.len() - 1;
|
||||
subpaths[last_subpath_index].manipulator_groups[last_index].out_handle = Some(out_handle);
|
||||
subpaths[last_subpath_index].manipulator_groups.push(round_point);
|
||||
subpaths[0].manipulator_groups[0].in_handle = Some(in_handle);
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -428,34 +465,68 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
||||
Subpath::new(manipulator_groups, self.closed)
|
||||
}
|
||||
|
||||
/// Helper function to combine the two offsets that make up an outline.
|
||||
pub(crate) fn combine_outline(&self, other: &Subpath<ManipulatorGroupId>, cap: Cap) -> Subpath<ManipulatorGroupId> {
|
||||
let mut result_manipulator_groups: Vec<ManipulatorGroup<ManipulatorGroupId>> = vec![];
|
||||
result_manipulator_groups.extend_from_slice(self.manipulator_groups());
|
||||
match cap {
|
||||
Cap::Butt => {
|
||||
result_manipulator_groups.extend_from_slice(other.manipulator_groups());
|
||||
}
|
||||
Cap::Round => {
|
||||
let last_index = result_manipulator_groups.len() - 1;
|
||||
let (out_handle, round_point, in_handle) = self.round_cap(other);
|
||||
result_manipulator_groups[last_index].out_handle = Some(out_handle);
|
||||
result_manipulator_groups.push(round_point);
|
||||
result_manipulator_groups.extend_from_slice(&other.manipulator_groups);
|
||||
result_manipulator_groups[last_index + 2].in_handle = Some(in_handle);
|
||||
|
||||
let last_index = result_manipulator_groups.len() - 1;
|
||||
let (out_handle, round_point, in_handle) = other.round_cap(self);
|
||||
result_manipulator_groups[last_index].out_handle = Some(out_handle);
|
||||
result_manipulator_groups.push(round_point);
|
||||
result_manipulator_groups[0].in_handle = Some(in_handle);
|
||||
}
|
||||
Cap::Square => {
|
||||
let square_points = self.square_cap(other);
|
||||
result_manipulator_groups.extend_from_slice(&square_points);
|
||||
result_manipulator_groups.extend_from_slice(other.manipulator_groups());
|
||||
let square_points = other.square_cap(self);
|
||||
result_manipulator_groups.extend_from_slice(&square_points);
|
||||
}
|
||||
}
|
||||
Subpath::new(result_manipulator_groups, true)
|
||||
}
|
||||
|
||||
// TODO: Replace this return type with `Path`, once the `Path` data type has been created.
|
||||
/// Outline returns a single closed subpath (if the original subpath was open) or two closed subpaths (if the original subpath was closed) that forms
|
||||
/// an approximate outline around the subpath at a specified distance from the curve. Outline takes the following parameters:
|
||||
/// - `distance` - The outline's distance from the curve.
|
||||
/// - `joint` - The joint type used to cap the endpoints of open bezier curves, and join successive subpath segments.
|
||||
/// <iframe frameBorder="0" width="100%" height="375px" src="https://graphite.rs/bezier-rs-demos#subpath/outline/solo" title="Outline Demo"></iframe>
|
||||
pub fn outline(&self, distance: f64, joint: Joint) -> (Subpath<ManipulatorGroupId>, Option<Subpath<ManipulatorGroupId>>) {
|
||||
let mut pos_offset = self.offset(distance, joint);
|
||||
let mut neg_offset = self.reverse().offset(distance, joint);
|
||||
/// - `join` - The join type used to cap the endpoints of open bezier curves, and join successive subpath segments.
|
||||
/// <iframe frameBorder="0" width="100%" height="450px" src="https://graphite.rs/bezier-rs-demos#subpath/outline/solo" title="Outline Demo"></iframe>
|
||||
pub fn outline(&self, distance: f64, join: Join, cap: Cap) -> (Subpath<ManipulatorGroupId>, Option<Subpath<ManipulatorGroupId>>) {
|
||||
let is_point = self.is_point();
|
||||
let (pos_offset, neg_offset) = if is_point {
|
||||
let point = self.manipulator_groups[0].anchor;
|
||||
(
|
||||
Subpath::new(vec![ManipulatorGroup::new_anchor(point + DVec2::NEG_Y * distance)], false),
|
||||
Subpath::new(vec![ManipulatorGroup::new_anchor(point + DVec2::Y * distance)], false),
|
||||
)
|
||||
} else {
|
||||
(self.offset(distance, join), self.reverse().offset(distance, join))
|
||||
};
|
||||
|
||||
if self.closed {
|
||||
if self.closed && !is_point {
|
||||
return (pos_offset, Some(neg_offset));
|
||||
}
|
||||
|
||||
match joint {
|
||||
Joint::Bevel => {
|
||||
pos_offset.manipulator_groups.append(&mut neg_offset.manipulator_groups);
|
||||
pos_offset.closed = true;
|
||||
(pos_offset, None)
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
(pos_offset.combine_outline(&neg_offset, cap), None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ManipulatorGroup, Subpath};
|
||||
use super::{Cap, Join, ManipulatorGroup, Subpath};
|
||||
use crate::compare::{compare_points, compare_subpaths, compare_vec_of_points};
|
||||
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use crate::utils::{SubpathTValue, TValue};
|
||||
@@ -509,6 +580,43 @@ mod tests {
|
||||
subpath
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_with_single_point_segment() {
|
||||
let subpath = Subpath::new(
|
||||
vec![
|
||||
ManipulatorGroup {
|
||||
anchor: DVec2::new(20., 20.),
|
||||
out_handle: Some(DVec2::new(10., 90.)),
|
||||
in_handle: None,
|
||||
id: EmptyId,
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: DVec2::new(150., 40.),
|
||||
out_handle: None,
|
||||
in_handle: Some(DVec2::new(60., 40.)),
|
||||
id: EmptyId,
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: DVec2::new(150., 40.),
|
||||
out_handle: Some(DVec2::new(40., 120.)),
|
||||
in_handle: None,
|
||||
id: EmptyId,
|
||||
},
|
||||
ManipulatorGroup {
|
||||
anchor: DVec2::new(100., 100.),
|
||||
out_handle: None,
|
||||
in_handle: None,
|
||||
id: EmptyId,
|
||||
},
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
let outline = subpath.outline(10., crate::Join::Round, crate::Cap::Round).0;
|
||||
assert!(outline.manipulator_groups.windows(2).all(|pair| !pair[0].anchor.abs_diff_eq(pair[1].anchor, MAX_ABSOLUTE_DIFFERENCE)));
|
||||
assert_eq!(outline.closed(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_an_open_subpath() {
|
||||
let subpath = set_up_open_subpath();
|
||||
@@ -628,9 +736,15 @@ mod tests {
|
||||
let result = temporary.reverse();
|
||||
let end = result.len();
|
||||
|
||||
assert_eq!(temporary.manipulator_groups[0].anchor, result.manipulator_groups[end - 1].anchor);
|
||||
assert_eq!(temporary.manipulator_groups[0].in_handle, result.manipulator_groups[end - 1].out_handle);
|
||||
assert_eq!(temporary.manipulator_groups[0].out_handle, result.manipulator_groups[end - 1].in_handle);
|
||||
// Second manipulator group on the temporary subpath should be the reflected version of the last in the result
|
||||
assert_eq!(temporary.manipulator_groups[1].anchor, result.manipulator_groups[end - 1].anchor);
|
||||
assert_eq!(temporary.manipulator_groups[1].in_handle, result.manipulator_groups[end - 1].out_handle);
|
||||
assert_eq!(temporary.manipulator_groups[1].out_handle, result.manipulator_groups[end - 1].in_handle);
|
||||
|
||||
// The first manipulator group in both should be the reflected versions of each other
|
||||
assert_eq!(temporary.manipulator_groups[0].anchor, result.manipulator_groups[0].anchor);
|
||||
assert_eq!(temporary.manipulator_groups[0].in_handle, result.manipulator_groups[0].out_handle);
|
||||
assert_eq!(temporary.manipulator_groups[0].out_handle, result.manipulator_groups[0].in_handle);
|
||||
assert_eq!(subpath, result);
|
||||
}
|
||||
|
||||
@@ -907,4 +1021,46 @@ mod tests {
|
||||
assert!(result.manipulator_groups[0].out_handle.is_none());
|
||||
assert_eq!(result.manipulator_groups.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_single_point_circle() {
|
||||
let ellipse: Subpath<EmptyId> = Subpath::new_ellipse(DVec2::new(0., 0.), DVec2::new(50., 50.)).reverse();
|
||||
let p = DVec2::new(25., 25.);
|
||||
|
||||
let subpath: Subpath<EmptyId> = Subpath::from_anchors([p, p, p], false);
|
||||
let outline_open = subpath.outline(25., Join::Bevel, Cap::Round);
|
||||
assert_eq!(outline_open.0, ellipse);
|
||||
assert_eq!(outline_open.1, None);
|
||||
|
||||
let subpath_closed: Subpath<EmptyId> = Subpath::from_anchors([p, p, p], true);
|
||||
let outline_closed = subpath_closed.outline(25., Join::Bevel, Cap::Round);
|
||||
assert_eq!(outline_closed.0, ellipse);
|
||||
assert_eq!(outline_closed.1, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outline_single_point_square() {
|
||||
let square: Subpath<EmptyId> = Subpath::from_anchors(
|
||||
[
|
||||
DVec2::new(25., 0.),
|
||||
DVec2::new(0., 0.),
|
||||
DVec2::new(0., 50.),
|
||||
DVec2::new(25., 50.),
|
||||
DVec2::new(50., 50.),
|
||||
DVec2::new(50., 0.),
|
||||
],
|
||||
true,
|
||||
);
|
||||
let p = DVec2::new(25., 25.);
|
||||
|
||||
let subpath: Subpath<EmptyId> = Subpath::from_anchors([p, p, p], false);
|
||||
let outline_open = subpath.outline(25., Join::Bevel, Cap::Square);
|
||||
assert_eq!(outline_open.0, square);
|
||||
assert_eq!(outline_open.1, None);
|
||||
|
||||
let subpath_closed: Subpath<EmptyId> = Subpath::from_anchors([p, p, p], true);
|
||||
let outline_closed = subpath_closed.outline(25., Join::Bevel, Cap::Square);
|
||||
assert_eq!(outline_closed.0, square);
|
||||
assert_eq!(outline_closed.1, None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user