{-# LANGUAGE BangPatterns, MultiWayIf #-}
module Geom2D.CubicBezier.Approximate
(approximatePath, approximateQuadPath, approximatePathMax, approximateQuadPathMax,
approximateCubic)
where
import Geom2D
import Geom2D.CubicBezier.Basic
import Geom2D.CubicBezier.Numeric
import Data.Maybe
import Data.List
import qualified Data.Vector.Unboxed as V
import qualified Data.Map as M
import Data.Function
interpolate :: (Num a) => a -> a -> a -> a
interpolate a b x = (1-x)*a + x*b
approximatePath :: (V.Unbox a, Ord a, Floating a) =>
(a -> (Point a, Point a))
-> Int
-> a
-> a
-> a
-> Bool
-> [CubicBezier a]
approximatePath f n tol tmin tmax fast
| err < tol = [curve]
| otherwise = approximatePath' f n tol tmin tmax fast
where
(curve, err) = approx1cubic n f tmin tmax (if fast then 0 else 5)
{-# SPECIALIZE approximatePath :: (Double -> (DPoint, DPoint)) -> Int -> Double
-> Double -> Double -> Bool -> [CubicBezier Double] #-}
approximateQuadPath :: (Show a, V.Unbox a, Ord a, Floating a) =>
(a -> (Point a, Point a))
-> a
-> a
-> a
-> Bool
-> [QuadBezier a]
approximateQuadPath f tol tmin tmax fast
| err < tol = [curve]
| otherwise = approximateQuad' f tol tmin tmax fast
where
curve = approx1quad f tmin tmax
err = maxDist f curve tmin tmax (if fast then 0 else 5)
{-# SPECIALIZE approximateQuadPath :: (Double -> (DPoint, DPoint)) -> Double ->
Double -> Double -> Bool -> [QuadBezier Double] #-}
quadDist :: (V.Unbox a, Ord a, Floating a) =>
(a -> (Point a, Point a)) -> QuadBezier a -> a
-> a -> Int -> a -> a
quadDist f qb tmin tmax maxiter t =
quadDist' f qb tmin tmax t (fst (f $ interpolate tmin tmax t)) maxiter (evalBezierDeriv qb t)
quadDist' :: (V.Unbox a, Ord a, Floating a) =>
(a -> (Point a, Point a))
-> QuadBezier a -> a -> a -> a -> Point a -> Int -> (Point a, Point a) -> a
quadDist' f qb tmin tmax t p maxiter (b, b')
| maxiter <= 1 || abs (err2-err1) <= err1 * (1/8) = err2
| otherwise = quadDist' f qb tmin tmax (t+ndist) p (maxiter-1) (b2, b2')
where dp = p ^-^ b
err1 = vectorMag dp
ndist = (dp ^.^ b') / (b' ^.^ b')
(b2, b2') = evalBezierDeriv qb (t+ndist)
err2 = vectorDistance p b2
maxDist :: (V.Unbox a, Ord a, Floating a) =>
(a -> (Point a, Point a)) ->
QuadBezier a -> a -> a -> Int -> a
maxDist f qb tmin tmax maxiter =
quadDist f qb tmin tmax maxiter $
goldSearch (quadDist f qb tmin tmax maxiter) 4
approxquad :: (Ord a, Floating a) =>
Point a -> Point a -> Point a -> Point a -> QuadBezier a
approxquad p0 p0' p1' p1
| abs (pointY q') < abs (pointX q'*1e-3) =
QuadBezier p0 (interpolateVector p0 p1 0.5) p1
| otherwise = QuadBezier p0 (p1^+^p1'^*t) p1
where
q = rotateVec (flipVector p0') $* p1^-^p0
q' = rotateVec (flipVector p0') $* p1'
t = - pointY q / pointY q'
approx1quad :: (Ord a, Floating a) =>
(a -> (Point a, Point a)) -> a -> a -> QuadBezier a
approx1quad f tmin tmax =
approxquad p0 p0' p1' p1
where (p0, p0') = f tmin
(p1, p1') = f tmax
splitQuad :: (Show a, V.Unbox a, Ord a, Floating a) =>
a -> a -> (a -> (Point a, Point a))
-> a -> a -> Int -> (a, a, QuadBezier a, a, QuadBezier a)
splitQuad node offset f tmin tmax maxiter =
splitQuad' node offset f tmin tmax maxiter maxiter
splitQuad' :: (Show a, V.Unbox a, Ord a, Floating a) =>
a -> a -> (a -> (Point a, Point a))
-> a -> a -> Int -> Int -> (a, a, QuadBezier a, a, QuadBezier a)
splitQuad' node offset f tmin tmax maxiter maxiter2
| maxiter < 1 || (err0 < 2*err1 && err0 > err1/2) =
(tmid, err0, curve0, err1, curve1)
| otherwise =
splitQuad' (if err0 < err1 then node+offset else node-offset)
(offset/2) f tmin tmax (maxiter-1) maxiter2
where
tmid = interpolate tmin tmax node
curve0 = approx1quad f tmin tmid
err0 = maxDist f curve0 tmin tmid maxiter2
curve1 = approx1quad f tmid tmax
err1 = maxDist f curve1 tmid tmax maxiter2
approximateQuad' :: (Show a, V.Unbox a, Ord a, Floating a) =>
(a -> (Point a, Point a)) ->
a -> a -> a -> Bool ->
[QuadBezier a]
approximateQuad' f tol tmin tmax fast =
(if err0 <= tol
then [curve0]
else approximateQuad' f tol tmin tmid fast) ++
(if err1 <= tol
then [curve1]
else approximateQuad' f tol tmid tmax fast)
where
(tmid, err0, curve0, err1, curve1) =
splitQuad 0.5 0.25 f tmin tmax (if fast then 0 else 5)
approximatePath' :: (V.Unbox a, Ord a, Floating a) =>
(a -> (Point a, Point a)) -> Int ->
a -> a -> a -> Bool ->
[CubicBezier a]
approximatePath' f n tol tmin tmax fast =
(if err0 <= tol
then [curve0]
else approximatePath' f n tol tmin tmid fast) ++
(if err1 <= tol
then [curve1]
else approximatePath' f n tol tmid tmax fast)
where
(tmid, err0, curve0, err1, curve1) =
splitCubic n 0.5 0.25 f tmin tmax (if fast then 0 else 5)
approximatePathMax :: (V.Unbox a, Floating a, Ord a) =>
Int
-> (a -> (Point a, Point a))
-> Int
-> a
-> a
-> a
-> Bool
-> [CubicBezier a]
approximatePathMax m f samples tol tmin tmax fast =
approxMax f tol m fast (splitCubic samples) segments
where segments = M.singleton err (FunctionSegment tmin tmax outline)
(p0, p0') = f tmin
(p1, p1') = f tmax
ts = V.map (\i -> fromIntegral i/(fromIntegral samples+1) `asTypeOf` tmin) $
V.enumFromN (1::Int) samples
points = V.map (fst . f . interpolate tmin tmax) ts
curveCb = CubicBezier p0 (p0^+^p0') (p1^-^p1') p1
(outline, err) =
approximateCubic curveCb points (Just ts) (if fast then 0 else 5)
{-# SPECIALIZE approximatePathMax ::
Int -> (Double -> (Point Double, Point Double)) -> Int
-> Double -> Double -> Double -> Bool -> [CubicBezier Double] #-}
data FunctionSegment b a = FunctionSegment {
fsTmin :: !a,
_fsTmax :: !a,
fsCurve :: b
}
approximateQuadPathMax :: (V.Unbox a, Show a, Floating a, Ord a) =>
Int
-> (a -> (Point a, Point a))
-> a
-> a
-> a
-> Bool
-> [QuadBezier a]
approximateQuadPathMax m f tol tmin tmax fast =
approxMax f tol m fast splitQuad segments
where segments = M.singleton err (FunctionSegment tmin tmax curveQd)
(p0, p0') = f tmin
(p1, p1') = f tmax
curveQd = approxquad p0 p0' p1' p1
err = maxDist f curveQd tmin tmax (if fast then 0 else 5)
{-# SPECIALIZE approximateQuadPathMax ::
Int -> (Double -> (Point Double, Point Double))
-> Double -> Double -> Double -> Bool -> [QuadBezier Double] #-}
approxMax :: (V.Unbox a, Ord a, Floating a) =>
(a -> (Point a, Point a)) -> a -> Int -> Bool
-> (a -> a -> (a -> (Point a, Point a)) -> a -> a -> Int -> (a, a, b, a, b))
-> M.Map a (FunctionSegment b a)
-> [b]
approxMax f tol maxCurves fast splitBez segments
| (maxCurves <= 1) || (err < tol) =
map fsCurve $ sortBy (compare `on` fsTmin) $
map snd $ M.toList segments
| otherwise = approxMax f tol (maxCurves-1) fast splitBez $
M.insert err_l (FunctionSegment t_min t_mid curve_l) $
M.insert err_r (FunctionSegment t_mid t_max curve_r)
newSegments
where
((err, FunctionSegment t_min t_max _), newSegments) =
M.deleteFindMax segments
(t_mid, err_l, curve_l, err_r, curve_r) =
splitBez 0.5 0.25 f t_min t_max (if fast then 0 else 5)
splitCubic :: (V.Unbox a, Ord a, Floating a) =>
Int -> a -> a -> (a -> (Point a, Point a))
-> a -> a -> Int -> (a, a, CubicBezier a, a, CubicBezier a)
splitCubic n node offset f tmin tmax maxiter
| maxiter < 1 || (err0 < 2*err1 && err0 > err1/2) =
(tmid, err0, curve0, err1, curve1)
| otherwise =
splitCubic n (if err0 < err1 then node+offset else node-offset)
(offset/2) f tmin tmax (maxiter-1)
where
tmid = interpolate tmin tmax node
(curve0, err0) = approx1cubic n f tmin tmid maxiter
(curve1, err1) = approx1cubic n f tmid tmax maxiter
approx1cubic :: (V.Unbox a, Ord a, Floating a) =>
Int -> (a -> (Point a, Point a)) -> a -> a ->
Int -> (CubicBezier a, a)
approx1cubic n f t0 t1 maxiter =
approximateCubic curveCb points (Just ts) maxiter
where (p0, p0') = f t0
(p1, p1') = f t1
ts = V.map (\i -> fromIntegral i/(fromIntegral n+1))
(V.enumFromN 1 n :: V.Vector Int)
points = V.map (fst . f . interpolate t0 t1) ts
curveCb = CubicBezier p0 (p0^+^p0') (p1^+^p1') p1
{-# SPECIALIZE approx1cubic :: Int -> (Double -> (Point Double, Point Double)) -> Double -> Double -> Int -> (CubicBezier Double, Double) #-}
approximateCubic :: (V.Unbox a, Ord a, Floating a) =>
CubicBezier a
-> V.Vector (Point a)
-> Maybe (V.Vector a)
-> Int
-> (CubicBezier a, a)
approximateCubic curve pts mbTs maxiter =
let ts = fromMaybe (approximateParams (cubicC0 curve) (cubicC3 curve) pts) mbTs
curve2 = fromMaybe curve $ lsqDist curve pts ts
(bt, bt') = V.unzip $ V.map (evalBezierDeriv curve2) ts
err = V.maximum $ V.zipWith vectorDistance pts bt
(c, _, _, err2, _) =
fromMaybe (curve2, ts, undefined, err, undefined) $
approximateCubic' curve2 pts ts maxiter err bt bt'
in (c, err2)
{-# SPECIALIZE approximateCubic :: CubicBezier Double -> V.Vector (Point Double)
-> Maybe (V.Vector Double) -> Int -> (CubicBezier Double, Double) #-}
leastSquares :: (V.Unbox a, Fractional a, Eq a) =>
V.Vector a -> V.Vector a -> V.Vector a -> Maybe (a, a)
leastSquares as bs epses = solveLinear2x2 a b c d e f
where
square x = x*x
a = V.sum $ V.map square as
b = V.sum $ V.zipWith (*) as bs
c = V.sum $ V.zipWith (*) as epses
d = b
e = V.sum $ V.map square bs
f = V.sum $ V.zipWith (*) bs epses
{-# SPECIALIZE leastSquares ::V.Vector Double -> V.Vector Double -> V.Vector Double -> Maybe (Double, Double) #-}
lsqDist :: (V.Unbox a, Fractional a, Eq a) =>
CubicBezier a
-> V.Vector (Point a) -> V.Vector a -> Maybe (CubicBezier a)
lsqDist (CubicBezier (Point !p1x !p1y) (Point !p2x !p2y) (Point !p3x !p3y) (Point !p4x !p4y)) pts ts = let
calcParams t (Point px py) = let
t2 = t * t; t3 = t2 * t
ax = 3 * (p2x - p1x) * (t3 - 2 * t2 + t)
ay = 3 * (p2y - p1y) * (t3 - 2 * t2 + t)
bx = 3 * (p3x - p4x) * (t2 - t3)
by = 3 * (p3y - p4y) * (t2 - t3)
cx = (p4x - p1x) * (3 * t2 - 2 * t3) + p1x - px
cy = (p4y - p1y) * (3 * t2 - 2 * t3) + p1y - py
in (ax * ax + ay * ay,
ax * bx + ay * by,
ax * cx + ay * cy,
bx * ax + by * ay,
bx * bx + by * by,
bx * cx + by * cy)
add6 (!a,!b,!c,!d,!e,!f) (!a',!b',!c',!d',!e',!f') =
(a+a',b+b',c+c',d+d',e+e',f+f')
( as, bs, cs, ds, es, fs ) = V.foldl1' add6 $ V.zipWith calcParams ts pts
in do (alpha1, alpha2) <- solveLinear2x2 as bs cs ds es fs
let cp1 = Point (alpha1 * (p2x - p1x) + p1x) (alpha1 * (p2y - p1y) + p1y)
cp2 = Point (alpha2 * (p3x - p4x) + p4x) (alpha2 * (p3y - p4y) + p4y)
Just $ CubicBezier (Point p1x p1y) cp1 cp2 (Point p4x p4y)
{-# SPECIALIZE lsqDist :: CubicBezier Double
-> V.Vector (Point Double) -> V.Vector Double -> Maybe (CubicBezier Double) #-}
approximateCubic' :: (V.Unbox a, Ord a, Floating a) =>
CubicBezier a
-> V.Vector (Point a) -> V.Vector a
-> Int -> a -> V.Vector (Point a)
-> V.Vector (Point a)
-> Maybe (CubicBezier a, V.Vector a, V.Vector a, a, V.Vector (Point a))
approximateCubic' (CubicBezier p1 p2 p3 p4) pts ts maxiter err bt bt' = do
let dir1 = V.map (($* (p2^-^p1)) . rotateVec . flipVector) bt'
dir2 = V.map (($* (p3^-^p4)) . rotateVec . flipVector) bt'
ps = V.zipWith3 (\b b' p ->
rotateVec (flipVector b') $*
(p^-^b)) bt bt' pts
errs = V.map (negate.pointY) ps
as = V.zipWith (\d t -> 3*pointY d*(1-t)*(1-t)*t)
dir1 ts
bs = V.zipWith (\d t -> 3*pointY d*(1-t)*t*t)
dir2 ts
(a,b) <- leastSquares as bs errs
let newTs = V.zipWith5 (\t p d1 d2 b' ->
max 0 $ min 1 $
t + (pointX p - 3*(1-t)*t*(a*pointX d1*(1-t) +
b*pointX d2*t)) /
vectorMag b')
ts ps dir1 dir2 bt'
newCurve = CubicBezier p1 (p2 ^+^ a*^(p2^-^p1)) (p3 ^+^ b*^(p3^-^p4)) p4
(bt2,bt2') = V.unzip $ V.map (evalBezierDeriv newCurve) newTs
err2 = V.zipWith vectorDistance pts bt2
maxErr = V.maximum err2
if maxiter < 1 || abs(err - maxErr) <= err/8
then return (newCurve, newTs, err2, maxErr, bt2)
else approximateCubic' newCurve pts newTs (maxiter-1) maxErr bt2 bt2'
{-# SPECIALIZE approximateCubic' ::
CubicBezier Double -> V.Vector (Point Double) -> V.Vector Double
-> Int -> Double -> V.Vector (Point Double)
-> V.Vector (Point Double)
-> Maybe (CubicBezier Double, V.Vector Double, V.Vector Double, Double, V.Vector (Point Double)) #-}
approximateParams :: (V.Unbox a, Floating a) =>
Point a -> Point a -> V.Vector (Point a) -> V.Vector a
approximateParams start end pts
| V.null pts = V.empty
| otherwise =
let dists = V.generate (V.length pts)
(\i -> if i == 0
then vectorDistance start (V.unsafeIndex pts 0)
else vectorDistance (V.unsafeIndex pts (i-1)) (V.unsafeIndex pts i))
total = V.sum dists + vectorDistance (V.last pts) end
in V.map (/ total) $ V.scanl1 (+) dists
{-# SPECIALIZE approximateParams ::
Point Double -> Point Double -> V.Vector (Point Double) -> V.Vector Double #-}