44//! Computation of offset curves of cubic Béziers.
55//!
66//! The main algorithm in this module is a new technique designed for robustness
7- //! and speed. The details are involved; hopefully there will be a paper. The
8- //! method is not publicly exported aside from stroke expansion, as we probably
9- //! want to iterate on the exact interface.
10- //!
11- //! There is also a slower algorithm based on curve fitting, which is deprecated
12- //! and not currently used in kurbo's stroke expansion methods. See the [Parallel
13- //! curves of cubic Béziers] blog post for a discussion of how this algorithm
14- //! works and what kind of results can be expected. In general, it is expected to
15- //! perform much better than most published algorithms. The number of curve
16- //! segments needed to attain a given accuracy scales as O(n^6) with accuracy.
17- //!
18- //! In this second algorithm, to compute the offset curve (also known as parallel
19- //! curve) of a cubic Bézier segment, create a [`CubicOffset`] struct with the
20- //! curve segment and offset, then use [`fit_to_bezpath`] o
21- //! [`fit_to_bezpath_opt`] depending on how much time to spend optimizing the
22- //! resulting path.
23- //!
24- //! [`fit_to_bezpath`]: crate::fit_to_bezpath
25- //! [`fit_to_bezpath_opt`]: crate::fit_to_bezpath_opt
26- //! [Parallel curves of cubic Béziers]: https://raphlinus.github.io/curves/2022/09/09/parallel-beziers.html
27- use core:: ops:: Range ;
7+ //! and speed. The details are involved; hopefully there will be a paper.
288
299use arrayvec:: ArrayVec ;
3010
@@ -33,159 +13,11 @@ use crate::common::FloatFuncs;
3313
3414use crate :: {
3515 common:: { solve_itp, solve_quadratic} ,
36- BezPath , CubicBez , CurveFitSample , ParamCurve , ParamCurveDeriv , ParamCurveFit , PathSeg , Point ,
37- QuadBez , Vec2 ,
16+ BezPath , CubicBez , ParamCurve , ParamCurveDeriv , PathSeg , Point , QuadBez , Vec2 ,
3817} ;
3918
40- /// The offset curve of a cubic Bézier.
41- ///
42- /// This is a representation of the offset curve of a cubic Bézier segment, for
43- /// purposes of curve fitting.
44- ///
45- /// See the [module-level documentation] for a bit more discussion of the approach,
46- /// and how this struct is to be used.
47- ///
48- /// This struct was formerly used by the stroke expansion logic, but has since been
49- /// replaced with a higher performance implementation not based on generic curve
50- /// fitting. It should be considered deprecated, and may be removed in a future
51- /// version.
52- ///
53- /// [module-level documentation]: crate::offset
54- #[ deprecated(
55- since = "0.11.2" ,
56- note = "stroke expansion internals are no longer public"
57- ) ]
58- pub struct CubicOffset {
59- /// Source curve.
60- c : CubicBez ,
61- /// Derivative of source curve.
62- q : QuadBez ,
63- /// Offset.
64- d : f64 ,
65- // c0 + c1 t + c2 t^2 is the cross product of second and first
66- // derivatives of the underlying cubic, multiplied by offset (for
67- // computing cusp).
68- c0 : f64 ,
69- c1 : f64 ,
70- c2 : f64 ,
71- }
72-
73- #[ expect( deprecated, reason = "implementation still expected to work" ) ]
74- impl CubicOffset {
75- /// Create a new curve from Bézier segment and offset.
76- ///
77- /// This method should only be used if the Bézier is smooth. Use
78- /// [`new_regularized`] instead to deal with a wider range of inputs.
79- ///
80- /// [`new_regularized`]: Self::new_regularized
81- pub fn new ( c : CubicBez , d : f64 ) -> Self {
82- let q = c. deriv ( ) ;
83- let d0 = q. p0 . to_vec2 ( ) ;
84- let d1 = 2.0 * ( q. p1 - q. p0 ) ;
85- let d2 = q. p0 . to_vec2 ( ) - 2.0 * q. p1 . to_vec2 ( ) + q. p2 . to_vec2 ( ) ;
86- CubicOffset {
87- c,
88- q,
89- d,
90- c0 : d * d1. cross ( d0) ,
91- c1 : d * 2.0 * d2. cross ( d0) ,
92- c2 : d * d2. cross ( d1) ,
93- }
94- }
95-
96- /// Create a new curve from Bézier segment and offset, with numerical robustness tweaks.
97- ///
98- /// The dimension represents a minimum feature size; the regularization is allowed to
99- /// perturb the curve by this amount in order to improve the robustness.
100- pub fn new_regularized ( c : CubicBez , d : f64 , dimension : f64 ) -> Self {
101- Self :: new (
102- c. regularize_endpoints ( dimension) . regularize_cusp ( dimension) ,
103- d,
104- )
105- }
106-
107- fn eval_offset ( & self , t : f64 ) -> Vec2 {
108- let dp = self . q . eval ( t) . to_vec2 ( ) ;
109- let norm = Vec2 :: new ( -dp. y , dp. x ) ;
110- // TODO: deal with hypot = 0
111- norm * self . d / dp. hypot ( )
112- }
113-
114- fn eval ( & self , t : f64 ) -> Point {
115- // Point on source curve.
116- self . c . eval ( t) + self . eval_offset ( t)
117- }
118-
119- /// Evaluate derivative of curve.
120- fn eval_deriv ( & self , t : f64 ) -> Vec2 {
121- self . cusp_sign ( t) * self . q . eval ( t) . to_vec2 ( )
122- }
123-
124- // Compute a function which has a zero-crossing at cusps, and is
125- // positive at low curvatures on the source curve.
126- fn cusp_sign ( & self , t : f64 ) -> f64 {
127- let ds2 = self . q . eval ( t) . to_vec2 ( ) . hypot2 ( ) ;
128- ( ( self . c2 * t + self . c1 ) * t + self . c0 ) / ( ds2 * ds2. sqrt ( ) ) + 1.0
129- }
130- }
131-
132- #[ expect( deprecated, reason = "implementation still expected to work" ) ]
133- impl ParamCurveFit for CubicOffset {
134- fn sample_pt_tangent ( & self , t : f64 , sign : f64 ) -> CurveFitSample {
135- let p = self . eval ( t) ;
136- const CUSP_EPS : f64 = 1e-8 ;
137- let mut cusp = self . cusp_sign ( t) ;
138- if cusp. abs ( ) < CUSP_EPS {
139- // This is a numerical derivative, which is probably good enough
140- // for all practical purposes, but an analytical derivative would
141- // be more elegant.
142- //
143- // Also, we're not dealing with second or higher order cusps.
144- cusp = sign * ( self . cusp_sign ( t + CUSP_EPS ) - self . cusp_sign ( t - CUSP_EPS ) ) ;
145- }
146- let tangent = self . q . eval ( t) . to_vec2 ( ) * cusp. signum ( ) ;
147- CurveFitSample { p, tangent }
148- }
149-
150- fn sample_pt_deriv ( & self , t : f64 ) -> ( Point , Vec2 ) {
151- ( self . eval ( t) , self . eval_deriv ( t) )
152- }
153-
154- fn break_cusp ( & self , range : Range < f64 > ) -> Option < f64 > {
155- const CUSP_EPS : f64 = 1e-8 ;
156- // When an endpoint is on (or very near) a cusp, move just far enough
157- // away from the cusp that we're confident we have the right sign.
158- let break_cusp_help = |mut x, mut d| {
159- let mut cusp = self . cusp_sign ( x) ;
160- while cusp. abs ( ) < CUSP_EPS && d < 1.0 {
161- x += d;
162- let old_cusp = cusp;
163- cusp = self . cusp_sign ( x) ;
164- if cusp. abs ( ) > old_cusp. abs ( ) {
165- break ;
166- }
167- d *= 2.0 ;
168- }
169- ( x, cusp)
170- } ;
171- let ( a, cusp0) = break_cusp_help ( range. start , 1e-12 ) ;
172- let ( b, cusp1) = break_cusp_help ( range. end , -1e-12 ) ;
173- if a >= b || cusp0 * cusp1 >= 0.0 {
174- // Discussion point: maybe we should search for double cusps in the interior
175- // of the range.
176- return None ;
177- }
178- let s = cusp1. signum ( ) ;
179- let f = |t| s * self . cusp_sign ( t) ;
180- let k1 = 0.2 / ( b - a) ;
181- const ITP_EPS : f64 = 1e-12 ;
182- let x = solve_itp ( f, a, b, ITP_EPS , 1 , k1, s * cusp0, s * cusp1) ;
183- Some ( x)
184- }
185- }
186-
18719/// State used for computing an offset curve of a single cubic.
188- struct CubicOffset2 {
20+ struct CubicOffset {
18921 /// The cubic being offset. This has been regularized.
19022 c : CubicBez ,
19123 /// The derivative of `c`.
@@ -271,10 +103,9 @@ struct SubdivisionPoint {
271103/// The parallel curve of `c` offset by `d` is written to the `result` path.
272104///
273105/// There is a fair amount of attention to robustness, but this method is not suitable
274- /// for degenerate cubics with entirely co-linear control points. Rather, those cases
275- /// are handled in the stroking logic, replacing them with linear segments and round
276- /// joins as needed.
277- pub ( crate ) fn offset_cubic ( c : CubicBez , d : f64 , tolerance : f64 , result : & mut BezPath ) {
106+ /// for degenerate cubics with entirely co-linear control points. Those cases should be
107+ /// handled before calling this function, by replacing them with linear segments.
108+ pub fn offset_cubic ( c : CubicBez , d : f64 , tolerance : f64 , result : & mut BezPath ) {
278109 result. truncate ( 0 ) ;
279110 // A tuning parameter for regularization. A value too large may distort the curve,
280111 // while a value too small may fail to generate smooth curves. This is a somewhat
@@ -288,7 +119,7 @@ pub(crate) fn offset_cubic(c: CubicBez, d: f64, tolerance: f64, result: &mut Bez
288119 // tangent determination to the caller, as those computations are the same for both
289120 // signs of `d`.
290121 let c_regularized = c. regularize_cusp ( tolerance * DIM_TUNE ) ;
291- let co = CubicOffset2 :: new ( c_regularized, d, tolerance) ;
122+ let co = CubicOffset :: new ( c_regularized, d, tolerance) ;
292123 let ( tan0, tan1) = PathSeg :: Cubic ( c) . tangents ( ) ;
293124 let utan0 = tan0. normalize ( ) ;
294125 let utan1 = tan1. normalize ( ) ;
@@ -299,15 +130,15 @@ pub(crate) fn offset_cubic(c: CubicBez, d: f64, tolerance: f64, result: &mut Bez
299130 co. offset_rec ( & rec, result) ;
300131}
301132
302- impl CubicOffset2 {
133+ impl CubicOffset {
303134 /// Create a new curve from Bézier segment and offset.
304135 fn new ( c : CubicBez , d : f64 , tolerance : f64 ) -> Self {
305136 let q = c. deriv ( ) ;
306137 let d2 = 2.0 * d;
307138 let p1xp0 = q. p1 . to_vec2 ( ) . cross ( q. p0 . to_vec2 ( ) ) ;
308139 let p2xp0 = q. p2 . to_vec2 ( ) . cross ( q. p0 . to_vec2 ( ) ) ;
309140 let p2xp1 = q. p2 . to_vec2 ( ) . cross ( q. p1 . to_vec2 ( ) ) ;
310- CubicOffset2 {
141+ CubicOffset {
311142 c,
312143 q,
313144 d,
@@ -786,11 +617,10 @@ const fn mk_a_weights(rev: bool) -> [f64; N_LSE] {
786617const A_WEIGHTS : [ f64 ; N_LSE ] = mk_a_weights ( false ) ;
787618const B_WEIGHTS : [ f64 ; N_LSE ] = mk_a_weights ( true ) ;
788619
789- #[ expect( deprecated, reason = "tests still valid on deprecated code path" ) ]
790620#[ cfg( test) ]
791621mod tests {
792- use super :: CubicOffset ;
793- use crate :: { fit_to_bezpath , fit_to_bezpath_opt , CubicBez , PathEl } ;
622+ use super :: offset_cubic ;
623+ use crate :: { BezPath , CubicBez , PathEl } ;
794624
795625 // This test tries combinations of parameters that have caused problems in the past.
796626 #[ test]
@@ -803,32 +633,36 @@ mod tests {
803633 } ;
804634 let offset = 3603.7267536453924 ;
805635 let accuracy = 0.1 ;
806- let offset_path = CubicOffset :: new ( curve, offset) ;
807- let path = fit_to_bezpath_opt ( & offset_path, accuracy) ;
808- assert ! ( matches!( path. iter( ) . next( ) , Some ( PathEl :: MoveTo ( _) ) ) ) ;
809- let path_opt = fit_to_bezpath ( & offset_path, accuracy) ;
810- assert ! ( matches!( path_opt. iter( ) . next( ) , Some ( PathEl :: MoveTo ( _) ) ) ) ;
636+
637+ let mut result = BezPath :: new ( ) ;
638+ offset_cubic ( curve, offset, accuracy, & mut result) ;
639+ assert ! ( matches!( result. iter( ) . next( ) , Some ( PathEl :: MoveTo ( _) ) ) ) ;
640+
641+ let mut result = BezPath :: new ( ) ;
642+ offset_cubic ( curve, offset, accuracy, & mut result) ;
643+ assert ! ( matches!( result. iter( ) . next( ) , Some ( PathEl :: MoveTo ( _) ) ) ) ;
811644 }
812645
813646 /// Cubic offset that used to trigger infinite recursion.
814647 #[ test]
815648 fn infinite_recursion ( ) {
816- const DIM_TUNE : f64 = 0.25 ;
817649 const TOLERANCE : f64 = 0.1 ;
650+ const OFFSET : f64 = -0.5 ;
818651 let c = CubicBez :: new (
819652 ( 1096.2962962962963 , 593.90243902439033 ) ,
820653 ( 1043.6213991769548 , 593.90243902439033 ) ,
821654 ( 1030.4526748971193 , 593.90243902439033 ) ,
822655 ( 1056.7901234567901 , 593.90243902439033 ) ,
823656 ) ;
824- let co = CubicOffset :: new_regularized ( c, -0.5 , DIM_TUNE * TOLERANCE ) ;
825- fit_to_bezpath ( & co, TOLERANCE ) ;
657+
658+ let mut result = BezPath :: new ( ) ;
659+ offset_cubic ( c, OFFSET , TOLERANCE , & mut result) ;
826660 }
827661
828662 #[ test]
829663 fn test_cubic_offset_simple_line ( ) {
830664 let cubic = CubicBez :: new ( ( 0. , 0. ) , ( 10. , 0. ) , ( 20. , 0. ) , ( 30. , 0. ) ) ;
831- let offset = CubicOffset :: new ( cubic , 5. ) ;
832- let _optimized = fit_to_bezpath ( & offset , 1e-6 ) ;
665+ let mut result = BezPath :: new ( ) ;
666+ offset_cubic ( cubic , 5. , 1e-6 , & mut result ) ;
833667 }
834668}
0 commit comments