{-# LANGUAGE BangPatterns #-} -- Author: Gergely Patai -- from sloth2d: https://github.com/cobbpg/sloth2d/blob/master/Physics/Sloth2D/Geometry2D.hs -- based on Garey, Johnson, Preparata, runtime O(n log n) module Graphics.Triangulation.GJPTriangulation where import Data.List import Data.Ord import Data.Vector (Vector, (!)) import qualified Data.Vector as V import qualified Data.Vector.Algorithms.Intro as V import Graphics.Formats.Collada.Vector2D3D data VertexType = TopCap | BottomCap | TopCup | BottomCup | Side deriving Show data Vertex = Vtx { idx :: Int , prev :: Int , next :: Int , vtype :: VertexType , px :: Float , py :: Float } deriving Show type MonotoneSegment = ([Int],[Int]) -- | Descriptor for a pair of features. The ordering stands for the -- following configurations: @LT@ - V to E, @EQ@ - E to E, @GT - E to -- V, where E stands for edge and V stands for vertex (in other words, -- you can think of edges being greater than vertices). The integers -- are the indices of the features: the vertex itself or the first -- vertex (in ccw order) of the edge. For instance, @(LT,2,4)@ means -- the pair formed by vertex 2 of the first body and the edge between -- vertices 4 and 5 of the second body. type Separation = (Ordering, Int, Int) -- | Checking whether an angle is within a given interval. between :: Angle -> (Angle,Angle) -> Bool a `between` (a1,a2) | a1 <= a2 = a >= a1 && a <= a2 | otherwise = a >= a1 || a <= a2 infixl 6 +< -- | The sum of two angles. (+<) :: Angle -> Angle -> Angle a1 +< a2 = if a < -pi then a+2*pi else if a > pi then a-2*pi else a where a = a1+a2 -- | Linear interpolation between two angles along the smaller arc. alerp :: Angle -> Angle -> Float -> Angle alerp a1 a2 t = a1+<(a2+<(-a1))*t -- | Applying a binary function to consecutive pairs in a vector with -- wrap-around. pairsWith :: (a -> a -> b) -> Vector a -> Vector b pairsWith f vs | V.null vs = V.empty | otherwise = V.zipWith f vs (V.snoc (V.tail vs) (V.head vs)) -- | The edge vectors of a polygon given as a list of vertices. edges :: Vector V2 -> Vector V2 edges vs = if V.length vs < 2 then V.empty else pairsWith (flip (-)) vs -- | The absolute angles (with respect to the x axis) of the edges of -- a polygon given as a list of vertices. angles :: Vector V2 -> Vector Angle angles = V.map dir . edges -- | The signed area of a simple polygon (positive if vertices are in -- counter-clockwise order). area :: Vector V2 -> Float area vs = 0.5 * V.sum (pairsWith cross vs) -- | The centroid of a simple polygon. centroid :: Vector V2 -> V2 centroid vs | V.null vs = V 0 0 | otherwise = divsum (V.foldl1' accum (pairsWith gen vs)) where gen v1 v2 = let c = v1 `cross` v2 in (c,(v1+v2)*.c) accum (!c1,!v1) (c2,v2) = (c1+c2,v1+v2) divsum (c,v) | c /= 0 = v*.(recip (3*c)) | otherwise = (V.minimum vs+V.maximum vs)*.0.5 -- | The moment of inertia of a simple polygon with respect to the origin. moment :: Vector V2 -> Float moment vs | V.length vs < 3 = 0 | otherwise = divsum (V.foldl1' accum (pairsWith gen vs)) where gen v1 v2 = let c = v2 `cross` v1 in (c,(v1 `dot` (v1+v2) + square v2)*c) accum (!s1,!s2) (p1,p2) = (s1+p1,s2+p2) divsum (s1,s2) | s1 /= 0 = s2/(6*s1) | otherwise = 0 -- | The convex hull of a collection of vertices in counter-clockwise -- order. (Andrew's Monotone Chain Algorithm) convexHull :: Vector V2 -> Vector V2 convexHull vs = case compare (V.length vs) 2 of LT -> vs EQ -> V.fromList . nub . V.toList $ vs GT -> V.fromList (avs' ++ bvs') where svs = V.modify V.sort vs vmin = V.head svs vmax = V.last svs vd = vmax-vmin (avs,bvs) = V.partition (\v -> vd `turnNR` v-vmax) . V.init . V.tail $ svs avs' = if V.null avs then [vmin] else tail . V.foldl' (flip addVertex) [V.head avs,vmin] $ V.snoc (V.tail avs) vmax bvs' = if V.null bvs then [vmax] else tail . V.foldr' addVertex [V.last bvs,vmax] $ V.cons vmin (V.init bvs) addVertex v (v1:vs@(v2:_)) | v1-v2 `turnNR` v-v1 = addVertex v vs addVertex v vs = v:vs -- | Monotone decomposition of a simple polygon. monotoneDecomposition :: Vector V2 -> [MonotoneSegment] monotoneDecomposition vs = (map getIndices . snd) (V.foldl' addVertex ([], []) scvs) where cw = area vs < 0 ovs = if cw then vs else V.reverse vs getIndices (l,r) = if cw then (map idx l, map idx r) else (map idx' l, map idx' r) where idx' v = V.length vs - 1 - idx v addVertex (mss, out) v = case vtype v of -- open new monotone segment with this sole vertex TopCap -> (([v], [v]) : mss, out) -- split monotone segment: all vertices are added to left side, -- only last two to right; this is the only case where we need -- to check geometry to find the matching segment BottomCap -> let (mss',(msl,msr):mss'') = break isContained mss ms' = (msl, v : msr) ms'' = ([v, head msr], [head msr]) in (mss' ++ ms':ms'':mss'', out) -- close the segment on the right side using the join vertex and -- the next vertex on its other side TopCup -> let ([(msl1,msr1),(msl2,msr2)], mssr) = partition isConnected mss (msl1',msr1',msl2',msr2') = if idx v == prev (head msr1) then let i = prev (head msr2) v' = cvs ! i in (msl1, v { prev = i } : msr1, v':v:msl2, v':msr2) else let i = prev (head msr1) v' = cvs ! i in (msl2, v { prev = i } : msr2, v':v:msl1, v':msr1) in ((msl1',msr1'):mssr,(msl2',msr2'):out) -- close monotone segment (stage for emission, remove from -- active collection) BottomCup -> let (mss',(msl,msr):mss'') = break isConnected mss in (mss' ++ mss'', (v:msl,v:msr):out) -- add to the segment the upper neighbour belongs to Side -> let (mss',(msl,msr):mss'') = break isConnected mss ms' = if idx v == next (head msl) then (v:msl, msr) else (msl, v:msr) in (mss' ++ ms':mss'', out) where isConnected ((vl:_), (vr:_)) = idx v == next vl || idx v == prev vr isConnected _ = error "isConnected" isContained ((vl:_), (vr:_)) = px v > xl && px v <= xr where vl' = cvs ! (next vl) vr' = cvs ! (prev vr) xl = px vl + (px vl' - px vl) * (py v - py vl) / (py vl' - py vl) xr = px vr + (px vr' - px vr) * (py v - py vr) / (py vr' - py vr) isContained _ = error "isContained" scvs = V.modify (V.sortBy (comparing py)) cvs cvs = V.imap classify ovs classify i1 v1@(V x1 y1) = Vtx i1 i0 i2 vty x1 y1 where vty = case (compare y1 y0, compare y1 y2, v2-v1 `turn` v1-v0) of (LT, LT, LT) -> BottomCap (EQ, LT, LT) -> BottomCap (LT, LT, GT) -> TopCap (LT, EQ, GT) -> TopCap (GT, GT, GT) -> BottomCup (EQ, GT, GT) -> BottomCup (GT, GT, LT) -> TopCup (GT, EQ, LT) -> TopCup _ -> Side i0 = if i1 == 0 then V.length ovs - 1 else i1-1 i2 = if i1 == V.length ovs - 1 then 0 else i1+1 v0@(V _ y0) = ovs ! i0 v2@(V _ y2) = ovs ! i2 -- | Triangulation of a monotone polygon. monotoneTriangulation :: Vector V2 -> MonotoneSegment -> [(Int,Int,Int)] monotoneTriangulation vs (msl,msr) = snd (foldl' addVertex ([si2,si1],[]) sis) where addVertex (si@(s,i):sis,ts) si'@(s',i') | s /= s' = ([si',si], zipWith (if s' then tl else tr) (si:sis) sis ++ ts) | concave = (si':si:sis,ts) | otherwise = (si':si'':map snd si2s'', zipWith (if s' then tr else tl) sis' sis'' ++ ts) where concave = isConcave (snd (head sis)) i (si2s',si2s'') = break visible (zip (si:sis) sis) where visible ((_,i1),(_,i2)) = isConcave i2 i1 (sis',sis'') = unzip si2s' si'' = last sis'' tl (_,i1) (_,i2) = (i',i2,i1) tr (_,i1) (_,i2) = (i',i1,i2) isConcave i0 i1 = s' == v1-v0 `turnL` v2-v1 where v0 = vs ! i0 v1 = vs ! i1 v2 = vs ! i' addVertex _ _ = error "addVertex" si1:si2:sis = merge msl (init (tail msr)) merge [] irs = map ((,) True) irs merge ils [] = map ((,) False) ils merge ils@(il:ils') irs@(ir:irs') | y1 < y2 = (True,ir) : merge ils irs' | otherwise = (False,il) : merge ils' irs where V _ y1 = vs ! il V _ y2 = vs ! ir -- | Triangulation of a simple polygon. triangulation :: Vector V2 -> [(Int, Int, Int)] triangulation vs = [tri | ms <- monotoneDecomposition vs, tri <- monotoneTriangulation vs ms] -- | A 5-tuple @(d2,ds,sep,v1,v2)@ that provides distance information -- on two convex polygons, where @d2@ is the square of the distance, -- @ds@ is its sign (negative in case of penetration), @sep@ describes -- the opposing features, while @v1@ and @v2@ are the absolute -- coordinates of the deepest points within the opposite polygon. If -- the third parameter is @True@, only negative distances are -- reported, and the function yields @Nothing@ for non-overlapping -- polygons. This is more efficient if we are only interested in -- collisions, since the computation can be cancelled upon finding the -- first separating axis. If the third parameter is @False@, the -- result cannot be @Nothing@. convexSeparation :: Vector V2 -- ^ The vertices of the first polygon (vs1) -> Vector V2 -- ^ The vertices of the second polygon (vs2) -> Bool -- ^ Whether we are only interested in overlapping -> Maybe (Float, Float, Separation, V2, V2) convexSeparation vs1 vs2 onlyCollision | onlyCollision = closestPenetratingPair firstValidPair | otherwise = Just (closestPair firstValidPair) where l1 = V.length vs1 l2 = V.length vs2 succ1 n = let n' = succ n in if n' >= l1 then 0 else n' succ2 n = let n' = succ n in if n' >= l2 then 0 else n' pred1 n = if n == 0 then l1-1 else pred n pred2 n = if n == 0 then l2-1 else pred n firstValidPair = until validSeparation stepBackwards (GT,0,0) -- Exhaustive search for the closest feature pair closestPair s = go (l1+l2-1) (stepBackwards s) (s,v12) dst where (dst,v12) = separation s go 0 _ (s,(v1,v2)) (sd,sgd) = (sd,-sgd,s,v1,v2) go n s sep dst | dst < dst' = go n' (stepBackwards s) sep dst | otherwise = go n' (stepBackwards s) (s,v12) dst' where (dst',v12) = separation s n' = n-1 -- Exhaustive search for the closest penetrating feature pair closestPenetratingPair s = go (l1+l2-1) (stepBackwards s) (s,v12) dst where (dst,v12) = separation s go 0 _ (s,(v1,v2)) (sd,sgd) = Just (sd,-sgd,s,v1,v2) go n s sep dst@(_,sg) | sg < 0 = Nothing | dst < dst' = go n' (stepBackwards s) sep dst | otherwise = go n' (stepBackwards s) (s,v12) dst' where (dst',v12) = separation s n' = n-1 {- -- Step towards the next feature pair counter-clockwise stepForward (rel,i1,i2) = case rel of LT -> (turn e1 e2',i1 ,i2') EQ -> (turn e1' e2',i1',i2') GT -> (turn e1' e2 ,i1',i2 ) where i1' = succ1 i1 i2' = succ2 i2 e1 = vs1 ! i1' - vs1 ! i1 e2 = vs2 ! i2 - vs2 ! i2' e1' = vs1 ! succ1 i1' - vs1 ! i1' e2' = vs2 ! i2' - vs2 ! succ2 i2' -} -- Step towards the next feature pair clockwise stepBackwards (_,i1,i2) = case turn e2 e1 of LT -> (LT,i1 ,i2') EQ -> (EQ,i1',i2') GT -> (GT,i1',i2 ) where i1' = pred1 i1 i2' = pred2 i2 e1 = vs1 ! i1 - vs1 ! i1' e2 = vs2 ! i2' - vs2 ! i2 -- Check if the feature pair is valid (i.e. the edge lies within -- the interval defined by the vertex, or the edges are parallel) validSeparation (rel,i1,i2) = case rel of LT -> turnNR e11 e22 && turnNR e22 e12 EQ -> parv e12 e22 GT -> turnNR e21 e12 && turnNR e12 e22 where v1 = vs1 ! i1 v2 = vs2 ! i2 e11 = v1 - vs1 ! pred1 i1 e12 = vs1 ! succ1 i1 - v1 e21 = vs2 ! pred2 i2 - v2 e22 = v2 - vs2 ! succ2 i2 -- Distance information for a given feature pair separation (rel,i1,i2) = case rel of LT -> swap (s v2 v2' e2 sd2 v1) GT -> s v1 v1' e1 sd1 v2 EQ | sd1 > sd2 -> min (s v1 v1' e1 sd1 v2) (s v1 v1' e1 sd1 v2') | otherwise -> swap (min (s v2 v2' e2 sd2 v1) (s v2 v2' e2 sd2 v1')) where swap (d,(v1,v2)) = (d,(v2,v1)) v1 = vs1 ! i1 v2 = vs2 ! i2 v1' = vs1 ! succ1 i1 v2' = vs2 ! succ2 i2 e1 = v1'-v1 e2 = v2'-v2 sd1 = square e1 sd2 = square e2 -- The squared distance of the v1 to v2 segment and the v3 vertex s v1 v2 e12 sd12 v3 = ((sd,signum cp),(v,v3)) where e13 = v3-v1 e23 = v3-v2 sd12' = recip sd12 dp = e12 `dot` e13 -- negative: separation, positive: penetration cp = e12 `cross` e13 (v,sd) | dp <= 0 = (v1,square e13) | dp >= sd12 = (v2,square e23) | otherwise = (v1+e12*.(dp*sd12'),cp*cp*sd12')