Skip to content

Commit 6352804

Browse files
DJMcNabjneem
andauthored
Remove CubicOffset (#489)
This was deprecated in #427 This PR is easily the most dubious of the three (#488, #487), because the functionality: - is expected to be publicly exposed ("as we probably want to iterate on the exact interface") - Doesn't have a neat replacement. This will likely need Raph's sign-off --------- Co-authored-by: Joe Neeman <joeneeman@gmail.com>
1 parent a3bcf39 commit 6352804

4 files changed

Lines changed: 30 additions & 252 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ It was increased to support floating point math in const functions.
3636
- Breaking change: The previously deprecated `BezPath::flatten`, `Ellipse::[with_]x_rotation`, `{Rect, Size}::is_empty`, `Shape::[in]to_bez_path`,
3737
and `TranslateScale::as_tuple` have been removed.([#487][] by [@DJMcNab][])
3838

39+
### Removed
40+
41+
- Breaking change: The `offset` module amd the deprecated `CubicOffset` have been removed. ([#489][] by [@DJMcNab][])
42+
3943
## [0.11.3][] (2025-07-21)
4044

4145
This release has an [MSRV][] of 1.65.
@@ -193,6 +197,7 @@ Note: A changelog was not kept for or before this release
193197
[#486]: https://github.com/linebender/kurbo/pull/486
194198
[#487]: https://github.com/linebender/kurbo/pull/487
195199
[#488]: https://github.com/linebender/kurbo/pull/488
200+
[#489]: https://github.com/linebender/kurbo/pull/489
196201
[#490]: https://github.com/linebender/kurbo/pull/490
197202

198203
[Unreleased]: https://github.com/linebender/kurbo/compare/v0.11.3...HEAD

kurbo/examples/offset.rs

Lines changed: 0 additions & 26 deletions
This file was deleted.

kurbo/src/cubicbez.rs

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -388,41 +388,6 @@ impl CubicBez {
388388
.collect()
389389
}
390390

391-
/// Preprocess a cubic Bézier to ease numerical robustness.
392-
///
393-
/// If the cubic Bézier segment has zero or near-zero derivatives at the
394-
/// endpoints, perturb the control points to make it easier to process
395-
/// (especially offset and stroke), avoiding numerical robustness problems.
396-
pub(crate) fn regularize_endpoints(&self, dimension: f64) -> CubicBez {
397-
let mut c = *self;
398-
// First step: if control point is too near the endpoint, nudge it away
399-
// along the tangent.
400-
let dim2 = dimension * dimension;
401-
if c.p0.distance_squared(c.p1) < dim2 {
402-
let d02 = c.p0.distance_squared(c.p2);
403-
if d02 >= dim2 {
404-
// TODO: moderate if this would move closer to p3
405-
c.p1 = c.p0.lerp(c.p2, (dim2 / d02).sqrt());
406-
} else {
407-
c.p1 = c.p0.lerp(c.p3, 1.0 / 3.0);
408-
c.p2 = c.p3.lerp(c.p0, 1.0 / 3.0);
409-
return c;
410-
}
411-
}
412-
if c.p3.distance_squared(c.p2) < dim2 {
413-
let d13 = c.p1.distance_squared(c.p2);
414-
if d13 >= dim2 {
415-
// TODO: moderate if this would move closer to p0
416-
c.p2 = c.p3.lerp(c.p1, (dim2 / d13).sqrt());
417-
} else {
418-
c.p1 = c.p0.lerp(c.p3, 1.0 / 3.0);
419-
c.p2 = c.p3.lerp(c.p0, 1.0 / 3.0);
420-
return c;
421-
}
422-
}
423-
c
424-
}
425-
426391
/// Preprocess a cubic Bézier to ease numerical robustness.
427392
///
428393
/// If the cubic Bézier segment has zero or near-zero derivatives as an interior

kurbo/src/offset.rs

Lines changed: 25 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,7 @@
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
299
use arrayvec::ArrayVec;
3010

@@ -33,159 +13,11 @@ use crate::common::FloatFuncs;
3313

3414
use 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] {
786617
const A_WEIGHTS: [f64; N_LSE] = mk_a_weights(false);
787618
const B_WEIGHTS: [f64; N_LSE] = mk_a_weights(true);
788619

789-
#[expect(deprecated, reason = "tests still valid on deprecated code path")]
790620
#[cfg(test)]
791621
mod 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

Comments
 (0)