{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE ViewPatterns #-} ----------------------------------------------------------------------------- -- | -- Module : Diagrams.Segment -- Copyright : (c) 2011-2013 diagrams-lib team (see LICENSE) -- License : BSD-style (see LICENSE) -- Maintainer : diagrams-discuss@googlegroups.com -- -- A /segment/ is a translation-invariant, atomic path. Currently, -- there are two types: linear (/i.e./ just a straight line to the -- endpoint) and cubic Bézier curves (/i.e./ a curve to an endpoint -- with two control points). This module contains tools for creating -- and manipulating segments, as well as a definition of segments with -- a fixed location (useful for backend implementors). -- -- Generally speaking, casual users of diagrams should not need this -- module; the higher-level functionality provided by -- "Diagrams.Trail", "Diagrams.TrailLike", and "Diagrams.Path" should -- usually suffice. However, directly manipulating segments can -- occasionally be useful. -- ----------------------------------------------------------------------------- module Diagrams.Segment ( -- * Open/closed tags Open, Closed -- * Segment offsets , Offset(..), segOffset -- * Constructing and modifying segments , Segment(..), straight, bezier3, bézier3, reverseSegment -- * Fixed (absolutely located) segments , FixedSegment(..) , mkFixedSeg, fromFixedSeg -- * Segment measures -- $segmeas , SegCount(..) , ArcLength(..) , getArcLengthCached, getArcLengthFun, getArcLengthBounded , TotalOffset(..) , OffsetEnvelope(..), oeOffset, oeEnvelope , SegMeasure ) where import Control.Lens (makeLenses, Wrapped(..), Rewrapped, iso, op) import Control.Applicative (liftA2) import Data.AffineSpace import Data.FingerTree import Data.Monoid.MList import Data.Semigroup import Data.VectorSpace hiding (Sum (..)) import Numeric.Interval.Kaucher (Interval (..)) import qualified Numeric.Interval.Kaucher as I import Diagrams.Core import Diagrams.Located import Diagrams.Parametric import Diagrams.Solve ------------------------------------------------------------ -- Open/closed type tags --------------------------------- ------------------------------------------------------------ -- Eventually we should use DataKinds for this, but not until we drop -- support for GHC 7.4. -- | Type tag for open segments. data Open -- | Type tag for closed segments. data Closed ------------------------------------------------------------ -- Segment offsets --------------------------------------- ------------------------------------------------------------ -- | The /offset/ of a segment is the vector from its starting point -- to its end. The offset for an /open/ segment is determined by -- the context, /i.e./ its endpoint is not fixed. The offset for a -- /closed/ segment is stored explicitly, /i.e./ its endpoint is at -- a fixed offset from its start. data Offset c v where OffsetOpen :: Offset Open v OffsetClosed :: !v -> Offset Closed v deriving instance Show v => Show (Offset c v) deriving instance Eq v => Eq (Offset c v) deriving instance Ord v => Ord (Offset c v) instance Functor (Offset c) where fmap _ OffsetOpen = OffsetOpen fmap f (OffsetClosed v) = OffsetClosed (f v) type instance V (Offset c v) = v instance HasLinearMap v => Transformable (Offset c v) where transform = fmap . apply ------------------------------------------------------------ -- Constructing segments --------------------------------- ------------------------------------------------------------ -- | The atomic constituents of the concrete representation currently -- used for trails are /segments/, currently limited to -- single straight lines or cubic Bézier curves. Segments are -- /translationally invariant/, that is, they have no particular -- \"location\" and are unaffected by translations. They are, -- however, affected by other transformations such as rotations and -- scales. data Segment c v = Linear !(Offset c v) -- ^ A linear segment with given offset. | Cubic !v !v !(Offset c v) -- ^ A cubic Bézier segment specified by -- three offsets from the starting -- point to the first control point, -- second control point, and ending -- point, respectively. deriving (Show, Functor, Eq, Ord) -- Note, can't yet have Haddock comments on GADT constructors; see -- http://trac.haskell.org/haddock/ticket/43. For now we don't need -- Segment to be a GADT but we might in the future. (?) type instance V (Segment c v) = v instance HasLinearMap v => Transformable (Segment c v) where transform = fmap . apply instance HasLinearMap v => Renderable (Segment c v) NullBackend where render _ _ = mempty -- | @'straight' v@ constructs a translationally invariant linear -- segment with direction and length given by the vector @v@. straight :: v -> Segment Closed v straight = Linear . OffsetClosed -- Note, if we didn't have a Linear constructor we could also create -- linear segments with @Cubic (v ^/ 3) (2 *^ (v ^/ 3)) v@. Those -- would not be precisely the same, however, since we can actually -- observe how segments are parametrized. -- | @bezier3 c1 c2 x@ constructs a translationally invariant cubic -- Bézier curve where the offsets from the first endpoint to the -- first and second control point and endpoint are respectively -- given by @c1@, @c2@, and @x@. bezier3 :: v -> v -> v -> Segment Closed v bezier3 c1 c2 x = Cubic c1 c2 (OffsetClosed x) -- | @bézier3@ is the same as @bezier3@, but with more snobbery. bézier3 :: v -> v -> v -> Segment Closed v bézier3 = bezier3 type instance Codomain (Segment Closed v) = v -- | 'atParam' yields a parametrized view of segments as continuous -- functions @[0,1] -> v@, which give the offset from the start of -- the segment for each value of the parameter between @0@ and @1@. -- It is designed to be used infix, like @seg ``atParam`` 0.5@. instance (VectorSpace v, Num (Scalar v)) => Parametric (Segment Closed v) where atParam (Linear (OffsetClosed x)) t = t *^ x atParam (Cubic c1 c2 (OffsetClosed x2)) t = (3 * t'*t'*t ) *^ c1 ^+^ (3 * t'*t *t ) *^ c2 ^+^ ( t *t *t ) *^ x2 where t' = 1-t instance Num (Scalar v) => DomainBounds (Segment Closed v) instance (VectorSpace v, Num (Scalar v)) => EndValues (Segment Closed v) where atStart = const zeroV atEnd (Linear (OffsetClosed v)) = v atEnd (Cubic _ _ (OffsetClosed v)) = v -- | Compute the offset from the start of a segment to the -- end. Note that in the case of a Bézier segment this is /not/ the -- same as the length of the curve itself; for that, see 'arcLength'. segOffset :: Segment Closed v -> v segOffset (Linear (OffsetClosed v)) = v segOffset (Cubic _ _ (OffsetClosed v)) = v ------------------------------------------------------------ -- Computing segment envelope ------------------------------ ------------------------------------------------------------ {- 3 (1-t)^2 t c1 + 3 (1-t) t^2 c2 + t^3 x2 Can we compute the projection of B(t) onto a given vector v? u.v = |u||v| cos th |proj_v u| = cos th * |u| = (u.v/|v|) so B_v(t) = (B(t).v/|v|) Then take the derivative of this wrt. t, get a quadratic, solve. B_v(t) = (1/|v|) * -- note this does not affect max/min, can solve for t first 3 (1-t)^2 t (c1.v) + 3 (1-t) t^2 (c2.v) + t^3 (x2.v) = t^3 ((3c1 - 3c2 + x2).v) + t^2 ((-6c1 + 3c2).v) + t (3c1.v) B_v'(t) = t^2 (3(3c1 - 3c2 + x2).v) + t (6(-2c1 + c2).v) + 3c1.v Set equal to zero, use quadratic formula. -} -- | The envelope for a segment is based at the segment's start. instance (InnerSpace v, OrderedField (Scalar v)) => Enveloped (Segment Closed v) where getEnvelope (s@(Linear {})) = mkEnvelope $ \v -> maximum (map (\t -> ((s `atParam` t) <.> v)) [0,1]) / magnitudeSq v getEnvelope (s@(Cubic c1 c2 (OffsetClosed x2))) = mkEnvelope $ \v -> maximum . map (\t -> ((s `atParam` t) <.> v) / magnitudeSq v) $ [0,1] ++ filter (liftA2 (&&) (>0) (<1)) (quadForm (3 * ((3 *^ c1 ^-^ 3 *^ c2 ^+^ x2) <.> v)) (6 * (((-2) *^ c1 ^+^ c2) <.> v)) ((3 *^ c1) <.> v)) ------------------------------------------------------------ -- Manipulating segments ------------------------------------------------------------ instance (VectorSpace v, Fractional (Scalar v)) => Sectionable (Segment Closed v) where splitAtParam (Linear (OffsetClosed x1)) t = (left, right) where left = straight p right = straight (x1 ^-^ p) p = lerp zeroV x1 t splitAtParam (Cubic c1 c2 (OffsetClosed x2)) t = (left, right) where left = bezier3 a b e right = bezier3 (c ^-^ e) (d ^-^ e) (x2 ^-^ e) p = lerp c1 c2 t a = lerp zeroV c1 t b = lerp a p t d = lerp c2 x2 t c = lerp p d t e = lerp b c t reverseDomain = reverseSegment -- | Reverse the direction of a segment. reverseSegment :: AdditiveGroup v => Segment Closed v -> Segment Closed v reverseSegment (Linear (OffsetClosed v)) = straight (negateV v) reverseSegment (Cubic c1 c2 (OffsetClosed x2)) = bezier3 (c2 ^-^ x2) (c1 ^-^ x2) (negateV x2) instance (InnerSpace v, Floating (Scalar v), Ord (Scalar v), AdditiveGroup v) => HasArcLength (Segment Closed v) where arcLengthBounded _ (Linear (OffsetClosed x1)) = I.singleton $ magnitude x1 arcLengthBounded m s@(Cubic c1 c2 (OffsetClosed x2)) | ub - lb < m = I lb ub | otherwise = arcLengthBounded (m/2) l + arcLengthBounded (m/2) r where (l,r) = s `splitAtParam` 0.5 ub = sum (map magnitude [c1, c2 ^-^ c1, x2 ^-^ c2]) lb = magnitude x2 arcLengthToParam m s _ | arcLength m s == 0 = 0.5 arcLengthToParam m s@(Linear {}) len = len / arcLength m s arcLengthToParam m s@(Cubic {}) len | len `I.elem` (I (-m/2) (m/2)) = 0 | len < 0 = - arcLengthToParam m (fst (splitAtParam s (-1))) (-len) | len `I.elem` slen = 1 | len > I.sup slen = 2 * arcLengthToParam m (fst (splitAtParam s 2)) len | len < I.sup llen = (*0.5) $ arcLengthToParam m l len | otherwise = (+0.5) . (*0.5) $ arcLengthToParam (9*m/10) r (len - I.midpoint llen) where (l,r) = s `splitAtParam` 0.5 llen = arcLengthBounded (m/10) l slen = arcLengthBounded m s -- Note, the above seems to be quite slow since it duplicates a lot of -- work. We could trade off some time for space by building a tree of -- parameter values (up to a certain depth...) ------------------------------------------------------------ -- Fixed segments ------------------------------------------------------------ -- | @FixedSegment@s are like 'Segment's except that they have -- absolute locations. @FixedSegment v@ is isomorphic to @Located -- (Segment Closed v)@, as witnessed by 'mkFixedSeg' and -- 'fromFixedSeg', but @FixedSegment@ is convenient when one needs -- the absolute locations of the vertices and control points. data FixedSegment v = FLinear (Point v) (Point v) | FCubic (Point v) (Point v) (Point v) (Point v) deriving Show type instance V (FixedSegment v) = v instance HasLinearMap v => Transformable (FixedSegment v) where transform t (FLinear p1 p2) = FLinear (transform t p1) (transform t p2) transform t (FCubic p1 c1 c2 p2) = FCubic (transform t p1) (transform t c1) (transform t c2) (transform t p2) instance VectorSpace v => HasOrigin (FixedSegment v) where moveOriginTo o (FLinear p1 p2) = FLinear (moveOriginTo o p1) (moveOriginTo o p2) moveOriginTo o (FCubic p1 c1 c2 p2) = FCubic (moveOriginTo o p1) (moveOriginTo o c1) (moveOriginTo o c2) (moveOriginTo o p2) instance (InnerSpace v, OrderedField (Scalar v)) => Enveloped (FixedSegment v) where getEnvelope f = moveTo p (getEnvelope s) where (p, s) = viewLoc $ fromFixedSeg f -- Eventually we might decide it's cleaner/more efficient (?) to -- have all the computation in the FixedSegment instance of -- Envelope, and implement the Segment instance in terms of it, -- instead of the other way around -- | Create a 'FixedSegment' from a located 'Segment'. mkFixedSeg :: AdditiveGroup v => Located (Segment Closed v) -> FixedSegment v mkFixedSeg ls = case viewLoc ls of (p, Linear (OffsetClosed v)) -> FLinear p (p .+^ v) (p, Cubic c1 c2 (OffsetClosed x2)) -> FCubic p (p .+^ c1) (p .+^ c2) (p .+^ x2) -- | Convert a 'FixedSegment' back into a located 'Segment'. fromFixedSeg :: AdditiveGroup v => FixedSegment v -> Located (Segment Closed v) fromFixedSeg (FLinear p1 p2) = straight (p2 .-. p1) `at` p1 fromFixedSeg (FCubic x1 c1 c2 x2) = bezier3 (c1 .-. x1) (c2 .-. x1) (x2 .-. x1) `at` x1 type instance Codomain (FixedSegment v) = Point v instance VectorSpace v => Parametric (FixedSegment v) where atParam (FLinear p1 p2) t = alerp p1 p2 t atParam (FCubic x1 c1 c2 x2) t = p3 where p11 = alerp x1 c1 t p12 = alerp c1 c2 t p13 = alerp c2 x2 t p21 = alerp p11 p12 t p22 = alerp p12 p13 t p3 = alerp p21 p22 t instance Num (Scalar v) => DomainBounds (FixedSegment v) instance (VectorSpace v, Num (Scalar v)) => EndValues (FixedSegment v) where atStart (FLinear p0 _) = p0 atStart (FCubic p0 _ _ _) = p0 atEnd (FLinear _ p1) = p1 atEnd (FCubic _ _ _ p1 ) = p1 instance (VectorSpace v, Fractional (Scalar v)) => Sectionable (FixedSegment v) where splitAtParam (FLinear p0 p1) t = (left, right) where left = FLinear p0 p right = FLinear p p1 p = alerp p0 p1 t splitAtParam (FCubic p0 c1 c2 p1) t = (left, right) where left = FCubic p0 a b cut right = FCubic cut c d p1 -- first round a = alerp p0 c1 t p = alerp c1 c2 t d = alerp c2 p1 t -- second round b = alerp a p t c = alerp p d t -- final round cut = alerp b c t reverseDomain (FLinear p0 p1) = FLinear p1 p0 reverseDomain (FCubic p0 c1 c2 p1) = FCubic p1 c2 c1 p0 ------------------------------------------------------------ -- Segment measures -------------------------------------- ------------------------------------------------------------ -- $segmeas -- Trails store a sequence of segments in a fingertree, which can -- automatically track various monoidal \"measures\" on segments. -- | A type to track the count of segments in a 'Trail'. newtype SegCount = SegCount (Sum Int) deriving (Semigroup, Monoid) instance Wrapped SegCount where type Unwrapped SegCount = Sum Int _Wrapped' = iso (\(SegCount x) -> x) SegCount instance Rewrapped SegCount SegCount -- | A type to represent the total arc length of a chain of -- segments. The first component is a \"standard\" arc length, -- computed to within a tolerance of @10e-6@. The second component is -- a generic arc length function taking the tolerance as an -- argument. newtype ArcLength v = ArcLength (Sum (Interval (Scalar v)), Scalar v -> Sum (Interval (Scalar v))) instance Wrapped (ArcLength v) where type Unwrapped (ArcLength v) = (Sum (Interval (Scalar v)), Scalar v -> Sum (Interval (Scalar v))) _Wrapped' = iso (\(ArcLength x) -> x) ArcLength instance Rewrapped (ArcLength v) (ArcLength v') -- | Project out the cached arc length, stored together with error -- bounds. getArcLengthCached :: ArcLength v -> Interval (Scalar v) getArcLengthCached = getSum . fst . op ArcLength -- | Project out the generic arc length function taking the tolerance as -- an argument. getArcLengthFun :: ArcLength v -> Scalar v -> Interval (Scalar v) getArcLengthFun = fmap getSum . snd . op ArcLength -- | Given a specified tolerance, project out the cached arc length if -- it is accurate enough; otherwise call the generic arc length -- function with the given tolerance. getArcLengthBounded :: (Num (Scalar v), Ord (Scalar v)) => Scalar v -> ArcLength v -> Interval (Scalar v) getArcLengthBounded eps al | I.width cached <= eps = cached | otherwise = getArcLengthFun al eps where cached = getArcLengthCached al deriving instance (Num (Scalar v), Ord (Scalar v)) => Semigroup (ArcLength v) deriving instance (Num (Scalar v), Ord (Scalar v)) => Monoid (ArcLength v) -- | A type to represent the total cumulative offset of a chain of -- segments. newtype TotalOffset v = TotalOffset v instance Wrapped (TotalOffset v) where type Unwrapped (TotalOffset v) = v _Wrapped' = iso (\(TotalOffset x) -> x) TotalOffset instance Rewrapped (TotalOffset v) (TotalOffset v') instance AdditiveGroup v => Semigroup (TotalOffset v) where TotalOffset v1 <> TotalOffset v2 = TotalOffset (v1 ^+^ v2) instance AdditiveGroup v => Monoid (TotalOffset v) where mempty = TotalOffset zeroV mappend = (<>) -- | A type to represent the offset and envelope of a chain of -- segments. They have to be paired into one data structure, since -- combining the envelopes of two consecutive chains needs to take -- the offset of the the offset of the first into account. data OffsetEnvelope v = OffsetEnvelope { _oeOffset :: !(TotalOffset v) , _oeEnvelope :: Envelope v } makeLenses ''OffsetEnvelope instance (InnerSpace v, OrderedField (Scalar v)) => Semigroup (OffsetEnvelope v) where (OffsetEnvelope o1 e1) <> (OffsetEnvelope o2 e2) = let !negOff = negateV . op TotalOffset $ o1 e2Off = moveOriginBy negOff e2 !() = maybe () (\f -> f `seq` ()) $ appEnvelope e2Off in OffsetEnvelope (o1 <> o2) (e1 <> e2Off) -- | @SegMeasure@ collects up all the measurements over a chain of -- segments. type SegMeasure v = SegCount ::: ArcLength v ::: OffsetEnvelope v ::: () -- unfortunately we can't cache Trace, since there is not a generic -- instance Traced (Segment Closed v), only Traced (Segment Closed R2). instance (InnerSpace v, OrderedField (Scalar v)) => Measured (SegMeasure v) (SegMeasure v) where measure = id instance (OrderedField (Scalar v), InnerSpace v) => Measured (SegMeasure v) (Segment Closed v) where measure s = (SegCount . Sum $ 1) -- cache arc length with two orders of magnitude more -- accuracy than standard, so we have a hope of coming out -- with an accurate enough total arc length for -- reasonable-length trails *: (ArcLength $ ( Sum $ arcLengthBounded (stdTolerance/100) s , Sum . flip arcLengthBounded s ) ) *: (OffsetEnvelope (TotalOffset . segOffset $ s) (getEnvelope s) ) *: ()