Files
loop/src/Geometry/Intersect.hs
T
justin 12e4a278d0 Smooth out slime splitting
There are probably possible errors from the use of cutPoly
2026-05-08 23:44:28 +01:00

411 lines
14 KiB
Haskell

{-# LANGUAGE BangPatterns #-}
--{-# LANGUAGE TupleSections #-}
{- Testing for and finding intersection points. -}
module Geometry.Intersect where
import Data.Monoid
import Data.List (sortOn)
import Control.Applicative
import Control.Lens
import Control.Monad
import Data.Maybe
import Geometry.Data
import Geometry.LHS
import Geometry.Vector
import Geometry.Vector3D
import Linear
-- | If two lines intersect, return 'Just' that point.
intersectLineLine :: Point2 -> Point2 -> Point2 -> Point2 -> Maybe Point2
{-# INLINE intersectLineLine #-}
--intersectLineLine a@(V2 x1 y1) b@(V2 x2 y2) c@(V2 x3 y3) d@(V2 x4 y4)
intersectLineLine a b c d = do
let den = detV (a-b) (c-d)
guard $ den /= 0
let t = detV (a-c) (c-d)
return $ a + (t/den) *^ (b-a)
intersectLinePlaneAlong :: Point3 -> Point3 -> Point3 -> Point3 -> Maybe Float
intersectLinePlaneAlong x y p n = do
let den = dot (y - x) n
guard $ den /= 0
return $ dot (p - x) n / den
-- this needs to be checked
intersectSegPlane :: Point3 -> Point3 -> Point3 -> Point3 -> Maybe Point3
intersectSegPlane x y p n = do
d <- intersectLinePlaneAlong x y p n
guard $ d >= 0 && d < 1
return $ x + d *^ (y - x)
intersectSegSurface ::
Point3 ->
Point3 ->
Point3 ->
Point3 ->
[(Point3, Point3)] ->
Maybe Point3
intersectSegSurface sp ep p n ss = do
xp <- intersectSegPlane sp ep p n
let f (a, b) = isNHS a b xp
guard $ all f ss
return xp
isNHS :: Point3 -> Point3 -> Point3 -> Bool
isNHS p n x = 0 > dot (p - x) n
intersectSegSegErrorTest :: Point2 -> Point2 -> Point2 -> Point2 -> Maybe Point2
{-# INLINE intersectSegSegErrorTest #-}
intersectSegSegErrorTest a b c d = case intersectSegSeg a b c d of
Nothing
| intersectSegSegFullTest a b c d ->
error $ "intersectSegSeg did not intersect" ++ show a ++ show b ++ show c ++ show d
Just x
| not $ intersectSegSegFullTest a b c d ->
error $
"intersectSegSeg did intersect" ++ show a ++ show b ++ show c ++ show d
++ " at "
++ show x
m -> m
-- | If two segments intersect, return 'Just' that point.
intersectSegSeg :: Point2 -> Point2 -> Point2 -> Point2 -> Maybe Point2
{-# INLINE intersectSegSeg #-}
intersectSegSeg (V2 x1 y1) (V2 x2 y2) (V2 x3 y3) (V2 x4 y4)
| V2 x1 y1 == V2 x2 y2 || V2 x3 y3 == V2 x4 y4 = Nothing
| den == 0 = Nothing
| den > 0 && (t' < 0 || u' < 0 || t' > den || u' > den) =
Nothing
| den < 0 && (t' > 0 || u' > 0 || t' < den || u' < den) =
Nothing
| otherwise = Just $ V2 (x1 + (x2 - x1) * t' / den) (y1 + (y2 - y1) * t' / den)
where
den = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
t' = (x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)
u' = (y1 - y2) * (x1 - x3) - (x1 - x2) * (y1 - y3)
{- | Intended to intersect a segment with a half-line-segment, ie a segment
extending infinitely in one direction.
Will intersect with the first endpoint of the segment, but NOT the second.
This is to allow sensible intersections with polygons described as lists of points.
It will also intersect with the point of the ray.
-}
intersectSegRay :: Point2 -> Point2 -> Point2 -> Point2 -> Maybe Point2
{-# INLINE intersectSegRay #-}
intersectSegRay (V2 x1 y1) (V2 x2 y2) (V2 x3 y3) (V2 x4 y4)
| den == 0 = Nothing
| den > 0 && (t' < 0 || u' < 0 || t' >= den) =
Nothing
| den < 0 && (t' > 0 || u' > 0 || t' <= den) =
Nothing
| otherwise = Just $ V2 (x1 + (x2 - x1) * t' / den) (y1 + (y2 - y1) * t' / den)
where
den = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
t' = (x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)
u' = (y1 - y2) * (x1 - x3) - (x1 - x2) * (y1 - y3)
{- | Intersect a segment with a line.
the line intersects with the first endpoint of the segment
but NOT the second
-}
intersectSegLine :: Point2 -> Point2 -> Point2 -> Point2 -> Maybe Point2
{-# INLINE intersectSegLine #-}
intersectSegLine (V2 x1 y1) (V2 x2 y2) (V2 x3 y3) (V2 x4 y4)
| den == 0 = Nothing
| den > 0 && (t < 0 || t >= den) =
Nothing
| den < 0 && (t > 0 || t <= den) =
Nothing
| otherwise = Just $ V2 (x1 + (x2 - x1) * t / den) (y1 + (y2 - y1) * t / den)
where
den = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
t = (x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)
-- | A test that should align with Just values from intersectSegSeg.
intersectSegSegFullTest ::
Point2 ->
Point2 ->
Point2 ->
Point2 ->
Bool
{-# INLINE intersectSegSegFullTest #-}
intersectSegSegFullTest x y z w =
f x y z w && f z w x y && x /= y && z /= w
&& normalizeV (x -.- y) /= normalizeV (z -.- w)
&& normalizeV (y -.- x) /= normalizeV (z -.- w)
where
f a b c d =
(not (isRHS a b c) && not (isLHS a b d))
|| (not (isLHS a b c) && not (isRHS a b d))
{- | It is not always necessary to find a point of intersection, sometimes a
test may suffice.
This should intersect on endpoints.
-}
intersectSegSegTest :: Point2 -> Point2 -> Point2 -> Point2 -> Bool
{-# INLINE intersectSegSegTest #-}
intersectSegSegTest x y z w =
f x y z w && f z w x y && x /= y && z /= w
where
f a b c d = compareLHS a b c /= compareLHS a b d
-- (not (isRHS a b c) && not (isLHS a b d))
-- || (not (isLHS a b c) && not (isRHS a b d))
-- (isRHS a b c && isLHS a b d)
-- || (isLHS a b c && isRHS a b d)
intersectSegSegPreTest ::
Point2 ->
Point2 ->
Point2 ->
Point2 ->
Maybe Point2
{-# INLINE intersectSegSegPreTest #-}
intersectSegSegPreTest a b c d
| intersectSegSegTest a b c d = myIntersectSegSeg a b c d
| otherwise = Nothing
{- | Due to floating point issues, 'intersectSegSeg'' is not always
accurate---'myIntersectSegSeg'
fixes at least some of
the problem cases by transforming the points into rationals and then doing the
intersection.
This version is, probably, slower---both testing and benchmarking should be
done.
-}
myIntersectSegSeg ::
Point2 ->
Point2 ->
Point2 ->
Point2 ->
Maybe Point2
{-# INLINE myIntersectSegSeg #-}
myIntersectSegSeg a@(V2 ax ay) b@(V2 bx by) c@(V2 cx cy) d@(V2 dx dy) = case ratIntersectLineLine a b c d of
Nothing -> Nothing
Just (V2 x y) ->
if inbetween x && inbetween' y
then Just (V2 x y)
else Nothing
where
inbetween x =
((ax <= x && x <= bx) || (bx <= x && x <= ax))
&& ((cx <= x && x <= dx) || (dx <= x && x <= cx))
inbetween' y =
((ay <= y && y <= by) || (by <= y && y <= ay))
&& ((cy <= y && y <= dy) || (dy <= y && y <= cy))
-- | Polymorphic intersection of fractional line points.
myIntersectLineLine :: (Eq a, Fractional a) => V2 a -> V2 a -> V2 a -> V2 a -> Maybe (V2 a)
{-# INLINE myIntersectLineLine #-}
myIntersectLineLine a@(V2 ax _) b c@(V2 cx _) d
| isNothing (linGrad a b) = V2 ax <$> axisInt (c *-* V2 ax 0) (d *-* V2 ax 0)
| isNothing (linGrad c d) = V2 cx <$> axisInt (a *-* V2 cx 0) (b *-* V2 cx 0)
| otherwise =
case linGrad a b ^-^ linGrad c d of
Just 0 -> Nothing
_ -> liftA2 V2 newx ((linGrad a b ^*^ newx) ^+^ axisInt a b)
where
-- (^-^) = liftA2 (-)
-- (^+^) = liftA2 (+)
(^/^) = liftA2 (/)
(^*^) = liftA2 (*)
newx = (axisInt c d ^-^ axisInt a b) ^/^ (linGrad a b ^-^ linGrad c d)
(*-*) (V2 ax' ay) (V2 bx by) = V2 (ax' - bx) (ay - by)
-- | Transforms floating points to rationals then performs line intersection.
ratIntersectLineLine :: Point2 -> Point2 -> Point2 -> Point2 -> Maybe Point2
{-# INLINE ratIntersectLineLine #-}
ratIntersectLineLine a b c d =
toNumPoint2
<$> myIntersectLineLine (toRatPoint2 a) (toRatPoint2 b) (toRatPoint2 c) (toRatPoint2 d)
where
toRatPoint2 (V2 x y) = V2 (toRational x) (toRational y)
toNumPoint2 (V2 x y) = V2 (fromRational x) (fromRational y)
{- | Round the floats within a 'Point2' to the nearest integer.
__Examples__
Rounding jumps after intervals of .5:
>>> roundPoint (0.5,0.5001)
(0.0,1.0)
but is symmetric around 0:
>>> roundPoint2 (0.5,-0.5)
(0.0,0.0)
-}
roundPoint2 :: Point2 -> Point2
roundPoint2 (V2 x y) = V2 (fromIntegral (round x :: Int)) (fromIntegral (round y :: Int))
-- | Given two points, finds the linear gradient if it is non-infinite.
linGrad :: (Eq a, Fractional a) => V2 a -> V2 a -> Maybe a
{-# INLINE linGrad #-}
linGrad (V2 x y) (V2 a b)
| x - a == 0 = Nothing
| otherwise = Just $ (y - b) / (x - a)
-- | Given two points, finds the intersection with the y axis if it exists.
axisInt :: (Eq a, Fractional a) => V2 a -> V2 a -> Maybe a
{-# INLINE axisInt #-}
axisInt p (V2 a b) = (\lg -> b - (a * lg)) <$> linGrad p (V2 a b)
-- | Placeholder, undefined.
intersectSegsSeg :: [Point2] -> Point2 -> Point2 -> Maybe Point2
intersectSegsSeg = undefined
-- | Placeholder: should intersect a segment with a bezier curve.
intersectSegBezquad :: Point2 -> Point2 -> Point2 -> Point2 -> Point2 -> [Point2]
intersectSegBezquad = undefined
{- | finds one (if any) of the points of intersection between a segment and a
polygon.
Can almost certainly be optimised.
-}
intersectSegPolyFirst :: Point2 -> Point2 -> [Point2] -> Maybe Point2
intersectSegPolyFirst a b xs = foldr (<|>) Nothing $ zipWith lineColl xs (tail xs ++ [head xs])
where
lineColl = intersectSegSeg a b
-- orders intersecting points according to the line
intersectLinePoly :: Point2 -> Point2 -> [Point2] -> [Point2]
intersectLinePoly a b (p:ps) = sortOn (dotV (b - a))
. catMaybes
$ zipWith (\x y -> intersectSegLine x y a b) (p:ps) (ps ++[p])
intersectLinePoly _ _ [] = error "intersectLinePoly empty polygon"
intersectRayPoly :: Point2 -> Point2 -> [Point2] -> Maybe Point2
intersectRayPoly a b (p:ps) = getFirst . mconcat $ zipWith f (p:ps) (ps++[p])
where
f x y = First $ intersectSegRay x y a b
intersectRayPoly _ _ _ = error "intersectRayPoly: polygon too small"
{- | Given a line and a point return the point on the line closest to the
point.
-}
closestPointOnLine ::
-- | First line point.
Point2 ->
-- | Second line point.
Point2 ->
-- | Point not on line.
Point2 ->
Point2
{-# INLINE closestPointOnLine #-}
closestPointOnLine !a !b !p = a +.+ u *.* (b -.- a)
where
u = closestPointOnLineParam a b p
{- | Given a line and a point return a value corresponding to how far along the
line the point is.
-}
closestPointOnLineParam ::
-- | First line point.
Point2 ->
-- | Second line point.
Point2 ->
-- | Point not on line.
Point2 ->
Float
{-# INLINE closestPointOnLineParam #-}
closestPointOnLineParam !a !b !p =
(p - a) `dotV` (b - a) / (b - a) `dotV` (b - a)
{- | Given a segment and external point, find the closest point on the segment.
clamps to the end of the segment
-}
closestPointOnSeg :: Point2 -> Point2 -> Point2 -> Point2
closestPointOnSeg segP1 segP2 p
| closestPointOnLineParam segP1 segP2 p <= 0 = segP1
| closestPointOnLineParam segP1 segP2 p >= 1 = segP2
| otherwise = closestPointOnLine segP1 segP2 p
{- | Given a segment and external point, find the closest point on the segment.
does not return closest points beyond the segment
-}
orthogonalPointOnSeg :: Point2 -> Point2 -> Point2 -> Maybe Point2
orthogonalPointOnSeg a b p
| param < 0 || param > 1 = Nothing
| otherwise = Just $ a + param *^ normalizeV (b - a)
where
param = closestPointOnLineParam a b p
inSegArea :: Point2 -> Point2 -> Point2 -> Bool
inSegArea a b c = param >= 0 && param <= dotV (b -.- a) (b -.- a)
where
param = dotV (b -.- a) (c -.- a)
intersectCircSeg :: Point2 -> Float -> Point2 -> Point2 -> (Maybe Point2,Maybe Point2)
intersectCircSeg c r s e = intersectCircLineAlong c r s e
& g
& each %~ f
where
f (Just x) | x >= 0 && x < 1 = Just $ s + x *^ (e - s)
f _ = Nothing
g Nothing = (Nothing,Nothing)
g (Just (x,y)) = (Just x,Just y)
--intersectCircSeg :: Point2 -> Float -> Point2 -> Point2 -> [Point2]
--intersectCircSeg c r a b
-- | y < 0 = []
-- | otherwise = nub $ filter (inSegArea a b) [d -.- v, d +.+ v]
-- where
-- d = closestPointOnLine a b c
-- x = dist d c
-- y = r ^ (2 :: Int) - x ^ (2 :: Int)
-- z = sqrt y
-- v = z *.* normalizeV (b -.- a)
intersectCircLineAlong :: Point2 -> Float -> Point2 -> Point2 -> Maybe (Float, Float)
intersectCircLineAlong p r x y = do
let d = y - x
f = p - x
dsc = (dot d f / dot d d) ** 2 - (dot f f - r ** 2) / dot d d
guard $ dsc > 0
let a = dot d f / dot d d
return (a - sqrt dsc, a + sqrt dsc)
intersectCircLine :: Point2 -> Float -> Point2 -> Point2 -> Maybe (Point2, Point2)
intersectCircLine c r x y =
intersectCircLineAlong c r x y
& _Just . _1 %~ f
& _Just . _2 %~ f
where
f :: Float -> Point2
f a = x + (a *.* (y - x))
intersectCylSeg :: Point3 -> Float -> Float -> Point3 -> Point3 ->
(Maybe (Point3, Point3), Maybe (Point3, Point3))
intersectCylSeg p r h s e = fromMaybe (Nothing, Nothing) $ do
(a, b) <- intersectCircLineAlong (p ^. _xy) r (s ^. _xy) (e ^. _xy)
return (f min a, f max b)
where
mtopx = intersectLinePlaneAlong s e (V3 0 0 (h + p ^. _z)) (V3 0 0 1)
mbotx = intersectLinePlaneAlong s e (V3 0 0 (p^._z)) (V3 0 0 1)
v = e - s
f mm a = do
let x = s + a *.*.* v
if x ^. _z < (h + p ^. _z) && x ^. _z > (p ^. _z)
then do
guard $ a >= 0 && a < 1
return (x, v & _xy %~ reflectInNormal (x ^. _xy - p ^. _xy))
else do
topx <- mtopx
botx <- mbotx
let d = mm topx botx
guard $ d > 0 && d < 1
let int = s + d *.*.* v
guard $ dist (int ^. _xy) (p ^. _xy) <= r
return (int, reflectInNormal (V3 0 0 1) v)
intersectCircSegTest :: Point2 -> Float -> Point2 -> Point2 -> Bool
intersectCircSegTest c r x y =
intersectSegSegTest (c + z) (c - z) x y
|| dist c x <= r
|| dist c y <= r
where
z = r *.* vNormal (normalizeV x - y)
intersectCircSegFirst :: Point2 -> Float -> Point2 -> Point2 -> Maybe Point2
intersectCircSegFirst c r a = fst . intersectCircSeg c r a