packages feed

geomancy-0.4.0.0: test/Spec/DTransform.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}

module Spec.DTransform (tests) where

import Geomancy
import Hedgehog
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.Hedgehog (testPropertyNamed)

import Data.Foldable (for_)
import Linear (M44, V3(..))

import Geomancy.DMat4 qualified
import Geomancy.DQuaternion qualified
import Geomancy.DTransform qualified as DTransform
import Geomancy.Mat4 qualified
import Geomancy.Transform ((!.))
import Geomancy.Transform qualified as Transform
import Geomancy.Vulkan.DView qualified as DView

import Support.Ball (Ball, balls, consistent, enclosed, exactly, narrow)
import Support.Fixtures
  (fourteenthTurn, intrinsicComposedD, intrinsicComposedRef, quarterTurn, rotationsMatch, trsRef, twelfthTurn)
import Support.Gen (genAngle, genTransform)
import Support.Linear (entries, toM44, toV3, toV3D)
import Support.Property (pattern PROP_TESTS)
import Support.Reference.Mat4 qualified as Ref.Mat4
import Support.Reference.Quaternion qualified as Ref.Quaternion
import Support.Reference.Transform qualified as Ref.Transform
import Support.Reference.Vulkan qualified as Ref.Vulkan

tests :: TestTree
tests = testGroup "DTransform"
  [ testPropertyNamed "det" "prop_dtransform_det" prop_dtransform_det
  , testPropertyNamed "rotate" "prop_dtransform_rotate" prop_dtransform_rotate
  , testPropertyNamed "node_order" "prop_dtransform_node_order" prop_dtransform_node_order
  , testPropertyNamed "float_parity" "prop_dtransform_float_parity" prop_dtransform_float_parity
  , testPropertyNamed "big_world" "prop_dtransform_big_world" prop_dtransform_big_world
  ]

prop_dtransform_det :: Property
prop_dtransform_det = withTests 1 $ property do
  1     === Geomancy.DMat4.det (DTransform.translate 1 2 3)
  0     === Geomancy.DMat4.det (DTransform.scaleX 0)
  -1    === Geomancy.DMat4.det (DTransform.scaleZ (-1))
  8     === Geomancy.DMat4.det (DTransform.scale 2)
  (1/8) === Geomancy.DMat4.det (DTransform.scale (1/2))

  let
    rotatedBall = Ref.Mat4.det (Ref.Transform.rotateY twelfthTurn) :: Ball Double
  [Nothing] === enclosed [rotatedBall] [Geomancy.DMat4.det (DTransform.rotateY twelfthTurn)]
  [Nothing] === enclosed [rotatedBall] [1 :: Double]

prop_dtransform_rotate :: Property
prop_dtransform_rotate = withTests 1 $ property do
  let
    axisQ axis = DTransform.rotateQ (Geomancy.DQuaternion.axisAngle axis quarterTurn)
    axisQBall axis = Ref.Transform.rotateQ (Ref.Quaternion.axisAngle axis quarterTurn)
    cases =
      [ (DTransform.rotateX quarterTurn, Ref.Transform.rotateX quarterTurn, dvec3 0 0 1, dvec3 0 (-1) 0)
      , (axisQ (dvec3 1 0 0), axisQBall (V3 1 0 0), dvec3 0 0 1, dvec3 0 (-1) 0)
      , (DTransform.rotateY quarterTurn, Ref.Transform.rotateY quarterTurn, dvec3 0 0 1, dvec3 1 0 0)
      , (axisQ (dvec3 0 1 0), axisQBall (V3 0 1 0), dvec3 0 0 1, dvec3 1 0 0)
      , (DTransform.rotateZ quarterTurn, Ref.Transform.rotateZ quarterTurn, dvec3 1 0 0, dvec3 0 1 0)
      , (axisQ (dvec3 0 0 1), axisQBall (V3 0 0 1), dvec3 1 0 0, dvec3 0 1 0)
      ]
  for_ cases \(t, tBall, input, expected) ->
    rotationsMatch toV3D (t DTransform.!.) (tBall Ref.Transform.!.) [(input, expected)]

prop_dtransform_node_order :: Property
prop_dtransform_node_order = withTests 1 $ property do
  let
    t = dvec3 2 3 5
    r = intrinsicComposedD 0 0 fourteenthTurn
    s = 11
    trsD = mconcat [DTransform.translateV t, DTransform.rotateQ r, DTransform.scaleV s]
    v = dvec3 0 0 1

    rBall = intrinsicComposedRef 0 0 fourteenthTurn
    nodeBall = Ref.Transform.node (V3 2 3 5) rBall 11 :: M44 (Ball Double)
    trsBall = trsRef (V3 2 3 5) rBall 11
    viaNodeBall = nodeBall Ref.Transform.!. V3 0 0 1
    viaTrsBall = trsBall Ref.Transform.!. V3 0 0 1
  replicate 3 Nothing === enclosed viaNodeBall (toV3D (DTransform.node t r s DTransform.!. v))
  replicate 3 Nothing === enclosed viaTrsBall (toV3D (trsD DTransform.!. v))
  replicate 3 Nothing === consistent viaNodeBall viaTrsBall

  let
    parent = DTransform.node (dvec3 4 5 6) (intrinsicComposedD 0 0 twelfthTurn) 2
    local = DTransform.node (dvec3 7 8 9) (intrinsicComposedD 0 twelfthTurn 0) (1/3)
    parentBall = Ref.Transform.node (V3 4 5 6) (intrinsicComposedRef 0 0 twelfthTurn) 2 :: M44 (Ball Double)
    localBall = Ref.Transform.node (V3 7 8 9) (intrinsicComposedRef 0 twelfthTurn 0) (1/3)
    precompBall = Ref.Mat4.compose parentBall localBall Ref.Transform.!. V3 0 0 1
    nocompBall = parentBall Ref.Transform.!. localBall Ref.Transform.!. V3 0 0 1
  replicate 3 Nothing === enclosed precompBall (toV3D (parent <> local DTransform.!. v))
  replicate 3 Nothing === enclosed nocompBall (toV3D (parent DTransform.!. local DTransform.!. v))
  replicate 3 Nothing === consistent precompBall nocompBall

prop_dtransform_float_parity :: Property
prop_dtransform_float_parity = withDiscards 1000 . withTests PROP_TESTS $ property do
  (_name, t) <- forAllWith fst genTransform
  Geomancy.Mat4.toListRowMajor t ===
    Geomancy.Mat4.toListRowMajor (DTransform.toTransform (DTransform.fromTransform t))

  angle <- forAll genAngle
  let
    a = realToFrac angle
    viaDouble = unTransform $ DTransform.toTransform (DTransform.rotateY a)
    viaFloat = unTransform $ Transform.rotateY angle
    viaDoubleBall = fmap (fmap narrow) $ Ref.Transform.rotateY (exactly a)
    viaFloatBall = Ref.Transform.rotateY (exactly angle)
  replicate 16 Nothing === enclosed (entries viaDoubleBall) (entries (toM44 viaDouble))
  replicate 16 Nothing === enclosed (entries viaFloatBall) (entries (toM44 viaFloat))
  replicate 16 Nothing === consistent (entries viaDoubleBall) (entries viaFloatBall)

prop_dtransform_big_world :: Property
prop_dtransform_big_world = withTests 1 $ property do
  let
    far = 1e9
    eye = dvec3 far 0 far
    view = DView.lookAtRH_ eye (eye + dvec3 0 0 1)
    model = DTransform.translate (far + 0.25) 0 (far + 5)
    composedInDouble = DTransform.toTransform (view <> model)
    composedInFloat = DTransform.toTransform view <> DTransform.toTransform model

    farBall = exactly far
    eyeBall = balls (toV3D eye)
    viewBall = Ref.Vulkan.lookAtRH_ eyeBall (eyeBall + V3 0 0 1)
    modelBall = Ref.Transform.translate (farBall + 0.25) 0 (farBall + 5)
    composedInDoubleBall = fmap (fmap narrow) $ Ref.Mat4.compose viewBall modelBall
    originBall = composedInDoubleBall Ref.Transform.!. V3 0 0 0
  replicate 3 Nothing === enclosed originBall (toV3 (composedInDouble !. vec3 0 0 0))
  replicate 3 Nothing === enclosed originBall (toV3 (vec3 0.25 0 5))
  withVec3 (composedInFloat !. vec3 0 0 0) \x _y _z ->
    diff x (/=) 0.25