moonlight-planar-1.1.0.0: docs/art-common/Moonlight/Planar/Exhibit/Geometry.hs
module Moonlight.Planar.Exhibit.Geometry
( ShapeError (..)
, Triangle (..)
, triangulateShape
, solidObj
) where
import Data.Bifunctor (first)
import Data.Function (on)
import Data.List (groupBy, sortOn)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Vector as Vector
import Moonlight.Planar.Cdt (CdtError, boundedRegionFaces, constrainedDelaunay)
import Moonlight.Planar.Dcel (innerFaceVertices, vertexPoint)
import Moonlight.Planar.Handles.HandleDefs (FaceId)
import Moonlight.Planar.Point (Point (..))
import Moonlight.Planar.Types (buildTriangulation, unitElementDefaults)
import Text.Printf (printf)
data ShapeError = MeshRejected !CdtError | MissingTriangle !FaceId
deriving stock (Show)
data Triangle = Triangle !Point !Point !Point
-- Concave outlines and holes share the native constrained-domain builder.
triangulateShape :: NonEmpty (NonEmpty Point) -> [Point] -> Either ShapeError [Triangle]
triangulateShape loops interior = do
let boundaries = NonEmpty.toList loops
offsets = scanl (+) 0 (NonEmpty.length <$> boundaries)
constraints = concat (zipWith loopConstraints offsets boundaries)
points = concatMap NonEmpty.toList boundaries <> interior
result <- first MeshRejected $
constrainedDelaunay unitElementDefaults (Vector.fromList points) (Vector.fromList constraints)
let mesh = buildTriangulation result
traverse
(\face -> case innerFaceVertices mesh face of
Nothing -> Left (MissingTriangle face)
Just (a, b, c) -> Right (Triangle (vertexPoint mesh a) (vertexPoint mesh b) (vertexPoint mesh c)))
(boundedRegionFaces mesh)
loopConstraints :: Int -> NonEmpty Point -> [(Int, Int)]
loopConstraints offset points =
let indices = [offset .. offset + NonEmpty.length points - 1]
in zip indices (drop 1 indices <> take 1 indices)
-- OBJ is a transport view. Coincident face vertices are welded on import;
-- only edges incident to one selected triangle receive a side wall.
solidObj :: String -> (Point -> Double) -> [Triangle] -> String
solidObj name halfThickness triangles =
let oriented = orient <$> triangles
boundary = concat . filter ((== 1) . length)
. groupBy ((==) `on` edgeKey) . sortOn edgeKey
$ concatMap triangleEdges oriented
in "o " <> name <> "\n"
<> foldMap surfaces oriented
<> foldMap wall boundary
where
orient :: Triangle -> Triangle
orient triangle@(Triangle a@(Point ax ay) b@(Point bx by) c@(Point cx cy))
| (bx - ax) * (cy - ay) - (by - ay) * (cx - ax) >= 0 = triangle
| otherwise = Triangle a c b
triangleEdges :: Triangle -> [(Point, Point)]
triangleEdges (Triangle a b c) = [(a, b), (b, c), (c, a)]
edgeKey :: (Point, Point) -> ((Double, Double), (Double, Double))
edgeKey (Point ax ay, Point bx by) =
(min (ax, ay) (bx, by), max (ax, ay) (bx, by))
surfaces :: Triangle -> String
surfaces (Triangle a b c) = face (-1) [c, b, a] <> face 1 [a, b, c]
face :: Double -> [Point] -> String
face side points = foldMap (vertex side) points <> "f -3 -2 -1\n"
wall :: (Point, Point) -> String
wall (a, b) = vertex (-1) a <> vertex (-1) b <> vertex 1 b <> vertex 1 a
<> "f -4 -3 -2 -1\n"
vertex :: Double -> Point -> String
vertex side point@(Point x y) = printf "v %.9f %.9f %.9f\n"
(x / 750) (side * halfThickness point) (negate y / 750)