{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE TemplateHaskell #-}
module Data.Geometry.BezierSpline(
BezierSpline (BezierSpline)
, controlPoints
, fromPointSeq
, evaluate
, split
, subBezier
, tangent
, approximate
, parameterOf
, snap
, pattern Bezier2, pattern Bezier3
) where
import Control.Lens hiding (Empty)
import qualified Data.Foldable as F
import Data.Geometry.Point
import Data.Geometry.Properties
import Data.Geometry.Transformation
import Data.Geometry.Vector
import Data.LSeq (LSeq)
import qualified Data.LSeq as LSeq
import Data.Sequence (Seq(..))
import qualified Data.Sequence as Seq
import Data.Traversable (fmapDefault,foldMapDefault)
import GHC.TypeNats
import qualified Test.QuickCheck as QC
newtype BezierSpline n d r = BezierSpline { _controlPoints :: LSeq (1+n) (Point d r) }
makeLenses ''BezierSpline
pattern Bezier2 :: Point d r -> Point d r -> Point d r -> BezierSpline 2 d r
pattern Bezier2 p q r <- ((F.toList . LSeq.take 3 . _controlPoints) -> [p,q,r])
where
Bezier2 p q r = fromPointSeq . Seq.fromList $ [p,q,r]
{-# COMPLETE Bezier2 #-}
pattern Bezier3 :: Point d r -> Point d r -> Point d r -> Point d r -> BezierSpline 3 d r
pattern Bezier3 p q r s <- ((F.toList . LSeq.take 4 . _controlPoints) -> [p,q,r,s])
where
Bezier3 p q r s = fromPointSeq . Seq.fromList $ [p,q,r,s]
{-# COMPLETE Bezier3 #-}
deriving instance (Arity d, Eq r) => Eq (BezierSpline n d r)
type instance Dimension (BezierSpline n d r) = d
type instance NumType (BezierSpline n d r) = r
instance (Arity n, Arity d, QC.Arbitrary r) => QC.Arbitrary (BezierSpline n d r) where
arbitrary = fromPointSeq . Seq.fromList <$> QC.vector (fromIntegral . (1+) . natVal $ C @n)
fromPointSeq :: Seq (Point d r) -> BezierSpline n d r
fromPointSeq = BezierSpline . LSeq.promise . LSeq.fromSeq
instance (Arity d, Show r) => Show (BezierSpline n d r) where
show (BezierSpline ps) =
mconcat [ "BezierSpline", show $ length ps - 1, " ", show (F.toList ps) ]
instance Arity d => Functor (BezierSpline n d) where
fmap = fmapDefault
instance Arity d => Foldable (BezierSpline n d) where
foldMap = foldMapDefault
instance Arity d => Traversable (BezierSpline n d) where
traverse f (BezierSpline ps) = BezierSpline <$> traverse (traverse f) ps
instance (Fractional r, Arity d, Arity (d + 1), Arity n)
=> IsTransformable (BezierSpline n d r) where
transformBy = transformPointFunctor
instance PointFunctor (BezierSpline n d) where
pmap f = over controlPoints (fmap f)
evaluate :: (Arity d, Ord r, Num r) => BezierSpline n d r -> r -> Point d r
evaluate b t = evaluate' (b^.controlPoints.to LSeq.toSeq)
where
evaluate' = \case
(p :<| Empty) -> p
pts@(_ :<| tl) -> let (ini :|> _) = pts in evaluate' $ Seq.zipWith blend ini tl
_ -> error "evaluate: absurd"
blend p q = p .+^ t *^ (q .-. p)
tangent :: (Arity d, Num r, 1 <= n) => BezierSpline n d r -> Vector d r
tangent b = b^?!controlPoints.ix 1 .-. b^?!controlPoints.ix 0
subBezier :: (KnownNat n, Arity d, Ord r, Num r)
=> r -> r -> BezierSpline n d r -> BezierSpline n d r
subBezier t u = fst . split u . snd . split t
split :: forall n d r. (KnownNat n, Arity d, Ord r, Num r)
=> r -> BezierSpline n d r -> (BezierSpline n d r, BezierSpline n d r)
split t b | t < 0 || t > 1 = error "Split parameter out of bounds."
| otherwise = let n = fromIntegral $ natVal (C @n)
ps = collect t $ b^.controlPoints
in ( fromPointSeq . Seq.take (n + 1) $ ps
, fromPointSeq . Seq.drop n $ ps
)
collect :: (Arity d, Ord r, Num r) => r -> LSeq n (Point d r) -> Seq (Point d r)
collect t = go . LSeq.toSeq
where
go = \case
ps@(_ :<| Empty) -> ps
ps@(p :<| tl) -> let (ini :|> q) = ps in (p :<| go (Seq.zipWith blend ini tl)) :|> q
_ -> error "collect: absurd"
blend p q = p .+^ t *^ (q .-. p)
approximate :: forall n d r. (KnownNat n, Arity d, Ord r, Fractional r)
=> r -> BezierSpline n d r -> [Point d r]
approximate eps b
| squaredEuclideanDist p q < eps^2 = [p,q]
| otherwise = let (b1, b2) = split 0.5 b
in approximate eps b1 ++ tail (approximate eps b2)
where
p = b^.controlPoints.to LSeq.head
q = b^.controlPoints.to LSeq.last
parameterOf :: (Arity d, Ord r, Fractional r) => BezierSpline n d r -> Point d r -> r
parameterOf b p = binarySearch (qdA p . evaluate b) treshold (1 - treshold)
where treshold = 0.0001
binarySearch :: (Ord r, Fractional r) => (r -> r) -> r -> r -> r
binarySearch f l r | abs (f l - f r) < treshold = m
| derivative f m > 0 = binarySearch f l m
| otherwise = binarySearch f m r
where m = (l + r) / 2
treshold = 0.0001
derivative :: Fractional r => (r -> r) -> r -> r
derivative f x = (f (x + delta) - f x) / delta
where delta = 0.00001
snap :: (Arity d, Ord r, Fractional r) => BezierSpline n d r -> Point d r -> Point d r
snap b = evaluate b . parameterOf b