curve: add ParamCurveTangent for pointwise tangents - #556
Conversation
|
This will make doing tangents for |
67016b5 to
b7ff2ce
Compare
This now adds support for |
b7ff2ce to
fc417b8
Compare
66521c9 to
fc417b8
Compare
There was a problem hiding this comment.
Aside from a few nits, the implementation looks good. I do wonder whether a different design could also be justified, where we require ParamCurveDeriv to be implemented instead. We could add ParamCurveDeriv::tangent with a default implementation for convenience (and overridable for optimization). That should work, unless there are cases where we know the tangent but can't construct the derivative curve. For Arc, the derivative is another Arc, with effectively the same math as here.
I don't think that the derivative of an That's why we didn't do |
Add `ParamCurveTangent` as an additive companion to `ParamCurveDeriv` for code that needs a tangent vector at a parameter value without requiring a derivative curve object. Place the new trait alongside the other curve traits in `param_curve.rs`, implement it for `Line`, `ConstPoint`, `QuadBez`, `CubicBez`, `Arc`, and `PathSeg`, and use it in internal call sites that were spelling pointwise tangent queries via `deriv().eval(t).to_vec2()`. Add tests that check the new tangent API against the existing derivative-curve behavior, cover degenerate normalization cases, and validate `Arc`'s tangent formula and reversed-sweep behavior.
fc417b8 to
3953a3a
Compare
There was a problem hiding this comment.
I think we should land this trait or something like it, but make it stronger so it covers cases ParamCurveDeriv cannot express, or cannot express nicely. In its current form it cannot quite do that.
The main thing that ParamCurveDeriv gives is the ability to use the result as a real object. For example, you can use it to find the second, third, etc., derivatives: if you want to know the second derivative of an Arc, you can let arc_deriv2 = arc.deriv().deriv(), and use it to get at, e.g., the curvature. That's still useful, also for Arc, but I'll open a separate PR for that discussion. It's also useful if you want to compute many tangents, as you only have to calculate the parametrization once.
What ParamCurveTangents could do, which ParamCurveDeriv cannot cleanly, is deal with discontinuities. The existing ParamCurveFit::sample_pt_tangent deals with that, but I think it would be more principled to promote that to a dedicated trait.
I see two ways we could do that. We could make ParamCurveTangent::tangent strong, giving it a parameter saying from which side the point is approached, or we could introduce a second trait here. Something as follows.
trait ParamCurveTangent: ParamCurve {
fn tangent(&self, t: f64) -> Vec2;
fn unit_tangent(&self, t: f64) -> Option<Vec2> { ... }
}
trait ParamCurveOneSidedTangent: ParamCurve {
/// One-sided derivative vector.
///
/// On smooth portions, this is the derivative of the curve
/// approached from the side of `t` encoded by `sign`. At a
/// cusp, this is zero.
fn tangent_from(&self, t: f64, sign: f64) -> Vec2; // or an `enum` instead of `sign: f64`, but `f64` may compose more nicely with other math
}
impl<T: ParamCurveTangent> ParamCurveOneSidedTangent for T {
fn tangent_from(&self, t: f64, _sign: f64) -> Vec2 {
self.tangent(t)
}
}This is important for some parametric curves, and important for paths, as every knot is a potential discontinuity: often the endpoint tangents are completely unrelated, sometimes the directions do match (G1 continuity), and rarely do the vectors match (C1 continuity). Any piecewise-C1 spline could implement that flavor of ParamCurveOneSidedTangent, but only fully-C1 splines can implement the version in this PR as-is.
There is one more important detail: sometimes a tangent's magnitude is zero, but its direction is well-defined. For example, the Bézier M 6 -3 C -2 3 -2 -3 6 3 has a cusp at t=0.5, whose tangent is (0,0) and tangent direction is (-1, 0) or (1, 0) depending on the side you approach it from. ParamCurveOneSidedTangent could have a unit_tangent_from method that can calculate this.
Then finally, there's a concern orthogonal to all the above. Sometimes when you want to know one tangent of a curve, you want to know more tangents. That argues in favor of storing the parametrization, slightly different to what ParamCurveDeriv does. For example, for a cubic Bézier, if you want to calculate unit_tangent_from, you would want to store the first and second derivatives so evaluation is cheap.
This solves a small part of linebender#556. Namely: being able to get at the tangents of an `Arc`. Similar to how e.g. `CubicBez::deriv` returns a `QuadraticBez` as a curve or how `Line::deriv` returns `ConstPoint` as a degenerate curve, even though the derivative actually is a tangent mapping, this implementation returns `ArcDeriv` as a curve. Numerically this implementation and the previous implementations are sound, but `ParamCurveDeriv` itself is muddying semantics a bit. We do document that already. `ArcDeriv` is closed under repeated differentation. Implementing `ParamCurveDeriv` gives the ability to use the result as a real object. Like the existing implementations, this makes it possible to find the second, third, etc., derivatives. You can compute the second derivative of an `Arc` as `let arc_deriv2 = arc.deriv().deriv()`, and use it to get at, e.g., the arc's curvature. There's more detail in the discussion of the [PR that triggered this](linebender#556), thinking of cleaner traits/types, and perhaps ways to get at tangents for curves that can't cleanly implement `ParamCurveDeriv`. This current PR intentionally does the simplest thing to make `Arc` differentiable.
This solves a small part of linebender#556. Namely: being able to get at the tangents of an `Arc`. Similar to how e.g. `CubicBez::deriv` returns a `QuadraticBez` as a curve or how `Line::deriv` returns `ConstPoint` as a degenerate curve, even though the derivative actually is a tangent mapping, this implementation returns `ArcDeriv` as a curve. Numerically this implementation and the previous implementations are sound, but `ParamCurveDeriv` itself is muddying semantics a bit. We do document that already. `ArcDeriv` is closed under repeated differentation. Implementing `ParamCurveDeriv` gives the ability to use the result as a real object. Like the existing implementations, this makes it possible to find the second, third, etc., derivatives. You can compute the second derivative of an `Arc` as `let arc_deriv2 = arc.deriv().deriv()`, and use it to get at, e.g., the arc's curvature. There's more detail in the discussion of the [PR that triggered this](linebender#556), thinking of cleaner traits/types, and perhaps ways to get at tangents for curves that can't cleanly implement `ParamCurveDeriv`. This current PR intentionally does the simplest thing to make `Arc` differentiable.
Add
ParamCurveTangentas an additive companion toParamCurveDerivfor code that needs a tangent vector at a parameter value without requiring a derivative curve object.Place the new trait alongside the other curve traits in p
aram_curve.rs, implement it forLine,ConstPoint,QuadBez,CubicBez,Arc, andPathSeg, and use it in internal call sites that were spelling pointwise tangent queries viaderiv().eval(t).to_vec2().Add tests that check the new tangent API against the existing derivative-curve behavior, cover degenerate normalization cases, and validate
Arc's tangent formula and reversed-sweep behavior.