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

Commit 26b57af6 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add utility to print debug version of a motion spec" into main

parents a07742d1 10145c11
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -126,6 +126,8 @@ data class MotionSpec(
            ?: segmentAtInput(newPosition, newDirection)
    }

    override fun toString() = toDebugString()

    companion object {
        /**
         * Default spring parameters for the reset spring. Matches the Fast Spatial spring of the
@@ -224,6 +226,8 @@ data class DirectionalMotionSpec(
        return result
    }

    override fun toString() = toDebugString()

    companion object {
        /* Empty spec, the full input domain is mapped to output using [Mapping.identity]. */
        val Empty =
+121 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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

/** Returns a string representation of the [MotionSpec] for debugging by humans. */
fun MotionSpec.toDebugString(): String {
    return buildString {
            if (minDirection == maxDirection) {
                appendLine("unidirectional:")
                appendLine(minDirection.toDebugString().prependIndent("  "))
            } else {
                appendLine("maxDirection:")
                appendLine(maxDirection.toDebugString().prependIndent("  "))
                appendLine("minDirection:")
                appendLine(minDirection.toDebugString().prependIndent("  "))
            }

            if (segmentHandlers.isNotEmpty()) {
                appendLine("segmentHandlers:")
                segmentHandlers.keys.forEach {
                    appendIndent(2)
                    appendSegmentKey(it)
                    appendLine()
                }
            }
        }
        .trim()
}

/** Returns a string representation of the [DirectionalMotionSpec] for debugging by humans. */
fun DirectionalMotionSpec.toDebugString(): String {
    return buildString {
            appendBreakpointLine(breakpoints.first())
            for (i in mappings.indices) {
                appendMappingLine(mappings[i], indent = 2)
                semantics.forEach { appendSemanticsLine(it.key, it.values[i], indent = 4) }
                appendBreakpointLine(breakpoints[i + 1])
            }
        }
        .trim()
}

private fun StringBuilder.appendIndent(indent: Int) {
    repeat(indent) { append(' ') }
}

private fun StringBuilder.appendBreakpointLine(breakpoint: Breakpoint, indent: Int = 0) {
    appendIndent(indent)
    append("@")
    append(breakpoint.position)

    append(" [")
    appendBreakpointKey(breakpoint.key)
    append("]")

    if (breakpoint.guarantee != Guarantee.None) {
        append(" guarantee=")
        append(breakpoint.key.debugLabel)
    }

    if (!breakpoint.spring.isSnapSpring) {
        append(" spring=")
        append(breakpoint.spring.stiffness)
        append("/")
        append(breakpoint.spring.dampingRatio)
    }

    appendLine()
}

private fun StringBuilder.appendBreakpointKey(key: BreakpointKey) {
    if (key.debugLabel != null) {
        append(key.debugLabel)
        append("|")
    }
    append("id:0x")
    append(System.identityHashCode(key.identity).toString(16).padStart(8, '0'))
}

private fun StringBuilder.appendSegmentKey(key: SegmentKey) {
    appendBreakpointKey(key.minBreakpoint)
    if (key.direction == InputDirection.Min) append(" << ") else append(" >> ")
    appendBreakpointKey(key.maxBreakpoint)
}

private fun StringBuilder.appendMappingLine(mapping: Mapping, indent: Int = 0) {
    appendIndent(indent)
    append(mapping.toString())
    appendLine()
}

private fun StringBuilder.appendSemanticsLine(
    semanticKey: SemanticKey<*>,
    value: Any?,
    indent: Int = 0,
) {
    appendIndent(indent)

    append(semanticKey.debugLabel)
    append("[id:0x")
    append(System.identityHashCode(semanticKey.identity).toString(16).padStart(8, '0'))
    append("]")

    append("=")
    append(value)
    appendLine()
}
+145 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.spec.ChangeSegmentHandlers.PreventDirectionChangeWithinCurrentSegment
import com.android.mechanics.spec.builder.MotionBuilderContext
import com.android.mechanics.spec.builder.effectsDirectionalMotionSpec
import com.android.mechanics.spec.builder.spatialDirectionalMotionSpec
import com.android.mechanics.testing.FakeMotionSpecBuilderContext
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MotionSpecDebugFormatterTest : MotionBuilderContext by FakeMotionSpecBuilderContext.Default {

    @Test
    fun motionSpec_unidirectionalSpec_formatIsUseful() {
        val spec = MotionSpec(effectsDirectionalMotionSpec { fixedValue(0f, value = 1f) })

        assertThat(formatForTest(spec.toDebugString()))
            .isEqualTo(
                """
unidirectional:
  @-Infinity [built-in::min|id:0x1234cdef]
    Fixed(value=0.0)
  @0.0 [id:0x1234cdef] spring=1600.0/1.0
    Fixed(value=1.0)
  @Infinity [built-in::max|id:0x1234cdef]"""
                    .trimIndent()
            )
    }

    @Test
    fun motionSpec_bidirectionalSpec_formatIsUseful() {
        val spec =
            MotionSpec(
                spatialDirectionalMotionSpec(Mapping.Zero) { fixedValue(0f, value = 1f) },
                spatialDirectionalMotionSpec(Mapping.One) { fixedValue(0f, value = 0f) },
            )

        assertThat(formatForTest(spec.toDebugString()))
            .isEqualTo(
                """
maxDirection:
  @-Infinity [built-in::min|id:0x1234cdef]
    Fixed(value=0.0)
  @0.0 [id:0x1234cdef] spring=700.0/0.9
    Fixed(value=1.0)
  @Infinity [built-in::max|id:0x1234cdef]
minDirection:
  @-Infinity [built-in::min|id:0x1234cdef]
    Fixed(value=1.0)
  @0.0 [id:0x1234cdef] spring=700.0/0.9
    Fixed(value=0.0)
  @Infinity [built-in::max|id:0x1234cdef]"""
                    .trimIndent()
            )
    }

    @Test
    fun motionSpec_semantics_formatIsUseful() {
        val semanticKey = SemanticKey<Float>("foo")

        val spec =
            MotionSpec(
                effectsDirectionalMotionSpec(semantics = listOf(semanticKey with 42f)) {
                    fixedValue(0f, value = 1f, semantics = listOf(semanticKey with 43f))
                }
            )

        assertThat(formatForTest(spec.toDebugString()))
            .isEqualTo(
                """
unidirectional:
  @-Infinity [built-in::min|id:0x1234cdef]
    Fixed(value=0.0)
      foo[id:0x1234cdef]=42.0
  @0.0 [id:0x1234cdef] spring=1600.0/1.0
    Fixed(value=1.0)
      foo[id:0x1234cdef]=43.0
  @Infinity [built-in::max|id:0x1234cdef]"""
                    .trimIndent()
            )
    }

    @Test
    fun motionSpec_segmentHandlers_formatIsUseful() {
        val key1 = BreakpointKey("1")
        val key2 = BreakpointKey("2")
        val spec =
            MotionSpec(
                effectsDirectionalMotionSpec {
                    fixedValue(0f, value = 1f, key = key1)
                    fixedValue(2f, value = 2f, key = key1)
                },
                segmentHandlers =
                    mapOf(
                        SegmentKey(key1, key2, InputDirection.Max) to
                            PreventDirectionChangeWithinCurrentSegment,
                        SegmentKey(key1, key2, InputDirection.Min) to
                            PreventDirectionChangeWithinCurrentSegment,
                    ),
            )

        assertThat(formatForTest(spec.toDebugString()))
            .isEqualTo(
                """
unidirectional:
  @-Infinity [built-in::min|id:0x1234cdef]
    Fixed(value=0.0)
  @0.0 [1|id:0x1234cdef] spring=1600.0/1.0
    Fixed(value=1.0)
  @2.0 [1|id:0x1234cdef] spring=1600.0/1.0
    Fixed(value=2.0)
  @Infinity [built-in::max|id:0x1234cdef]
segmentHandlers:
  1|id:0x1234cdef >> 2|id:0x1234cdef
  1|id:0x1234cdef << 2|id:0x1234cdef"""
                    .trimIndent()
            )
    }

    companion object {
        private val idMatcher = Regex("id:0x[0-9a-f]{8}")

        fun formatForTest(debugString: String) =
            debugString.replace(idMatcher, "id:0x1234cdef").trim()
    }
}