marxup-3.0.0: MarXup/Diagram/Object.hs
{-# LANGUAGE DataKinds, KindSignatures, OverloadedStrings, EmptyDataDecls, MultiParamTypeClasses, FlexibleContexts, OverlappingInstances, TypeSynonymInstances, FlexibleInstances #-}
module MarXup.Diagram.Object where
-- import MarXup
import MarXup.Tex
import MarXup.Diagram.Tikz
import MarXup.Diagram.Path
import MarXup.Diagram.Point
import MarXup.Diagram.Layout
import MarXup.MultiRef (BoxSpec(..))
import Control.Monad
import Control.Applicative
-- import Data.Algebra
-- import Data.List (intersperse)
import Control.Lens (set)
data Anchor = Center | N | NW | W | SW | S | SE | E | NE | BaseW | Base | BaseE
deriving Show
-- | Box-shaped object. (a subtype)
type Box = Object
newtype Anchorage = Anchorage {boxAnchors :: Anchor -> Point}
data Object = Object {objectOutline :: Path, objectAnchorage :: Anchorage}
class Anchored a where
anchors :: a -> Anchor -> Point
infix 8 #
(#) :: Anchored a => a -> Anchor -> Point
(#) = anchors
instance Anchored Anchorage where
anchors = boxAnchors
instance Anchored Object where
anchors = anchors . objectAnchorage
instance Anchored Point where
anchors p _ = p
-- | Horizontal distance between objects
hdist :: Anchored a => a -> a -> Expr
hdist x y = xpart (y # W - x # E)
-- | Vertical distance between objects
vdist :: Anchored a => a -> a -> Expr
vdist x y = ypart (y # S - x # N)
-- | Extend the box boundaries by the given delta
extend :: Expr -> Anchorage -> Anchorage
extend e o = Anchorage $ \a -> o # a + shiftInDir a e
-- | Makes a shift of size 'd' in the given direction.
shiftInDir :: Anchor -> Expr -> Point
shiftInDir N d = 0 `Point` d
shiftInDir S d = 0 `Point` negate d
shiftInDir W d = negate d `Point` 0
shiftInDir E d = d `Point` 0
shiftInDir NW d = negate d `Point` d
shiftInDir SE d = d `Point` negate d
shiftInDir SW d = negate d `Point` negate d
shiftInDir NE d = d `Point` d
shiftInDir _ _ = 0 `Point` 0
-- | Make a label object. This is just some text surrounded by 4
-- points of blank.
mkLabel :: TeX -> Diagram Anchorage
mkLabel texCode = extend 4 <$> texBox texCode
labelObj :: TeX -> Diagram Box
labelObj = rectangleShape <=< mkLabel
-- | Label a point by a given TeX expression, at the given anchor.
labelPt :: TeX -> Anchor -> Point -> Diagram Box
labelPt labell anchor labeled = do
t <- labelObj labell
t # anchor .=. labeled
return t
-- | A free point
point :: Diagram Point
point = do
[x,y] <- newVars (replicate 2 ContVar)
return $ Point x y
-- | A point anchorage (similar to a box of zero width and height)
pointBox :: Diagram Anchorage
pointBox = do
p <- point
return $ Anchorage $ \a -> case a of _ -> p
-- | A box. Anchors are aligned along a grid.
box :: Diagram Anchorage
box = do
[n,s,e,w,base,midx,midy] <- newVars (replicate 7 ContVar)
n >== base
base >== s
w <== e
midx === avg [w,e]
midy === avg [n,s]
let pt = flip Point
return $ Anchorage $ \anch -> case anch of
NW -> pt n w
N -> pt n midx
NE -> pt n e
E -> pt midy e
SE -> pt s e
S -> pt s midx
SW -> pt s w
W -> pt midy w
Center -> pt midy midx
Base -> pt base midx
BaseE -> pt base e
BaseW -> pt base w
-- | A box of zero width
vrule :: Diagram Anchorage
vrule = do
o <- box
align xpart [o # W, o #Center, o#E]
return o
-- | A box of zero height
hrule :: Diagram Anchorage
hrule = do
o <- box
height o === 0
return o
height o = ypart (o # N - o # S)
width o = xpart (o # E - o # W)
ascent o = ypart (o # N - o # Base)
descent o = ypart (o # Base - o # S)
-- fitsVerticallyIn :: Anchorage -> Anchorage -> Diagram ()
o `fitsVerticallyIn` o' = do
let dyN = ypart $ o' # N - o # N
dyS = ypart $ o # S - o' # S
minimize dyN
dyN >== 0
minimize dyS
dyS >== 0
-- fitsHorizontallyIn :: Anchorage -> Anchorage -> Diagram ()
o `fitsHorizontallyIn` o' = do
let dyW = xpart $ o # W - o' # W
dyE = xpart $ o' # E - o # E
minimize dyW
dyW >== 0
minimize dyE
dyE >== 0
a `fitsIn` b = do
a `fitsHorizontallyIn` b
a `fitsVerticallyIn` b
circleShape :: Diagram Object
circleShape = do
anch <- box
width anch === height anch
let radius = 0.5 *- width anch
let p = circle (anch # Center) radius
path p
return $ Object p anch
-- let k1 :: Constant
-- k1 = sqrt 2 / 2
-- k = k1 *^ r
-- p = circle center r
-- return $ Object p $ Anchorage $ \a -> center + case a of
-- N -> Point 0 r
-- S -> Point 0 (-r)
-- E -> Point r 0
-- W -> Point (-r) 0
-- Center -> Point 0 0
-- NE -> Point k k
rectangleShape :: Anchorage -> Diagram Object
rectangleShape l = do
let p = polygon (map (l #) [NW,NE,SE,SW])
path p
return $ Object p l
traceAnchorage :: Anchored a => Color -> a -> Diagram ()
traceAnchorage c l = do
stroke c $ path $ polygon (map (l #) [NW,NE,SE,SW])
-- TODO: draw the baseline, etc.
-- | Typeset a piece of text and return its bounding box.
texBox :: TeX -> Diagram Anchorage
texBox t = do
l <- box
-- traceAnchorage "red" l
BoxSpec wid h desc <- drawText (l # NW) t
width l === constant wid
descent l === constant desc
height l === constant (h + desc)
return l
data Incidence = Incidence { incidencePoint, incidenceNormal :: Point }
swap :: Incidence -> Incidence
swap (Incidence p v) = Incidence p (negate v)
-- | Traces a straight edge between two objects.
-- The midpoint is returned, as well as a normal vector.
edge :: Object -> Object -> Diagram Incidence
edge source target = do
let points@[a,b] = [source # Center,target # Center]
link = polyline points
targetArea = objectOutline target
sourceArea = objectOutline source
l' <- freeze link
sa' <- freeze sourceArea
ta' <- freeze targetArea
frozenPath $ (l' `cutAfter` ta') `cutBefore` sa'
return $ Incidence (avg points) (rotate90 (b-a))
(.<.) :: Point -> Point -> Diagram ()
Point x1 y1 .<. Point x2 y2 = do
x1 <== x2
y1 <== y2
-- | Forces the point to be inside the (bounding box) of the object.
inside :: Point -> Box -> Diagram ()
inside p o = do
(o # SW) .<. p
p .<. (o # NE)
-- | @autoLabel label i@ Layouts the label at the given incidence
-- point.
autoLabel :: Box -> Incidence -> Diagram ()
autoLabel lab (Incidence pt norm) = do
pt `inside` lab
minimize =<< orthoDist (lab#Center) (pt + norm)
-- | @labeledEdge label source target@
labeledEdge :: Object -> Object -> Box -> Diagram ()
labeledEdge source target lab = autoLabel lab =<< edge source target
-------------------
-- Even higher-level primitives:
--example: spread hdist 30 ps
spread f d (x:y:xs) = do
f x y === d
spread f d (y:xs)
spread f _ _ = return ()
node lab = do
l <- extend 4 <$> texBox lab
c <- draw $ circleShape
l `fitsIn` c
l # Center .=. c # Center
return c
arrow :: Object -> Object -> Diagram Incidence
arrow src trg = using (outline "black" . set endTip LatexTip) $ do
edge src trg