packages feed

linden-0: src/Linden/RuleDSL.hs

{-# LANGUAGE OverloadedStrings, DeriveGeneric #-}
module Linden.RuleDSL (
   RuleContext(..), RuleDSL, compileRules
 , addOption
 , doNothing, deleteMe, addBranch, addBranches
 , alreadyExists, noDup, nodeCount, maxNodes
 , curBranch, gaiaPathSyms, angleSum, angleToVertical
 , distance, distanceSq, curChildren
 , lightCones, lightHits, lightAng, lightIntensity
 , colorTemp
 , materialChildrenSyms, directSiblings
 , weighted, readBranchSym, attachLocation, allLights
 -- To suppress warnings about constructors used for TH naming
 , pX, pY, gRoot, gAttach
 , Point(..), Grips(..)
 ) where

import Data.Char
import Data.Maybe
import Data.Tree
import Data.Tree.Zipper
import Data.Bifunctor
import Data.Biapplicative
import Data.Random.RVar
import Data.Random.Distribution.Categorical
import Control.Monad.Identity
import Control.Monad.Reader
import Control.Monad.Writer.Strict
import Control.Monad.Supply (MonadSupply(..))
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import Data.Aeson ((.:))
import qualified Data.Aeson as JS
import qualified Data.Text as T
import GHC.Generics
import System.FilePath
import Data.Digest.Pure.SHA (sha1, showDigest)
import qualified Data.Map.Lazy as Map
import Data.Hashable (hash)

import Debug.Trace

import Linden.Types

data Point =
    Point { pX :: Int, pY :: Int }
  deriving (Read, Show, Eq, Ord, Generic)

instance JS.ToJSON Point where
  toJSON (Point x y) = JS.object [("x", JS.toJSON x), ("y", JS.toJSON y)]

instance JS.FromJSON Point where
  parseJSON (JS.Object o) =
    Point <$> o .: "x" <*> o .: "y"
  parseJSON _ = mzero

data Grips =
  Grips {
    gRoot :: Point
  , gAttach :: [Point]
  }
  deriving (Read, Show, Eq, Ord, Generic)

instance JS.ToJSON Grips where
  toJSON (Grips r a) = JS.object [("attach", JS.toJSON a), ("root", JS.toJSON r)]

instance JS.FromJSON Grips where
  parseJSON (JS.Object o) =
    Grips <$> o .: "root" <*> o .: "attach"
  parseJSON _ = mzero

unPoint :: Point -> (Int, Int)
unPoint (Point x y) = (x, y)

readBranchSym :: [Int] -> FilePath -> ReaderT FilePath IO BranchSym
readBranchSym = readBranchSym'

readBranchSym' :: [Int] -> FilePath -> ReaderT FilePath IO BranchSym
readBranchSym' cls fp = do
  bp <- ask
  imfl <- liftIO $ BSL.readFile (bp </> "tiff" </> (addExtension fp "tif"))
  let imhash = take 16 . showDigest . sha1 $ imfl
  let gfl = bp </> "assets" </> (take 16 imhash++".json")
  gjson <- liftIO $ BS.readFile gfl
  case JS.eitherDecodeStrict' gjson of
    Left err -> fail $ "Failed to parse json of "++fp++" because "++show err
    Right (Grips (Point rX rY) as) ->
        return $ BranchSym (Just . T.pack $ addExtension fp "png")
                           (rX, rY) (map unPoint as) False False (map hash cls)

weighted :: [(Double, LEnv EditCommand)] -> RVar (LEnv EditCommand)
weighted = weightedCategorical

data RuleContext =
    RC {
      rcLights :: [Light]
    , rcAttachLocs :: BranchPossition
    , rcTreePos :: TreePos Full Branch
    }
  deriving (Read, Show, Eq)

type RuleDSL a = ReaderT RuleContext (WriterT [(Double, LEnv EditCommand)] RVar) a

compileRules :: RuleDSL () -> Rule
compileRules act bps lights tp =
    weighted =<< (execWriterT . runReaderT act $ RC lights bps tp)

curBranch :: RuleDSL Branch
curBranch = label <$> asks rcTreePos

curChildren :: RuleDSL Int
curChildren = (length . offspring) <$> asks rcTreePos

directSiblings :: RuleDSL Int
directSiblings =
  (fromMaybe 0 . fmap ((1-) . length . subForest . tree) . parent) <$> asks rcTreePos

-- | Finds if a command adds an edge that already exists.
alreadyExists :: LEnv EditCommand -> RuleDSL Bool
alreadyExists mc = do
  let c = runLEnv mc (Supply 0) -- We're an ID-independant comparison.
  case fst c of
    DeleteMe -> return False
    DoNothing -> return False
    AddChildren nsf -> do
      esf <- offspring <$> asks rcTreePos
      return . or $ [ ((bAngle nc) == (bAngle ec)) && ((bImg nc) == (bImg ec))
                    | nc <- rootLabel <$> nsf, ec <- fmap label esf]

nodeCount :: RuleDSL Int
nodeCount = (length . flatten . toTree) <$> asks rcTreePos

maxNodes :: Int -> RuleDSL () -> RuleDSL ()
maxNodes mx act = do
  nc <- nodeCount
  case nc of
    l | l >= mx -> doNothing 1
    _ -> act

isMaterial :: RuleDSL Bool
isMaterial = (not . isJust . bProxyFor . label) <$> asks rcTreePos

materialChildrenSyms :: RuleDSL [Branch]
materialChildrenSyms = do
    tp <- asks rcTreePos
    im <- isMaterial
    case im of
      -- We're material, so only our children can be immaterial.
      True -> return $ searchDown . tree $ tp
      False -> return $ maybe [] searchDown . findMaterial $ tp
  where
    findMaterial :: TreePos Full Branch -> Maybe (Tree Branch)
    findMaterial tp =
      case parent tp of
        Nothing -> Nothing
        Just tp' | (isJust . bProxyFor . label $ tp') -> findMaterial tp'
        Just tp' -> Just . tree $ tp'
    searchDown :: Tree Branch -> [Branch]
    searchDown (Node b sf) =
      case isJust . bProxyFor $ b of
        True -> concatMap searchDown sf
        False -> [b]

noDup :: RuleDSL a -> RuleDSL a
noDup act = do
    rc <- ask
    (a, w) <- lift . lift . runWriterT . runReaderT act $ rc
    forM_ w $ \(weight, ec) -> do
      d <- alreadyExists ec
      unless d $ tell [(weight, ec)]
    return a

addOption :: Double -> LEnv EditCommand -> RuleDSL ()
addOption w ec = tell [(w, ec)]

doNothing :: Double -> RuleDSL ()
doNothing w = addOption w . return $ DoNothing

deleteMe :: Double -> RuleDSL ()
deleteMe w = do
  im <- (not . bImmutable) <$> curBranch
  when im $ addOption w . return $ DeleteMe

explodeSym :: BranchSym -> Angle -> Scale -> Double -> LEnv (Tree Branch)
explodeSym (BranchSym img r [attach] im rgd cls) a s wv = do
  i <- supply
  return $ Node (Branch i img r attach a s im rgd cls Nothing wv) []
explodeSym (BranchSym img r attachs im rgd cls) a s wv = do
  i <- supply
  sf <- forM attachs $ \at -> do
     i' <- supply
     return $ Node (Branch i' Nothing (0, 0) at 0 1 im True cls (Just i) 0) []
  return $ Node (Branch i img r (0, 0) a s im rgd cls Nothing wv) sf

addBranch :: Double -> BranchSym -> Angle -> Scale -> Double -> RuleDSL ()
addBranch w bs a s wv =
  addOption w ((AddChildren . pure) <$> explodeSym bs a s wv)

addBranches :: Double -> [(BranchSym, Angle, Scale, Double)] -> RuleDSL ()
addBranches w as = addOption w $ do
  cs <- forM as $ \(bs, a, s, wv) -> do
         explodeSym bs a s wv
  return . AddChildren $ cs

gaiaPathSyms :: RuleDSL [Branch]
gaiaPathSyms = (map (\(_, p, _) -> p) . parents) <$> asks rcTreePos

angleToVertical :: RuleDSL Angle
angleToVertical = negate <$> angleSum

angleSum :: RuleDSL Angle
angleSum = (absAngle . sum . map bAngle) <$> gaiaPathSyms

absAngle :: Angle -> Angle
absAngle d = d + (fromIntegral $ (ceiling ((negate d) / 360.0)::Integer)) * 360.0

lightAng :: (X, Y) -> Light -> Angle
lightAng p l =
  let (dx, dy) = ((-), (-)) <<*>> p <<*>> (bimap fromIntegral fromIntegral $ lPos l)
  in absAngle $ (180/pi) * atan2 dy dx

lightHits :: (X, Y) -> Light -> Bool
lightHits p l =
    let ang = lightAng p l
    in (abs $ ang-(absAngle $ lPointAngle l)) <= (lBeamAngle l)/2

attachLocation :: RuleDSL (X, Y)
attachLocation =
    (fst . fromJust) <$> (Map.lookup <$> (bId <$> curBranch) <*> (asks rcAttachLocs))

allLights :: RuleDSL [Light]
allLights = asks rcLights

lightCones :: RuleDSL [Light]
lightCones = do
  p <- attachLocation
  filter (lightHits p) <$> asks rcLights

distance :: (X, Y) -> (Int, Int) -> Double
distance a b = sqrt . distanceSq a $ b

distanceSq :: (X, Y) -> (Int, Int) -> Double
distanceSq (x0, y0) (x1', y1') =
    ((x0-x1)^(2::Int))+((y0-y1)^(2::Int))
  where
    x1 = fromIntegral x1'
    y1 = fromIntegral y1'

colorTemp :: RuleDSL Double
colorTemp = do
  lcs <- lightCones
  return $ if (null lcs)
           then -10
           else (sum . map lTemp $ lcs) / (fromIntegral $ length lcs)

lightIntensity :: RuleDSL Double
lightIntensity = do
  p <- attachLocation
  hitting <- lightCones
  let intenses = map (\l -> (10^(4::Int))*(360/(lBeamAngle l)) / (distanceSq p (lPos l))) hitting
  return . sum $ 0:intenses