diagrams-core-1.2: src/Diagrams/Core/Trace.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
-----------------------------------------------------------------------------
-- |
-- Module : Diagrams.Core.Trace
-- Copyright : (c) 2012 diagrams-core team (see LICENSE)
-- License : BSD-style (see LICENSE)
-- Maintainer : diagrams-discuss@googlegroups.com
--
-- @diagrams-core@ defines the core library of primitives
-- forming the basis of an embedded domain-specific language for
-- describing and rendering diagrams.
--
-- The @Trace@ module defines a data type and type class for
-- \"traces\", aka functional boundaries, essentially corresponding to
-- embedding a raytracer with each diagram.
--
-----------------------------------------------------------------------------
module Diagrams.Core.Trace
( -- * SortedList
SortedList
, mkSortedList, getSortedList, onSortedList, unsafeOnSortedList
-- * Traces
, Trace(Trace)
, appTrace
, mkTrace
-- * Traced class
, Traced(..)
-- * Computing with traces
, traceV, traceP
, maxTraceV, maxTraceP
, getRayTrace
, rayTraceV, rayTraceP
, maxRayTraceV, maxRayTraceP
) where
import Control.Applicative
import Control.Lens
import Data.List (sort)
import qualified Data.Map as M
import Data.Semigroup
import qualified Data.Set as S
import Data.AffineSpace
import Data.VectorSpace
import Diagrams.Core.HasOrigin
import Diagrams.Core.Points
import Diagrams.Core.Transform
import Diagrams.Core.V
------------------------------------------------------------
-- SortedList --------------------------------------------
------------------------------------------------------------
-- Traces return sorted lists of intersections, so we define a newtype
-- wrapper to represent sorted lists.
-- | A newtype wrapper around a list which maintains the invariant
-- that the list is sorted. The constructor is not exported; use
-- the smart constructor 'mkSortedList' (which sorts the given list)
-- instead.
newtype SortedList a = SortedList [a]
-- | A smart constructor for the 'SortedList' type, which sorts the
-- input to ensure the 'SortedList' invariant.
mkSortedList :: Ord a => [a] -> SortedList a
mkSortedList = SortedList . sort
-- | Project the (guaranteed sorted) list out of a 'SortedList'
-- wrapper.
getSortedList :: SortedList a -> [a]
getSortedList (SortedList as) = as
-- | Apply a list function to a 'SortedList'. The function need not
-- result in a sorted list; the result will be sorted before being
-- rewrapped as a 'SortedList'.
onSortedList :: Ord b => ([a] -> [b]) -> (SortedList a -> SortedList b)
onSortedList f = unsafeOnSortedList (sort . f)
-- | Apply an /order-preserving/ list function to a 'SortedList'. No
-- sorts or checks are done.
unsafeOnSortedList :: ([a] -> [b]) -> (SortedList a -> SortedList b)
unsafeOnSortedList f (SortedList as) = SortedList (f as)
-- | Merge two sorted lists. The result is the sorted list containing
-- all the elements of both input lists (with duplicates).
merge :: Ord a => SortedList a -> SortedList a -> SortedList a
merge (SortedList as) (SortedList bs) = SortedList (merge' as bs)
where
merge' xs [] = xs
merge' [] ys = ys
merge' (x:xs) (y:ys) =
if x <= y
then x : merge' xs (y:ys)
else y : merge' (x:xs) ys
-- | 'SortedList' forms a semigroup with 'merge' as composition.
instance Ord a => Semigroup (SortedList a) where
(<>) = merge
-- | 'SortedList' forms a monoid with 'merge' and the empty list.
instance Ord a => Monoid (SortedList a) where
mappend = (<>)
mempty = SortedList []
------------------------------------------------------------
-- Trace -------------------------------------------------
------------------------------------------------------------
-- > traceEx = mkTraceDia def
-- | Every diagram comes equipped with a /trace/. Intuitively, the
-- trace for a diagram is like a raytracer: given a line
-- (represented as a base point and a direction vector), the trace
-- computes a sorted list of signed distances from the base point to
-- all intersections of the line with the boundary of the
-- diagram.
--
-- Note that the outputs are not absolute distances, but multipliers
-- relative to the input vector. That is, if the base point is @p@
-- and direction vector is @v@, and one of the output scalars is
-- @s@, then there is an intersection at the point @p .+^ (s *^ v)@.
--
-- <<diagrams/src_Diagrams_Core_Trace_traceEx.svg#diagram=traceEx&width=200>>
newtype Trace v = Trace { appTrace :: Point v -> v -> SortedList (Scalar v) }
instance Wrapped (Trace v) where
type Unwrapped (Trace v) = Point v -> v -> SortedList (Scalar v)
_Wrapped' = iso appTrace Trace
instance Rewrapped (Trace v) (Trace v')
mkTrace :: (Point v -> v -> SortedList (Scalar v)) -> Trace v
mkTrace = Trace
-- | Traces form a semigroup with pointwise minimum as composition.
-- Hence, if @t1@ is the trace for diagram @d1@, and
-- @e2@ is the trace for @d2@, then @e1 \`mappend\` e2@
-- is the trace for @d1 \`atop\` d2@.
deriving instance (Ord (Scalar v)) => Semigroup (Trace v)
deriving instance (Ord (Scalar v)) => Monoid (Trace v)
type instance V (Trace v) = v
instance (VectorSpace v) => HasOrigin (Trace v) where
moveOriginTo (P u) = (_Wrapping' Trace) %~ \f p -> f (p .+^ u)
instance Show (Trace v) where
show _ = "<trace>"
------------------------------------------------------------
-- Transforming traces -----------------------------------
------------------------------------------------------------
instance HasLinearMap v => Transformable (Trace v) where
transform t = _Wrapped' %~ \f p v -> f (papply (inv t) p) (apply (inv t) v)
------------------------------------------------------------
-- Traced class ------------------------------------------
------------------------------------------------------------
-- | @Traced@ abstracts over things which have a trace.
class (Ord (Scalar (V a)), VectorSpace (V a)) => Traced a where
-- | Compute the trace of an object.
getTrace :: a -> Trace (V a)
instance (Ord (Scalar v), VectorSpace v) => Traced (Trace v) where
getTrace = id
-- | The trace of a single point is the empty trace, /i.e./ the one
-- which returns no intersection points for every query. Arguably
-- it should return a single finite distance for vectors aimed
-- directly at the given point, but due to floating-point inaccuracy
-- this is problematic. Note that the envelope for a single point
-- is /not/ the empty envelope (see "Diagrams.Core.Envelope").
instance (Ord (Scalar v), VectorSpace v) => Traced (Point v) where
getTrace = const mempty
instance Traced t => Traced (TransInv t) where
getTrace = getTrace . op TransInv
instance (Traced a, Traced b, V a ~ V b) => Traced (a,b) where
getTrace (x,y) = getTrace x <> getTrace y
instance (Traced b) => Traced [b] where
getTrace = mconcat . map getTrace
instance (Traced b) => Traced (M.Map k b) where
getTrace = mconcat . map getTrace . M.elems
instance (Traced b) => Traced (S.Set b) where
getTrace = mconcat . map getTrace . S.elems
------------------------------------------------------------
-- Computing with traces ---------------------------------
------------------------------------------------------------
-- | Compute the vector from the given point @p@ to the \"smallest\"
-- boundary intersection along the given vector @v@. The
-- \"smallest\" boundary intersection is defined as the one given by
-- @p .+^ (s *^ v)@ for the smallest (most negative) value of
-- @s@. Return @Nothing@ if there is no intersection. See also
-- 'traceP'.
--
-- See also 'rayTraceV' which uses the smallest /positive/
-- intersection, which is often more intuitive behavior.
--
-- <<diagrams/src_Diagrams_Core_Trace_traceVEx.svg#diagram=traceVEx&width=600>>
traceV :: Traced a => Point (V a) -> V a -> a -> Maybe (V a)
traceV p v a = case getSortedList $ op Trace (getTrace a) p v of
(s:_) -> Just (s *^ v)
[] -> Nothing
-- > traceVEx = mkTraceDiasABC def { drawV = True, sFilter = take 1 }
-- | Compute the \"smallest\" boundary point along the line determined
-- by the given point @p@ and vector @v@. The \"smallest\" boundary
-- point is defined as the one given by @p .+^ (s *^ v)@ for
-- the smallest (most negative) value of @s@. Return @Nothing@ if
-- there is no such boundary point. See also 'traceV'.
--
-- See also 'rayTraceP' which uses the smallest /positive/
-- intersection, which is often more intuitive behavior.
--
-- <<diagrams/src_Diagrams_Core_Trace_tracePEx.svg#diagram=tracePEx&width=600>>
traceP :: Traced a => Point (V a) -> V a -> a -> Maybe (Point (V a))
traceP p v a = (p .+^) <$> traceV p v a
-- > tracePEx = mkTraceDiasABC def { sFilter = take 1 }
-- | Like 'traceV', but computes a vector to the \"largest\" boundary
-- point instead of the smallest. (Note, however, the \"largest\"
-- boundary point may still be in the opposite direction from the
-- given vector, if all the boundary points are, as in the third
-- example shown below.)
--
-- <<diagrams/src_Diagrams_Core_Trace_maxTraceVEx.svg#diagram=maxTraceVEx&width=600>>
maxTraceV :: Traced a => Point (V a) -> V a -> a -> Maybe (V a)
maxTraceV p = traceV p . negateV
-- > maxTraceVEx = mkTraceDiasABC def { drawV = True, sFilter = dropAllBut1 }
-- | Like 'traceP', but computes the \"largest\" boundary point
-- instead of the smallest. (Note, however, the \"largest\" boundary
-- point may still be in the opposite direction from the given
-- vector, if all the boundary points are.)
--
-- <<diagrams/src_Diagrams_Core_Trace_maxTracePEx.svg#diagram=maxTracePEx&width=600>>
maxTraceP :: Traced a => Point (V a) -> V a -> a -> Maybe (Point (V a))
maxTraceP p v a = (p .+^) <$> maxTraceV p v a
-- > maxTracePEx = mkTraceDiasABC def { sFilter = dropAllBut1 }
-- | Get a modified 'Trace' for an object which only returns positive
-- boundary points, /i.e./ those boundary points given by a positive
-- scalar multiple of the direction vector. Note, this property
-- will be destroyed if the resulting 'Trace' is translated at all.
getRayTrace :: (Traced a, Num (Scalar (V a))) => a -> Trace (V a)
getRayTrace a = Trace $ \p v -> unsafeOnSortedList (dropWhile (<0)) $ appTrace (getTrace a) p v
-- | Compute the vector from the given point to the closest boundary
-- point of the given object in the given direction, or @Nothing@ if
-- there is no such boundary point (as in the third example
-- below). Note that unlike 'traceV', only /positive/ boundary
-- points are considered, /i.e./ boundary points corresponding to a
-- positive scalar multiple of the direction vector. This is
-- intuitively the \"usual\" behavior of a raytracer, which only
-- considers intersections \"in front of\" the camera. Compare the
-- second example diagram below with the second example shown for
-- 'traceV'.
--
-- <<diagrams/src_Diagrams_Core_Trace_rayTraceVEx.svg#diagram=rayTraceVEx&width=600>>
rayTraceV :: (Traced a, Num (Scalar (V a)))
=> Point (V a) -> V a -> a -> Maybe (V a)
rayTraceV p v a = case getSortedList $ op Trace (getRayTrace a) p v of
(s:_) -> Just (s *^ v)
[] -> Nothing
-- > rayTraceVEx = mkTraceDiasABC def { drawV = True, sFilter = take 1 . filter (>0) }
-- | Compute the boundary point on an object which is closest to the
-- given base point in the given direction, or @Nothing@ if there is
-- no such boundary point. Note that unlike 'traceP', only /positive/
-- boundary points are considered, /i.e./ boundary points
-- corresponding to a positive scalar multiple of the direction
-- vector. This is intuitively the \"usual\" behavior of a raytracer,
-- which only considers intersection points \"in front of\" the
-- camera.
--
-- <<diagrams/src_Diagrams_Core_Trace_rayTracePEx.svg#diagram=rayTracePEx&width=600>>
rayTraceP :: (Traced a, Num (Scalar (V a)))
=> Point (V a) -> V a -> a -> Maybe (Point (V a))
rayTraceP p v a = (p .+^) <$> rayTraceV p v a
-- > rayTracePEx = mkTraceDiasABC def { sFilter = take 1 . filter (>0) }
-- | Like 'rayTraceV', but computes a vector to the \"largest\"
-- boundary point instead of the smallest. Considers only
-- /positive/ boundary points.
--
-- <<diagrams/src_Diagrams_Core_Trace_maxRayTraceVEx.svg#diagram=maxRayTraceVEx&width=600>>
maxRayTraceV :: (Traced a, Num (Scalar (V a)))
=> Point (V a) -> V a -> a -> Maybe (V a)
maxRayTraceV p v a =
case getSortedList $ op Trace (getRayTrace a) p v of
[] -> Nothing
xs -> Just ((last xs) *^ v)
-- > maxRayTraceVEx = mkTraceDiasABC def { drawV = True, sFilter = dropAllBut1 . filter (>0) }
-- | Like 'rayTraceP', but computes the \"largest\" boundary point
-- instead of the smallest. Considers only /positive/ boundary
-- points.
--
-- <<diagrams/src_Diagrams_Core_Trace_maxRayTracePEx.svg#diagram=maxRayTracePEx&width=600>>
maxRayTraceP :: (Traced a, Num (Scalar (V a)))
=> Point (V a) -> V a -> a -> Maybe (Point (V a))
maxRayTraceP p v a = (p .+^) <$> maxRayTraceV p v a
-- > maxRayTracePEx = mkTraceDiasABC def { sFilter = dropAllBut1 . filter (>0) }
------------------------------------------------------------
-- Drawing trace diagrams
------------------------------------------------------------
-- > import Data.Default.Class
-- > import Control.Lens ((^.))
-- > import Data.Maybe (fromMaybe)
-- >
-- > thingyT :: Trail R2
-- > thingyT =
-- > fromOffsets
-- > [ 3 *^ unitX, 3 *^ unitY, 2 *^ unit_X, 1 *^ unit_Y
-- > , 1 *^ unitX, 1 *^ unit_Y, 2 *^ unit_X, 1 *^ unit_Y ]
-- >
-- > thingy = strokeTrail thingyT
-- >
-- > data TraceDiaOpts
-- > = TDO { traceShape :: Diagram B R2
-- > , basePt :: P2
-- > , dirV :: R2
-- > , sFilter :: [Double] -> [Double]
-- > , drawV :: Bool
-- > }
-- >
-- > instance Default TraceDiaOpts where
-- > def = TDO { traceShape = thingy
-- > , basePt = pointB
-- > , dirV = 0.3 ^& 0.5
-- > , sFilter = id
-- > , drawV = False
-- > }
-- >
-- > pointA = 1 ^& (-1.5)
-- > pointB = 1 ^& 1.2
-- > pointC = 2.5 ^& 3.5
-- >
-- > dot = circle 0.05 # lw none
-- >
-- > mkTraceDia :: TraceDiaOpts -> Diagram B R2
-- > mkTraceDia tdo = mconcat
-- > [ mconcat $ map (place (dot # fc red)) pts
-- > , if drawV tdo then resultArrow else mempty
-- > , arrowAt (basePt tdo) (dirV tdo) # lc blue
-- > , dot # fc blue # moveTo (basePt tdo)
-- > , traceLine (basePt tdo) maxPosPt
-- > , traceLine (basePt tdo) minNegPt
-- > , traceShape tdo
-- > ]
-- > # centerXY # pad 1.1
-- > where
-- > ss = sFilter tdo . getSortedList
-- > $ appTrace (traceShape tdo ^. trace) (basePt tdo) (dirV tdo)
-- > pts = map mkPt ss
-- > mkPt s = basePt tdo .+^ (s *^ dirV tdo)
-- > maxPosPt = (mkPt <$>) . safeLast $ filter (>0) ss
-- > minNegPt = (mkPt <$>) . safeHead $ filter (<0) ss
-- > minPt = (mkPt <$>) . safeHead $ ss
-- > resultArrow = fromMaybe mempty (arrowBetween (basePt tdo) <$> minPt)
-- > # lc green
-- >
-- > safeLast [] = Nothing
-- > safeLast xs = Just $ last xs
-- > safeHead [] = Nothing
-- > safeHead (x:_) = Just x
-- > dropAllBut1 [] = []
-- > dropAllBut1 xs = [last xs]
-- >
-- > traceLine _ Nothing = mempty
-- > traceLine p (Just q) = (p ~~ q) # dashingG [0.1,0.1] 0
-- >
-- > mkTraceDias :: [TraceDiaOpts] -> Diagram B R2
-- > mkTraceDias = hcat' (with & sep .~ 1) . map mkTraceDia
-- >
-- > mkTraceDiasABC :: TraceDiaOpts -> Diagram B R2
-- > mkTraceDiasABC tdo = mkTraceDias (map (\p -> tdo { basePt = p }) [pointA, pointB, pointC])