|
3 | 3 |
|
4 | 4 | //! An ellipse arc. |
5 | 5 |
|
6 | | -use crate::{Affine, Ellipse, ParamCurve, PathEl, Point, Rect, Shape, Vec2}; |
| 6 | +use crate::{Affine, Ellipse, ParamCurve, ParamCurveDeriv, PathEl, Point, Rect, Shape, Vec2}; |
7 | 7 | use core::{ |
8 | 8 | f64::consts::{FRAC_PI_2, PI}, |
9 | 9 | iter, |
@@ -193,6 +193,121 @@ impl ParamCurve for Arc { |
193 | 193 | } |
194 | 194 | } |
195 | 195 |
|
| 196 | +/// The "derivative curve" of an [`Arc`]. |
| 197 | +/// |
| 198 | +/// Note that, like the documentation of [`ParamCurveDeriv::deriv`] states, implementations on this |
| 199 | +/// type are semantically somewhat inaccurate: the derivative of a curve is not, strictly speaking, |
| 200 | +/// a curve itself. |
| 201 | +/// |
| 202 | +/// The difference with [`Arc`] is that this object has no `center`: that's a constant that |
| 203 | +/// differentiates to zero. |
| 204 | +pub struct ArcDeriv { |
| 205 | + /// The arc's radii, where the vector's x-component is the radius in the |
| 206 | + /// positive x direction after applying `x_rotation`. |
| 207 | + pub radii: Vec2, |
| 208 | + /// The start angle in radians. |
| 209 | + pub start_angle: f64, |
| 210 | + /// The angle between the start and end of the arc, in radians. |
| 211 | + pub sweep_angle: f64, |
| 212 | + /// How much the arc is rotated, in radians. |
| 213 | + pub x_rotation: f64, |
| 214 | +} |
| 215 | + |
| 216 | +impl ArcDeriv { |
| 217 | + #[inline(always)] |
| 218 | + const fn as_arc_with_center(&self, center: Point) -> Arc { |
| 219 | + Arc { |
| 220 | + center, |
| 221 | + radii: self.radii, |
| 222 | + start_angle: self.start_angle, |
| 223 | + sweep_angle: self.sweep_angle, |
| 224 | + x_rotation: self.x_rotation, |
| 225 | + } |
| 226 | + } |
| 227 | +} |
| 228 | + |
| 229 | +impl ParamCurveDeriv for Arc { |
| 230 | + type DerivResult = ArcDeriv; |
| 231 | + |
| 232 | + fn deriv(&self) -> ArcDeriv { |
| 233 | + // The center is constant over `t` and differentiates away, and as the arc's derivative is |
| 234 | + // represented by another arc (but without center), we can just delegate to `ArcDeriv` |
| 235 | + // directly. |
| 236 | + ArcDeriv { |
| 237 | + radii: self.radii, |
| 238 | + start_angle: self.start_angle, |
| 239 | + sweep_angle: self.sweep_angle, |
| 240 | + x_rotation: self.x_rotation, |
| 241 | + } |
| 242 | + .deriv() |
| 243 | + } |
| 244 | +} |
| 245 | + |
| 246 | +impl ParamCurveDeriv for ArcDeriv { |
| 247 | + type DerivResult = ArcDeriv; |
| 248 | + |
| 249 | + fn deriv(&self) -> ArcDeriv { |
| 250 | + // Given the axis-aligned parametric elliptical arc |
| 251 | + // |
| 252 | + // ``` |
| 253 | + // p(a) = center + (radii.x * cos(a), radii.y * sin(a)) |
| 254 | + // ``` |
| 255 | + // |
| 256 | + // we have tangents |
| 257 | + // |
| 258 | + // ``` |
| 259 | + // p'(a) = (-radii.x sin(a), radii.y * cos(a)). |
| 260 | + // ``` |
| 261 | + // |
| 262 | + // Our parameterization is over `t` with `a = angle_at(t) = start_angle + sweep_angle * t`. |
| 263 | + // Hence, |
| 264 | + // |
| 265 | + // ``` |
| 266 | + // p'(t) = sweep_angle * (-radii.x sin(angle_at(t)), radii.y * cos(angle_at(t))) |
| 267 | + // ``` |
| 268 | + // |
| 269 | + // or equivalently |
| 270 | + // |
| 271 | + // ``` |
| 272 | + // p'(t) = sweep_angle * (radii.x cos(angle_at(t) + pi/2), radii.y * sin(angle_at(t) + pi/2)). |
| 273 | + // ``` |
| 274 | + // |
| 275 | + // Instead of being axis-aligned, our ellipse is rotated by the 2x2 rotation matrix |
| 276 | + // `R(x_rotation)`. This has the effect of rotating the tangents by the same amount. Hence, |
| 277 | + // the derivative is given by |
| 278 | + // |
| 279 | + // ``` |
| 280 | + // sweep_angle * R(x_rotation) * (radii.x cos(angle_at(t) + pi/2), radii.y * sin(angle_at(t) + pi/2)). |
| 281 | + // ``` |
| 282 | + // |
| 283 | + // This is exactly another `Arc`, but with zero center, precisely what `ArcDeriv` models. |
| 284 | + // As this has the same form again, `ArcDeriv` is closed under repeated differentiation. |
| 285 | + ArcDeriv { |
| 286 | + radii: self.radii * self.sweep_angle, |
| 287 | + start_angle: self.start_angle + FRAC_PI_2, |
| 288 | + sweep_angle: self.sweep_angle, |
| 289 | + x_rotation: self.x_rotation, |
| 290 | + } |
| 291 | + } |
| 292 | +} |
| 293 | + |
| 294 | +impl ParamCurve for ArcDeriv { |
| 295 | + fn eval(&self, t: f64) -> Point { |
| 296 | + self.as_arc_with_center(Point::ZERO).eval(t) |
| 297 | + } |
| 298 | + |
| 299 | + fn subsegment(&self, range: Range<f64>) -> Self { |
| 300 | + let arc = self.as_arc_with_center(Point::ZERO).subsegment(range); |
| 301 | + |
| 302 | + Self { |
| 303 | + radii: arc.radii, |
| 304 | + start_angle: arc.start_angle, |
| 305 | + sweep_angle: arc.sweep_angle, |
| 306 | + x_rotation: arc.x_rotation, |
| 307 | + } |
| 308 | + } |
| 309 | +} |
| 310 | + |
196 | 311 | impl Shape for Arc { |
197 | 312 | type PathElementsIter<'iter> = iter::Chain<iter::Once<PathEl>, ArcAppendIter>; |
198 | 313 |
|
@@ -338,4 +453,25 @@ mod tests { |
338 | 453 | let arc = Arc::new((0.0, 0.0), (2.0, 1.0), 1.0, 1e-9, FRAC_PI_4); |
339 | 454 | assert_subsegment_matches(arc, 0.25..0.75); |
340 | 455 | } |
| 456 | + |
| 457 | + #[test] |
| 458 | + fn arc_first_and_second_derivatives() { |
| 459 | + let arc = Arc::new((1.0, -2.0), (2.0, 1.0), 0.0, PI, FRAC_PI_2); |
| 460 | + let d1 = arc.deriv(); |
| 461 | + let d2 = d1.deriv(); |
| 462 | + |
| 463 | + // Unrotated: (2 cos(pi t), sin(pi t)) |
| 464 | + // Rotated by pi/2: (-sin(pi t), 2 cos(pi t)) |
| 465 | + // So: |
| 466 | + // p'(t) = (-pi cos(pi t), -2 pi sin(pi t)) |
| 467 | + // p''(t) = (pi^2 sin(pi t), -2 pi^2 cos(pi t)) |
| 468 | + |
| 469 | + assert_point_near(d1.eval(0.0), Point::new(-PI, 0.0)); |
| 470 | + assert_point_near(d1.eval(0.5), Point::new(0.0, -2.0 * PI)); |
| 471 | + assert_point_near(d1.eval(1.0), Point::new(PI, 0.0)); |
| 472 | + |
| 473 | + assert_point_near(d2.eval(0.0), Point::new(0.0, -2.0 * PI * PI)); |
| 474 | + assert_point_near(d2.eval(0.5), Point::new(PI * PI, 0.0)); |
| 475 | + assert_point_near(d2.eval(1.0), Point::new(0.0, 2.0 * PI * PI)); |
| 476 | + } |
341 | 477 | } |
0 commit comments