Skip to content

DualQuaternionfSmokeTest.kt

src/jvmMain/kotlin/afield/examples/four/DualQuaternionfSmokeTest.kt · 133 lines

package afield.examples.four

import afield.graphics.vulkan.util.CameraPose
import afield.animation.keyframes.KeyframeScope
import afield.math.linalg.remAssign
import afield.data.collections.times
import afield.math.autodiff.times
import afield.math.linalg.times
import afield.math.units.times
import afield.math.linalg.DualQuaternionf
import afield.math.linalg.Quaternionf
import afield.math.linalg.Vector3f
import java.nio.file.Files
import kotlin.math.PI
import kotlin.math.abs

object DualQuaternionfSmokeTest

FIELD_TEST id: examples.four.dual-quaternionf-smoke runner: jvm-main entrypoint: afield.examples.four.DualQuaternionfSmokeTest timeoutSec: 20 expect: stdout: - "DualQuaternionfSmokeTest ok"

fun main(args: Array<String>)

body — 105 lines, from line 28
    @JvmStatic
    fun main(args: Array<String>) {
        constructionAndTransform()
        compositionOrder()
        screwInterpolation()
        cameraPoseRoundTrip()
        keyframeInterpolationAndPersistence()
        println("DualQuaternionfSmokeTest ok")
    }

    private fun constructionAndTransform() {
        val rotation = Quaternionf().rotateY((PI / 2.0).toFloat())
        val translation = Vector3f(1f, 2f, 3f)
        val dq = DualQuaternionf(rotation, translation)

        assertVecClose("translation", dq.getTranslation(), translation)

        val point = Vector3f(0f, 0f, -2f)
        val expected = rotation.transform(Vector3f(point)).add(translation)
        val actual = dq.transform(point)
        assertVecClose("transform", actual, expected)
        assertVecClose("dq * point", dq * point, expected)
        assertVecClose("point * dq", point * dq, expected)
        assertVecClose("-dq same transform", (-dq).transform(point), expected)

        val copied = DualQuaternionf()
        copied %= dq
        assertVecClose("copy assignment", copied * point, expected)
    }

    private fun compositionOrder() {
        val a = DualQuaternionf(
            Quaternionf().rotateY((PI / 2.0).toFloat()),
            Vector3f(1f, 0f, 0f)
        )
        val b = DualQuaternionf(
            Quaternionf().rotateX((PI / 2.0).toFloat()),
            Vector3f(0f, 2f, 0f)
        )
        val point = Vector3f(0.25f, 0.5f, -1f)

        val composed = DualQuaternionf(a).mul(b)
        val expected = a.transform(b.transform(point))
        assertVecClose("composition", composed.transform(point), expected)
        assertVecClose("operator composition", (a * b) * point, expected)
    }

    private fun screwInterpolation() {
        val start = DualQuaternionf()
        val end = DualQuaternionf(
            Quaternionf().rotateZ(PI.toFloat()),
            Vector3f(0f, 0f, 10f)
        )
        val mid = start.sclerp(end, 0.5f)

        assertVecClose("sclerp midpoint translation", mid.getTranslation(), Vector3f(0f, 0f, 5f))
        assertVecClose("sclerp midpoint transform", mid.transform(Vector3f(1f, 0f, 0f)), Vector3f(0f, 1f, 5f))
        assertVecClose("sclerp start", start.sclerp(end, 0f).getTranslation(), Vector3f())
        assertVecClose("sclerp end", start.sclerp(end, 1f).getTranslation(), Vector3f(0f, 0f, 10f))
    }

    private fun cameraPoseRoundTrip() {
        val eye = Vector3f(1f, 2f, 3f)
        val center = Vector3f(1f, 2f, 2f)
        val pose = CameraPose.lookAt(eye, center)

        assertVecClose("camera pose position", pose.position, eye)
        assertVecClose("camera pose target", pose.target, center)
        assertVecClose("camera pose forward", pose.forward, Vector3f(0f, 0f, -1f))
        assertVecClose("camera pose up", pose.up, Vector3f(0f, 1f, 0f))
    }

    private fun keyframeInterpolationAndPersistence() {
        val scope = KeyframeScope()
        val start = CameraPose.lookAlong(Vector3f(0f, 0f, 0f), Vector3f(0f, 0f, -1f), focusDistance = 2f)
        val end = CameraPose.lookAlong(Vector3f(0f, 0f, 10f), Vector3f(0f, 1f, 0f), focusDistance = 4f)
        val keyA = scope.Key("camera" to start)
        val keyB = scope.Key("camera" to end)

        val mid = scope.interpolate(keyA, keyB, 0.5)["camera"] as CameraPose
        require(abs(mid.focusDistance - 3f) < 1e-5f) { "Expected interpolated focus distance 3, got ${mid.focusDistance}" }

        val path = Files.createTempFile("field-dualquat-key", ".json")
        try {
            scope.saveKey(path, keyB)
            val loaded = scope.loadKey(path)["camera"] as CameraPose
            assertVecClose("loaded camera position", loaded.position, end.position)
            require(abs(loaded.focusDistance - end.focusDistance) < 1e-5f) {
                "Loaded focus distance ${loaded.focusDistance}, expected ${end.focusDistance}"
            }
        } finally {
            val versionsDir = path.resolveSibling("${path.fileName}.versions")
            Files.deleteIfExists(path)
            Files.deleteIfExists(versionsDir.resolve("manifest.json"))
            Files.deleteIfExists(versionsDir.resolve("000000.json"))
            Files.deleteIfExists(versionsDir)
        }
    }

    private fun assertVecClose(name: String, actual: Vector3f, expected: Vector3f, eps: Float = 1e-4f) {
        require(actual.distance(expected) <= eps) {
            "$name expected $expected, got $actual"
        }
    }
}