packages feed

geomancy-0.4.0.0: test/Spec/DQuaternion.hs

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

module Spec.DQuaternion (tests) where

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

import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Linear (V3(..))
import Linear.Quaternion qualified

import Geomancy.DQuaternion qualified
import Geomancy.DTransform qualified as DTransform
import Geomancy.Quaternion qualified

import Support.Ball (balls, consistent, enclosed, exactly, narrow, widen)
import Support.Fixtures (intrinsicComposedD, intrinsicComposedRef, quarterTurn, rotationsMatch)
import Support.Gen (genAngle, genAngleD, genDVec3, genSmallDVec3, genVec3)
import Support.Linear (toQ, toQD, toV3D)
import Support.Reference.Linear qualified as Ref.Linear
import Support.Reference.Quaternion qualified as Ref.Quaternion
import Support.Reference.Transform qualified as Ref.Transform

tests :: TestTree
tests = testGroup "DQuaternion"
  [ testPropertyNamed "ref" "prop_dquaternion_ref" prop_dquaternion_ref
  , testPropertyNamed "rotate_matrix" "prop_dquaternion_rotate_matrix" prop_dquaternion_rotate_matrix
  , testPropertyNamed "associativity" "prop_dquaternion_associativity" prop_dquaternion_associativity
  , testPropertyNamed "intrinsic" "prop_dquaternion_intrinsic" prop_dquaternion_intrinsic
  , testPropertyNamed "extrinsic" "prop_dquaternion_extrinsic" prop_dquaternion_extrinsic
  , testPropertyNamed "intrinsic_ref" "prop_dquaternion_intrinsic_ref" prop_dquaternion_intrinsic_ref
  , testPropertyNamed "float_parity" "prop_dquaternion_float_parity" prop_dquaternion_float_parity
  ]

prop_dquaternion_ref :: Property
prop_dquaternion_ref = property do
  annotate "Construction"
  (axis, angle) <- forAll $ (,) <$> genDVec3 <*> genAngleD
  let
    qg = Geomancy.DQuaternion.axisAngle axis angle
    ql = Linear.Quaternion.axisAngle (toV3D axis) angle
    qgBall = Ref.Quaternion.axisAngle (balls (toV3D axis)) (exactly angle)
    qlBall = Linear.Quaternion.axisAngle (balls (toV3D axis)) (exactly angle)
  replicate 4 Nothing === enclosed qgBall (toQD qg)
  replicate 4 Nothing === enclosed qlBall ql
  replicate 4 Nothing === consistent qgBall qlBall

  annotate "Rotation"
  p <- forAll genSmallDVec3
  let
    rpg = Geomancy.DQuaternion.rotate qg p
    rpl = Linear.Quaternion.rotate ql (toV3D p)
    rpgBall = Ref.Quaternion.rotate qgBall (balls (toV3D p))
    rplBall = Ref.Linear.rotate qlBall (balls (toV3D p))
  replicate 3 Nothing === enclosed rpgBall (toV3D rpg)
  replicate 3 Nothing === enclosed rplBall rpl
  replicate 3 Nothing === consistent rpgBall rplBall

  annotate "Multiplication"
  (axisB, angleB) <- forAll $ (,) <$> genDVec3 <*> genAngleD
  let
    qgB = Geomancy.DQuaternion.axisAngle axisB angleB
    qlB = Linear.Quaternion.axisAngle (toV3D axisB) angleB
    productGBall =
      qgBall `Ref.Quaternion.multiply`
        Ref.Quaternion.axisAngle (balls (toV3D axisB)) (exactly angleB)
    productLBall =
      qlBall `Ref.Linear.multiply`
        Linear.Quaternion.axisAngle (balls (toV3D axisB)) (exactly angleB)
  replicate 4 Nothing === enclosed productGBall (toQD (qg * qgB))
  replicate 4 Nothing === enclosed productLBall (ql * qlB)
  replicate 4 Nothing === consistent productGBall productLBall

prop_dquaternion_rotate_matrix :: Property
prop_dquaternion_rotate_matrix = property do
  q <- forAll $ dquaternion <$> genUnitish <*> genUnitish <*> genUnitish <*> genUnitish
  p <- forAll genSmallDVec3
  let
    viaRotate = Geomancy.DQuaternion.rotate q p
    viaMatrix = DTransform.rotateQ q DTransform.!. p
    viaRotateBall = Ref.Quaternion.rotate (balls (toQD q)) (balls (toV3D p))
    viaMatrixBall = Ref.Transform.rotateQ (balls (toQD q)) Ref.Transform.!. balls (toV3D p)
  replicate 3 Nothing === enclosed viaRotateBall (toV3D viaRotate)
  replicate 3 Nothing === enclosed viaMatrixBall (toV3D viaMatrix)
  replicate 3 Nothing === consistent viaRotateBall viaMatrixBall
  where
    genUnitish = Gen.double (Range.linearFracFrom 0.0 (-1) 1)

prop_dquaternion_associativity :: Property
prop_dquaternion_associativity = property do
  (a, b, c) <- forAll $ (,,)
    <$> (Geomancy.DQuaternion.axisAngle <$> genDVec3 <*> genAngleD)
    <*> (Geomancy.DQuaternion.axisAngle <$> genDVec3 <*> genAngleD)
    <*> (Geomancy.DQuaternion.axisAngle <$> genDVec3 <*> genAngleD)
  let ab'c = (a * b) * c
  annotateShow ab'c
  let a'bc = a * (b * c)
  annotateShow a'bc
  let
    aBall = balls (toQD a)
    bBall = balls (toQD b)
    cBall = balls (toQD c)
    ab'cBall = (aBall `Ref.Quaternion.multiply` bBall) `Ref.Quaternion.multiply` cBall
    a'bcBall = aBall `Ref.Quaternion.multiply` (bBall `Ref.Quaternion.multiply` cBall)
  replicate 4 Nothing === enclosed ab'cBall (toQD ab'c)
  replicate 4 Nothing === enclosed a'bcBall (toQD a'bc)
  replicate 4 Nothing === consistent ab'cBall a'bcBall

prop_dquaternion_intrinsic :: Property
prop_dquaternion_intrinsic = withTests 1 $ property do
  let
    q = Geomancy.DQuaternion.intrinsic 0 quarterTurn quarterTurn
    qBall = Ref.Quaternion.intrinsic 0 quarterTurn quarterTurn
  rotationsMatch toV3D
    (Geomancy.DQuaternion.rotate q)
    (Ref.Quaternion.rotate qBall)
    [ (dvec3 0 0 1, dvec3 0 (-1) 0)
    , (dvec3 1 0 0, dvec3 0 0 (-1))
    , (dvec3 0 (-1) 0, dvec3 (-1) 0 0)
    ]

prop_dquaternion_extrinsic :: Property
prop_dquaternion_extrinsic = withTests 1 $ property do
  let
    q = Geomancy.DQuaternion.extrinsic quarterTurn quarterTurn 0
    qBall = Ref.Quaternion.extrinsic quarterTurn quarterTurn 0
  rotationsMatch toV3D
    (Geomancy.DQuaternion.rotate q)
    (Ref.Quaternion.rotate qBall)
    [ (dvec3 0 0 1, dvec3 1 0 0)
    , (dvec3 1 0 0, dvec3 0 1 0)
    , (dvec3 0 (-1) 0, dvec3 0 0 (-1))
    ]

prop_dquaternion_intrinsic_ref :: Property
prop_dquaternion_intrinsic_ref = property do
  (roll, pitch, yaw) <- forAll $ (,,) <$> genAngleD <*> genAngleD <*> genAngleD
  let fused = Geomancy.DQuaternion.intrinsic roll pitch yaw
  annotateShow fused
  let composed = intrinsicComposedD roll pitch yaw
  annotateShow composed
  let
    viaFused = Geomancy.DQuaternion.rotate fused (dvec3 0 0 1)
    viaComposed = Geomancy.DQuaternion.rotate composed (dvec3 0 0 1)
    viaFusedBall =
      Ref.Quaternion.rotate
        (Ref.Quaternion.intrinsic (exactly roll) (exactly pitch) (exactly yaw))
        (V3 0 0 1)
    viaComposedBall =
      Ref.Quaternion.rotate
        (intrinsicComposedRef (exactly roll) (exactly pitch) (exactly yaw))
        (V3 0 0 1)
  replicate 3 Nothing === enclosed viaFusedBall (toV3D viaFused)
  replicate 3 Nothing === enclosed viaComposedBall (toV3D viaComposed)
  replicate 3 Nothing === consistent viaFusedBall viaComposedBall

prop_dquaternion_float_parity :: Property
prop_dquaternion_float_parity = property do
  (a, b) <- forAll $ (,)
    <$> (Geomancy.Quaternion.axisAngle <$> genVec3 <*> genAngle)
    <*> (Geomancy.Quaternion.axisAngle <$> genVec3 <*> genAngle)
  a === Geomancy.DQuaternion.toQuaternion (Geomancy.DQuaternion.fromQuaternion a)
  let
    viaDouble =
      Geomancy.DQuaternion.toQuaternion $
        Geomancy.DQuaternion.fromQuaternion a * Geomancy.DQuaternion.fromQuaternion b
    aBall = balls (toQ a)
    bBall = balls (toQ b)
    viaDoubleBall =
      fmap narrow $
        fmap widen aBall `Ref.Quaternion.multiply` fmap widen bBall
    viaFloatBall = aBall `Ref.Quaternion.multiply` bBall
  replicate 4 Nothing === enclosed viaDoubleBall (toQ viaDouble)
  replicate 4 Nothing === enclosed viaFloatBall (toQ (a * b))
  replicate 4 Nothing === consistent viaDoubleBall viaFloatBall