Change-Id: I6c6847fac56f0a9a1a2209792e00a3aec5e672b9 Reviewed-on: https://cl.tvl.fyi/c/depot/+/10809 Autosubmit: aspen <root@gws.fyi> Reviewed-by: sterni <sternenseemann@systemli.org> Tested-by: BuildkiteCI Reviewed-by: lukegb <lukegb@tvl.fyi>
177 lines
6.3 KiB
Haskell
177 lines
6.3 KiB
Haskell
{-# LANGUAGE TemplateHaskell #-}
|
|
-- | Graphics algorithms and utils for rendering things in 2D space
|
|
--------------------------------------------------------------------------------
|
|
module Xanthous.Util.Graphics
|
|
( circle
|
|
, filledCircle
|
|
, line
|
|
, straightLine
|
|
, delaunay
|
|
|
|
-- * Debugging and testing tools
|
|
, renderBooleanGraphics
|
|
, showBooleanGraphics
|
|
) where
|
|
--------------------------------------------------------------------------------
|
|
import Xanthous.Prelude
|
|
--------------------------------------------------------------------------------
|
|
-- https://github.com/noinia/hgeometry/issues/28
|
|
-- import qualified Algorithms.Geometry.DelaunayTriangulation.DivideAndConquer
|
|
-- as Geometry
|
|
import qualified Algorithms.Geometry.DelaunayTriangulation.Naive
|
|
as Geometry
|
|
import qualified Algorithms.Geometry.DelaunayTriangulation.Types as Geometry
|
|
import Control.Monad.State (execState, State)
|
|
import qualified Data.Geometry.Point as Geometry
|
|
import Data.Ext ((:+)(..))
|
|
import Data.List (unfoldr)
|
|
import Data.List.NonEmpty (NonEmpty((:|)))
|
|
import qualified Data.List.NonEmpty as NE
|
|
import Data.Ix (Ix)
|
|
import Linear.V2
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
-- | Generate a circle centered at the given point and with the given radius
|
|
-- using the <midpoint circle algorithm
|
|
-- https://en.wikipedia.org/wiki/Midpoint_circle_algorithm>.
|
|
--
|
|
-- Code taken from <https://rosettacode.org/wiki/Bitmap/Midpoint_circle_algorithm#Haskell>
|
|
circle :: (Num i, Ord i)
|
|
=> V2 i -- ^ center
|
|
-> i -- ^ radius
|
|
-> [V2 i]
|
|
circle (V2 x₀ y₀) radius
|
|
-- Four initial points, plus the generated points
|
|
= V2 x₀ (y₀ + radius)
|
|
: V2 x₀ (y₀ - radius)
|
|
: V2 (x₀ + radius) y₀
|
|
: V2 (x₀ - radius) y₀
|
|
: points
|
|
where
|
|
-- Creates the (x, y) octet offsets, then maps them to absolute points in all octets.
|
|
points = concatMap generatePoints $ unfoldr step initialValues
|
|
|
|
generatePoints (V2 x y)
|
|
= [ V2 (x₀ `xop` x') (y₀ `yop` y')
|
|
| (x', y') <- [(x, y), (y, x)]
|
|
, xop <- [(+), (-)]
|
|
, yop <- [(+), (-)]
|
|
]
|
|
|
|
initialValues = (1 - radius, 1, (-2) * radius, 0, radius)
|
|
|
|
step (f, ddf_x, ddf_y, x, y)
|
|
| x >= y = Nothing
|
|
| otherwise = Just (V2 x' y', (f', ddf_x', ddf_y', x', y'))
|
|
where
|
|
(f', ddf_y', y') | f >= 0 = (f + ddf_y' + ddf_x', ddf_y + 2, y - 1)
|
|
| otherwise = (f + ddf_x, ddf_y, y)
|
|
ddf_x' = ddf_x + 2
|
|
x' = x + 1
|
|
|
|
|
|
data FillState i
|
|
= FillState
|
|
{ _inCircle :: Bool
|
|
, _result :: NonEmpty (V2 i)
|
|
}
|
|
makeLenses ''FillState
|
|
|
|
runFillState :: NonEmpty (V2 i) -> State (FillState i) a -> [V2 i]
|
|
runFillState circumference s
|
|
= toList
|
|
. view result
|
|
. execState s
|
|
$ FillState False circumference
|
|
|
|
-- | Generate a *filled* circle centered at the given point and with the given
|
|
-- radius by filling a circle generated with 'circle'
|
|
filledCircle :: (Num i, Integral i, Ix i)
|
|
=> V2 i -- ^ center
|
|
-> i -- ^ radius
|
|
-> [V2 i]
|
|
filledCircle center radius =
|
|
case NE.nonEmpty (circle center radius) of
|
|
Nothing -> []
|
|
Just circumference -> runFillState circumference $
|
|
-- the first and last lines of all circles are solid, so the whole "in the
|
|
-- circle, out of the circle" thing doesn't work... but that's fine since
|
|
-- we don't need to fill them. So just skip them
|
|
for_ [succ minX..pred maxX] $ \x ->
|
|
for_ [minY..maxY] $ \y -> do
|
|
let pt = V2 x y
|
|
next = V2 x $ succ y
|
|
whenM (use inCircle) $ result %= NE.cons pt
|
|
|
|
when (pt `elem` circumference && next `notElem` circumference)
|
|
$ inCircle %= not
|
|
|
|
where
|
|
(V2 minX minY, V2 maxX maxY) = minmaxes circumference
|
|
|
|
-- | Draw a line between two points using Bresenham's line drawing algorithm
|
|
--
|
|
-- Code taken from <https://wiki.haskell.org/Bresenham%27s_line_drawing_algorithm>
|
|
line :: (Num i, Ord i) => V2 i -> V2 i -> [V2 i]
|
|
line pa@(V2 xa ya) pb@(V2 xb yb)
|
|
= (if maySwitch pa < maySwitch pb then id else reverse) points
|
|
where
|
|
points = map maySwitch . unfoldr go $ (x₁, y₁, 0)
|
|
steep = abs (yb - ya) > abs (xb - xa)
|
|
maySwitch = if steep then view _yx else id
|
|
[V2 x₁ y₁, V2 x₂ y₂] = sort [maySwitch pa, maySwitch pb]
|
|
δx = x₂ - x₁
|
|
δy = abs (y₂ - y₁)
|
|
ystep = if y₁ < y₂ then 1 else -1
|
|
go (xTemp, yTemp, err)
|
|
| xTemp > x₂ = Nothing
|
|
| otherwise = Just (V2 xTemp yTemp, (xTemp + 1, newY, newError))
|
|
where
|
|
tempError = err + δy
|
|
(newY, newError) = if (2 * tempError) >= δx
|
|
then (yTemp + ystep, tempError - δx)
|
|
else (yTemp, tempError)
|
|
{-# SPECIALIZE line :: V2 Int -> V2 Int -> [V2 Int] #-}
|
|
{-# SPECIALIZE line :: V2 Word -> V2 Word -> [V2 Word] #-}
|
|
|
|
straightLine :: (Num i, Ord i) => V2 i -> V2 i -> [V2 i]
|
|
straightLine pa@(V2 xa _) pb@(V2 _ yb) = line pa midpoint ++ line midpoint pb
|
|
where midpoint = V2 xa yb
|
|
|
|
delaunay
|
|
:: (Ord n, Fractional n)
|
|
=> NonEmpty (V2 n, p)
|
|
-> [((V2 n, p), (V2 n, p))]
|
|
delaunay
|
|
= map (over both fromPoint)
|
|
. Geometry.edgesAsPoints
|
|
. Geometry.delaunayTriangulation
|
|
. map toPoint
|
|
where
|
|
toPoint (V2 px py, pid) = Geometry.Point2 px py :+ pid
|
|
fromPoint (Geometry.Point2 px py :+ pid) = (V2 px py, pid)
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
renderBooleanGraphics :: forall i. (Num i, Ord i, Enum i) => [V2 i] -> String
|
|
renderBooleanGraphics [] = ""
|
|
renderBooleanGraphics (pt : pts') = intercalate "\n" rows
|
|
where
|
|
rows = row <$> [minX..maxX]
|
|
row x = [minY..maxY] <&> \y -> if V2 x y `member` ptSet then 'X' else ' '
|
|
(V2 minX minY, V2 maxX maxY) = minmaxes pts
|
|
pts = pt :| pts'
|
|
ptSet :: Set (V2 i)
|
|
ptSet = setFromList $ toList pts
|
|
|
|
showBooleanGraphics :: forall i. (Num i, Ord i, Enum i) => [V2 i] -> IO ()
|
|
showBooleanGraphics = putStrLn . pack . renderBooleanGraphics
|
|
|
|
minmaxes :: forall i. (Ord i) => NonEmpty (V2 i) -> (V2 i, V2 i)
|
|
minmaxes xs =
|
|
( V2 (minimum1Of (traverse1 . _x) xs)
|
|
(minimum1Of (traverse1 . _y) xs)
|
|
, V2 (maximum1Of (traverse1 . _x) xs)
|
|
(maximum1Of (traverse1 . _y) xs)
|
|
)
|