packages feed

not-gloss-0.7.0.1: src/Vis/Vis.hs

{-# OPTIONS_GHC -Wall #-}

module Vis.Vis ( Options(..)
               , vis
               , FullState
               ) where

import Data.Maybe ( fromMaybe )
import Data.IORef ( newIORef )
import System.Exit ( exitSuccess )
import Data.Time.Clock ( getCurrentTime, diffUTCTime, addUTCTime )
import Control.Concurrent ( MVar, readMVar, swapMVar, newMVar, takeMVar, putMVar, forkIO, threadDelay )
import Control.Monad ( unless, forever )
import Graphics.UI.GLUT ( Capability(..), ClearBuffer(..), Color4(..), ColorMaterialParameter(..)
                        , ComparisonFunction(..), Cursor(..), DisplayMode(..), Face(..)
                        , Key(..), KeyState(..), Light(..), Modifiers(..), Position(..)
                        , ShadingModel(..), Size(..)
                        , DisplayCallback, ReshapeCallback
                        , ($=)
                        )
import qualified Graphics.UI.GLUT as GLUT
import Graphics.Rendering.OpenGL.Raw

import Vis.VisObject ( VisObject(..), drawObjects, setPerspectiveMode )
import qualified Vis.GlossColor as GC

-- | user state and internal states
type FullState a = (a, Float)

data Options =
  Options
  { -- ^ optional background color
    optBackgroundColor :: Maybe GC.Color
    -- ^ optional (x,y) window size in pixels
  , optWindowSize :: Maybe (Int,Int)
    -- ^ optional (x,y) window origin in pixels
  , optWindowPosition :: Maybe (Int,Int)
    -- ^ window name
  , optWindowName :: String
  } deriving Show

myGlInit :: Options -> IO ()
myGlInit opts = do
  GLUT.initialDisplayMode $= [ DoubleBuffered, RGBAMode, WithDepthBuffer ]

  Size x y <- GLUT.get GLUT.screenSize
  putStrLn $ "screen resolution " ++ show x ++ "x" ++ show y
  let intScale d i = round $ d*(realToFrac i :: Double)
      x0 = intScale 0.3 x
      xf = intScale 0.95 x
      y0 = intScale 0.05 y
      yf = intScale 0.95 y

      (xsize, ysize) = fromMaybe (xf - x0, yf - y0) (optWindowSize opts)
      (xpos, ypos) = fromMaybe (x0,y0) (optWindowPosition opts)

  GLUT.initialWindowSize $= Size (fromIntegral xsize) (fromIntegral ysize)
  GLUT.initialWindowPosition $= Position (fromIntegral xpos) (fromIntegral ypos)
  _ <- GLUT.createWindow (optWindowName opts)

  case optBackgroundColor opts of
    Nothing  -> GLUT.clearColor $= Color4 0 0 0 0
    Just col -> GLUT.clearColor $= Color4 (realToFrac r) (realToFrac g) (realToFrac b) (realToFrac a)
      where
        (r,g,b,a) = GC.rgbaOfColor col
  GLUT.shadeModel $= Smooth
  GLUT.depthFunc $= Just Less
  GLUT.lighting $= Enabled
  GLUT.light (Light 0) $= Enabled
  GLUT.ambient (Light 0) $= Color4 1 1 1 1
   
  GLUT.materialDiffuse Front $= Color4 0.5 0.5 0.5 1
  GLUT.materialSpecular Front $= Color4 1 1 1 1
  GLUT.materialShininess Front $= 100
  GLUT.colorMaterial $= Just (Front, Diffuse)

  glEnable gl_BLEND
  glBlendFunc gl_SRC_ALPHA gl_ONE_MINUS_SRC_ALPHA

drawScene :: MVar (FullState a) -> MVar Bool -> IO () -> (FullState a -> IO ()) -> DisplayCallback
drawScene stateMVar visReadyMVar setCameraFun userDrawFun = do
   GLUT.clear [ ColorBuffer, DepthBuffer ]
   
   -- draw the scene
   GLUT.preservingMatrix $ do
     -- set the camera's position and orientation
     setCameraFun
     
     -- call user function
     state <- readMVar stateMVar
     userDrawFun state

   GLUT.flush
   GLUT.swapBuffers
   _ <- swapMVar visReadyMVar True
   GLUT.postRedisplay Nothing


reshape :: ReshapeCallback
reshape size@(Size _ _) = do
   GLUT.viewport $= (Position 0 0, size)
   setPerspectiveMode
   GLUT.loadIdentity
   GLUT.postRedisplay Nothing


vis :: Real b =>
       Options -- ^ user options
       -> Double -- ^ sample time
       -> a   -- ^ initial state
       -> (FullState a -> IO a)             -- ^ sim function
       -> (FullState a -> IO (VisObject b, Maybe Cursor)) -- ^ draw function, can give a different cursor
       -> (a -> IO ())                      -- ^ set camera function
       -> Maybe (a -> Key -> KeyState -> Modifiers -> Position -> a) -- ^ keyboard/mouse callback
       -> Maybe (a -> Position -> a)              -- ^ motion callback
       -> Maybe (a -> Position -> a)              -- ^ passive motion callback
       -> IO ()
vis opts ts x0 userSimFun userDraw userSetCamera
  userKeyMouseCallback userMotionCallback userPassiveMotionCallback = do
  -- init glut/scene
  _ <- GLUT.getArgsAndInitialize
  
  myGlInit opts
   
  -- create internal state
  let fullState0 = (x0, 0)
  stateMVar <- newMVar fullState0
  visReadyMVar <- newMVar False

  -- start sim thread
  _ <- forkIO $ simThread stateMVar visReadyMVar userSimFun ts
  
  -- setup the callbacks
  let makePictures x = do
        (visobs,cursor') <- userDraw x
        drawObjects $ (fmap realToFrac) visobs
        case cursor' of Nothing -> return ()
                        Just cursor'' -> GLUT.cursor $= cursor''

      setCamera = do
        (state,_) <- readMVar stateMVar
        userSetCamera state

      -- kill sim thread when someone hits ESC
      exitOverride k0 k1 k2 k3 = case (k0,k1) of
        (Char '\27', Down) -> exitSuccess
        _ -> case userKeyMouseCallback of
          Nothing -> return ()
          Just cb -> do
            (state0',time) <- takeMVar stateMVar
            putMVar stateMVar (cb state0' k0 k1 k2 k3, time)
            GLUT.postRedisplay Nothing

      motionCallback' pos = case userMotionCallback of
        Nothing -> return ()
        Just cb -> do
          (state0',ts') <- takeMVar stateMVar
          putMVar stateMVar (cb state0' pos, ts')
          GLUT.postRedisplay Nothing

      passiveMotionCallback' pos = case userPassiveMotionCallback of
        Nothing -> return ()
        Just cb -> do
          (state0',ts') <- takeMVar stateMVar
          putMVar stateMVar (cb state0' pos, ts')
          GLUT.postRedisplay Nothing

  GLUT.displayCallback $= drawScene stateMVar visReadyMVar setCamera makePictures
  GLUT.reshapeCallback $= Just reshape
  GLUT.keyboardMouseCallback $= Just exitOverride
  GLUT.motionCallback $= Just motionCallback'
  GLUT.passiveMotionCallback $= Just passiveMotionCallback'

  -- start main loop
  GLUT.mainLoop

simThread :: MVar (FullState a) -> MVar Bool -> (FullState a -> IO a) -> Double -> IO ()
simThread stateMVar visReadyMVar userSimFun ts = do
  let waitUntilDisplayIsReady :: IO ()
      waitUntilDisplayIsReady = do 
        visReady <- readMVar visReadyMVar
        unless visReady $ do
          threadDelay 10000
          waitUntilDisplayIsReady
  
  waitUntilDisplayIsReady
  
  t0 <- getCurrentTime
  lastTimeRef <- newIORef t0

  forever $ do
    -- calculate how much longer to sleep before taking a timestep
    currentTime <- getCurrentTime
    lastTime <- GLUT.get lastTimeRef
    let usRemaining :: Int
        usRemaining = round $ 1e6*(ts - realToFrac (diffUTCTime currentTime lastTime))
        secondsSinceStart = realToFrac (diffUTCTime currentTime t0)
    
    if usRemaining <= 0
      -- slept for long enough, do a sim iteration
      then do
        lastTimeRef $= addUTCTime (realToFrac ts) lastTime

        let getNextState = do
              state <- readMVar stateMVar
              userSimFun state
            putState x = swapMVar stateMVar (x, secondsSinceStart)

        nextState <- getNextState
        _ <- nextState `seq` putState nextState

        GLUT.postRedisplay Nothing
       
      -- need to sleep longer
      else threadDelay usRemaining