diagrams-contrib-1.4.0.1: Collection of user contributions to diagrams EDSL

Copyright(c) 2015 Brent Yorgey
LicenseBSD-style (see LICENSE)
Maintainerbyorgey@gmail.com
Safe HaskellNone
LanguageHaskell2010

Diagrams.TwoD.Layout.Constrained

Contents

Description

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_ sameY), and (2) the squares all lie on a diagonal line (using 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

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.

data DiaID s Source #

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 DiaIDs can only be used with the constrained system in which they were introduced.

Instances

Eq (DiaID s) Source # 

Methods

(==) :: DiaID s -> DiaID s -> Bool #

(/=) :: DiaID s -> DiaID s -> Bool #

Ord (DiaID s) Source # 

Methods

compare :: DiaID s -> DiaID s -> Ordering #

(<) :: DiaID s -> DiaID s -> Bool #

(<=) :: DiaID s -> DiaID s -> Bool #

(>) :: DiaID s -> DiaID s -> Bool #

(>=) :: DiaID s -> DiaID s -> Bool #

max :: DiaID s -> DiaID s -> DiaID s #

min :: DiaID s -> DiaID s -> DiaID s #

Show (DiaID s) Source # 

Methods

showsPrec :: Int -> DiaID s -> ShowS #

show :: DiaID s -> String #

showList :: [DiaID s] -> ShowS #

Generic (DiaID s) Source # 

Associated Types

type Rep (DiaID s) :: * -> * #

Methods

from :: DiaID s -> Rep (DiaID s) x #

to :: Rep (DiaID s) x -> DiaID s #

Hashable (DiaID s) Source # 

Methods

hashWithSalt :: Int -> DiaID s -> Int #

hash :: DiaID s -> Int #

type Rep (DiaID s) Source # 
type Rep (DiaID s) = D1 (MetaData "DiaID" "Diagrams.TwoD.Layout.Constrained" "diagrams-contrib-1.4.0.1" True) (C1 (MetaCons "DiaID" PrefixI False) (S1 (MetaSel (Nothing Symbol) NoSourceUnpackedness NoSourceStrictness DecidedLazy) (Rec0 Int)))

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.

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 Envelopes in order to call this function.

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 () Source #

constrainDir d p q constrains the direction from p to q to be d. That is, the direction of the vector q .-. p must be d.

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

along d ps constrains the points ps to all lie along a ray parallel to the direction d.