diagrams-contrib-1.4: src/Diagrams/TwoD/Layout/Constrained.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE ViewPatterns #-}
-----------------------------------------------------------------------------
-- |
-- Module : Diagrams.TwoD.Layout.Constrained
-- Copyright : (c) 2015 Brent Yorgey
-- License : BSD-style (see LICENSE)
-- Maintainer : byorgey@gmail.com
--
-- 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:
--
-- <<diagrams/src_Diagrams_TwoD_Layout_Constrained_constrCircleSq.svg#diagram=constrCircleSq&width=300>>
--
-- 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
--
-- <<diagrams/src_Diagrams_TwoD_Layout_Constrained_circleRow.svg#diagram=circleRow&width=300>>
--
-- 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)
--
-- <<diagrams/src_Diagrams_TwoD_Layout_Constrained_diagonalLayout.svg#diagram=diagonalLayout&width=400>>
--
-- 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!
-----------------------------------------------------------------------------
module Diagrams.TwoD.Layout.Constrained
( -- * Basic types
Expr, mkExpr, Constrained, ConstrainedState, DiaID
-- * Layout
, layout
-- * 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, newDias, newScalableDia
, newPoint, newPointOn
, newScalar
-- * Diagram accessors
-- | Combinators for extracting constrainable attributes of
-- an introduced diagram.
, centerOf, xOf, yOf, scaleOf
-- * Constraints
, (====), (=.=), (=^=)
, sameX, sameY
, allSame
, constrainWith
, constrainDir
, along
)
where
import qualified Control.Lens as L
import qualified Control.Lens.Extras as L
import Control.Monad.Except
import Control.Monad.State
import qualified Data.Foldable as F
import Data.Hashable
import Data.List (sortBy)
import qualified Data.Map as M
import Data.Maybe (fromJust)
import Data.Ord (comparing)
import GHC.Generics
import qualified Math.MFSolve as MFS
import Diagrams.Coordinates
import Diagrams.Prelude
------------------------------------------------------------
-- Variables and expressions
------------------------------------------------------------
-- | 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.
newtype DiaID s = DiaID Int
deriving (Ord, Eq, Show, Generic)
-- | Variables can track one of four things: an x-coordinate, a
-- y-coordinate, a scaling factor, or a length.
data VarType = S -- ^ scaling factor
| L -- ^ length
| X -- ^ X-coordinate of a point
| Y -- ^ Y-coordinate of a point
deriving (Eq, Ord, Read, Show, Generic)
-- | A variable has a name and a type, and may optionally be
-- associated to some particular diagram.
data Var s = Var { _varID :: Maybe (DiaID s), _varName :: String, _varType :: VarType }
deriving (Eq, Ord, Generic, Show)
makeLensesWith (lensRulesFor [("_varType", "varType")]) ''Var
-- Auto-derive Hashable instances using Generic
instance Hashable (DiaID s)
instance Hashable VarType
instance Hashable (Var s)
-- | 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'.
type Expr s n = MFS.Expr (Var s) n
-- | 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)@.
mkExpr :: n -> Expr s n
mkExpr = MFS.makeConstant
------------------------------------------------------------
-- Functions for variable and expression creation
------------------------------------------------------------
-- | Create an internal variable corresponding to a diagram, with
-- the given name and variable type. Not intended to be called by
-- end users.
diaVar :: DiaID s -> String -> VarType -> Var s
diaVar = Var . Just
-- | Create an internal variable unattached to any particular diagram, with
-- a given name and variable type. Not intended to be called by end
-- users.
newVar :: String -> VarType -> Var s
newVar = Var Nothing
-- | Create a variable corresponding to a particular diagram, with a
-- given name and type. Not intended to be called by end users.
mkDVar :: Num n => DiaID s -> String -> VarType -> Expr s n
mkDVar d s ty = MFS.makeVariable (diaVar d s ty)
-- | Create a variable unattached to any particular diagram, with a
-- a given name and type. Not intended to be called by end users.
mkVar :: Num n => String -> VarType -> Expr s n
mkVar s ty = MFS.makeVariable (newVar s ty)
-- | Make a variable tracking the local origin of a given diagram.
-- Not intended to be called by end users.
mkDPVar :: Num n => DiaID s -> String -> P2 (Expr s n)
mkDPVar d s = mkDVar d s X ^& mkDVar d s Y
-- | Make a variable corresponding to a 2D point. Not intended to be
-- called by end users.
mkPVar :: Num n => String -> P2 (Expr s n)
mkPVar s = mkVar s X ^& mkVar s Y
------------------------------------------------------------
-- Constraints
------------------------------------------------------------
-- | A set of 'Constraints' is a monadic computation
-- in the 'MFS.MFSolver' monad. Users need not concern themselves
-- with the details of 'MFS.MFSolver'; it should suffice to think of
-- 'Constraints' as an abstract type.
--
-- This type is INTERNAL to the library and should not be exported.
-- There is no need to have two separate layers of combining
-- things---combining Constraints and then also combining
-- Constrained systems, both using a monadic interface. In the
-- user-facing API, we just immediately turn each Constraints value
-- into a Constrained computation, which can then be combined.
type Constraints s n = MFS.MFSolver (Var s) n ()
-- | 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 ConstrainedState s b n m = ConstrainedState
{ _equations :: Constraints s n
-- ^ Current set of constraints
, _diaCounter :: Int
-- ^ Global counter for unique diagram IDs
, _varCounter :: Int
-- ^ Global counter for unique variable IDs
, _diagrams :: M.Map (DiaID s) (QDiagram b V2 n m)
-- ^ Map from diagram IDs to diagrams
}
makeLenses ''ConstrainedState
-- | The initial ConstrainedState: no equations, no diagrams, and
-- counters at 0.
initConstrainedState :: ConstrainedState s b n m
initConstrainedState = ConstrainedState
{ _equations = return ()
, _diaCounter = 0
, _varCounter = 0
, _diagrams = M.empty
}
-- | 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.
type Constrained s b n m a = State (ConstrainedState s b n m) a
------------------------------------------------------------
-- Constraint DSL
------------------------------------------------------------
--------------------------------------------------
-- Creating constrainable things
-- | 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'.
newDia
:: (Hashable n, Floating n, RealFrac n)
=> QDiagram b V2 n m -> Constrained s b n m (DiaID s)
newDia dia = do
d <- newScalableDia dia
scaleOf d ==== 1
return d
-- | 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.
newScalableDia :: QDiagram b V2 n m -> Constrained s b n m (DiaID s)
newScalableDia dia = do
d <- DiaID <$> (diaCounter <+= 1)
diagrams . L.at d ?= dia
return d
-- | Introduce a list of diagrams into the constrained system.
-- Returns a corresponding list of unique IDs for use in referring
-- to the diagrams later.
newDias
:: (Hashable n, Floating n, RealFrac n)
=> [QDiagram b V2 n m] -> Constrained s b n m [DiaID s]
newDias = mapM newDia
--------------------------------------------------
-- Constrained points etc.
-- | 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)@
centerOf :: Num n => DiaID s -> P2 (Expr s n)
centerOf d = mkDPVar d "center"
-- | 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.
xOf :: Num n => DiaID s -> Expr s n
xOf d = mkDVar d "center" X
-- | 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.
yOf :: Num n => DiaID s -> Expr s n
yOf d = mkDVar d "center" Y
-- | 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.)
--
scaleOf :: Num n => DiaID s -> Expr s n
scaleOf d = mkDVar d "scale" S
-- | 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]
--
-- <<diagrams/src_Diagrams_TwoD_Layout_Constrained_circleWithCircle.svg#diagram=circleWithCircle&width=300>>
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))
newPointOn d getP = do
-- the fromJust is justified, because the type discipline on DiaIDs ensures
-- they will always represent a valid index in the Map.
dia <- fromJust <$> use (diagrams . L.at d)
let p = getP dia
v <- varCounter <+= 1
let newPt = mkDPVar d ("a" ++ show v)
-- constrain the new point to move relative to the diagram origin,
-- taking possible scaling into account
centerOf d .+^ (scaleOf d *^ (mkExpr <$> (p .-. origin))) =.= newPt
return newPt
-- | 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.
newPoint :: Num n => Constrained s b n m (P2 (Expr s n))
newPoint = do
v <- varCounter <+= 1
return $ mkPVar ("a" ++ show v)
-- | Introduce a new scalar value which can be constrained. If still
-- unconstrained at the end, it will default to 1.
newScalar :: Num n => Constrained s b n m (Expr s n)
newScalar = do
v <- varCounter <+= 1
return $ mkVar ("s" ++ show v) S
--------------------------------------------------
-- Specifying constraints
-- | Apply some constraints.
constrain :: Constraints s n -> Constrained s b n m ()
constrain newConstraints = equations %= (>> newConstraints)
-- XXX should this be right-nested instead? Does it matter?
infix 1 =.=, =^=, ====
-- | Constrain two scalar expressions to be equal.
-- Note that you need not worry about introducing redundant
-- constraints; they are ignored.
(====)
:: (Floating n, RealFrac n, Hashable n)
=> Expr s n -> Expr s n -> Constrained s b n m ()
a ==== b = constrain $ MFS.ignore (a MFS.=== b)
-- | Constrain two points to be equal.
(=.=)
:: (Hashable n, Floating n, RealFrac n)
=> P2 (Expr s n) -> P2 (Expr s n) -> Constrained s b n m ()
(coords -> px :& py) =.= (coords -> qx :& qy) = do
px ==== qx
py ==== qy
-- | Constrain two vectors to be equal.
(=^=)
:: (Hashable n, Floating n, RealFrac n)
=> V2 (Expr s n) -> V2 (Expr s n) -> Constrained s b n m ()
(coords -> px :& py) =^= (coords -> qx :& qy) = do
px ==== qx
py ==== qy
-- | 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.
constrainWith
:: (Hashable n, RealFrac n, Floating n, Monoid' m)
=> -- (forall a. (...) => [a] -> a)
([[Located (Envelope V2 n)]] -> [Located (Envelope V2 n)])
-> [DiaID s]
-> Constrained s b n m ()
constrainWith _ [] = return ()
constrainWith f hs = do
diaMap <- use diagrams
let dias = map (fromJust . flip M.lookup diaMap) hs
envs = map ((:[]) . (`at` origin) . getEnvelope) dias
envs' = f envs
eCtrs = map loc envs'
offs = zipWith (.-.) (tail eCtrs) eCtrs
rtps = zipWith envelopeP offs (init envs')
ltps = zipWith (envelopeP . negated) offs (tail envs')
gaps' = (map . fmap) mkExpr $ zipWith (.-.) ltps rtps
rts <- zipWithM newPointOn (init hs) (map envelopeP offs)
lts <- zipWithM newPointOn (tail hs) (map (envelopeP . negated) offs)
zipWithM3_ (\r g l -> r .+^ g =.= l) rts gaps' lts
zipWithM3_ :: Monad m => (a -> b -> c -> m d) -> [a] -> [b] -> [c] -> m ()
zipWithM3_ f as bs cs = sequence_ $ zipWith3 f as bs cs
-- | Constrain the origins of two diagrams to have the same
-- x-coordinate.
sameX
:: (Hashable n, Floating n, RealFrac n)
=> DiaID s -> DiaID s -> Constrained s b n m ()
sameX h1 h2 = xOf h1 ==== xOf h2
-- | Constrain the origins of two diagrams to have the same
-- y-coordinate.
sameY
:: (Hashable n, Floating n, RealFrac n)
=> DiaID s -> DiaID s -> Constrained s b n m ()
sameY h1 h2 = yOf h1 ==== yOf h2
-- | Constrain a list of scalar expressions to be all equal.
allSame
:: (Hashable n, Floating n, RealFrac n)
=> [Expr s n] -> Constrained s b n m ()
allSame as = zipWithM_ (====) as (tail as)
-- | @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@.
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 ()
constrainDir dir p q = do
s <- newScalar
p .+^ (s *^ fromDirection dir) =.= q
-- | @along d ps@ constrains the points @ps@ to all lie along a ray
-- parallel to the direction @d@.
along :: (Hashable n, Floating n, RealFrac n) => Direction V2 (Expr s n) -> [P2 (Expr s n)] -> Constrained s b n m ()
along dir ps = zipWithM_ (constrainDir dir) ps (tail ps)
------------------------------------------------------------
-- Constraint resolution
------------------------------------------------------------
-- | A data type holding a variable together with its resolution
-- status: its solved value, if it exists, or Nothing if the
-- variable is still unresolved.
data VarResolution s n = VR { _resolvedVar :: Var s, _resolution :: Maybe n }
makeLenses ''VarResolution
-- | Check whether a variable has been resolved.
isResolved :: VarResolution s n -> Bool
isResolved = L.is _Just . view resolution
-- | Get the three variables associated with a diagram: X, Y, and
-- Scale.
getDiaVars
:: MFS.Dependencies (Var s) n -> DiaID s -> M.Map VarType (VarResolution s n)
getDiaVars deps d = M.fromList $
[ (X, getRes (diaVar d "center" X))
, (Y, getRes (diaVar d "center" Y))
, (S, getRes (diaVar d "scale" S))
]
where
getRes v
= VR v (either (const Nothing) Just $ MFS.getKnown v deps)
-- | 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.
layout
:: (Monoid' m, Hashable n, Floating n, RealFrac n, Show n)
=> (forall s. Constrained s b n m a)
-> QDiagram b V2 n m
layout constr =
case MFS.execSolver (MFS.ignore $ s ^. equations) MFS.noDeps of
Left _depError -> error "overconstrained"
Right deps ->
let deps' = resolve (map fst dias) deps
in mconcat . flip map dias $ \(d, dia) ->
let vars = getDiaVars deps' d
expectedRes ty = vars ^?! L.at ty . _Just . resolution . _Just
in
case F.all (isResolved) vars of
True -> dia # scale (expectedRes S)
# translate (expectedRes X ^& expectedRes Y)
_ -> error . unlines $
[ "Diagrams.TwoD.Layout.Constrained.layout: impossible!"
, "Diagram variables not resolved. Please report this as a bug:"
, " https://github.com/diagrams/diagrams-contrib/issues"
]
-- 'resolve' should always set the S, X, and Y variables for
-- a diagram if they aren't already constrained, so getDiaVars
-- should return three resolved variables
where
s = execState constr initConstrainedState
dias = M.assocs (s ^. diagrams)
resolve
:: (Hashable n, RealFrac n, Floating n, Show n)
=> [DiaID s] -> MFS.Dependencies (Var s) n -> MFS.Dependencies (Var s) n
resolve diaIDs deps =
case unresolved of
[] -> deps
((VR v _) : _) ->
let eq = MFS.makeVariable v - (if v^.varType == S then 1 else 0)
in case MFS.addEquation deps eq of
Right deps' -> resolve diaIDs deps'
Left err -> error . unlines $
[ "Diagrams.TwoD.Layout.Constrained.layout: impossible!"
, "Adding equation for unconstrained variable produced an error:"
, show err
, "Please report this as a bug:"
, " https://github.com/diagrams/diagrams-contrib/issues"
]
where
diaVars = diaIDs >>= (M.elems . getDiaVars deps)
unresolved
= sortBy (comparing (view (resolvedVar.varType)))
. filter (not . isResolved)
$ diaVars