packages feed

waterfall-cad-0.7.0.0: src/Waterfall/Diagram.hs

{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE DeriveGeneric #-}
module Waterfall.Diagram
( Diagram
, getDiagramFills
, getDiagramLines
, LineType (..)
, Visibility (..)
, solidDiagram
, pathDiagram
, shapeDiagram
, diagramLines
, diagramBoundingBox
) where

import Linear.V3 (V3)
import Linear.V2 (V2)
import Linear (_xy)
import Control.Lens ((^.))
import Waterfall.Internal.Solid (Solid (solidPaintMap), acquireSolid)
import qualified OpenCascade.HLRBRep.TypeOfResultingEdge as HLRBRep
import qualified OpenCascade.HLRBRep.Algo as HLRBRep.Algo
import qualified OpenCascade.HLRAlgo.Projector as HLRAlgo.Projector
import qualified OpenCascade.HLRBRep.HLRToShape as HLRBRep.HLRToShape
import qualified OpenCascade.GP as GP
import qualified OpenCascade.GP.Ax2 as GP.Ax2
import qualified OpenCascade.Bnd.Box as Bnd.Box
import qualified OpenCascade.BRepBndLib as BRepBndLib
import Waterfall.Internal.ToOpenCascade (v3ToDir, v3ToVertex)
import Waterfall.TwoD.Internal.Path2D (Path2D (..))
import Control.Monad.IO.Class (liftIO)
import Waterfall.Internal.Finalizers (unsafeFromAcquire, acquireForeignPtr, fromAcquirePtrT)
import Waterfall.Internal.Edges (allEdges, buildEdgeCurve3D, edgeToWire)
import Waterfall.Internal.Path.Common (RawPath (..))
import Waterfall.TwoD.Transforms (Transformable2D (..))
import Waterfall.Internal.FromOpenCascade (gpPntToV3)
import OpenCascade.Inheritance (upcast)
import Control.Monad (forM_, (<=<))
import Data.Map (Map)
import Waterfall.TwoD.Internal.Shape (Shape (..))
import Waterfall.Paint (Paint)
import qualified Data.Map as M
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Either (partitionEithers)
import Waterfall.Internal.Diagram (buildSolidDiagramFills)
import Waterfall.TwoD.Shape (shapePaths)
import GHC.Generics (Generic)
import Control.DeepSeq (NFData (rnf), rwhnf)

-- | `Diagram` of a Waterfall part
--
-- Contains:
--
-- * A collection of `Path2D` indexed by `LineType` and `Visibility`
-- * A collection of pairs of `Paint` and `Shape`, representing flat regions of colour
data Diagram = Diagram
     -- | The `Diagram`'s lines
     { getDiagramLines :: Map (LineType, Visibility) [Path2D]
     -- | Flat regions of colour
     , getDiagramFills :: [(Paint, Shape)]
     } deriving Generic

instance NFData Diagram

instance Semigroup Diagram where
    a <> b = Diagram 
        { getDiagramLines = M.unionWith (<>) (getDiagramLines a) (getDiagramLines b)
        , getDiagramFills = getDiagramFills a <> getDiagramFills b
        }

instance Monoid Diagram where
    mempty = Diagram mempty mempty

mapDiagram :: (forall a. Transformable2D a => a -> a) -> Diagram -> Diagram
mapDiagram f (Diagram theLines theFills) =
    Diagram 
        { getDiagramLines = fmap (fmap f) theLines
        , getDiagramFills = fmap (fmap f) theFills
        }

instance Transformable2D Diagram where
  matTransform2D m = mapDiagram (matTransform2D m)
  rotate2D theta = mapDiagram (rotate2D theta)
  scale2D s = mapDiagram (scale2D s)
  uScale2D s = mapDiagram (uScale2D s)
  translate2D v = mapDiagram (translate2D v)
  mirror2D v = mapDiagram (mirror2D v)

-- | Categorize the lines in a diagram
data LineType = 
    -- | Represents lines at the edge of objects, the "silhouette" 
    -- 
    -- Does not include those parts of the silhouette that are also sharp
    OutLine 
    -- | Sharp edges, parts of an object with C0 Continuity
    | SharpLine
    | RawLine HLRBRep.TypeOfResultingEdge
    deriving (Eq, Ord, Show)

instance NFData LineType where
    rnf (RawLine t) = rwhnf t
    rnf other = rwhnf other

normaliseLineType :: LineType -> LineType
normaliseLineType (RawLine HLRBRep.Sharp) = SharpLine
normaliseLineType (RawLine HLRBRep.OutLine) = OutLine
normaliseLineType other = other

lineTypeToOpenCascade :: LineType -> HLRBRep.TypeOfResultingEdge
lineTypeToOpenCascade OutLine = HLRBRep.OutLine
lineTypeToOpenCascade SharpLine = HLRBRep.Sharp
lineTypeToOpenCascade (RawLine lt) = lt

-- | Whether an edge is visible in a given projection, or not
data Visibility = Visible | Hidden deriving (Eq, Ord, Show)

instance NFData Visibility where rnf = rwhnf

-- | Produce a diagram of a `Solid`
-- 
-- Uses an orthographic projection, viewed from the provided direction
solidDiagram :: V3 Double -> Solid -> Diagram
solidDiagram projectionDirection solid =  unsafeFromAcquire $ do
    s' <- acquireSolid solid
    algo <- HLRBRep.Algo.new
    liftIO $ HLRBRep.Algo.add algo s'
    o <- GP.origin
    d <- v3ToDir projectionDirection
    projector <- HLRAlgo.Projector.fromAx2 =<< GP.Ax2.newAutoX o d
    liftIO $ do 
        HLRBRep.Algo.projector algo projector
        HLRBRep.Algo.update algo
        HLRBRep.Algo.hide algo

    extractor <- HLRBRep.HLRToShape.fromAlgo algo

    theLines <- fmap M.fromList . sequence $
        [ do
            compoundOfEdges <- HLRBRep.HLRToShape.compoundOfEdges 
                extractor 
                (lineTypeToOpenCascade lt) 
                (visibility == Visible) 
                False
            rawEdges <- allEdges compoundOfEdges
            wires <- liftIO . fromAcquirePtrT $ traverse ( edgeToWire <=< buildEdgeCurve3D) rawEdges
            return ((lt, visibility), Path2D . ComplexRawPath <$> wires)

        | lt <- 
            [ OutLine
            , SharpLine
            , RawLine HLRBRep.IsoLine
            , RawLine HLRBRep.Rg1Line
            , RawLine HLRBRep.RgNLine
        ]
        , visibility <- [Visible, Hidden]
        ]

    theFills <-  buildSolidDiagramFills 
        (solidPaintMap solid) 
        s' 
        projectionDirection 
        projector
        extractor 

    return $ Diagram 
        { getDiagramLines = theLines
        , getDiagramFills = theFills
        }
        

-- | Produce a `Diagram` from a `Path2D`
-- 
-- @ diagramLines lt v . pathDiagram lt v = pure @
pathDiagram :: LineType -> Visibility -> Path2D -> Diagram
pathDiagram lt v path = Diagram
    { getDiagramLines = M.singleton (normaliseLineType lt, v) [path]
    , getDiagramFills = []
    }

-- | Produce a `Diagram` from a `Shape` and `Paint`
--
-- @ getDiagramFills . uncurry shapeDiagram = pure @
shapeDiagram :: Paint -> Shape -> Diagram
shapeDiagram paint shape = Diagram
    { getDiagramLines = mempty
    , getDiagramFills = [(paint, shape)]
    }

-- | Access the lines in a `Diagram` as `Path2D`
diagramLines :: LineType -> Visibility -> Diagram -> [Path2D]
diagramLines lt v = fromMaybe [] . M.lookup (normaliseLineType lt, v) . getDiagramLines

-- | Compute the Axis Aligned Bounding Box of a `Diagram`
-- 
-- Returns Nothing if the `Diagram` does not contain lines that are `OutLine` or `Sharp`
diagramBoundingBox :: Diagram -> Maybe (V2 Double, V2 Double)
diagramBoundingBox d = unsafeFromAcquire $ do
    let outline = diagramLines OutLine Visible d
    let sharpLine = diagramLines SharpLine Visible d
        fillLines = concatMap (shapePaths . snd) $ getDiagramFills d
    let allLines = outline <> sharpLine <> fillLines
    let partitionLine (Path2D (ComplexRawPath wire)) = Just (Left wire)
        partitionLine (Path2D (SinglePointRawPath point)) = Just (Right point)
        partitionLine _ = Nothing
        (allWires, allPoints) = partitionEithers . mapMaybe partitionLine $ allLines
    if null allWires && null allPoints
        then pure Nothing
        else do
            theBox <- Bnd.Box.new
            forM_ allWires $ \wire -> do
                w <- acquireForeignPtr wire
                liftIO $ BRepBndLib.addOptimal (upcast w) theBox True False
            forM_ allPoints $ \point -> do
                v <- v3ToVertex point
                liftIO $ BRepBndLib.addOptimal (upcast v) theBox True False

            p1 <- liftIO . gpPntToV3 =<< Bnd.Box.cornerMin theBox
            p2 <- liftIO . gpPntToV3 =<< Bnd.Box.cornerMax theBox
            return $ Just (p1 ^. _xy, p2 ^. _xy)