Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 71b7a4c7 authored by Mike Schneider's avatar Mike Schneider
Browse files

Add query-logic to the [MotionSpec] #MotionMechanics

Adding logic to the spec to query breakpoints and segments. This is going to be used in the next CL, in the [MotionValue] implementation.

Background on design and motivation in go/motionvalue-design

Flag: NONE Initial commits for new library, currently unused.
Test: atest mechanics_tests
Bug: 379248269
Change-Id: Ie03fa9fec0c8e9bb88e1e8e27c81ae1b1cb28ea9
parent 75d34657
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -25,7 +25,7 @@ package com.android.mechanics.spec
 * The directions are labelled [Min] and [Max] to reflect descending and ascending input values
 * respectively, but it does not imply an spatial direction.
 */
enum class InputDirection {
    Min,
    Max,
enum class InputDirection(val sign: Int) {
    Min(sign = -1),
    Max(sign = +1),
}
+76 −2
Original line number Diff line number Diff line
@@ -56,6 +56,63 @@ data class MotionSpec(
    val resetSpring: SpringParameters = DefaultResetSpring,
    val segmentHandlers: Map<SegmentKey, OnChangeSegmentHandler> = emptyMap(),
) {

    /** The [DirectionalMotionSpec] for the specified [direction]. */
    operator fun get(direction: InputDirection): DirectionalMotionSpec {
        return when (direction) {
            InputDirection.Min -> minDirection
            InputDirection.Max -> maxDirection
        }
    }

    /** Whether this spec contains a segment with the specified [segmentKey]. */
    fun containsSegment(segmentKey: SegmentKey): Boolean {
        return get(segmentKey.direction).findSegmentIndex(segmentKey) != -1
    }

    /**
     * The [SegmentData] for an input with the specified [position] and [direction].
     *
     * The returned [SegmentData] will be cached while [SegmentData.isValidForInput] returns `true`.
     */
    fun segmentAtInput(position: Float, direction: InputDirection): SegmentData {
        require(position.isFinite())

        return with(get(direction)) {
            var idx = findBreakpointIndex(position)
            if (direction == InputDirection.Min && breakpoints[idx].position == position) {
                // The segment starts at `position`. Since the breakpoints are sorted ascending, no
                // matter the spec's direction, need to return the previous segment in the min
                // direction.
                idx--
            }

            SegmentData(
                this@MotionSpec,
                breakpoints[idx],
                breakpoints[idx + 1],
                direction,
                mappings[idx],
            )
        }
    }

    /**
     * Looks up the new [SegmentData] once the [currentSegment] is not valid for an input with
     * [newPosition] and [newDirection].
     *
     * This will delegate to the [segmentHandlers], if registered for the [currentSegment]'s key.
     */
    internal fun onChangeSegment(
        currentSegment: SegmentData,
        newPosition: Float,
        newDirection: InputDirection,
    ): SegmentData {
        val segmentChangeHandler = segmentHandlers[currentSegment.key]
        return segmentChangeHandler?.invoke(this, currentSegment, newPosition, newDirection)
            ?: segmentAtInput(newPosition, newDirection)
    }

    companion object {
        /**
         * Default spring parameters for the reset spring. Matches the Fast Spatial spring of the
@@ -97,10 +154,11 @@ data class DirectionalMotionSpec(val breakpoints: List<Breakpoint>, val mappings
    /**
     * Returns the index of the closest breakpoint where `Breakpoint.position <= position`.
     *
     * Guaranteed to be a valid index into [breakpoints], and guaranteed not to be the last element.
     * Guaranteed to be a valid index into [breakpoints], and guaranteed to be neither the first nor
     * the last element.
     *
     * @param position the position in the input domain.
     * @return Index into [breakpoints], guaranteed to be in range `0..breakpoints.size - 2`
     * @return Index into [breakpoints], guaranteed to be in range `1..breakpoints.size - 2`
     */
    fun findBreakpointIndex(position: Float): Int {
        require(position.isFinite())
@@ -119,6 +177,22 @@ data class DirectionalMotionSpec(val breakpoints: List<Breakpoint>, val mappings
        return result
    }

    /**
     * The index of the breakpoint with the specified [breakpointKey], or `-1` if no such breakpoint
     * exists.
     */
    fun findBreakpointIndex(breakpointKey: BreakpointKey): Int {
        return breakpoints.indexOfFirst { it.key == breakpointKey }
    }

    /** Index into [mappings] for the specified [segmentKey], or `-1` if no such segment exists. */
    fun findSegmentIndex(segmentKey: SegmentKey): Int {
        val result = breakpoints.indexOfFirst { it.key == segmentKey.minBreakpoint }
        if (result < 0 || breakpoints[result + 1].key != segmentKey.maxBreakpoint) return -1

        return result
    }

    companion object {
        /* Empty spec, the full input domain is mapped to output using [Mapping.identity]. */
        val Empty =
+13 −0
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ data class SegmentKey(
 * respective breakpoint.
 */
data class SegmentData(
    val spec: MotionSpec,
    val minBreakpoint: Breakpoint,
    val maxBreakpoint: Breakpoint,
    val direction: InputDirection,
@@ -62,6 +63,18 @@ data class SegmentData(
            InputDirection.Min -> inputPosition > minBreakpoint.position
        }
    }

    /**
     * The breakpoint at the side of the segment's start.
     *
     * The [entryBreakpoint]'s [Guarantee] is the relevant guarantee for this segment.
     */
    val entryBreakpoint: Breakpoint
        get() =
            when (direction) {
                InputDirection.Max -> minBreakpoint
                InputDirection.Min -> maxBreakpoint
            }
}

/**
+177 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.mechanics.spec

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.mechanics.spring.SpringParameters
import com.google.common.truth.Truth.assertThat
import kotlin.math.nextDown
import kotlin.math.nextUp
import kotlin.test.assertFailsWith
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class DirectionalMotionSpecTest {

    @Test
    fun noBreakpoints_throws() {
        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(emptyList(), emptyList())
        }
    }

    @Test
    fun wrongSentinelBreakpoints_throws() {
        val breakpoint1 = Breakpoint(b1, position = 10f, spring, Guarantee.None)
        val breakpoint2 = Breakpoint(b2, position = 20f, spring, Guarantee.None)

        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(listOf(breakpoint1, breakpoint2), listOf(Mapping.Identity))
        }
    }

    @Test
    fun tooFewMappings_throws() {
        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(listOf(Breakpoint.minLimit, Breakpoint.maxLimit), emptyList())
        }
    }

    @Test
    fun tooManyMappings_throws() {
        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(
                listOf(Breakpoint.minLimit, Breakpoint.maxLimit),
                listOf(Mapping.One, Mapping.Two),
            )
        }
    }

    @Test
    fun breakpointsOutOfOrder_throws() {
        val breakpoint1 = Breakpoint(b1, position = 10f, spring, Guarantee.None)
        val breakpoint2 = Breakpoint(b2, position = 20f, spring, Guarantee.None)
        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(
                listOf(Breakpoint.minLimit, breakpoint2, breakpoint1, Breakpoint.maxLimit),
                listOf(Mapping.Zero, Mapping.One, Mapping.Two),
            )
        }
    }

    @Test
    fun findBreakpointIndex_returnsMinForEmptySpec() {
        val underTest = DirectionalMotionSpec.builder(spring).complete()

        assertThat(underTest.findBreakpointIndex(0f)).isEqualTo(0)
        assertThat(underTest.findBreakpointIndex(Float.MAX_VALUE)).isEqualTo(0)
        assertThat(underTest.findBreakpointIndex(-Float.MAX_VALUE)).isEqualTo(0)
    }

    @Test
    fun findBreakpointIndex_throwsForNonFiniteInput() {
        val underTest = DirectionalMotionSpec.builder(spring).complete()

        assertFailsWith<IllegalArgumentException> { underTest.findBreakpointIndex(Float.NaN) }
        assertFailsWith<IllegalArgumentException> {
            underTest.findBreakpointIndex(Float.NEGATIVE_INFINITY)
        }
        assertFailsWith<IllegalArgumentException> {
            underTest.findBreakpointIndex(Float.POSITIVE_INFINITY)
        }
    }

    @Test
    fun findBreakpointIndex_atBreakpoint_returnsIndex() {
        val underTest =
            DirectionalMotionSpec.builder(spring).toBreakpoint(10f).completeWith(Mapping.Identity)

        assertThat(underTest.findBreakpointIndex(10f)).isEqualTo(1)
    }

    @Test
    fun findBreakpointIndex_afterBreakpoint_returnsPreviousIndex() {
        val underTest =
            DirectionalMotionSpec.builder(spring).toBreakpoint(10f).completeWith(Mapping.Identity)

        assertThat(underTest.findBreakpointIndex(10f.nextUp())).isEqualTo(1)
    }

    @Test
    fun findBreakpointIndex_beforeBreakpoint_returnsIndex() {
        val underTest =
            DirectionalMotionSpec.builder(spring).toBreakpoint(10f).completeWith(Mapping.Identity)

        assertThat(underTest.findBreakpointIndex(10f.nextDown())).isEqualTo(0)
    }

    @Test
    fun findBreakpointIndexByKey_returnsIndex() {
        val underTest =
            DirectionalMotionSpec.builder(spring)
                .toBreakpoint(10f, key = b1)
                .completeWith(Mapping.Identity)

        assertThat(underTest.findBreakpointIndex(b1)).isEqualTo(1)
    }

    @Test
    fun findBreakpointIndexByKey_unknown_returnsMinusOne() {
        val underTest =
            DirectionalMotionSpec.builder(spring)
                .toBreakpoint(10f, key = b1)
                .completeWith(Mapping.Identity)

        assertThat(underTest.findBreakpointIndex(b2)).isEqualTo(-1)
    }

    @Test
    fun findSegmentIndex_returnsIndexForSegment_ignoringDirection() {
        val underTest =
            DirectionalMotionSpec.builder(spring)
                .toBreakpoint(10f, key = b1)
                .continueWith(Mapping.One)
                .toBreakpoint(20f, key = b2)
                .completeWith(Mapping.Identity)

        assertThat(underTest.findSegmentIndex(SegmentKey(b1, b2, InputDirection.Max))).isEqualTo(1)
        assertThat(underTest.findSegmentIndex(SegmentKey(b1, b2, InputDirection.Min))).isEqualTo(1)
    }

    @Test
    fun findSegmentIndex_forInvalidKeys_returnsMinusOne() {
        val underTest =
            DirectionalMotionSpec.builder(spring)
                .toBreakpoint(10f, key = b1)
                .continueWith(Mapping.One)
                .toBreakpoint(20f, key = b2)
                .continueWith(Mapping.One)
                .toBreakpoint(30f, key = b3)
                .completeWith(Mapping.Identity)

        assertThat(underTest.findSegmentIndex(SegmentKey(b2, b1, InputDirection.Max))).isEqualTo(-1)
        assertThat(underTest.findSegmentIndex(SegmentKey(b1, b3, InputDirection.Max))).isEqualTo(-1)
    }

    companion object {
        val b1 = BreakpointKey("one")
        val b2 = BreakpointKey("two")
        val b3 = BreakpointKey("three")
        val spring = SpringParameters(stiffness = 100f, dampingRatio = 1f)
    }
}
+182 −58
Original line number Diff line number Diff line
@@ -18,10 +18,8 @@ package com.android.mechanics.spec

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.mechanics.spring.SpringParameters
import com.android.mechanics.testing.BreakpointSubject.Companion.assertThat
import com.google.common.truth.Truth.assertThat
import kotlin.math.nextDown
import kotlin.math.nextUp
import kotlin.test.assertFailsWith
import org.junit.Test
import org.junit.runner.RunWith

@@ -29,100 +27,226 @@ import org.junit.runner.RunWith
class MotionSpecTest {

    @Test
    fun directionalMotionSpec_noBreakpoints_throws() {
        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(emptyList(), emptyList())
        }
    fun containsSegment_unknownSegment_returnsFalse() {
        val underTest = MotionSpec.builder(spring).complete()
        assertThat(underTest.containsSegment(SegmentKey(b1, b2, InputDirection.Max))).isFalse()
    }

    @Test
    fun directionalMotionSpec_wrongSentinelBreakpoints_throws() {
        val breakpoint1 = Breakpoint(b1, position = 10f, spring, Guarantee.None)
        val breakpoint2 = Breakpoint(b2, position = 20f, spring, Guarantee.None)
    fun containsSegment_symmetricSpec_knownSegment_returnsTrue() {
        val underTest =
            MotionSpec.builder(spring)
                .toBreakpoint(10f, key = b1)
                .continueWith(Mapping.One)
                .toBreakpoint(20f, key = b2)
                .completeWith(Mapping.Identity)

        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(listOf(breakpoint1, breakpoint2), listOf(Mapping.Identity))
        }
        assertThat(underTest.containsSegment(SegmentKey(b1, b2, InputDirection.Max))).isTrue()
        assertThat(underTest.containsSegment(SegmentKey(b1, b2, InputDirection.Min))).isTrue()
    }

    @Test
    fun directionalMotionSpec_tooFewMappings_throws() {
        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(listOf(Breakpoint.minLimit, Breakpoint.maxLimit), emptyList())
        }
    fun containsSegment_asymmetricSpec_knownMaxDirectionSegment_trueOnlyInMaxDirection() {
        val forward =
            DirectionalMotionSpec.builder(spring)
                .toBreakpoint(10f, key = b1)
                .continueWith(Mapping.One)
                .toBreakpoint(20f, key = b2)
                .completeWith(Mapping.Identity)
        val reverse = DirectionalMotionSpec.builder(spring).complete()

        val underTest = MotionSpec(forward, reverse)

        assertThat(underTest.containsSegment(SegmentKey(b1, b2, InputDirection.Max))).isTrue()
        assertThat(underTest.containsSegment(SegmentKey(b1, b2, InputDirection.Min))).isFalse()
    }

    @Test
    fun directionalMotionSpec_tooManyMappings_throws() {
        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(
                listOf(Breakpoint.minLimit, Breakpoint.maxLimit),
                listOf(Mapping.One, Mapping.Two),
            )
        }
    fun containsSegment_asymmetricSpec_knownMinDirectionSegment_trueOnlyInMinDirection() {
        val forward = DirectionalMotionSpec.builder(spring).complete()
        val reverse =
            DirectionalMotionSpec.builder(spring)
                .toBreakpoint(10f, key = b1)
                .continueWith(Mapping.One)
                .toBreakpoint(20f, key = b2)
                .completeWith(Mapping.Identity)

        val underTest = MotionSpec(forward, reverse)

        assertThat(underTest.containsSegment(SegmentKey(b1, b2, InputDirection.Max))).isFalse()
        assertThat(underTest.containsSegment(SegmentKey(b1, b2, InputDirection.Min))).isTrue()
    }

    @Test
    fun directionalMotionSpec_breakpointsOutOfOrder_throws() {
        val breakpoint1 = Breakpoint(b1, position = 10f, spring, Guarantee.None)
        val breakpoint2 = Breakpoint(b2, position = 20f, spring, Guarantee.None)
        assertFailsWith<IllegalArgumentException> {
            DirectionalMotionSpec(
                listOf(Breakpoint.minLimit, breakpoint2, breakpoint1, Breakpoint.maxLimit),
                listOf(Mapping.Zero, Mapping.One, Mapping.Two),
            )
    fun segmentAtInput_emptySpec_maxDirection_segmentDataIsCorrect() {
        val underTest = MotionSpec.builder(spring).complete()

        val segmentAtInput = underTest.segmentAtInput(0f, InputDirection.Max)

        assertThat(segmentAtInput.spec).isSameInstanceAs(underTest)
        assertThat(segmentAtInput.minBreakpoint).isSameInstanceAs(Breakpoint.minLimit)
        assertThat(segmentAtInput.maxBreakpoint).isSameInstanceAs(Breakpoint.maxLimit)
        assertThat(segmentAtInput.direction).isEqualTo(InputDirection.Max)
        assertThat(segmentAtInput.mapping).isEqualTo(Mapping.Identity)
    }

    @Test
    fun segmentAtInput_emptySpec_minDirection_segmentDataIsCorrect() {
        val underTest = MotionSpec.builder(spring).complete()

        val segmentAtInput = underTest.segmentAtInput(0f, InputDirection.Min)

        assertThat(segmentAtInput.spec).isSameInstanceAs(underTest)
        assertThat(segmentAtInput.minBreakpoint).isSameInstanceAs(Breakpoint.minLimit)
        assertThat(segmentAtInput.maxBreakpoint).isSameInstanceAs(Breakpoint.maxLimit)
        assertThat(segmentAtInput.direction).isEqualTo(InputDirection.Min)
        assertThat(segmentAtInput.mapping).isEqualTo(Mapping.Identity)
    }

    @Test
    fun directionalMotionSpec_findBreakpointIndex_returnsMinForEmptySpec() {
        val underTest = DirectionalMotionSpec.builder(spring).complete()
    fun segmentAtInput_atBreakpointPosition() {
        val underTest =
            MotionSpec.builder(spring)
                .toBreakpoint(10f, key = b1)
                .continueWith(Mapping.One)
                .toBreakpoint(20f, key = b2)
                .completeWith(Mapping.Identity)

        val segmentAtInput = underTest.segmentAtInput(10f, InputDirection.Max)

        assertThat(underTest.findBreakpointIndex(0f)).isEqualTo(0)
        assertThat(underTest.findBreakpointIndex(Float.MAX_VALUE)).isEqualTo(0)
        assertThat(underTest.findBreakpointIndex(-Float.MAX_VALUE)).isEqualTo(0)
        assertThat(segmentAtInput.key).isEqualTo(SegmentKey(b1, b2, InputDirection.Max))
        assertThat(segmentAtInput.minBreakpoint).isAt(10f)
        assertThat(segmentAtInput.maxBreakpoint).isAt(20f)
        assertThat(segmentAtInput.mapping).isEqualTo(Mapping.One)
    }

    @Test
    fun directionalMotionSpec_findBreakpointIndex_throwsForNonFiniteInput() {
        val underTest = DirectionalMotionSpec.builder(spring).complete()
    fun segmentAtInput_reverse_atBreakpointPosition() {
        val underTest =
            MotionSpec.builder(spring)
                .toBreakpoint(10f, key = b1)
                .continueWith(Mapping.One)
                .toBreakpoint(20f, key = b2)
                .completeWith(Mapping.Identity)

        assertFailsWith<IllegalArgumentException> { underTest.findBreakpointIndex(Float.NaN) }
        assertFailsWith<IllegalArgumentException> {
            underTest.findBreakpointIndex(Float.NEGATIVE_INFINITY)
        val segmentAtInput = underTest.segmentAtInput(20f, InputDirection.Min)

        assertThat(segmentAtInput.key).isEqualTo(SegmentKey(b1, b2, InputDirection.Min))
        assertThat(segmentAtInput.minBreakpoint).isAt(10f)
        assertThat(segmentAtInput.maxBreakpoint).isAt(20f)
        assertThat(segmentAtInput.mapping).isEqualTo(Mapping.One)
    }
        assertFailsWith<IllegalArgumentException> {
            underTest.findBreakpointIndex(Float.POSITIVE_INFINITY)

    @Test
    fun containsSegment_asymmetricSpec_readsFromIndicatedDirection() {
        val forward =
            DirectionalMotionSpec.builder(spring)
                .toBreakpoint(10f, key = b1)
                .continueWith(Mapping.One)
                .toBreakpoint(20f, key = b2)
                .completeWith(Mapping.Identity)
        val reverse =
            DirectionalMotionSpec.builder(spring)
                .toBreakpoint(5f, key = b1)
                .continueWith(Mapping.Two)
                .toBreakpoint(25f, key = b2)
                .completeWith(Mapping.Identity)

        val underTest = MotionSpec(forward, reverse)

        val segmentAtInputMax = underTest.segmentAtInput(15f, InputDirection.Max)
        assertThat(segmentAtInputMax.key).isEqualTo(SegmentKey(b1, b2, InputDirection.Max))
        assertThat(segmentAtInputMax.minBreakpoint).isAt(10f)
        assertThat(segmentAtInputMax.maxBreakpoint).isAt(20f)
        assertThat(segmentAtInputMax.mapping).isEqualTo(Mapping.One)

        val segmentAtInputMin = underTest.segmentAtInput(15f, InputDirection.Min)
        assertThat(segmentAtInputMin.key).isEqualTo(SegmentKey(b1, b2, InputDirection.Min))
        assertThat(segmentAtInputMin.minBreakpoint).isAt(5f)
        assertThat(segmentAtInputMin.maxBreakpoint).isAt(25f)
        assertThat(segmentAtInputMin.mapping).isEqualTo(Mapping.Two)
    }

    @Test
    fun onSegmentChanged_noHandler_returnsEqualSegmentForSameInput() {
        val underTest =
            MotionSpec.builder(spring)
                .toBreakpoint(10f, key = b1)
                .continueWith(Mapping.One)
                .toBreakpoint(20f, key = b2)
                .completeWith(Mapping.Identity)

        val segmentAtInput = underTest.segmentAtInput(15f, InputDirection.Max)
        val onChangedResult = underTest.onChangeSegment(segmentAtInput, 15f, InputDirection.Max)
        assertThat(segmentAtInput).isEqualTo(onChangedResult)
    }

    @Test
    fun directionalMotionSpec_findBreakpointIndex_atBreakpoint_returnsIndex() {
    fun onSegmentChanged_noHandler_returnsNewSegmentForNewInput() {
        val underTest =
            DirectionalMotionSpec.builder(spring).toBreakpoint(10f).completeWith(Mapping.Identity)
            MotionSpec.builder(spring)
                .toBreakpoint(10f, key = b1)
                .continueWith(Mapping.One)
                .toBreakpoint(20f, key = b2)
                .completeWith(Mapping.Identity)

        val segmentAtInput = underTest.segmentAtInput(15f, InputDirection.Max)
        val onChangedResult = underTest.onChangeSegment(segmentAtInput, 15f, InputDirection.Min)
        assertThat(segmentAtInput).isNotEqualTo(onChangedResult)

        assertThat(underTest.findBreakpointIndex(10f)).isEqualTo(1)
        assertThat(onChangedResult.key).isEqualTo(SegmentKey(b1, b2, InputDirection.Min))
    }

    @Test
    fun directionalMotionSpec_findBreakpointIndex_afterBreakpoint_returnsPreviousIndex() {
    fun onSegmentChanged_withHandlerReturningNull_returnsSegmentAtInput() {
        val underTest =
            DirectionalMotionSpec.builder(spring).toBreakpoint(10f).completeWith(Mapping.Identity)
            MotionSpec.builder(spring)
                .toBreakpoint(10f, key = b1)
                .continueWith(Mapping.One)
                .toBreakpoint(20f, key = b2)
                .completeWith(Mapping.Identity)
                .copy(
                    segmentHandlers =
                        mapOf(SegmentKey(b1, b2, InputDirection.Max) to { _, _, _ -> null })
                )

        val segmentAtInput = underTest.segmentAtInput(15f, InputDirection.Max)
        val onChangedResult = underTest.onChangeSegment(segmentAtInput, 15f, InputDirection.Min)

        assertThat(underTest.findBreakpointIndex(10f.nextUp())).isEqualTo(1)
        assertThat(segmentAtInput).isNotEqualTo(onChangedResult)
        assertThat(onChangedResult.key).isEqualTo(SegmentKey(b1, b2, InputDirection.Min))
    }

    @Test
    fun directionalMotionSpec_findBreakpointIndex_beforeBreakpoint_returnsIndex() {
    fun onSegmentChanged_withHandlerReturningSegment_returnsHandlerResult() {
        val underTest =
            DirectionalMotionSpec.builder(spring).toBreakpoint(10f).completeWith(Mapping.Identity)
            MotionSpec.builder(spring)
                .toBreakpoint(10f, key = b1)
                .continueWith(Mapping.One)
                .toBreakpoint(20f, key = b2)
                .completeWith(Mapping.Identity)
                .copy(
                    segmentHandlers =
                        mapOf(
                            SegmentKey(b1, b2, InputDirection.Max) to
                                { _, _, _ ->
                                    segmentAtInput(0f, InputDirection.Min)
                                }
                        )
                )

        val segmentAtInput = underTest.segmentAtInput(15f, InputDirection.Max)
        val onChangedResult = underTest.onChangeSegment(segmentAtInput, 15f, InputDirection.Min)

        assertThat(underTest.findBreakpointIndex(10f.nextDown())).isEqualTo(0)
        assertThat(segmentAtInput).isNotEqualTo(onChangedResult)
        assertThat(onChangedResult.key)
            .isEqualTo(SegmentKey(Breakpoint.minLimit.key, b1, InputDirection.Min))
    }

    companion object {
        val b1 = BreakpointKey("one")
        val b2 = BreakpointKey("two")
        val b3 = BreakpointKey("three")
        val spring = SpringParameters(stiffness = 100f, dampingRatio = 1f)
    }
}
Loading