tinytools-0.1.0.4: src/Potato/Flow/Math.hs
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Potato.Flow.Math (
XY
, LBox(..)
, nilLBox
, make_0area_lBox_from_XY
, make_1area_lBox_from_XY
, make_lBox_from_XYs
, make_lBox_from_XYlist
, does_lBox_contains_XY
, lBox_tl
, lBox_area
, lBox_to_axis
, translate_lBox
, add_XY_to_lBox
, make_lBox_from_axis
, union_lBox
, lBox_expand
, intersect_lBox
, intersect_lBox_include_zero_area
, does_lBox_intersect
, does_lBox_intersect_include_zero_area
, substract_lBox
-- these helpers maybe belong in a different file, they have very specific usages
, CanonicalLBox(..)
, canonicalLBox_from_lBox
, canonicalLBox_from_lBox_
, lBox_from_canonicalLBox
, deltaLBox_via_canonicalLBox
, lBox_isCanonicalLBox
, Delta(..)
, DeltaXY(..)
, DeltaLBox(..)
, deltaLBox_invert
, module Linear.V2
) where
import Relude
import Data.Aeson
import Data.Binary
import Linear.V2
import qualified Text.Show
import Control.Exception (assert)
{-
CORDINATE SYSTEM
UPPER LEFT CORNER is 0 0
(0,0)--- +x
|
|
+y
-}
type XY = V2 Int
instance FromJSON XY
instance ToJSON XY
instance FromJSONKey XY
instance ToJSONKey XY
-- | a point in screen space
-- should only be used by VC, so does not belong here
--newtype VPoint = VPoint (Int, Int) deriving (Generic, Show, FromJSON, ToJSON)
-- | a box in logical space
-- note size is non inclusive
-- e.g. an LBox with size (1,1) is exactly 1 point at ul
-- e.g. an LBox with size (0,0) contains nothing
data LBox = LBox {
_lBox_tl :: XY
, _lBox_size :: XY
} deriving (Eq, Generic)
instance Show LBox where
show (LBox (V2 x y) (V2 w h)) = "LBox: " <> show x <> " " <> show y <> " " <> show w <> " " <> show h
instance FromJSON LBox
instance ToJSON LBox
instance Binary LBox
instance NFData LBox
nilLBox :: LBox
nilLBox = LBox 0 0
lBox_area :: LBox -> Int
lBox_area (LBox _ (V2 w h)) = w*h
lBox_tl :: LBox -> XY
lBox_tl (LBox p _) = p
translate_lBox :: XY -> LBox -> LBox
translate_lBox pan (LBox p s) = LBox (p+pan) s
-- | returns a 0 area LBox
make_0area_lBox_from_XY :: XY -> LBox
make_0area_lBox_from_XY p = LBox p 0
-- | returns a 1 area LBox
make_1area_lBox_from_XY :: XY -> LBox
make_1area_lBox_from_XY p = LBox p 1
-- TODO rename to make_lBox_from_XY_XY
-- | always returns a canonical LBox
make_lBox_from_XYs :: XY -> XY -> LBox
make_lBox_from_XYs (V2 x1 y1) (V2 x2 y2) =
LBox {
_lBox_tl= V2 (min x1 x2) (min y1 y2)
, _lBox_size = V2 (abs (x1 - x2)) (abs (y1 - y2))
}
-- TODO rename to make_lBox_from_XYs
-- | always returns a canonical LBox
make_lBox_from_XYlist :: [XY] -> LBox
make_lBox_from_XYlist [] = nilLBox
make_lBox_from_XYlist (x:xs) = foldr add_XY_to_lBox (make_0area_lBox_from_XY x) xs
-- | always returns a canonical LBox
-- bottom/right XYs cells are not included in
add_XY_to_lBox :: XY -> LBox -> LBox
add_XY_to_lBox (V2 px py) lbox = r where
(LBox (V2 bx by) (V2 bw bh)) = canonicalLBox_from_lBox_ lbox
r = LBox {
_lBox_tl = V2 (min px bx) (min py by)
, _lBox_size = V2 (max bw $ max (abs (px-bx)) (abs (px-(bx+bw)))) (max bh $ max (abs (py-by)) (abs (py-(by+bh))))
}
-- specifically `make_1area_lBox_from_XY pos` must be contained in lbox
-- so XYs on the bottom/right border are not included
does_lBox_contains_XY :: LBox -> XY -> Bool
does_lBox_contains_XY (LBox (V2 bx by) (V2 bw bh)) (V2 px py) =
px >= bx && py >= by && px < (bx + bw) && py < (by + bh)
-- | right and bottom axis are non-inclusive
make_lBox_from_axis :: (Int, Int, Int, Int) -> LBox
make_lBox_from_axis (x1,x2,y1,y2) = LBox (V2 rx ry) (V2 rw rh) where
rx = min x1 x2
ry = min y1 y2
rw = abs (x1-x2)
rh = abs (y1-y2)
-- | (left, right, top, bottom)
-- right and bottom are non-inclusive
lBox_to_axis :: LBox -> (Int, Int, Int, Int)
lBox_to_axis (LBox (V2 x y) (V2 w h)) = (min x (x+w), max x (x+w), min y (y+h), max y (y+h))
min4 :: (Ord a) => a -> a -> a -> a -> a
min4 a1 a2 a3 a4 = min (min (min a1 a2) a3) a4
max4 :: (Ord a) => a -> a -> a -> a -> a
max4 a1 a2 a3 a4 = max (max (max a1 a2) a3) a4
-- | inverted LBox are treated as if not inverted
union_lBox :: LBox -> LBox -> LBox
union_lBox (LBox (V2 x1 y1) (V2 w1 h1)) (LBox (V2 x2 y2) (V2 w2 h2)) = combined where
cx1 = x1 + w1
cy1 = y1 + h1
cx2 = x2 + w2
cy2 = y2 + h2
combined = make_lBox_from_axis (min4 x1 cx1 x2 cx2, max4 x1 cx1 x2 cx2, min4 y1 cy1 y2 cy2, max4 y1 cy1 y2 cy2)
-- assumes lbox is canonical
lBox_expand :: LBox -> (Int, Int, Int, Int) -> LBox
lBox_expand (LBox (V2 x y) (V2 w h)) (l, r, u, d) = LBox (V2 (x-l) (y-u)) (V2 (w+l+r) (h+u+d))
-- | inverted LBox are treated as if not inverted
intersect_lBox :: LBox -> LBox -> Maybe LBox
intersect_lBox lb1@(LBox (V2 x1 y1) (V2 w1 h1)) lb2@(LBox (V2 x2 y2) (V2 w2 h2)) = r where
cx1 = x1 + w1
cy1 = y1 + h1
cx2 = x2 + w2
cy2 = y2 + h2
l1 = min cx1 x1
l2 = min cx2 x2
r1 = max cx1 x1
r2 = max cx2 x2
t1 = min cy1 y1
t2 = min cy2 y2
b1 = max cy1 y1
b2 = max cy2 y2
r = if does_lBox_intersect lb1 lb2
then Just $ make_lBox_from_axis (max l1 l2, min r1 r2, max t1 t2, min b1 b2)
else Nothing
intersect_lBox_include_zero_area :: LBox -> LBox -> Maybe LBox
intersect_lBox_include_zero_area lb1@(LBox (V2 x1 y1) (V2 w1 h1)) lb2@(LBox (V2 x2 y2) (V2 w2 h2)) = r where
cx1 = x1 + w1
cy1 = y1 + h1
cx2 = x2 + w2
cy2 = y2 + h2
l1 = min cx1 x1
l2 = min cx2 x2
r1 = max cx1 x1
r2 = max cx2 x2
t1 = min cy1 y1
t2 = min cy2 y2
b1 = max cy1 y1
b2 = max cy2 y2
r = if does_lBox_intersect_include_zero_area lb1 lb2
then Just $ make_lBox_from_axis (max l1 l2, min r1 r2, max t1 t2, min b1 b2)
else Nothing
does_lBox_intersect :: LBox -> LBox -> Bool
does_lBox_intersect lb1 lb2 = r where
(l1,r1,t1,b1) = lBox_to_axis lb1
(l2,r2,t2,b2) = lBox_to_axis lb2
r | lBox_area lb1 == 0 = False
| lBox_area lb2 == 0 = False
| l1 >= r2 = False
| l2 >= r1 = False
| t1 >= b2 = False
| t2 >= b1 = False
| otherwise = True
does_lBox_intersect_include_zero_area :: LBox -> LBox -> Bool
does_lBox_intersect_include_zero_area lb1 lb2 = r where
(l1,r1,t1,b1) = lBox_to_axis lb1
(l2,r2,t2,b2) = lBox_to_axis lb2
r | lb1 == lb2 = True -- this covers the case of 2 0 area boxes over each other
| l1 >= r2 = False
| l2 >= r1 = False
| t1 >= b2 = False
| t2 >= b1 = False
| otherwise = True
-- | substract lb2 from lb1 and return [LBox] representing the difference
substract_lBox :: LBox -> LBox -> [LBox]
substract_lBox lb1@(LBox _ (V2 w1 h1)) lb2 = r where
(l1,r1,t1,b1) = lBox_to_axis lb1
(l2,r2,t2,b2) = lBox_to_axis lb2
mleft = if l1 < l2
then Just $ LBox (V2 l1 t1) (V2 (min (l2-l1) w1) h1)
else Nothing
mright = if r1 > r2
then Just $ LBox (V2 (max r2 l1) t1) (V2 (min (r1-r2) w1) h1)
else Nothing
mtop' = if t1 < t2
then Just $ LBox (V2 l1 t1) (V2 w1 (min (t2-t1) h1))
else Nothing
mbot' = if b1 > b2
then Just $ LBox (V2 l1 (max b2 t1)) (V2 w1 (min (b1-b2) h1))
else Nothing
-- TODO crop away mleft/mright from mtop'/mbot'
mtop = mtop'
mbot = mbot'
r = catMaybes [mleft,mright,mtop, mbot]
-- | CanonicalLBox is always has non-negative width/height
-- and tracks which axis are flipped to return back to original LBox
-- first Bool is if x values are flipped, second is for y
data CanonicalLBox = CanonicalLBox Bool Bool LBox
canonicalLBox_from_lBox :: LBox -> CanonicalLBox
canonicalLBox_from_lBox (LBox (V2 x y) (V2 w h)) = r where
fx = w < 0
fy = h < 0
r = CanonicalLBox fx fy $ make_lBox_from_axis (x, x+w, y, y+h)
-- | same as canonicalLBox_from_lBox but returns just the canonical LBox
canonicalLBox_from_lBox_ :: LBox -> LBox
canonicalLBox_from_lBox_ lbox = r where
(CanonicalLBox _ _ r) = canonicalLBox_from_lBox lbox
lBox_from_canonicalLBox :: CanonicalLBox -> LBox
lBox_from_canonicalLBox (CanonicalLBox fx fy (LBox (V2 x y) (V2 w h))) = LBox (V2 x' y') (V2 w' h') where
x' = if fx then x+w else x
y' = if fy then y+h else y
w' = if fx then -w else w
h' = if fy then -h else h
deltaLBox_via_canonicalLBox :: CanonicalLBox -> DeltaLBox -> DeltaLBox
deltaLBox_via_canonicalLBox (CanonicalLBox fx fy _) DeltaLBox {..} = r where
V2 tx ty = _deltaLBox_translate
V2 sx sy = _deltaLBox_resizeBy
(rtx, rsx) = if fx then (sx, tx) else (tx, sx)
(rty, rsy) = if fy then (sy, ty) else (ty, sy)
r = DeltaLBox (V2 rtx rty) (V2 rsx rsy)
lBox_isCanonicalLBox :: LBox -> Bool
lBox_isCanonicalLBox lbx = canonicalLBox_from_lBox_ lbx == lbx
-- TODO maybe DELETE?
class Delta x dx where
plusDelta :: x -> dx -> x
minusDelta :: x -> dx -> x
instance Delta XY XY where
plusDelta = (+)
minusDelta = (-)
instance (Show a, Eq a) => Delta a (a,a) where
plusDelta s (b, a) = if b /= s
then error $ show s <> " " <> show b <> " " <> show a
else a
minusDelta s (b, a) = assert (a == s) b
newtype DeltaXY = DeltaXY XY deriving (Eq, Generic, Show)
instance NFData DeltaXY
instance Delta XY DeltaXY where
plusDelta xy (DeltaXY dxy) = xy + dxy
minusDelta xy (DeltaXY dxy) = xy - dxy
instance (Delta a c, Delta b d) => Delta (a,b) (c,d) where
plusDelta (a,b) (c,d) = (plusDelta a c, plusDelta b d)
minusDelta (a,b) (c,d) = (minusDelta a c, minusDelta b d)
data DeltaLBox = DeltaLBox {
_deltaLBox_translate :: XY
, _deltaLBox_resizeBy :: XY
} deriving (Eq, Generic, Show)
instance NFData DeltaLBox
instance Delta LBox DeltaLBox where
plusDelta LBox {..} DeltaLBox {..} = LBox {
_lBox_tl = plusDelta _lBox_tl _deltaLBox_translate
, _lBox_size = plusDelta _lBox_size _deltaLBox_resizeBy
}
minusDelta LBox {..} DeltaLBox {..} = LBox {
_lBox_tl = minusDelta _lBox_tl _deltaLBox_translate
, _lBox_size = minusDelta _lBox_size _deltaLBox_resizeBy
}
deltaLBox_invert :: DeltaLBox -> DeltaLBox
deltaLBox_invert DeltaLBox {..} = DeltaLBox {
_deltaLBox_translate = negate _deltaLBox_translate
, _deltaLBox_resizeBy = negate _deltaLBox_resizeBy
}