Curves

In this section we discuss the mathematics behind the curves we provide in this package, and the operations that we perform on curves.

AbstractParametricCurve

Before we get into the curves themselves, let us first discuss what we need from a parametric curve, i.e. for a curve subtyping AbstractParametricCurve. We let r:[0,1]R2 denote a parametric curve, and we always assume the curve to be non-intersecting (except at the endpoints) and three times continuously differentiable. For the discussion below, there may be some curves which take a simpler approach than the general approach described, and this will be indicated later when we discuss these specific curves.

Arc length

One important quantity to compute is the arc length over some interval t1tt2 along the curve. In general, this is given by the integral

s(t1,t2)=t1t2r(t)dt,

which is typically computed using numerical integration. In this package, we use Gauss-Legendre quadrature so that

s(t1,t2)t2t12i=1nwir(t2t12ξi+t1+t22),

where ξi are the nodes and wi are the weights of the Gauss-Legendre quadrature rule, and n are the number of nodes in use. We use n=250 for all integrals in this package.

Curvature

The curvature of a curve describes how much the curve deviates from a straight line at a given point. It is defined as

κ(t)=r(t)×r(t)r(t)3,

where × denotes the cross product; in R2, we are defining u×v=u1v2u2v1. Writing r(t)=(x(t),y(t)), this can be written as

κ(t)=x(t)y(t)y(t)x(t)(x(t)2+y(t)2)3/2.

Total variation

The total variation of r(t) over t1tt2 is defined as the total absolute change in the tangent angle, which we can write as

TV(t1,t2)=C(t1,t2)|dθ|,

where θ is the tangent angle of the curve, and C(t1,t2) is the curve segment from t1 to t2. This is also called the total absolute curvature, since |dθ|=|κ(s)|ds (treating s as the arc length parameter). Thus,

TV(t1,t2)=t1t2|κ(t)|r(t)dt,

where we have used ds=r(t)dt. We evaluate this using numerical integration just as we did for arc length, writing

TV(t1,t2)t2t12i=1nwi|κ(t2t12ξi+t1+t22)|r(t2t12ξi+t1+t22).

The reason that total variation is important is that it gives a measure of how much the curve is bending over the interval t1tt2, meaning we can use the total variation to assess how many edges are needed to give a reasonable piecewise linear approximation to the curve.

There is a way to evaluate TV(t1,t2) without computing the integral. By default, any new AbstractParametricCurve will do so, but for the curves we have implemented in this package we apply the following idea. First, note that the total variation TV(t1,t2) over some curve where the curve's orientation is monotone is simply the angle between the tangent vectors at the endpoints. To see this, note that since the orientation is monotone the angle θ is monotone, and so the total change in the angle is simply the difference in the angles at the endpoints. With this idea, we take C(t1,t2) and write C(t1,t2)=i=1mCi where CiCj= for ij and Ci is a curve segment where the orientation is monotone. Thus, the integral can be evaluated by simply summing the angles between the tangent vectors at the endpoints of each Ci.

One complication with the above approach is that actually finding the Ci can be difficult. To do so, we need to understand where the curve's orientation changes. There are five cases where this can occur that we list below, letting t be a point where the orientation changes and r(t)=(x(t),y(t)):

  1. Horizontal turning point: x(t)=0.
  2. Vertical turning point: y(t)=0.
  3. Horizontal inflection point: x(t)=0 and x(t)0.
  4. Vertical inflection point: y(t)=0 and y(t)0.
  5. Inflection point: κ(t)=0.

These cases do not have to occur simultaneously - any of them is sufficient to potentially cause a change in orientation; in our implementation, for cases 3 and 4 we do not check whether x(t) or y(t) is zero. Note that the first four conditions together are not necessarily needed for finding these monotone pieces, but they guarantee that the change in orientation is at most π/2, making it easier to compute the total change in orientation (since a change in orientation of β could also be 2πβ if β>π/2). To find these points t, we use Newton's method on all of the respective functions. So, when initialising a curve that uses this approach, we first find all t[0,1] where any of these changes occur, and order them so that 0t1tm1. These values define the monotone pieces that the curve is split into. An example of these orientation markers t is shown below, where we show the markers on the curve using red dots.

Example block output

There are many orientation markers on this curve. Only two of these come from κ(t)=0, with all the others coming from x,y,x, and y, as we can see from the graphs shown.

Point on a curve closest to a point

An important problem is finding the point on a curve r(t) that is closest to a given point p. Our generic approach to this relies on there being a lookup table for r(t) which stores the values r(ti) for i=1,2,,n, where ti=(i1)/(n1) to allow for binary search to be used.

The approach starts by computing (i,δ2), where i is the index so that the point q=r(ti) in the lookup table is closest to p, and δ=pq. We can then apply binary search in the following way to find an approximation to the closest point on r(t) closest to p up to some tolerance ε (ε=1012 in this package); in what follows, we assume 1<i<n, but these boundary cases can be handled in a similar way:

  1. First, let t=ti1, tc=ti, and tr=ti+1, and compute w=trt. If w<ε, then we are done, otherwise we go to step 2.
  2. Compute tc=(t+tc)/2, tcr=(tc+tr)/2, δc=pr(tc), and δcr=pr(tcr). If δc<δcr, then we choose the left-middle to be the new center point, replacing tr with tc and tc with tc. Otherwise, we choose the right-middle to be the new center point, replacing t with tc and tc with tcr. We then go back to step 1 with w=trt.

Once the above procedure converges, the final value of tc gives the point on r(t) that is closest to p, i.e. r(tc) is the approximation to the closest point on r(t) to p.

An example of this procedure is shown below: The point of interest is shown in red, and the lookup points are shown in blue. The lookup point closest to the point of interest is shown via the green line, which defines ttctr, and the magenta line shows the section of the curve corresponding to this interval [t,tr]. The black dot shows the closest point on the curve to the point of interest. The binary search for this problem would start by shrinking into the interval [tc,tr], replacing tc by (tc+tr)/2, since p is closer to that side of tc than to [t,tc].

Example block output

Position of a point relative to a curve

It is important that we know how to find the position of a point p relative to some curve r(t), meaning whether p is left or right of r(t). The approach we take for this is relatively simple:

  1. First, find t so that q=r(t) is the closest point on r(t) to p.
  2. Compute T=r(t) and construct the line segment L that connects q and T.
  3. The position of p is then determined by the position of p relative to L.

In the example below, p would be right of the curve as it is to the right of the tangent vector T.

Example block output

Computing equidistant splits

We may want to split a curve between two points t1tt2 at a point t such that s(t1,t)=s(t,t2). This is done using a simple bisection method:

  1. Compute s12=s(a,b), let s=s12/2, and set t=(a+t2)/2, where a=t1.
  2. For 100 iterations, do: Compute s=s(a,t). If |ss|<ε1 or |ba|<ε2, then break and return t=t. Otherwise, replace t1 with t if s<s, and replace t2 with t otherwise. Finally, let t=(t1+t2)/2 and continue iterating.
  3. Return t as the equidistant split point.

Computing equivariation splits

Another important operation is the computation of a point t that splits a given curve r(t) between two points t1tt2 at a point t such that TV(t1,t)=TV(t,t2). This is called an equivariation split, and is useful as it gives a way to divide a curve into two halves in a more natural way than using, say, an equidistant split which might not be as appropriate a split for certain curves. The computation of t is done using a simple bisection method that follows the exact same approach as for equidistant splits, except that instead of computing arc length we compute the total variation, and we initialise t as the equidistant split rather than the midpoint of the interval.

Computing the inverse of a curve

We often need to find the point t that corresponds to a point q on a curve r(t). This is done by simply finding the closest point on the curve to q and returning the parameter value of that point.

Computing the angle between two curves

Given two curves r1 and r2, a problem of interest is to consider the angle between them at some point t. This is done by computing the tangent vectors at the point t for both curves, and then computing the angle between these two tangent vectors. Computing these angles is needed during mesh refinement, since we need to protect against small angles.

Computing the intersection of a curve with a circle

For mesh refinement we need to use concentric circular shells to protect against small angles. For curves, this means that we need to know how to compute the intersection of a circle with a given curve. In particular, given a circle of radius r centered at r(t1), we need to find the first intersection t of the curve with the circle, where t1tt2, assuming that such an intersection exists.

The procedure for this problem starts by computing an interval [ti,tj] to be used for computing t using a bisection method. To find ti and tj, we initially set ti=t1 and tj=t2. Then, for n=1,2,,500: Compute tn=t1+(n1)(t2t1)/(5001) and let q=r(tn). If pq>r we set tj=tn and return [ti,tj], else we set ti=tn and continue. There is also a method for finding [ti,tj] using a lookup table, but we do not describe that here. Once we have this interval [ti,tj], a bisection method is used to find t, remembering that we are trying to solve r(t)r(t)=r. An example of such an intersection is shown below. The red curve shows the portion of the curve over an interval [t1,t2], and the green circle shows the circle of radius r centered at a point r(t1). The blue point shows the first intersection of the curve with the circle. The other intersection is ignored as it is not on the red portion.

Example block output

LineSegment

Now we discuss in detail all the curves we provide in this package, starting with LineSegment. We represent line segments as oriented segments between points p and q parametrised by r(t)=p+t(qp) for 0t1. The implementation of the above operations for LineSegment is straightforward, and we do not discuss them here.

CircularArc

The curve CircularArc is for representing a circular arc, meaning a part of a circle with radius r and center c spanning some angle θ1θθ2, parametrised over 0t1. We parametrise this arc using

r(t)=c+r(cosθt,sinθt),

where θt=θ1+tΔθ and Δθ=θ2θ1. With this parametrisation, we also have

r(t)=(rΔθsinθt,rΔθcosθt),r(t)=(rΔθ2cosθt,rΔθ2sinθt).

In addition:

  • To determine the position of a point relative to the circle, we simply use the in_circle predicate.
  • The arc length s(t1,t2) is simply s(t1,t2)=r|Δθ|(t2t1).
  • The curvature is κ(t)=sgn(Δθ)/r.
  • The total variation is TV(t1,t2)=|Δθ|(t2t1).
  • The equidistant and equivariation splits are both t=(t1+t2)/2.
  • Given a point p on the circle, its inverse is t=(θθ1)/Δθ, where θ=arctan(pycy,pxcx). If t<0, the inverse is instead t+2π/|Δθ|; if t>1, the inverse is instead t2π/|Δθ|.

EllipticalArc

An EllipticalArc is used to represent an arc of an ellipse, defined by a center c, horizontal radius α, vertical radius β, a rotation angle θ, a start angle θ1 (as measured from the center), and a final angle θ2. We define Δθ=θ2θ1. The parametrisation of this arc is

r(t)=[cx+αcos(θt)cosθβsin(θt)sinθcy+αcos(θt)sinθ+βsin(θt)cosθ],

where θt=θ1+tΔθ. The derivatives of r(t) are given by

r(t)=Δθ[αsin(θt)cosθβcos(θt)sinθαsin(θt)sinθ+βcos(θt)cosθ],r(t)=Δθ2[αcos(θt)+βsin(θt)sinθαcos(θt)sinθβsin(θt)cosθ].

The curvature is given by

κ(t)=sgn(Δθ)αβ(α2sin2(θt)+β2cos2(θt))3/2.

The total variation is easily computed by simply taking the angle between the tangent vectors at the endpoints since the orientation is monotone.

To determine the position of a point relative to the arc, we transform into coordinates where the ellipse becomes a unit circle and consider the position of the transformed coordinates. In particular, for a point p=(x,y), we transform it into

p=((xcx)cosθ+(ycy)sinθα,(xcx)sinθ+(ycy)cosθβ),

and then compare it to the unit circle.

The inverse of a point p is computed as follows. First, compute

(x,y)=((pxcx)cosθ+(pycy)sinθα,(pxcx)sinθ+(pycy)cosθβ),

and then t=arctan(y,x) modulo 2π. Finally, t=(tθ1)/Δθ; if t<0, the inverse is instead t+2π/|Δθ|; if t>1, the inverse is instead t2π/|Δθ|.

BezierCurve

The next curve we discuss is the BezierCurve. Bézier curves are curves that are defined by a set of control points. They are not interpolating curves, but their shape can be highly controlled using the control points. A great resource on Bézier curves is this primer. The precise definition of Bézier curves will not be delved into here.

To write down the parametrisation of a Bézier curve, let the control points be p1,,pn. Our parametrisation is then

r(t)=i=0n1(n1i)(1t)n1itipi+1.

This is not the form we use to evaluate r(t), though. Instead, de Casteljau's algorithm is used to evaluate the curve, a recursive algorithm for evaluating r(t).

To differentiate r(t), we can use the fact that the derivative of an (n1)th degree Bézier curve (our curve is of degree n1) is an (n2)th degree Bézier curve with control points pi+1pi. Thus, differentiating r(t) is the same as applying de Casteljau's algorithm to an (n2)th degree Bézier curve with control points pi+1pi. The same is true for twice and thrice differentiating r(t); for the second derivatives the control points are pi+22pi+1+pi, and for the third derivatives the control points are pi+33pi+2+3pi+1pi.

For all the other operations, we use the generic approaches previously described, as Bézier curves do not lend themselves nicely to simple formulas for these operations.

BSpline

The next curve we discuss is the BSpline. B-splines are controlled by a set of control points and a set of knots. They are not interpolating curves. In our implementation, we do not allow for general knots to be provided, and the spline always goes through the first and last points, but not any of the intermediate points. Rather than go through the precise definition here, you are better off reading for example this section of the primer here, or The NURBs Book by Piegl and Tiller (1997).

For our implementation, you just need to know that we use de Boor's algorithm for evaluating a B-spline. Similarly, since the derivative of a B-spline is just another B-spline, we can use de Boor's algorithm to differentiate a B-spline, and similarly for its second and third derivatives. For all the other operations we use the generic approaches previously described just as we did for Bézier curves.

CatmullRomSpline

We now discuss Catmull-Rom splines, which is an interpolating spline defined by a set of piecewise cubic polynomials. To discuss these splines, we need to consider the Catmull-Rom spline defined by a set of four points p0,p1,p2,p3. This is a cubic polynomial of the form q(t)=at3+bt2+ct+d over 0t1 that interpolates the four points, but is drawn only between p2 and p3 with q(0)=p1 and q(1)=p2. We also define two parameters α and τ that define the type of the parametrisation and the tightness of the spline, respectively; α=0 is called a uniform parmetrisation, α=1/2 is called a centripetal parametrisation, and α=1 is called a chordal parametrisation. Increasing τ makes the spline tighter, and τ=1 turns the spline into a piecewise linear curve. In this package, we only directly support α=1/2 and τ=0.

Using these control points and parameters, following this article, we can compute the coefficients of our polynomial as follows:

m1=(1τ)(t2t1)(p1p0t1t0p2p0t2t0+p2p1t2t1),m2=(1τ)(t2t1)(p2p1t2t1p3p1t3t1+p3p2t3t2),a=2(p1p2)+m1+m2,b=3(p1p2)2m1m2,c=m1,d=p1,

where ti=ti1+pipi1α and t0=0 for i=1,2,3.

Using this definition of a Catmull-Rom spline through four points, we can generalise it to a set of n control points p1,,pn. Firstly, since a Catmull-Rom spline through four points only draws through the inner two points, if we want the spline to interpolate through all of the control points then we need to define two new control points p0 and pn+1 so that p1 and pn are included in the interior of some spline segment (if the spline is closed so that p1=pn, no new points are needed). Using ideas from CatmullRom.jl, we define p0 as follows: We reflect x2, the x coordinate of p2, left of x1 to a point x=x1(x2x1)=2x1x2. We then need to find the point y that best suits the spline at this point x. We do this using rational interpolation, specifically Thiele's quartic rational interpolant. If the resulting y is not finite, we use the cubic rational interpolant and, if that fails, we use quadratic interpolation. If x1=x2, we instead apply this idea to y. We then set p0=(x,y), and apply the same ideas to pn+1.

We now have a set of control points p0,p1,,pn,pn+1, and are interested in a Catmull-Rom spline that interpolates through p1,,pn. To evlauate the spline r(t), we start by defining the knots ti=ti1+pipi1α, where t1=0, and then set ti=ti/tn so that 0ti1. We then find the knot interval [tj,tj+1] that contains t, and map t into this interval using t=(ttj)/(tj+1tj). Next, we construct the Catmull-Rom spline segment through pj1, pj, pj+1, and pj+2, and evaluate this segment at t to get r(t).

To differentiate r(t), we apply similar ideas as above except that we differentiate the Catmull-Rom spline segment through pj1, pj, pj+1, and pj+2, and evaluate this derivative at t to get r(t) (after multiplying by 1/(tj+1tj)). Similar ideas apply for computing r(t) and r(t).

To compute the arc length s(t1,t2), we store the arc lengths of each spline segment (computed using numerical integration) to quickly compute the arc length across any segments entirely covered in t1tt2, and then use numerical integration for any remaining segments not completely covered.

For the remaining operations, we use the generic approaches previously described.