packages feed

brush-stroking-0.1.0.0: src/BrushStroking/Document.hs

module BrushStroking.Document where

-- base
import GHC.Generics
  ( Generic )

-- containers
import Data.Map.Strict
  ( Map )
import qualified Data.Map.Strict as Map
import Data.Set
  ( Set )
import qualified Data.Set as Set

-- deepseq
import Control.DeepSeq
  ( NFData(..) )

-- text
import Data.Text
  ( Text )

-- brush-strokes
import Math.Linear
  ( ℝ(..), T(..) )

-- MetaBrush
import BrushStroking.Layer
  ( LayerMetadata, emptyHierarchy )
import BrushStroking.Stroke
  ( StrokeHierarchy, PointIndex )
import BrushStroking.Unique
  ( Unique )

--------------------------------------------------------------------------------

-- | Document, together with some extra metadata.
data Document
  = Document
  { documentContent  :: !DocumentContent
    -- ^ Main document content, which we keep track throughout history.
  , documentMetadata :: !DocumentMetadata
    -- ^ Metadata about the document, that we don't track throughout history.
  }
  deriving stock    ( Show, Generic )
  deriving anyclass NFData

newtype Zoom = Zoom { zoomFactor :: Double }
  deriving stock ( Show, Eq, Ord )
  deriving newtype NFData

-- | A collection of points, indexed first by the stroke they belong to
-- and then their position in that stroke.
newtype StrokePoints = StrokePoints { strokePoints :: Map Unique ( Set PointIndex ) }
  deriving newtype ( Eq, Show, NFData )
    -- Invariant: the sets are never empty.

instance Semigroup StrokePoints where
  ( StrokePoints pts1 ) <> ( StrokePoints pts2 ) =
    StrokePoints ( Map.unionWith Set.union pts1 pts2 )
instance Monoid StrokePoints where
  mempty = StrokePoints Map.empty

-- | Remove the second set of points from the first.
differenceStrokePoints :: StrokePoints -> StrokePoints -> StrokePoints
differenceStrokePoints ( StrokePoints pts1 ) ( StrokePoints pts2 ) =
  StrokePoints $
    Map.differenceWith remove pts1 pts2
      where
        remove :: Set PointIndex -> Set PointIndex -> Maybe ( Set PointIndex )
        remove old new =
          let new' = old Set.\\ new
          in if null new'
             then Nothing
             else Just new'

noStrokePoints :: StrokePoints -> Bool
noStrokePoints ( StrokePoints pts ) = null pts

elemStrokePoint :: Unique -> PointIndex -> StrokePoints -> Bool
elemStrokePoint u i ( StrokePoints pts ) =
  case Map.lookup u pts of
    Nothing -> False
    Just is -> Set.member i is

-- | Metadata about a document and its content, that we don't track through
-- history.
data DocumentMetadata =
  Metadata
    { documentName     :: !Text
    , documentFilePath :: !( Maybe FilePath )
    , viewportCenter   :: !( ℝ 2 )
    , documentZoom     :: !Zoom
    , documentSize     :: !( Maybe DocumentSize )
    , documentGuides   :: !( Map Unique Guide )
    , layerMetadata    :: !LayerMetadata
    , selectedPoints   :: !StrokePoints
    }
  deriving stock    ( Show, Generic )
  deriving anyclass NFData

data DocumentSize =
  DocumentSize
    { documentTopLeft, documentBottomRight :: !( ℝ 2 ) }
  deriving stock    ( Show, Generic )
  deriving anyclass NFData

-- | Main content of document (data which we keep track of throughout history).
data DocumentContent
  = Content
  { unsavedChanges  :: !Bool
    -- ^ Whether this current content is unsaved.
  , strokeHierarchy :: !StrokeHierarchy
    -- ^ Hierarchical structure of layers and groups.
  }
  deriving stock    ( Show, Generic )
  deriving anyclass NFData

-- | A guide, i.e. a horizontal or vertical line used for alignment.
data Guide
  = Guide
  { guidePoint  :: !( ℝ 2 )       -- ^ point on the guide line
  , guideNormal :: !( T ( ℝ 2 ) ) -- ^ /normalised/ normal vector of the guide
  }
  deriving stock    ( Show, Generic )
  deriving anyclass NFData

emptyDocument :: Text -> Document
emptyDocument docName =
  Document
    { documentContent  = emptyDocumentContent
    , documentMetadata = emptyDocumentMetadata docName
    }

emptyDocumentContent :: DocumentContent
emptyDocumentContent =
  Content
    { strokeHierarchy = emptyHierarchy
    , unsavedChanges  = False
    }

emptyDocumentMetadata :: Text -> DocumentMetadata
emptyDocumentMetadata docName =
  Metadata
    { documentName     = docName
    , documentFilePath = Nothing
    , viewportCenter   = ℝ2 0 0
    , documentZoom     = Zoom { zoomFactor = 1 }
    , documentSize     = Nothing
    , documentGuides   = Map.empty
    , layerMetadata    = mempty
    , selectedPoints   = mempty
    }