{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TypeFamilies #-} ----------------------------------------------------------------------------- -- | -- Module : Diagrams.TwoD.Path.Follow -- Copyright : (c) 2016 Brent Yorgey -- License : BSD-style (see LICENSE) -- Maintainer : byorgey@gmail.com -- -- An alternative monoid for trails which rotates trails so their -- starting and ending tangents match at join points. -- ----------------------------------------------------------------------------- module Diagrams.TwoD.Path.Follow ( Following, follow, unfollow ) where import Diagrams.Prelude import Data.Monoid.SemiDirectProduct.Strict -- | @Following@ is just like @Trail' Line V2@, except that it has a -- different 'Monoid' instance. @Following@ values are -- concatenated, just like regular lines, except that they are also -- rotated so the tangents match at the join point. In addition, -- they are normalized so the tangent at the start point is in the -- direction of the positive x axis (essentially we are considering -- trails equivalent up to rotation). -- -- Pro tip: you can concatenate a list of trails so their tangents -- match using 'ala' from "Control.Lens", like so: -- -- @ala follow foldMap :: [Trail' Line V2 n] -> Trail' Line V2 n@ -- -- This is illustrated in the example below. -- -- <<diagrams/src_Diagrams_TwoD_Path_Follow_followExample.svg#diagram=followExample&width=400>> -- -- > import Control.Lens (ala) -- > import Diagrams.TwoD.Path.Follow -- > -- > wibble :: Trail' Line V2 Double -- > wibble = hrule 1 <> hrule 0.5 # rotateBy (1/6) <> hrule 0.5 # rotateBy (-1/6) <> a -- > where a = arc (xDir # rotateBy (-1/4)) (1/5 @@ turn) -- > # scale 0.7 -- > -- > followExample = -- > [ wibble -- > , wibble -- > # replicate 5 -- > # ala follow foldMap -- > ] -- > # map stroke -- > # map centerXY -- > # vsep 1 -- > # frame 0.5 -- newtype Following n = Following { unFollowing :: Semi (Trail' Line V2 n) (Angle n) } deriving (Monoid) -- | Note this is only an iso when considering trails equivalent up to -- rotation. instance RealFloat n => Wrapped (Following n) where type Unwrapped (Following n) = Trail' Line V2 n _Wrapped' = iso unfollow follow instance RealFloat n => Rewrapped (Following n) (Following n') -- | Create a @Following@ from a line, normalizing it (by rotation) -- so that it starts in the positive x direction. follow :: RealFloat n => Trail' Line V2 n -> Following n follow t = Following $ (t # rotate (signedAngleBetween unitX s)) `tag` theta where s = tangentAtStart t e = tangentAtEnd t theta = signedAngleBetween e s -- | Project out the line from a `Following`. -- -- If trails are considered equivalent up to rotation, then -- 'unfollow' and 'follow' are inverse. unfollow :: Following n -> Trail' Line V2 n unfollow = untag . unFollowing