{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE FlexibleContexts #-} {-# OPTIONS -Wall #-} -------------------------------------------------------------------------------- -- | -- Module : Wumpus.Core.Geometry -- Copyright : (c) Stephen Tetley 2009-2011 -- License : BSD3 -- -- Maintainer : stephen.tetley@gmail.com -- Stability : unstable -- Portability : GHC -- -- Objects and operations for 2D geometry. -- -- Vector, point, 3x3 matrix, and radian representations, -- plus a type family @DUnit@ for parameterizing type classes -- with some /dimension/. -- -------------------------------------------------------------------------------- module Wumpus.Core.Geometry ( -- * Type family DUnit , Tolerance(..) -- * Data types , Vec2(..) , DVec2 , Point2(..) , DPoint2 , Matrix3'3(..) , DMatrix3'3 , Radian , MatrixMult(..) -- * Tolerance helpers , tEQ , tGT , tLT , tGTE , tLTE , tCompare -- * Vector operations , zeroVec , vec , hvec , vvec , avec , pvec , vreverse , vdirection , vlength , vangle -- * Point operations , zeroPt , minPt , maxPt , lineDirection -- * Matrix contruction , identityMatrix , scalingMatrix , translationMatrix , rotationMatrix , originatedRotationMatrix -- * Matrix operations , invert , determinant , transpose -- * Radian operations , toRadian , fromRadian , d2r , r2d , circularModulo -- * Bezier curves , bezierCircle , bezierEllipse , rbezierEllipse , bezierArc , subdivisionCircle ) where import Wumpus.Core.Utils.FormatCombinators import Data.AffineSpace -- package: vector-space import Data.VectorSpace -------------------------------------------------------------------------------- -- | Some unit of dimension usually Double. -- -- This very useful for reducing the kind of type classes to *. -- -- Then constraints on the Unit type can be declared on the -- instances rather than in the class declaration. -- type family DUnit a :: * -- Not exported - thanks to Max Bollingbroke. -- type family GuardEq a b :: * type instance GuardEq a a = a -- | Class for tolerance on floating point numbers. -- -- Two tolerances are required tolerance for equality - commonly -- used for testing if two points are equal - and tolerance for -- path length measurement. -- -- Path length measurement in Wumpus does not have a strong -- need to be exact (precision is computational costly) - by -- default it is 100x the equality tolerance. -- -- Bezier path lengths are calculated by iteration, so greater -- accuracy requires more compution. As it is hard to visually -- differentiate measures of less than a point the tolerance -- for Points is quite high quite high (0.1). -- -- The situation is more complicated for contextual units -- (Em and En) as they are really scaling factors. The bigger -- the point size the less accurate the measure is. -- class Num u => Tolerance u where eq_tolerance :: u length_tolerance :: u length_tolerance = 100 * eq_tolerance instance Tolerance Double where eq_tolerance = 0.001 length_tolerance = 0.1 -- Datatypes -- | 2D Vector - both components are strict. -- -- Note - equality is defined with 'Tolerance' and tolerance is -- quite high for the usual units. See the note for 'Point2'. -- data Vec2 u = V2 { vector_x :: !u , vector_y :: !u } deriving (Show) type DVec2 = Vec2 Double -- | 2D Point - both components are strict. -- -- Note - equality is defined with 'Tolerance' and tolerance is -- quite high for the usual units. -- -- This is useful for drawing, *but* unacceptable data centric -- work. If more accurate equality is needed define a newtype -- wrapper over the unit type and make a @Tolerance@ instance with -- much greater accuracy. -- data Point2 u = P2 { point_x :: !u , point_y :: !u } deriving (Show) type DPoint2 = Point2 Double -- | 3x3 matrix, considered to be in row-major form. -- -- > (M3'3 a b c -- > d e f -- > g h i) -- -- For instance the rotation matrix is represented as -- -- > ( cos(a) -sin(a) 0 -- > sin(a) cos(a) 0 -- > 0 0 1 ) -- -- This seems commplace in geometry texts, but PostScript -- represents the @current-transformation-matrix@ in -- column-major form. -- -- The right-most column is considered to represent a -- coordinate: -- -- > ( 1 0 x -- > 0 1 y -- > 0 0 1 ) -- > -- -- So a translation matrix representing the displacement in x -- of 40 and in y of 10 would be: -- -- > ( 1 0 40 -- > 0 1 10 -- > 0 0 1 ) -- > -- data Matrix3'3 u = M3'3 !u !u !u !u !u !u !u !u !u deriving (Eq) type DMatrix3'3 = Matrix3'3 Double -- | Radian is represented with a distinct type. -- Equality and ordering are approximate where the epsilon -- is 0.0001. newtype Radian = Radian { getRadian :: Double } deriving (Num,Real,Fractional,Floating,RealFrac,RealFloat) -------------------------------------------------------------------------------- -- Family instances type instance DUnit (Point2 u) = u type instance DUnit (Vec2 u) = u type instance DUnit (Matrix3'3 u) = u type instance DUnit (Maybe a) = DUnit a type instance DUnit (a,b) = GuardEq (DUnit a) (DUnit b) -------------------------------------------------------------------------------- -- lifters / convertors lift2Vec2 :: (u -> u -> u) -> Vec2 u -> Vec2 u -> Vec2 u lift2Vec2 op (V2 x y) (V2 x' y') = V2 (x `op` x') (y `op` y') lift2Matrix3'3 :: (u -> u -> u) -> Matrix3'3 u -> Matrix3'3 u -> Matrix3'3 u lift2Matrix3'3 op (M3'3 a b c d e f g h i) (M3'3 m n o p q r s t u) = M3'3 (a `op` m) (b `op` n) (c `op` o) (d `op` p) (e `op` q) (f `op` r) (g `op` s) (h `op` t) (i `op` u) -------------------------------------------------------------------------------- -- instances -- Eq (with tolerance) instance (Tolerance u, Ord u) => Eq (Vec2 u) where V2 x0 y0 == V2 x1 y1 = x0 `tEQ` x1 && y0 `tEQ` y1 instance (Tolerance u, Ord u) => Eq (Point2 u) where P2 x0 y0 == P2 x1 y1 = x0 `tEQ` x1 && y0 `tEQ` y1 -- Ord (with Tolerance) instance (Tolerance u, Ord u) => Ord (Vec2 u) where V2 x0 y0 `compare` V2 x1 y1 = case tCompare x0 x1 of EQ -> tCompare y0 y1 ans -> ans instance (Tolerance u, Ord u) => Ord (Point2 u) where P2 x0 y0 `compare` P2 x1 y1 = case tCompare x0 x1 of EQ -> tCompare y0 y1 ans -> ans -- Functor instance Functor Vec2 where fmap f (V2 a b) = V2 (f a) (f b) instance Functor Point2 where fmap f (P2 a b) = P2 (f a) (f b) instance Functor Matrix3'3 where fmap f (M3'3 m n o p q r s t u) = M3'3 (f m) (f n) (f o) (f p) (f q) (f r) (f s) (f t) (f u) -- Show instance Show u => Show (Matrix3'3 u) where show (M3'3 a b c d e f g h i) = "(M3'3 " ++ body ++ ")" where body = show [[a,b,c],[d,e,f],[g,h,i]] -- Num instance Num u => Num (Matrix3'3 u) where (+) = lift2Matrix3'3 (+) (-) = lift2Matrix3'3 (-) (*) (M3'3 a b c d e f g h i) (M3'3 m n o p q r s t u) = M3'3 (a*m+b*p+c*s) (a*n+b*q+c*t) (a*o+b*r+c*u) (d*m+e*p+f*s) (d*n+e*q+f*t) (d*o+e*r+f*u) (g*m+h*p+i*s) (g*n+h*q+i*t) (g*o+h*r+i*u) abs = fmap abs negate = fmap negate signum = fmap signum fromInteger a = M3'3 a' a' a' a' a' a' a' a' a' where a' = fromInteger a -------------------------------------------------------------------------------- -- Instances for Radian which are 'special'. instance Show Radian where showsPrec i (Radian a) = showsPrec i a instance Eq Radian where (==) = req instance Ord Radian where compare a b | a `req` b = EQ | otherwise = getRadian a `compare` getRadian b -------------------------------------------------------------------------------- -- Pretty printing instance Format u => Format (Vec2 u) where format (V2 a b) = parens (text "Vec" <+> format a <+> format b) instance Format u => Format (Point2 u) where format (P2 a b) = parens (format a <> comma <+> format b) instance Format u => Format (Matrix3'3 u) where format (M3'3 a b c d e f g h i) = vcat [matline a b c, matline d e f, matline g h i] where matline x y z = char '|' <+> (hcat $ map (fill 12 . format) [x,y,z]) <+> char '|' instance Format Radian where format (Radian d) = double d <> text ":rad" -------------------------------------------------------------------------------- -- Vector space instances instance Num u => AdditiveGroup (Vec2 u) where zeroV = V2 0 0 (^+^) = lift2Vec2 (+) negateV = fmap negate instance Num u => VectorSpace (Vec2 u) where type Scalar (Vec2 u) = u s *^ v = fmap (s*) v -- scalar (dot / inner) product via the class InnerSpace -- -- This definition mandates UndecidableInstances, but this seems -- in line with Data.VectorSpace... -- instance (Num u, InnerSpace u, u ~ Scalar u) => InnerSpace (Vec2 u) where (V2 a b) <.> (V2 a' b') = (a <.> a') ^+^ (b <.> b') instance Num u => AffineSpace (Point2 u) where type Diff (Point2 u) = Vec2 u (P2 a b) .-. (P2 x y) = V2 (a-x) (b-y) (P2 a b) .+^ (V2 vx vy) = P2 (a+vx) (b+vy) instance Num u => AdditiveGroup (Matrix3'3 u) where zeroV = fromInteger 0 (^+^) = (+) negateV = negate instance Num u => VectorSpace (Matrix3'3 u) where type Scalar (Matrix3'3 u) = u s *^ m = fmap (s*) m -------------------------------------------------------------------------------- -- Matrix multiply infixr 7 *# -- | Matrix multiplication - typically of points and vectors -- represented as homogeneous coordinates. -- class MatrixMult t where (*#) :: Num u => Matrix3'3 u -> t u -> t u instance MatrixMult Vec2 where (M3'3 a b c d e f _ _ _) *# (V2 m n) = V2 (a*m + b*n + c*0) (d*m + e*n + f*0) instance MatrixMult Point2 where (M3'3 a b c d e f _ _ _) *# (P2 m n) = P2 (a*m + b*n + c*1) (d*m + e*n + f*1) -------------------------------------------------------------------------------- infix 4 `tEQ`, `tLT`, `tGT` -- | Tolerant equality - helper function for defining Eq instances -- that use tolerance. -- -- Note - the definition actually needs Ord which is -- unfortunate (as Ord is /inaccurate/). -- tEQ :: (Tolerance u, Ord u) => u -> u -> Bool tEQ a b = (abs (a-b)) < eq_tolerance -- | Tolerant less than. -- -- Note - the definition actually needs Ord which is -- unfortunate (as Ord is /inaccurate/). -- tLT :: (Tolerance u, Ord u) => u -> u -> Bool tLT a b = a < b && (b - a) > eq_tolerance -- | Tolerant greater than. -- -- Note - the definition actually needs Ord which is -- unfortunate (as Ord is /inaccurate/). -- tGT :: (Tolerance u, Ord u) => u -> u -> Bool tGT a b = a > b && (a - b) > eq_tolerance -- | Tolerant less than or equal. -- -- Note - the definition actually needs Ord which is -- unfortunate (as Ord is /inaccurate/). -- tLTE :: (Tolerance u, Ord u) => u -> u -> Bool tLTE a b = tEQ a b || tLT a b -- | Tolerant greater than or equal. -- -- Note - the definition actually needs Ord which is -- unfortunate (as Ord is /inaccurate/). -- tGTE :: (Tolerance u, Ord u) => u -> u -> Bool tGTE a b = tEQ a b || tGT a b -- | Tolerant @compare@. -- tCompare :: (Tolerance u, Ord u) => u -> u -> Ordering tCompare a b | a `tEQ` b = EQ | otherwise = compare a b -------------------------------------------------------------------------------- -- Vectors -- | Construct a the empty vector (0,0). -- -- Note - this is equivalent to @zeroV@ in @Data.AdditiveGroup@. -- It is provided here for convenience as it may save an extra -- module import in client code. -- zeroVec :: Num u => Vec2 u zeroVec = V2 0 0 -- | 'vec' : @ x_component * y_component -> Vec2 @ -- -- A synonym for the constructor 'V2' with a Num constraint on -- the arguments. -- -- Essentially this function is superfluous, but it is slightly -- more pleasant typographically when used in lists of vectors: -- -- > [ vec 2 2, vvec 4, hvec 4, vec 2 2 ] -- -- Versus: -- -- > [ V2 2 2, vvec 4, hvec 4, V2 2 2 ] -- vec :: Num u => u -> u -> Vec2 u vec = V2 -- | 'hvec' : @ x_component -> Vec2 @ -- -- Construct a vector with horizontal displacement. -- hvec :: Num u => u -> Vec2 u hvec d = V2 d 0 -- | 'vvec' @ y_component -> Vec2 @ -- -- Construct a vector with vertical displacement. -- vvec :: Num u => u -> Vec2 u vvec d = V2 0 d -- | 'avec' : @ angle * distance -> Vec2 @ -- -- Construct a vector from an angle and magnitude. -- avec :: Floating u => Radian -> u -> Vec2 u avec theta d = V2 x y where ang = fromRadian $ circularModulo theta x = d * cos ang y = d * sin ang -- | 'pvec' : @ point_from * point_to -> Vec2 @ -- -- The vector between two points -- -- > pvec = flip (.-.) -- pvec :: Num u => Point2 u -> Point2 u -> Vec2 u pvec = flip (.-.) -- | 'vreverse' : @ vec -> Vec2 @ -- -- Reverse a vector. -- vreverse :: Num u => Vec2 u -> Vec2 u vreverse (V2 x y) = V2 (-x) (-y) -- | 'vdirection' : @ vec -> Radian @ -- -- Direction of a vector - i.e. the counter-clockwise angle -- from the x-axis. -- vdirection :: (Floating u, Real u) => Vec2 u -> Radian vdirection (V2 x y) = lineDirection (P2 0 0) (P2 x y) -- | 'vlength' : @ vec -> Length @ -- -- Length of a vector. -- vlength :: Floating u => Vec2 u -> u vlength (V2 x y) = sqrt $ x*x + y*y -- | 'vangle' : @ vec1 * vec2 -> Radian @ -- -- Extract the angle between two vectors. -- vangle :: (Floating u, Real u, InnerSpace (Vec2 u)) => Vec2 u -> Vec2 u -> Radian vangle u v = realToFrac $ acos $ (u <.> v) / (magnitude u * magnitude v) -------------------------------------------------------------------------------- -- Points -- | Construct a point at (0,0). -- zeroPt :: Num u => Point2 u zeroPt = P2 0 0 -- | 'minPt' : @ point1 * point2 -> Point2 @ -- -- Synthetic, /component-wise/ min on points. Standard 'min' and -- 'max' via Ord are defined lexographically on pairs, e.g.: -- -- > min (1,2) (2,1) = (1,2) -- -- For Points we want the component-wise min and max, that -- potentially synthesizes a new point, e.g: -- -- > minPt (P2 1 2) (Pt 2 1) = Pt 1 1 -- > maxPt (P2 1 2) (Pt 2 1) = Pt 2 2 -- minPt :: Ord u => Point2 u -> Point2 u -> Point2 u minPt (P2 x y) (P2 x' y') = P2 (min x x') (min y y') -- | 'maxPt' : @ point1 * point2 -> Point @ -- -- Synthetic, /component-wise/ max on points. -- -- > maxPt (P2 1 2) (Pt 2 1) = Pt 2 2 -- maxPt :: Ord u => Point2 u -> Point2 u -> Point2 u maxPt (P2 x y) (P2 x' y') = P2 (max x x') (max y y') -- | 'lineDirection' : @ start_point * end_point -> Radian @ -- -- Calculate the counter-clockwise angle between two points -- and the x-axis. -- lineDirection :: (Floating u, Real u) => Point2 u -> Point2 u -> Radian lineDirection (P2 x1 y1) (P2 x2 y2) = step (x2 - x1) (y2 - y1) where -- Special cases for continuity - the equality test should -- catch both 0.0 and (-0.0). -- Note - there is undoubtedly a better way of doing this. step x y | x == 0 && y == 0 = 0 step x y | x == 0 = if y >=0 then 0.5*pi else 1.5*pi step x y | y == 0 = if x >=0 then 0 else pi -- north-east quadrant step x y | pve x && pve y = toRadian $ atan (y/x) -- north-west quadrant step x y | pve y = pi - (toRadian $ atan (y / abs x)) -- south-east quadrant step x y | pve x = (2*pi) - (toRadian $ atan (abs y / x)) -- otherwise... south-west quadrant step x y = pi + (toRadian $ atan (y/x)) pve a = signum a >= 0 -------------------------------------------------------------------------------- -- Matrix construction -- | Construct the identity matrix: -- -- > (M3'3 1 0 0 -- > 0 1 0 -- > 0 0 1 ) -- identityMatrix :: Num u => Matrix3'3 u identityMatrix = M3'3 1 0 0 0 1 0 0 0 1 -- Common transformation matrices (for 2d homogeneous coordinates) -- | 'scalingMatrix' : @ x_scale_factor * y_scale_factor -> Matrix @ -- -- Construct a scaling matrix: -- -- > (M3'3 sx 0 0 -- > 0 sy 0 -- > 0 0 1 ) -- scalingMatrix :: Num u => u -> u -> Matrix3'3 u scalingMatrix sx sy = M3'3 sx 0 0 0 sy 0 0 0 1 -- | 'translationMatrix' : @ x_displacement * y_displacement -> Matrix @ -- -- Construct a translation matrix: -- -- > (M3'3 1 0 x -- > 0 1 y -- > 0 0 1 ) -- translationMatrix :: Num u => u -> u -> Matrix3'3 u translationMatrix x y = M3'3 1 0 x 0 1 y 0 0 1 -- | 'rotationMatrix' : @ ang -> Matrix @ -- -- Construct a rotation matrix: -- -- > (M3'3 cos(a) -sin(a) 0 -- > sin(a) cos(a) 0 -- > 0 0 1 ) -- rotationMatrix :: (Floating u, Real u) => Radian -> Matrix3'3 u rotationMatrix a = M3'3 (cos ang) (negate $ sin ang) 0 (sin ang) (cos ang) 0 0 0 1 where ang = fromRadian a -- No reflectionMatrix function -- A reflection about the x-axis is a scale of 1 (-1) -- A reflection about the y-axis is a scale of (-1) 1 -- | 'originatedRotationMatrix' : @ ang * point -> Matrix @ -- -- Construct a matrix for rotation about some /point/. -- -- This is the product of three matrices: T R T^-1 -- -- (T being the translation matrix, R the rotation matrix and -- T^-1 the inverse of the translation matrix). -- originatedRotationMatrix :: (Floating u, Real u) => Radian -> (Point2 u) -> Matrix3'3 u originatedRotationMatrix ang (P2 x y) = mT * (rotationMatrix ang) * mTinv where mT = M3'3 1 0 x 0 1 y 0 0 1 mTinv = M3'3 1 0 (-x) 0 1 (-y) 0 0 1 -------------------------------------------------------------------------------- -- Matrix ops -- | Invert a matrix. -- invert :: Fractional u => Matrix3'3 u -> Matrix3'3 u invert m = (1 / determinant m) *^ adjoint m -- | Determinant of a matrix. -- determinant :: Num u => Matrix3'3 u -> u determinant (M3'3 a b c d e f g h i) = a*e*i - a*f*h - b*d*i + b*f*g + c*d*h - c*e*g -- | Transpose a matrix. -- transpose :: Matrix3'3 u -> Matrix3'3 u transpose (M3'3 a b c d e f g h i) = M3'3 a d g b e h c f i -- Helpers adjoint :: Num u => Matrix3'3 u -> Matrix3'3 u adjoint = transpose . cofactor . mofm cofactor :: Num u => Matrix3'3 u -> Matrix3'3 u cofactor (M3'3 a b c d e f g h i) = M3'3 a (-b) c (-d) e (-f) g (-h) i mofm :: Num u => Matrix3'3 u -> Matrix3'3 u mofm (M3'3 a b c d e f g h i) = M3'3 m11 m12 m13 m21 m22 m23 m31 m32 m33 where m11 = (e*i) - (f*h) m12 = (d*i) - (f*g) m13 = (d*h) - (e*g) m21 = (b*i) - (c*h) m22 = (a*i) - (c*g) m23 = (a*h) - (b*g) m31 = (b*f) - (c*e) m32 = (a*f) - (c*d) m33 = (a*e) - (b*d) -------------------------------------------------------------------------------- -- Radians -- | The epislion used for floating point equality on radians. -- radian_epsilon :: Double radian_epsilon = 0.0001 -- | Equality on radians, this is the operation used for (==) in -- Radian\'s Eq instance. -- req :: Radian -> Radian -> Bool req a b = (fromRadian $ abs (a-b)) < radian_epsilon -- | Convert to radians. -- toRadian :: Real a => a -> Radian toRadian = Radian . realToFrac -- | Convert from radians. -- fromRadian :: Fractional a => Radian -> a fromRadian = realToFrac . getRadian -- | Degrees to radians. -- d2r :: (Floating a, Real a) => a -> Radian d2r = Radian . realToFrac . (*) (pi/180) -- | Radians to degrees. -- r2d :: (Floating a, Real a) => Radian -> a r2d = (*) (180/pi) . fromRadian -- | Modulo a (positive) angle into the range @0..2*pi@. -- circularModulo :: Radian -> Radian circularModulo r = d2r $ dec + (fromIntegral $ i `mod` 360) where i :: Integer dec :: Double (i,dec) = properFraction $ r2d r -------------------------------------------------------------------------------- -- Bezier curves kappa :: Floating u => u kappa = 4 * ((sqrt 2 - 1) / 3) -- | 'bezierCircle' : @ radius * center -> [Point] @ -- -- Make a circle from four Bezier curves. Although this function -- produces an approximation of a circle, the approximation seems -- fine in practice. -- bezierCircle :: (Fractional u, Floating u) => u -> Point2 u -> [Point2 u] bezierCircle radius (P2 x y) = [ p00,c01,c02, p03,c04,c05, p06,c07,c08, p09,c10,c11, p00 ] where rl = radius * kappa p00 = P2 (x + radius) y c01 = p00 .+^ vvec rl c02 = p03 .+^ hvec rl p03 = P2 x (y + radius) c04 = p03 .+^ hvec (-rl) c05 = p06 .+^ vvec rl p06 = P2 (x - radius) y c07 = p06 .+^ vvec (-rl) c08 = p09 .+^ hvec (-rl) p09 = P2 x (y - radius) c10 = p09 .+^ hvec rl c11 = p00 .+^ vvec (-rl) -- | 'bezierEllipse' : @ x_radius * y_radius * center -> [Point] @ -- -- Make an ellipse from four Bezier curves. Although this function -- produces an approximation of a ellipse, the approximation seems -- fine in practice. -- bezierEllipse :: (Fractional u, Floating u) => u -> u -> Point2 u -> [Point2 u] bezierEllipse rx ry (P2 x y) = [ p00,c01,c02, p03,c04,c05, p06,c07,c08, p09,c10,c11, p00 ] where lrx = rx * kappa lry = ry * kappa p00 = P2 (x + rx) y c01 = p00 .+^ vvec lry c02 = p03 .+^ hvec lrx p03 = P2 x (y + ry) c04 = p03 .+^ hvec (-lrx) c05 = p06 .+^ vvec lry p06 = P2 (x - rx) y c07 = p06 .+^ vvec (-lry) c08 = p09 .+^ hvec (-lrx) p09 = P2 x (y - ry) c10 = p09 .+^ hvec lrx c11 = p00 .+^ vvec (-lry) -- | 'rbezierEllipse' : @ x_radius * y_radius * center * angle -> [Point] @ -- -- Make an rotated ellipse from four Bezier curves. -- -- Although this function produces an approximation of a ellipse, -- the approximation seems fine in practice. -- rbezierEllipse :: (Real u, Floating u) => u -> u -> Radian -> Point2 u -> [Point2 u] rbezierEllipse rx ry theta pt@(P2 x y) = [ p00,c01,c02, p03,c04,c05, p06,c07,c08, p09,c10,c11, p00 ] where lrx = rx * kappa lry = ry * kappa rotM = originatedRotationMatrix theta pt -- hvec becomes para para = \d -> avec theta d -- vvec becomes perp perp = \d -> avec (circularModulo $ theta + pi*0.5) d mkPt = \p1 -> rotM *# p1 p00 = mkPt $ P2 (x + rx) y c01 = p00 .+^ perp lry c02 = p03 .+^ para lrx p03 = mkPt $ P2 x (y + ry) c04 = p03 .+^ para (-lrx) c05 = p06 .+^ perp lry p06 = mkPt $ P2 (x - rx) y c07 = p06 .+^ perp (-lry) c08 = p09 .+^ para (-lrx) p09 = mkPt $ P2 x (y - ry) c10 = p09 .+^ para lrx c11 = p00 .+^ perp (-lry) -- | 'bezierArc' : @ radius * ang1 * ang2 * center -> -- (start_point, control_point1, control_point2, end_point) @ -- -- Create an arc - this construction is the analogue of -- PostScript\'s @arc@ command, but the arc is created as a -- Bezier curve so it should span less than 90deg. -- -- CAVEAT - ang2 must be greater than ang1 -- bezierArc :: Floating u => u -> Radian -> Radian -> Point2 u -> (Point2 u, Point2 u, Point2 u, Point2 u) bezierArc r ang1 ang2 pt = (p0,p1,p2,p3) where theta = ang2 - ang1 e = r * fromRadian ((2 * sin (theta/2)) / (1+ 2 * cos (theta/2))) p0 = pt .+^ avec ang1 r p1 = p0 .+^ avec (ang1 + pi/2) e p2 = p3 .+^ avec (ang2 - pi/2) e p3 = pt .+^ avec ang2 r -- | 'subvisionCircle' : @ subdivisions * radius * center -> [Point] @ -- -- Make a circle from Bezier curves - the number of subdivsions -- controls the accuracy or the curve, more subdivisions produce -- better curves, but less subdivisions are better for rendering -- (producing more efficient PostScript). -- -- Before revision 0.43.0, this was the only method in Wumpus to -- draw Bezier circles in Wumpus. However the kappa method seems -- to draw equally good circles and is more efficient both in the -- Haskell implementation and the generated PostScript code. This -- function is retained for completeness and testing. -- subdivisionCircle :: (Fractional u, Floating u) => Int -> u -> Point2 u -> [Point2 u] subdivisionCircle n radius pt = start $ subdivisions (n*4) (2*pi) where start (a:b:xs) = s : cp1 : cp2 : e : rest (b:xs) where (s,cp1,cp2,e) = bezierArc radius a b pt start _ = [] rest (a:b:xs) = cp1 : cp2 : e : rest (b:xs) where (_,cp1,cp2,e) = bezierArc radius a b pt rest _ = [] subdivisions i a = 0 : take i (iterate (+one) one) where one = a / fromIntegral i