packages feed

moonlight-planar-1.1.0.0: docs/knight-rig/Main.hs

module Main (main) where

import Data.Bifunctor (first)
import Data.Foldable (toList)
import Data.List (mapAccumL)
import Data.List.NonEmpty (NonEmpty (..))
import Moonlight.Planar.Affine (affine2)
import qualified Moonlight.Planar.Curve as Curve
import Moonlight.Planar.Exact
import Moonlight.Planar.Exhibit.Geometry (ShapeError, solidObj, triangulateShape)
import Moonlight.Planar.Point (Point (..), PointValidationError, queryPointValue)
import System.Environment (getArgs)
import System.Exit (die)

type XZ = (Double, Double)
data Part = Part String Double (NonEmpty XZ) [XZ]

data KnightError = KnightGeometry ShapeError | KnightPoint PointValidationError
  | KnightScalar ScalarRefinementError | KnightArithmetic ExactArithmeticError
  deriving stock (Show)

main :: IO ()
main = do
  args <- getArgs
  case args of
    [path] -> either (die . show) (writeFile path) knight
    _ -> die "usage: moonlight-planar-knight-rig OUTPUT.obj"

knight :: Either KnightError String
knight = ("# Native Moonlight Planar CDT - layered Knight fan art\n" <>) . concat
  <$> (parts >>= traverse build)
 where
  build :: Part -> Either KnightError String
  build (Part name thickness outline seeds) =
    solidObj name (const thickness) <$> first KnightGeometry (triangulateShape (fmap point outline :| []) (fmap point seeds))
  point :: XZ -> Point
  point (x,z) = Point (snap (750*x)) (snap (-750*z))
  snap :: Double -> Double
  snap v = fromIntegral (round (v*65536) :: Integer) / 65536

-- A continuous mask-and-horn silhouette; the rigid mask is not an image card.
mask :: Either KnightError (NonEmpty XZ)
mask = ((0,0.98) :|) . concat <$> sequence
  [ curve (0,0.98) (-0.31,0.96) (-0.41,1.10) (-0.39,1.32)
  , curve (-0.39,1.32) (-0.40,1.53) (-0.49,1.72) (-0.38,1.88)
  , curve (-0.38,1.88) (-0.40,1.68) (-0.20,1.64) (-0.22,1.45)
  , curve (-0.22,1.45) (-0.12,1.40) (0.12,1.40) (0.22,1.45)
  , curve (0.22,1.45) (0.20,1.64) (0.40,1.68) (0.38,1.88)
  , curve (0.38,1.88) (0.49,1.72) (0.40,1.53) (0.39,1.32)
  , curve (0.39,1.32) (0.41,1.10) (0.31,0.96) (0,0.98)
  ]

-- Existing sixteen samples per authored cubic, evaluated by the curve owner.
curve :: XZ -> XZ -> XZ -> XZ -> Either KnightError [XZ]
curve a b c d = do
  start <- exactXZ a
  controlA <- exactXZ b
  controlB <- exactXZ c
  end <- exactXZ d
  let step = Curve.curveStep
        (Curve.cubic (exactVectorFromPoints start controlA) (exactVectorFromPoints start controlB))
        (exactVectorFromPoints start end)
  parameters <- traverse (sampleParameter 16)
    [i | i <- [1..16], i < 16 || d /= (0,0.98)]
  traverse (embeddedXZ . translateExactPoint start . (`Curve.evaluateStep` step)) parameters

-- The ellipse is the canonical four rational conics. Twelve parameter samples
-- per quarter retain 48 vertices; rational parameter spacing is not uniform
-- angular spacing. Trigonometry here specifies the tilted coordinate frame only.
ellipse :: XZ -> XZ -> Double -> Either KnightError (NonEmpty XZ)
ellipse (cx,cz) (rx,rz) tilt = do
  center <- exactXZ (cx,cz)
  axisX <- exactXZ (rx*cos tilt,rx*sin tilt)
  axisY <- exactXZ (-rz*sin tilt,rz*cos tilt)
  let origin = exactPoint 0 0
      placement = affine2 (ExactVector 1 0) (ExactVector 0 1) (exactVectorFromPoints origin center)
      outline = Curve.transformLocatedClosedTrail placement
        (Curve.ellipse (exactVectorFromPoints origin axisX) (exactVectorFromPoints origin axisY))
      start = Curve.location outline
      (_, locatedSteps) = mapAccumL locateStep start
        (toList (Curve.closedTrailSteps (Curve.locatedValue outline)))
      locateStep :: ExactPoint -> Curve.CurveStep -> (ExactPoint,(ExactPoint,Curve.CurveStep))
      locateStep point step = (translateExactPoint point (Curve.curveStepEnd step),(point,step))
  parameters <- traverse (sampleParameter 12) [0..11]
  let samples = concatMap (\(point,step) ->
        map (translateExactPoint point . (`Curve.evaluateStep` step)) parameters) locatedSteps
  traverse embeddedXZ (start :| drop 1 samples)

sampleParameter :: Integer -> Integer -> Either KnightError UnitInterval
sampleParameter count index = first KnightArithmetic (exactRational index count)
  >>= first KnightScalar . unitInterval

exactXZ :: XZ -> Either KnightError ExactPoint
exactXZ (x,z) = first KnightPoint (exactPointFromPoint (Point x z))

-- Binary64 projection is a transport candidate; the unchanged snapping and CDT
-- admission below remain responsible for the actual mesh embedding.
embeddedXZ :: ExactPoint -> Either KnightError XZ
embeddedXZ point = toXZ . queryPointValue <$> first KnightPoint (exactPointToEmbeddingCandidate point)
 where
  toXZ :: Point -> XZ
  toXZ (Point x z) = (x,z)

ellipticPart :: String -> Double -> XZ -> XZ -> Double -> Either KnightError Part
ellipticPart name thickness center axes tilt =
  (\outline -> Part name thickness outline []) <$> ellipse center axes tilt

strip :: (Double -> XZ) -> (Double -> Double) -> NonEmpty XZ
strip center width = side (-1) 0 :|
  ([side (-1) (fromIntegral i/24) | i <- [1..24 :: Int]]
   <> [side 1 (fromIntegral i/24) | i <- [24,23..0 :: Int]])
 where
  side :: Double -> Double -> XZ
  side sign t = let (x,z)=center t in (x+sign*width t,z)

limb :: String -> Double -> Double -> Double -> Double -> Part
limb name x top bottom width = Part name 0.026
  (strip (\t -> (x + 0.022*sin(pi*t),top+(bottom-top)*t)) (\t -> width*(1-0.28*t)))
  [(x+0.022*sin(pi*t),top+(bottom-top)*t) | i <- [1..23 :: Int], let t=fromIntegral i/24]

cloak :: Int -> Part
cloak i = Part ("Cloak_"<>show i) 0.014
  (strip center width) [center (fromIntegral j/24) | j<-[1..23 :: Int]]
 where
  axis :: Double
  axis=fromIntegral (i-2)
  center :: Double -> XZ
  center t = (axis*(0.064+0.102*t) + 0.008*sin(pi*t),1.025-(0.55+0.025*cos(axis*1.5))*t)
  width :: Double -> Double
  width t = (0.094+0.018*t)*(1-0.48*t^ (8::Int))

parts :: Either KnightError [Part]
parts = sequence $
  [ (\outline -> Part "Mask" 0.052 outline []) <$> mask
  , ellipticPart "Eye_L" 0.003 (-0.177,1.218) (0.072,0.103) (-0.12)
  , ellipticPart "Eye_R" 0.003 (0.177,1.218) (0.072,0.103) 0.12
  , ellipticPart "Body" 0.068 (0,0.665) (0.16,0.255) 0
  , ellipticPart "Collar" 0.018 (0,0.991) (0.205,0.045) (-0.06)
  , Right (limb "Leg_L" (-0.115) 0.57 0.075 0.047)
  , Right (limb "Leg_R" 0.115 0.57 0.075 0.047)
  , ellipticPart "Foot_L" 0.033 (-0.086,0.051) (0.079,0.033) 0
  , ellipticPart "Foot_R" 0.033 (0.144,0.051) (0.079,0.033) 0
  , Right (limb "Arm_L" (-0.23) 0.935 0.57 0.037)
  , Right (limb "Arm_R" 0.23 0.935 0.57 0.037)
  , ellipticPart "Hand_L" 0.027 (-0.23,0.565) (0.045,0.052) (-0.1)
  , ellipticPart "Hand_R" 0.027 (0.23,0.565) (0.045,0.052) 0.1
  ] <> fmap (Right . cloak) [0..4]