//! Defines the `PathSegment` enum and related functionality for representing and //! manipulating path segments in 2D space. //! //! This module provides implementations for various types of path segments including //! lines, cubic and quadratic Bézier curves, and elliptical arcs. It also includes //! utility functions for operations such as bounding box calculation, segment splitting, //! and arc-to-cubic conversion. //! //! The implementations in this module closely follow the SVG path specification, //! making it suitable for use in vector graphics applications. use crate::EPS; use crate::aabb::{Aabb, bounding_box_around_point, expand_bounding_box, extend_bounding_box, merge_bounding_boxes}; use crate::math::{lerp, vector_angle}; use glam::{DMat2, DMat3, DVec2}; use std::f64::consts::{PI, TAU}; /// Represents a segment of a path in a 2D space, based on the SVG path specification. /// /// This enum closely follows the path segment types defined in the SVG 2 specification. /// For more details, see: /// /// Each variant of this enum corresponds to a different type of path segment: /// - Line: A straight line between two points. /// - Cubic: A cubic Bézier curve. /// - Quadratic: A quadratic Bézier curve. /// - Arc: An elliptical arc. /// /// # Examples /// /// Creating a line segment: /// ``` /// use path_bool::PathSegment; /// use glam::DVec2; /// /// let line = PathSegment::Line(DVec2::new(0., 0.), DVec2::new(1., 1.)); /// ``` /// /// Creating a cubic Bézier curve: /// ``` /// use path_bool::PathSegment; /// use glam::DVec2; /// /// let cubic = PathSegment::Cubic( /// DVec2::new(0., 0.), /// DVec2::new(1., 0.), /// DVec2::new(1., 1.), /// DVec2::new(2., 1.) /// ); /// ``` #[derive(Clone, Copy, Debug, PartialEq)] pub enum PathSegment { /// A line segment from the first point to the second. /// Corresponds to the SVG "L" command. Line(DVec2, DVec2), /// A cubic Bézier curve with start point, two control points, and end point. /// Corresponds to the SVG "C" command. Cubic(DVec2, DVec2, DVec2, DVec2), /// A quadratic Bézier curve with start point, control point, and end point. /// Corresponds to the SVG "Q" command. Quadratic(DVec2, DVec2, DVec2), /// An elliptical arc. /// Corresponds to the SVG "A" command. /// /// Parameters: /// - Start point /// - X-axis radius /// - Y-axis radius /// - X-axis rotation (in radians) /// - Large arc flag (true if the arc should be greater than or equal to 180 degrees) /// - Sweep flag (true if the arc should be drawn in a "positive-angle" direction) /// - End point Arc(DVec2, f64, f64, f64, bool, bool, DVec2), } impl PathSegment { /// Calculates the angle of the tangent at the start point of the segment. /// /// This method computes the angle (in radians) of the tangent vector at the /// beginning of the path segment. The angle is measured clockwise /// from the positive x-axis. /// /// # Returns /// /// A float representing the angle in radians, normalized to the range [0, 2π). /// /// # Examples /// /// ``` /// use path_bool::PathSegment; /// use glam::DVec2; /// use std::f64::consts::{TAU, FRAC_PI_4}; /// /// let line = PathSegment::Line(DVec2::new(0., 0.), DVec2::new(1., 1.)); /// assert_eq!(line.start_angle(), TAU - (FRAC_PI_4)); /// ``` pub fn start_angle(&self) -> f64 { let angle = match *self { PathSegment::Line(start, end) => (end - start).angle_to(DVec2::X), PathSegment::Cubic(start, control1, control2, _) => { let diff = control1 - start; if diff.abs_diff_eq(DVec2::ZERO, EPS.point) { // if this diff were empty too, the segments would have been converted to a line (control2 - start).angle_to(DVec2::X) } else { diff.angle_to(DVec2::X) } } // Apply same logic as for cubic bezier PathSegment::Quadratic(start, control, _) => (control - start).to_angle(), PathSegment::Arc(..) => self.arc_segment_to_cubics(0.001)[0].start_angle(), }; use std::f64::consts::TAU; (angle + TAU) % TAU } /// Computes the curvature at the start point of the segment. /// /// The curvature is a measure of how sharply a curve bends. A straight line /// has a curvature of 0, while a tight curve has a higher curvature value. /// /// # Returns /// /// A float representing the curvature. Positive values indicate a left /// curve, while negative values indicate a right curve. /// /// # Examples /// /// ``` /// use path_bool::PathSegment; /// use glam::DVec2; /// /// let line = PathSegment::Line(DVec2::new(0., 0.), DVec2::new(1., 1.)); /// assert_eq!(line.start_curvature(), 0.); /// /// let curve = PathSegment::Cubic( /// DVec2::new(0., 0.), /// DVec2::new(0., 1.), /// DVec2::new(1., 1.), /// DVec2::new(1., 0.) /// ); /// assert!(curve.start_curvature() < 0.); /// ``` pub fn start_curvature(&self) -> f64 { match *self { PathSegment::Line(_, _) => 0., PathSegment::Cubic(start, control1, control2, _) => { let a = control1 - start; let a = 3. * a; let b = start - 2. * control1 + control2; let b = 6. * b; let numerator = a.x * b.y - a.y * b.x; let denominator = a.length_squared() * a.length(); if denominator == 0. { 0. } else { numerator / denominator } } PathSegment::Quadratic(start, control, end) => { // First derivative let a = 2. * (control - start); // Second derivative let b = 2. * (start - 2. * control + end); let numerator = a.x * b.y - a.y * b.x; let denominator = a.length_squared() * a.length(); if denominator == 0. { 0. } else { numerator / denominator } } PathSegment::Arc(..) => self.arc_segment_to_cubics(0.001)[0].start_curvature(), } } /// Converts the segment to a cubic Bézier curve representation. /// /// This method provides a uniform representation of all segment types as /// cubic Bézier curves. For segments that are not naturally cubic Bézier /// curves (like lines or quadratic Bézier curves), an equivalent cubic /// Bézier representation is computed. /// /// # Returns /// /// An array of four `DVec2` points representing the cubic Bézier curve: /// [start point, first control point, second control point, end point] /// /// # Examples /// /// ``` /// use path_bool::PathSegment; /// use glam::DVec2; /// /// let line = PathSegment::Line(DVec2::new(0., 0.), DVec2::new(1., 1.)); /// let cubic = line.to_cubic(); /// assert_eq!(cubic[0], DVec2::new(0., 0.)); /// assert_eq!(cubic[3], DVec2::new(1., 1.)); /// ``` /// /// # Panics /// /// This method is not implemented for `PathSegment::Arc`. Attempting to call /// `to_cubic()` on an `Arc` segment will result in a panic. pub fn to_cubic(&self) -> [DVec2; 4] { match *self { PathSegment::Line(start, end) => [start, start, end, end], PathSegment::Cubic(s, c1, c2, e) => [s, c1, c2, e], PathSegment::Quadratic(start, control, end) => { // C0 = Q0 // C1 = Q0 + (2/3) (Q1 - Q0) // C2 = Q2 + (2/3) (Q1 - Q2) // C3 = Q2 let d1 = control - start; let d2 = control - end; [start, start + (2. / 3.) * d1, end + (2. / 3.) * d2, end] } PathSegment::Arc(..) => unimplemented!(), } } #[must_use] /// Retrieves the start point of a path segment. pub fn start(&self) -> DVec2 { match self { PathSegment::Line(start, _) => *start, PathSegment::Cubic(start, _, _, _) => *start, PathSegment::Quadratic(start, _, _) => *start, PathSegment::Arc(start, _, _, _, _, _, _) => *start, } } #[must_use] /// Retrieves the end point of a path segment. pub fn end(&self) -> DVec2 { match self { PathSegment::Line(_, end) => *end, PathSegment::Cubic(_, _, _, end) => *end, PathSegment::Quadratic(_, _, end) => *end, PathSegment::Arc(_, _, _, _, _, _, end) => *end, } } #[must_use] /// Reverses the direction of the path segment. /// /// This method creates a new `PathSegment` that represents the same geometric shape /// but in the opposite direction. /// /// # Examples /// /// ``` /// use path_bool::PathSegment; /// use glam::DVec2; /// /// let line = PathSegment::Line(DVec2::new(0., 0.), DVec2::new(1., 1.)); /// let reversed = line.reverse(); /// assert_eq!(reversed.start(), DVec2::new(1., 1.)); /// assert_eq!(reversed.end(), DVec2::new(0., 0.)); /// ``` pub fn reverse(&self) -> PathSegment { match *self { PathSegment::Line(start, end) => PathSegment::Line(end, start), PathSegment::Cubic(p1, p2, p3, p4) => PathSegment::Cubic(p4, p3, p2, p1), PathSegment::Quadratic(p1, p2, p3) => PathSegment::Quadratic(p3, p2, p1), PathSegment::Arc(start, rx, ry, phi, fa, fs, end) => PathSegment::Arc(end, rx, ry, phi, fa, !fs, start), } } #[must_use] /// Converts an arc segment to its center parameterization. /// /// This method is only meaningful for `Arc` segments. For other segment types, /// it returns `None`. /// /// # Returns /// /// An `Option` containing `PathArcSegmentCenterParametrization` if the segment /// is an `Arc`, or `None` otherwise. pub fn arc_segment_to_center(&self) -> Option { if let PathSegment::Arc(xy1, rx, ry, phi, fa, fs, xy2) = *self { if rx == 0. || ry == 0. { return None; } let rotation_matrix = DMat2::from_angle(-phi.to_radians()); let xy1_prime = rotation_matrix * (xy1 - xy2) * 0.5; let mut rx2 = rx * rx; let mut ry2 = ry * ry; let x1_prime2 = xy1_prime.x * xy1_prime.x; let y1_prime2 = xy1_prime.y * xy1_prime.y; let mut rx = rx.abs(); let mut ry = ry.abs(); let lambda = x1_prime2 / rx2 + y1_prime2 / ry2 + 1e-12; if lambda > 1. { let lambda_sqrt = lambda.sqrt(); rx *= lambda_sqrt; ry *= lambda_sqrt; let lambda_abs = lambda.abs(); rx2 *= lambda_abs; ry2 *= lambda_abs; } let sign = if fa == fs { -1. } else { 1. }; let multiplier = ((rx2 * ry2 - rx2 * y1_prime2 - ry2 * x1_prime2) / (rx2 * y1_prime2 + ry2 * x1_prime2)).sqrt(); let cx_prime = sign * multiplier * ((rx * xy1_prime.y) / ry); let cy_prime = sign * multiplier * ((-ry * xy1_prime.x) / rx); let cxy = rotation_matrix.transpose() * DVec2::new(cx_prime, cy_prime) + (xy1 + xy2) * 0.5; let vec1 = DVec2::new((xy1_prime.x - cx_prime) / rx, (xy1_prime.y - cy_prime) / ry); let theta1 = vector_angle(DVec2::new(1., 0.), vec1); let mut delta_theta = vector_angle(vec1, DVec2::new((-xy1_prime.x - cx_prime) / rx, (-xy1_prime.y - cy_prime) / ry)); if !fs && delta_theta > 0. { delta_theta -= TAU; } else if fs && delta_theta < 0. { delta_theta += TAU; } Some(PathArcSegmentCenterParametrization { center: cxy, theta1, delta_theta, rx, ry, phi, }) } else { None } } #[must_use] /// Samples a point on the path segment at a given parameter value. /// /// # Arguments /// /// * `t` - A value between 0. and 1. representing the position along the segment. /// /// # Examples /// /// ``` /// use path_bool::PathSegment; /// use glam::DVec2; /// /// let line = PathSegment::Line(DVec2::new(0., 0.), DVec2::new(2., 2.)); /// assert_eq!(line.sample_at(0.5), DVec2::new(1., 1.)); /// ``` pub fn sample_at(&self, t: f64) -> DVec2 { match *self { PathSegment::Line(start, end) => start.lerp(end, t), PathSegment::Cubic(p1, p2, p3, p4) => { let p01 = p1.lerp(p2, t); let p12 = p2.lerp(p3, t); let p23 = p3.lerp(p4, t); let p012 = p01.lerp(p12, t); let p123 = p12.lerp(p23, t); p012.lerp(p123, t) } PathSegment::Quadratic(p1, p2, p3) => { let p01 = p1.lerp(p2, t); let p12 = p2.lerp(p3, t); p01.lerp(p12, t) } PathSegment::Arc(start, rx, ry, phi, _, _, end) => { if let Some(center_param) = self.arc_segment_to_center() { let theta = center_param.theta1 + t * center_param.delta_theta; let p = DVec2::new(rx * theta.cos(), ry * theta.sin()); let rotation_matrix = DMat2::from_angle(phi); rotation_matrix * p + center_param.center } else { start.lerp(end, t) } } } } #[must_use] /// Approximates an arc segment with a series of cubic Bézier curves. /// /// This method is primarily used for `Arc` segments, converting them into /// a series of cubic Bézier curves for easier rendering or manipulation. /// For non-`Arc` segments, it returns a vector containing only the original segment. /// /// # Arguments /// /// * `max_delta_theta` - The maximum angle (in radians) that each cubic Bézier /// curve approximation should span. /// /// # Returns /// /// A vector of `PathSegment::Cubic` approximating the original segment. pub fn arc_segment_to_cubics(&self, max_delta_theta: f64) -> Vec { if let PathSegment::Arc(start, rx, ry, phi, _, _, end) = *self { if let Some(center_param) = self.arc_segment_to_center() { let count = ((center_param.delta_theta.abs() / max_delta_theta).ceil() as usize).max(1); let from_unit = DMat3::from_translation(center_param.center) * DMat3::from_angle(phi.to_radians()) * DMat3::from_scale(DVec2::new(rx, ry)); let theta = center_param.delta_theta / count as f64; let k = (4. / 3.) * (theta / 4.).tan(); let sin_theta = theta.sin(); let cos_theta = theta.cos(); (0..count) .map(|i| { let start = DVec2::new(1., 0.); let control1 = DVec2::new(1., k); let control2 = DVec2::new(cos_theta + k * sin_theta, sin_theta - k * cos_theta); let end = DVec2::new(cos_theta, sin_theta); let matrix = DMat3::from_angle(center_param.theta1 + i as f64 * theta) * from_unit; let start = (matrix * start.extend(1.)).truncate(); let control1 = (matrix * control1.extend(1.)).truncate(); let control2 = (matrix * control2.extend(1.)).truncate(); let end = (matrix * end.extend(1.)).truncate(); PathSegment::Cubic(start, control1, control2, end) }) .collect() } else { vec![PathSegment::Line(start, end)] } } else { vec![*self] } } } /// Represents the center parameterization of an elliptical arc. /// /// This struct is used internally to perform calculations on arc segments. pub struct PathArcSegmentCenterParametrization { center: DVec2, theta1: f64, delta_theta: f64, rx: f64, ry: f64, phi: f64, } /// Converts the center parameterization back to an arc segment. /// /// # Arguments /// /// * `start` - Optional start point of the arc. If `None`, the start point is calculated. /// * `end` - Optional end point of the arc. If `None`, the end point is calculated. /// /// # Returns /// /// A `PathSegment::Arc` representing the arc described by this parameterization. impl PathArcSegmentCenterParametrization { #[must_use] pub fn arc_segment_from_center(&self, start: Option, end: Option) -> PathSegment { let rotation_matrix = DMat2::from_angle(self.phi); let mut xy1 = rotation_matrix * DVec2::new(self.rx * self.theta1.cos(), self.ry * self.theta1.sin()) + self.center; let mut xy2 = rotation_matrix * DVec2::new(self.rx * (self.theta1 + self.delta_theta).cos(), self.ry * (self.theta1 + self.delta_theta).sin()) + self.center; let fa = self.delta_theta.abs() > PI; let fs = self.delta_theta > 0.; xy1 = start.unwrap_or(xy1); xy2 = end.unwrap_or(xy2); PathSegment::Arc(xy1, self.rx, self.ry, self.phi, fa, fs, xy2) } } /// Evaluates a 1D cubic Bézier curve at a given parameter value. /// /// # Arguments /// /// * `p0`, `p1`, `p2`, `p3` - Control points of the cubic Bézier curve. /// * `t` - Parameter value between 0 and 1. /// /// # Returns /// /// The value of the Bézier curve at parameter `t`. fn eval_cubic_1d(p0: f64, p1: f64, p2: f64, p3: f64, t: f64) -> f64 { let p01 = lerp(p0, p1, t); let p12 = lerp(p1, p2, t); let p23 = lerp(p2, p3, t); let p012 = lerp(p01, p12, t); let p123 = lerp(p12, p23, t); lerp(p012, p123, t) } /// Computes the bounding interval of a 1D cubic Bézier curve. /// /// This function finds the minimum and maximum values of a cubic Bézier curve /// over the interval [0, 1]. /// /// # Arguments /// /// * `p0`, `p1`, `p2`, `p3` - Control points of the cubic Bézier curve. /// /// # Returns /// /// A tuple `(min, max)` representing the bounding interval. fn cubic_bounding_interval(p0: f64, p1: f64, p2: f64, p3: f64) -> (f64, f64) { let mut min = p0.min(p3); let mut max = p0.max(p3); let a = 3. * (-p0 + 3. * p1 - 3. * p2 + p3); let b = 6. * (p0 - 2. * p1 + p2); let c = 3. * (p1 - p0); let d = b * b - 4. * a * c; if d < 0. || a == 0. { // TODO: if a=0, solve linear return (min, max); } let sqrt_d = d.sqrt(); let t0 = (-b - sqrt_d) / (2. * a); if 0. < t0 && t0 < 1. { let x0 = eval_cubic_1d(p0, p1, p2, p3, t0); min = min.min(x0); max = max.max(x0); } let t1 = (-b + sqrt_d) / (2. * a); if 0. < t1 && t1 < 1. { let x1 = eval_cubic_1d(p0, p1, p2, p3, t1); min = min.min(x1); max = max.max(x1); } (min, max) } /// Evaluates a 1D quadratic Bézier curve at a given parameter value. /// /// # Arguments /// /// * `p0`, `p1`, `p2` - Control points of the quadratic Bézier curve. /// * `t` - Parameter value between 0 and 1. /// /// # Returns /// /// The value of the Bézier curve at parameter `t`. fn eval_quadratic_1d(p0: f64, p1: f64, p2: f64, t: f64) -> f64 { let p01 = lerp(p0, p1, t); let p12 = lerp(p1, p2, t); lerp(p01, p12, t) } /// Computes the bounding interval of a 1D quadratic Bézier curve. /// /// This function finds the minimum and maximum values of a quadratic Bézier curve /// over the interval [0, 1]. /// /// # Arguments /// /// * `p0`, `p1`, `p2` - Control points of the quadratic Bézier curve. /// /// # Returns /// /// A tuple `(min, max)` representing the bounding interval. fn quadratic_bounding_interval(p0: f64, p1: f64, p2: f64) -> (f64, f64) { let mut min = p0.min(p2); let mut max = p0.max(p2); let denominator = p0 - 2. * p1 + p2; if denominator == 0. { return (min, max); } let t = (p0 - p1) / denominator; if (0.0..=1.).contains(&t) { let x = eval_quadratic_1d(p0, p1, p2, t); min = min.min(x); max = max.max(x); } (min, max) } fn in_interval(x: f64, x0: f64, x1: f64) -> bool { (x0..=x1).contains(&x) } impl PathSegment { /// Computes the bounding box of the path segment. /// /// # Returns /// /// An [`Aabb`] representing the axis-aligned bounding box of the segment. pub(crate) fn bounding_box(&self) -> Aabb { match *self { PathSegment::Line(start, end) => Aabb { top: start.y.min(end.y), right: start.x.max(end.x), bottom: start.y.max(end.y), left: start.x.min(end.x), }, PathSegment::Cubic(p1, p2, p3, p4) => { let (left, right) = cubic_bounding_interval(p1.x, p2.x, p3.x, p4.x); let (top, bottom) = cubic_bounding_interval(p1.y, p2.y, p3.y, p4.y); Aabb { top, right, bottom, left } } PathSegment::Quadratic(p1, p2, p3) => { let (left, right) = quadratic_bounding_interval(p1.x, p2.x, p3.x); let (top, bottom) = quadratic_bounding_interval(p1.y, p2.y, p3.y); Aabb { top, right, bottom, left } } PathSegment::Arc(start, rx, ry, phi, _, _, end) => { if let Some(center_param) = self.arc_segment_to_center() { let theta2 = center_param.theta1 + center_param.delta_theta; let mut bounding_box = extend_bounding_box(Some(bounding_box_around_point(start, 0.)), end); if phi == 0. || rx == ry { // TODO: Fix the fact that the following gives false positives, resulting in larger boxes if in_interval(-PI, center_param.theta1, theta2) || in_interval(PI, center_param.theta1, theta2) { bounding_box = extend_bounding_box(Some(bounding_box), DVec2::new(center_param.center.x - rx, center_param.center.y)); } if in_interval(-PI / 2., center_param.theta1, theta2) || in_interval(3. * PI / 2., center_param.theta1, theta2) { bounding_box = extend_bounding_box(Some(bounding_box), DVec2::new(center_param.center.x, center_param.center.y - ry)); } if in_interval(0., center_param.theta1, theta2) || in_interval(2. * PI, center_param.theta1, theta2) { bounding_box = extend_bounding_box(Some(bounding_box), DVec2::new(center_param.center.x + rx, center_param.center.y)); } if in_interval(PI / 2., center_param.theta1, theta2) || in_interval(5. * PI / 2., center_param.theta1, theta2) { bounding_box = extend_bounding_box(Some(bounding_box), DVec2::new(center_param.center.x, center_param.center.y + ry)); } expand_bounding_box(&bounding_box, 1e-11) // TODO: Get rid of expansion } else { // TODO: Don't convert to cubics let cubics = self.arc_segment_to_cubics(PI / 16.); let mut bounding_box = None; for cubic_seg in cubics { bounding_box = Some(merge_bounding_boxes(bounding_box, &cubic_seg.bounding_box())); } bounding_box.unwrap_or_else(|| bounding_box_around_point(start, 0.)) } } else { extend_bounding_box(Some(bounding_box_around_point(start, 0.)), end) } } } } /// Splits the path segment at a given parameter value. /// /// # Arguments /// /// * `t` - A value between 0. and 1. representing the split point along the segment. /// /// # Returns /// /// A tuple of two `PathSegment`s representing the parts before and after the split point. /// /// # Examples /// /// ``` /// use path_bool::PathSegment; /// use glam::DVec2; /// /// let line = PathSegment::Line(DVec2::new(0., 0.), DVec2::new(2., 2.)); /// let (first_half, second_half) = line.split_at(0.5); /// assert_eq!(first_half.end(), DVec2::new(1., 1.)); /// assert_eq!(second_half.start(), DVec2::new(1., 1.)); /// ``` pub fn split_at(&self, t: f64) -> (PathSegment, PathSegment) { match *self { PathSegment::Line(start, end) => { let p = start.lerp(end, t); (PathSegment::Line(start, p), PathSegment::Line(p, end)) } PathSegment::Cubic(p0, p1, p2, p3) => { let p01 = p0.lerp(p1, t); let p12 = p1.lerp(p2, t); let p23 = p2.lerp(p3, t); let p012 = p01.lerp(p12, t); let p123 = p12.lerp(p23, t); let p = p012.lerp(p123, t); (PathSegment::Cubic(p0, p01, p012, p), PathSegment::Cubic(p, p123, p23, p3)) } PathSegment::Quadratic(p0, p1, p2) => { let p01 = p0.lerp(p1, t); let p12 = p1.lerp(p2, t); let p = p01.lerp(p12, t); (PathSegment::Quadratic(p0, p01, p), PathSegment::Quadratic(p, p12, p2)) } PathSegment::Arc(start, _, _, _, _, _, end) => { if let Some(center_param) = self.arc_segment_to_center() { let mid_delta_theta = center_param.delta_theta * t; let seg1 = PathArcSegmentCenterParametrization { delta_theta: mid_delta_theta, ..center_param } .arc_segment_from_center(Some(start), None); let seg2 = PathArcSegmentCenterParametrization { theta1: center_param.theta1 + mid_delta_theta, delta_theta: center_param.delta_theta - mid_delta_theta, ..center_param } .arc_segment_from_center(None, Some(end)); (seg1, seg2) } else { // https://svgwg.org/svg2-draft/implnote.html#ArcCorrectionOutOfRangeRadii let p = start.lerp(end, t); (PathSegment::Line(start, p), PathSegment::Line(p, end)) } } } } }