module Data.Sifflet.Geometry (Position(..), positionDelta, positionDistance, positionDistanceSquared, positionCloseEnough, Circle(..), pointInCircle, Size(..), BBox(..), bbX, bbY, bbWidth, bbSetWidth, bbHeight, bbPosition, bbSize, bbToRect, bbFromRect, bbCenter, bbLeft, bbXCenter, bbRight, bbTop, bbYCenter, bbBottom, bbMerge, bbMergeList, pointInBB, Widen(widen), Translate(..) ) where import Data.Tree as T import Graphics.UI.Gtk(Rectangle(Rectangle)) -- A Position may be interpreted either absolutely, as a point (x, y); -- or relatively, as an offset (dx, dy) data Position = Position {posX :: Double, posY :: Double} -- x, y deriving (Eq, Read, Show) positionDelta :: Position -> Position -> (Double, Double) positionDelta (Position x1 y1) (Position x2 y2) = (x2 - x1, y2 - y1) positionDistance :: Position -> Position -> Double positionDistance p1 p2 = sqrt (positionDistanceSquared p1 p2) positionDistanceSquared :: Position -> Position -> Double positionDistanceSquared (Position x1 y1) (Position x2 y2) = (x1 - x2) ** 2 + (y1 - y2) ** 2 positionCloseEnough :: Position -> Position -> Double -> Bool positionCloseEnough p1 p2 radius = -- Essentially asks if p1 and p2 are nearly intersecting, -- i.e., if p1 is within a circle with center p2 and the given radius positionDistanceSquared p1 p2 <= radius ** 2 data Circle = Circle {circleCenter :: Position, circleRadius :: Double} deriving (Eq, Read, Show) pointInCircle :: Position -> Circle -> Bool pointInCircle point (Circle center radius) = positionCloseEnough point center radius data Size = Size {sizeW :: Double, sizeH :: Double} -- width, height deriving (Eq, Read, Show) -- | BBox x y width height; (x, y) is the top left corner data BBox = BBox Double Double Double Double deriving (Eq, Read, Show) -- | BBox accessors and utilities bbX, bbY, bbWidth, bbHeight :: BBox -> Double bbX (BBox x _y _w _h) = x bbY (BBox _x y _w _h) = y bbWidth (BBox _x _y w _h) = w bbHeight (BBox _x _y _w h) = h bbPosition :: BBox -> Position bbPosition (BBox x y _w _h) = Position x y bbSize :: BBox -> Size bbSize (BBox _x _y w h) = Size w h bbCenter :: BBox -> Position bbCenter (BBox x y w h) = Position (x + w / 2) (y + h / 2) bbSetWidth :: BBox -> Double -> BBox bbSetWidth (BBox x y _w h) nwidth = BBox x y nwidth h bbLeft, bbXCenter, bbRight :: BBox -> Double bbLeft = bbX bbXCenter (BBox x _y w _h) = x + w / 2 bbRight (BBox x _y w _h) = x + w bbTop, bbYCenter, bbBottom :: BBox -> Double bbTop = bbY bbYCenter (BBox _x y _w h) = y + h / 2 bbBottom (BBox _x y _w h) = y + h bbToRect :: BBox -> Rectangle bbToRect (BBox x y w h) = Rectangle (round x) (round y) (round w) (round h) bbFromRect :: Rectangle -> BBox bbFromRect (Rectangle x y w h) = BBox (fromIntegral x) (fromIntegral y) (fromIntegral w) (fromIntegral h) -- | Form a new BBox which encloses two bboxes bbMerge :: BBox -> BBox -> BBox bbMerge bb1 bb2 = let f1 ! f2 = f1 (f2 bb1) (f2 bb2) bottom = max ! bbBottom -- i.e., max (bbBottom bb1) (bbBottom bb2) top = min ! bbTop left = min ! bbLeft right = max ! bbRight in BBox left top (right - left) (bottom - top) bbMergeList :: [BBox] -> BBox bbMergeList [] = error "bbMergeList: empty list" bbMergeList (b:bs) = foldl bbMerge b bs -- Test whether a point (e.g., from mouse click) is within a -- bounding box pointInBB :: Position -> BBox -> Bool pointInBB (Position x y) (BBox x1 y1 w h) = x >= x1 && x <= x1 + w && y >= y1 && y <= y1 + h class Widen a where -- | Make an object have at least a specified minimum width; -- does nothing if it's already at least that wide widen :: a -> Double -> a instance Widen BBox where widen bb@(BBox x y w h) minWidth = if w >= minWidth then bb else BBox x y minWidth h -- | A Translate is a thing that can be repositioned by -- delta x and delta y class Translate a where translate :: Double -- ^ delta X -> Double -- ^ delta Y -> a -- ^ thing in old position -> a -- ^ thing in new position instance (Translate e) => Translate [e] where translate dx dy = map (translate dx dy) instance (Translate e) => Translate (Tree e) where translate dx dy t = T.Node (translate dx dy (rootLabel t)) (translate dx dy (subForest t)) instance Translate BBox where translate dx dy (BBox x y w h) = BBox (x + dx) (y + dy) w h instance Translate Position where translate dx dy (Position x y) = Position (x + dx) (y + dy) instance Translate Circle where translate dx dy (Circle center radius) = Circle (translate dx dy center) radius