sylvia-0.2.2: Sylvia/Render/Core.hs
{-# LANGUAGE TemplateHaskell #-}
-- |
-- Module : Sylvia.Render.Core
-- Copyright : GPLv3
--
-- Maintainer : chrisyco@gmail.com
-- Portability : portable
--
-- This module provides the algorithm used to render expressions. It can
-- be called in two ways:
--
-- 1. A function, 'render', that uses "Sylvia.Render.Backend" to draw a
-- pretty picture;
--
-- 2. Another function, 'render'', that spews its internals all over the
-- place.
module Sylvia.Render.Core
(
-- * A function
render
-- * Another function
, render'
-- ** The result type
, Result()
, resultImage
, resultSize
, resultRhyme
, resultThroatY
-- ** Internal bits and pieces
, Rhyme
, RhymeUnit()
, ruIndex
, ruDest
) where
import Control.Applicative
import Data.Foldable (foldMap)
import Data.Lens.Common
import Data.Lens.Template
import Data.List (foldl')
import Data.Monoid
import Data.Void (Void, vacuous)
import Sylvia.Render.Backend
import Sylvia.Render.Pair
import Sylvia.Model
type Rhyme = [RhymeUnit]
-- | Specifies a /rhyme line/: a straight line protruding from the left
-- edge of a bounding box, connecting a variable to the sub-expression
-- that uses it.
data RhymeUnit = RhymeUnit
{ _ruIndex :: Integer
, _ruDest :: Int
}
deriving (Show)
$(makeLens ''RhymeUnit)
-- | The result of a rendering operation.
data Result r = Result
{ _resultImage :: r
-- ^ The rendered image.
, _resultSize :: PInt
-- ^ The size of the image's bounding box in grid units, when all
-- round things are removed.
, _resultRhyme :: Rhyme
-- ^ The expression's rhyme.
, _resultThroatY :: Int
-- ^ The Y offset of the expression's ear and throat, measured
-- from the /bottom/ of its bounding box.
}
deriving (Show)
$(makeLens ''Result)
-- | Render an expression, returning an image along with its size.
render :: Backend r => Exp Void -> (r, PInt)
render e =
let Result image size rhyme _ = render' $ vacuous e
in case rhyme of
[] -> (image, size)
_ -> error $ "render: the impossible happened -- "
++ "extra free variables: " ++ show rhyme
-- | Render an expression, with extra juicy options.
render' :: Backend r => Exp Integer -> Result r
render' e = case e of
Ref x -> Result mempty (0 :| 0) [RhymeUnit x 0] 0
Lam e' -> renderLambda e'
App a b -> case a of
Lam _ -> renderBeside a b
_ -> renderAtop a b
renderBeside :: Backend r => Exp Integer -> Exp Integer -> Result r
renderBeside a b = Result image size rhyme aThroatY
where
image = mconcat $
[ aImage
, bImage
]
(Result aImage (aWidth :| aHeight) aRhyme aThroatY,
Result bImage (bWidth :| bHeight) bRhyme bThroatY)
= alignThroats
(render' a)
(shiftX (-aWidth) $ renderWithThroatLine False 1 b)
size = (aWidth + bWidth :| max aHeight bHeight)
rhyme = aRhyme ++ bRhyme
alignThroats :: Backend r => Result r -> Result r -> (Result r, Result r)
alignThroats
a@(Result _ _ _ aThroatY)
b@(Result _ _ _ bThroatY)
= (shiftY' (minThroatY - aThroatY) a,
shiftY' (minThroatY - bThroatY) b)
where
minThroatY = min aThroatY bThroatY
renderAtop :: Backend r => Exp Integer -> Exp Integer -> Result r
renderAtop a b = Result image size rhyme bThroatY
where
image = mconcat $
-- Draw the two sub-expressions
[ aImage
, bImage
-- Extend the lower sub-expression so it matches up with the
-- bigger one
, relativeTo (-bWidth :| 0)
$ extendAcross (bWidth - aWidth) bRhyme
-- Connect them with a vertical line
, drawLine (0 :| aThroatY) (0 :| bThroatY)
-- Application dot
, drawDot (0 :| bThroatY)
]
Result aImage (aWidth :| aHeight) aRhyme aThroatY
= shiftY (-1 - bHeight) $ renderWithThroatLine False bWidth a
Result bImage (bWidth :| bHeight) bRhyme bThroatY
= renderWithThroatLine False 1 b
size = (aWidth :| aHeight + bHeight + 1)
rhyme = aRhyme ++ bRhyme
-- | Render an expression with a horizontal line sticking out of its
-- throat. Doesn't sound too comfortable, to be honest.
--
-- The 'resultSize' includes the length of this extra line.
renderWithThroatLine
:: Backend r
=> Bool -- ^ Whether the enclosing expression is a lambda.
-> Int -- ^ Length of the throat line. This should be positive.
-> Exp Integer -> Result r
renderWithThroatLine outerIsLam lineLength e = Result image size rhyme throatY
where
Result innerImage innerSize rhyme innerThroatY = render' e
-- Shift the main image to the left, then draw a line next to it
image = relativeTo (-lineLength :| 0) innerImage <> throatLine
throatLine = drawZigzag (-lineLength :| innerThroatY) (0 :| throatY)
throatY = if outerIsLam && containsLam e then innerThroatY - 1 else innerThroatY
size = innerSize |+| (lineLength :| 0)
-- | Return whether there is a nested box touching the bottom edge of the
-- bounding box. If True, it means that everything in the image should
-- be shifted down one unit, making the ear and throat lines zigzag.
containsLam :: Exp a -> Bool
containsLam e = case e of
Ref _ -> False
Lam _ -> True
App _ b -> containsLam b
-- | Render a lambda expression.
renderLambda :: Backend r => Exp (Inc Integer) -> Result r
renderLambda e' = Result image size rhyme throatY
where
Result innerImage (innerWidth :| innerHeight) innerRhyme throatY
= shiftY (-1) . renderWithThroatLine True 1 $ fmap shiftDown e'
-- Draw a box around it
image = drawBox (negateP size) size throatY
<> relativeTo (-width :| 0) rhymeImage
<> innerImage
(rhymeImage, rhyme) = renderRhyme throatY innerRhyme
rhymeHeight = fromInteger . maximumOr 0 $ map (getL ruIndex) innerRhyme
size@(width :| _) = (innerWidth + 1 :| (max innerHeight rhymeHeight) + 2)
-- | Like 'maximum', but returns a default value on an empty list rather
-- than throwing a hissy fit.
maximumOr :: Ord a => a -> [a] -> a
maximumOr def = foldl' max def
-- | Render an expression's rhyme.
renderRhyme
:: Backend r
=> Int -- ^ Throat offset (see 'resultThroatY')
-> Rhyme -- ^ The inner expression's rhyme
-> (r, Rhyme) -- ^ The resulting image, along with the outer rhyme
renderRhyme throatY innerRhyme = (foldMap renderOne innerRhyme, outerRhyme)
where
renderOne (RhymeUnit index dest) = drawLine (0 :| throatY - fromInteger index) (1 :| dest)
outerRhyme =
[ RhymeUnit (pred index) (throatY - fromInteger index)
| RhymeUnit index _ <- innerRhyme
, index > 0
]
-- | Shift an image horizontally by a specified amount.
shiftX :: Backend r => Int -> Result r -> Result r
shiftX dx = modL resultImage (relativeTo (dx :| 0))
-- | Shift an image vertically by a specified amount, changing the rhyme
-- and throat position to compensate.
shiftY :: Backend r => Int -> Result r -> Result r
shiftY dy
= modL resultImage (relativeTo (0 :| dy))
. modL resultRhyme (map (modL ruDest (+ dy)))
. modL resultThroatY (+ dy)
-- | Like 'shiftY', but expand the image's bounding box rather than shifting it.
shiftY' :: Backend r => Int -> Result r -> Result r
shiftY' dy
= modL resultSize (modL sndP (subtract dy)) -- dy is usually negative, so this /increases/ the size
. shiftY dy
extendAcross :: Backend r => Int -> Rhyme -> r
extendAcross dx = foldMap $ drawLine
<$> ( 0 :|) . getL ruDest
<*> (dx :|) . getL ruDest