geomancy-layout-0.1: src/Geomancy/Layout/Box.hs
module Geomancy.Layout.Box where
import Prelude hiding (or)
import Geomancy
import Control.Monad (when)
import Foreign qualified
import Geomancy.Mat4 qualified as Mat4
import GHC.Generics (Generic)
import Graphics.Gl.Block qualified as Block
{- | 2D rectangle with its origin at the center.
Size transformations don't affect its position and vice versa.
@
┏━━━━━┓
┃ ┃
┃ * ┃
┃ ┃
┗━━━━━┛
@
-}
data Box = Box
{ position :: Vec2
, size :: Vec2
}
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass Block.Block
deriving Foreign.Storable via (Block.Packed Box)
-- | Place a 'Box' with given dimensions at @(0,0)@.
{-# INLINE box_ #-}
box_ :: Vec2 -> Box
box_ = Box 0
instance Semigroup Box where
{-# INLINE (<>) #-}
(<>) = union
-- | Check if one of the dimensions is negative.
{-# INLINE degenerate #-}
degenerate :: Box -> Bool
degenerate box =
withVec2 box.size \w h ->
w <= 0 ||
h <= 0
-- | Move the 'Box' by the given vector.
{-# INLINE move #-}
move :: Vec2 -> Box -> Box
move delta box = box
{ position =
box.position + delta
}
-- | Adjust 'Box' size by a given amount (absolute).
{-# INLINE resize #-}
resize :: Vec2 -> Box -> Box
resize delta box = box
{ size =
box.size + delta
}
-- | Adjust 'Box' size by a given amount (relative).
{-# INLINE rescale #-}
rescale :: Vec2 -> Box -> Box
rescale delta box = box
{ size =
box.size * delta
}
-- * Edge representation
-- | Packed top- right- bottom- left- edge values.
newtype TRBL = TRBL Vec4
deriving stock (Eq, Ord, Show, Generic)
instance Semigroup TRBL where
{-# INLINE (<>) #-}
TRBL a <> TRBL b =
withVec4 a \at ar ab al ->
withVec4 b \bt br bb bl ->
TRBL $ vec4 (min at bt) (min ar br) (min ab bb) (max al bl)
type WithTRBL r = Float -> Float -> Float -> Float -> r
{-# INLINE fromTRBL #-}
fromTRBL :: WithTRBL Box
fromTRBL t r b l =
Box
{ position =
-- XXX: recover midpoint
vec2
(l * 0.5 + r * 0.5)
(t * 0.5 + b * 0.5)
, size =
-- XXX: recover size
vec2 (r - l) (b - t)
}
{-# INLINE toTRBL #-}
toTRBL :: Box -> TRBL
toTRBL box = TRBL $ withTRBL box vec4
{-# INLINE withTRBL #-}
withTRBL :: Box -> WithTRBL r -> r
withTRBL Box{..} f =
withVec2 position \x y ->
withVec2 size \w h ->
let
t = y - h * 0.5
r = x + w * 0.5
b = y + h * 0.5
l = x - w * 0.5
in
f t r b l
-- | Construct a smaller Box by adding non-uniform padding.
{-# INLINE addPadding #-}
addPadding :: TRBL -> Box -> Box
addPadding (TRBL padding) box =
withVec4 padding \pt pr pb pl ->
withTRBL box \t r b l ->
fromTRBL (t + pt) (r - pr) (b - pb) (l + pl)
-- | Construct a smaller Box by adding non-uniform padding as a fraction of 'Box' size.
{-# INLINE addPaddingRel #-}
addPaddingRel :: TRBL -> Box -> Box
addPaddingRel (TRBL padding) box =
withVec2 box.size \w h ->
addPadding (TRBL $ padding * vec4 h w h w) box
-- | Construct a larger Box by adding non-uniform margins.
{-# INLINE addMargins #-}
addMargins :: TRBL -> Box -> Box
addMargins (TRBL margins) box =
withVec4 margins \mt mr mb ml ->
withTRBL box \t r b l ->
fromTRBL (t - mt) (r + mr) (b + mb) (l - ml)
-- | Construct a larger Box by adding non-uniform margins as a fraction of 'Box' size.
{-# INLINE addMarginsRel #-}
addMarginsRel :: TRBL -> Box -> Box
addMarginsRel (TRBL margins) box =
withVec2 box.size \w h ->
addMargins (TRBL $ margins * vec4 h w h w) box
-- * AABB representation
-- | Bounding box from 2 points, automatically sorted.
{-# INLINE fromCorners #-}
fromCorners :: Vec2 -> Vec2 -> Box
fromCorners a b =
withVec2 a \ax ay ->
withVec2 b \bx by ->
fromTRBL (min ay by) (max ax bx) (max ay by) (min ax bx)
-- | 2-point AABB.
{-# INLINE toCorners #-}
toCorners :: Box -> (Vec2, Vec2)
toCorners box = withCorners box (,)
{-# INLINE withCorners #-}
withCorners :: Box -> (Vec2 -> Vec2 -> r) -> r
withCorners box f =
withTRBL box \t r b l ->
f (vec2 l t) (vec2 r b)
-- * Point-box interaction
-- | Project a point into the 'Box' space.
{-# INLINE projectInto #-}
projectInto :: Vec2 -> Box -> Vec2
projectInto point box = point - box.position
-- | Test if a point is within the 'Box' bounds.
{-# INLINE inside #-}
inside :: Vec2 -> Box -> Bool
inside point box =
withVec2 (point `projectInto` box) \px py ->
withVec2 (box.size / 2) \hw hh ->
px > -hw && px < hw &&
py > -hh && py < hh
whenInside :: Applicative m => Vec2 -> Box -> (Vec2 -> m ()) -> m ()
whenInside point box action =
when (inside point box) $
action (point `projectInto` box)
-- * Box-box interaction
-- | Test if a 'Box' can contain a given 'Box'.
{-# INLINE canContain #-}
canContain :: Box -> Box -> Bool
canContain outer inner =
withVec2 inner.size \iw ih ->
withVec2 outer.size \ow oh ->
iw <= ow &&
ih <= oh
-- | Test if a 'Box' fully contains a given 'Box'.
{-# INLINE contains #-}
contains :: Box -> Box -> Bool
contains outer inner =
withTRBL outer \ot or ob ol ->
withTRBL inner \it ir ib il ->
it >= ot &&
ir <= or &&
ib <= ob &&
il >= ol
{-# INLINE union #-}
-- | Get a 'Box' that tightly wraps both its elements.
union :: Box -> Box -> Box
union a b =
withTRBL a \at ar ab al ->
withTRBL b \bt br bb bl ->
fromTRBL (min at bt) (max ar br) (max ab bb) (min al bl)
{- | Get an intersection between two boxes, if there is one.
Use faster `intersects` instead if only need a test.
-}
intersection :: Box -> Box -> Maybe Box
intersection a b =
if degenerate candidate then
Nothing
else
Just candidate
where
candidate = intersectionDirty a b
-- | Get a potentially-degenerate intersection between two boxes.
{-# INLINE intersectionDirty #-}
intersectionDirty :: Box -> Box -> Box
intersectionDirty a b =
withTRBL a \at ar ab al ->
withTRBL b \bt br bb bl ->
fromTRBL (max at bt) (min ar br) (min ab bb) (max al bl)
{- | Box-box intersection test.
Any edge contact counts as intersection.
For area contact use 'intersection`, which is a little less efficient.
-}
{-# INLINE intersects #-}
intersects :: Box -> Box -> Bool
intersects a b =
withTRBL a \at ar ab al ->
withTRBL b \bt br bb bl ->
at <= bb &&
al <= br &&
bl <= ar &&
bt <= ab
-- TODO: SIMD `intersects`
{- | Remaining space when one box is placed inside another.
All positive when the box is fully inside.
Negative edges mean the box is "outside" in that direction.
@
addPadding (leftovers inner outer) inner === outer
addMargins (leftovers inner outer) outer === inner
@
-}
leftovers :: Box -> Box -> TRBL
leftovers a b =
TRBL $ withLeftovers a b vec4
withLeftovers :: Box -> Box -> WithTRBL r -> r
withLeftovers a b f =
withTRBL a \ta ra ba la ->
withTRBL b \tb rb bb lb ->
f
(tb - ta)
(ra - rb)
(ba - bb)
(lb - la)
-- * Conversion
-- | Build a transformation matrix to stretch a unit square and place it at depth 0.0.
{-# INLINE mkTransform #-}
mkTransform :: Box -> Transform
mkTransform = mkTransformZ 0.0
-- | Build a transformation matrix to stretch a unit square and place it at a given depth.
{-# INLINE mkTransformZ #-}
mkTransformZ :: Float -> Box -> Transform
mkTransformZ z Box{..} =
withVec2 position \x y ->
withVec2 size \w h ->
Mat4.rowMajor
w 0 0 0
0 h 0 0
0 0 1 0
x y z 1