packages feed

implicit-0.3.0.0: programs/implicitsnap.hs

-- Implicit CAD. Copyright (C) 2011, Christopher Olah (chris@colah.ca)
-- Copyright (C) 2014 2015, Julia Longtin (julial@turinglace.com)
-- Released under the GNU AGPLV3+, see LICENSE

-- FIXME: what are these for?
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}

-- A Snap(HTTP) server providing an ImplicitCAD REST API.

-- FIXME: we need AuthN/AuthZ for https://github.com/kliment/explicitcad to be useful.

-- Let's be explicit about what we're getting from where :)

import Prelude (IO, Maybe(Just, Nothing), String, Bool(True, False), Show, ($), (<>), (>), (.), (-), (/), (*), (**), (==), null, sqrt, min, max, minimum, maximum, show, return, fmap, otherwise, filter, not)

import Control.Applicative ((<|>))

import Snap.Core (Snap, route, writeBS, method, Method(GET), modifyResponse, setContentType, setTimeout, getRequest, rqParam)
import Snap.Http.Server (quickHttpServe)
import Snap.Util.GZip (withCompression)

-- Our Extended OpenScad interpreter, and the extrudeR function for making 2D objects 3D.
import Graphics.Implicit (runOpenscad, extrudeR)

-- Variable access functionality, so we can look up a requested resolution, along with message processing, and options into the scad engine.
import Graphics.Implicit.ExtOpenScad.Definitions (OVal(ONum), VarLookup, lookupVarIn, Message, Message(Message), MessageType(TextOut), ScadOpts(ScadOpts))

-- Functions for finding a box around an object, so we can define the area we need to raytrace inside of.
import Graphics.Implicit.ObjectUtil (getBox2, getBox3)

-- Definitions of the datatypes used for 2D objects, 3D objects, and for defining the resolution to raytrace at.
import Graphics.Implicit.Definitions (SymbolicObj2(UnionR2), SymbolicObj3(UnionR3), ℝ, Polyline, TriangleMesh)

-- Use default values when a Maybe is Nothing.
import Data.Maybe (fromMaybe, maybe)

import Graphics.Implicit.Export.TriangleMeshFormats (jsTHREE, stl)
--import Graphics.Implicit.Export.NormedTriangleMeshFormats (obj)

import Graphics.Implicit.Export.PolylineFormats (svg, dxf2, hacklabLaserGCode)

-- Operator to subtract two points. Used when defining the resolution of a 2d object.
import Data.AffineSpace ((.-.))

-- class DiscreteApprox
import Graphics.Implicit.Export.DiscreteAproxable (discreteAprox)

import Data.Text.Lazy (Text)

import Data.List (intercalate)

import Data.String (IsString)

import System.IO.Unsafe (unsafePerformIO)

import qualified Data.ByteString.Char8 as BS.Char (pack, unpack)
import qualified Data.Text.Lazy as TL (unpack)

-- | The entry point. uses snap to serve a website.
main :: IO ()
main = quickHttpServe site

-- | Our site definition. Renders requests to "render/", discards all else.
site :: Snap ()
site = route
    [
        ("render/", renderHandler)
    ] <|> writeBS "fall through"

-- | Our render/ handler. Uses source, callback, and opitional format to render an object.
renderHandler :: Snap ()
renderHandler = method GET $ withCompression $ do
    modifyResponse $ setContentType "application/x-javascript"
    setTimeout 600
    request <- getRequest
    case (rqParam "source" request, rqParam "callback" request, rqParam "format" request)  of
        (Just [source], Just [callback], Nothing) ->
            writeBS . BS.Char.pack $ executeAndExport
                (BS.Char.unpack source)
                (BS.Char.unpack callback)
                Nothing
        (Just [source], Just [callback], Just [format]) ->
            writeBS . BS.Char.pack $ executeAndExport
                (BS.Char.unpack source)
                (BS.Char.unpack callback)
                (Just $ BS.Char.unpack format)
        (_, _, _)       -> writeBS "must provide source and callback as 1 GET variable each"

-- | Find the resolution to raytrace at.
getRes :: (VarLookup, [SymbolicObj2], [SymbolicObj3], [Message]) -> ℝ
-- | If specified, use a resolution specified by the "$res" a variable in the input file.
getRes (lookupVarIn "$res" -> Just (ONum res), _, _, _) = res
-- | If there was no resolution specified, use a resolution chosen for 3D objects.
--   FIXME: magic numbers.
getRes (vars, _, obj:objs, _) =
    let
        ((x1,y1,z1),(x2,y2,z2)) = getBox3 (UnionR3 0 (obj:objs))
        (x,y,z) = (x2-x1, y2-y1, z2-z1)
    in case fromMaybe (ONum 1) $ lookupVarIn "$quality" vars of
        ONum qual | qual > 0  -> min (minimum [x,y,z]/2) ((x*y*z/qual)**(1/3) / 22)
        _                     -> min (minimum [x,y,z]/2) ((x*y*z)**(1/3) / 22)
-- | ... Or use a resolution chosen for 2D objects.
--   FIXME: magic numbers.
getRes (vars, obj:objs, _, _) =
    let
        (p1,p2) = getBox2 (UnionR2 0 (obj:objs))
        (x,y) = p2 .-. p1
    in case fromMaybe (ONum 1) $ lookupVarIn "$quality" vars of
        ONum qual | qual > 0 -> min (min x y/2) (sqrt(x*y/qual) / 30)
        _                    -> min (min x y/2) (sqrt(x*y) / 30)
-- | fallthrough value.
getRes _ = 1

-- | get the maximum dimension of the object being rendered.
--   FIXME: shouldn't this get the diagonal across the box?
getWidth :: (VarLookup, [SymbolicObj2], [SymbolicObj3], [Message]) -> ℝ
getWidth (_,     _, obj:objs, _) = maximum [x2-x1, y2-y1, z2-z1]
    where ((x1,y1,z1),(x2,y2,z2)) = getBox3 $ UnionR3 0 (obj:objs)
getWidth (_, obj:objs,     _, _) = max (x2-x1) (y2-y1)
    where ((x1,y1),(x2,y2)) = getBox2 $ UnionR2 0 (obj:objs)
getWidth (_,    [],    [], _) = 0

getOutputHandler2 :: String -> ([Polyline] -> Text)
getOutputHandler2 name
  | name == "SVG"                   = svg
  | name == "gcode/hacklab-laser"   = hacklabLaserGCode
  | otherwise                       = dxf2

-- FIXME: OBJ support
getOutputHandler3 :: String -> (TriangleMesh -> Text)
getOutputHandler3 name
  | name == "STL"                   = stl
  | otherwise                       = jsTHREE

isTextOut :: Message -> Bool
isTextOut (Message TextOut _ _ ) = True
isTextOut _                      = False

-- | decide what options to send to the scad engine.
generateScadOpts :: ScadOpts
generateScadOpts = ScadOpts compat_flag import_flag
  where
    compat_flag = False -- Do not try to be extra compatible with openscad.
    import_flag = False -- Do not honor include or use statements.

-- | Give an openscad object to run and the basename of
--   the target to write to... write an object!
executeAndExport :: String -> String -> Maybe String -> String
executeAndExport content callback maybeFormat =
    let
        showB :: IsString t => Bool -> t
        showB True  = "true"
        showB False = "false"
        callbackF :: Bool -> Bool -> ℝ -> String -> String
        callbackF False is2D w msg =
            callback <> "([null," <> show msg <> "," <> showB is2D <> "," <> show w  <> "]);"
        callbackF True  is2D w msg =
            callback <> "([new Shape()," <> show msg <> "," <> showB is2D <> "," <> show w <> "]);"
        callbackS :: (Show a1, Show a) => a -> a1 -> String
        callbackS str          msg =
            callback <> "([" <> show str <> "," <> show msg <> ",null,null]);"
        scadOptions = generateScadOpts
        openscadProgram = runOpenscad scadOptions [] content
    in
      unsafePerformIO $ do
      s@(_, obj2s, obj3s, messages) <- openscadProgram
      let
        res = getRes   s
        w   = getWidth s
        resError :: String
        resError = "Unreasonable resolution requested: "
                   <> "the server imps revolt! "
                   <> "(Install ImplicitCAD locally -- github.com/colah/ImplicitCAD/)"
        render = res > 0
        scadMessages = intercalate "\n"
                       (fmap show (filter (not . isTextOut) messages) <>
                        fmap show (filter isTextOut messages))

      return $ case (obj2s, obj3s, render) of
        (_ ,        _, False) -> callbackF False False 1 resError
        ([], obj:objs, _    ) -> do
          let target           = if null objs
                                 then obj
                                 else UnionR3 0 (obj:objs)
              unionWarning :: String
              unionWarning     = if null objs
                                 then ""
                                 else " \nWARNING: Multiple objects detected. Adding a Union around them."
              output3d         = TL.unpack $ maybe jsTHREE getOutputHandler3 maybeFormat $ discreteAprox res target
          if fromMaybe "jsTHREE" maybeFormat == "jsTHREE"
            then output3d <> callbackF True False w (scadMessages <> unionWarning)
            else callbackS output3d (scadMessages <> unionWarning)
        (obj:objs, []   , _) -> do
          let target          = if null objs
                                then obj
                                else UnionR2 0 (obj:objs)
              unionWarning :: String
              unionWarning    = if null objs
                                then ""
                                else " \nWARNING: Multiple objects detected. Adding a Union around them."
              output3d        = TL.unpack $ maybe jsTHREE getOutputHandler3 maybeFormat $ discreteAprox res $ extrudeR 0 target res
              output2d        = TL.unpack $ maybe svg getOutputHandler2 maybeFormat $ discreteAprox res target
          if fromMaybe "jsTHREE" maybeFormat == "jsTHREE"
            then output3d <> callbackF True True w (scadMessages <> unionWarning)
            else callbackS output2d (scadMessages <> unionWarning)
        ([], []         , _) -> callbackF False False 1 $ intercalate "\n" [scadMessages, "Nothing to render."]
        _                    -> callbackF False False 1 $ intercalate "\n" [scadMessages, "ERROR: File contains a mixture of 2D and 3D objects, what do you want to render?"]