[librsvg: 1/2] Add PathBuilder Arc command



commit ba04ca79a11feddfc629501ca09323de3206e3a2
Author: letheed <letheed outlook com>
Date:   Thu Aug 9 14:47:33 2018 +0200

    Add PathBuilder Arc command

 rsvg_internals/src/marker.rs       |  45 +++-
 rsvg_internals/src/path_builder.rs | 494 +++++++++++++++++++++----------------
 rsvg_internals/src/path_parser.rs  |   6 +-
 3 files changed, 329 insertions(+), 216 deletions(-)
---
diff --git a/rsvg_internals/src/marker.rs b/rsvg_internals/src/marker.rs
index 88b6ca1b..ce466c29 100644
--- a/rsvg_internals/src/marker.rs
+++ b/rsvg_internals/src/marker.rs
@@ -381,15 +381,54 @@ pub fn path_builder_to_segments(builder: &PathBuilder) -> Vec<Segment> {
                 state = SegmentState::InSubpath;
             }
 
-            PathCommand::CurveTo((x2, y2), (x3, y3), (x4, y4)) => {
-                cur_x = x4;
-                cur_y = y4;
+            PathCommand::CurveTo(curve) => {
+                let CubicBezierCurve {
+                    pt1: (x2, y2),
+                    pt2: (x3, y3),
+                    to,
+                } = curve;
+                cur_x = to.0;
+                cur_y = to.1;
 
                 segments.push(make_curve(last_x, last_y, x2, y2, x3, y3, cur_x, cur_y));
 
                 state = SegmentState::InSubpath;
             }
 
+            PathCommand::Arc(arc) => {
+                cur_x = arc.to.0;
+                cur_y = arc.to.1;
+
+                match arc.center_parameterization() {
+                    ArcParameterization::CenterParameters {
+                        center,
+                        radii,
+                        theta1,
+                        delta_theta,
+                    } => {
+                        let rot = arc.x_axis_rotation;
+                        let theta2 = theta1 + delta_theta;
+                        let n_segs = (delta_theta / (PI * 0.5 + 0.001)).abs().ceil() as u32;
+                        let d_theta = delta_theta / f64::from(n_segs);
+
+                        let segment1 = arc_segment(center, radii, rot, theta1, theta1 + d_theta);
+                        let segment2 = arc_segment(center, radii, rot, theta2 - d_theta, theta2);
+
+                        let (x2, y2) = segment1.pt1;
+                        let (x3, y3) = segment2.pt2;
+                        segments.push(make_curve(last_x, last_y, x2, y2, x3, y3, cur_x, cur_y));
+
+                        state = SegmentState::InSubpath;
+                    }
+                    ArcParameterization::LineTo => {
+                        segments.push(make_line(last_x, last_y, cur_x, cur_y));
+
+                        state = SegmentState::InSubpath;
+                    }
+                    ArcParameterization::Omit => {}
+                }
+            }
+
             PathCommand::ClosePath => {
                 cur_x = subpath_start_x;
                 cur_y = subpath_start_y;
diff --git a/rsvg_internals/src/path_builder.rs b/rsvg_internals/src/path_builder.rs
index 7161b57b..a37b56b2 100644
--- a/rsvg_internals/src/path_builder.rs
+++ b/rsvg_internals/src/path_builder.rs
@@ -15,15 +15,271 @@ pub enum Sweep {
     Positive,
 }
 
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct CubicBezierCurve {
+    /// The (x, y) coordinates of the first control point.
+    pub pt1: (f64, f64),
+    /// The (x, y) coordinates of the second control point.
+    pub pt2: (f64, f64),
+    /// The (x, y) coordinates of the end point of this path segment.
+    pub to: (f64, f64),
+}
+
+impl CubicBezierCurve {
+    fn to_cairo(self, cr: &cairo::Context) {
+        let Self { pt1, pt2, to } = self;
+        cr.curve_to(pt1.0, pt1.1, pt2.0, pt2.1, to.0, to.1);
+    }
+}
+
+/// When attempting to compute the center parameterization of the arc,
+/// out of range parameters may see an arc omited or treated as a line.
+pub enum ArcParameterization {
+    /// Center parameterization of the arc.
+    CenterParameters {
+        /// Center of the ellipse.
+        center: (f64, f64),
+        /// Radii of the ellipse (corrected).
+        radii: (f64, f64),
+        /// Angle of the start point.
+        theta1: f64,
+        /// Delta angle to the end point.
+        delta_theta: f64,
+    },
+    /// Treat the arc as a line to the end point.
+    LineTo,
+    /// Omit the arc.
+    Omit,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct EllipticalArc {
+    /// The (x-axis, y-axis) radii for the ellipse.
+    pub r: (f64, f64),
+    /// The rotation angle in degrees for the ellipse's x-axis
+    /// relative to the x-axis of the user coordinate system.
+    pub x_axis_rotation: f64,
+    /// Flag indicating whether the arc sweep should be
+    /// greater than or equal to 180 degrees, or smaller than 180 degrees.
+    pub large_arc: LargeArc,
+    /// Flag indicating the angular direction in which the arc is drawn.
+    pub sweep: Sweep,
+    /// The (x, y) coordinates for the start point of this path segment.
+    pub from: (f64, f64),
+    /// The (x, y) coordinates for the end point of this path segment.
+    pub to: (f64, f64),
+}
+
+impl EllipticalArc {
+    /// Calculates a center parameterization from the endpoint parameterization.
+    ///
+    /// Radii may be adjusted if there is no solution.
+    ///
+    /// See Appendix F.6 Elliptical arc implementation notes.
+    /// http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
+    pub(crate) fn center_parameterization(self) -> ArcParameterization {
+        let Self {
+            r: (mut rx, mut ry),
+            x_axis_rotation,
+            large_arc,
+            sweep,
+            from: (x1, y1),
+            to: (x2, y2),
+        } = self;
+
+        // If the end points are identical, omit the arc segment entirely.
+        if x1.approx_eq_cairo(&x2) && y1.approx_eq_cairo(&y2) {
+            return ArcParameterization::Omit;
+        }
+
+        // Ensure radii are non-zero.
+        // Otherwise this arc is treated as a line segment joining the end points.
+        //
+        // A bit further down we divide by the square of the radii.
+        // Check that we won't divide by zero.
+        // See http://bugs.debian.org/508443
+        if rx * rx < f64::EPSILON || ry * ry < f64::EPSILON {
+            return ArcParameterization::LineTo;
+        }
+
+        let is_large_arc = large_arc.0;
+        let is_positive_sweep = sweep == Sweep::Positive;
+
+        let f = x_axis_rotation * PI / 180.0;
+        let sinf = f.sin();
+        let cosf = f.cos();
+
+        // Ensure radii are positive.
+        rx = rx.abs();
+        ry = ry.abs();
+
+        // The equations simplify after a translation which places
+        // the origin at the midpoint of the line joining (x1, y1) to (x2, y2),
+        // followed by a rotation to line up the coordinate axes
+        // with the axes of the ellipse.
+        // All transformed coordinates will be written with primes.
+        //
+        // Compute (x1', y1').
+        let mid_x = (x1 - x2) / 2.0;
+        let mid_y = (y1 - y2) / 2.0;
+        let x1_ = cosf * mid_x + sinf * mid_y;
+        let y1_ = -sinf * mid_x + cosf * mid_y;
+
+        // Ensure radii are large enough.
+        let lambda = (x1_ / rx).powi(2) + (y1_ / ry).powi(2);
+        if lambda > 1.0 {
+            // If not, scale up the ellipse uniformly
+            // until there is exactly one solution.
+            rx *= lambda.sqrt();
+            ry *= lambda.sqrt();
+        }
+
+        // Compute the transformed center (cx', cy').
+        let d = (rx * y1_).powi(2) + (ry * x1_).powi(2);
+        if d.approx_eq_cairo(&0.0) {
+            return ArcParameterization::Omit;
+        }
+        let k = {
+            let mut k = ((rx * ry).powi(2) / d - 1.0).abs().sqrt();
+            if is_positive_sweep == is_large_arc {
+                k = -k;
+            }
+            k
+        };
+        let cx_ = k * rx * y1_ / ry;
+        let cy_ = -k * ry * x1_ / rx;
+
+        // Compute the center (cx, cy).
+        let cx = cosf * cx_ - sinf * cy_ + (x1 + x2) / 2.0;
+        let cy = sinf * cx_ + cosf * cy_ + (y1 + y2) / 2.0;
+
+        // Compute the start angle θ1.
+        let ux = (x1_ - cx_) / rx;
+        let uy = (y1_ - cy_) / ry;
+        let u_len = (ux * ux + uy * uy).abs().sqrt();
+        if u_len.approx_eq_cairo(&0.0) {
+            return ArcParameterization::Omit;
+        }
+        let cos_theta1 = clamp(ux / u_len, -1.0, 1.0);
+        let theta1 = {
+            let mut theta1 = cos_theta1.acos();
+            if uy < 0.0 {
+                theta1 = -theta1;
+            }
+            theta1
+        };
+
+        // Compute the total delta angle Δθ.
+        let vx = (-x1_ - cx_) / rx;
+        let vy = (-y1_ - cy_) / ry;
+        let v_len = (vx * vx + vy * vy).abs().sqrt();
+        if v_len.approx_eq_cairo(&0.0) {
+            return ArcParameterization::Omit;
+        }
+        let dp_uv = ux * vx + uy * vy;
+        let cos_delta_theta = clamp(dp_uv / (u_len * v_len), -1.0, 1.0);
+        let delta_theta = {
+            let mut delta_theta = cos_delta_theta.acos();
+            if ux * vy - uy * vx < 0.0 {
+                delta_theta = -delta_theta;
+            }
+            if is_positive_sweep && delta_theta < 0.0 {
+                delta_theta += PI * 2.0;
+            } else if !is_positive_sweep && delta_theta > 0.0 {
+                delta_theta -= PI * 2.0;
+            }
+            delta_theta
+        };
+
+        ArcParameterization::CenterParameters {
+            center: (cx, cy),
+            radii: (rx, ry),
+            theta1,
+            delta_theta,
+        }
+    }
+
+    fn to_cairo(self, cr: &cairo::Context) {
+        match self.center_parameterization() {
+            ArcParameterization::CenterParameters {
+                center,
+                radii,
+                theta1,
+                delta_theta,
+            } => {
+                let n_segs = (delta_theta / (PI * 0.5 + 0.001)).abs().ceil() as u32;
+                let d_theta = delta_theta / f64::from(n_segs);
+
+                let mut theta = theta1;
+                for _ in 0..n_segs {
+                    arc_segment(center, radii, self.x_axis_rotation, theta, theta + d_theta)
+                        .to_cairo(cr);
+                    theta += d_theta;
+                }
+            }
+            ArcParameterization::LineTo => {
+                let (x2, y2) = self.to;
+                cr.line_to(x2, y2);
+            }
+            ArcParameterization::Omit => {}
+        }
+    }
+}
+
+/// Turns an arc segment into a cubic bezier curve.
+///
+/// Takes the center, the radii and the x-axis rotation of the ellipse,
+/// the angles of the start and end points,
+/// and returns cubic bezier curve parameters.
+pub(crate) fn arc_segment(
+    c: (f64, f64),
+    r: (f64, f64),
+    x_axis_rotation: f64,
+    th0: f64,
+    th1: f64,
+) -> CubicBezierCurve {
+    let (cx, cy) = c;
+    let (rx, ry) = r;
+    let f = x_axis_rotation * PI / 180.0;
+    let (sinf, cosf) = (f.sin(), f.cos());
+
+    let th_half = 0.5 * (th1 - th0);
+    let t = (8.0 / 3.0) * (th_half * 0.5).sin().powi(2) / th_half.sin();
+    let x1 = rx * (th0.cos() - t * th0.sin());
+    let y1 = ry * (th0.sin() + t * th0.cos());
+    let x3 = rx * th1.cos();
+    let y3 = ry * th1.sin();
+    let x2 = x3 + rx * (t * th1.sin());
+    let y2 = y3 + ry * (-t * th1.cos());
+
+    CubicBezierCurve {
+        pt1: (cx + cosf * x1 - sinf * y1, cy + sinf * x1 + cosf * y1),
+        pt2: (cx + cosf * x2 - sinf * y2, cy + sinf * x2 + cosf * y2),
+        to: (cx + cosf * x3 - sinf * y3, cy + sinf * x3 + cosf * y3),
+    }
+}
+
 #[derive(Debug, PartialEq)]
 pub enum PathCommand {
     MoveTo(f64, f64),
     LineTo(f64, f64),
-    CurveTo((f64, f64), (f64, f64), (f64, f64)), /* (x2, y2), (x3, y3), (x4, y4) <- this is
-                                                  * the destination point */
+    CurveTo(CubicBezierCurve),
+    Arc(EllipticalArc),
     ClosePath,
 }
 
+impl PathCommand {
+    fn to_cairo(&self, cr: &cairo::Context) {
+        match *self {
+            PathCommand::MoveTo(x, y) => cr.move_to(x, y),
+            PathCommand::LineTo(x, y) => cr.line_to(x, y),
+            PathCommand::CurveTo(curve) => curve.to_cairo(cr),
+            PathCommand::Arc(arc) => arc.to_cairo(cr),
+            PathCommand::ClosePath => cr.close_path(),
+        }
+    }
+}
+
 pub struct PathBuilder {
     path_commands: Vec<PathCommand>,
 }
@@ -36,28 +292,6 @@ impl Default for PathBuilder {
     }
 }
 
-impl PathCommand {
-    fn to_cairo(&self, cr: &cairo::Context) {
-        match *self {
-            PathCommand::MoveTo(x, y) => {
-                cr.move_to(x, y);
-            }
-
-            PathCommand::LineTo(x, y) => {
-                cr.line_to(x, y);
-            }
-
-            PathCommand::CurveTo((x2, y2), (x3, y3), (x4, y4)) => {
-                cr.curve_to(x2, y2, x3, y3, x4, y4);
-            }
-
-            PathCommand::ClosePath => {
-                cr.close_path();
-            }
-        }
-    }
-}
-
 impl PathBuilder {
     pub fn new() -> PathBuilder {
         PathBuilder::default()
@@ -72,208 +306,43 @@ impl PathBuilder {
     }
 
     pub fn curve_to(&mut self, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) {
-        self.path_commands
-            .push(PathCommand::CurveTo((x2, y2), (x3, y3), (x4, y4)));
-    }
-
-    pub fn close_path(&mut self) {
-        self.path_commands.push(PathCommand::ClosePath);
+        let curve = CubicBezierCurve {
+            pt1: (x2, y2),
+            pt2: (x3, y3),
+            to: (x4, y4),
+        };
+        self.path_commands.push(PathCommand::CurveTo(curve));
     }
 
-    pub fn get_path_commands(&self) -> &[PathCommand] {
-        &self.path_commands
-    }
-
-    /// * * * * * * * * * * *
-    // x1/y1: starting coordinates
-    // rx/ry: radiuses before rotation
-    // x_axis_rotation: Rotation angle for axes, in degrees
-    // large_arc: false for arc length <= 180, true for arc >= 180
-    // sweep: negative or positive angle
-    // x2/y2: ending coordinates
     pub fn arc(
         &mut self,
         x1: f64,
         y1: f64,
-        mut rx: f64,
-        mut ry: f64,
+        rx: f64,
+        ry: f64,
         x_axis_rotation: f64,
         large_arc: LargeArc,
         sweep: Sweep,
         x2: f64,
         y2: f64,
     ) {
-        // See Appendix F.6 Elliptical arc implementation notes
-        // http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
-        let f: f64;
-        let sinf: f64;
-        let cosf: f64;
-        let x1_: f64;
-        let y1_: f64;
-        let cx_: f64;
-        let cy_: f64;
-        let cx: f64;
-        let cy: f64;
-        let gamma: f64;
-        let mut theta1: f64;
-        let mut delta_theta: f64;
-        let mut k1: f64;
-        let mut k2: f64;
-        let k3: f64;
-        let k4: f64;
-        let mut k5: f64;
-        let n_segs: i32;
-
-        if x1.approx_eq_cairo(&x2) && y1.approx_eq_cairo(&y2) {
-            return;
-        }
-
-        let is_positive_sweep = sweep == Sweep::Positive;
-
-        let is_large_arc = large_arc.0;
-
-        // X-axis
-        f = x_axis_rotation * PI / 180.0;
-        sinf = f.sin();
-        cosf = f.cos();
-
-        rx = rx.abs();
-        ry = ry.abs();
-
-        // A bit further down we divide by the square of the radii.  Check
-        // that we won't divide by zero.
-        // See http://bugs.debian.org/508443
-        if ((rx * rx) < f64::EPSILON) || ((ry * ry) < f64::EPSILON) {
-            self.line_to(x2, y2);
-            return;
-        }
-
-        k1 = (x1 - x2) / 2.0;
-        k2 = (y1 - y2) / 2.0;
-
-        x1_ = cosf * k1 + sinf * k2;
-        y1_ = -sinf * k1 + cosf * k2;
-
-        gamma = (x1_ * x1_) / (rx * rx) + (y1_ * y1_) / (ry * ry);
-        if gamma > 1.0 {
-            rx *= gamma.sqrt();
-            ry *= gamma.sqrt();
-        }
-
-        // Compute the center
-        k1 = rx * rx * y1_ * y1_ + ry * ry * x1_ * x1_;
-        if k1.approx_eq_cairo(&0.0) {
-            return;
-        }
-
-        k1 = ((rx * rx * ry * ry) / k1 - 1.0).abs().sqrt();
-        if is_positive_sweep == is_large_arc {
-            k1 = -k1;
-        }
-
-        cx_ = k1 * rx * y1_ / ry;
-        cy_ = -k1 * ry * x1_ / rx;
-
-        cx = cosf * cx_ - sinf * cy_ + (x1 + x2) / 2.0;
-        cy = sinf * cx_ + cosf * cy_ + (y1 + y2) / 2.0;
-
-        // Compute start angle
-        k1 = (x1_ - cx_) / rx;
-        k2 = (y1_ - cy_) / ry;
-        k3 = (-x1_ - cx_) / rx;
-        k4 = (-y1_ - cy_) / ry;
-
-        k5 = (k1 * k1 + k2 * k2).abs().sqrt();
-        if k5.approx_eq_cairo(&0.0) {
-            return;
-        }
-
-        k5 = k1 / k5;
-        k5 = clamp(k5, -1.0, 1.0);
-        theta1 = k5.acos();
-        if k2 < 0.0 {
-            theta1 = -theta1;
-        }
-
-        // Compute delta_theta
-        k5 = ((k1 * k1 + k2 * k2) * (k3 * k3 + k4 * k4)).abs().sqrt();
-        if k5.approx_eq_cairo(&0.0) {
-            return;
-        }
-
-        k5 = (k1 * k3 + k2 * k4) / k5;
-        k5 = clamp(k5, -1.0, 1.0);
-        delta_theta = k5.acos();
-        if k1 * k4 - k3 * k2 < 0.0 {
-            delta_theta = -delta_theta;
-        }
-
-        if is_positive_sweep && delta_theta < 0.0 {
-            delta_theta += PI * 2.0;
-        } else if !is_positive_sweep && delta_theta > 0.0 {
-            delta_theta -= PI * 2.0;
-        }
+        let arc = EllipticalArc {
+            r: (rx, ry),
+            x_axis_rotation,
+            large_arc,
+            sweep,
+            from: (x1, y1),
+            to: (x2, y2),
+        };
+        self.path_commands.push(PathCommand::Arc(arc));
+    }
 
-        // Now draw the arc
-        n_segs = (delta_theta / (PI * 0.5 + 0.001)).abs().ceil() as i32;
-        let n_segs_dbl = f64::from(n_segs);
-
-        for i in 0..n_segs {
-            self.arc_segment(
-                cx,
-                cy,
-                theta1 + f64::from(i) * delta_theta / n_segs_dbl,
-                theta1 + f64::from(i + 1) * delta_theta / n_segs_dbl,
-                rx,
-                ry,
-                x_axis_rotation,
-            );
-        }
+    pub fn close_path(&mut self) {
+        self.path_commands.push(PathCommand::ClosePath);
     }
 
-    fn arc_segment(
-        &mut self,
-        xc: f64,
-        yc: f64,
-        th0: f64,
-        th1: f64,
-        rx: f64,
-        ry: f64,
-        x_axis_rotation: f64,
-    ) {
-        let x1: f64;
-        let y1: f64;
-        let x2: f64;
-        let y2: f64;
-        let x3: f64;
-        let y3: f64;
-        let t: f64;
-        let th_half: f64;
-        let f: f64;
-        let sinf: f64;
-        let cosf: f64;
-
-        f = x_axis_rotation * PI / 180.0;
-        sinf = f.sin();
-        cosf = f.cos();
-
-        th_half = 0.5 * (th1 - th0);
-        t = (8.0 / 3.0) * (th_half * 0.5).sin() * (th_half * 0.5).sin() / th_half.sin();
-        x1 = rx * (th0.cos() - t * th0.sin());
-        y1 = ry * (th0.sin() + t * th0.cos());
-        x3 = rx * th1.cos();
-        y3 = ry * th1.sin();
-        x2 = x3 + rx * (t * th1.sin());
-        y2 = y3 + ry * (-t * th1.cos());
-
-        self.curve_to(
-            xc + cosf * x1 - sinf * y1,
-            yc + sinf * x1 + cosf * y1,
-            xc + cosf * x2 - sinf * y2,
-            yc + sinf * x2 + cosf * y2,
-            xc + cosf * x3 - sinf * y3,
-            yc + sinf * x3 + cosf * y3,
-        );
+    pub fn get_path_commands(&self) -> &[PathCommand] {
+        &self.path_commands
     }
 
     pub fn to_cairo(&self, cr: &cairo::Context) {
@@ -291,10 +360,11 @@ mod tests {
     fn survives_degenerate_arcs() {
         let mut builder = PathBuilder::new();
         builder.move_to(0.0, 0.0);
+        // Deliberately close to 0 to try to trigger division by 0.
         builder.arc(
             0.0,
             0.0,
-            f64::EPSILON, // deliberately close to 0 to try to trigger division by 0
+            f64::EPSILON,
             f64::EPSILON,
             0.0,
             LargeArc(true),
diff --git a/rsvg_internals/src/path_parser.rs b/rsvg_internals/src/path_parser.rs
index df7bbaef..db715a56 100644
--- a/rsvg_internals/src/path_parser.rs
+++ b/rsvg_internals/src/path_parser.rs
@@ -981,7 +981,11 @@ mod tests {
     }
 
     fn curveto(x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> PathCommand {
-        PathCommand::CurveTo((x2, y2), (x3, y3), (x4, y4))
+        PathCommand::CurveTo(CubicBezierCurve {
+            pt1: (x2, y2),
+            pt2: (x3, y3),
+            to: (x4, y4),
+        })
     }
 
     fn closepath() -> PathCommand {


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]