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!
- 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
- 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.
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 Envelope
s
in order to call this function.