brush-strokes-0.1.0.0: src/lib/Math/Bezier/ArcLength.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Math.Bezier.ArcLength
( -- * Arc-length reparametrisation
ArcLengthParametrisation(..)
, curveArcLengthParametrisation
-- ** Options
, ArcLengthOptions(..), defaultArcLengthOptions
, Integrator(..)
, Regulariser(..), defaultRegulariser
, RegularisationMethod(..)
, NewtonRaphsonOptions(..), defaultNewtonRaphsonOptions
-- * Spline arc-length parametrisation
, SplineArcLengthParam(..)
, splineArcLengthParam
, splineTotalArcLength
, splineParameterAtArcLength
, splitSplineAtArcLengths
)
where
-- base
import Data.Complex
( realPart )
import Data.Maybe
( fromMaybe )
import GHC.Exts
( fromList, proxy# )
import GHC.TypeNats
( KnownNat, natVal', SomeNat (..), someNatVal )
-- acts
import Data.Act
( Torsor (..) )
-- containers
import Data.Sequence
( Seq )
import qualified Data.Sequence as Seq
( Seq(..) )
-- primitive
import Data.Primitive.PrimArray
( PrimArray )
import qualified Data.Primitive.PrimArray as Prim
( indexPrimArray )
-- brush-strokes
import qualified Math.Bezier.Cubic as Cubic
( Bezier(..), bezier', speedCriticalPoints, subdivide )
import qualified Math.Bezier.Quadratic as Quadratic
( Bezier(..), bezier', speedCriticalPoints, subdivide )
import Math.Bezier.Spline
( Curve(..), Curves(..), NextPoint(..), Spline(..), SplineType(..)
, SplineTypeI
, adjustSplineType, openCurveEnd, subdivideSplineAt
)
import Math.Epsilon
( epsilon )
import Math.Module
( Module, Inner, norm )
import Math.Roots
( newtonRaphson )
--------------------------------------------------------------------------------
-- Curve arc-length
data ArcLengthOptions
= ArcLengthOptions
{ arcLengthIntegrator :: !Integrator
-- ^ Quadrature method used for arc-length integration.
, splitAtCriticalPoints :: !Bool
-- ^ Whether to split at (near) critical points.
-- (A "near" critical point is the real part of a possibly complex critical point.)
--
-- (Ignored for the Gravesen method.)
, newtonRaphsonOptions :: !NewtonRaphsonOptions
-- ^ Options for the Newton–Raphson method used to compute the
-- inverse of the arc-length function.
}
-- | Method used to regularise the integrand near a low-speed endpoint.
data RegularisationMethod
= SquareRoot
-- ^ Substitute \( t = u^2 \) near the endpoint, so that the integrand
-- \( \|B'(t)\| \sim \sqrt{t} \) becomes smooth in \( u \).
-- (No other methods yet; more may be added in the future.)
-- | Cusp-endpoint regularisation settings.
data Regulariser
= Regulariser
{ regularisationMethod :: !RegularisationMethod
, regularisationThreshold :: !Double
-- ^ Speed threshold below which the substitution is applied at an endpoint.
}
defaultRegulariser :: Regulariser
defaultRegulariser = Regulariser
{ regularisationMethod = SquareRoot
, regularisationThreshold = 0.5
}
data Integrator
= ClenshawCurtis
{ clenshawCurtisDegree :: !Int -- ^ degree (must be even)
, regulariser :: !( Maybe Regulariser )
-- ^ Regularisation at cuspidal endpoints?
}
| TanhSinh
{ tanhSinhNodes :: !Int
-- ^ @N@: nodes per side (2N+1 nodes total).
--
-- Recommended value: such that @N*h ≥ 3.2@.
, tanhSinhStepSize :: !Double
-- ^ @h@: step size.
--
-- Recommended value: between @0.5@ (fast) and @0.1@ (precise).
}
| GaussLegendre
{ gaussLegendreDegree :: !Int
-- ^ Number of quadrature nodes.
--
-- An @n@-point rule is exact for polynomials of degree up to @2n−1@.
, regulariser :: !( Maybe Regulariser )
-- ^ Cusp-endpoint regularisation (@Nothing@ disables it).
}
| Gravesen
{ gravesenTol :: !Double
-- ^ Stopping tolerance on the polygon-length minus chord-length gap
-- for each piece.
-- The tolerance is halved at each subdivision level, so the total
-- arc-length error is bounded by @gravesenTol@.
}
defaultArcLengthOptions :: ArcLengthOptions
defaultArcLengthOptions =
ArcLengthOptions
{ arcLengthIntegrator = ClenshawCurtis
{ clenshawCurtisDegree = 16
, regulariser = Just defaultRegulariser
}
, splitAtCriticalPoints = True
, newtonRaphsonOptions = defaultNewtonRaphsonOptions
}
data NewtonRaphsonOptions
= NewtonRaphsonOptions
{ newtonRaphsonDigits :: !Int -- ^ desired bits of precision
, newtonRaphsonMaxIterations :: !Word -- ^ maximum number of iterations (at least 1)
}
defaultNewtonRaphsonOptions :: NewtonRaphsonOptions
defaultNewtonRaphsonOptions =
NewtonRaphsonOptions
{ newtonRaphsonDigits = 50
, newtonRaphsonMaxIterations = 64
}
data ArcLengthParametrisation
= ArcLengthParametrisation
{ totalArcLength :: !Double
, parameterFromArcLength :: !( Double -> Double )
}
-- | Compute the arc-length parametrisation of a 'Curve'.
curveArcLengthParametrisation
:: forall diff ptData crvData
. ( Torsor diff ptData, Module Double diff, Inner Double diff
, Show ptData )
=> ArcLengthOptions
-> ptData
-> Curve Open crvData ptData
-> ArcLengthParametrisation
curveArcLengthParametrisation opts p0 = \case
LineTo { curveEnd = NextPoint p1 } ->
let !len = norm @diff ( p0 --> p1 )
in ArcLengthParametrisation
{ totalArcLength = len
, parameterFromArcLength =
\ s ->
if len > 0
then max 0 $ min 1 $ s / len
else 0
}
Bezier2To { controlPoint = p1, curveEnd = NextPoint p2 } ->
let !bez = Quadratic.Bezier { p0, p1, p2 }
speed = norm @diff . Quadratic.bezier' @diff bez
!critPts =
if splitAtCriticalPoints opts
then Quadratic.speedCriticalPoints @diff bez
else []
in build speed $
pickIntegrator ( gravesenQuadIntegrator @diff bez ) speed critPts
Bezier3To { controlPoint1 = p1, controlPoint2 = p2, curveEnd = NextPoint p3 } ->
let !bez = Cubic.Bezier { p0, p1, p2, p3 }
speed = norm @diff . Cubic.bezier' @diff bez
!critPts =
if splitAtCriticalPoints opts
then map realPart $ Cubic.speedCriticalPoints @diff bez
else []
in build speed $
pickIntegrator ( gravesenCubicIntegrator @diff bez ) speed critPts
where
pickIntegrator
:: ( Double -> Double -> Double ) -- ^ Gravesen cumulative arc-length integrator
-> ( Double -> Double ) -- ^ speed function
-> [ Double ] -- ^ critical points
-> ( Double -> Double ) -- ^ cumulative arc-length function
pickIntegrator gravesenFn speed critPts =
case arcLengthIntegrator opts of
Gravesen { gravesenTol = eps }
-> gravesenFn eps
ClenshawCurtis { clenshawCurtisDegree = d, regulariser = reg }
| SomeNat @n _ <- someNatVal ( fromIntegral d )
-> makeCompositeCCIntegrator @n reg speed critPts
TanhSinh { tanhSinhNodes = nodesPerSide, tanhSinhStepSize = tsH }
-> makeCompositeTSIntegrator nodesPerSide tsH speed critPts
GaussLegendre { gaussLegendreDegree = d, regulariser = reg }
-> makeCompositeGLIntegrator d reg speed critPts
-- | Build the final parametrisation from speed and cumulative arc-length.
build speed integrate =
let !lg = integrate 1
in
ArcLengthParametrisation
{ totalArcLength = lg
, parameterFromArcLength =
\ s ->
if | s <= 0
-> 0
| s >= lg
-> 1
| otherwise
-> let t_0 = s / lg
in case newtonRaphson
( newtonRaphsonMaxIterations $ newtonRaphsonOptions opts )
( newtonRaphsonDigits $ newtonRaphsonOptions opts )
( \ t -> (# integrate t - s, speed t #) )
t_0
of
Just t' -> t'
Nothing -> t_0
}
{-# INLINEABLE curveArcLengthParametrisation #-}
--------------------------------------------------------------------------------
-- Spline arc-length
-- | Precomputed arc-length data for all curves of a spline.
data SplineArcLengthParam = SplineArcLengthParam
{ splineArcLengthCurveEnds :: !( Seq Double )
-- ^ Cumulative arc lengths at the end of each curve.
, splineArcLengthCurveParams :: !( Seq ArcLengthParametrisation )
-- ^ Per-curve arc-length parametrisations.
}
-- | Compute the arc-length parametrisation of a spline.
splineArcLengthParam
:: forall diff clo crvData ptData
. ( SplineTypeI clo
, Torsor diff ptData, Module Double diff, Inner Double diff
, Show ptData )
=> ArcLengthOptions
-> Spline clo crvData ptData
-> SplineArcLengthParam
splineArcLengthParam opts spl =
let Spline { splineStart, splineCurves = OpenCurves curves } =
adjustSplineType @Open spl
pairs = go Seq.Empty 0 splineStart curves
in SplineArcLengthParam
{ splineArcLengthCurveEnds = fmap fst pairs
, splineArcLengthCurveParams = fmap snd pairs
}
where
go :: Seq (Double, ArcLengthParametrisation)
-> Double
-> ptData
-> Seq ( Curve Open crvData ptData )
-> Seq (Double, ArcLengthParametrisation)
go acc _ _ Seq.Empty = acc
go acc cum pt ( crv Seq.:<| rest ) =
let param = curveArcLengthParametrisation @diff opts pt crv
cum' = cum + totalArcLength param
in go ( acc Seq.:|> ( cum', param ) ) cum' ( openCurveEnd crv ) rest
-- | Total arc length of a spline.
splineTotalArcLength :: SplineArcLengthParam -> Double
splineTotalArcLength ( SplineArcLengthParam { splineArcLengthCurveEnds = ends } ) =
case ends of
Seq.Empty -> 0
_ Seq.:|> x -> x
-- | Find the @(curve index, Bézier parameter t ∈ [0,1])@ within the spline
-- at the given arc length.
splineParameterAtArcLength
:: SplineArcLengthParam
-> Double -- ^ target arc length
-> ( Int, Double ) -- ^ (curve index, Bézier parameter t ∈ [0,1])
splineParameterAtArcLength
( SplineArcLengthParam
{ splineArcLengthCurveEnds = ends
, splineArcLengthCurveParams = crvParams
}
)
s = go 0 0 ends crvParams
where
go i _cumStart Seq.Empty _
= ( max 0 ( i - 1 ), 1 )
go i cumStart ( end Seq.:<| restEnds ) ( param Seq.:<| restParams )
| s <= end || null restEnds
= ( i, parameterFromArcLength param ( s - cumStart ) )
| otherwise
= go ( i + 1 ) end restEnds restParams
go i _cumStart _ _
= ( max 0 ( i - 1 ), 1 )
-- | Split an open spline at a sorted list of absolute arc-lengths.
splitSplineAtArcLengths
:: forall diff crvData ptData
. ( Torsor diff ptData, Module Double diff, Inner Double diff
, Show ptData -- debugging
)
=> ArcLengthOptions
-> [ Double ] -- ^ sorted arc-length positions
-> Spline Open crvData ptData
-> [ Spline Open crvData ptData ]
splitSplineAtArcLengths opts positions spl = subdivideSplineAt @diff spl splitParams
where
!param = splineArcLengthParam @diff opts spl
splitParams = map ( splineParameterAtArcLength param ) positions
--------------------------------------------------------------------------------
-- Clenshaw–Curtis quadrature
-- | Chebyshev node of the second kind.
chebyshevNode
:: forall n
. KnownNat n
=> Int -- ^ 0 <= k <= n
-> Double
chebyshevNode k = cos ( pi * fromIntegral k / fromIntegral n )
where
n :: Int
n = fromIntegral $ natVal' @n proxy#
{-# INLINEABLE chebyshevNode #-}
-- | Clenshaw–Curtis nodes.
ccNodes :: forall n. KnownNat n => PrimArray Double
ccNodes =
fromList
[ 0.5 * ( 1 + chebyshevNode @n k )
| k <- [ 0 .. n ]
]
where
n :: Int
n = fromIntegral $ natVal' @n proxy#
{-# INLINEABLE ccNodes #-}
-- | Pre computation of a Clenshaw–Curtis integrator.
makeCCIntegrator
:: forall n
. KnownNat n
=> PrimArray Double
-> ( Double -> Double )
makeCCIntegrator ys =
-- Pre-computation
let
n :: Int
n = fromIntegral $ natVal' @n proxy#
ep :: Int -> Double
ep k = if k == 0 || k == n then 0.5 else 1
-- Chebyshev coefficients (pre-computation)
cs :: PrimArray Double
cs =
fromList
[ ( 2 / fromIntegral n )
* sum [ ep k
* ( ys `Prim.indexPrimArray` k )
* cos ( fromIntegral j * fromIntegral k * pi / fromIntegral n )
| k <- [0..n]
]
| j <- [0..n]
]
-- The integrator proper
in \t ->
let y0 = 2*t - 1 -- map t ∈ [0,1] to y₀ ∈ [−1,1]
-- Chebyshev recurrence (T_{j−1}, T_j) as plain
go :: Double -> Double -> Double -> Int -> Double
go !acc !tm1 !tj !j
| j > n
= acc
| otherwise
= let
!tj1 = 2 * y0 * tj - tm1
!cj = cs `Prim.indexPrimArray` j
!ej = cj * ( if j == 0 || j == n then 0.5 else 1 )
a | j == 0 = ej * ( y0 + 1)
| j == 1 = ej * ( tj1 - 1 ) / 4
| otherwise = ej * ( tj1 / fromIntegral ( 2 * ( j + 1 ))
- tm1 / fromIntegral ( 2 * ( j - 1 ))
- ( if even j then 1 else -1 ) / fromIntegral ( j * j - 1 ) )
in go ( acc + a ) tj tj1 ( j + 1 )
in 0.5 * go 0 y0 1 0
{-# INLINEABLE makeCCIntegrator #-}
--------------------------------------------------------------------------------
-- Endpoint regularisation
-- | Select the regularisation substitution for a single piece @[a,b]@.
-- Returns the transformed integrand on @[0,1]@ and the map @t ↦ u@.
applyRegulariser
:: Maybe Regulariser
-> Double -- ^ @a@
-> Double -- ^ @b@
-> ( Double -> Double ) -- ^ speed
-> ( Double -> Double -- ^ transformed integrand on @[0,1]@
, Double -> Double ) -- ^ \( t \in [a,b] \mapsto u \in [0,1] \)
applyRegulariser Nothing a b speed =
( \u -> speed ( a + (b-a)*u ) * (b-a)
, \x -> (x-a) / (b-a) )
applyRegulariser
( Just Regulariser { regularisationMethod = meth, regularisationThreshold = tol } )
a b speed
| sa <= tol && sa <= sb = case meth of
SquareRoot ->
( \u -> speed ( a + (b-a)*u*u ) * 2*(b-a)*u
, \x -> sqrt ( max 0 ( (x-a) / (b-a) ) ) )
| sb <= tol && sb < sa = case meth of
SquareRoot ->
( \u -> speed ( b - (b-a)*(1-u)*(1-u) ) * 2*(b-a)*(1-u)
, \x -> 1 - sqrt ( max 0 ( (b-x) / (b-a) ) ) )
| otherwise =
( \u -> speed ( a + (b-a)*u ) * (b-a)
, \x -> (x-a) / (b-a) )
where
sa = speed a
sb = speed b
{-# INLINEABLE applyRegulariser #-}
--------------------------------------------------------------------------------
-- Clenshaw–Curtis quadrature (continued)
-- | Composite Clenshaw–Curtis integrator that splits @[0,1]@ at the critical
-- points of @|B′(t)|²@ and optionally applies an endpoint regularisation
-- substitution on each piece at the low-speed endpoint.
--
-- The splitting avoids bad convergence around critical points; the
-- regularisation restores exponential convergence when a boundary point is a
-- critical point (Bernstein ellipse expansion).
makeCompositeCCIntegrator
:: forall n
. KnownNat n
=> Maybe Regulariser -- ^ cusp-endpoint regularisation
-> ( Double -> Double ) -- ^ speed function on @[0,1]@
-> [ Double ] -- ^ critical points of @|speed|²@ in @(0,1)@, sorted ascending
-> ( Double -> Double ) -- ^ \( t \mapsto \int_0^t speed(x) dx \)
makeCompositeCCIntegrator reg speed critPts =
let
n = fromIntegral $ natVal' @n proxy#
bps = 0 : critPts ++ [1]
pieceData =
[ ( a, b, integr, toU, integr 1 )
| ( a, b ) <- zip bps ( drop 1 bps )
, b - a > 1e-8
, let ( f01, toU ) = applyRegulariser reg a b speed
ys = fromList
[ f01 ( ccNodes @n `Prim.indexPrimArray` k )
| k <- [ 0 .. n ] ]
integr = makeCCIntegrator @n ys
]
!totalLen = sum [ fullLen | (_, _, _, _, fullLen) <- pieceData ]
in \ t ->
if | t <= 0
-> 0
| t >= 1
-> totalLen
| otherwise
-> sum [ if b <= t then fullLen else integr ( toU t )
| ( a, b, integr, toU, fullLen ) <- pieceData
, a < t
]
{-# INLINEABLE makeCompositeCCIntegrator #-}
--------------------------------------------------------------------------------
-- Tanh–Sinh quadrature
-- | Composite Tanh–Sinh integrator that splits @[0,1]@ at the given subdivision
-- points (e.g. cusps), and applies Tanh–Sinh (double-exponential) quadrature
-- on each piece.
makeCompositeTSIntegrator
:: Int -- ^ @N@: nodes per side (@2N+1@ nodes total)
-> Double -- ^ @h@: step size
-> ( Double -> Double ) -- ^ speed function on @[0,1]@
-> [ Double ] -- ^ subdivision points
-> ( Double -> Double ) -- ^ \( t \mapsto \int_0^t speed(x) dx \)
makeCompositeTSIntegrator nodesPerSide h speed subdivPts =
let
pi_2 :: Double
pi_2 = pi / 2
bps :: [ Double ]
bps = 0 : subdivPts ++ [1]
n :: Int
n = 2 * nodesPerSide + 1 -- total number of nodes
-- Pre-compute normalised Tanh–Sinh nodes and weights for [-1,1]:
-- φ_k = tanh(π/2 · sinh(k·h)) node, ∈ (−1,1)
-- ψ_k = π/2 · cosh(k·h) / cosh²(π/2 · sinh(k·h)) weight kernel
--
-- Integral on [a,b] ≈ h · (b−a)/2 · Σ_k ψ_k · f( (a+b)/2 + (b−a)/2 · φ_k )
φs :: PrimArray Double
!φs = fromList
[ tanh ( pi_2 * sinh ( fromIntegral k * h ) )
| k <- [ -nodesPerSide .. nodesPerSide ]
]
ψs :: PrimArray Double
!ψs = fromList
[ let skh = sinh ( fromIntegral k * h )
ckh = cosh ( fromIntegral k * h )
c = cosh ( pi_2 * skh )
in pi_2 * ckh / ( c * c )
| k <- [ -nodesPerSide .. nodesPerSide ]
]
-- Tanh–Sinh quadrature of 'speed' on [a, b].
tsIntegral :: Double -> Double -> Double
tsIntegral a b =
let mid = ( a + b ) * 0.5
half = ( b - a ) * 0.5
go :: Double -> Int -> Double
go !acc !i
| i >= n = acc
| otherwise =
let φ = φs `Prim.indexPrimArray` i
ψ = ψs `Prim.indexPrimArray` i
in go ( acc + ψ * speed ( mid + half * φ ) ) ( i + 1 )
in h * half * go 0 0
pieceData :: [ ( Double, Double, Double ) ]
pieceData =
[ ( a, b, tsIntegral a b )
| ( a, b ) <- zip bps ( drop 1 bps )
, b - a > 1e-8
]
totalLen :: Double
!totalLen = sum [ fullLen | ( _, _, fullLen ) <- pieceData ]
in \ t ->
if
| t <= 0
-> 0
| t >= 1
-> totalLen
| otherwise
-> sum [ if t >= b then fullLen else tsIntegral a t
| ( a, b, fullLen ) <- pieceData
, a < t
]
{-# INLINEABLE makeCompositeTSIntegrator #-}
--------------------------------------------------------------------------------
-- Gauss–Legendre quadrature
-- | Compute @n@-point Gauss–Legendre nodes (on [−1,1]) and weights
-- using Newton iteration on the Legendre polynomial recurrence.
computeGLNodesWeights :: Int -> ( PrimArray Double, PrimArray Double )
computeGLNodesWeights n =
let
-- Evaluate (P_n(x), P_n′(x)) via the three-term recurrence:
-- P_0 = 1, P_0′ = 0
-- P_1 = x, P_1′ = 1
-- (k+1) P_{k+1} = (2k+1) x P_k − k P_{k−1}
legendre :: Double -> (# Double, Double #)
legendre x
| n == 0 = (# 1, 0 #)
| otherwise = go 1 1 x 0 1
where
go :: Int -> Double -> Double -> Double -> Double -> (# Double, Double #)
go !k !p_0 !p_1 !dp_0 !dp_1
| k == n = (# p_1, dp_1 #)
| otherwise =
let
kd, p_2, dp_2 :: Double
!kd = fromIntegral k
!p_2 = ( ( 2 * kd + 1 ) * x * p_1 - kd * p_0 ) / ( kd + 1 )
!dp_2 = ( ( 2 * kd + 1 ) * ( p_1 + x * dp_1 ) - kd * dp_0 ) / ( kd + 1 )
in go (k+1) p_1 p_2 dp_1 dp_2
-- Newton iteration to refine a root of P_n.
refineRoot :: Double -> Double
refineRoot x0 = fromMaybe x0 $ newtonRaphson 50 50 legendre x0
-- Compute the m largest positive roots (including 0 for odd n).
m = ( n + 1 ) `div` 2
halfData :: [ ( Double, Double ) ]
halfData =
[ let x_i = refineRoot ( cos ( pi * ( fromIntegral k - 0.25 )
/ ( fromIntegral n + 0.5 ) ) )
(# _, dp_i #) = legendre x_i
w_i = 2 / ( ( 1 - x_i * x_i ) * dp_i * dp_i )
in ( x_i, w_i )
| k <- [ 1 .. m ]
]
-- Use symmetry to obtain all nodes
( xs, ws ) = unzip $
concatMap
( \ ( x_i, w_i ) ->
if abs x_i < epsilon
then [ ( 0 , w_i ) ]
else [ ( x_i, w_i )
, ( -x_i, w_i ) ]
) halfData
in ( fromList xs
, fromList ws )
{-# INLINEABLE computeGLNodesWeights #-}
-- | Composite Gauss–Legendre integrator that splits @[0,1]@ at the critical
-- points of @|B′(t)|²@ and optionally applies an endpoint regularisation
-- substitution at the low-speed endpoint of each piece.
makeCompositeGLIntegrator
:: Int -- ^ number of quadrature nodes
-> Maybe Regulariser -- ^ cusp-endpoint regularisation
-> ( Double -> Double ) -- ^ speed function on @[0,1]@
-> [ Double ] -- ^ critical points of @|speed|²@ in @(0,1)@, sorted ascending
-> ( Double -> Double ) -- ^ \( t \mapsto \int_0^t speed(x) dx \)
makeCompositeGLIntegrator nPts reg speed critPts =
let
bps = 0 : critPts ++ [ 1 ]
( glXs, glWs ) = computeGLNodesWeights nPts
-- GL quadrature of f on [a, b].
glQuad :: ( Double -> Double ) -> Double -> Double -> Double
glQuad f a b =
let mid = ( a + b ) * 0.5
half = ( b - a ) * 0.5
go :: Double -> Int -> Double
go !acc !i
| i >= nPts = acc
| otherwise =
let xi = glXs `Prim.indexPrimArray` i
wi = glWs `Prim.indexPrimArray` i
in go ( acc + wi * f ( mid + half * xi ) ) ( i + 1 )
in half * go 0 0
pieceData :: [ ( Double, Double, Double -> Double -> Double, Double -> Double, Double ) ]
pieceData =
[ ( a, b, glQuad f01, toU, fullLen )
| ( a, b ) <- zip bps ( drop 1 bps )
, b - a > 1e-8
, let ( f01, toU ) = applyRegulariser reg a b speed
, let fullLen = glQuad f01 0 1
]
!totalLen = sum [ fullLen | ( _, _, _, _, fullLen ) <- pieceData ]
in \ t ->
if | t <= 0
-> 0
| t >= 1
-> totalLen
| otherwise
-> sum [ if b <= t then fullLen else intgr 0 ( toU t )
| ( a, b, intgr, toU, fullLen ) <- pieceData
, a < t
]
{-# INLINEABLE makeCompositeGLIntegrator #-}
--------------------------------------------------------------------------------
-- Gravesen adaptive subdivision
-- | Cumulative arc-length integrator for a quadratic Bézier using the
-- Gravesen (1993) adaptive polygon/chord method.
gravesenQuadIntegrator
:: forall diff ptData
. ( Torsor diff ptData, Module Double diff, Inner Double diff )
=> Quadratic.Bezier ptData
-> Double -- ^ @eps@: stopping tolerance
-> ( Double -> Double )
gravesenQuadIntegrator bez eps =
let !total = go eps bez
in \ t ->
if t <= 0 then 0
else if t >= 1 then total
else go eps $ fst $ Quadratic.subdivide @diff bez t
where
go :: Double -> Quadratic.Bezier ptData -> Double
go e b@( Quadratic.Bezier { p0, p1, p2 } ) =
let l0 = norm @diff ( p0 --> p2 )
l1 = norm @diff ( p0 --> p1 ) + norm @diff ( p1 --> p2 )
in if l1 - l0 < e
then ( 2 * l0 + l1 ) / 3
else let ( lb, rb ) = Quadratic.subdivide @diff b 0.5
in go ( e / 2 ) lb + go ( e / 2 ) rb
{-# INLINEABLE gravesenQuadIntegrator #-}
-- | Cumulative arc-length integrator for a cubic Bézier using the
-- Gravesen (1993) adaptive polygon/chord method.
gravesenCubicIntegrator
:: forall diff ptData
. ( Torsor diff ptData, Module Double diff, Inner Double diff )
=> Cubic.Bezier ptData
-> Double -- ^ @eps@: stopping tolerance
-> ( Double -> Double )
gravesenCubicIntegrator bez eps =
let !total = go eps bez
in \ t ->
if t <= 0 then 0
else if t >= 1 then total
else go eps $ fst $ Cubic.subdivide @diff bez t
where
go :: Double -> Cubic.Bezier ptData -> Double
go e b@( Cubic.Bezier { p0, p1, p2, p3 } ) =
let l0 = norm @diff ( p0 --> p3 )
l1 = norm @diff ( p0 --> p1 ) + norm @diff ( p1 --> p2 ) + norm @diff ( p2 --> p3 )
in if l1 - l0 < e
then ( l0 + l1 ) / 2 -- = (2·l0 + 2·l1) / 4, degree-3 Gravesen estimate
else let ( lb, rb ) = Cubic.subdivide @diff b 0.5
in go ( e / 2 ) lb + go ( e / 2 ) rb
{-# INLINEABLE gravesenCubicIntegrator #-}