cubicbezier 0.1.0 → 0.2.0
raw patch · 11 files changed
+729/−508 lines, 11 filesdep +deepseq
Dependencies added: deepseq
Files
- Geom2D.hs +23/−9
- Geom2D/CubicBezier.hs +6/−2
- Geom2D/CubicBezier/Approximate.hs +110/−15
- Geom2D/CubicBezier/Basic.hs +34/−22
- Geom2D/CubicBezier/Curvature.hs +23/−18
- Geom2D/CubicBezier/Intersection.hs +39/−21
- Geom2D/CubicBezier/MetaPath.hs +430/−0
- Geom2D/CubicBezier/Outline.hs +7/−62
- LICENSE +25/−336
- Math/BernsteinPoly.hs +27/−20
- cubicbezier.cabal +5/−3
Geom2D.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE BangPatterns #-}+ -- | Basic 2 dimensional geometry functions. module Geom2D where @@ -6,8 +8,8 @@ infixr 5 $* data Point = Point {- pointX :: Double,- pointY :: Double}+ pointX :: {-# UNPACK #-} !Double,+ pointY :: {-# UNPACK #-} !Double} instance Show Point where show (Point x y) =@@ -15,12 +17,12 @@ -- | A transformation (x, y) -> (ax + by + c, dx + ey + d) data Transform = Transform {- xformA :: Double,- xformB :: Double,- xformC :: Double,- xformD :: Double,- xformE :: Double,- xformF :: Double }+ xformA :: {-# UNPACK #-} !Double,+ xformB :: {-# UNPACK #-} !Double,+ xformC :: {-# UNPACK #-} !Double,+ xformD :: {-# UNPACK #-} !Double,+ xformE :: {-# UNPACK #-} !Double,+ xformF :: {-# UNPACK #-} !Double } deriving Show data Line = Line Point Point@@ -49,7 +51,7 @@ inverse :: Transform -> Maybe Transform inverse (Transform a b c d e f) = case a*e - b*d of 0 -> Nothing- det -> Just $ Transform (a/det) (d/det) (-(a*c + d*f)/det) (b/det) (e/det)+ det -> Just $! Transform (a/det) (d/det) (-(a*c + d*f)/det) (b/det) (e/det) (-(b*c + e*f)/det) -- | Return the parameters (a, b, c) for the normalised equation@@ -71,15 +73,18 @@ -- | The lenght of the vector. vectorMag :: Point -> Double vectorMag (Point x y) = sqrt(x*x + y*y)+{-# INLINE vectorMag #-} -- | The angle of the vector, in the range @(-'pi', 'pi']@. vectorAngle :: Point -> Double vectorAngle (Point 0.0 0.0) = 0.0 vectorAngle (Point x y) = atan2 y x+{-# INLINE vectorAngle #-} -- | The unitvector with the given angle. dirVector :: Double -> Point dirVector angle = Point (cos angle) (sin angle)+{-# INLINE dirVector #-} -- | The unit vector with the same direction. normVector :: Point -> Point@@ -89,38 +94,47 @@ -- | Scale vector by constant. (*^) :: Double -> Point -> Point s *^ (Point x y) = Point (s*x) (s*y)+{-# INLINE (*^) #-} -- | Scale vector by reciprocal of constant. (^/) :: Point -> Double -> Point (Point x y) ^/ s = Point (x/s) (y/s)+{-# INLINE (^/) #-} -- | Scale vector by constant, with the arguments swapped. (^*) :: Point -> Double -> Point p ^* s = s *^ p+{-# INLINE (^*) #-} -- | Add two vectors. (^+^) :: Point -> Point -> Point (Point x1 y1) ^+^ (Point x2 y2) = Point (x1+x2) (y1+y2)+{-# INLINE (^+^) #-} -- | Subtract two vectors. (^-^) :: Point -> Point -> Point (Point x1 y1) ^-^ (Point x2 y2) = Point (x1-x2) (y1-y2)+{-# INLINE (^-^) #-} -- | Dot product of two vectors. (^.^) :: Point -> Point -> Double (Point x1 y1) ^.^ (Point x2 y2) = x1*x2 + y1*y2+{-# INLINE (^.^) #-} -- | Cross product of two vectors. vectorCross :: Point -> Point -> Double vectorCross (Point x1 y1) (Point x2 y2) = x1*y2 - y1*x2+{-# INLINE vectorCross #-} -- | Distance between two vectors. vectorDistance :: Point -> Point -> Double vectorDistance p q = vectorMag (p^-^q)+{-# INLINE vectorDistance #-} -- | Interpolate between two vectors. interpolateVector :: Point -> Point -> Double -> Point interpolateVector a b t = t*^b ^+^ (1-t)*^a+{-# INLINE interpolateVector #-} -- | Create a transform that rotates by the angle of the given vector -- with the x-axis
Geom2D/CubicBezier.hs view
@@ -1,18 +1,22 @@--- | Export all the cubic bezier functions, but not the numeric helper functions+-- | Export all the cubic bezier functions. module Geom2D.CubicBezier (module Geom2D.CubicBezier.Basic, module Geom2D.CubicBezier.Approximate, module Geom2D.CubicBezier.Outline, module Geom2D.CubicBezier.Curvature,- module Geom2D.CubicBezier.Intersection+ module Geom2D.CubicBezier.Intersection,+ module Geom2D.CubicBezier.MetaPath,+ module Geom2D ) where +import Geom2D import Geom2D.CubicBezier.Basic import Geom2D.CubicBezier.Approximate import Geom2D.CubicBezier.Outline import Geom2D.CubicBezier.Curvature import Geom2D.CubicBezier.Intersection+import Geom2D.CubicBezier.MetaPath
Geom2D/CubicBezier/Approximate.hs view
@@ -1,5 +1,6 @@+{-# LANGUAGE BangPatterns #-} module Geom2D.CubicBezier.Approximate (- approximateCurve, approximateCurveWithParams)+ approximatePath, approximatePathMax, approximateCurve, approximateCurveWithParams) where import Geom2D import Geom2D.CubicBezier.Numeric@@ -7,7 +8,92 @@ import Data.Function import Data.List import Data.Maybe+import qualified Data.Map as M +interpolate :: Double -> Double -> Double -> Double+interpolate a b x = (1-x)*a + x*b++-- | Approximate a function with piecewise cubic bezier splines using+-- a least-squares fit, within the given tolerance. Each subcurve is+-- approximated by using a finite number of samples. It is recommended+-- to avoid changes in direction by subdividing the original function+-- at points of inflection.++approximatePath :: (Double -> (Point, Point)) -- ^ The function to approximate and it's derivative+ -> Double -- ^ The number of discrete samples taken to approximate each subcurve+ -> Double -- ^ The tolerance+ -> Double -- ^ The lower parameter of the function + -> Double -- ^ The upper parameter of the function+ -> [CubicBezier]+approximatePath f n tol tmin tmax+ | err <= tol = [cb_out]+ | otherwise = approximatePath f n tol tmin terr +++ approximatePath f n tol terr tmax+ where+ (cb_out, terr', err) = approximateCurveWithParams curveCb+ points ts tol+ terr = interpolate tmin tmax terr'+ ts = [i/(n+1) | i <- [1..n]]+ points = map (fst . f . interpolate tmin tmax) ts+ (t0, t0') = f tmin+ (t1, t1') = f tmax+ curveCb = CubicBezier t0 (t0^+^t0') (t1^-^t1') t1+++-- | Like approximatePath, but limit the number of subcurves.+approximatePathMax :: Int -- ^ The maximum number of subcurves+ -> (Double -> (Point, Point)) -- ^ The function to approximate and it's derivative+ -> Double -- ^ The number of discrete samples taken to approximate each subcurve+ -> Double -- ^ The tolerance+ -> Double -- ^ The lower parameter of the function + -> Double -- ^ The upper parameter of the function+ -> [CubicBezier]+approximatePathMax m f n tol tmin tmax =+ approxMax f tol m ts segments+ where segments = M.singleton err (FunctionSegment tmin tmax t_err outline)+ (p0, p0') = f tmin+ (p1, p1') = f tmax+ ts = [i/(n+1) | i <- [1..n]]+ points = map (fst . f . interpolate tmin tmax) ts+ curveCb = CubicBezier p0 (p0^+^p0') (p1^-^p1') p1+ (outline, t_err', err) = approximateCurveWithParams curveCb+ points ts tol+ t_err = interpolate tmin tmax t_err'++data FunctionSegment = FunctionSegment {+ fs_t_min :: {-# UNPACK #-} !Double, -- the least t param of the segment in the original curve+ _fs_t_max :: {-# UNPACK #-} !Double, -- the max t param of the segment in the original curve+ _fs_t_err :: {-# UNPACK #-} !Double, -- the param where the error is maximal+ fs_curve :: CubicBezier -- the curve segment+ }++-- Keep a map from maxError to FunctionSegment for each subsegment to keep+-- track of the segment with the maximum error. This ensures a n+-- log(n) execution time, rather than n^2 when a list is used.+approxMax :: (Double -> (Point, Point)) -> Double -> Int+ -> [Double] -> M.Map Double FunctionSegment -> [CubicBezier]+approxMax f tol n ts segments+ | n < 1 = error "Minimum number of segments is one."+ | (n == 1) || (err < tol) =+ map fs_curve $ sortBy (compare `on` fs_t_min) $ map snd $ M.toList segments+ | otherwise = approxMax f tol (n-1) ts $+ M.insert err_l (FunctionSegment t_min t_err t_err_l curve_l) $+ M.insert err_r (FunctionSegment t_err t_max t_err_r curve_r)+ newSegments+ where+ ((err, FunctionSegment t_min t_max t_err _), newSegments) = M.deleteFindMax segments+ (fmin, fmin') = f t_min+ (fmid, fmid') = f t_err+ (fmax, fmax') = f t_max+ fcurve_l = CubicBezier fmin (fmin^+^fmin') (fmid^-^fmid') fmid+ fcurve_r = CubicBezier fmid (fmid^+^fmid') (fmax^-^fmax') fmax+ pointsl = map (fst . f . interpolate t_min t_err) ts+ pointsr = map (fst . f . interpolate t_err t_max) ts+ t_err_l = interpolate t_min t_err t_err_l'+ t_err_r = interpolate t_err t_max t_err_r'+ (curve_l, t_err_l', err_l) = approximateCurveWithParams fcurve_l pointsl ts tol+ (curve_r, t_err_r', err_r) = approximateCurveWithParams fcurve_r pointsr ts tol+ -- | @approximateCurve b pts eps@ finds the least squares fit of a bezier -- curve to the points @pts@. The resulting bezier has the same first -- and last control point as the curve @b@, and have tangents colinear with @b@.@@ -31,9 +117,16 @@ (t, maxError) = maximumBy (compare `on` snd) (zip ts distances) in (c, t, maxError) -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')+data LSParams = LSParams {-# UNPACK #-} !Double+ {-# UNPACK #-} !Double+ {-# UNPACK #-} !Double+ {-# UNPACK #-} !Double+ {-# UNPACK #-} !Double+ {-# UNPACK #-} !Double +addParams :: LSParams -> LSParams -> LSParams+addParams (LSParams a b c d e f) (LSParams a' b' c' d' e' f') =+ LSParams (a+a') (b+b') (c+c') (d+d') (e+e') (f+f') -- find the least squares between the points p_i and B(t_i) for -- bezier curve B, where pts contains the points p_i and ts@@ -46,8 +139,8 @@ -- with two unknown values (alpha1 and alpha2), which can be -- solved easily leastSquares :: CubicBezier -> [Point] -> [Double] -> Maybe CubicBezier-leastSquares (CubicBezier (Point p1x p1y) (Point p2x p2y) (Point p3x p3y) (Point p4x p4y)) pts ts = let- calcParams t (Point px py) = let+leastSquares (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)@@ -55,13 +148,14 @@ 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)- (a, b, c, d, e, f) = foldl1' add6 $ zipWith calcParams ts pts+ in LSParams+ (ax * ax + ay * ay)+ (ax * bx + ay * by)+ (ax * cx + ay * cy)+ (bx * ax + by * ay)+ (bx * bx + by * by)+ (bx * cx + by * cy)+ LSParams !a !b !c !d !e !f = foldl1' addParams (zipWith calcParams ts pts) in do (alpha1, alpha2) <- solveLinear2x2 a b c d e f let cp1 = Point (alpha1 * (p2x - p1x) + p1x) (alpha1 * (p2y - p1y) + p1y) cp2 = Point (alpha2 * (p3x - p4x) + p4x) (alpha2 * (p3y - p4y) + p4y)@@ -76,8 +170,8 @@ newCurve <- leastSquares curve pts ts let deltaTs = zipWith (calcDeltaT newCurve) pts ts ts' = map (max 0 . min 1) $ zipWith (-) ts deltaTs- newCurve <- leastSquares curve pts ts'- let deltaTs' = zipWith (calcDeltaT newCurve) pts ts'+ newerCurve <- leastSquares curve pts ts'+ let deltaTs' = zipWith (calcDeltaT newerCurve) pts ts' newTs = interpolateTs ts ts' deltaTs deltaTs' thisDeltaT = maximum $ map abs $ zipWith (-) newTs ts if maxiter < 1 ||@@ -122,7 +216,8 @@ -- the reduction of t is one iteration of Newton Raphson: f'(t)/f''(t) -- using more iterations doesn't appear to give an improvement -- See Curve Fitting with Piecewise Parametric Cubics by Stone & Plass-calcDeltaT curve (Point ptx pty) t = let+calcDeltaT :: CubicBezier -> Point -> Double -> Double+calcDeltaT curve (Point !ptx !pty) t = let [Point bezx bezy, Point dbezx dbezy, Point ddbezx ddbezy, _] = evalBezierDerivs curve t in ((bezx - ptx) * dbezx + (bezy - pty) * dbezy) / (dbezx * dbezx + dbezy * dbezy + (bezx - ptx) * ddbezx + (bezy - pty) * ddbezy)
Geom2D/CubicBezier/Basic.hs view
@@ -19,7 +19,10 @@ bezierC3 :: Point} deriving Show data PathJoin = JoinLine | JoinCurve Point Point-data Path = Path Point [(PathJoin, Point)]+ deriving Show+data Path = OpenPath [(Point, PathJoin)] Point+ | ClosedPath [(Point, PathJoin)]+ deriving Show instance AffineTransform CubicBezier where transform t (CubicBezier c0 c1 c2 c3) =@@ -39,11 +42,11 @@ -- can use the maximum of the convex hull of the derivative, and double it to -- have some margin for larger values. bezierParamTolerance :: CubicBezier -> Double -> Double-bezierParamTolerance (CubicBezier p1 p2 p3 p4) eps = eps / maxDist+bezierParamTolerance (CubicBezier !p1 !p2 !p3 !p4) eps = eps / maxDist where - maxDist = 6 * maximum [vectorDistance p1 p2,- vectorDistance p2 p3,- vectorDistance p3 p4]+ maxDist = 6 * (max (vectorDistance p1 p2) $+ max (vectorDistance p2 p3)+ (vectorDistance p3 p4)) -- | Reorient to the curve B(1-t). reorient :: CubicBezier -> CubicBezier@@ -57,23 +60,28 @@ -- | Calculate a value on the curve. evalBezier :: CubicBezier -> Double -> Point-evalBezier b t = Point (bernsteinEval x t) (bernsteinEval y t)- where (x, y) = bezierToBernstein b+evalBezier b = fst . evalBezierDeriv b -- | Calculate a value and the first derivative on the curve. evalBezierDeriv :: CubicBezier -> Double -> (Point, Point)-evalBezierDeriv b =- let (px, py) = bezierToBernstein b- px' = bernsteinDeriv px- py' = bernsteinDeriv py- in \t -> (Point (bernsteinEval px t) (bernsteinEval py t),- Point (bernsteinEval px' t) (bernsteinEval py' t))-+evalBezierDeriv (CubicBezier !p0 !p1 !p2 !p3) t = (bt, bt')+ where+ b0' = 3*^(p1^-^p0)+ b0'' = 2*^(3*^(p2^-^p1) ^-^ b0')+ b0''' = 6*^(p3^-^ 2*^p2 ^+^ p1) ^-^ b0''+ bt' = b0'^+^(b0''^+^ t*^b0'''^/2)^*t+ bt = p0 ^+^ t*^(b0' ^+^ t*^(b0''^/2 ^+^ t*^(b0'''^/6)))+ -- | Calculate a value and all derivatives on the curve. evalBezierDerivs :: CubicBezier -> Double -> [Point]-evalBezierDerivs b t = zipWith Point (bernsteinEvalDerivs px t)- (bernsteinEvalDerivs py t)- where (px, py) = bezierToBernstein b+evalBezierDerivs (CubicBezier !p0 !p1 !p2 !p3) t = [bt, bt', bt'', b0''']+ where+ b0' = 3*^(p1^-^p0)+ b0'' = 2*^(3*^(p2^-^p1) ^-^ b0')+ b0''' = 6*^(p3^-^ 2*^p2 ^+^ p1) ^-^ b0''+ bt'' = b0''^+^ b0'''^*t+ bt' = b0'^+^(b0''^+^ t*^b0'''^/2)^*t+ bt = p0 ^+^ t*^(b0' ^+^ t*^(b0''^/2 ^+^ t*^(b0'''^/6))) -- | @findBezierTangent p b@ finds the parameters where -- the tangent of the bezier curve @b@ has the same direction as vector p.@@ -140,10 +148,11 @@ where distDeriv t' = vectorMag $ snd $ evalD t' evalD = evalBezierDeriv b +outline :: CubicBezier -> Double outline (CubicBezier c0 c1 c2 c3) =- sum [vectorDistance c0 c1,- vectorDistance c1 c2,- vectorDistance c2 c3]+ vectorDistance c0 c1 ++ vectorDistance c1 c2 ++ vectorDistance c2 c3 arcLengthEstimate :: CubicBezier -> Double -> (Double, (Double, Double)) arcLengthEstimate b eps = (arclen, (estimate, ol))@@ -153,17 +162,20 @@ ol = outline b (arcL, (estL, olL)) = arcLengthEstimate bl eps (arcR, (estR, olR)) = arcLengthEstimate br eps- arclen | (abs(estL + estR - estimate) < eps) = estL + estR+ arclen | abs(estL + estR - estimate) < eps = estL + estR | otherwise = arcL + arcR -- | arcLengthParam c len tol finds the parameter where the curve c has the arclength len, -- within tolerance tol.+arcLengthParam :: CubicBezier -> Double -> Double -> Double arcLengthParam b len eps = arcLengthP b len ol (len/ol) 1 eps where ol = outline b -- Use the Newton rootfinding method. Start with large tolerance -- values, and decrease tolerance as we go closer to the root.+arcLengthP :: CubicBezier -> Double -> Double ->+ Double -> Double -> Double -> Double arcLengthP !b !len !tot !t !dt !eps | abs diff < eps = t - newDt | otherwise = arcLengthP b len tot (t - newDt) newDt eps@@ -202,7 +214,7 @@ -- | Return True if all the control points are colinear within tolerance. colinear :: CubicBezier -> Double -> Bool-colinear (CubicBezier a b c d) eps =+colinear (CubicBezier !a !b !c !d) eps = abs (ld b) < eps && abs (ld c) < eps where ld = lineDistance (Line a d)
Geom2D/CubicBezier/Curvature.hs view
@@ -6,7 +6,8 @@ import Geom2D.CubicBezier.Intersection import Math.BernsteinPoly --- | Curvature of the Bezier curve.+-- | Curvature of the Bezier curve. A negative curvature means the curve+-- curves to the right. curvature :: CubicBezier -> Double -> Double curvature b t | t == 0 = curvature' b@@ -14,7 +15,8 @@ | t < 0.5 = curvature' $ snd $ splitBezier b t | otherwise = negate $ curvature' $ reorient $ fst $ splitBezier b t -curvature' (CubicBezier c0 c1 c2 c3) = 2/3 * b/a^3+curvature' :: CubicBezier -> Double+curvature' (CubicBezier c0 c1 c2 _c3) = 2/3 * b/a^(3::Int) where a = vectorDistance c1 c0 b = (c1^-^c0) `vectorCross` (c2^-^c1)@@ -25,29 +27,29 @@ radiusOfCurvature b t = 1 / curvature b t extrema :: CubicBezier -> BernsteinPoly-extrema (CubicBezier p0 p1 p2 p3) =- let bez = [p0, p1, p2, p3]- x' = bernsteinDeriv $ listToBernstein $ map pointX bez- y' = bernsteinDeriv $ listToBernstein $ map pointY bez+extrema bez = + let (x, y) = bezierToBernstein bez+ x' = bernsteinDeriv x+ y' = bernsteinDeriv y x'' = bernsteinDeriv x' y'' = bernsteinDeriv y' x''' = bernsteinDeriv x'' y''' = bernsteinDeriv y''- in -- (y'^2 + x'^2) * (x'*y''' - y'*x''') -- -- 3 * (x'*y'' - y'*x'') * (y'*y'' + x'*x'')- (y'~*y' ~+ x'~*x') ~* (x'~*y''' ~- y'~*x''') ~-- 3 *~ (x'~*y'' ~- y'~*x'') ~* (y'~*y'' ~+ x'~*x'')+ in (y'~*y' ~+ x'~*x') ~* (x'~*y''' ~- y'~*x''') ~-+ 3 *~ (x'~*y'' ~- y'~*x'') ~* (y'~*y'' ~+ x'~*x'') -- | Find extrema of the curvature, but not inflection points. curvatureExtrema :: CubicBezier -> Double -> [Double]-curvatureExtrema b eps = bezierFindRoot (extrema b) 0 1 $- bezierParamTolerance b eps+curvatureExtrema b eps+ | colinear b eps = []+ | otherwise = bezierFindRoot (extrema b) 0 1 $+ bezierParamTolerance b eps radiusSquareEq :: CubicBezier -> Double -> BernsteinPoly-radiusSquareEq (CubicBezier p0 p1 p2 p3) d =- let bez = [p0, p1, p2, p3]- x' = bernsteinDeriv $ listToBernstein $ map pointX bez- y' = bernsteinDeriv $ listToBernstein $ map pointY bez+radiusSquareEq bez d =+ let (x, y) = bezierToBernstein bez+ x' = bernsteinDeriv x+ y' = bernsteinDeriv y x'' = bernsteinDeriv x' y'' = bernsteinDeriv y' a = x'~*x' ~+ y'~*y'@@ -55,9 +57,12 @@ in (a~*a~*a) ~- (d*d) *~ b~*b -- | Find points on the curve that have a certain radius of curvature.+-- Values to the left and to the right of the curve are returned. findRadius :: CubicBezier -- ^ the curve -> Double -- ^ distance -> Double -- ^ tolerance -> [Double] -- ^ result-findRadius b d eps = bezierFindRoot (radiusSquareEq b d) 0 1 $- bezierParamTolerance b eps+findRadius b d eps+ | colinear b eps = [] -- either empty or a huge list of t's+ | otherwise = bezierFindRoot (radiusSquareEq b d) 0 1 $+ bezierParamTolerance b eps
Geom2D/CubicBezier/Intersection.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BangPatterns #-} -- | Intersection routines using Bezier Clipping. Provides also functions for finding the roots of onedimensional bezier curves. This can be used as a general polynomial root solver by converting from the power basis to the bernstein basis. module Geom2D.CubicBezier.Intersection- (bezierIntersection, bezierLineIntersections, bezierFindRoot)+ (bezierIntersection, bezierLineIntersections, bezierFindRoot, closest) where import Geom2D import Geom2D.CubicBezier.Basic@@ -11,19 +11,21 @@ -- find the convex hull by comparing the angles of the vectors with -- the cross product and backtracking if necessary.-findOuter' upper !dir !p1 l@(p2:rest)+findOuter' :: Bool -> Point -> Point -> [Point] -> Either [Point] [Point]+findOuter' !upper !dir !p1 l@(p2:rest) -- backtrack if the direction is outward | if upper then dir `vectorCross` (p2^-^p1) > 0 -- left turn- else dir `vectorCross` (p2^-^p1) < 0 = Left l+ else dir `vectorCross` (p2^-^p1) < 0 = Left $! l -- succeed | otherwise = case findOuter' upper (p2^-^p1) p2 rest of Left m -> findOuter' upper dir p1 m Right m -> Right (p1:m) -findOuter' _ _ p1 p = Right (p1:p)+findOuter' _ _ p1 p = Right $! (p1:p) -- find the outermost point. It doesn't look at the x values.+findOuter :: Bool -> [Point] -> [Point] findOuter upper (p1:p2:rest) = case findOuter' upper (p2^-^p1) p2 rest of Right l -> p1:l@@ -40,38 +42,40 @@ findOuter False points) -- test if the chords cross the fat line--- use continuation passing style+-- return the continuation if above the line testBelow :: Double -> [Point] -> Maybe Double -> Maybe Double-testBelow dmin [] _ = Nothing-testBelow dmin [_] _ = Nothing-testBelow dmin (p:q:rest) cont+testBelow _ [] _ = Nothing+testBelow _ [_] _ = Nothing+testBelow !dmin (p:q:rest) cont | pointY p >= dmin = cont | pointY p > pointY q = Nothing | pointY q < dmin = testBelow dmin (q:rest) cont- | otherwise = Just $ intersectPt dmin p q+ | otherwise = Just $! intersectPt dmin p q testBetween :: Double -> Point -> Maybe Double -> Maybe Double-testBetween dmax (Point x y) cont+testBetween !dmax (Point !x !y) cont | y <= dmax = Just x | otherwise = cont -- test if the chords cross the line y=dmax somewhere testAbove :: Double -> [Point] -> Maybe Double-testAbove dmax [] = Nothing-testAbove dmax [_] = Nothing+testAbove _ [] = Nothing+testAbove _ [_] = Nothing testAbove dmax (p:q:rest) | pointY p < pointY q = Nothing | pointY q > dmax = testAbove dmax (q:rest)- | otherwise = Just $ intersectPt dmax p q+ | otherwise = Just $! intersectPt dmax p q -- find the x value where the line through the two points -- intersect the line y=d+intersectPt :: Double -> Point -> Point -> Double intersectPt d (Point x1 y1) (Point x2 y2) = x1 + (d - y1) * (x2 - x1) / (y2 - y1) -- make a hull and test over which interval the -- curve is garuanteed to lie inside the fat line-chopHull dmin dmax ds = do+chopHull :: Double -> Double -> [Double] -> Maybe (Double, Double)+chopHull !dmin !dmax ds = do let (upper, lower) = makeHull ds left_t <- testBelow dmin upper $ testBetween dmax (head upper) $@@ -81,8 +85,11 @@ testAbove dmax (reverse lower) Just (left_t, right_t) +bezierClip :: CubicBezier -> CubicBezier -> Double -> Double+ -> Double -> Double -> Double -> Double -> Bool+ -> [(Double, Double)] bezierClip p@(CubicBezier !p0 !p1 !p2 !p3) q@(CubicBezier !q0 !q1 !q2 !q3)- tmin tmax umin umax prevClip eps reverse+ tmin tmax umin umax prevClip eps revCurves -- no intersection | isNothing chop_interval = []@@ -94,17 +101,17 @@ then let (pl, pr) = splitBezier newP 0.5 half_t = new_tmin + (new_tmax - new_tmin) / 2- in bezierClip q pl umin umax new_tmin half_t newClip eps (not reverse) ++- bezierClip q pr umin umax half_t new_tmax newClip eps (not reverse)+ in bezierClip q pl umin umax new_tmin half_t newClip eps (not revCurves) +++ bezierClip q pr umin umax half_t new_tmax newClip eps (not revCurves) else let (ql, qr) = splitBezier q 0.5 half_t = umin + (umax - umin) / 2- in bezierClip ql newP umin half_t new_tmin new_tmax newClip eps (not reverse) ++- bezierClip qr newP half_t umax new_tmin new_tmax newClip eps (not reverse)+ in bezierClip ql newP umin half_t new_tmin new_tmax newClip eps (not revCurves) +++ bezierClip qr newP half_t umax new_tmin new_tmax newClip eps (not revCurves) -- within tolerance | max (umax - umin) (new_tmax - new_tmin) < eps =- if reverse+ if revCurves then [ (umin + (umax-umin)/2, new_tmin + (new_tmax-new_tmin)/2) ] else [ (new_tmin + (new_tmax-new_tmin)/2,@@ -112,7 +119,7 @@ -- iterate with the curves reversed. | otherwise =- bezierClip q newP umin umax new_tmin new_tmax newClip eps (not reverse)+ bezierClip q newP umin umax new_tmin new_tmax newClip eps (not revCurves) where d = lineDistance (Line q0 q3)@@ -188,3 +195,14 @@ bezierParamTolerance b eps where (CubicBezier p0 p1 p2 p3) = fromJust (inverse $ translate p $* rotateVec (q ^-^ p)) $* b++-- | Find the closest value(s) on the bezier to the given point, within tolerance.+closest :: CubicBezier -> Point -> Double -> [Double]+closest cb (Point px py) eps = bezierFindRoot poly 0 1 eps+ where+ (bx, by) = bezierToBernstein cb+ bx' = bernsteinDeriv bx+ by' = bernsteinDeriv by+ poly = (bx ~- listToBernstein [px, px, px, px]) ~* bx' ~++ (by ~- listToBernstein [py, py, py, py]) ~* by'+
+ Geom2D/CubicBezier/MetaPath.hs view
@@ -0,0 +1,430 @@+{-# LANGUAGE BangPatterns #-}+-- | This module implements an extension to paths as used in+-- D.E.Knuth's /Metafont/. Metafont gives a more intuitive method to+-- specify paths than bezier curves. I'll give a brief overview of+-- the metafont curves. For a more in depth explanation look at+-- /The MetafontBook/.+-- +-- Each spline has a tension parameter, which is a relative measure of+-- the length of the curve. You can specify the tension for the left+-- side and the right side of the spline separately. By default+-- metafont gives a tension of 1, which gives a good looking curve.+-- Tensions shouldn't be less than 3/4, but this implementation+-- doesn't check for it. If you want to avoid points of inflection on+-- the spline, you can use @TensionAtLeast@ instead of @Tension@,+-- which will adjust the length of the control points so they fall+-- into the /bounding triangle/, if such a triangle exist.+--+-- You can either give directions for each node, or let metafont find+-- them. Metafont will solve a set of equations to find the+-- directions. You can also let metafont find directions at corner+-- points by setting the /curl/, which is how much the point /curls/+-- at that point. At endpoints a curl of 1 is implied when it is not+-- given.+--+-- Metafont will then find the control points from the path for you.+-- You can also specify the control points explicitly.+--+-- Here is an example path from the metafont program text:+-- +-- @+-- z0..z1..tension atleast 1..{curl 2}z2..z3{-1,-2}..tension 3 and 4..z4..controls z45 and z54..z5+-- @+-- +-- This path is equivalent to:+--+-- @+-- z0{curl 1}..tension atleast 1 and atleast 1..{curl 2}z2{curl 2}..tension 1 and 1..+-- {-1,-2}z3{-1,-2}..tension 3 and 4..z4..controls z45 and z54..z5+-- @+--+-- This path can be used with the following datatype:+-- +-- @+-- OpenMetaPath [ (z0, MetaJoin Open (Tension 1) (Tension 1) Open)+-- , (z1, MetaJoin Open (TensionAtLeast 1) (TensionAtLeast 1) (Curl 2))+-- , (z2, MetaJoin Open (Tension 1) (Tension 1) Open)+-- , (z3, MetaJoin (Direction (Point (-1) (-2))) (Tension 3) (Tension 4) Open)+-- , (z4, Controls z45 z54)+-- ] z5+-- @+--+-- Cyclic paths are similar, but use the @CyclicMetaPath@ contructor.+-- There is no ending point, since the ending point will be the same+-- as the first point.++module Geom2D.CubicBezier.MetaPath+ (unmeta, MetaPath (..), MetaJoin (..), MetaNodeType (..), Tension (..))+where+import Geom2D+import Geom2D.CubicBezier.Basic+import Data.List+import Text.Printf++data MetaPath = OpenMetaPath [(Point, MetaJoin)] Point+ | CyclicMetaPath [(Point, MetaJoin)]++data MetaJoin = MetaJoin { metaTypeL :: MetaNodeType+ , tensionL :: Tension+ , tensionR :: Tension+ , metaTypeR :: MetaNodeType+ }+ | Controls Point Point+ deriving Show++data MetaNodeType = Open+ | Curl {curlgamma :: Double}+ | Direction {nodedir :: Point}+ deriving Show++data Tension = Tension {tensionValue :: Double}+ | TensionAtLeast {tensionValue :: Double}+ deriving (Eq, Show)++instance Show MetaPath where+ show (CyclicMetaPath nodes) =+ showPath nodes ++ "cycle"+ show (OpenMetaPath nodes lastpoint) =+ showPath nodes ++ showPoint lastpoint++showPath :: [(Point, MetaJoin)] -> [Char]+showPath = concatMap showNodes+ where+ showNodes (p, Controls u v) =+ showPoint p ++ "..controls " ++ showPoint u ++ "and " ++ showPoint v ++ ".."+ showNodes (p, MetaJoin m1 t1 t2 m2) =+ showPoint p ++ typename m1 ++ ".." ++ tensions ++ typename m2+ where+ tensions+ | t1 == t2 && t1 == Tension 1 = ""+ | t1 == t2 = printf "tension %s.." (showTension t1)+ | otherwise = printf "tension %s and %s.."+ (showTension t1) (showTension t2)+ showTension (TensionAtLeast t) = printf "atleast %.3f" t :: String+ showTension (Tension t) = printf "%.3f" t :: String+ typename Open = ""+ typename (Curl g) = printf "{curl %.3f}" g :: String+ typename (Direction dir) = printf "{%s}" (showPoint dir) :: String+ +showPoint :: Point -> String+showPoint (Point x y) = printf "(%.3f, %.3f)" x y++-- | Create a normal path from a metapath.+unmeta :: MetaPath -> Path+unmeta (OpenMetaPath nodes endpoint) =+ unmetaOpen (sanitizeOpen nodes) endpoint++unmeta (CyclicMetaPath nodes) =+ case span (bothOpen . snd) nodes of+ (l, []) -> unmetaCyclic l+ (l, (m:n)) ->+ if leftOpen $ snd m+ then unmetaAsOpen (l++[m]) n+ else unmetaAsOpen l (m:n)++unmetaOpen :: [(Point, MetaJoin)] -> Point -> Path+unmetaOpen nodes endpoint =+ let subsegs = openSubSegments nodes endpoint+ path = joinSegments $ map unmetaSubSegment subsegs+ in OpenPath path endpoint++-- decompose into a list of subsegments that need to be solved.+openSubSegments :: [(Point, MetaJoin)] -> Point -> [MetaPath]+openSubSegments l p = openSubSegments' (tails l) p++openSubSegments' :: [[(Point, MetaJoin)]] -> Point -> [MetaPath]+openSubSegments' [[]] _ = []+openSubSegments' [] _ = []+openSubSegments' l lastPoint = case break breakPoint l of+ (m, n:o) ->+ let point = case o of+ (((p,_):_):_) -> p+ _ -> lastPoint+ in OpenMetaPath (map head (m ++ [n])) point :+ openSubSegments' o lastPoint+ _ -> error "openSubSegments': unexpected end of segments"++-- join subsegments into one segment+joinSegments :: [Path] -> [(Point, PathJoin)]+joinSegments = concatMap nodes+ where nodes (OpenPath n _) = n+ nodes (ClosedPath n) = n++-- solve a cyclic metapath where all angles depend on the each other.+unmetaCyclic :: [(Point, MetaJoin)] -> Path+unmetaCyclic nodes =+ let points = map fst nodes+ chords = zipWith (^-^) points (last points : points)+ tensionsA = (map (tensionL . snd) nodes)+ tensionsB = (map (tensionR . snd) nodes)+ turnAngles = zipWith turnAngle chords (tail $ cycle chords)+ thetas = solveCyclicTriD $+ eqsCycle tensionsA+ points+ tensionsB+ turnAngles+ phis = zipWith (\x y -> -(x+y)) turnAngles (tail thetas ++ [head thetas])+ in ClosedPath $ zip points $+ zipWith6 unmetaJoin points (tail points ++ [head points])+ thetas phis tensionsA tensionsB++-- solve a cyclic metapath as an open path if possible.+-- rotate to the defined node, and rotate back after+-- solving the path.+unmetaAsOpen :: [(Point, MetaJoin)] -> [(Point, MetaJoin)] -> Path+unmetaAsOpen l m = ClosedPath (l'++m') + where n = length m+ OpenPath o _ = unmetaOpen (sanitizeCycle (m++l)) (fst $ head m)+ (m',l') = splitAt n o++-- solve a subsegment+unmetaSubSegment :: MetaPath -> Path++-- the simple case where the control points are already given.+unmetaSubSegment (OpenMetaPath [(p, Controls u v)] q) =+ OpenPath [(p, JoinCurve u v)] q++-- otherwise solve the angles, and find the control points+unmetaSubSegment (OpenMetaPath nodes lastpoint) =+ let points = map fst nodes ++ [lastpoint]+ joins = map snd nodes+ chords = zipWith (^-^) (tail points) points+ tensionsA = map tensionL joins+ tensionsB = map tensionR joins+ turnAngles = zipWith turnAngle chords (tail chords) ++ [0]+ thetas = solveTriDiagonal $+ eqsOpen points joins chords turnAngles+ (map tensionValue tensionsA)+ (map tensionValue tensionsB)+ phis = zipWith (\x y -> -x-y) turnAngles (tail thetas)+ pathjoins = zipWith6 unmetaJoin points (tail points) thetas phis tensionsA tensionsB+ in OpenPath (zip points pathjoins) lastpoint++unmetaSubSegment _ = error "unmetaSubSegment: subsegment should not be cyclic"++bothOpen :: MetaJoin -> Bool+bothOpen (MetaJoin Open _ _ Open) = True+bothOpen _ = False++leftOpen :: MetaJoin -> Bool+leftOpen (MetaJoin Open _ _ _) = True+leftOpen _ = False++replaceLast :: [a] -> a -> [a]+replaceLast [] _ = []+replaceLast [_] n = [n]+replaceLast (l:ls) n = l:replaceLast ls n++sanitizeCycle :: [(Point, MetaJoin)] -> [(Point, MetaJoin)]+sanitizeCycle l = replaceLast ls l'+ where+ (l':ls) = sanitizeRest (last l: l)++-- replace open nodetypes with more defined nodetypes if possible+sanitizeOpen :: [(Point, MetaJoin)] -> [(Point, MetaJoin)]+sanitizeOpen [] = []++-- starting open => curl+sanitizeOpen ((p, MetaJoin Open t1 t2 m):rest) =+ sanitizeRest ((p, MetaJoin (Curl 1) t1 t2 m):rest)+sanitizeOpen l = sanitizeRest l+ +sanitizeRest :: [(Point, MetaJoin)] -> [(Point, MetaJoin)]+sanitizeRest [] = []++-- ending open => curl+sanitizeRest [(p, MetaJoin m t1 t2 Open)] =+ [(p, MetaJoin m t1 t2 (Curl 1))]++sanitizeRest (node1@(p, MetaJoin m1 tl tr m2): node2@(q, MetaJoin n1 sl sr n2): rest) =+ case (m2, n1) of+ (Curl g, Open) -> -- curl, open => curl, curl+ node1 : sanitizeRest ((q, MetaJoin (Curl g) sl sr n2):rest)+ (Open, Curl g) -> -- open, curl => curl, curl+ (p, MetaJoin m1 tl tr (Curl g)) : sanitizeRest (node2:rest)+ (Direction dir, Open) -> -- given, open => given, given+ node1 : sanitizeRest ((q, (MetaJoin (Direction dir) sl sr n2)) : rest)+ (Open, Direction dir) -> -- open, given => given, given+ (p, MetaJoin m1 tl tr (Direction dir)) : sanitizeRest (node2:rest)+ _ -> node1 : sanitizeRest (node2:rest)++sanitizeRest ((p, m): (q, n): rest) =+ case (m, n) of+ (Controls _u v, MetaJoin Open t1 t2 mt2) -> -- explicit, open => explicit, given+ (p, m) : sanitizeRest ((q, MetaJoin (Direction (q^-^v)) t1 t2 mt2): rest)+ (MetaJoin mt1 tl tr Open, Controls u _v) -> -- open, explicit => given, explicit+ (p, MetaJoin mt1 tl tr (Direction (u^-^p))) : sanitizeRest ((q, n): rest)+ _ -> (p, m) : sanitizeRest ((q, n) : rest)++sanitizeRest (n:l) = n:sanitizeRest l++-- break the subsegment if the angle to the left or the right is defined or a curl.+breakPoint :: [(Point, MetaJoin)] -> Bool+breakPoint ((_, MetaJoin _ _ _ Open):(_, MetaJoin Open _ _ _):_) = False+breakPoint _ = True++-- solve the tridiagonal system for t[i]:+-- a[n] t[i-1] + b[i] t[i] + c[b] t[i+1] = d[i]+-- where a[0] = c[n] = 0+-- by first rewriting it into+-- the system t[i] + u[i] t[i+1] = v[i]+-- where u[n] = 0+-- then solving for t[n]+-- see metafont the program: ¶ 283+solveTriDiagonal :: [(Double, Double, Double, Double)] -> [Double]+solveTriDiagonal [] = error "solveTriDiagonal: not enough equations"+solveTriDiagonal ((_, b0, c0, d0): rows) = solutions+ where+ ((_, vn): twovars) =+ reverse $ scanl nextrow (c0/b0, d0/b0) rows+ nextrow (u, v) (ai, bi, ci, di) =+ (ci/(bi - u*ai), (di - v*ai)/(bi - u*ai))+ solutions = reverse $ scanl nextsol vn twovars+ nextsol ti (u, v) = v - u*ti++-- test = ((80.0,58.0,51.0),[(-432.0,78.0,102.0,503.0),(71.0,-82.0,20.0,2130.0),(52.39,-10.43,4.0,56.0),(34.0,38.0,0.0,257.0)])++-- solve the cyclic tridiagonal system.+-- see metafont the program: ¶ 286+solveCyclicTriD :: [(Double, Double, Double, Double)] -> [Double]+solveCyclicTriD rows = solutions+ where+ (!un, !vn, !wn): threevars =+ reverse $ tail $ scanl nextrow (0, 0, 1) rows+ nextrow (!u, !v, !w) (!ai, !bi, !ci, !di) =+ (ci/(bi - ai*u), (di - ai*v)/(bi - ai*u), -ai*w/(bi - ai*u))+ (totvn, totwn) = foldl (\(v', w') (u, v, w) ->+ (v - u*v', w - u*w'))+ (0, 1) threevars+ t0 = (vn - un*totvn) / (1 - (wn - un*totwn))+ solutions = scanl nextsol t0+ ((un, vn, wn) : reverse (tail threevars))+ nextsol t (!u, !v, !w) = (v + w*t0 - t)/u++turnAngle :: Point -> Point -> Double+turnAngle (Point x y) q = vectorAngle $ rotateVec p $* q+ where p = Point x (-y)++zipPrev :: [a] -> [(a, a)]+zipPrev [] = []+zipPrev l = zip (last l : l) l++-- find the equations for a cycle containing only open points+eqsCycle :: [Tension] -> [Point] -> [Tension]+ -> [Double] -> [(Double, Double, Double, Double)]+eqsCycle tensionsA points tensionsB turnAngles = + zipWith4 eqTension+ (zipPrev (map tensionValue tensionsA))+ (zipPrev dists)+ (zipPrev turnAngles)+ (zipPrev (map tensionValue tensionsB))+ where + dists = zipWith vectorDistance points (tail $ cycle points)++-- find the equations for an path with open points.+-- The first and last node should be a curl or a given angle++eqsOpen :: [Point] -> [MetaJoin] -> [Point] -> [Double]+ -> [Double] -> [Double] -> [(Double, Double, Double, Double)]+eqsOpen _ [join] [delta] _ _ _ =+ case join of+ MetaJoin (Curl _) _ _ (Curl _) ->+ [(0, 1, 0, 0), (0, 1, 0, 0)]+ MetaJoin (Curl g) t1 t2 (Direction dir) ->+ [eqCurl0 g (tensionValue t1) (tensionValue t2) 0,+ (0, 1, 0, turnAngle delta dir)]+ MetaJoin (Direction dir) t1 t2 (Curl g) ->+ [(0, 1, 0, turnAngle delta dir),+ eqCurlN g (tensionValue t1) (tensionValue t2)]+ MetaJoin (Direction dir) _ _ (Direction dir2) ->+ [(0, 1, 0, turnAngle delta dir),+ (0, 1, 0, turnAngle delta dir2)]+ _ -> error "eqsOpen: illegal nodetype in subsegment"++eqsOpen points joins chords turnAngles tensionsA tensionsB =+ eq0 : restEquations joins tensionsA dists turnAngles tensionsB+ where+ dists = zipWith vectorDistance points (tail points) + eq0 = case head joins of+ (MetaJoin (Curl g) _ _ _) -> eqCurl0 g (head tensionsA) (head tensionsB) (head turnAngles)+ (MetaJoin (Direction dir) _ _ _) -> (0, 1, 0, turnAngle (head chords) dir)+ _ -> error "eqsOpen: illegal subsegment first nodetype"++ restEquations [lastnode] (tensionA:_) _ _ (tensionB:_) =+ case lastnode of+ MetaJoin _ _ _ (Curl g) -> [eqCurlN g tensionA tensionB]+ MetaJoin _ _ _ (Direction dir) -> [(0, 1, 0, turnAngle (last chords) dir)]+ _ -> error "eqsOpen: illegal subsegment last nodetype"++ restEquations (_:othernodes) (tensionA:restTA) (d:restD) (turn:restTurn) (tensionB:restTB) =+ eqTension (tensionA, head restTA) (d, head restD) (turn, head restTurn) (tensionB, head restTB) :+ restEquations othernodes restTA restD restTurn restTB++ restEquations _ _ _ _ _ = error "eqsOpen: illegal rest equations"++-- the equation for an open node+eqTension :: (Double, Double) -> (Double, Double)+ -> (Double, Double) -> (Double, Double)+ -> (Double, Double, Double, Double)+eqTension (tensionA', tensionA) (dist', dist) (psi', psi) (tensionB', tensionB) =+ (a, b+c, d, -b*psi' - d*psi)+ where+ a = (tensionB' * tensionB' / (tensionA' * dist'))+ b = (3 - 1/tensionA') * tensionB' * tensionB' / dist'+ c = (3 - 1/tensionB) * tensionA * tensionA / dist+ d = tensionA * tensionA / (tensionB * dist)++-- the equation for a starting curl+eqCurl0 :: Double -> Double -> Double -> Double -> (Double, Double, Double, Double)+eqCurl0 gamma tensionA tensionB psi = (0, c, d, r)+ where+ c = chi/tensionA + 3 - 1/tensionB+ d = (3 - 1/tensionA)*chi + 1/tensionB+ chi = gamma*tensionB*tensionB / (tensionA*tensionA)+ r = -d*psi++-- the equation for an ending curl+eqCurlN :: Double -> Double -> Double -> (Double, Double, Double, Double)+eqCurlN gamma tensionA tensionB = (a, b, 0, 0)+ where+ a = (3 - 1/tensionB)*chi + 1/tensionA+ b = chi/tensionB + 3 - 1/tensionA+ chi = gamma*tensionA*tensionA / (tensionB*tensionB)++-- magic formula for getting the control points by John Hobby+unmetaJoin :: Point -> Point -> Double -> Double -> Tension -> Tension -> PathJoin+unmetaJoin !z0 !z1 !theta !phi !alpha !beta+ | abs phi < 1e-4 && abs theta < 1e-4 = JoinLine+ | otherwise = JoinCurve u v+ where Point dx dy = z1^-^z0+ bounded = (sf <= 0 && st <= 0 && sf <= 0) ||+ (sf >= 0 && st >= 0 && sf >= 0)+ rr' = velocity st sf ct cf alpha+ ss' = velocity sf st cf ct beta+ stf = st*cf + sf*ct -- sin (theta + phi)+ st = sin theta+ sf = sin phi+ ct = cos theta+ cf = cos phi+ rr = case alpha of+ TensionAtLeast _ | bounded ->+ min rr' (sf/stf)+ _ -> rr'+ ss = case beta of+ TensionAtLeast _ | bounded ->+ min ss' (st/stf)+ _ -> ss'+ u = z0 ^+^ rr *^ Point (dx*ct - dy*st) (dy*ct + dx*st) -- z0 + rr * (rotate theta chord)+ v = z1 ^-^ ss *^ Point (dx*cf + dy*sf) (dy*cf - dx*sf) -- z1 - ss * (rotate (-phi) chord)++constant1, constant2, sqrt2 :: Double+constant1 = 3/2*(sqrt 5 - 1)+constant2 = 3/2*(3 - sqrt 5)+sqrt2 = sqrt 2++-- another magic formula by John Hobby.+velocity :: Double -> Double -> Double+ -> Double -> Tension -> Double+velocity st sf ct cf t =+ (2 + sqrt2 * (st - sf/16)*(sf - st/16)*(ct - cf)) /+ ((3 + constant1*ct + constant2*cf) * tensionValue t)
Geom2D/CubicBezier/Outline.hs view
@@ -7,79 +7,24 @@ import Geom2D.CubicBezier.Basic import Geom2D.CubicBezier.Approximate import Geom2D.CubicBezier.Curvature-import qualified Data.Map as M-import Data.Function-import Data.List offsetPoint :: Double -> Point -> Point -> Point offsetPoint dist start tangent = start ^+^ (rotate90L $* dist *^ normVector tangent) -bezierOffsetPoint :: CubicBezier -> Double -> Double -> Point-bezierOffsetPoint cb dist t =- uncurry (offsetPoint dist) $- evalBezierDeriv cb t+bezierOffsetPoint :: CubicBezier -> Double -> Double -> (Point, Point)+bezierOffsetPoint cb dist t = (offsetPoint dist p p', p')+ where (p, p') = evalBezierDeriv cb t -- Approximate the bezier curve offset by dist. A positive value -- means to the left, a negative to the right.-approximateOffset :: CubicBezier -> Double -> Double -> (CubicBezier, Double, Double)-approximateOffset cb@(CubicBezier p1 p2 p3 p4) dist tol =- approximateCurveWithParams offsetCb points ts tol- where tan1 = p2 ^-^ p1- tan2 = p4 ^-^ p3- offsetCb = CubicBezier- (offsetPoint dist p1 tan1)- (offsetPoint dist p2 tan1)- (offsetPoint dist p3 tan2)- (offsetPoint dist p4 tan2)- points = map (bezierOffsetPoint cb dist) ts- ts = [i/16 | i <- [1..15]]---- subdivide the original curve and approximate the offset until--- the maximum error is below tolerance offsetSegment :: Double -> Double -> CubicBezier -> [CubicBezier]-offsetSegment dist tol cb- | err <= tol = [cb_out]- | otherwise = offsetSegment dist tol cb_l ++- offsetSegment dist tol cb_r - where- (cb_out, t, err) = approximateOffset cb dist tol- (cb_l, cb_r) = splitBezier cb t--data OutlineSegment = OutlineSegment {- os_t_min :: Double, -- the least t param of the segment in the original curve- os_t_err :: Double, -- the param where the error is maximal- os_curve :: CubicBezier, -- the segment on the original curve- os_outline :: CubicBezier } -- the outline of the segment---- Keep a map from maxError to OutlineSegment for each subsegment to keep--- track of the segment with the maximum error. This ensures a n--- log(n) execution time, rather than n^2 when a list is used.-offsetMax :: Double -> Double -> Int ->- M.Map Double OutlineSegment ->- [CubicBezier]-offsetMax dist tol n segments- | n <= 1 = error "minimum segments to offset is 1"- | (n == 1) || (err < tol) = map os_outline $- sortBy (compare `on` os_t_min) $- M.elems segments+offsetSegment dist tol cb =+ approximatePath (bezierOffsetPoint cb dist) 15 tol 0 1 - -- split the maximum curve in two and add the two segments to the map- | otherwise = offsetMax dist tol (n-1) $- M.insert err_l (OutlineSegment t_min t_err_l cb_l outline_l) $- M.insert err_r (OutlineSegment t_err t_err_r cb_r outline_r) $- newSegments- where- ((err, OutlineSegment t_min t_err curve _), newSegments) = M.deleteFindMax segments- (cb_l, cb_r) = splitBezier curve t_err- (outline_l, t_err_l, err_l) = approximateOffset cb_l dist tol- (outline_r, t_err_r, err_r) = approximateOffset cb_r dist tol- offsetSegmentMax :: Int -> Double -> Double -> CubicBezier -> [CubicBezier]-offsetSegmentMax n dist tol cb =- offsetMax dist tol n segments- where segments = M.singleton err (OutlineSegment 0 t_err cb outline)- (outline, t_err, err) = approximateOffset cb dist tol+offsetSegmentMax m dist tol cb =+ approximatePathMax m (bezierOffsetPoint cb dist) 15 tol 0 1 -- | Calculate an offset path from the bezier curve to within -- tolerance. If the distance is positive offset to the left,
LICENSE view
@@ -1,339 +1,28 @@-GNU GENERAL PUBLIC LICENSE- Version 2, June 1991-- Copyright (C) 1989, 1991 Free Software Foundation, Inc.,- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA- Everyone is permitted to copy and distribute verbatim copies- of this license document, but changing it is not allowed.-- Preamble-- The licenses for most software are designed to take away your-freedom to share and change it. By contrast, the GNU General Public-License is intended to guarantee your freedom to share and change free-software--to make sure the software is free for all its users. This-General Public License applies to most of the Free Software-Foundation's software and to any other program whose authors commit to-using it. (Some other Free Software Foundation software is covered by-the GNU Lesser General Public License instead.) You can apply it to-your programs, too.-- When we speak of free software, we are referring to freedom, not-price. Our General Public Licenses are designed to make sure that you-have the freedom to distribute copies of free software (and charge for-this service if you wish), that you receive source code or can get it-if you want it, that you can change the software or use pieces of it-in new free programs; and that you know you can do these things.-- To protect your rights, we need to make restrictions that forbid-anyone to deny you these rights or to ask you to surrender the rights.-These restrictions translate to certain responsibilities for you if you-distribute copies of the software, or if you modify it.-- For example, if you distribute copies of such a program, whether-gratis or for a fee, you must give the recipients all the rights that-you have. You must make sure that they, too, receive or can get the-source code. And you must show them these terms so they know their-rights.-- We protect your rights with two steps: (1) copyright the software, and-(2) offer you this license which gives you legal permission to copy,-distribute and/or modify the software.-- Also, for each author's protection and ours, we want to make certain-that everyone understands that there is no warranty for this free-software. If the software is modified by someone else and passed on, we-want its recipients to know that what they have is not the original, so-that any problems introduced by others will not reflect on the original-authors' reputations.-- Finally, any free program is threatened constantly by software-patents. We wish to avoid the danger that redistributors of a free-program will individually obtain patent licenses, in effect making the-program proprietary. To prevent this, we have made it clear that any-patent must be licensed for everyone's free use or not licensed at all.-- The precise terms and conditions for copying, distribution and-modification follow.-- GNU GENERAL PUBLIC LICENSE- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION-- 0. This License applies to any program or other work which contains-a notice placed by the copyright holder saying it may be distributed-under the terms of this General Public License. The "Program", below,-refers to any such program or work, and a "work based on the Program"-means either the Program or any derivative work under copyright law:-that is to say, a work containing the Program or a portion of it,-either verbatim or with modifications and/or translated into another-language. (Hereinafter, translation is included without limitation in-the term "modification".) Each licensee is addressed as "you".--Activities other than copying, distribution and modification are not-covered by this License; they are outside its scope. The act of-running the Program is not restricted, and the output from the Program-is covered only if its contents constitute a work based on the-Program (independent of having been made by running the Program).-Whether that is true depends on what the Program does.-- 1. You may copy and distribute verbatim copies of the Program's-source code as you receive it, in any medium, provided that you-conspicuously and appropriately publish on each copy an appropriate-copyright notice and disclaimer of warranty; keep intact all the-notices that refer to this License and to the absence of any warranty;-and give any other recipients of the Program a copy of this License-along with the Program.--You may charge a fee for the physical act of transferring a copy, and-you may at your option offer warranty protection in exchange for a fee.-- 2. You may modify your copy or copies of the Program or any portion-of it, thus forming a work based on the Program, and copy and-distribute such modifications or work under the terms of Section 1-above, provided that you also meet all of these conditions:-- a) You must cause the modified files to carry prominent notices- stating that you changed the files and the date of any change.-- b) You must cause any work that you distribute or publish, that in- whole or in part contains or is derived from the Program or any- part thereof, to be licensed as a whole at no charge to all third- parties under the terms of this License.-- c) If the modified program normally reads commands interactively- when run, you must cause it, when started running for such- interactive use in the most ordinary way, to print or display an- announcement including an appropriate copyright notice and a- notice that there is no warranty (or else, saying that you provide- a warranty) and that users may redistribute the program under- these conditions, and telling the user how to view a copy of this- License. (Exception: if the Program itself is interactive but- does not normally print such an announcement, your work based on- the Program is not required to print an announcement.)--These requirements apply to the modified work as a whole. If-identifiable sections of that work are not derived from the Program,-and can be reasonably considered independent and separate works in-themselves, then this License, and its terms, do not apply to those-sections when you distribute them as separate works. But when you-distribute the same sections as part of a whole which is a work based-on the Program, the distribution of the whole must be on the terms of-this License, whose permissions for other licensees extend to the-entire whole, and thus to each and every part regardless of who wrote it.--Thus, it is not the intent of this section to claim rights or contest-your rights to work written entirely by you; rather, the intent is to-exercise the right to control the distribution of derivative or-collective works based on the Program.--In addition, mere aggregation of another work not based on the Program-with the Program (or with a work based on the Program) on a volume of-a storage or distribution medium does not bring the other work under-the scope of this License.-- 3. You may copy and distribute the Program (or a work based on it,-under Section 2) in object code or executable form under the terms of-Sections 1 and 2 above provided that you also do one of the following:-- a) Accompany it with the complete corresponding machine-readable- source code, which must be distributed under the terms of Sections- 1 and 2 above on a medium customarily used for software interchange; or,-- b) Accompany it with a written offer, valid for at least three- years, to give any third party, for a charge no more than your- cost of physically performing source distribution, a complete- machine-readable copy of the corresponding source code, to be- distributed under the terms of Sections 1 and 2 above on a medium- customarily used for software interchange; or,-- c) Accompany it with the information you received as to the offer- to distribute corresponding source code. (This alternative is- allowed only for noncommercial distribution and only if you- received the program in object code or executable form with such- an offer, in accord with Subsection b above.)--The source code for a work means the preferred form of the work for-making modifications to it. For an executable work, complete source-code means all the source code for all modules it contains, plus any-associated interface definition files, plus the scripts used to-control compilation and installation of the executable. However, as a-special exception, the source code distributed need not include-anything that is normally distributed (in either source or binary-form) with the major components (compiler, kernel, and so on) of the-operating system on which the executable runs, unless that component-itself accompanies the executable.--If distribution of executable or object code is made by offering-access to copy from a designated place, then offering equivalent-access to copy the source code from the same place counts as-distribution of the source code, even though third parties are not-compelled to copy the source along with the object code.-- 4. You may not copy, modify, sublicense, or distribute the Program-except as expressly provided under this License. Any attempt-otherwise to copy, modify, sublicense or distribute the Program is-void, and will automatically terminate your rights under this License.-However, parties who have received copies, or rights, from you under-this License will not have their licenses terminated so long as such-parties remain in full compliance.-- 5. You are not required to accept this License, since you have not-signed it. However, nothing else grants you permission to modify or-distribute the Program or its derivative works. These actions are-prohibited by law if you do not accept this License. Therefore, by-modifying or distributing the Program (or any work based on the-Program), you indicate your acceptance of this License to do so, and-all its terms and conditions for copying, distributing or modifying-the Program or works based on it.-- 6. Each time you redistribute the Program (or any work based on the-Program), the recipient automatically receives a license from the-original licensor to copy, distribute or modify the Program subject to-these terms and conditions. You may not impose any further-restrictions on the recipients' exercise of the rights granted herein.-You are not responsible for enforcing compliance by third parties to-this License.-- 7. If, as a consequence of a court judgment or allegation of patent-infringement or for any other reason (not limited to patent issues),-conditions are imposed on you (whether by court order, agreement or-otherwise) that contradict the conditions of this License, they do not-excuse you from the conditions of this License. If you cannot-distribute so as to satisfy simultaneously your obligations under this-License and any other pertinent obligations, then as a consequence you-may not distribute the Program at all. For example, if a patent-license would not permit royalty-free redistribution of the Program by-all those who receive copies directly or indirectly through you, then-the only way you could satisfy both it and this License would be to-refrain entirely from distribution of the Program.--If any portion of this section is held invalid or unenforceable under-any particular circumstance, the balance of the section is intended to-apply and the section as a whole is intended to apply in other-circumstances.--It is not the purpose of this section to induce you to infringe any-patents or other property right claims or to contest validity of any-such claims; this section has the sole purpose of protecting the-integrity of the free software distribution system, which is-implemented by public license practices. Many people have made-generous contributions to the wide range of software distributed-through that system in reliance on consistent application of that-system; it is up to the author/donor to decide if he or she is willing-to distribute software through any other system and a licensee cannot-impose that choice.--This section is intended to make thoroughly clear what is believed to-be a consequence of the rest of this License.-- 8. If the distribution and/or use of the Program is restricted in-certain countries either by patents or by copyrighted interfaces, the-original copyright holder who places the Program under this License-may add an explicit geographical distribution limitation excluding-those countries, so that distribution is permitted only in or among-countries not thus excluded. In such case, this License incorporates-the limitation as if written in the body of this License.-- 9. The Free Software Foundation may publish revised and/or new versions-of the General Public License from time to time. Such new versions will-be similar in spirit to the present version, but may differ in detail to-address new problems or concerns.--Each version is given a distinguishing version number. If the Program-specifies a version number of this License which applies to it and "any-later version", you have the option of following the terms and conditions-either of that version or of any later version published by the Free-Software Foundation. If the Program does not specify a version number of-this License, you may choose any version ever published by the Free Software-Foundation.-- 10. If you wish to incorporate parts of the Program into other free-programs whose distribution conditions are different, write to the author-to ask for permission. For software which is copyrighted by the Free-Software Foundation, write to the Free Software Foundation; we sometimes-make exceptions for this. Our decision will be guided by the two goals-of preserving the free status of all derivatives of our free software and-of promoting the sharing and reuse of software generally.-- NO WARRANTY-- 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY-FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN-OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES-PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED-OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS-TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,-REPAIR OR CORRECTION.-- 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,-INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING-OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED-TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY-YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER-PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE-POSSIBILITY OF SUCH DAMAGES.-- END OF TERMS AND CONDITIONS-- How to Apply These Terms to Your New Programs-- If you develop a new program, and you want it to be of the greatest-possible use to the public, the best way to achieve this is to make it-free software which everyone can redistribute and change under these terms.-- To do so, attach the following notices to the program. It is safest-to attach them to the start of each source file to most effectively-convey the exclusion of warranty; and each file should have at least-the "copyright" line and a pointer to where the full notice is found.-- Haskell library for manipulating cubic bezier curves- Copyright (C) 2013 Kristof Bastiaensen-- This program is free software; you can redistribute it and/or modify- it under the terms of the GNU General Public License as published by- the Free Software Foundation; either version 2 of the License, or- (at your option) any later version.-- This program is distributed in the hope that it will be useful,- but WITHOUT ANY WARRANTY; without even the implied warranty of- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the- GNU General Public License for more details.-- You should have received a copy of the GNU General Public License along- with this program; if not, write to the Free Software Foundation, Inc.,- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.--Also add information on how to contact you by electronic and paper mail.--If the program is interactive, make it output a short notice like this-when it starts in an interactive mode:-- Gnomovision version 69, Copyright (C) year name of author- Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.- This is free software, and you are welcome to redistribute it- under certain conditions; type `show c' for details.--The hypothetical commands `show w' and `show c' should show the appropriate-parts of the General Public License. Of course, the commands you use may-be called something other than `show w' and `show c'; they could even be-mouse-clicks or menu items--whatever suits your program.--You should also get your employer (if you work as a programmer) or your-school, if any, to sign a "copyright disclaimer" for the program, if-necessary. Here is a sample; alter the names:+Copyright (c) 2013, Kristof Bastiaensen+All rights reserved. - Yoyodyne, Inc., hereby disclaims all copyright interest in the program- `Gnomovision' (which makes passes at compilers) written by James Hacker.+Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met: - {signature of Ty Coon}, 1 April 1989- Ty Coon, President of Vice+ Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.+ Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in+ the documentation and/or other materials provided with the+ distribution. Neither the name of the organisation nor the+ names of its contributors may be used to endorse or promote+ products derived from this software without specific prior written+ permission. -This General Public License does not permit incorporating your program into-proprietary programs. If your program is a subroutine library, you may-consider it more useful to permit linking proprietary applications with the-library. If this is what you want to do, use the GNU Lesser General-Public License instead of this License.+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Math/BernsteinPoly.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE BangPatterns #-} module Math.BernsteinPoly (BernsteinPoly(..), bernsteinSubsegment, listToBernstein, zeroPoly, (~*), (*~), (~+), (~-), degreeElevate, bernsteinSplit, bernsteinEval,@@ -7,8 +8,8 @@ import Data.List data BernsteinPoly = BernsteinPoly {- bernsteinDegree :: Int,- bernsteinCoeffs :: [Double] }+ bernsteinDegree :: !Int,+ bernsteinCoeffs :: ![Double] } deriving Show infixl 7 ~*, *~@@ -49,32 +50,39 @@ a' = zipWith (*) a (binCoeff la) b' = zipWith (*) b (binCoeff lb) -degreeElevate' :: BernsteinPoly -> Int -> BernsteinPoly-degreeElevate' b 0 = b-degreeElevate' (BernsteinPoly lp p) times =- degreeElevate' (BernsteinPoly (lp+1) (head p:inner p 1)) (times-1)- where- inner [a] _ = [a]- inner (a:b:rest) i =- (i*a/fromIntegral lp + b*(1 - i/fromIntegral lp))- : inner (b:rest) (i+1) -- find the binomial coefficients of degree n. binCoeff :: Int -> [Double] binCoeff n = map fromIntegral $ scanl (\x m -> x * (n-m+1) `quot` m) 1 [1..n] --- | Degree elevate a bernstein polynomail.+-- | Degree elevate a bernstein polynomail a number of times. degreeElevate :: BernsteinPoly -> Int -> BernsteinPoly-degreeElevate l times = degreeElevate' l times+degreeElevate b 0 = b+degreeElevate (BernsteinPoly lp p) times =+ degreeElevate (BernsteinPoly (lp+1) (head p:inner p 1)) (times-1)+ where+ n = fromIntegral lp+ inner [] _ = error "empty bernstein coefficients"+ inner [a] _ = [a]+ inner (a:b:rest) i =+ (i*a/(n+1) + b*(1 - i/(n+1)))+ : inner (b:rest) (i+1) + -- | Evaluate the bernstein polynomial. bernsteinEval :: BernsteinPoly -> Double -> Double-bernsteinEval (BernsteinPoly lp p) t = foldl' addcoeff 0 $- zip3 ts (binCoeff lp) p- where ts = iterate (*t) 1- u = 1-t- addcoeff a (s, d, b) = (a*u + b*s*d)+bernsteinEval (BernsteinPoly lp [b]) _ = b+bernsteinEval (BernsteinPoly lp (b':bs)) t = go t n (b'*u) 2 bs+ where u = 1-t+ n = fromIntegral lp+ go !tn !bc !tmp _ [b] = tmp + tn*bc*b+ go !tn !bc !tmp !i (b:rest) =+ go (tn*t) -- tn+ (bc*(n-i+1)/i) -- bc+ ((tmp + tn*bc*b)*u) -- tmp+ (i+1) -- i+ rest -- | Evaluate the bernstein polynomial and its derivatives. bernsteinEvalDerivs :: BernsteinPoly -> Double -> [Double]@@ -88,8 +96,7 @@ bernsteinDeriv (BernsteinPoly 0 _) = zeroPoly bernsteinDeriv (BernsteinPoly lp p) = BernsteinPoly (lp-1) $- map (* fromIntegral lp) $- zipWith (-) (tail p) p+ map (* fromIntegral lp) $ zipWith (-) (tail p) p -- | Split a bernstein polynomial bernsteinSplit :: BernsteinPoly -> Double -> (BernsteinPoly, BernsteinPoly)
cubicbezier.cabal view
@@ -1,10 +1,10 @@ Name: cubicbezier-Version: 0.1.0+Version: 0.2.0 Synopsis: Efficient manipulating of 2D cubic bezier curves. Category: Graphics, Geometry, Typography Copyright: Kristof Bastiaensen (2013) Stability: Unstable-License: GPL-2+License: BSD3 License-file: LICENSE Author: Kristof Bastiaensen Maintainer: Kristof Bastiaensen@@ -26,7 +26,8 @@ location: https://github.com/kuribas/cubicbezier Library- Build-depends: base >= 3 && < 5, containers > 0.4, integration >= 0.1.1+ Ghc-options: -Wall+ Build-depends: base >= 3 && < 5, containers > 0.4, integration >= 0.1.1, deepseq >= 1.3.0 Exposed-Modules: Geom2D Geom2D.CubicBezier@@ -35,6 +36,7 @@ Geom2D.CubicBezier.Outline Geom2D.CubicBezier.Curvature Geom2D.CubicBezier.Intersection+ Geom2D.CubicBezier.MetaPath Math.BernsteinPoly Other-Modules: Geom2D.CubicBezier.Numeric