{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE TypeFamilies #-} -- | Set operations on paths. As a side effect it removes overlapping -- regions. Since `Path` is `TrailLike`, you can use these operations -- directly with any combinator which generates `Loop`s, like `circle` -- or `fromSegments`. `Line`s are discarded, only `Loop`s are -- used. If you have several paths, you can combine them with `<>` first. -- Use `toPath` if you want to convert a `Trail` or `Located` `Trail` -- to a `Path`. The `FillRule` argument determines how /insideness/ -- is calculated for the input. module Diagrams.TwoD.Path.Boolean ( -- * Operations on Paths union, difference, intersection, exclusion, -- * Operations on Paths with tolerance union', difference', intersection', exclusion', -- * Operations on Loops loopUnion, loopDifference, loopIntersection, loopExclusion,) where import Control.Lens hiding (at) import Data.Maybe import Diagrams.Located import Diagrams.Path import Diagrams.Points import Diagrams.Segment import Diagrams.Trail import Diagrams.TrailLike import Diagrams.TwoD.Path import qualified Geom2D.CubicBezier as C import Linear fillrule :: FillRule -> C.FillRule fillrule Winding = C.NonZero fillrule EvenOdd = C.EvenOdd loop2path :: Located (Trail' Loop V2 Double) -> C.ClosedPath Double loop2path t = C.ClosedPath $ go x0 y0 (lineSegments $ cutLoop $ unLoc t) where (P (V2 x0 y0)) = loc t go :: Double -> Double -> [Segment Closed V2 Double] -> [(C.DPoint, C.PathJoin Double)] go _ _ [] = [] go x y (Linear (OffsetClosed (V2 x3 y3)):r) = (C.Point x y, C.JoinLine) : go (x+x3) (y+y3) r go x y (Cubic (V2 x1 y1) (V2 x2 y2) (OffsetClosed (V2 x3 y3)):r) = (C.Point x y, C.JoinCurve (C.Point (x+x1) (y+y1)) (C.Point (x+x2) (y+y2))) : go (x+x3) (y+y3) r path2loop :: C.ClosedPath Double -> Located (Trail' Loop V2 Double) path2loop (C.ClosedPath []) = fromSegments [] `at` origin path2loop (C.ClosedPath ((C.Point x0 y0, join):r)) = fromSegments (go x0 y0 join r) `at` P (V2 x0 y0) where go x y C.JoinLine [] = [straight (V2 (x0-x) (y0-y))] go x y C.JoinLine ((C.Point x2 y2, join'):r') = straight (V2 (x2-x) (y2-y)): go x2 y2 join' r' go x y (C.JoinCurve (C.Point x1 y1) (C.Point x2 y2)) r' = case r' of [] -> [bezier3 (V2 (x1-x) (y1-y)) (V2 (x2-x) (y2-y)) (V2 (x0-x) (y0-y))] ((C.Point x3 y3, join'):r'') -> bezier3 (V2 (x1-x) (y1-y)) (V2 (x2-x) (y2-y)) (V2 (x3-x) (y3-y)) : go x3 y3 join' r'' trail2loop :: Located (Trail V2 Double) -> Maybe (Located (Trail' Loop V2 Double)) trail2loop = located (withTrail (const Nothing) Just) offsetMax :: Offset c V2 Double -> Double offsetMax (OffsetClosed (V2 m n)) = max (abs m) (abs n) offsetMax OffsetOpen = 0 segmentMax :: Segment c V2 Double -> Double segmentMax (Linear o) = offsetMax o segmentMax (Cubic (V2 a b) (V2 c d) o) = maximum [offsetMax o, abs a, abs b, abs c, abs d] loopMax :: Trail' Loop V2 Double -> Double loopMax l = maximum (segmentMax lastSeg: map segmentMax segs) where (segs, lastSeg) = loopSegments l defaultTol :: Double defaultTol = 1e-7 loop2trail :: Located (Trail' Loop V2 Double) -> Located (Trail V2 Double) loop2trail = over located wrapLoop -- | Remove overlapping regions in the path. If you have several -- paths, combine them using `<>` first. -- -- <> -- -- > import Diagrams.TwoD.Path.Boolean -- > import Diagrams.Prelude hiding (union) -- > -- > unionEx = frame 0.1 . strokePath $ union Winding $ -- > (square 1) <> circle 0.5 # translate (V2 0.5 (-0.5)) union :: FillRule -> Path V2 Double -> Path V2 Double union fill p = Path $ map loop2trail $ loopUnion tol fill loops where loops = mapMaybe trail2loop $ pathTrails p tol = maximum (map (loopMax.unLoc) loops) * defaultTol -- | Intersection of two paths. First overlap is removed in the two -- input arguments, then the intersection is calculated. -- -- <> -- -- > import Diagrams.TwoD.Path.Boolean -- > import Diagrams.Prelude hiding (intersection) -- > -- > isectEx = frame 0.1 . strokePath $ -- > intersection Winding (square 1) $ -- > circle 0.5 # translate (V2 0.5 (-0.5)) intersection :: FillRule -> Path V2 Double -> Path V2 Double -> Path V2 Double intersection fill path1 path2 = Path $ map loop2trail $ loopIntersection tol fill loops1 loops2 where loops1 = mapMaybe trail2loop $ pathTrails path1 loops2 = mapMaybe trail2loop $ pathTrails path2 tol = max (maximum (map (loopMax.unLoc) loops1)) (maximum (map (loopMax.unLoc) loops2)) * defaultTol -- | Difference of two paths. First overlap is removed in the two -- input arguments, then the difference is calculated. -- -- <> -- -- > import Diagrams.TwoD.Path.Boolean -- > import Diagrams.Prelude hiding (difference) -- > -- > diffEx = frame 0.1 . strokePath $ -- > difference Winding (square 1) $ -- > circle 0.5 # translate (V2 0.5 (-0.5)) difference :: FillRule -> Path V2 Double -> Path V2 Double -> Path V2 Double difference fill path1 path2 = Path $ map loop2trail $ loopDifference tol fill loops1 loops2 where loops1 = mapMaybe trail2loop $ pathTrails path1 loops2 = mapMaybe trail2loop $ pathTrails path2 tol = max (maximum (map (loopMax.unLoc) loops1)) (maximum (map (loopMax.unLoc) loops2)) * defaultTol -- | Exclusion (exclusive or) of two paths. First overlap is removed in the two -- input arguments, then the exclusion is calculated. -- -- <> -- -- > import Diagrams.TwoD.Path.Boolean -- > -- > exclEx = fc grey . frame 0.1 . strokePath $ -- > exclusion Winding (square 1) $ -- > circle 0.5 # translate (V2 0.5 (-0.5)) exclusion :: FillRule -> Path V2 Double -> Path V2 Double -> Path V2 Double exclusion fill path1 path2 = Path $ map loop2trail $ loopExclusion tol fill loops1 loops2 where loops1 = mapMaybe trail2loop $ pathTrails path1 loops2 = mapMaybe trail2loop $ pathTrails path2 tol = max (maximum (map (loopMax.unLoc) loops1)) (maximum (map (loopMax.unLoc) loops2)) * defaultTol -- | Like `union`, but takes a tolerance parameter. union' :: Double -> FillRule -> Path V2 Double -> Path V2 Double union' tol fill p = Path $ map loop2trail $ loopUnion tol fill $ mapMaybe trail2loop $ pathTrails p -- | Like `intersection`, but takes a tolerance parameter. intersection' :: Double -> FillRule -> Path V2 Double -> Path V2 Double -> Path V2 Double intersection' tol fill path1 path2 = Path $ map loop2trail $ loopIntersection tol fill (mapMaybe trail2loop $ pathTrails path1) (mapMaybe trail2loop $ pathTrails path2) -- | Like `difference`, but takes a tolerance parameter. difference' :: Double -> FillRule -> Path V2 Double -> Path V2 Double -> Path V2 Double difference' tol fill path1 path2 = Path $ map loop2trail $ loopDifference tol fill (mapMaybe trail2loop $ pathTrails path1) (mapMaybe trail2loop $ pathTrails path2) -- | Like `exclusion`, but takes a tolerance parameter. exclusion' :: Double -> FillRule -> Path V2 Double -> Path V2 Double -> Path V2 Double exclusion' tol fill path1 path2 = Path $ map loop2trail $ loopExclusion tol fill (mapMaybe trail2loop $ pathTrails path1) (mapMaybe trail2loop $ pathTrails path2) -- | Union of a list of loops. loopUnion :: Double -> FillRule -> [Located (Trail' Loop V2 Double)] -> [Located (Trail' Loop V2 Double)] loopUnion tol fill p = map path2loop $ C.union (map loop2path p) (fillrule fill) tol -- | Difference between loops. The loops in both lists are first merged using `union`. loopDifference :: Double -> FillRule -> [Located (Trail' Loop V2 Double)] -> [Located (Trail' Loop V2 Double)] -> [Located (Trail' Loop V2 Double)] loopDifference tol fill path1 path2 = map path2loop $ C.difference (map loop2path path1) (map loop2path path2) (fillrule fill) tol -- | Intersection of loops. The loops in both lists are first merged using `union`. loopIntersection :: Double -> FillRule -> [Located (Trail' Loop V2 Double)] -> [Located (Trail' Loop V2 Double)] -> [Located (Trail' Loop V2 Double)] loopIntersection tol fill path1 path2 = map path2loop $ C.intersection (map loop2path path1) (map loop2path path2) (fillrule fill) tol -- | Exclusion (xor) of loops. The loops in both lists are first merged using `union`. loopExclusion :: Double -> FillRule -> [Located (Trail' Loop V2 Double)] -> [Located (Trail' Loop V2 Double)] -> [Located (Trail' Loop V2 Double)] loopExclusion tol fill path1 path2 = map path2loop $ C.exclusion (map loop2path path1) (map loop2path path2) (fillrule fill) tol