-- SGdemo; a demonstration of the SG library
-- Copyright (C) 2009, Neil Brown
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see .
-- | This program serves as a demonstration of the functions of the SG library
-- (to be found on Hackage:
-- )) by
-- visualising it with OpenGL. It also serves as a sort of informal test
-- suite for the SG library.
--
-- The demonstration has a persistent state of placed shapes, lines and points,
-- which you can interact with.
--
-- There are four different modes of operation:
--
-- * Place shape (press 's'). Your cursor will shown you the shape to be placed.
-- You can alter the shape type by scrolling your mouse-wheel (for those without
-- mouse wheels, the up and down cursor keys can be used). Intersections with
-- lines are shown by black crosses. If you intersect another shape, black
-- lines will indicate the results of the overlap function, drawn from the centre
-- of each shape.
--
-- * Place line (press 'l'). At first you have a cross cursor. You can left-click
-- to place the start of the line there. Then, your cursor shows a prospective
-- line from the start point. Intersections with shapes are shown with black crosses.
-- A purple line will be drawn from each point to the line, indicating the shortest
-- path from the line to each point. Intersections with other lines are shown with
-- a black circle, and a blue line indicates the output of the reflectAgainstIfNeeded2
-- function, reflecting the prospective line against the pre-existing-line-as-surface-normal.
--
-- * Place point (press 'p'). Your cursor becomes a dashed circle, and left-clicking
-- will place a point there permanently. Points are mainly useful when you later
-- switch to line mode.
--
-- * Rotate (press 'r'). Your cursor indicates a straight line from the centre.
-- If you left-click, that line will become the new upwards vertical, and everything
-- in the world will be rotated to match.
--
-- If in doubt, take a look at the code -- that's what it's there for!
module Main (main) where
import Data.SG
import Control.Arrow
import Control.Monad
import Data.IORef
import Data.Maybe
import Graphics.Rendering.OpenGL hiding (Polygon, multMatrix)
import Graphics.UI.GLUT hiding (cursor, Polygon, Solid, multMatrix)
import qualified Graphics.Rendering.OpenGL as GL
import Prelude hiding (lines)
-- Define our own versions of the SG types using the openGL double type:
type Point2 = Point2' GLdouble
type Rel2 = Rel2' GLdouble
type Line2 = Line2' GLdouble
type Shape = Shape' GLdouble
-- Allow for easy conversion from a Point2 type into an openGL 2D vertex:
instance IsomorphicVectors Point2' Vertex2 where
iso (Point2 (x, y)) = Vertex2 x y
-- Helper function for drawing a point as an openGL vertex
vertex' :: Point2 -> IO ()
vertex' v = vertex (iso v :: Vertex2 GLdouble)
-- Draws a cross at the given point:
drawCross, drawSmallCircle :: Point2 -> IO ()
drawCross p = renderPrimitive Lines $ do
-- First line:
vertex' $ p `plusDir` makeRel2 (-0.01, 0.01)
vertex' $ p `plusDir` makeRel2 (0.01, -0.01)
-- Second line:
vertex' $ p `plusDir` makeRel2 (0.01, 0.01)
vertex' $ p `plusDir` makeRel2 (-0.01, -0.01)
drawSmallCircle p = renderPrimitive GL.LineStrip $ drawCircle (p, 0.01)
-- Just draws the points; you choose the primitive outside!
-- Draws the first point at the end again (effectively).
drawCircle :: (Point2, GLdouble) -> IO ()
drawCircle (c, r) = mapM_ vertex' ps
where
ps = map (c `plusDir`) $ regularPoints 32 r
-- Includes the first point twice, effectively. Gives back N points spaced equally
-- around the origin with the given radius. So a low number (e.g. 3) will serve
-- as a regular polygon; higher numbers begin to approximate a circle.
regularPoints :: Integer -> GLdouble -> [Rel2]
regularPoints numPoints r
= reverse antiClockwisePoints -- We need clockwise points for polygons
where
angleToDir t = scaleRel r $ makeRel2 (cos t, sin t)
antiClockwisePoints = map angleToDir angles
-- Equally spaced points, starting at 90 degrees (straight-up) and going all
-- the way around.:
angles = [(2*pi * (t / fromInteger numPoints)) + (pi/2)
| t <- [0 .. fromInteger numPoints]]
-- We draw shapes empty when considering placing them, and solid when placed:
data DrawAs = Solid | WireFrame
drawShape :: DrawAs -> Shape -> IO ()
drawShape Solid (Circle c r)
= renderPrimitive GL.TriangleFan $ vertex' c >> drawCircle (c, r)
drawShape WireFrame (Circle c r)
= renderPrimitive GL.LineStrip $ drawCircle (c, r)
drawShape Solid s = renderPrimitive GL.Polygon $ mapM_ vertex' $ shapePoints s
drawShape WireFrame s = renderPrimitive GL.LineLoop $ mapM_ vertex' $ shapePoints s
-- Draws a line, with a cross at the start:
drawLine, drawLineNoCross :: Line2 -> IO ()
drawLine l = drawCross (getLineStart l) >> drawLineNoCross l
drawLineNoCross l = renderPrimitive Lines $ do
vertex' (getLineStart l)
vertex' (getLineEnd l)
-- Current mode we are in:
data Mode = PlaceShape Integer | PlaceLine (Maybe Point2) | PlacePoint | Rotate
deriving (Show)
-- Makes a shape. Pass numbers in the range 2 or higher, where 2 is a circle:
makeShape :: Integer -> Point2 -> Shape
makeShape 2 p = Circle p 0.03
makeShape 4 p = Rectangle p (0.05, 0.03)
makeShape n p = Polygon p $ init $ regularPoints n 0.04
data WorldState = WorldState
{ shapes :: [Shape]
, lines :: [Line2]
, points :: [Point2]
, cursor :: Point2
, curMode :: Mode
}
deriving (Show)
rotateWorld :: GLdouble -> WorldState -> WorldState
rotateWorld a w = w
{ shapes = [(rotateShape a s) {shapeCentre = rot (shapeCentre s)}
| s <- shapes w]
, lines = [uncurry makeLine $ rot *** rot $ getLineVecs l
| l <- lines w]
, points = map rot $ points w
}
where
rot :: (IsomorphicVectors Pair p, IsomorphicVectors p Pair) => p GLdouble -> p GLdouble
rot = multMatrix m
m :: Matrix22' GLdouble
m = rotateZaxis a
startWorld :: WorldState
startWorld = WorldState [] [] [] origin PlacePoint
-- Like mapMaybe, but keeps the original alongside the mapped version:
mapMaybe' :: (a -> Maybe b) -> [a] -> [(a, b)]
mapMaybe' f xs = [(x, y) | (x, Just y) <- map (id &&& f) xs]
-- Short for in-range-zero-to-one:
zo :: GLdouble -> Bool
zo x = 0 <= x && x <= 1
-- Given a shape, draws stuff for anything that shape might intersect with:
drawAllIntersectShape :: Shape -> WorldState -> IO ()
drawAllIntersectShape s w
-- Draw everything that might intersect with a shape: other shapes, lines
= do let intersectionsWithLines
= [map (`alongLine` l) -- Turn them back into points
$ filter zo [a,b] -- Only those in bounds
| (l, (a, b)) <- mapMaybe' (`intersectLineShape` s) $ lines w]
currentColor $= slColour
sequence_ [drawCross p | p <- concat intersectionsWithLines]
currentColor $= ssColour
sequence_ [do drawLine (makeLine (shapeCentre s) a)
drawLine (makeLine (shapeCentre s') b)
| (s', (a, b)) <- mapMaybe' (overlap s) $ shapes w]
-- Given a line, draws stuff for anything that line might intersect with:
drawAllIntersectLine :: Line2 -> WorldState -> IO ()
drawAllIntersectLine l w
-- Draw everything that might intersect with a line: shapes, other lines (TODO points)
= do let intersectionsWithShapes
= [map (`alongLine` l) -- Turn them back into points
$ filter zo [a,b] -- Only those in bounds
| (s, (a, b)) <- mapMaybe' (l `intersectLineShape`) $ shapes w]
currentColor $= slColour
sequence_ [drawCross p | p <- concat intersectionsWithShapes]
sequence_ [do let intersectPoint = a `alongLine` l
reflectedDir = getLineDir l `reflectAgainstIfNeeded2` getLineDir l'
currentColor $= llColour
drawSmallCircle intersectPoint
currentColor $= reflectColour
drawLineNoCross $ makeLine intersectPoint reflectedDir
| (l', (a, b)) <- mapMaybe' (intersectLines2 l) $ lines w
, zo a -- Must be in bounds
, zo b -- of both lines
]
currentColor $= lpColour
-- Draws points that are exactly on the line. Note that this is unlikely
-- to happen, even if it looks like it should. Also, this is indistinguishable
-- (at the moment) from the nearest point case below.
sequence_ [drawCross p | p <- points w, p `isOnLine` l]
sequence_ [let dist = (p `nearestDistOnLine` l)
in when (zo dist) $ drawLine $ (dist `alongLine` l) `lineTo` p
| p <- points w]
-- Draws everything in the world:
drawWorld :: WorldState -> IO ()
drawWorld w = do currentColor $= drawnColour
mapM_ (drawShape Solid) $ shapes w
mapM_ drawLine $ lines w
mapM_ (renderPrimitive GL.LineStrip . drawCircle . (id &&& const 0.01)) $ points w
currentColor $= draftColour
-- Draw cursor according to current mode (e.g. shapes)
case (curMode w) of
PlaceShape n -> do let s = makeShape n (cursor w)
drawShape WireFrame s
currentColor $= Color4 0.2 0.2 0.5 0
drawAllIntersectShape s w
PlaceLine (Just p) -> do let l = cursor w `lineFrom` p
drawLine l
currentColor $= Color4 0.2 0.2 0.5 0
drawAllIntersectLine l w
PlaceLine Nothing -> drawCross (cursor w)
Rotate -> drawLineNoCross $ cursor w `lineFrom` origin
-- All other modes:
-- Deliberately leave gaps in cursor by drawing as lines:
_ -> renderPrimitive GL.Lines $ drawCircle (cursor w, 0.02)
currentColor $= textColour
drawStatusText (text w)
text :: WorldState -> [String]
text w = (case curMode w of
PlaceShape n -> ["SHAPE: " ++ shapeName n
,"Mousewheel: change shape"
,"Left click: place shape"]
PlaceLine Nothing -> ["LINE", "Left click: place start"]
PlaceLine (Just _) -> ["LINE", "Left click: place end"]
PlacePoint -> ["POINT", "Left click: place point"]
Rotate -> ["ROTATE", "Left click: set new vertical"]
) ++ modes
where
shapeName 2 = "Circle"
shapeName 3 = "Triangle"
shapeName 4 = "Rectangle"
shapeName n = show n ++ "-sided"
modes = ["s: Shape Mode"
,"l: Line Mode"
,"p: Point Mode"
,"r: Rotate Mode"
]
-- Draws the given status text.
drawStatusText :: [String] -> IO ()
drawStatusText strLines = preservingMatrix $ do
matrixMode $= Modelview 0
loadIdentity
let toDouble = fromInteger . toInteger
let targetWidth = 0.4
origWidth <- liftM (maximum . map toDouble) $ mapM (stringWidth Roman) strLines
let sc = targetWidth / origWidth
lineWidth $= 2
-- lineSmooth $= Enabled
sequence_ [ do translate $ Vector3 0.1 (0.2 + (150 * sc * n)) 0
scale sc sc (1 :: GLdouble)
renderString Roman s
loadIdentity
| (n, s) <- zip [0..] $ reverse strLines]
draftColour, drawnColour, reflectColour, slColour, ssColour, llColour, lpColour,
textColour :: Color4 GLclampf
textColour = Color4 0 0 0.5 0
-- For things relating to the cursor:
draftColour = Color4 1 0 0 0
-- For things now in the world properly:
drawnColour = Color4 1 0.25 0.5 0
-- For reflective lines:
reflectColour = Color4 0 0 1 0
-- For shapes intersecting lines:
slColour = Color4 0 0 0 0
-- For shapes intersecting shapes:
ssColour = slColour
-- For lines intersecting lines:
llColour = slColour
-- For lines nearest to points:
lpColour = Color4 0.75 0.25 0.75 0
processWorld :: Key -> WorldState -> WorldState
processWorld k ws = case (k, curMode ws) of
-- Mode changes:
(Char 'l', _) -> ws { curMode = PlaceLine Nothing }
(Char 'p', _) -> ws { curMode = PlacePoint }
(Char 's', _) -> ws { curMode = PlaceShape 2}
(Char 'r', _) -> ws { curMode = Rotate}
-- Point placement:
(MouseButton LeftButton, PlacePoint)
-> ws { points = cursor ws : points ws }
-- Shape placement:
(MouseButton WheelUp, PlaceShape 2)
-> ws
(MouseButton WheelUp, PlaceShape n)
-> ws { curMode = PlaceShape $ pred n }
(MouseButton WheelDown, PlaceShape n)
-> ws { curMode = PlaceShape $ succ n }
(SpecialKey KeyUp, PlaceShape 2)
-> ws
(SpecialKey KeyUp, PlaceShape n)
-> ws { curMode = PlaceShape $ pred n }
(SpecialKey KeyDown, PlaceShape n)
-> ws { curMode = PlaceShape $ succ n }
(MouseButton LeftButton, PlaceShape n)
-> ws { shapes = makeShape n (cursor ws) : shapes ws }
-- Line placement:
(MouseButton LeftButton, PlaceLine Nothing)
-> ws { curMode = PlaceLine $ Just $ cursor ws }
(MouseButton LeftButton, PlaceLine (Just p))
-> ws { curMode = PlaceLine Nothing
, lines = (cursor ws `lineFrom` p) : lines ws }
-- Rotation:
(MouseButton LeftButton, Rotate)
-> rotateWorld (negate (toAngle (cursor ws `fromPt` origin)) + (pi/2)) ws
-- Anything else:
_ -> ws
glRunAs2D :: IO () -> IO ()
glRunAs2D draw = do
matrixMode $= Modelview 0
loadIdentity
matrixMode $= Projection
loadIdentity
ortho (-0.5) 0.5 (-0.5) 0.5 (-1000) 1000
preservingMatrix draw
main :: IO ()
main = do
world <- newIORef startWorld
-- Create the window:
initialWindowSize $= Size 500 500
getArgsAndInitialize
initialDisplayMode $= [DoubleBuffered]
_window <- createWindow "SGdemo"
-- Register callbacks:
let translate :: Position -> IO Point2
translate (Position x y)
= do (_, Size w h) <- get viewport
return (Point2 ( (conv x / conv w) - 0.5
, negate (conv y / conv h) + 0.5))
conv = fromInteger . toInteger
moveCursor p = do w <- readIORef world
writeIORef world $ w { cursor = p }
passiveMotionCallback $= Just (\pos ->
translate pos >>= moveCursor >> postRedisplay Nothing)
keyboardMouseCallback $= Just (\k st _ pos ->
do translate pos >>= moveCursor
when (st == Down) $
modifyIORef world $ processWorld k
postRedisplay Nothing)
displayCallback $= (glRunAs2D $ do
clearColor $= Color4 1 1 1 1
lineWidth $= 1
lineSmooth $= Disabled
clear [ColorBuffer, DepthBuffer]
readIORef world >>= drawWorld
flush
swapBuffers)
mainLoop