Skip to content

FLineInstructionTrajectoryTrackerSmokeTest.kt

src/jvmMain/kotlin/afield/examples/graphics/FLineInstructionTrajectoryTrackerSmokeTest.kt · 193 lines

package afield.examples.graphics

import afield.geometry.path.FLine
import afield.geometry.path.FLineSideways
import afield.math.linalg.Vec4
import afield.math.linalg.vec
import kotlin.math.abs

FIELD_TEST id: graphics.fline-instruction-trajectory-tracker.smoke runner: jvm-main entrypoint: afield.examples.graphics.FLineInstructionTrajectoryTrackerSmokeTest tags: [smoke, graphics, fline, trajectories] timeoutSec: 20 expect: exitCode: 0 stdoutContains: - "FLineInstructionTrajectoryTracker smoke tests passed"

object FLineInstructionTrajectoryTrackerSmokeTest

Lightweight smoke tests for FLineInstructionTrajectoryTracker.

object FLineInstructionTrajectoryTrackerSmokeTest {

fun runTests(): Boolean

    fun runTests(): Boolean {
        return try {
            testStableTopologyAccumulates()
            testStableTopologyFastPathKeepsInstructionIdentityWhenClose()
            testStableTopologyFastPathFallsBackWhenAboveThreshold()
            testDroppedIntermediateInstructionDoesNotShiftEverything()
            testDisappearAndReappearCreatesGap()
            testEffectiveInstructionColorIsCopied()
            println("✅ FLineInstructionTrajectoryTracker smoke tests passed")
            true
        } catch (t: Throwable) {
            println("❌ FLineInstructionTrajectoryTracker smoke tests failed: ${t.message}")
            t.printStackTrace()
            false
        }
    }

fun main(args: Array<String>)

body — 150 lines, from line 43
    @JvmStatic
    fun main(args: Array<String>) {
        check(runTests()) { "FLineInstructionTrajectoryTracker smoke tests failed" }
    }

    private fun testStableTopologyAccumulates() {
        val tracker = FLineSideways(maxMatchDistance = 5f)

        tracker.update(listOf(polyline(0f, 1f, 2f, y = 0f)))
        tracker.update(listOf(polyline(0f, 1f, 2f, y = 1f)))

        val withTwoSamples = tracker.trajectories().filter { it.nodes.size == 2 }
        require(withTwoSamples.size == 3) { "Expected 3 two-sample trajectories, got ${withTwoSamples.size}" }

        withTwoSamples.forEach { t ->
            val end = t.nodes.last().to
            require(abs(end.y - 1f) < 1e-5f) { "Expected second sample y=1, got ${end.y}" }
        }
    }

    private fun testDroppedIntermediateInstructionDoesNotShiftEverything() {
        val tracker = FLineSideways(maxMatchDistance = 2f)

        tracker.update(listOf(polyline(0f, 1f, 2f, 3f)))
        tracker.update(listOf(polyline(0f, 2f, 3f)))

        val trajectories = tracker.trajectories()
        val x2 = findTrajectoryStartingNear(trajectories, 2f)
        val x3 = findTrajectoryStartingNear(trajectories, 3f)

        val x2End = x2.nodes.last().to.x
        val x3End = x3.nodes.last().to.x

        require(abs(x2End - 2f) < 1e-5f) { "Expected x=2 trajectory to remain at x=2, got $x2End" }
        require(abs(x3End - 3f) < 1e-5f) { "Expected x=3 trajectory to remain at x=3, got $x3End" }
    }

    private fun testStableTopologyFastPathKeepsInstructionIdentityWhenClose() {
        val tracker = FLineSideways(
            maxMatchDistance = 2f,
            stableTopologyDistanceThreshold = 1f,
            instructionIndexWeight = 0.0
        )

        tracker.update(listOf(polyline(0f, 1f)))
        tracker.update(listOf(polyline(0.9f, 0.1f)))

        val trajectories = tracker.trajectories()
        val firstTrack = findTrajectoryStartingNear(trajectories, 0f)
        val secondTrack = findTrajectoryStartingNear(trajectories, 1f)

        val firstEnd = firstTrack.nodes.last().to.x
        val secondEnd = secondTrack.nodes.last().to.x
        require(abs(firstEnd - 0.9f) < 1e-5f) {
            "Expected first instruction track to stay with instruction index 0, got $firstEnd"
        }
        require(abs(secondEnd - 0.1f) < 1e-5f) {
            "Expected second instruction track to stay with instruction index 1, got $secondEnd"
        }
    }

    private fun testStableTopologyFastPathFallsBackWhenAboveThreshold() {
        val tracker = FLineSideways(
            maxMatchDistance = 2f,
            stableTopologyDistanceThreshold = 0.5f,
            instructionIndexWeight = 0.0
        )

        tracker.update(listOf(polyline(0f, 1f)))
        tracker.update(listOf(polyline(0.9f, 0.1f)))

        val trajectories = tracker.trajectories()
        val firstTrack = findTrajectoryStartingNear(trajectories, 0f)
        val secondTrack = findTrajectoryStartingNear(trajectories, 1f)

        val firstEnd = firstTrack.nodes.last().to.x
        val secondEnd = secondTrack.nodes.last().to.x
        require(abs(firstEnd - 0.1f) < 1e-5f) {
            "Expected fallback matching to choose nearest point for first track, got $firstEnd"
        }
        require(abs(secondEnd - 0.9f) < 1e-5f) {
            "Expected fallback matching to choose nearest point for second track, got $secondEnd"
        }
    }

    private fun testDisappearAndReappearCreatesGap() {
        val tracker = FLineSideways(
            maxMatchDistance = 1f,
            maxMissedFrames = 3,
            breakOnGap = true
        )

        tracker.update(listOf(polyline(0f)))
        tracker.update(emptyList())
        tracker.update(listOf(polyline(0.2f)))

        val trajectories = tracker.trajectories()
        require(trajectories.size == 1) { "Expected one trajectory, got ${trajectories.size}" }

        val t = trajectories.first()
        require(t.nodes.size == 2) { "Expected 2 nodes in trajectory, got ${t.nodes.size}" }
        require(t.nodes[0] is FLine.MoveTo) { "Expected first node to be MoveTo" }
        require(t.nodes[1] is FLine.MoveTo) { "Expected second node to be MoveTo after gap" }
    }

    private fun testEffectiveInstructionColorIsCopied() {
        val tracker = FLineSideways(maxMatchDistance = 5f)
        val f = FLine()
        f.properties["color"] = vec(0.2, 0.3, 0.4, 1.0)
        f.moveTo(0f, 0f, 0f)
        f.here.properties["color"] = vec(1.0, 0.0, 0.0, 1.0)
        f.lineTo(1f, 0f, 0f)
        f.here.properties["color"] = vec(0.0, 1.0, 0.0, 1.0)

        tracker.update(listOf(f))

        val trajectories = tracker.trajectories()
        val redTrack = findTrajectoryStartingNear(trajectories, 0f)
        val greenTrack = findTrajectoryStartingNear(trajectories, 1f)

        val red = (redTrack.properties["color"] as? Vec4) ?: error("Missing color on red track")
        val green = (greenTrack.properties["color"] as? Vec4) ?: error("Missing color on green track")
        require(abs(red.x - 1f) < 1e-5f && abs(red.y - 0f) < 1e-5f && abs(red.z - 0f) < 1e-5f) {
            "Unexpected red track color: $red"
        }
        require(abs(green.x - 0f) < 1e-5f && abs(green.y - 1f) < 1e-5f && abs(green.z - 0f) < 1e-5f) {
            "Unexpected green track color: $green"
        }

        val redNodeColor = redTrack.nodes.first().properties["color"]
        val greenNodeColor = greenTrack.nodes.first().properties["color"]
        require(redNodeColor != null) { "Expected node-level color on red track" }
        require(greenNodeColor != null) { "Expected node-level color on green track" }
    }

    private fun polyline(vararg xs: Float, y: Float = 0f): FLine {
        require(xs.isNotEmpty()) { "Need at least one x coordinate" }
        val f = FLine().moveTo(xs.first(), y, 0f)
        for (i in 1 until xs.size) {
            f.lineTo(xs[i], y, 0f)
        }
        return f
    }

    private fun findTrajectoryStartingNear(trajectories: List<FLine>, x: Float, eps: Float = 1e-4f): FLine {
        return trajectories.firstOrNull { t ->
            t.nodes.isNotEmpty() && abs(t.nodes.first().to.x - x) <= eps
        } ?: error("No trajectory starts near x=$x")
    }
}