diagrams-lib-1.6: src/Diagrams/TwoD/Segment.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- Orphan Traced instances for Segment Closed V2 and FixedSegment V2.
-- They can't go in Traced; but they shouldn't really go in
-- Diagrams.Segment either because we only have Traced instances for
-- the special case of R2.
-- |
-- Module : Diagrams.TwoD.Segment
-- Copyright : (c) 2012 diagrams-lib team (see LICENSE)
-- License : BSD-style (see LICENSE)
-- Maintainer : diagrams-discuss@googlegroups.com
--
-- Segments in two dimensions are special since we may meaningfully
-- compute their point of intersection with a ray.
module Diagrams.TwoD.Segment (
-- * Segment intersections
intersectPointsS,
intersectPointsS',
-- * Closest point on a segment
closestPoint,
closestPoint',
closestDistance,
closestDistance',
closestParam,
closestParam',
-- ** Low level functions
segmentSegment,
lineSegment,
)
where
import Control.Lens hiding (at, contains, transform, (#))
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NE
import Data.Maybe
import Diagrams.Core
import Diagrams.Direction
import Diagrams.Located
import Diagrams.Parametric
import Diagrams.Segment
import Diagrams.TwoD.Points
import Diagrams.TwoD.Segment.Bernstein
import Diagrams.TwoD.Transform
import Diagrams.TwoD.Types hiding (p2)
import Diagrams.TwoD.Vector
import Linear.Affine
import Linear.Metric
{- All instances of Traced should maintain the invariant that the list of
traces is sorted in increasing order.
-}
instance OrderedField n => Traced (Segment Closed V2 n) where
getTrace = getTrace . mkFixedSeg . (`at` origin)
instance OrderedField n => Traced (FixedSegment V2 n) where
getTrace seg = mkTrace $ \p v ->
mkSortedList . map (view _1) $ lineSegment defEps (v `at` p) seg
defEps :: Fractional n => n
defEps = 1e-8
-- | Compute the intersections between two fixed segments.
intersectPointsS :: OrderedField n => FixedSegment V2 n -> FixedSegment V2 n -> [P2 n]
intersectPointsS = intersectPointsS' defEps
-- | Compute the intersections between two segments using the given tolerance.
intersectPointsS' :: OrderedField n => n -> FixedSegment V2 n -> FixedSegment V2 n -> [P2 n]
intersectPointsS' eps s1 s2 = map (view _3) $ segmentSegment eps s1 s2
-- | Get the closest distance(s) from a point to a 'FixedSegment'.
closestDistance :: OrderedField n => FixedSegment V2 n -> P2 n -> [n]
closestDistance = closestDistance' defEps
-- | Get the closest distance(s) from a point to a 'FixedSegment' within given
-- tolerance.
closestDistance' :: OrderedField n => n -> FixedSegment V2 n -> P2 n -> [n]
closestDistance' eps seg p = map (distanceA p) $ closestPoint' eps seg p
-- | Get the closest point(s) on a 'FixedSegment' from a point.
closestPoint :: OrderedField n => FixedSegment V2 n -> P2 n -> [P2 n]
closestPoint = closestPoint' defEps
-- | Get the closest point(s) on a 'FixedSegment' from a point within given
-- tolerance.
closestPoint' :: OrderedField n => n -> FixedSegment V2 n -> P2 n -> [P2 n]
closestPoint' eps seg = map (seg `atParam`) . closestParam' eps seg
-- | Find the closest value(s) on the Bêzier to the given point.
closestParam :: OrderedField n => FixedSegment V2 n -> P2 n -> [n]
closestParam = closestParam' defEps
-- | Find the closest value(s) on the Bêzier to the given point within given
-- tolerance.
closestParam' :: OrderedField n => n -> FixedSegment V2 n -> P2 n -> [n]
closestParam' _ (FLinear p0 p1) p
| t < 0 = [0]
| t > 1 = [1]
| otherwise = [t]
where
vp = p .-. p0
v = p1 .-. p0
dp = vp `dot` v
t = dp / quadrance v
closestParam' eps cb (P (V2 px py)) = bezierFindRoot eps poly 0 1
where
(bx, by) = bezierToBernstein cb
bx' = bernsteinDeriv bx
by' = bernsteinDeriv by
poly =
(bx - listToBernstein [px, px, px, px]) * bx'
+ (by - listToBernstein [py, py, py, py]) * by'
------------------------------------------------------------------------
-- Low level
------------------------------------------------------------------------
-- | Return the intersection points with the parameters at which each segment
-- intersects.
segmentSegment :: OrderedField n => n -> FixedSegment V2 n -> FixedSegment V2 n -> [(n, n, P2 n)]
segmentSegment eps s1 s2 =
case (s1, s2) of
(FCubic {}, FCubic {}) ->
map (\(t1, t2) -> (t1, t2, s1 `atParam` t1)) $
bezierClip eps s1 s2
(FCubic {}, FLinear {}) -> map flip12 $ linearSeg (segLine s2) s1
_ -> linearSeg (segLine s1) s2 -- s1 is linear
where
linearSeg l s = filter (inRange . view _1) $ lineSegment eps l s
flip12 (a, b, c) = (b, a, c)
-- | Return the intersection points with the parameters at which the line and segment
-- intersect.
lineSegment :: OrderedField n => n -> Located (V2 n) -> FixedSegment V2 n -> [(n, n, P2 n)]
lineSegment _ l1 p@(FLinear p0 p1) =
map (\(tl, tp) -> (tl, tp, p `atParam` tp))
. filter (inRange . snd)
. maybeToList
$ lineLine l1 (mkLine p0 p1)
lineSegment eps (viewLoc -> (p, r)) cb = map addPoint params
where
params = bezierFindRoot eps (listToBernstein $ cb' ^.. each . _y) 0 1
cb' = transform (inv (rotationTo $ dir r)) . moveOriginTo p $ cb
--
addPoint bt = (lt, bt, intersect)
where
intersect = cb `atParam` bt
lt = (cb' `atParam` bt) ^. _x / norm r
-- Adapted from from kuribas's cubicbezier package https://github.com/kuribas/cubicbezier
-- | Use the Bêzier clipping algorithm to return the parameters at which the
-- Bêzier curves intersect.
bezierClip :: OrderedField n => n -> FixedSegment V2 n -> FixedSegment V2 n -> [(n, n)]
bezierClip eps p_ q_ =
filter (allOf both inRange) $ -- sometimes this returns NaN
go p_ q_ 0 1 0 1 0 False
where
go p q tmin tmax umin umax clip revCurves = case chopCubics p q of
Nothing -> []
-- This check happens before the subdivision
-- test to avoid non-termination as values
-- transition to within epsilon.
Just (tminChop, tmaxChop)
| max (umax - umin) (tmax' - tmin') < eps ->
if revCurves -- return parameters in correct order
then [(avg umin umax, avg tmin' tmax')]
else [(avg tmin' tmax', avg umin umax)]
-- split the curve if there isn't enough reduction
| clip > 0.8 && clip' > 0.8 ->
if tmax' - tmin' > umax - umin -- split the longest segment
then
let (pl, pr) = p' `splitAtParam` 0.5
tmid = avg tmin' tmax'
in go q pl umin umax tmin' tmid clip' (not revCurves)
++ go q pr umin umax tmid tmax' clip' (not revCurves)
else
let (ql, qr) = q `splitAtParam` 0.5
umid = avg umin umax
in go ql p' umin umid tmin' tmax' clip' (not revCurves)
++ go qr p' umid umax tmin' tmax' clip' (not revCurves)
-- iterate with the curves reversed.
| otherwise -> go q p' umin umax tmin' tmax' clip' (not revCurves)
where
p' = section p tminChop tmaxChop
clip' = tmaxChop - tminChop
tmin' = tmax * tminChop + tmin * (1 - tminChop)
tmax' = tmax * tmaxChop + tmin * (1 - tmaxChop)
-- | Find the zero of a 1D Bêzier curve of any degree. Note that this
-- can be used as a Bernstein polynomial root solver by converting from
-- the power basis to the Bernstein basis.
bezierFindRoot ::
OrderedField n =>
-- | The accuracy
n ->
-- | the Bernstein coefficients of the polynomial
BernsteinPoly n ->
-- | The lower bound of the interval
n ->
-- | The upper bound of the interval
n ->
-- | The roots found
[n]
bezierFindRoot eps p initMin initMax
-- If we generated the max number of roots and tmax is also a root
-- (which is not among the generated ones), there must in fact be an
-- infinite number of roots, so just include tmax.
| length roots == bernsteinDegree p && last roots /= initMax && abs (evaluateBernstein p initMax) <= eps =
roots ++ [initMax]
| otherwise = roots
where
-- Lazily take a number of roots at most the degree of the bernstein
-- polynomial, to avoid generating a ton of roots in the case of a
-- straight Bezier segment along the x-axis. See https://github.com/diagrams/diagrams-contrib/issues/91 .
roots = take (bernsteinDegree p) $ go p initMin initMax
go pInit tmin tmax = case chopYs (bernsteinCoeffs pInit) of
Nothing -> []
Just (tminChop, tmaxChop)
| tmax' - tmin' < eps -> [avg tmin' tmax']
| clip > 0.8 ->
let (p1, p2) = splitAtParam newP 0.5
tmid = tmin' + (tmax' - tmin') / 2
in go p1 tmin' tmid
++ go p2 tmid tmax'
| otherwise -> bezierFindRoot eps newP tmin' tmax'
where
newP = section pInit tminChop tmaxChop
clip = tmaxChop - tminChop
tmin' = tmax * tminChop + tmin * (1 - tminChop)
tmax' = tmax * tmaxChop + tmin * (1 - tmaxChop)
------------------------------------------------------------------------
-- Internal
------------------------------------------------------------------------
-- | An approximation of the fat line for a cubic Bêzier segment. Returns
-- @(0,0)@ for a linear segment.
fatLine :: OrderedField n => FixedSegment V2 n -> (n, n)
fatLine (FCubic p0 p1 p2 p3) =
case (d1 > 0, d2 > 0) of
(True, True) -> (0, 0.75 * max d1 d2)
(False, False) -> (0.75 * min d1 d2, 0)
(True, False) -> (4 / 9 * d2, 4 / 9 * d1)
(False, True) -> (4 / 9 * d1, 4 / 9 * d2)
where
d = lineDistance p0 p3
d1 = d p1
d2 = d p2
fatLine _ = (0, 0)
chopYs :: OrderedField n => NonEmpty n -> Maybe (n, n)
chopYs ds = chopHull 0 0 points
where
points = NE.zipWith mkP2 (NE.zipWith (\i _ -> fromIntegral @Integer i / fromIntegral n) (0 :| [1 ..]) ds) ds
n = NE.length ds - 1
chopCubics :: OrderedField n => FixedSegment V2 n -> FixedSegment V2 n -> Maybe (n, n)
chopCubics p q@(FCubic q0 _ _ q3) =
chopHull dmin dmax dps
where
dps = NE.zipWith mkP2 (0 :| [1 / 3, 2 / 3, 1]) ds
ds = NE.map d $ case p of
FLinear p0 p1 -> p0 :| [p1]
FCubic p0 p1 p2 p3 -> p0 :| [p1, p2, p3]
d = lineDistance q0 q3
--
(dmin, dmax) = fatLine q
chopCubics _ _ = Nothing
-- Reduce the interval which the intersection is known to lie in using the fat
-- line of one curve and convex hull of the points formed from the distance to
-- the thin line of the other
chopHull :: OrderedField n => n -> n -> NonEmpty (P2 n) -> Maybe (n, n)
chopHull dmin dmax dps = do
tL <- testBelow upper $ testBetween (NE.head upper) $ testAbove lower
tR <- testBelow (NE.reverse upper) $ testBetween (NE.last upper) $ testAbove (NE.reverse lower)
Just (tL, tR)
where
(upper, lower) = sortedConvexHull dps
testBelow (p1@(P (V2 _ y1)) :| p2@(P (V2 _ y2)) : ps) continue
| y1 >= dmin = continue
| y1 > y2 = Nothing
| y2 < dmin = testBelow (p2 :| ps) continue
| otherwise = Just $ intersectPt dmin p1 p2
testBelow _ _ = Nothing
testBetween (P (V2 x y)) continue
| y <= dmax = Just x
| otherwise = continue
testAbove (p1@(P (V2 _ y1)) :| p2@(P (V2 _ y2)) : ps)
| y1 < y2 = Nothing
| y2 > dmax = testAbove (p2 :| ps)
| y2 - y1 == 0 = Nothing -- Check this condition to prevent
-- division by zero in `intersectPt`.
| otherwise = Just $ intersectPt dmax p1 p2
testAbove _ = Nothing
-- find the x value where the line through the two points
-- intersect the line y=d. Note that `y2 - y1 != 0` due
-- to checks above.
intersectPt d (P (V2 x1 y1)) (P (V2 x2 y2)) =
x1 + (d - y1) * (x2 - x1) / (y2 - y1)
bezierToBernstein :: Fractional n => FixedSegment V2 n -> (BernsteinPoly n, BernsteinPoly n)
bezierToBernstein seg =
(listToBernstein $ map (view _x) coeffs, listToBernstein $ map (view _y) coeffs)
where
coeffs = toListOf each seg
------------------------------------------------------------------------
-- Lines
------------------------------------------------------------------------
-- Could split this into a separate module.
-- | Returns @(a, b, c, d)@ such that @ax + by + c = 0@ is the line going through
-- @p1@ and @p2@ with @(a^2)/d + (b^2)/d = 1@. We delay the division by
-- @d@ as it may not be needed in all cases and @d@ may be zero.
lineEquation :: Floating n => P2 n -> P2 n -> (n, n, n, n)
lineEquation (P (V2 x1 y1)) (P (V2 x2 y2)) = (a, b, c, d)
where
c = -(x1 * a + y1 * b)
a = y1 - y2
b = x2 - x1
d = a * a + b * b
-- | Return the distance from a point to the line.
lineDistance :: (Ord n, Floating n) => P2 n -> P2 n -> P2 n -> n
lineDistance p1 p2 p3@(P (V2 x y))
-- I have included the check that d' <= 0 in case
-- there exists some d > 0 where sqrt d == 0. I don't
-- think this can happen as sqrt is at least recommended
-- to be within one value of correct for sqrt and near
-- zero values get bigger.
| d <= 0 || d' <= 0 = norm (p1 .-. p3)
| otherwise = (a * x + b * y + c) / d'
where
(a, b, c, d) = lineEquation p1 p2
d' = sqrt d
-- clockwise :: (Num n, Ord n) => V2 n -> V2 n -> Bool
-- clockwise a b = a `cross2` b <= 0
avg :: Fractional n => n -> n -> n
avg a b = (a + b) / 2
lineLine :: (Fractional n, Eq n) => Located (V2 n) -> Located (V2 n) -> Maybe (n, n)
lineLine (viewLoc -> (p, r)) (viewLoc -> (q, s))
| x1 == 0 && x2 /= 0 = Nothing -- parallel
| otherwise = Just (x3 / x1, x2 / x1) -- intersecting or collinear
where
x1 = r × s
x2 = v × r
x3 = v × s
v = q .-. p
(×) :: Num n => V2 n -> V2 n -> n
(×) = cross2
mkLine :: InSpace v n (v n) => Point v n -> Point v n -> Located (v n)
mkLine p0 p1 = (p1 .-. p0) `at` p0
segLine :: InSpace v n (v n) => FixedSegment v n -> Located (v n)
segLine (FLinear p0 p1) = mkLine p0 p1
segLine (FCubic p0 _ _ p3) = mkLine p0 p3
-- This function uses `defEps`, but is used in functions
-- above that take an epsilon parameter. It would be nice
-- to clearify the meaning of each of these epsilons.
inRange :: (Fractional n, Ord n) => n -> Bool
inRange x = x < (1 + defEps) && x > (-defEps)