Copyright | (c) 2015 Brent Yorgey |
---|---|

License | BSD-style (see LICENSE) |

Maintainer | byorgey@gmail.com |

Safe Haskell | None |

Language | Haskell2010 |

Lay out diagrams by specifying constraints. Currently, the API is fairly simple: only equational constraints are supported (not inequalities), and you can only use it to compose a collection of diagrams (and not to, say, compute the position of some point). Future versions may support additional features.

As a basic example, we can introduce a circle and a square, and constrain them to be next to each other:

import Diagrams.TwoD.Layout.Constrained constrCircleSq = frame 0.2 $ layout $ do c <- newDia (circle 1) s <- newDia (square 2) constrainWith hcat [c, s]

We start a block of constraints with `layout`

; introduce new
diagrams with `newDia`

, and then constrain them, in this case using
the `constrainWith`

function. The result looks like this:

Of course this is no different than just writing ```
circle 1 |||
square 2
```

. The interest comes when we start constraining things in
more interesting ways.

For example, the following code creates a row of differently-sized circles with a bit of space in between them, and then draws a square which is tangent to the last circle and passes through the center of the third. Manually computing the size (and position) of this square would be tedious. Instead, the square is declared to be scalable, meaning it may be uniformly scaled to accomodate constraints. Then a point on the left side of the square is constrained to be equal to the center of the third circle, and a point on the right side of the square is made equal to a point on the edge of the rightmost circle. This causes the square to be automatically positioned and scaled appropriately.

import Diagrams.TwoD.Layout.Constrained circleRow = frame 1 $ layout $ do cirs <- newDias (map circle [1..5]) constrainWith (hsep 1) cirs rc <- newPointOn (last cirs) (envelopeP unitX) sq <- newScalableDia (square 1) ls <- newPointOn sq (envelopeP unit_X) rs <- newPointOn sq (envelopeP unitX) ls =.= centerOf (cirs !! 2) rs =.= rc

As a final example, the following code draws a vertical stack of
circles, along with an accompanying set of squares, such that (1)
each square constrained to lie on the same horizontal line as a
circle (using `zipWithM_ `

), and (2) the squares all lie on
a diagonal line (using `sameY`

`along`

).

import Diagrams.TwoD.Layout.Constrained import Control.Monad (zipWithM_) diagonalLayout = frame 1 $ layout $ do cirs <- newDias (map circle [1..5] # fc blue) sqs <- newDias (replicate 5 (square 2) # fc orange) constrainWith vcat cirs zipWithM_ sameY cirs sqs constrainWith hcat [cirs !! 0, sqs !! 0] along (direction (1 ^& (-1))) (map centerOf sqs)

Take a look at the implementations of combinators such as `sameX`

,
`allSame`

, `constrainDir`

, and `along`

for ideas on implementing
your own constraint combinators.

Ideas for future versions of this module:

- Introduce z-index constraints. Right now the diagrams are just drawn in the order that they are introduced.
- A way to specify default values ---
*i.e.*be able to introduce new point or scalar variables with a specified default value (instead of just defaulting to the origin or to 1). - Doing something more reasonable than crashing for overconstrained systems.

I am also open to other suggestions and/or pull requests!

## Synopsis

- type Expr s n = Expr (Var s) n
- mkExpr :: n -> Expr s n
- type Constrained s b n m a = State (ConstrainedState s b n m) a
- data ConstrainedState s b n m
- data DiaID s
- layout :: (Monoid' m, Hashable n, Floating n, RealFrac n, Show n) => (forall s. Constrained s b n m a) -> QDiagram b V2 n m
- runLayout :: (Monoid' m, Hashable n, Floating n, RealFrac n, Show n) => (forall s. Constrained s b n m a) -> (a, QDiagram b V2 n m)
- newDia :: (Hashable n, Floating n, RealFrac n) => QDiagram b V2 n m -> Constrained s b n m (DiaID s)
- newDias :: (Hashable n, Floating n, RealFrac n) => [QDiagram b V2 n m] -> Constrained s b n m [DiaID s]
- newScalableDia :: QDiagram b V2 n m -> Constrained s b n m (DiaID s)
- newPoint :: Num n => Constrained s b n m (P2 (Expr s n))
- newPointOn :: (Hashable n, Floating n, RealFrac n) => DiaID s -> (QDiagram b V2 n m -> P2 n) -> Constrained s b n m (P2 (Expr s n))
- newScalar :: Num n => Constrained s b n m (Expr s n)
- centerOf :: Num n => DiaID s -> P2 (Expr s n)
- xOf :: Num n => DiaID s -> Expr s n
- yOf :: Num n => DiaID s -> Expr s n
- scaleOf :: Num n => DiaID s -> Expr s n
- (====) :: (Floating n, RealFrac n, Hashable n) => Expr s n -> Expr s n -> Constrained s b n m ()
- (=.=) :: (Hashable n, Floating n, RealFrac n) => P2 (Expr s n) -> P2 (Expr s n) -> Constrained s b n m ()
- (=^=) :: (Hashable n, Floating n, RealFrac n) => V2 (Expr s n) -> V2 (Expr s n) -> Constrained s b n m ()
- sameX :: (Hashable n, Floating n, RealFrac n) => DiaID s -> DiaID s -> Constrained s b n m ()
- sameY :: (Hashable n, Floating n, RealFrac n) => DiaID s -> DiaID s -> Constrained s b n m ()
- allSame :: (Hashable n, Floating n, RealFrac n) => [Expr s n] -> Constrained s b n m ()
- constrainWith :: (Hashable n, RealFrac n, Floating n, Monoid' m) => ([[Located (Envelope V2 n)]] -> [Located (Envelope V2 n)]) -> [DiaID s] -> Constrained s b n m ()
- constrainDir :: (Hashable n, Floating n, RealFrac n) => Direction V2 (Expr s n) -> P2 (Expr s n) -> P2 (Expr s n) -> Constrained s b n m ()
- along :: (Hashable n, Floating n, RealFrac n) => Direction V2 (Expr s n) -> [P2 (Expr s n)] -> Constrained s b n m ()

# Basic types

type Expr s n = Expr (Var s) n Source #

The type of reified expressions over `Vars`

, with
numeric values taken from the type `n`

. The important point to
note is that `Expr`

is an instance of `Num`

, `Fractional`

, and
`Floating`

, so `Expr`

values can be combined and manipulated as
if they were numeric expressions, even when they occur inside
other types. For example, 2D vector values of type ```
V2 (Expr s
n)
```

and point values of type `P2 (Expr s n)`

can be combined
using operators such as `.+^`

, `.-.`

, and so on, in order to
express constraints on vectors and points.

To create literal `Expr`

values, you can use `mkExpr`

.
Otherwise, they are introduced by creation functions such as
`newPoint`

, `newScalar`

, or diagram accessor functions like
`centerOf`

or `xOf`

.

mkExpr :: n -> Expr s n Source #

Convert a literal numeric value into an `Expr`

. To convert
structured types such as vectors or points, you can use e.g. ```
fmap
mkExpr :: V2 n -> V2 (Expr s n)
```

.

type Constrained s b n m a = State (ConstrainedState s b n m) a Source #

A monad for constrained systems. It suffices to think of it as
an abstract monadic type; the constructor for the internal state
is intentionally not exported. `Constrained`

values can be
created using the combinators below; combined using the `Monad`

interface; and discharged by the `layout`

function.

Note that `s`

is a phantom parameter, used in a similar fashion
to the `ST`

monad, to ensure that generated diagram IDs cannot be
mixed between different `layout`

blocks.

data ConstrainedState s b n m Source #

The state maintained by the Constrained monad. Note that `s`

is a phantom parameter, used in a similar fashion to the `ST`

monad, to ensure that generated diagram IDs do not leak.

An abstract type representing unique IDs for diagrams. The
constructor is not exported, so the only way to obtain a `DiaID`

is by calling `newDia`

or `newDias`

. The phantom type parameter
`s`

ensures that such `DiaID`

s can only be used with the
constrained system in which they were introduced.

# Layout

layout :: (Monoid' m, Hashable n, Floating n, RealFrac n, Show n) => (forall s. Constrained s b n m a) -> QDiagram b V2 n m Source #

Solve a constrained system, combining the resulting diagrams with
`mconcat`

. This is the top-level function for introducing a
constrained system, and is the only way to generate an actual
diagram.

Redundant constraints are ignored. If there are any unconstrained diagram variables remaining, they are given default values one at a time, beginning with defaulting remaining scaling factors to 1, then defaulting x- and y-coordinates to zero.

An overconstrained system will cause `layout`

to simply crash.
This is obviously not ideal. A future version may do something
more reasonable.

runLayout :: (Monoid' m, Hashable n, Floating n, RealFrac n, Show n) => (forall s. Constrained s b n m a) -> (a, QDiagram b V2 n m) Source #

Like `layout`

, but also allows the caller to retrieve the result of the
`Constrained`

computation.

# Creating constrainable things

Diagrams, points, *etc.* which will participate in a
system of constraints must first be explicitly
introduced using one of the functions in this section.

newDia :: (Hashable n, Floating n, RealFrac n) => QDiagram b V2 n m -> Constrained s b n m (DiaID s) Source #

Introduce a new diagram into the constrained system. Returns a unique ID for use in referring to the diagram later.

The position of the diagram's origin may be constrained. If
unconstrained, the origin will default to (0,0). For a diagram
whose scaling factor may also be constrained, see
`newScalableDia`

.

newDias :: (Hashable n, Floating n, RealFrac n) => [QDiagram b V2 n m] -> Constrained s b n m [DiaID s] Source #

Introduce a list of diagrams into the constrained system. Returns a corresponding list of unique IDs for use in referring to the diagrams later.

newScalableDia :: QDiagram b V2 n m -> Constrained s b n m (DiaID s) Source #

Introduce a new diagram into the constrained system. Returns a unique ID for use in referring to the diagram later.

Both the position of the diagram's origin and its scaling factor may be constrained. If unconstrained, the origin will default to (0,0), and the scaling factor to 1, respectively.

newPoint :: Num n => Constrained s b n m (P2 (Expr s n)) Source #

Introduce a new constrainable point, unattached to any particular diagram. If either of the coordinates are still unconstrained at the end, they will default to zero.

newPointOn :: (Hashable n, Floating n, RealFrac n) => DiaID s -> (QDiagram b V2 n m -> P2 n) -> Constrained s b n m (P2 (Expr s n)) Source #

Create a new (constrainable) point attached to the given diagram, using a function that picks a point given a diagram.

For example, to get the point on the right edge of a diagram's envelope, one may write

rt <- newPointOn d (envelopeP unitX)

To get the point (1,1),

one_one <- newPointOn d (const (1 ^& 1))

This latter example is far from useless: note that `one_one`

now
corresponds not to the absolute coordinates (1,1), but to the
point which lies at (1,1) /relative to the unscaled diagram's
origin/. If the diagram is positioned or scaled to satisfy some
other constraints, `one_one`

will move right along with it.

For example, the following code establishes a small circle which is located at a specific point relative to a big circle. The small circle is carried along with the big circle as it is laid out in between some squares.

import Diagrams.TwoD.Layout.Constrained circleWithCircle = frame 0.3 $ layout $ do c2 <- newScalableDia (circle 2) p <- newPointOn c2 (const $ (1 ^& 0) # rotateBy (1/8)) c1 <- newDia (circle 1) centerOf c1 =.= p [a,b] <- newDias (replicate 2 (square 2)) constrainWith hcat [a,c2,b]

newScalar :: Num n => Constrained s b n m (Expr s n) Source #

Introduce a new scalar value which can be constrained. If still unconstrained at the end, it will default to 1.

# Diagram accessors

Combinators for extracting constrainable attributes of an introduced diagram.

centerOf :: Num n => DiaID s -> P2 (Expr s n) Source #

The point at the center (i.e. local origin) of the given
diagram. For example, to constrain the origin of diagram `b`

to
be offset from the origin of diagram `a`

by one unit to the right
and one unit up, one may write

centerOf b =.= centerOf a .+^ (1 ^& 1)

xOf :: Num n => DiaID s -> Expr s n Source #

The x-coordinate of the center for the given diagram, which can be used in constraints to determine the x-position of this diagram relative to others.

For example,

xOf d1 + 2 === xOf d2

constrains diagram `d2`

to lie 2 units to the right of `d1`

in
the horizontal direction, though it does not constrain their
relative positioning in the vertical direction.

yOf :: Num n => DiaID s -> Expr s n Source #

The y-coordinate of the center for the given diagram, which can be used in constraints to determine the y-position of this diagram relative to others.

For example,

allSame (map yOf ds)

constrains the diagrams `ds`

to all lie on the same horizontal
line.

scaleOf :: Num n => DiaID s -> Expr s n Source #

The scaling factor applied to this diagram.

For example,

scaleOf d1 === 2 * scaleOf d2

constrains `d1`

to be scaled twice as much as `d2`

. (It does not,
however, guarantee anything about their actual relative sizes;
that depends on their relative size when unscaled.)

# Constraints

(====) :: (Floating n, RealFrac n, Hashable n) => Expr s n -> Expr s n -> Constrained s b n m () infix 1 Source #

Constrain two scalar expressions to be equal. Note that you need not worry about introducing redundant constraints; they are ignored.

(=.=) :: (Hashable n, Floating n, RealFrac n) => P2 (Expr s n) -> P2 (Expr s n) -> Constrained s b n m () infix 1 Source #

Constrain two points to be equal.

(=^=) :: (Hashable n, Floating n, RealFrac n) => V2 (Expr s n) -> V2 (Expr s n) -> Constrained s b n m () infix 1 Source #

Constrain two vectors to be equal.

sameX :: (Hashable n, Floating n, RealFrac n) => DiaID s -> DiaID s -> Constrained s b n m () Source #

Constrain the origins of two diagrams to have the same x-coordinate.

sameY :: (Hashable n, Floating n, RealFrac n) => DiaID s -> DiaID s -> Constrained s b n m () Source #

Constrain the origins of two diagrams to have the same y-coordinate.

allSame :: (Hashable n, Floating n, RealFrac n) => [Expr s n] -> Constrained s b n m () Source #

Constrain a list of scalar expressions to be all equal.

constrainWith :: (Hashable n, RealFrac n, Floating n, Monoid' m) => ([[Located (Envelope V2 n)]] -> [Located (Envelope V2 n)]) -> [DiaID s] -> Constrained s b n m () Source #

Constrain a collection of diagrams to be positioned relative to
one another according to a function such as `hcat`

, `vcat`

, `hsep`

,
and so on.

A typical use would be

cirs <- newDias (map circle [1..5]) constrainWith (hsep 1) cirs

which creates five circles and constrains them to be positioned in a row, with one unit of space in between adjacent pairs.

The funny type signature is something of a hack. The sorts of
functions which should be passed as the first argument to
`constrainWith`

tend to be highly polymorphic; `constrainWith`

uses a concrete type which it can use to extract relevant
information about the function by observing its behavior. In
short, you do not need to know anything about `Located Envelope`

s
in order to call this function.