{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
module Diagrams.TwoD.Polygons(
PolyType(..)
, PolyOrientation(..)
, PolygonOpts(..), polyType, polyOrient, polyCenter
, polygon
, polyTrail
, polyPolarTrail
, polySidesTrail
, polyRegularTrail
, orient
, StarOpts(..)
, star
, GraphPart(..)
, orbits, mkGraph
) where
import Control.Lens (Lens', generateSignatures, lensRules,
makeLensesWith, view, (.~), (^.))
import Control.Monad (forM, liftM)
import Control.Monad.ST (ST, runST)
import Data.Array.ST (STUArray, newArray, readArray,
writeArray)
import Data.Default.Class
import Data.List (maximumBy, minimumBy)
import Data.Maybe (catMaybes)
#if __GLASGOW_HASKELL__ < 710
import Data.Monoid (mconcat, mempty)
#endif
import Data.Ord (comparing)
import Diagrams.Angle
import Diagrams.Core
import Diagrams.Located
import Diagrams.Path
import Diagrams.Points (centroid)
import Diagrams.Trail
import Diagrams.TrailLike
import Diagrams.TwoD.Types
import Diagrams.TwoD.Vector (leftTurn, unitX, unitY, unit_Y)
import Diagrams.Util (tau, ( # ))
import Linear.Affine
import Linear.Metric
import Linear.Vector
data PolyType n = PolyPolar [Angle n] [n]
| PolySides [Angle n] [n]
| PolyRegular Int n
data PolyOrientation n = NoOrient
| OrientH
| OrientV
| OrientTo (V2 n)
deriving (Eq, Ord, Show, Read)
data PolygonOpts n = PolygonOpts
{ _polyType :: PolyType n
, _polyOrient :: PolyOrientation n
, _polyCenter :: Point V2 n
}
makeLensesWith (generateSignatures .~ False $ lensRules) ''PolygonOpts
-- | Specification for the polygon's vertices.
polyType :: Lens' (PolygonOpts n) (PolyType n)
-- | Should a rotation be applied to the polygon in order to orient it in a
-- particular way?
polyOrient :: Lens' (PolygonOpts n) (PolyOrientation n)
-- | Should a translation be applied to the polygon in order to place the center
-- at a particular location?
polyCenter :: Lens' (PolygonOpts n) (Point V2 n)
-- | The default polygon is a regular pentagon of radius 1, centered
-- at the origin, aligned to the x-axis.
instance Num n => Default (PolygonOpts n) where
def = PolygonOpts (PolyRegular 5 1) OrientH origin
-- | Generate a polygon. See 'PolygonOpts' for more information.
polyTrail :: OrderedField n => PolygonOpts n -> Located (Trail V2 n)
polyTrail po = transform ori tr
where
tr = case po^.polyType of
PolyPolar ans szs -> polyPolarTrail ans szs
PolySides ans szs -> polySidesTrail ans szs
PolyRegular n r -> polyRegularTrail n r
ori = case po^.polyOrient of
OrientH -> orient unit_Y tr
OrientV -> orient unitX tr
OrientTo v -> orient v tr
NoOrient -> mempty
-- | Generate the polygon described by the given options.
polygon :: (InSpace V2 n t, TrailLike t) => PolygonOpts n -> t
polygon = trailLike . polyTrail
-- | Generate the located trail of a polygon specified by polar data
-- (central angles and radii). See 'PolyPolar'.
polyPolarTrail :: OrderedField n => [Angle n] -> [n] -> Located (Trail V2 n)
polyPolarTrail [] _ = emptyTrail `at` origin
polyPolarTrail _ [] = emptyTrail `at` origin
polyPolarTrail ans (r:rs) = tr `at` p1
where
p1 = p2 (1,0) # scale r
tr = closeTrail . trailFromVertices $
zipWith
(\a l -> rotate a . scale l $ p2 (1,0))
(scanl (^+^) zero ans)
(r:rs)
-- | Generate the vertices of a polygon specified by side length and
-- angles, and a starting point for the trail such that the origin
-- is at the centroid of the vertices. See 'PolySides'.
polySidesTrail :: OrderedField n => [Angle n] -> [n] -> Located (Trail V2 n)
polySidesTrail ans ls = tr `at` (centroid ps # scale (-1))
where
ans' = scanl (^+^) zero ans
offsets = zipWith rotate ans' (map (unitY ^*) ls)
ps = scanl (.+^) origin offsets
tr = closeTrail . trailFromOffsets $ offsets
-- | Generate the vertices of a regular polygon. See 'PolyRegular'.
polyRegularTrail :: OrderedField n => Int -> n -> Located (Trail V2 n)
polyRegularTrail n r = polyPolarTrail
(replicate (n - 1) $ fullTurn ^/ fromIntegral n)
(repeat r)
-- | Generate a transformation to orient a trail. @orient v t@
-- generates the smallest rotation such that one of the segments
-- adjacent to the vertex furthest in the direction of @v@ is
-- perpendicular to @v@.
orient :: OrderedField n => V2 n -> Located (Trail V2 n) -> Transformation V2 n
orient v = orientPoints v . trailVertices
orientPoints :: OrderedField n => V2 n -> [Point V2 n] -> Transformation V2 n
orientPoints _ [] = mempty
orientPoints _ [_] = mempty
orientPoints v xs = rotation a
where
(n1,x,n2) = maximumBy (comparing (distAlong v . sndOf3))
(zip3 (tail (cycle xs)) xs (last xs : init xs))
distAlong w ((.-. origin) -> p) = signum (w `dot` p) * norm (project w p)
sndOf3 (_,b,_) = b
-- a :: Angle (Scalar v)
a = minimumBy (comparing $ abs . view rad)
. map (angleFromNormal . (.-. x)) $ [n1,n2]
v' = signorm v
-- angleFromNormal :: v -> Angle (Scalar v)
angleFromNormal o
| leftTurn o' v' = phi
| otherwise = negated phi
where
o' = signorm o
theta = acos (v' `dot` o')
-- phi :: Angle (Scalar v)
phi
| theta <= tau/4 = tau/4 - theta @@ rad
| otherwise = theta - tau/4 @@ rad
------------------------------------------------------------
-- Function graphs
------------------------------------------------------------
-- $graphs
-- These functions are used to implement 'star', but are exported on
-- the offchance that someone else finds them useful.
-- | Pieces of a function graph can either be cycles or \"hairs\".
data GraphPart a = Cycle [a]
| Hair [a]
deriving (Show, Functor)
-- | @orbits f n@ computes the graph of @f@ on the integers mod @n@.
orbits :: (Int -> Int) -> Int -> [GraphPart Int]
orbits f n = runST genOrbits
where
f_n i = f i `mod` n
genOrbits :: ST s [GraphPart Int]
genOrbits = newArray (0,n-1) False >>= genOrbits'
genOrbits' :: STUArray s Int Bool -> ST s [GraphPart Int]
genOrbits' marks = liftM (concat . catMaybes) (forM [0 .. n-1] (genPart marks))
genPart :: STUArray s Int Bool -> Int -> ST s (Maybe [GraphPart Int])
genPart marks i = do
tr <- markRho i marks
case tr of
[] -> return Nothing
_ -> return . Just . splitParts $ tr
markRho :: Int -> STUArray s Int Bool -> ST s [Int]
markRho i marks = do
isMarked <- readArray marks i
if isMarked
then return []
else writeArray marks i True >>
liftM (i:) (markRho (f_n i) marks)
splitParts :: [Int] -> [GraphPart Int]
splitParts tr = hair ++ cyc
where hair | not (null tl) = [Hair $ tl ++ [f_n (last tl)]]
| otherwise = []
cyc | not (null body) = [Cycle body]
| otherwise = []
l = last tr
(tl, body) = span (/= f_n l) tr
-- | Generate a function graph from the given function and labels.
mkGraph :: (Int -> Int) -> [a] -> [GraphPart a]
mkGraph f xs = (map . fmap) (xs!!) $ orbits f (length xs)
------------------------------------------------------------
-- Star polygons
------------------------------------------------------------
-- | Options for creating \"star\" polygons, where the edges connect
-- possibly non-adjacent vertices.
data StarOpts = StarFun (Int -> Int)
-- ^ Specify the order in which the vertices should be
-- connected by a function that maps each vertex
-- index to the index of the vertex that should come
-- next. Indexing of vertices begins at 0.
| StarSkip Int
-- ^ Specify a star polygon by a \"skip\". A skip of
-- 1 indicates a normal polygon, where edges go
-- between successive vertices. A skip of 2 means
-- that edges will connect every second vertex,
-- skipping one in between. Generally, a skip of
-- /n/ means that edges will connect every /n/th
-- vertex.
-- | Create a generalized /star/ /polygon/. The 'StarOpts' are used
-- to determine in which order the given vertices should be
-- connected. The intention is that the second argument of type
-- @[Point v]@ could be generated by a call to 'polygon', 'regPoly', or
-- the like, since a list of vertices is 'TrailLike'. But of course
-- the list can be generated any way you like. A @'Path' 'v'@ is
-- returned (instead of any 'TrailLike') because the resulting path
-- may have more than one component, for example if the vertices are
-- to be connected in several disjoint cycles.
star :: OrderedField n => StarOpts -> [Point V2 n] -> Path V2 n
star sOpts vs = graphToPath $ mkGraph f vs
where f = case sOpts of
StarFun g -> g
StarSkip k -> (+k)
graphToPath = mconcat . map partToPath
partToPath (Cycle ps) = pathFromLocTrail
. mapLoc closeTrail
. fromVertices
$ ps
partToPath (Hair ps) = fromVertices ps