{-# LANGUAGE NamedFieldPuns #-} -- | A simple datatype and parser for 3D OFF files. A single type is -- used for OFF information with or without color. Loosely based on -- , but uses the -- file header to determine whether the object's faces contain color -- values. module Graphics.Formats.OFF.Simple ( OFF (..) , Face (..) , vertexCount , faceCount , hasColor , parseOFF , readOFFFile ) where import Control.Applicative hiding ( (<|>), many, optional ) import qualified Data.Vector as V import Data.Vector ( Vector ) import Numeric import Text.Parsec import Text.Parsec.String ( Parser ) -- | A vertex is just a triple of 'Double's. type Vertex = (Double, Double, Double) -- | A color is a 4-tuple of 'Double's representing RGB values. type Color = (Double, Double, Double) -- | A face is a vector of vertex indices and an optional color value. data Face = Face (Vector Int) (Maybe Color) deriving (Show, Eq, Ord) -- | Representation of an object in OFF format; a pair of vectors -- containing the vertices and the faces of the object. data OFF = OFF { vertices :: Vector Vertex , faces :: Vector Face } deriving (Show, Eq, Ord) -- | The number of vertices in an 'OFF' object. vertexCount :: OFF -> Int vertexCount (OFF { vertices }) = V.length vertices -- | The number of faces in an 'OFF' object. faceCount :: OFF -> Int faceCount (OFF { faces }) = V.length faces -- | Returns 'True' if the 'OFF' object has color values associated -- with its faces. hasColor :: OFF -> Bool hasColor (OFF { faces }) = case faces V.!? 0 of Just (Face _ (Just _)) -> True _ -> False -- | Determines whether we're handling color files. parseHeader :: Parser Bool parseHeader = string "OFF" *> (char 'C' *> return True <|> return False) -- | Comments span from @#@ to the end of the line. parseComment :: Parser () parseComment = char '#' >> manyTill anyChar newline >> return () -- | Parse the vertex and face counts. parseCounts :: Parser (Int, Int) parseCounts = do vc <- parseInt spaces fc <- parseInt optional (many (oneOf "\t ") >> parseInt) -- edges ignored return (vc, fc) "vertex, face, and edge count" -- | Parse a line of vertex coordinates. parseVertex :: Parser Vertex parseVertex = do [x, y, z] <- count 3 (parseDouble <* spaces) return (x, y, z) "x, y, z coordinates" -- | Parse the given number of vertex lines parseVertices :: Int -> Parser (Vector Vertex) parseVertices n = V.replicateM n (parseVertex <* eatWhitespace) show n ++ " vertices" -- | Parse a line of non-colored vertex indices defining a face. parseFace :: Parser Face parseFace = do numVerts <- parseInt spaces verts <- V.replicateM numVerts (parseInt <* spaces) return $ Face verts Nothing "vertex indices" -- | First parse the indices of a face, then parse three additional -- 'Double's representing the color value. parseFaceC :: Parser Face parseFaceC = do (Face verts Nothing) <- parseFace [r, g, b] <- count 3 (parseDouble <* spaces) return $ Face verts (Just (r, g, b)) "3 color components" -- | Parse the given number of non-colored faces. parseFaces :: Int -> Parser (Vector Face) parseFaces n = V.replicateM n (parseFace <* eatWhitespace) show n ++ " faces" -- | Parse the given number of colored faces. parseFacesC :: Int -> Parser (Vector Face) parseFacesC n = V.replicateM n (parseFaceC <* eatWhitespace) show n ++ " faces" -- | Parse a 'Text' string representing an OFF object. parseOFF :: Parser OFF parseOFF = do eatWhitespace isColor <- parseHeader eatWhitespace (numVerts, numFaces) <- parseCounts eatWhitespace verts <- parseVertices numVerts eatWhitespace faces <- if isColor then parseFacesC numFaces else parseFaces numFaces eatWhitespace >> eof return $ OFF verts faces -- | Read an OFF object from the given 'FilePath', returning either -- the corresponding 'OFF' value or a 'ParseError'. readOFFFile :: FilePath -> IO (Either ParseError OFF) readOFFFile f = parse parseOFF f <$> readFile f -- | Parse and discard whitespace and comments up until the next -- non-comment, non-whitespace character. eatWhitespace :: Parser () eatWhitespace = try (spaces >> parseComment >> eatWhitespace) <|> spaces <|> return () -- | Parse an unsigned decimal 'Int'. parseInt :: Parser Int parseInt = do s <- getInput case readDec s of [(n, s')] -> n <$ setInput s' _ -> empty -- | Parse a signed 'Double'. parseDouble :: Parser Double parseDouble = do s <- getInput case readSigned readFloat s of [(n, s')] -> n <$ setInput s' _ -> empty