packages feed

gemstone-0.1: Gemstone/Box.hs

{-# LANGUAGE EmptyDataDecls, TemplateHaskell #-}
module Gemstone.Box (
    Box(), BoxLike(..), unBox, box,
    pInter, bInter,
    makeXYWH, makeXYWHValid, makeXYXYValid,
    bLeft, bBot, bRight, bTop,
    bW, bH, bW', bH',
    bX, bY, bXY,
    center,
    scaleBox,
) where

import Control.Lens

data BoxLike v = BoxLike { _bx1, _by1, _bx2, _by2 :: v }
    deriving (Show)

makeLenses ''BoxLike

newtype Box v = Box { unBox :: BoxLike v }
    deriving (Show)

-- | Whether two vertices would determine the lower-left and upper-right corners
--   of a rectangle.
--
--   This is the predicate for determining whether a box is valid.
pInter :: Ord v => v -> v -> v -> v -> Bool
pInter x1 y1 x2 y2 = x1 < x2 && y1 < y2

-- | Prism for getting a Box from a BoxLike.
--
--   The Box constructor isn't exported, so this is the only way to obtain a
--   valid Box.
box :: Ord v => Simple Prism (BoxLike v) (Box v)
box = prism unBox f
    where
    f b | pred b = Right $ Box b
        | otherwise = Left b
    pred (BoxLike x1 y1 x2 y2) = pInter x1 y1 x2 y2

-- | Whether two boxes intersect.
--
--   The boxes have to be good.
bInter :: Ord v => Box v -> Box v -> Bool
bInter (Box b1) (Box b2) =
    pInter (b1 ^. bx1) (b1 ^. by1) (b2 ^. bx2) (b2 ^. by2) &&
    pInter (b2 ^. bx1) (b2 ^. by1) (b1 ^. bx2) (b1 ^. by2)

-- | Make a box with width and height.
makeXYWH :: (Ord v, Num v) => v -> v -> v -> v -> BoxLike v
makeXYWH x y w h = BoxLike x y (x + w) (y + h)

-- | Like makeXYWH, but valid.
--
--   Includes a slap on the face if the box is not valid.
makeXYWHValid :: (Ord v, Num v) => v -> v -> v -> v -> Box v
makeXYWHValid x y w h = case makeXYWH x y w h ^? box of
    Just b -> b
    Nothing -> error "makeXYWHValid: Zero width or height"

-- | Make a valid box.
--
--   Same signature as BoxLike, but making a Box.
makeXYXYValid :: (Ord v, Num v) => v -> v -> v -> v -> Box v
makeXYXYValid x1 y1 x2 y2 = case BoxLike x1 y1 x2 y2 ^? box of
    Just b -> b
    Nothing -> error "makeXYXYValid: Zero width or height"

-- | Resize a box by moving an edge.
--
--   Inherently unsafe.
bLeft, bBot, bRight, bTop :: Simple Lens (BoxLike a) a
bLeft   = bx1
bBot    = by1
bRight  = bx2
bTop    = by2

-- | Resize a box by changing its width or height.
--
--   Inherently unsafe.
bW, bH, bW', bH' :: Num a => Simple Lens (BoxLike a) a
bW  f (BoxLike x1 y1 x2 y2) =
    fmap (\w -> BoxLike x1 y1 (x2 + w) y2) (f (x2 - x1))
bH  f (BoxLike x1 y1 x2 y2) =
    fmap (\h -> BoxLike x1 y1 x2 (y2 + h)) (f (y2 - y1))
bW' f (BoxLike x1 y1 x2 y2) =
    fmap (\w -> BoxLike (x1 - w) y1 x2 y2) (f (x2 - x1))
bH' f (BoxLike x1 y1 x2 y2) =
    fmap (\h -> BoxLike x1 (y1 - h) x2 y2) (f (y2 - y1))

-- | Move a box.
bX, bY :: Num a => Simple Lens (Box a) a
bX f (Box (BoxLike x1 y1 x2 y2)) =
    fmap (\x' -> Box (BoxLike x' y1 (x' + x2 - x1) y2)) (f x1)
bY f (Box (BoxLike x1 y1 x2 y2)) =
    fmap (\y' -> Box (BoxLike x1 y' x2 (y' + y2 - y1))) (f y1)

-- Move a box more efficiently.
bXY :: Num a => Simple Lens (Box a) (a, a)
bXY f (Box (BoxLike x1 y1 x2 y2)) = let
    f' (w, h) = Box $ BoxLike w h (w + x2 - x1) (h + y2 - y1)
    in fmap f' $ f (x1, y1)

-- The center of a box. Read-only.
center :: Fractional a => Box a -> (a, a)
center (Box (BoxLike x1 y1 x2 y2)) = ((x1 + x2) / 2, (y1 + y2) / 2)

-- Scale a box.
scaleBox :: (Eq v, Num v) => v -> v -> Box v -> Box v
scaleBox 0 _ _ = error "scaleBox: Zero width"
scaleBox _ 0 _ = error "scaleBox: Zero height"
scaleBox sx sy (Box (BoxLike x1 y1 x2 y2)) =
    Box $ BoxLike (x1 * sx) (y1 * sy) (x2 * sx) (y2 * sy)