packages feed

nano-ui-diagrams-0.1.0.0: lib/NanoUI/Diagrams/Tessellation.hs

-- | Polygon triangulation and filling, polyline stroking, and cubic Bezier
-- flattening for the diagrams backend.
module NanoUI.Diagrams.Tessellation
  ( triangulatePolygon
  , fillPolygon
  , strokePolyline
  , flattenCubic
  ) where

import Control.Monad (forM_)
import Control.Monad.ST (runST)
import Data.Primitive.PrimArray
  ( PrimArray
  , indexPrimArray
  , newPrimArray
  , readPrimArray
  , runPrimArray
  , sizeofPrimArray
  , writePrimArray
  )
import NanoUI (Color, DrawOp (..), Rect (..))

bezierTolerance :: Float
bezierTolerance = 0.5

triangulatePolygon :: [(Float, Float)] -> [((Float, Float), (Float, Float), (Float, Float))]
triangulatePolygon [] = []
triangulatePolygon [_] = []
triangulatePolygon pts0 =
  let pts = stripClosed pts0
    in earClip (pointsArray pts)

stripClosed :: [(Float, Float)] -> [(Float, Float)]
stripClosed [] = []
stripClosed [p] = [p]
stripClosed (p : rest)
  | p == last rest = p : init rest
  | otherwise = p : rest

-- | Points stored as x then y.
pointsArray :: [(Float, Float)] -> PrimArray Float
pointsArray pts = runPrimArray $ do
  out <- newPrimArray (2 * length pts)
  let fill !_ [] = pure out
      fill !i ((x, y) : rest) = do
        writePrimArray out (2 * i) x
        writePrimArray out (2 * i + 1) y
        fill (i + 1) rest
  fill 0 pts

{-# INLINE pointAt #-}
pointAt :: PrimArray Float -> Int -> (Float, Float)
pointAt vs i = (indexPrimArray vs (2 * i), indexPrimArray vs (2 * i + 1))

signedArea :: PrimArray Float -> Float
signedArea vs =
  let n = sizeofPrimArray vs `div` 2
   in foldl' (\acc i -> acc + cross (pointAt vs i) (pointAt vs ((i + 1) `mod` n)) / 2) 0 [0 .. n - 1]

cross :: (Float, Float) -> (Float, Float) -> Float
cross (x0, y0) (x1, y1) = x0 * y1 - x1 * y0

diff :: (Float, Float) -> (Float, Float) -> (Float, Float)
diff (x0, y0) (x1, y1) = (x1 - x0, y1 - y0)

isConvex :: Bool -> (Float, Float) -> (Float, Float) -> (Float, Float) -> Bool
isConvex ccw a b c =
  let ab = diff a b
      bc = diff b c
   in if ccw then cross ab bc >= 0 else cross ab bc <= 0

pointInTri :: (Float, Float) -> (Float, Float) -> (Float, Float) -> (Float, Float) -> Bool
pointInTri p a b c =
  let sign (p1, p2, p3) = cross (diff p1 p3) (diff p2 p3)
      d1 = sign (p, a, b)
      d2 = sign (p, b, c)
      d3 = sign (p, c, a)
   in not ((d1 < 0 || d2 < 0 || d3 < 0) && (d1 > 0 || d2 > 0 || d3 > 0))

earClip :: PrimArray Float -> [((Float, Float), (Float, Float), (Float, Float))]
earClip vs
  | n < 3 = []
  | n == 3 = [(at 0, at 1, at 2)]
  | otherwise = runST $ do
      let !ccw = signedArea vs >= 0
      -- Coordinates never move. Remove an ear by relinking two neighbours,
      -- instead of copying the remaining coordinates at every step.
      prevs <- newPrimArray n
      nexts <- newPrimArray n
      forM_ [0 .. n - 1] $ \i -> do
        writePrimArray prevs i ((i - 1 + n) `mod` n)
        writePrimArray nexts i ((i + 1) `mod` n)
      let triangle i = do
            p <- readPrimArray prevs i
            q <- readPrimArray nexts i
            pure (p, q, (at p, at i, at q))
          isEarAt first count i p q (a, b, c)
            | not (isConvex ccw a b c) = pure False
            | otherwise = outside first count
            where
              outside !_ 0 = pure True
              outside !j !left
                | j /= p && j /= i && j /= q && pointInTri (at j) a b c = pure False
                | otherwise = do
                    next <- readPrimArray nexts j
                    outside next (left - 1)
          convex !_ 0 = pure True
          convex !i !left = do
            (_, q, (a, b, c)) <- triangle i
            if isConvex ccw a b c then convex q (left - 1) else pure False
          fan origin i left
            | left <= 0 = pure []
            | otherwise = do
                q <- readPrimArray nexts i
                rest <- fan origin q (left - 1)
                pure ((at origin, at i, at q) : rest)
          go !first !count !idx !tries tris
            | count == 3 = do
                second <- readPrimArray nexts first
                third <- readPrimArray nexts second
                pure ((at first, at second, at third) : tris)
            | tries >= count = do
                isConvexRing <- convex first count
                if isConvexRing then do
                  second <- readPrimArray nexts first
                  rest <- fan first second (count - 2)
                  pure (tris ++ rest)
                else pure tris
            | otherwise = do
                (p, q, tri) <- triangle idx
                ear <- isEarAt first count idx p q tri
                if ear then do
                  writePrimArray nexts p q
                  writePrimArray prevs q p
                  let !first' = if idx == first then q else first
                  go first' (count - 1) first' 0 (tri : tris)
                else go first count q (tries + 1) tris
      reverse <$> go 0 n 0 0 []
  where
    n = sizeofPrimArray vs `div` 2
    at = pointAt vs

fillPolygon :: Color -> [(Float, Float)] -> [DrawOp]
fillPolygon col pts =
  case axisAlignedRect pts of
    Just r -> [FillRect r col]
    Nothing ->
      [ FillTriangle x0 y0 x1 y1 x2 y2 col
      | ((x0, y0), (x1, y1), (x2, y2)) <- triangulatePolygon pts
      ]

axisAlignedRect :: [(Float, Float)] -> Maybe Rect
axisAlignedRect pts =
  case stripClosed pts of
    [(x0, y0), (x1, y1), (x2, y2), (x3, y3)]
      | near y0 y1 && near x1 x2 && near y2 y3 && near x3 x0 ->
          Just (Rect (min x0 x3) (min y0 y2) (abs (x1 - x0)) (abs (y2 - y0)))
    [(x0, y0), (x1, y1), (x2, y2), (x3, y3)]
      | near x0 x1 && near y1 y2 && near x2 x3 && near y3 y0 ->
          Just (Rect (min x0 x2) (min y0 y1) (abs (x2 - x0)) (abs (y1 - y0)))
    _ -> Nothing
  where
    near a b = abs (a - b) <= 1e-3


strokePolyline :: Color -> Float -> Bool -> [(Float, Float)] -> [DrawOp]
strokePolyline _ _ _ [] = []
strokePolyline _ _ _ [_] = []
strokePolyline col w closed pts0 =
  let pts = if closed && length pts0 > 2 then stripClosed pts0 else pts0
      hw = w / 2
      !vPts = pointsArray pts
      !n = sizeofPrimArray vPts `div` 2
   in if n < 2
        then []
        else
          let !segCount = if closed then n else n - 1
              -- Each segment's unit normal, x then y.
              !segNormals = runPrimArray $ do
                out <- newPrimArray (2 * segCount)
                forM_ [0 .. segCount - 1] $ \i -> do
                  let !(p0x, p0y) = pointAt vPts i
                      !(p1x, p1y) = pointAt vPts ((i + 1) `mod` n)
                      nx = p0y - p1y
                      ny = p1x - p0x
                      d = sqrt (nx * nx + ny * ny)
                  writePrimArray out (2 * i) (if d <= 1e-9 then 0 else nx / d)
                  writePrimArray out (2 * i + 1) (if d <= 1e-9 then 0 else ny / d)
                pure out
              joinNormal !i
                | not closed && i <= 0 = pointAt segNormals 0
                | not closed && i >= n - 1 = pointAt segNormals (segCount - 1)
                | otherwise =
                    let (ax, ay) = pointAt segNormals ((i - 1 + segCount) `mod` segCount)
                        (bx, by) = pointAt segNormals (i `mod` segCount)
                        sx = ax + bx
                        sy = ay + by
                        d = sqrt (sx * sx + sy * sy)
                     in if d <= 1e-9 then (0, 0) else (sx / d, sy / d)
              -- Adjacent quads share a vertex, so offset each vertex once: the
              -- two sides' points, four numbers a vertex.
              !offsets = runPrimArray $ do
                out <- newPrimArray (4 * n)
                forM_ [0 .. n - 1] $ \i -> do
                  let (!px, !py) = pointAt vPts i
                      (!nx, !ny) = joinNormal i
                  writePrimArray out (4 * i) (px + hw * nx)
                  writePrimArray out (4 * i + 1) (py + hw * ny)
                  writePrimArray out (4 * i + 2) (px - hw * nx)
                  writePrimArray out (4 * i + 3) (py - hw * ny)
                pure out
              offset i k = indexPrimArray offsets (4 * i + k)
              buildQuads !i
                | i >= segCount = []
                | otherwise =
                    let !j = if closed then (i + 1) `mod` n else i + 1
                     in FillTriangle (offset i 0) (offset i 1) (offset j 0) (offset j 1) (offset j 2) (offset j 3) col
                          : FillTriangle (offset i 0) (offset i 1) (offset j 2) (offset j 3) (offset i 2) (offset i 3) col
                          : buildQuads (i + 1)
           in buildQuads 0

flattenCubic ::
  (Float, Float) ->
  (Float, Float) ->
  (Float, Float) ->
  (Float, Float) ->
  [(Float, Float)]
flattenCubic p0 c1 c2 p1 = go p0 c1 c2 p1
  where
    go a b c d =
      let mid (p, q) = ((fst p + fst q) / 2, (snd p + snd q) / 2)
          ab = mid (a, b)
          bc = mid (b, c)
          cd = mid (c, d)
          abbc = mid (ab, bc)
          bccd = mid (bc, cd)
          mid12 = mid (abbc, bccd)
          flat =
            let (dx, dy) = diff d a
                len = sqrt (dx * dx + dy * dy)
                dist =
                  if len <= 1e-9
                    then 0
                    else abs (cross (diff b a) (dx, dy)) / len
             in dist <= bezierTolerance
       in if flat
            then [a, d]
            else
              let rest = go mid12 bccd cd d
               in go a ab abbc mid12 ++ drop 1 rest